Skip to content

Commit

Permalink
servctrl: serv status is updated from terminal cmd
Browse files Browse the repository at this point in the history
config: removed MinecraftServerStartupTime
servctrl-cmd: fixed cmd child process receiving SIGINT
servctrl-cmd: servctrl.Execute() now returns the first line of the output
servctrl-cmd: simplified servctrl.waitForExit()
  • Loading branch information
gekigek99 committed Feb 18, 2021
1 parent 1b89a13 commit fca264a
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 95 deletions.
11 changes: 5 additions & 6 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
{
"basic": {
"serverDirPath": "{path/to/server/folder}",
"serverFileName": "{server.jar}",
"Basic": {
"ServerDirPath": "{path/to/server/folder}",
"ServerFileName": "{server.jar}",
"StartMinecraftServer": "java {-Xmx1024M} {-Xms1024M} -jar serverFileName nogui",
"StopMinecraftServer": "stop",
"StopMinecraftServerForce": "",
"HibernationInfo": " \u0026fserver status:\n \u0026b\u0026lHIBERNATING",
"StartingInfo": " \u0026fserver status:\n \u00266\u0026lWARMING UP",
"MinecraftServerStartupTime": 20,
"TimeBeforeStoppingEmptyServer": 60,
"CheckForUpdates": true
},
"advanced": {
"Advanced": {
"ListenHost": "0.0.0.0",
"ListenPort": "25555",
"TargetHost": "127.0.0.1",
"TargetPort": "25565",
"Debug": false,
"ServerVersion": "1.16.4",
"ServerVersion": "1.16.5",
"ServerProtocol": "754"
}
}
1 change: 0 additions & 1 deletion lib/confctrl/confctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ type basic struct {
StopMinecraftServerForce string
HibernationInfo string
StartingInfo string
MinecraftServerStartupTime int
TimeBeforeStoppingEmptyServer int
CheckForUpdates bool
}
Expand Down
4 changes: 2 additions & 2 deletions lib/connctrl/connctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ func HandleClientSocket(clientSocket net.Conn) {
servctrl.StartMinecraftServer()
log.Printf("*** %s tried to join from %s:%s to %s:%s\n", playerName, clientAddress, confctrl.Config.Advanced.ListenPort, confctrl.Config.Advanced.TargetHost, confctrl.Config.Advanced.TargetPort)
// answer to client with text in the loadscreen
clientSocket.Write(servprotocol.BuildMessage("txt", fmt.Sprintf("Server start command issued. Please wait... Time left: %d seconds", servctrl.ServStats.TimeLeftUntilUp)))
clientSocket.Write(servprotocol.BuildMessage("txt", fmt.Sprintf("Server start command issued. Please wait...")))

} else if servctrl.ServStats.Status == "starting" {
log.Printf("*** %s tried to join from %s:%s to %s:%s during server startup\n", playerName, clientAddress, confctrl.Config.Advanced.ListenPort, confctrl.Config.Advanced.TargetHost, confctrl.Config.Advanced.TargetPort)
// answer to client with text in the loadscreen
clientSocket.Write(servprotocol.BuildMessage("txt", fmt.Sprintf("Server is starting. Please wait... Time left: %d seconds", servctrl.ServStats.TimeLeftUntilUp)))
clientSocket.Write(servprotocol.BuildMessage("txt", fmt.Sprintf("Server is starting. Please wait...")))
}
}

Expand Down
121 changes: 89 additions & 32 deletions lib/servctrl/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import (
"os/exec"
"strings"
"sync"
"syscall"
"time"

"msh/lib/asyncctrl"
"msh/lib/confctrl"
"msh/lib/debugctrl"
)

// ServTerm is the minecraft server terminal
type ServTerm struct {
IsActive bool
isActive bool
Wg sync.WaitGroup
cmd *exec.Cmd
out readcl
Expand All @@ -34,6 +38,9 @@ type writecl struct {
io.WriteCloser
}

// lastLine is a channel used to communicate the last line got from the printer function
var lastLine = make(chan string)

var colRes string = "\033[0m"
var colCya string = "\033[36m"

Expand All @@ -48,9 +55,10 @@ func CmdStart(dir, command string) (*ServTerm, error) {
return nil, err
}

go term.out.printer()
go term.err.printer()
go term.in.scanner()
term.Wg.Add(2)
go term.out.printer(term)
go term.err.printer(term)
go term.scanner()

err = term.cmd.Start()
if err != nil {
Expand All @@ -59,38 +67,53 @@ func CmdStart(dir, command string) (*ServTerm, error) {

go term.waitForExit()

// initialization
ServStats.Status = "starting"
ServStats.LoadProgress = "0%"
ServStats.Players = 0
log.Print("*** MINECRAFT SERVER IS STARTING!")

return term, nil
}

// Execute executes a command on the specified term
func (term *ServTerm) Execute(command string) error {
if !term.IsActive {
return fmt.Errorf("terminal is not active")
func (term *ServTerm) Execute(command string) (string, error) {
if !term.isActive {
return "", fmt.Errorf("servctrl-cmd: Execute: terminal not active")
}

commands := strings.Split(command, "\n")

for _, com := range commands {
// needs to be added otherwise the virtual "enter" button is not pressed
com += "\n"

log.Print("terminal execute: ", com)

// write to cmd
_, err := term.in.Write([]byte(com))
if err != nil {
return err
if ServStats.Status == "online" {
debugctrl.Logger("sending", com, "to terminal")

// write to cmd (\n indicates the enter key)
_, err := term.in.Write([]byte(com + "\n"))
if err != nil {
return "", err
}
} else {
return "", fmt.Errorf("servctrl-cmd: Execute: server not online")
}
}

return nil
return <-lastLine, nil
}

func (term *ServTerm) loadCmd(dir, command string) {
cSplit := strings.Split(command, " ")

term.cmd = exec.Command(cSplit[0], cSplit[1:]...)
term.cmd.Dir = dir

// launch as new process group so that signals (ex: SIGINT) are not sent also the the child process
term.cmd.SysProcAttr = &syscall.SysProcAttr{
// windows //
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
// linux //
// Setpgid: true,
}
}

func (term *ServTerm) loadStdPipes() error {
Expand All @@ -114,28 +137,29 @@ func (term *ServTerm) loadStdPipes() error {
return nil
}

// waitForExit manages term.isActive parameter and set ServStats.Status = "offline" when it exits
func (term *ServTerm) waitForExit() {
term.IsActive = true
term.isActive = true

term.Wg.Add(1)
err := term.cmd.Wait()
if err != nil {
debugctrl.Logger("waitForExit: error while waiting for cmd exit")
}
term.Wg.Done()

term.IsActive = false
// wait for printer (out-err) to exit
term.Wg.Wait()

term.out.Close()
term.err.Close()
term.in.Close()

fmt.Println("terminal exited correctly")
term.isActive = false
debugctrl.Logger("cmd: waitForExit: terminal exited")

ServStats.Status = "offline"
log.Print("*** MINECRAFT SERVER IS OFFLINE!")
}

func (cmdOutErrReader *readcl) printer() {
func (cmdOutErrReader *readcl) printer(term *ServTerm) {
var line string

defer term.Wg.Done()

scanner := bufio.NewScanner(cmdOutErrReader)

for scanner.Scan() {
Expand All @@ -144,12 +168,41 @@ func (cmdOutErrReader *readcl) printer() {
fmt.Println(colCya + line + colRes)

if cmdOutErrReader.typ == "out" {
// look for flag strings in stdout

// case where the server is starting
if ServStats.Status == "starting" {
if strings.Contains(line, "Preparing spawn area: ") {
ServStats.LoadProgress = strings.Split(strings.Split(line, "Preparing spawn area: ")[1], "\n")[0]
}
if strings.Contains(line, "[Server thread/INFO]: Done") {
ServStats.Status = "online"
log.Print("*** MINECRAFT SERVER IS ONLINE!")

// launch a stopInstance so that if no players connect the server will shutdown
asyncctrl.WithLock(func() { ServStats.StopInstances++ })
time.AfterFunc(time.Duration(confctrl.Config.Basic.TimeBeforeStoppingEmptyServer)*time.Second, func() { StopEmptyMinecraftServer(false) })
}
}

// case where the server is online
if ServStats.Status == "online" {
// if the terminal contains "Stopping" this means that the minecraft server is stopping
if strings.Contains(line, "[Server thread/INFO]: Stopping") {
ServStats.Status = "stopping"
log.Print("*** MINECRAFT SERVER IS STOPPING!")
}
}
}

// communicate to lastLine so that Execute function can return the first line after the command
select {
case lastLine <- line:
default:
}
}
}

func (cmdInWriter *writecl) scanner() {
func (term *ServTerm) scanner() {
var line string
var err error

Expand All @@ -158,10 +211,14 @@ func (cmdInWriter *writecl) scanner() {
for {
line, err = reader.ReadString('\n')
if err != nil {
debugctrl.Logger("cmdInWriter scanner:", err.Error())
debugctrl.Logger("servTerm scanner:", err.Error())
continue
}

cmdInWriter.Write([]byte(line))
_, err = term.Execute(line)
if err != nil {
debugctrl.Logger("servTerm scanner:", err.Error())
continue
}
}
}
83 changes: 32 additions & 51 deletions lib/servctrl/servctrl.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,75 +25,56 @@ func StartMinecraftServer() {
}

// initialization
ServStats.Status = "starting"
ServStats.TimeLeftUntilUp = confctrl.Config.Basic.MinecraftServerStartupTime
ServStats.Players = 0

log.Print("*** MINECRAFT SERVER IS STARTING!")

// sets serverStatus == "online".
// After {TimeBeforeStoppingEmptyServer} executes stopEmptyMinecraftServer(false)
var setServerStatusOnline = func() {
ServStats.Status = "online"
log.Print("*** MINECRAFT SERVER IS UP!")

asyncctrl.WithLock(func() { ServStats.StopInstances++ })
time.AfterFunc(time.Duration(confctrl.Config.Basic.TimeBeforeStoppingEmptyServer)*time.Second, func() { StopEmptyMinecraftServer(false) })
}
// updates TimeLeftUntilUp each second. if TimeLeftUntilUp == 0 it executes setServerStatusOnline()
var updateTimeleft func()
updateTimeleft = func() {
if ServStats.TimeLeftUntilUp > 0 {
ServStats.TimeLeftUntilUp--
time.AfterFunc(1*time.Second, func() { updateTimeleft() })
} else if ServStats.TimeLeftUntilUp == 0 {
setServerStatusOnline()
}
}

time.AfterFunc(1*time.Second, func() { updateTimeleft() })
}

// StopEmptyMinecraftServer stops the minecraft server
func StopEmptyMinecraftServer(force bool) {
if force && ServStats.Status != "offline" {
// skip some checks to issue the stop server command forcefully
// wait for the starting server to become online
for ServStats.Status != "starting" {
time.Sleep(1 * time.Second)
}
// if server is not online return
if ServStats.Status != "online" {
debugctrl.Logger("servctrl: StopEmptyMinecraftServer: server is not online")
return
}

if force {
// skip checks to issue the stop server command forcefully
} else {
// check that there is only one "stop server command" instance running and players <= 0 and ServerStatus != "offline".
// check that there is only one "stop server command" instance running and players <= 0.
// on the contrary the server won't be stopped
asyncctrl.WithLock(func() { ServStats.StopInstances-- })

if asyncctrl.WithLock(func() interface{} {
return ServStats.StopInstances > 0 || ServStats.Players > 0 || ServStats.Status == "offline"
}).(bool) {
if asyncctrl.WithLock(func() interface{} { return ServStats.StopInstances > 0 || ServStats.Players > 0 }).(bool) {
return
}
}

// execute stop command
if force && confctrl.Config.Basic.StopMinecraftServerForce != "" {
err := servTerm.Execute(confctrl.Config.Basic.StopMinecraftServerForce)
if err != nil {
log.Printf("stopEmptyMinecraftServer: error stopping minecraft server: %v\n", err)
return
}
// waits for the terminal to exit
debugctrl.Logger("waiting for server terminal to exit")
servTerm.Wg.Wait()
debugctrl.Logger("server terminal exited")
} else {
err := servTerm.Execute(confctrl.Config.Basic.StopMinecraftServer)
if err != nil {
log.Printf("stopEmptyMinecraftServer: error stopping minecraft server: %v\n", err)
return
var stopCom string
stopCom = confctrl.Config.Basic.StopMinecraftServer
if force {
if confctrl.Config.Basic.StopMinecraftServerForce != "" {
stopCom = confctrl.Config.Basic.StopMinecraftServerForce
}
}

ServStats.Status = "offline"
_, err := servTerm.Execute(stopCom)
if err != nil {
log.Printf("stopEmptyMinecraftServer: error stopping minecraft server: %s\n", err.Error())
return
}

if force {
log.Print("*** MINECRAFT SERVER IS FORCEFULLY SHUTTING DOWN!")
} else {
log.Print("*** MINECRAFT SERVER IS SHUTTING DOWN!")
if ServStats.Status == "stopping" {
// wait for the terminal to exit
debugctrl.Logger("waiting for server terminal to exit")
servTerm.Wg.Wait()
} else {
log.Println()
debugctrl.Logger("server does not seem to be stopping, is the stopForce command correct?")
}
}
}
7 changes: 4 additions & 3 deletions lib/servctrl/servstats.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package servctrl

type serverStats struct {
// ServerStatus represent the status of the minecraft server ("offline", "starting", "online")
// ServerStatus represent the status of the minecraft server ("offline", "starting", "online", "stopping")
Status string
// Players keeps track of players connected to the server
Players int
// StopInstances keeps track of how many times stopEmptyMinecraftServer() has been called in the last {TimeBeforeStoppingEmptyServer} seconds
StopInstances int
// TimeLeftUntilUp keeps track of how many seconds are still needed to reach serverStatus == "online"
TimeLeftUntilUp int
// LoadProgress indicates the loading percentage while the server is starting
LoadProgress string
}

// ServStats contains the info relative to server
Expand All @@ -19,5 +19,6 @@ func init() {
Status: "offline",
Players: 0,
StopInstances: 0,
LoadProgress: "0%",
}
}

0 comments on commit fca264a

Please sign in to comment.