mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-04-28 23:35:23 +00:00
Merge pull request #425 from pschord/remote-admin
Create PsAdmin framework
This commit is contained in:
commit
fbca774a37
16 changed files with 419 additions and 7 deletions
|
|
@ -25,6 +25,7 @@ lazy val commonSettings = Seq(
|
||||||
"-sourcepath", baseDirectory.value.getAbsolutePath // needed for scaladoc relative source paths
|
"-sourcepath", baseDirectory.value.getAbsolutePath // needed for scaladoc relative source paths
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat,
|
||||||
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
|
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots",
|
||||||
libraryDependencies ++= Seq(
|
libraryDependencies ++= Seq(
|
||||||
"com.typesafe.akka" %% "akka-actor" % "2.4.4",
|
"com.typesafe.akka" %% "akka-actor" % "2.4.4",
|
||||||
|
|
@ -46,9 +47,9 @@ lazy val commonSettings = Seq(
|
||||||
"org.ini4j" % "ini4j" % "0.5.4",
|
"org.ini4j" % "ini4j" % "0.5.4",
|
||||||
"org.scala-graph" %% "graph-core" % "1.12.5",
|
"org.scala-graph" %% "graph-core" % "1.12.5",
|
||||||
"io.kamon" %% "kamon-bundle" % "2.1.0",
|
"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(
|
lazy val pscryptoSettings = Seq(
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,10 @@ trait ConfigParser {
|
||||||
config_map = map
|
config_map = map
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def GetRawConfig : Map[String, Any] = {
|
||||||
|
config_map
|
||||||
|
}
|
||||||
|
|
||||||
def FormatErrors(invalidResult : Invalid) : Seq[String] = {
|
def FormatErrors(invalidResult : Invalid) : Seq[String] = {
|
||||||
var count = 0;
|
var count = 0;
|
||||||
|
|
||||||
|
|
@ -209,7 +213,7 @@ trait ConfigParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected def parseSection(sectionIni : org.ini4j.Profile.Section, entry : ConfigEntry, map : Map[String, Any]) : ValidationResult = {
|
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 full_key : String = sectionIni.getName + "." + entry.key
|
||||||
|
|
||||||
val value = if (rawValue == null) {
|
val value = if (rawValue == null) {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,16 @@ class InterstellarCluster(zones : List[Zone]) extends Actor {
|
||||||
case None => //zone_number does not exist
|
case None => //zone_number does not exist
|
||||||
sender ! Zone.Lattice.NoValidSpawnPoint(zone_number, None)
|
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, _, _, _) =>
|
case msg @ Zone.Lattice.RequestSpecificSpawnPoint(zone_number, _, _, _) =>
|
||||||
recursiveFindWorldInCluster(zones.iterator, _.Number == zone_number) match {
|
recursiveFindWorldInCluster(zones.iterator, _.Number == zone_number) match {
|
||||||
|
|
@ -188,6 +198,9 @@ object InterstellarCluster {
|
||||||
*/
|
*/
|
||||||
final case class GiveWorld(zoneId : String, zone : Zone)
|
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.
|
* Signal to the cluster that a new client needs to be initialized for all listed `Zone` destinations.
|
||||||
* @see `Zone`
|
* @see `Zone`
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ ServerType = Released
|
||||||
# Important: Must be different from the worldserver.ListeningPort. Ports below 1024 are
|
# Important: Must be different from the worldserver.ListeningPort. Ports below 1024 are
|
||||||
# privileged on Linux and may require root.
|
# privileged on Linux and may require root.
|
||||||
# Range: [1, 65535] - (UDP port 1, UDP port 65535)
|
# 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
|
ListeningPort = 51000
|
||||||
|
|
||||||
|
|
@ -134,6 +134,22 @@ ListeningPort = 51000
|
||||||
|
|
||||||
CreateMissingAccounts = yes
|
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
|
# NETWORK SETTINGS
|
||||||
###################################################################################################
|
###################################################################################################
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright (c) 2017 PSForever
|
// Copyright (c) 2017 PSForever
|
||||||
|
import net.psforever.WorldConfig
|
||||||
import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
|
import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
|
||||||
import com.github.mauricio.async.db.{Configuration, QueryResult, RowData, SSLConfiguration}
|
import com.github.mauricio.async.db.{Configuration, QueryResult, RowData, SSLConfiguration}
|
||||||
import scala.util.{Try,Success,Failure}
|
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.Account
|
||||||
import net.psforever.objects.DefaultCancellable
|
import net.psforever.objects.DefaultCancellable
|
||||||
import net.psforever.types.PlanetSideEmpire
|
import net.psforever.types.PlanetSideEmpire
|
||||||
|
import net.psforever.WorldConfig
|
||||||
import services.ServiceManager
|
import services.ServiceManager
|
||||||
import services.ServiceManager.Lookup
|
import services.ServiceManager.Lookup
|
||||||
import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
|
import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import net.psforever.config.{Invalid, Valid}
|
||||||
import net.psforever.crypto.CryptoInterface
|
import net.psforever.crypto.CryptoInterface
|
||||||
import net.psforever.objects.zones._
|
import net.psforever.objects.zones._
|
||||||
import net.psforever.objects.guid.TaskResolver
|
import net.psforever.objects.guid.TaskResolver
|
||||||
|
import net.psforever.psadmin.PsAdminActor
|
||||||
|
import net.psforever.WorldConfig
|
||||||
import org.slf4j
|
import org.slf4j
|
||||||
import org.fusesource.jansi.Ansi._
|
import org.fusesource.jansi.Ansi._
|
||||||
import org.fusesource.jansi.Ansi.Color._
|
import org.fusesource.jansi.Ansi.Color._
|
||||||
|
|
@ -260,6 +262,7 @@ object PsLogin {
|
||||||
|
|
||||||
val loginServerPort = WorldConfig.Get[Int]("loginserver.ListeningPort")
|
val loginServerPort = WorldConfig.Get[Int]("loginserver.ListeningPort")
|
||||||
val worldServerPort = WorldConfig.Get[Int]("worldserver.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 {
|
val netSim : Option[NetworkSimulatorParameters] = WorldConfig.Get[Boolean]("developer.NetSim.Active") match {
|
||||||
case true =>
|
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")
|
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")
|
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")
|
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)
|
// 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...
|
Locale.setDefault(Locale.US); // to have floats with dots, not comma...
|
||||||
this.args = args
|
this.args = args
|
||||||
run()
|
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 akka.actor.SupervisorStrategy.Stop
|
||||||
import net.psforever.packet.PacketCoding
|
import net.psforever.packet.PacketCoding
|
||||||
import net.psforever.packet.control.ConnectionClose
|
import net.psforever.packet.control.ConnectionClose
|
||||||
|
import net.psforever.WorldConfig
|
||||||
import services.ServiceManager
|
import services.ServiceManager
|
||||||
import services.ServiceManager.Lookup
|
import services.ServiceManager.Lookup
|
||||||
import services.account.{IPAddress, StoreIPAddress}
|
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
|
// Copyright (c) 2019 PSForever
|
||||||
|
package net.psforever
|
||||||
|
|
||||||
import scala.util.matching.Regex
|
import scala.util.matching.Regex
|
||||||
import net.psforever.config._
|
import net.psforever.config._
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
@ -31,6 +33,9 @@ object WorldConfig extends ConfigParser {
|
||||||
ConfigEntryTime("Session.InboundGraceTime", 1 minute, Constraints.min(10 seconds)),
|
ConfigEntryTime("Session.InboundGraceTime", 1 minute, Constraints.min(10 seconds)),
|
||||||
ConfigEntryTime("Session.OutboundGraceTime", 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",
|
ConfigSection("developer",
|
||||||
ConfigEntryBool ("NetSim.Active", false),
|
ConfigEntryBool ("NetSim.Active", false),
|
||||||
ConfigEntryFloat("NetSim.Loss", 0.02f, Constraints.min(0.0f), Constraints.max(1.0f)),
|
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 scala.io.Source
|
||||||
import org.specs2.mutable._
|
import org.specs2.mutable._
|
||||||
import net.psforever.config._
|
import net.psforever.config._
|
||||||
|
import net.psforever.WorldConfig
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
class ConfigTest extends Specification {
|
class ConfigTest extends Specification {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue