diff --git a/src/ectl/main.go b/src/ectl/main.go index 3ce530e..bfd460f 100644 --- a/src/ectl/main.go +++ b/src/ectl/main.go @@ -57,7 +57,59 @@ func main() { return } else if flag.Args()[0] == "service" || flag.Args()[0] == "sv" { if len(flag.Args()) <= 1 { - fmt.Println("Usage: ectl service [service]") + fmt.Println("Usage: ectl service [service]") + return + } + if flag.Arg(1) == "reload" { + type ServiceCommandJsonStruct struct { + Command string `json:"command"` + Service string `json:"service"` + } + serviceCommandJson := ServiceCommandJsonStruct{ + Command: flag.Arg(1), + } + + // Encode struct to json string + jsonData, err := json.Marshal(serviceCommandJson) + if err != nil { + log.Fatalf("Could not encode JSON data! Error: %s\n", err) + } + + _, err = conn.Write(jsonData) + if err != nil { + log.Fatalf("Could not write JSON data to socket! Error: %s\n", err) + } + + // Create a buffer for incoming data. + buf := make([]byte, 4096) + + // Read data from the connection. + n, err := conn.Read(buf) + if err != nil { + return + } + + // Print json data if flag is set + if *printJson { + fmt.Println(string(buf[:n])) + return + } + + // Decoode JSON data + var returnedJsonData map[string]any + err = json.Unmarshal(buf[:n], &returnedJsonData) + if err != nil { + log.Fatalf("Could not decode JSON data from connection!") + } + + if err, ok := returnedJsonData["error"]; ok { + log.Fatal(err) + } else if msg, ok := returnedJsonData["success"]; ok { + fmt.Println(msg) + } else { + log.Fatal("Connection returned empty string!") + } + return } else if flag.Arg(1) == "start" || flag.Arg(1) == "stop" || flag.Arg(1) == "restart" { // Ensure service name argument has been set diff --git a/src/esvm/main.go b/src/esvm/main.go index 7ff03c2..643237d 100644 --- a/src/esvm/main.go +++ b/src/esvm/main.go @@ -1,6 +1,7 @@ package main import ( + "crypto/sha256" "flag" "fmt" "io" @@ -143,19 +144,21 @@ func Init() { } service := EnitService{ - Name: "", - Description: "", - Dependencies: make([]string, 0), - Type: "", - StartCmd: "", - ExitMethod: "", - StopCmd: "", - Restart: "", - CrashOnSafeExit: true, - restartCount: 0, - stopChannel: make(chan bool), - LogOutput: true, - state: EnitServiceUnloaded, + Name: "", + Description: "", + Dependencies: make([]string, 0), + Type: "", + StartCmd: "", + ExitMethod: "", + StopCmd: "", + Restart: "", + CrashOnSafeExit: true, + LogOutput: true, + Filepath: path.Join(serviceConfigDir, "services", entry.Name()), + filepathChecksum: sha256.Sum256(bytes), + restartCount: 0, + stopChannel: make(chan bool), + state: EnitServiceUnloaded, } if err := yaml.Unmarshal(bytes, &service); err != nil { logger.Printf("Error: could not read service file %s", path.Join(serviceConfigDir, "services", entry.Name())) @@ -227,6 +230,16 @@ func Init() { logger.Println("ESVM initialized successfully!") } +func Reload() { + logger.Println("Reloading all ESVM services...") + + for _, service := range Services { + service.ReloadService() + } + + logger.Println("All ESVM services have been reloaded!") +} + func Destroy() { logger.Println("Stopping all ESVM services...") diff --git a/src/esvm/service.go b/src/esvm/service.go index e3031e3..37fb615 100644 --- a/src/esvm/service.go +++ b/src/esvm/service.go @@ -1,6 +1,7 @@ package main import ( + "crypto/sha256" "fmt" "io" "os" @@ -37,21 +38,24 @@ var EnitServiceStateNames map[EnitServiceState]string = map[EnitServiceState]str } type EnitService struct { - Name string `yaml:"name"` - Description string `yaml:"description,omitempty"` - Dependencies []string `yaml:"dependencies,omitempty"` - Type string `yaml:"type"` - StartCmd string `yaml:"start_cmd"` - ExitMethod string `yaml:"exit_method"` - CrashOnSafeExit bool `yaml:"crash_on_safe_exit"` - StopCmd string `yaml:"stop_cmd,omitempty"` - Restart string `yaml:"restart,omitempty"` - ReadyFd int `yaml:"ready_fd"` - LogOutput bool `yaml:"log_output,omitempty"` - state EnitServiceState - processID int - restartCount int - stopChannel chan bool + Name string `yaml:"name"` + Description string `yaml:"description,omitempty"` + Dependencies []string `yaml:"dependencies,omitempty"` + Type string `yaml:"type"` + StartCmd string `yaml:"start_cmd"` + ExitMethod string `yaml:"exit_method"` + CrashOnSafeExit bool `yaml:"crash_on_safe_exit"` + StopCmd string `yaml:"stop_cmd,omitempty"` + Restart string `yaml:"restart,omitempty"` + ReadyFd int `yaml:"ready_fd"` + LogOutput bool `yaml:"log_output,omitempty"` + Filepath string + filepathChecksum [32]byte + state EnitServiceState + processID int + restartCount int + stopChannel chan bool + shouldReload bool } var Services = make([]*EnitService, 0) @@ -117,6 +121,90 @@ func (service *EnitService) GetLogFile() (file *os.File, err error) { return file, nil } +func (service *EnitService) ReloadService() { + bytes, err := os.ReadFile(service.Filepath) + checksum := sha256.Sum256(bytes) + if slices.Equal(checksum[:], service.filepathChecksum[:]) { + return + } + + if service.state == EnitServiceStarting || service.state == EnitServiceRunning { + service.shouldReload = true + logger.Printf("Warning: Service (%s) is currently running and will be reloaded when stopped\n", service.Name) + return + } + service.shouldReload = false + + logger.Printf("Reloading service (%s)...\n", service.Filepath) + + if os.IsNotExist(err) { + Services = slices.DeleteFunc(Services, func(sv *EnitService) bool { + return sv == service + }) + logger.Printf("Service (%s) has been removed\n", service.Name) + return + } else if err != nil { + logger.Printf("Error: Could not read service file (%s)", service.Filepath) + return + } + + newService := EnitService{ + Name: "", + Description: "", + Dependencies: make([]string, 0), + Type: "", + StartCmd: "", + ExitMethod: "", + StopCmd: "", + Restart: "", + CrashOnSafeExit: true, + LogOutput: true, + Filepath: service.Filepath, + filepathChecksum: checksum, + restartCount: service.restartCount, + stopChannel: service.stopChannel, + state: service.state, + } + if err := yaml.Unmarshal(bytes, &newService); err != nil { + logger.Printf("Error: could not read service file %s", service.Filepath) + return + } + + for _, sv := range Services { + if sv.Name == newService.Name && sv != service { + logger.Printf("Error: service with name (%s) has already been initialized", service.Name) + } + } + + switch newService.Type { + case "simple", "background": + default: + logger.Printf("Error: unknown service type (%s)", newService.Type) + return + } + + switch newService.ExitMethod { + case "stop_command", "kill": + default: + logger.Printf("Error: unknown exit method (%s)\n", newService.ExitMethod) + return + } + + switch newService.Restart { + case "true", "always": + default: + newService.Restart = "false" + } + + for i, sv := range Services { + if sv == service { + Services[i] = &newService + } + } + + logger.Printf("Service (%s) has been reloaded!\n", newService.Name) +} + func (service *EnitService) StartService() (err error) { if service == nil { return nil @@ -218,6 +306,14 @@ func (service *EnitService) StartService() (err error) { service.restartCount = 0 if service.ExitMethod != "stop_command" { service.state = EnitServiceCompleted + + // Reload service if needed + if service.shouldReload { + service.ReloadService() + if GetServiceByName(service.Name) == nil { + return + } + } } else { service.state = EnitServiceRunning } @@ -231,6 +327,15 @@ func (service *EnitService) StartService() (err error) { service.state = EnitServiceCrashed } + // Reload service if needed + if service.shouldReload { + service.ReloadService() + if GetServiceByName(service.Name) == nil { + return + } + service = GetServiceByName(service.Name) + } + if service.Restart == "always" { _ = service.StartService() } else if service.Restart == "true" && service.restartCount < 5 { @@ -263,6 +368,15 @@ func (service *EnitService) StopService() error { defer func() { service.state = newServiceStatus service.processID = 0 + + // Reload service if needed + if service.shouldReload { + service.ReloadService() + if GetServiceByName(service.Name) == nil { + return + } + service = GetServiceByName(service.Name) + } }() if service.ExitMethod == "kill" { diff --git a/src/esvm/socket.go b/src/esvm/socket.go index c916320..0feeb12 100644 --- a/src/esvm/socket.go +++ b/src/esvm/socket.go @@ -17,6 +17,7 @@ func initSocket() (socket net.Listener, err error) { } // Register command handlers + commandHandlers["reload"] = handleReloadServicesCommand commandHandlers["start"] = handleStartServiceCommand commandHandlers["stop"] = handleStopServiceCommand commandHandlers["restart"] = handleRestartServiceCommand @@ -74,6 +75,13 @@ func listenToSocket() { }(conn) } +func handleReloadServicesCommand(conn net.Conn, jsonData map[string]any) { + // Reload services + Reload() + + conn.Write(wrapSuccessMsgInJson("Services reloaded successfully")) +} + func handleStartServiceCommand(conn net.Conn, jsonData map[string]any) { // Get service name from json data serviceName, ok := jsonData["service"]