mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-01-19 18:14:44 +00:00
Create PsAdmin framework
PsAdmin uses a dedicated TCP port to allow for remote queries and command to be sent to the running World/Login server. Commands are a single command followed by zero or more arguments. Commands may require access to the ActorSystem, so they will get their own dedicated actors to be able to handle the different messages required that can be sent in response to a query. The return line is in JSON to allow for easy parsing by applications, such as web servers. An interactive client is easy as being able to parse json and buffer command input. Some basic commands are implemented for now: * shutdown - kills the actor system * list_players - gets a list of players on the interstellar cluster * dump_config - get the running config * thread_dump - dumps all thread backtraces (useful for prod debugging) More advanced commands like kick/ban will require additional testing.
This commit is contained in:
parent
ceb58ed39a
commit
82e8840176
|
|
@ -46,9 +46,9 @@ lazy val commonSettings = Seq(
|
|||
"org.ini4j" % "ini4j" % "0.5.4",
|
||||
"org.scala-graph" %% "graph-core" % "1.12.5",
|
||||
"io.kamon" %% "kamon-bundle" % "2.1.0",
|
||||
"io.kamon" %% "kamon-apm-reporter" % "2.1.0"
|
||||
"io.kamon" %% "kamon-apm-reporter" % "2.1.0",
|
||||
"org.json4s" %% "json4s-native" % "3.6.8",
|
||||
),
|
||||
classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
|
||||
)
|
||||
|
||||
lazy val pscryptoSettings = Seq(
|
||||
|
|
|
|||
|
|
@ -190,6 +190,10 @@ trait ConfigParser {
|
|||
config_map = map
|
||||
}
|
||||
|
||||
def GetRawConfig : Map[String, Any] = {
|
||||
config_map
|
||||
}
|
||||
|
||||
def FormatErrors(invalidResult : Invalid) : Seq[String] = {
|
||||
var count = 0;
|
||||
|
||||
|
|
@ -209,7 +213,7 @@ trait ConfigParser {
|
|||
}
|
||||
|
||||
protected def parseSection(sectionIni : org.ini4j.Profile.Section, entry : ConfigEntry, map : Map[String, Any]) : ValidationResult = {
|
||||
var rawValue = sectionIni.get(entry.key)
|
||||
var rawValue = sectionIni.get(entry.key, 0)
|
||||
val full_key : String = sectionIni.getName + "." + entry.key
|
||||
|
||||
val value = if (rawValue == null) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,16 @@ class InterstellarCluster(zones : List[Zone]) extends Actor {
|
|||
case None => //zone_number does not exist
|
||||
sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None)
|
||||
}
|
||||
case InterstellarCluster.ListPlayers() =>
|
||||
var players : List[String] = List()
|
||||
|
||||
for(zone <- zones) {
|
||||
val zonePlayers = zone.Players
|
||||
for (player <- zonePlayers) {
|
||||
players ::= player.name
|
||||
}
|
||||
}
|
||||
sender ! InterstellarCluster.PlayerList(players)
|
||||
|
||||
case msg @ Zone.Lattice.RequestSpecificSpawnPoint(zone_number, _, _, _) =>
|
||||
recursiveFindWorldInCluster(zones.iterator, _.Number == zone_number) match {
|
||||
|
|
@ -188,6 +198,9 @@ object InterstellarCluster {
|
|||
*/
|
||||
final case class GiveWorld(zoneId : String, zone : Zone)
|
||||
|
||||
final case class ListPlayers()
|
||||
final case class PlayerList(players : List[String])
|
||||
|
||||
/**
|
||||
* Signal to the cluster that a new client needs to be initialized for all listed `Zone` destinations.
|
||||
* @see `Zone`
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ ServerType = Released
|
|||
# Important: Must be different from the worldserver.ListeningPort. Ports below 1024 are
|
||||
# privileged on Linux and may require root.
|
||||
# Range: [1, 65535] - (UDP port 1, UDP port 65535)
|
||||
# Default: 51000 - (Listen on UDP port 5100)
|
||||
# Default: 51000 - (Listen on UDP port 51000)
|
||||
|
||||
ListeningPort = 51000
|
||||
|
||||
|
|
@ -134,6 +134,22 @@ ListeningPort = 51000
|
|||
|
||||
CreateMissingAccounts = yes
|
||||
|
||||
###################################################################################################
|
||||
# PSADMIN SETTINGS
|
||||
###################################################################################################
|
||||
|
||||
[psadmin]
|
||||
|
||||
# ListeningPort (int)
|
||||
# Description: The TCP listening port for the server admin interface.
|
||||
# Important: Must be different from the worldserver and loginserver ListeningPort.
|
||||
# Ports below 1024 are privileged on Linux and may require root.
|
||||
# NEVER EXPOSE THIS PORT TO THE INTERNET! CHECK YOUR FIREWALL CONFIG.
|
||||
# Range: [1, 65535] - (TCP port 1, TCP port 65535)
|
||||
# Default: 51002 - (Listen on TCP port 51002)
|
||||
|
||||
ListeningPort = 51002
|
||||
|
||||
###################################################################################################
|
||||
# NETWORK SETTINGS
|
||||
###################################################################################################
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
import net.psforever.WorldConfig
|
||||
import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
|
||||
import com.github.mauricio.async.db.{Configuration, QueryResult, RowData, SSLConfiguration}
|
||||
import scala.util.{Try,Success,Failure}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.github.mauricio.async.db.{Connection, QueryResult}
|
|||
import net.psforever.objects.Account
|
||||
import net.psforever.objects.DefaultCancellable
|
||||
import net.psforever.types.PlanetSideEmpire
|
||||
import net.psforever.WorldConfig
|
||||
import services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import net.psforever.config.{Invalid, Valid}
|
|||
import net.psforever.crypto.CryptoInterface
|
||||
import net.psforever.objects.zones._
|
||||
import net.psforever.objects.guid.TaskResolver
|
||||
import net.psforever.psadmin.PsAdminActor
|
||||
import net.psforever.WorldConfig
|
||||
import org.slf4j
|
||||
import org.fusesource.jansi.Ansi._
|
||||
import org.fusesource.jansi.Ansi.Color._
|
||||
|
|
@ -260,6 +262,7 @@ object PsLogin {
|
|||
|
||||
val loginServerPort = WorldConfig.Get[Int]("loginserver.ListeningPort")
|
||||
val worldServerPort = WorldConfig.Get[Int]("worldserver.ListeningPort")
|
||||
val psAdminPort = WorldConfig.Get[Int]("psadmin.ListeningPort")
|
||||
|
||||
val netSim : Option[NetworkSimulatorParameters] = WorldConfig.Get[Boolean]("developer.NetSim.Active") match {
|
||||
case true =>
|
||||
|
|
@ -295,6 +298,8 @@ object PsLogin {
|
|||
loginListener = system.actorOf(Props(new UdpListener(loginRouter, "login-session-router", LoginConfig.serverIpAddress, loginServerPort, netSim)), "login-udp-endpoint")
|
||||
worldListener = system.actorOf(Props(new UdpListener(worldRouter, "world-session-router", LoginConfig.serverIpAddress, worldServerPort, netSim)), "world-udp-endpoint")
|
||||
|
||||
val adminListener = system.actorOf(Props(new TcpListener(classOf[PsAdminActor], "psadmin-client-", InetAddress.getLoopbackAddress, psAdminPort)), "psadmin-tcp-endpoint")
|
||||
|
||||
logger.info(s"NOTE: Set client.ini to point to ${LoginConfig.serverIpAddress.getHostAddress}:$loginServerPort")
|
||||
|
||||
// Add our shutdown hook (this works for Control+C as well, but not in Cygwin)
|
||||
|
|
@ -321,8 +326,5 @@ object PsLogin {
|
|||
Locale.setDefault(Locale.US); // to have floats with dots, not comma...
|
||||
this.args = args
|
||||
run()
|
||||
|
||||
// Wait forever until the actor system shuts down
|
||||
Await.result(system.whenTerminated, Duration.Inf)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import scala.collection.mutable
|
|||
import akka.actor.SupervisorStrategy.Stop
|
||||
import net.psforever.packet.PacketCoding
|
||||
import net.psforever.packet.control.ConnectionClose
|
||||
import net.psforever.WorldConfig
|
||||
import services.ServiceManager
|
||||
import services.ServiceManager.Lookup
|
||||
import services.account.{IPAddress, StoreIPAddress}
|
||||
|
|
|
|||
53
pslogin/src/main/scala/TcpListener.scala
Normal file
53
pslogin/src/main/scala/TcpListener.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
import java.net.{InetAddress, InetSocketAddress}
|
||||
|
||||
import akka.actor.SupervisorStrategy.Stop
|
||||
import akka.actor.{Actor, ActorRef, OneForOneStrategy, Props, Terminated}
|
||||
import akka.io._
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
import akka.util.ByteString
|
||||
|
||||
class TcpListener[T <: Actor](actorClass : Class[T],
|
||||
nextActorName : String,
|
||||
listenAddress : InetAddress,
|
||||
port : Int) extends Actor {
|
||||
private val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def supervisorStrategy = OneForOneStrategy() {
|
||||
case _ => Stop
|
||||
}
|
||||
|
||||
import context.system
|
||||
|
||||
IO(Tcp) ! Tcp.Bind(self, new InetSocketAddress(listenAddress, port))
|
||||
|
||||
var sessionId = 0L
|
||||
var bytesRecevied = 0L
|
||||
var bytesSent = 0L
|
||||
var nextActor : ActorRef = Actor.noSender
|
||||
|
||||
def receive = {
|
||||
case Tcp.Bound(local) =>
|
||||
log.info(s"Now listening on TCP:$local")
|
||||
|
||||
context.become(ready(sender()))
|
||||
case Tcp.CommandFailed(Tcp.Bind(_, address, _, _, _)) =>
|
||||
log.error("Failed to bind to the network interface: " + address)
|
||||
context.system.terminate()
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
def ready(socket: ActorRef): Receive = {
|
||||
case Tcp.Connected(remote, local) =>
|
||||
val connection = sender()
|
||||
val session = sessionId
|
||||
val handler = context.actorOf(Props(actorClass, remote, connection), nextActorName + session)
|
||||
connection ! Tcp.Register(handler)
|
||||
sessionId += 1
|
||||
case Tcp.Unbind => socket ! Tcp.Unbind
|
||||
case Tcp.Unbound => context.stop(self)
|
||||
case default => log.error(s"Unhandled message: $default")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
package net.psforever
|
||||
|
||||
import scala.util.matching.Regex
|
||||
import net.psforever.config._
|
||||
import scala.concurrent.duration._
|
||||
|
|
@ -31,6 +33,9 @@ object WorldConfig extends ConfigParser {
|
|||
ConfigEntryTime("Session.InboundGraceTime", 1 minute, Constraints.min(10 seconds)),
|
||||
ConfigEntryTime("Session.OutboundGraceTime", 1 minute, Constraints.min(10 seconds))
|
||||
),
|
||||
ConfigSection("psadmin",
|
||||
ConfigEntryInt("ListeningPort", 51002, Constraints.min(1), Constraints.max(65535))
|
||||
),
|
||||
ConfigSection("developer",
|
||||
ConfigEntryBool ("NetSim.Active", false),
|
||||
ConfigEntryFloat("NetSim.Loss", 0.02f, Constraints.min(0.0f), Constraints.max(1.0f)),
|
||||
|
|
|
|||
30
pslogin/src/main/scala/psadmin/CmdInternal.scala
Normal file
30
pslogin/src/main/scala/psadmin/CmdInternal.scala
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package net.psforever.psadmin
|
||||
|
||||
import net.psforever.WorldConfig
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
object CmdInternal {
|
||||
|
||||
def cmdDumpConfig(args : Array[String]) = {
|
||||
val config = WorldConfig.GetRawConfig
|
||||
|
||||
CommandGoodResponse(s"Dump of WorldConfig", config)
|
||||
}
|
||||
|
||||
def cmdThreadDump(args : Array[String]) = {
|
||||
import scala.collection.JavaConverters._
|
||||
|
||||
var data = Map[String,Any]()
|
||||
val traces = Thread.getAllStackTraces().asScala
|
||||
var traces_fmt = List[String]()
|
||||
|
||||
for ((thread, trace) <- traces) {
|
||||
val info = s"Thread ${thread.getId} - ${thread.getName}\n"
|
||||
traces_fmt = traces_fmt ++ List(info + trace.mkString("\n"))
|
||||
}
|
||||
|
||||
data{"trace"} = traces_fmt
|
||||
|
||||
CommandGoodResponse(s"Dump of ${traces.size} threads", data)
|
||||
}
|
||||
}
|
||||
41
pslogin/src/main/scala/psadmin/CmdListPlayers.scala
Normal file
41
pslogin/src/main/scala/psadmin/CmdListPlayers.scala
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.psadmin
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.actor.{Actor, Stash}
|
||||
import akka.io.Tcp
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
import scala.collection.mutable.Map
|
||||
import akka.util.ByteString
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import scala.collection.JavaConverters._
|
||||
import net.psforever.objects.zones.InterstellarCluster
|
||||
|
||||
import services.ServiceManager.Lookup
|
||||
import services._
|
||||
|
||||
class CmdListPlayers(args : Array[String], services : Map[String,ActorRef]) extends Actor {
|
||||
private [this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
override def preStart = {
|
||||
services{"cluster"} ! InterstellarCluster.ListPlayers()
|
||||
}
|
||||
|
||||
override def receive = {
|
||||
case InterstellarCluster.PlayerList(players) =>
|
||||
val data = Map[String,Any]()
|
||||
data{"player_count"} = players.size
|
||||
data{"player_list"} = Array[String]()
|
||||
|
||||
if (players.isEmpty) {
|
||||
context.parent ! CommandGoodResponse("No players currently online!", data)
|
||||
} else {
|
||||
data{"player_list"} = players
|
||||
context.parent ! CommandGoodResponse(s"${players.length} players online\n", data)
|
||||
}
|
||||
case default => log.error(s"Unexpected message $default")
|
||||
}
|
||||
}
|
||||
17
pslogin/src/main/scala/psadmin/CmdShutdown.scala
Normal file
17
pslogin/src/main/scala/psadmin/CmdShutdown.scala
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.psadmin
|
||||
|
||||
import akka.actor.{Actor,ActorRef}
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
class CmdShutdown(args : Array[String], services : Map[String,ActorRef]) extends Actor {
|
||||
override def preStart = {
|
||||
var data = Map[String,Any]()
|
||||
context.parent ! CommandGoodResponse("Shutting down", data)
|
||||
context.system.terminate()
|
||||
}
|
||||
|
||||
override def receive = {
|
||||
case default =>
|
||||
}
|
||||
}
|
||||
195
pslogin/src/main/scala/psadmin/PsAdminActor.scala
Normal file
195
pslogin/src/main/scala/psadmin/PsAdminActor.scala
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.psadmin
|
||||
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import akka.actor.{ActorRef, ActorSystem, Props}
|
||||
import akka.actor.{Actor, Stash}
|
||||
import akka.io.Tcp
|
||||
import scodec.bits._
|
||||
import scodec.interop.akka._
|
||||
import scala.collection.mutable.Map
|
||||
import akka.util.ByteString
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import scala.collection.JavaConverters._
|
||||
import net.psforever.objects.zones.InterstellarCluster
|
||||
|
||||
import org.json4s._
|
||||
import org.json4s.Formats._
|
||||
import org.json4s.native.Serialization.write
|
||||
|
||||
import services.ServiceManager.Lookup
|
||||
import services._
|
||||
|
||||
object PsAdminActor {
|
||||
val whiteSpaceRegex = """\s+""".r
|
||||
}
|
||||
|
||||
class PsAdminActor(peerAddress : InetSocketAddress, connection : ActorRef) extends Actor with Stash {
|
||||
private [this] val log = org.log4s.getLogger(self.path.name)
|
||||
|
||||
val services = Map[String,ActorRef]()
|
||||
val servicesToResolve = Array("cluster")
|
||||
var buffer = ByteString()
|
||||
|
||||
implicit val formats = DefaultFormats // for JSON serialization
|
||||
|
||||
case class CommandCall(operation : String, args : Array[String])
|
||||
|
||||
override def preStart() = {
|
||||
log.trace(s"PsAdmin connection started $peerAddress")
|
||||
|
||||
for (service <- servicesToResolve) {
|
||||
ServiceManager.serviceManager ! Lookup(service)
|
||||
}
|
||||
}
|
||||
|
||||
override def receive = ServiceLookup
|
||||
|
||||
def ServiceLookup : Receive = {
|
||||
case ServiceManager.LookupResult(service, endpoint) =>
|
||||
services{service} = endpoint
|
||||
|
||||
if (services.size == servicesToResolve.size) {
|
||||
unstashAll()
|
||||
context.become(ReceiveCommand)
|
||||
}
|
||||
|
||||
case default => stash()
|
||||
}
|
||||
|
||||
def ReceiveCommand : Receive = {
|
||||
case Tcp.Received(data) =>
|
||||
buffer ++= data
|
||||
|
||||
var pos = -1;
|
||||
var amount = 0
|
||||
do {
|
||||
pos = buffer.indexOf('\n')
|
||||
if (pos != -1) {
|
||||
val (cmd, rest) = buffer.splitAt(pos)
|
||||
buffer = rest.drop(1); // drop the newline
|
||||
|
||||
// make sure the CN cant crash us
|
||||
val line = cmd.decodeString("utf-8").trim
|
||||
|
||||
if (line != "") {
|
||||
val tokens = PsAdminActor.whiteSpaceRegex.split(line)
|
||||
val cmd = tokens.head
|
||||
val args = tokens.tail
|
||||
|
||||
|
||||
amount += 1
|
||||
self ! CommandCall(cmd, args)
|
||||
}
|
||||
}
|
||||
} while (pos != -1)
|
||||
|
||||
if (amount > 0)
|
||||
context.become(ProcessCommands)
|
||||
|
||||
case Tcp.PeerClosed =>
|
||||
context.stop(self)
|
||||
|
||||
case default =>
|
||||
log.error(s"Unexpected message $default")
|
||||
}
|
||||
|
||||
/// Process all buffered commands and stash other ones
|
||||
def ProcessCommands : Receive = {
|
||||
case c : CommandCall =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ProcessCommand)
|
||||
|
||||
case default =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ReceiveCommand)
|
||||
}
|
||||
|
||||
/// Process a single command
|
||||
def ProcessCommand : Receive = {
|
||||
case CommandCall(cmd, args) =>
|
||||
val data = Map[String,Any]()
|
||||
|
||||
if (cmd == "help" || cmd == "?") {
|
||||
if (args.size == 0) {
|
||||
var resp = "PsAdmin command usage\n"
|
||||
|
||||
for ((command, info) <- PsAdminCommands.commands) {
|
||||
resp += s"${command} - ${info.usage}\n"
|
||||
}
|
||||
|
||||
data{"message"} = resp
|
||||
} else {
|
||||
if (PsAdminCommands.commands.contains(args(0))) {
|
||||
val info = PsAdminCommands.commands{args(0)}
|
||||
|
||||
data{"message"} = s"${args(0)} - ${info.usage}"
|
||||
} else {
|
||||
data{"message"} = s"Unknown command ${args(0)}"
|
||||
data{"error"} = true
|
||||
}
|
||||
}
|
||||
|
||||
sendLine(write(data.toMap))
|
||||
} else if (PsAdminCommands.commands.contains(cmd)) {
|
||||
val cmd_template = PsAdminCommands.commands{cmd}
|
||||
|
||||
cmd_template match {
|
||||
case PsAdminCommands.Command(usage, handler) =>
|
||||
context.actorOf(Props(handler, args, services))
|
||||
|
||||
case PsAdminCommands.CommandInternal(usage, handler) =>
|
||||
val resp = handler(args)
|
||||
|
||||
resp match {
|
||||
case CommandGoodResponse(msg, data) =>
|
||||
data{"message"} = msg
|
||||
sendLine(write(data.toMap))
|
||||
|
||||
case CommandErrorResponse(msg, data) =>
|
||||
data{"message"} = msg
|
||||
data{"error"} = true
|
||||
sendLine(write(data.toMap))
|
||||
}
|
||||
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
} else {
|
||||
data{"message"} = "Unknown command"
|
||||
data{"error"} = true
|
||||
sendLine(write(data.toMap))
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
|
||||
case resp : CommandResponse =>
|
||||
resp match {
|
||||
case CommandGoodResponse(msg, data) =>
|
||||
data{"message"} = msg
|
||||
sendLine(write(data.toMap))
|
||||
|
||||
case CommandErrorResponse(msg, data) =>
|
||||
data{"message"} = msg
|
||||
data{"error"} = true
|
||||
sendLine(write(data.toMap))
|
||||
}
|
||||
|
||||
context.become(ProcessCommands)
|
||||
context.stop(sender())
|
||||
case default =>
|
||||
stash()
|
||||
unstashAll()
|
||||
context.become(ProcessCommands)
|
||||
}
|
||||
|
||||
def sendLine(line : String) = {
|
||||
ByteVector.encodeUtf8(line + "\n") match {
|
||||
case Left(e) =>
|
||||
log.error(s"Message encoding failure: $e")
|
||||
case Right(bv) =>
|
||||
connection ! Tcp.Write(bv.toByteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
pslogin/src/main/scala/psadmin/PsAdminCommands.scala
Normal file
31
pslogin/src/main/scala/psadmin/PsAdminCommands.scala
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2020 PSForever
|
||||
package net.psforever.psadmin
|
||||
|
||||
import scala.collection.mutable.Map
|
||||
|
||||
sealed trait CommandResponse
|
||||
case class CommandGoodResponse(message : String, data : Map[String,Any]) extends CommandResponse
|
||||
case class CommandErrorResponse(message : String, data : Map[String,Any]) extends CommandResponse
|
||||
|
||||
object PsAdminCommands {
|
||||
import CmdInternal._
|
||||
|
||||
val commands : Map[String,CommandInfo] = Map(
|
||||
"list_players" -> Command("""Return a list of players connected to the interstellar cluster.""", classOf[CmdListPlayers]),
|
||||
"dump_config" -> CommandInternal("""Dumps entire running config.""", cmdDumpConfig),
|
||||
"shutdown" -> Command("""Shuts down the server forcefully.""", classOf[CmdShutdown]),
|
||||
"thread_dump" -> CommandInternal("""Returns all thread's stack traces.""", cmdThreadDump)
|
||||
)
|
||||
|
||||
sealed trait CommandInfo {
|
||||
def usage: String
|
||||
}
|
||||
|
||||
/// A command with full access to the ActorSystem and WorldServer services.
|
||||
/// Spawns an Actor to handle the request and the service queries
|
||||
case class Command[T](usage : String, handler : Class[T]) extends CommandInfo
|
||||
|
||||
/// A command without access to the ActorSystem or any services
|
||||
case class CommandInternal(usage : String, handler : ((Array[String]) => CommandResponse)) extends CommandInfo
|
||||
}
|
||||
|
||||
|
|
@ -3,6 +3,7 @@ import java.io._
|
|||
import scala.io.Source
|
||||
import org.specs2.mutable._
|
||||
import net.psforever.config._
|
||||
import net.psforever.WorldConfig
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class ConfigTest extends Specification {
|
||||
|
|
|
|||
Loading…
Reference in a new issue