45 Commits
Author SHA1 Message Date
EnumDev 88f41adea4 other: update version setting using git 2025-12-08 17:03:50 +02:00
EnumDev fa17840ad5 esvm: move enabled services to enabled-services.yml file 2025-12-08 15:19:57 +02:00
EnumDev 06f5e6564f ectl: move enable/disable service functionality back into ectl 2025-12-07 18:03:13 +02:00
EnumDev 354fa510fd ectl: reorganize code 2025-12-07 15:10:32 +02:00
EnumDev b45f739bc3 Move dialSocket function call into 'sv' subcommand 2025-12-07 13:26:31 +02:00
EnumDev 26a6952091 Remove redundant 'exit_method' field 2025-12-03 19:03:37 +02:00
EnumDev e1a51649ae Remove obsolete 'dependencies' field from lo-interface.esv 2025-12-03 18:01:04 +02:00
EnumDev bc6ea591c3 Add 'user' field to services 2025-12-03 17:59:41 +02:00
EnumDev 8abc4cf49c Tweak timeouts 2025-11-25 21:52:21 +02:00
EnumDev e5c465610c Only wait for background service processes to exit 2025-11-24 16:34:55 +02:00
EnumDev bcff6fecf8 Separate root remounting into new function and improve killProcesses function 2025-11-18 19:47:29 +02:00
EnumDev 5ee42a5966 Improve service initialization and reloading 2025-11-17 20:49:46 +02:00
EnumDev 7bbfa9f198 Kill processes and unmount filesystems before shutdown/reboot 2025-11-17 12:29:09 +02:00
EnumDev 81421b0cc4 Prevent killing process if pid is 0 2025-11-17 10:03:45 +02:00
EnumDev 2aef0c0ff9 Improve StopService function 2025-11-16 20:18:08 +02:00
EnumDev 33e7479044 Fix esvm exiting on service crash 2025-11-16 17:27:47 +02:00
EnumDev 92587d8099 Improve error handling and fstab parsing 2025-11-16 14:14:31 +02:00
EnumDev 7f37da2091 Allow reading data of any size through socket 2025-11-09 19:33:32 +02:00
EnumDev 285ede0e9a Show service description when running 'ectl sv list' or 'ectl sv status' 2025-11-09 18:27:16 +02:00
EnumDev 93ab1c0607 Remove service dependencies field 2025-11-09 14:32:01 +02:00
EnumDev dea5760d75 Read enabled services file during runtime 2025-11-09 11:28:51 +02:00
EnumDev 70ef610ce6 Use process group IDs for terminating service subprocesses 2025-11-08 20:06:59 +02:00
EnumDev 5ed1128e43 Add 'reload' service subcommand to ectl 2025-11-06 18:48:26 +02:00
EnumDev 87412f20f8 Fix possible crash when shutting down due to closed channel 2025-11-04 20:53:41 +02:00
EnumDev 2be3e0c947 Add ready_fd service field 2025-10-14 20:39:34 +03:00
EnumDev 6d34559335 Store service state in struct 2025-09-24 19:08:51 +03:00
EnumDev 09d8b70009 Store service process ID in struct 2025-09-24 18:58:46 +03:00
EnumDev 8ed8efa9b6 Put enabled services to stage 3 when converting plain text format 2025-09-24 15:07:48 +03:00
EnumDev 2b4feb60ec Fix esvm looking for stage 0 enabled services 2025-09-24 13:51:41 +03:00
EnumDev 085bc676a0 Set default enabled service stage to 3 2025-09-24 13:37:16 +03:00
EnumDev d83c822c73 Minor enabled service stage fixes 2025-09-24 13:34:46 +03:00
EnumDev f470c0ec78 Add enabled service stages and redo esvm startup and shutdown 2025-09-23 21:32:17 +03:00
EnumDev 3c1d39831f Keep simple services running if exit method is "stop_command" 2025-09-01 16:10:56 +03:00
EnumDev b3565cb9e1 Merge pull request 'Make ESVM socket accept json data instead of raw text commands' (#1) from json_switch into master
Reviewed-on: #1
2025-08-30 10:54:57 +00:00
EnumDev c58802301a Add --json flag that prints out output in json format 2025-08-28 11:10:04 +03:00
EnumDev 71435bc676 Move service list functionality to esvm 2025-08-28 11:09:00 +03:00
EnumDev 1e35b0a000 Move service status functionality to esvm 2025-08-27 20:23:15 +03:00
EnumDev ba51f2ec4d Use new isEnabled function in ESVM initialization 2025-08-27 16:17:39 +03:00
EnumDev 0df715dda1 Move service enable/disable functionality to esvm 2025-08-27 15:57:56 +03:00
EnumDev d344347782 Switch service start, stop and restart commands to JSON 2025-08-27 12:51:55 +03:00
EnumDev 1d4a51abe0 Separate go files in esvm 2025-08-26 14:15:31 +03:00
EnumDev 634e0271f5 Skip reading /etc/fstab if it does not exist 2025-08-26 14:05:04 +03:00
EnumDev ff8c130ed0 Improve logging system 2025-08-25 19:43:39 +03:00
EnumDev 19e9b6ecdb Rename cmd directory to src 2025-04-09 09:30:26 +03:00
EnumDev 0359524f9f Move logger setup to after flag and permission checks 2025-04-09 09:29:27 +03:00
28 changed files with 2105 additions and 1220 deletions
+4 -16
View File
@@ -7,25 +7,13 @@ LOCALSTATEDIR ?= $(PREFIX)/var
RUNSTATEDIR ?= $(LOCALSTATEDIR)/run RUNSTATEDIR ?= $(LOCALSTATEDIR)/run
GO ?= $(shell type -a -P go | head -n 1) GO ?= $(shell type -a -P go | head -n 1)
# Set version variable VERSION ?= $(shell git describe --tags --dirty)
ifeq ($(VERSION),)
COMMIT := $(shell git rev-parse --short HEAD)
TAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1)
TAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true)
VERSION := $(COMMIT)
ifeq ($(COMMIT), $(TAG_COMMIT))
VERSION := $(TAG)
endif
ifneq ($(shell git status --porcelain),)
VERSION := $(VERSION)-dirty
endif
endif
build: build:
mkdir -p build mkdir -p build
cd cmd/enit; $(GO) build -ldflags "-w -X main.version=$(VERSION)" -o ../../build/enit enit cd src/enit; $(GO) build -ldflags "-w -X main.version=$(VERSION)" -o ../../build/enit enit
cd cmd/esvm; $(GO) build -ldflags "-w -X main.version=$(VERSION)" -o ../../build/esvm esvm cd src/esvm; $(GO) build -ldflags "-w -X main.version=$(VERSION)" -o ../../build/esvm esvm
cd cmd/ectl; $(GO) build -ldflags "-w -X main.version=$(VERSION) -X main.sysconfdir=$(SYSCONFDIR) -X main.runstatedir=$(RUNSTATEDIR)" -o ../../build/ectl ectl cd src/ectl; $(GO) build -ldflags "-w -X main.version=$(VERSION) -X main.sysconfdir=$(SYSCONFDIR) -X main.runstatedir=$(RUNSTATEDIR)" -o ../../build/ectl ectl
install: build/enit build/ectl install: build/enit build/ectl
mkdir -p $(DESTDIR)$(SBINDIR) mkdir -p $(DESTDIR)$(SBINDIR)
-5
View File
@@ -1,5 +0,0 @@
module ectl
go 1.23.4
require gopkg.in/yaml.v3 v3.0.1
-340
View File
@@ -1,340 +0,0 @@
package main
import (
"flag"
"fmt"
"gopkg.in/yaml.v3"
"log"
"net"
"os"
"path"
"strconv"
"strings"
"syscall"
"time"
)
// Build-time variables
var version = "dev"
var sysconfdir = "/etc/"
var runstatedir = "/var/run/"
var socket net.Conn
func main() {
// Set and parse flags
printVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
// Dial esvm socket
dialSocket()
defer socket.Close()
if flag.NArg() < 1 {
printUsage()
os.Exit(1)
}
if *printVersion || flag.Args()[0] == "version" {
fmt.Printf("Enit Control version %s\n", version)
return
} else if flag.Args()[0] == "help" {
printUsage()
return
} else if flag.Args()[0] == "shutdown" || flag.Args()[0] == "poweroff" || flag.Args()[0] == "halt" {
err := syscall.Kill(1, syscall.SIGUSR1)
if err != nil {
log.Fatalf("Could not send shutdown signal! Error: %s\n", err)
}
return
} else if flag.Args()[0] == "reboot" || flag.Args()[0] == "restart" || flag.Args()[0] == "reset" {
err := syscall.Kill(1, syscall.SIGTERM)
if err != nil {
log.Fatalf("Could not send shutdown signal! Error: %s\n", err)
}
return
} else if flag.Args()[0] == "service" || flag.Args()[0] == "sv" {
if len(flag.Args()) <= 1 {
fmt.Println("Usage: ectl service <start/stop/enable/disable/status/list> [service]")
return
} else if flag.Args()[1] == "list" {
if _, err := os.Stat(path.Join(runstatedir, "esvm")); err != nil {
log.Fatalf("Could not list services! Error: %s\n", err)
}
entries, err := os.ReadDir(path.Join(runstatedir, "esvm"))
if err != nil {
log.Fatalf("Could not list services! Error: %s\n", err)
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
state := getServiceState(entry.Name())
enabled := strconv.FormatBool(isServiceEnabled(entry.Name()))
enabled = strings.ToUpper(enabled[:1]) + strings.ToLower(enabled[1:])
fmt.Println("Service name: " + entry.Name())
fmt.Printf(" State: %s\n", state)
fmt.Printf(" Enabled: %s\n", enabled)
}
return
} else if len(flag.Args()) <= 2 {
fmt.Printf("Usage: ectl service %s <service>\n", flag.Args()[1])
return
} else if flag.Args()[1] == "start" {
if _, err := os.Stat(path.Join(runstatedir, "esvm", flag.Args()[2])); err != nil {
log.Fatalf("Could not start service! Error: %s\n", err)
}
_, err := socket.Write([]byte("start " + flag.Args()[2]))
if err != nil {
log.Fatalf("Could not start service! Error: %s\n", err)
}
buf := make([]byte, 1024)
n, err := socket.Read(buf)
if err != nil {
log.Fatalf("Could not start service! Error: %s\n", err)
}
if string(buf[:n]) != "ok" {
log.Fatalf("Could not start service! Error: expcted 'ok' got '%s'\n", string(buf))
}
fmt.Println("Service started successfully!")
return
} else if flag.Args()[1] == "stop" {
if _, err := os.Stat(path.Join(runstatedir, "esvm", flag.Args()[2])); err != nil {
log.Fatalf("Could not stop service! Error: %s\n", err)
}
_, err := socket.Write([]byte("stop " + flag.Args()[2]))
if err != nil {
log.Fatalf("Could not stop service! Error: %s\n", err)
}
buf := make([]byte, 1024)
n, err := socket.Read(buf)
if err != nil {
log.Fatalf("Could not stop service! Error: %s\n", err)
}
if string(buf[:n]) != "ok" {
log.Fatalf("Could not stop service! Error: expcted 'ok' got '%s'\n", string(buf))
}
fmt.Println("Service stopped successfully!")
return
} else if flag.Args()[1] == "restart" || flag.Args()[1] == "reload" {
if _, err := os.Stat(path.Join(runstatedir, "esvm", flag.Args()[2])); err != nil {
log.Fatalf("Could not restart service! Error: %s\n", err)
}
_, err := socket.Write([]byte("restart " + flag.Args()[2]))
if err != nil {
log.Fatalf("Could not restart service! Error: %s\n", err)
}
buf := make([]byte, 1024)
n, err := socket.Read(buf)
if err != nil {
log.Fatalf("Could not restart service! Error: %s\n", err)
}
if string(buf[:n]) != "ok" {
log.Fatalf("Could not restart service! Error: expcted 'ok' got '%s'\n", string(buf))
}
fmt.Println("Service restarted successfully!")
return
} else if flag.Args()[1] == "enable" {
// Check if service exists
found := false
entries, err := os.ReadDir(path.Join(sysconfdir, "esvm/services/"))
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
type minimalServiceStruct struct {
Name string `yaml:"name"`
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".esv") {
continue
}
bytes, err := os.ReadFile(path.Join(sysconfdir, "esvm/services", entry.Name()))
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
sv := minimalServiceStruct{Name: ""}
err = yaml.Unmarshal(bytes, &sv)
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
if sv.Name == flag.Args()[2] {
found = true
break
}
}
if !found {
log.Fatalf("Service does not exist!")
}
if _, err := os.Stat(path.Join(sysconfdir, "esvm/enabled_services")); err != nil {
err := os.WriteFile(path.Join(sysconfdir, "esvm/enabled_services"), []byte(flag.Args()[2]+"\n"), 0644)
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
return
}
file, err := os.ReadFile(path.Join(sysconfdir, "esvm/enabled_services"))
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
for _, line := range strings.Split(string(file), "\n") {
if strings.TrimSpace(line) == flag.Args()[2] {
fmt.Println("Service is already enabled!")
return
}
}
err = os.WriteFile(path.Join(sysconfdir, "esvm/enabled_services"), []byte(string(file)+flag.Args()[2]+"\n"), 0644)
if err != nil {
log.Fatalf("Could not enable service! Error: %s\n", err)
}
fmt.Printf("Service (%s) has been enabled!\n", flag.Args()[2])
return
} else if flag.Args()[1] == "disable" {
if _, err := os.Stat(path.Join(sysconfdir, "esvm/enabled_services")); err != nil {
fmt.Println("Service is already disabled!")
return
}
file, err := os.ReadFile(path.Join(sysconfdir, "esvm/enabled_services"))
if err != nil {
log.Fatalf("Could not disable service! Error: %s\n", err)
}
lines := strings.Split(string(file), "\n")
found := false
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if strings.TrimSpace(line) == flag.Args()[2] {
lines = append(lines[:i], lines[i+1:]...)
found = true
} else if strings.TrimSpace(line) == "" {
lines = append(lines[:i], lines[i+1:]...)
}
}
if !found {
fmt.Println("Service is already disabled!")
return
}
err = os.WriteFile(path.Join(sysconfdir, "esvm/enabled_services"), []byte(strings.Join(lines, "\n")+"\n"), 0644)
if err != nil {
log.Fatalf("Could not disable service! Error: %s\n", err)
}
fmt.Printf("Service (%s) has been disabled!\n", flag.Args()[2])
return
} else if flag.Args()[1] == "status" {
if _, err := os.Stat(path.Join(runstatedir, "esvm", flag.Args()[2])); err != nil {
log.Fatalf("Could not get service status! Error: %s\n", err)
}
state := getServiceState(flag.Args()[2])
enabled := strconv.FormatBool(isServiceEnabled(flag.Args()[2]))
enabled = strings.ToUpper(enabled[:1]) + strings.ToLower(enabled[1:])
fmt.Println("Service name: " + flag.Args()[2])
fmt.Printf(" State: %s\n", state)
fmt.Printf(" Enabled: %s\n", enabled)
return
}
}
printUsage()
os.Exit(1)
}
func getServiceState(serviceName string) string {
if _, err := os.Stat(path.Join(runstatedir, "esvm", serviceName)); err != nil {
return ""
}
var state uint64
bytes, err := os.ReadFile(path.Join(runstatedir, "esvm", serviceName, "state"))
if err != nil {
state = 0
}
state, err = strconv.ParseUint(string(bytes), 10, 8)
switch state {
case 1:
return "Unloaded"
case 2:
return "Running"
case 3:
return "Stopped"
case 4:
return "Crashed"
case 5:
return "Completed"
default:
return "Unknown"
}
}
func isServiceEnabled(serviceName string) bool {
if _, err := os.Stat(path.Join(sysconfdir, "esvm/enabled_services")); err != nil {
return false
}
file, err := os.ReadFile(path.Join(sysconfdir, "esvm/enabled_services"))
if err != nil {
return false
}
for _, line := range strings.Split(string(file), "\n") {
if strings.TrimSpace(line) == serviceName {
return true
}
}
return false
}
func printUsage() {
fmt.Println("Available sucommands:")
fmt.Println("ectl version | Show enit version")
fmt.Println("ectl shutdown/poweroff/halt | Shutdown the system")
fmt.Println("ectl reboot/restart | Reboot the system")
fmt.Println("ectl help | Show command explanations")
fmt.Println("ectl sv/service start <service> | Start a service")
fmt.Println("ectl sv/service stop <service> | Stop a service")
fmt.Println("ectl sv/service enable <service> | Enable a service at startup")
fmt.Println("ectl sv/service disable <service> | Disable a service at startup")
fmt.Println("ectl sv/service status <service> | Show service status")
fmt.Println("ectl sv/service list | Show all enabled services")
}
func dialSocket() {
if _, err := os.Stat(path.Join(runstatedir, "esvm/esvm.sock")); err != nil {
log.Fatalf("Could not find esvm.sock! Error: %s\n", err)
}
var err error
socket, err = net.Dial("unix", path.Join(runstatedir, "esvm/esvm.sock"))
if err != nil {
log.Fatalf("Failed to connect to esvm.sock! Error: %s\n", err)
}
if err := socket.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
log.Fatalf("Failed to set write deadline! Error: %s\n", err)
}
}
-5
View File
@@ -1,5 +0,0 @@
module enit
go 1.23.4
require golang.org/x/sys v0.31.0
-2
View File
@@ -1,2 +0,0 @@
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-157
View File
@@ -1,157 +0,0 @@
package main
import (
"fmt"
"golang.org/x/sys/unix"
"os"
"slices"
"strings"
"unsafe"
)
var flagsEquivalence = map[string]uintptr{
"dirsync": unix.MS_DIRSYNC,
"lazytime": unix.MS_LAZYTIME,
"noatime": unix.MS_NOATIME,
"nodev": unix.MS_NODEV,
"nodiratime": unix.MS_NODIRATIME,
"noexec": unix.MS_NOEXEC,
"nosuid": unix.MS_NOSUID,
"ro": unix.MS_RDONLY,
"rw": 0,
"relatime": unix.MS_RELATIME,
"silent": unix.MS_SILENT,
"strictatime": unix.MS_STRICTATIME,
"sync": unix.MS_SYNCHRONOUS,
"defaults": 0,
}
// Split string flags to mount flags and mount data
func convertMountOptions(options string) (flags []uintptr, data string) {
for _, flag := range strings.Split(options, ",") {
if unixFlag, ok := flagsEquivalence[flag]; ok {
flags = append(flags, unixFlag)
} else {
if data == "" {
data = flag
} else {
data += "," + flag
}
}
}
return flags, data
}
// Combine a unix flag slice or array into a single uintptr
func combineUnixFlags(flagsSlice []uintptr) (flags uintptr) {
flags = 0
for _, flag := range flagsSlice {
flags |= flag
}
return flags
}
// Check whether a certain path is a mountpoint
func isMountpoint(mountpoint string) bool {
if mountpoint != "/" {
mountpoint = strings.TrimRight(mountpoint, "/")
}
if _, err := os.Stat("/proc/mounts"); err != nil {
return false
}
bytes, err := os.ReadFile("/proc/mounts")
if err != nil {
return false
}
for _, line := range strings.Split(string(bytes), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.Split(line, " ")[1] == mountpoint {
return true
}
}
return false
}
func mount(source, target, fstype string, options string, mkdir bool) error {
flags, data := convertMountOptions(options)
if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) {
flags = append(flags, unix.MS_REMOUNT)
}
if mkdir {
err := os.MkdirAll(target, 0755)
if err != nil {
return err
}
}
if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil {
return err
}
return nil
}
func mountFstabEntries() error {
if _, err := os.Stat("/etc/fstab"); err != nil {
return err
}
bytes, err := os.ReadFile("/etc/fstab")
if err != nil {
return err
}
swapPriority := -2
for _, line := range strings.Split(string(bytes), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") || line == "" {
continue
}
source := strings.Split(line, " ")[0]
target := strings.Split(line, " ")[1]
fstype := strings.Split(line, " ")[2]
options := strings.Split(line, " ")[3]
flags, data := convertMountOptions(options)
if slices.Contains(strings.Split(data, ","), "noauto") {
continue
}
if fstype == "swap" {
b := append([]byte(source), 0)
const SwapFlagPrioShift = 0
const SwapFlagPrioMask = 0x7fff
_, _, err := unix.Syscall(unix.SYS_SWAPON, uintptr(unsafe.Pointer(&b[0])), uintptr((swapPriority<<SwapFlagPrioShift)&SwapFlagPrioMask), 0)
swapPriority--
if err != 0 {
return fmt.Errorf("swapon syscall returned none-zero error code: %d", err)
}
continue
}
if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) {
flags = append(flags, unix.MS_REMOUNT)
}
if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil {
return err
}
}
return nil
}
-644
View File
@@ -1,644 +0,0 @@
package main
import (
"flag"
"fmt"
"gopkg.in/yaml.v3"
"io"
"log"
"net"
"os"
"os/exec"
"os/signal"
"path"
"slices"
"strconv"
"strings"
"syscall"
"time"
)
type EnitServiceState uint8
const (
EnitServiceUnknown EnitServiceState = iota
EnitServiceUnloaded
EnitServiceRunning
EnitServiceStopped
EnitServiceCrashed
EnitServiceCompleted
)
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"`
LogOutput bool `yaml:"log_output,omitempty"`
ServiceRunPath string
restartCount int
stopChannel chan bool
}
// Build-time variables
var version = "dev"
var runtimeServiceDir string
var serviceConfigDir string
var Services = make([]EnitService, 0)
var EnabledServices = make([]string, 0)
var logger *log.Logger
var socket net.Listener
func main() {
// Setup main logger
err := setupESVMLogger()
if err != nil {
log.Printf("Could not setup main ESVM logger! Error: %s\n", err)
logger = log.Default()
}
// Parse flags
printVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *printVersion || flag.NArg() != 2 {
fmt.Printf("Enit Service Manager version %s\n", version)
os.Exit(0)
}
if os.Getppid() != 1 {
fmt.Println("Esvm must be run by PID 1!")
os.Exit(1)
}
// Set directory variables
runtimeServiceDir = flag.Arg(0)
serviceConfigDir = flag.Arg(1)
Init()
if err != nil {
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigc
Destroy()
os.Exit(0)
}()
for {
listenToSocket()
}
}
func setupESVMLogger() error {
err := os.MkdirAll("/var/log/esvm", 0755)
if err != nil {
return err
}
loggerFile, err := os.OpenFile("/var/log/esvm/esvm.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
logger = log.New(loggerFile, "[ESVM] ", log.Lshortfile|log.LstdFlags)
// Print an empty line as separator
_, err = loggerFile.WriteString("------ " + time.Now().Format(time.UnixDate) + " ------\n")
return nil
}
func Init() {
logger.Println("Initializing ESVM...")
if _, err := os.Stat(runtimeServiceDir); err == nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", fmt.Errorf("runtime service directory %s already exists", runtimeServiceDir))
}
err := os.MkdirAll(runtimeServiceDir, 0755)
if err != nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", err)
}
socket, err = net.Listen("unix", path.Join(runtimeServiceDir, "esvm.sock"))
if err != nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", err)
}
if stat, err := os.Stat(serviceConfigDir); err != nil || !stat.IsDir() {
logger.Println("ESVM initialized successfully!")
return
}
dirEntries, err := os.ReadDir(path.Join(serviceConfigDir, "services"))
if err != nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", err)
}
// Read and initialize service files
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".esv") {
logger.Printf("Initializing service (%s)...\n", entry.Name())
bytes, err := os.ReadFile(path.Join(serviceConfigDir, "services", entry.Name()))
if err != nil {
logger.Printf("Could not read service file at %s!\n", path.Join(serviceConfigDir, "services", entry.Name()))
continue
}
service := EnitService{
Name: "",
Description: "",
Dependencies: make([]string, 0),
Type: "",
StartCmd: "",
ExitMethod: "",
StopCmd: "",
Restart: "",
CrashOnSafeExit: true,
ServiceRunPath: "",
restartCount: 0,
stopChannel: make(chan bool),
LogOutput: true,
}
if err := yaml.Unmarshal(bytes, &service); err != nil {
logger.Printf("Could not read service file at %s!\n", path.Join(serviceConfigDir, "services", entry.Name()))
continue
}
for _, sv := range Services {
if sv.Name == service.Name {
logger.Printf("Service with name (%s) has already been initialized!", service.Name)
}
}
switch service.Type {
case "simple", "background":
default:
logger.Printf("Unknown service type: %s\n", service.Type)
continue
}
switch service.ExitMethod {
case "stop_command", "kill":
default:
logger.Printf("Unknown exit method: %s\n", service.ExitMethod)
continue
}
switch service.Restart {
case "true", "always":
default:
service.Restart = "false"
}
service.ServiceRunPath = path.Join(runtimeServiceDir, service.Name)
err = os.MkdirAll(path.Join(service.ServiceRunPath), 0755)
if err != nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", err)
}
err = service.setCurrentState(EnitServiceUnloaded)
if err != nil {
logger.Fatalf("Could not initialize ESVM! Error: %s", err)
}
Services = append(Services, service)
logger.Printf("Service (%s) has been initialized!\n", service.Name)
}
}
// Get enabled services
if _, err := os.Stat(path.Join(serviceConfigDir, "enabled_services")); err == nil {
file, err := os.ReadFile(path.Join(serviceConfigDir, "enabled_services"))
if err != nil {
return
}
for _, line := range strings.Split(string(file), "\n") {
if line != "" {
EnabledServices = append(EnabledServices, line)
}
}
}
// Get enabled services that meet their dependencies
servicesWithMetDepends := make([]EnitService, 0)
for _, service := range Services {
if slices.Contains(EnabledServices, service.Name) && len(service.GetUnmetDependencies()) == 0 {
servicesWithMetDepends = append(servicesWithMetDepends, service)
}
}
// Loop until all enabled services have started or timed out
for start := time.Now(); time.Since(start) < 60*time.Second; {
if len(servicesWithMetDepends) == 0 {
break
}
for i := len(servicesWithMetDepends) - 1; i >= 0; i-- {
service := servicesWithMetDepends[i]
canStart := true
for _, dependency := range service.Dependencies {
if strings.HasPrefix(dependency, "/") {
// File dependency
if _, err := os.Stat(dependency); err != nil {
canStart = false
break
}
} else {
// Service dependency
if GetServiceByName(dependency).GetCurrentState() != EnitServiceRunning && GetServiceByName(dependency).GetCurrentState() != EnitServiceCompleted {
canStart = false
break
}
}
}
if canStart {
err := service.StartService()
if err != nil {
logger.Printf("Could not start service (%s)! Error: %s", service.Name, err)
}
servicesWithMetDepends = append(servicesWithMetDepends[:i], servicesWithMetDepends[i+1:]...)
}
}
}
if len(servicesWithMetDepends) > 0 {
for _, service := range servicesWithMetDepends {
logger.Printf("Could not start service (%s)! Error: dependencies not met", service.Name)
}
}
logger.Println("ESVM initialized successfully!")
}
func Destroy() {
logger.Println("Stopping all ESVM services...")
for _, service := range Services {
if err := service.StopService(); err != nil {
logger.Printf("Error stopping service %s! Error: %s\n", service.Name, err)
}
}
logger.Println("All ESVM services have stopped!")
}
func GetServiceByName(name string) *EnitService {
for _, service := range Services {
if service.Name == name {
return &service
}
}
return nil
}
func (service *EnitService) GetUnmetDependencies() (missingDependencies []string) {
for _, dependency := range service.Dependencies {
if strings.HasPrefix(dependency, "/") {
// File dependency
if _, err := os.Stat(dependency); err != nil {
missingDependencies = append(missingDependencies, dependency)
}
} else {
// Service dependency
depService := GetServiceByName(dependency)
if depService == nil {
missingDependencies = append(missingDependencies, dependency)
}
}
}
return missingDependencies
}
func (service *EnitService) GetProcess() *os.Process {
bytes, err := os.ReadFile(path.Join(service.ServiceRunPath, "process"))
if err != nil {
return nil
}
pid, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
if err != nil {
return nil
}
process, err := os.FindProcess(pid)
if err != nil {
return nil
}
return process
}
func (service *EnitService) setProcessID(pid int) error {
if err := os.WriteFile(path.Join(service.ServiceRunPath, "process"), []byte(strconv.Itoa(pid)), 0644); err != nil {
return err
}
return nil
}
func (service *EnitService) GetCurrentState() EnitServiceState {
bytes, err := os.ReadFile(path.Join(service.ServiceRunPath, "state"))
if err != nil {
return EnitServiceUnknown
}
state, err := strconv.Atoi(strings.TrimSpace(string(bytes)))
if err != nil {
return EnitServiceUnknown
}
return EnitServiceState(state)
}
func (service *EnitService) setCurrentState(state EnitServiceState) error {
if err := os.WriteFile(path.Join(service.ServiceRunPath, "state"), []byte(strconv.Itoa(int(state))), 0644); err != nil {
return err
}
return nil
}
func (service *EnitService) GetLogFile() (file *os.File, err error) {
err = os.MkdirAll(path.Join("/var/log/esvm/"), 0755)
if err != nil {
return nil, err
}
file, err = os.OpenFile(path.Join("/var/log/esvm/", service.Name+".log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return nil, err
}
_, err = file.WriteString("------ " + time.Now().Format(time.UnixDate) + " ------\n")
if err != nil {
file.Close()
return nil, err
}
return file, nil
}
func (service *EnitService) StartService() error {
if service == nil {
return nil
}
if service.GetCurrentState() == EnitServiceRunning {
return nil
}
logger.Printf("Starting service (%s)...\n", service.Name)
// Get log file if service logs output
var logFile *os.File
if service.LogOutput {
var err error
logFile, err = service.GetLogFile()
if err != nil {
return err
}
}
cmd := exec.Command("/bin/sh", "-c", "exec "+service.StartCmd)
if logFile != nil {
cmd.Stdout = logFile
cmd.Stderr = logFile
}
if err := cmd.Start(); err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
err := service.setProcessID(cmd.Process.Pid)
if err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
err = service.setCurrentState(EnitServiceRunning)
if err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
go func() {
err := cmd.Wait()
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
select {
case <-service.stopChannel:
service.restartCount = 0
_ = service.setCurrentState(EnitServiceStopped)
default:
if service.Type == "simple" && err == nil {
service.restartCount = 0
_ = service.setCurrentState(EnitServiceCompleted)
return
}
if !service.CrashOnSafeExit {
logger.Printf("Service (%s) has exited\n", service.Name)
_ = service.setCurrentState(EnitServiceStopped)
} else {
logger.Printf("Service (%s) has crashed!\n", service.Name)
_ = service.setCurrentState(EnitServiceCrashed)
}
if service.Restart == "always" {
_ = service.StartService()
} else if service.Restart == "true" && service.restartCount < 5 {
service.restartCount++
_ = service.StartService()
}
}
}()
logger.Printf("Service (%s) has started!\n", service.Name)
return nil
}
func (service *EnitService) StopService() error {
if service.GetCurrentState() != EnitServiceRunning {
return nil
}
logger.Printf("Stopping service (%s)...\n", service.Name)
if service.ExitMethod == "kill" {
process := service.GetProcess()
if err := process.Signal(syscall.Signal(0)); err != nil {
logger.Printf("Service (%s) has stopped. (Process already dead)\n", service.Name)
return nil
}
go func() { service.stopChannel <- true }()
err := service.GetProcess().Signal(syscall.SIGTERM)
if err != nil {
return err
}
exit := false
for timeout := time.After(5 * time.Second); ; {
if exit {
break
}
select {
case <-timeout:
logger.Println("Process took too long to finish. Forcefully killing process...")
err := service.GetProcess().Kill()
if err != nil {
return err
}
exit = true
default:
if process == nil {
exit = true
break
}
err = process.Signal(syscall.Signal(0))
if err != nil {
exit = true
}
}
}
} else {
cmd := exec.Command("/bin/sh", "-c", service.StopCmd)
if err := cmd.Run(); err != nil {
return err
}
}
err := service.setCurrentState(EnitServiceStopped)
if err != nil {
return err
}
err = service.setProcessID(0)
if err != nil {
return err
}
logger.Printf("Service (%s) has stopped!\n", service.Name)
return nil
}
func (service *EnitService) RestartService() error {
if err := service.StopService(); err != nil {
return err
}
if err := service.StartService(); err != nil {
return err
}
return nil
}
func listenToSocket() {
conn, err := socket.Accept()
if err != nil {
logger.Println("Could not accept socket connection!")
panic(err)
}
// Handle the connection in a separate goroutine.
go func(conn net.Conn) {
defer conn.Close()
// Create a buffer for incoming data.
buf := make([]byte, 4096)
// Read data from the connection.
n, err := conn.Read(buf)
if err == io.EOF {
return
}
if err != nil {
logger.Fatal(err)
}
command := string(buf[:n])
commandSplit := strings.Split(command, " ")
if len(commandSplit) >= 2 {
if commandSplit[0] == "start" {
service := GetServiceByName(commandSplit[1])
if service == nil {
_, err := conn.Write([]byte("service not found"))
if err != nil {
return
}
}
if err := service.StartService(); err != nil {
_, err := conn.Write([]byte("could not start service"))
if err != nil {
return
}
}
_, err := conn.Write([]byte("ok"))
if err != nil {
return
}
} else if commandSplit[0] == "stop" {
service := GetServiceByName(commandSplit[1])
if service == nil {
_, err := conn.Write([]byte("service not found"))
if err != nil {
return
}
}
if err := service.StopService(); err != nil {
_, err := conn.Write([]byte("could not stop service"))
if err != nil {
return
}
}
_, err := conn.Write([]byte("ok"))
if err != nil {
return
}
} else if commandSplit[0] == "restart" {
service := GetServiceByName(commandSplit[1])
if service == nil {
_, err := conn.Write([]byte("service not found"))
if err != nil {
return
}
}
if err := service.RestartService(); err != nil {
_, err := conn.Write([]byte("could not restart service"))
if err != nil {
return
}
}
_, err := conn.Write([]byte("ok"))
if err != nil {
return
}
}
}
}(conn)
}
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty --noclear tty1
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty tty2
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty tty3
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty tty4
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty tty5
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1
View File
@@ -5,3 +5,4 @@ start_cmd: /usr/bin/setsid /sbin/agetty tty6
exit_method: kill exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
-1
View File
@@ -1,6 +1,5 @@
name: lo-interface name: lo-interface
description: Enable loopback interface on boot description: Enable loopback interface on boot
dependencies: ["/usr/sbin/ip"]
type: simple type: simple
start_cmd: ip link set lo up start_cmd: ip link set lo up
exit_method: kill exit_method: kill
+8
View File
@@ -0,0 +1,8 @@
module ectl
go 1.23.4
require (
github.com/spf13/pflag v1.0.10
gopkg.in/yaml.v3 v3.0.1
)
+2
View File
@@ -1,3 +1,5 @@
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+54
View File
@@ -0,0 +1,54 @@
package main
import (
"fmt"
"log"
"os"
"syscall"
)
// Build-time variables
var version = "dev"
var sysconfdir = "/etc/"
var runstatedir = "/var/run/"
func main() {
// Show usage if no arguments specified
if len(os.Args) == 1 {
printUsage()
return
}
subcommand := os.Args[1]
switch subcommand {
case "v", "version":
fmt.Printf("Enit Control version %s\n", version)
case "shutdown", "poweroff", "halt":
err := syscall.Kill(1, syscall.SIGUSR1)
if err != nil {
log.Fatalf("Could not send shutdown signal! Error: %s\n", err)
}
case "reboot", "restart", "reset":
err := syscall.Kill(1, syscall.SIGTERM)
if err != nil {
log.Fatalf("Could not send reboot signal! Error: %s\n", err)
}
case "sv", "service":
handleServiceSubcommand()
default:
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Println("Usage: ectl <subcommand> [options]")
fmt.Println("Description: Shutdown, reboot and manage system services")
fmt.Println("Sucommands:")
fmt.Println(" v, version Show enit version")
fmt.Println(" shutdown, poweroff, halt Shutdown the system")
fmt.Println(" reboot, restart, reset Reboot the system")
fmt.Println(" sv, service Manage system services")
}
+476
View File
@@ -0,0 +1,476 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net"
"os"
"path"
"strconv"
"strings"
"time"
flag "github.com/spf13/pflag"
)
var currentFlagSet *flag.FlagSet
var conn net.Conn
func handleServiceSubcommand() {
if len(os.Args) == 2 {
printSvUsage()
return
}
subcommand := os.Args[2]
switch subcommand {
case "start", "stop", "restart":
// Setup flags and help
currentFlagSet = flag.NewFlagSet(subcommand, flag.ExitOnError)
currentFlagSet.BoolP("json", "j", false, "Return output in json format")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s %s <options> <service>", os.Args[1], subcommand), fmt.Sprintf("%s the specified service", strings.Title(subcommand)), os.Args[3:])
// Dial esvm socket
dialSocket()
defer conn.Close()
startStopRestartService(subcommand)
case "enable", "disable":
// Setup flags and help
currentFlagSet = flag.NewFlagSet(subcommand, flag.ExitOnError)
currentFlagSet.BoolP("json", "j", false, "Return output in json format")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s %s <options> <service>", os.Args[1], subcommand), fmt.Sprintf("%s the specified service", strings.Title(subcommand)), os.Args[3:])
enableDisableService(subcommand)
case "status":
// Setup flags and help
currentFlagSet = flag.NewFlagSet("status", flag.ExitOnError)
currentFlagSet.BoolP("json", "j", false, "Return output in json format")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s status <options> <service>", os.Args[1]), "Show service status", os.Args[3:])
// Dial esvm socket
dialSocket()
defer conn.Close()
showServiceStatus()
case "list":
// Setup flags and help
currentFlagSet = flag.NewFlagSet("list", flag.ExitOnError)
currentFlagSet.BoolP("json", "j", false, "Return output in json format")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "List all services", os.Args[3:])
// Dial esvm socket
dialSocket()
defer conn.Close()
listAllServices()
case "reload":
// Setup flags and help
currentFlagSet = flag.NewFlagSet("reload", flag.ExitOnError)
currentFlagSet.BoolP("json", "j", false, "Return output in json format")
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "Reload all services", os.Args[3:])
// Dial esvm socket
dialSocket()
defer conn.Close()
reloadAllServices()
default:
printSvUsage()
os.Exit(1)
}
}
func startStopRestartService(subcommand string) {
// Get flags
printJson, _ := currentFlagSet.GetBool("json")
// Ensure service name argument has been set
if currentFlagSet.NArg() == 0 {
fmt.Printf("Usage: ectl service %s <service>\n", subcommand)
return
}
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: subcommand,
Service: currentFlagSet.Arg(0),
}
// 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)
}
// Read data from the connection.
data, err := readAllConn(conn)
if err != nil {
log.Fatalf("Could not read data from socket! Error: %s\n", err)
return
}
// Print json data if flag is set
if printJson {
fmt.Println(string(data))
return
}
// Decoode JSON data
var returnedJsonData map[string]any
err = json.Unmarshal(data, &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!")
}
}
func enableDisableService(subcommand string) {
// Get flags
printJson, _ := currentFlagSet.GetBool("json")
// Ensure service name argument has been set
if currentFlagSet.NArg() == 0 {
fmt.Printf("Usage: ectl service %s <service> [stage]\n", subcommand)
return
}
service := currentFlagSet.Arg(0)
// Get service stage
stage := 3
if subcommand == "disable" {
stage = 0
} else if len(currentFlagSet.Args()) > 1 {
flagStr := currentFlagSet.Arg(1)
_stage, err := strconv.ParseInt(flagStr, 10, 32)
if err != nil {
log.Fatalf("Error: could not parse stage number: %s", err)
}
stage = int(_stage)
}
verb := "enabled"
if stage == 0 {
verb = "disabled"
}
// Return if service is already enabled
if _, enabledStage := isServiceEnabled(service); enabledStage == stage {
if printJson {
fmt.Printf("{\"success\":\"Service (%s) is already %s\"}\n", service, verb)
} else {
fmt.Printf("Service (%s) is already %s\n", service, verb)
}
return
}
// Enable service
err := setServiceEnabled(service, stage)
if err != nil {
verb := "enable"
if stage == 0 {
verb = "disable"
}
if printJson {
fmt.Printf("{\"error\":\"Could not %s service! Error: %s\"}\n", verb, err)
} else {
fmt.Printf("Could not %s service! Error: %s\n", verb, err)
}
os.Exit(1)
}
if printJson {
fmt.Printf("{\"success\":\"Service (%s) was %s sucessfully\"}\n", service, verb)
return
} else {
fmt.Printf("Service (%s) was %s sucessfully\n", service, verb)
}
}
func showServiceStatus() {
// Get flags
printJson, _ := currentFlagSet.GetBool("json")
// Ensure service name argument has been set
if len(currentFlagSet.Args()) == 0 {
fmt.Println("Usage: ectl service status <service>")
return
}
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: "status",
Service: currentFlagSet.Arg(0),
}
// 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)
}
// Read data from the connection.
data, err := readAllConn(conn)
if err != nil {
log.Fatalf("Could not read data from socket! Error: %s\n", err)
return
}
// Decoode JSON data
var returnedJsonData map[string]any
err = json.Unmarshal(data, &returnedJsonData)
if err != nil {
log.Fatalf("Could not decode JSON data from connection!")
}
if err, ok := returnedJsonData["error"]; ok {
if printJson {
fmt.Println(string(data))
os.Exit(1)
} else {
log.Fatal(err)
}
}
// Set is_enabled and stage fields in json data
returnedJsonData["is_enabled"], returnedJsonData["stage"] = isServiceEnabled(currentFlagSet.Arg(0))
// Print json data if flag is set
if printJson {
data, _ = json.Marshal(returnedJsonData)
fmt.Println(string(data))
return
}
serviceState := returnedJsonData["state"].(string)
serviceDescription := returnedJsonData["description"].(string)
serviceEnabled := returnedJsonData["is_enabled"].(bool)
serviceStage := returnedJsonData["stage"].(int)
processID := int(returnedJsonData["process_id"].(float64))
fmt.Printf("Name: %s\n", currentFlagSet.Arg(0))
fmt.Printf("Description: %s\n", serviceDescription)
fmt.Printf("State: %s\n", serviceState)
if serviceEnabled {
fmt.Printf("Enabled: %t (Stage %d)\n", serviceEnabled, serviceStage)
} else {
fmt.Printf("Enabled: %t\n", serviceEnabled)
}
if serviceState == "running" {
fmt.Printf("Process ID: %d\n", processID)
}
}
func listAllServices() {
// Get flags
printJson, _ := currentFlagSet.GetBool("json")
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: "list",
}
// 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)
}
// Read data from the connection.
data, err := readAllConn(conn)
if err != nil {
log.Fatalf("Could not read data from socket! Error: %s\n", err)
return
}
// Print json data if flag is set
if printJson {
fmt.Println(string(data))
return
}
// Decoode JSON data
var returnedJsonData map[string]any
err = json.Unmarshal(data, &returnedJsonData)
if err != nil {
log.Fatalf("Could not decode JSON data from connection!")
}
if err, ok := returnedJsonData["error"]; ok {
log.Fatal(err)
}
for _, serviceMap := range returnedJsonData["services"].([]any) {
serviceName := serviceMap.(map[string]any)["name"].(string)
serviceDescription := serviceMap.(map[string]any)["description"].(string)
serviceState := serviceMap.(map[string]any)["state"].(string)
serviceEnabled := serviceMap.(map[string]any)["is_enabled"].(bool)
serviceStage := int(serviceMap.(map[string]any)["stage"].(float64))
processID := int(serviceMap.(map[string]any)["process_id"].(float64))
fmt.Printf("Name: %s\n", serviceName)
fmt.Printf("Description: %s\n", serviceDescription)
fmt.Printf("State: %s\n", serviceState)
if serviceEnabled {
fmt.Printf("Enabled: %t (Stage %d)\n", serviceEnabled, serviceStage)
} else {
fmt.Printf("Enabled: %t\n", serviceEnabled)
}
if serviceState == "running" {
fmt.Printf("Process ID: %d\n", processID)
}
fmt.Println()
}
}
func reloadAllServices() {
// Get flags
printJson, _ := currentFlagSet.GetBool("json")
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: "reload",
}
// 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)
}
// Read data from the connection.
data, err := readAllConn(conn)
if err != nil {
log.Fatalf("Could not read data from socket! Error: %s\n", err)
return
}
// Print json data if flag is set
if printJson {
fmt.Println(string(data))
return
}
// Decoode JSON data
var returnedJsonData map[string]any
err = json.Unmarshal(data, &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!")
}
}
func printSvUsage() {
fmt.Printf("Usage: ectl %s <subcommand> [options] [service]\n", os.Args[1])
fmt.Println("Description: Manage system services")
fmt.Println("Sucommands:")
fmt.Println(" start Start service")
fmt.Println(" stop Stop service")
fmt.Println(" restart Restart service")
fmt.Println(" enable Enable service")
fmt.Println(" disable Disable service")
fmt.Println(" status Show service status")
fmt.Println(" list List services")
fmt.Println(" reload Reload services")
}
func setupFlagsAndHelp(flagset *flag.FlagSet, usage, desc string, args []string) {
flagset.Usage = func() {
fmt.Println("Usage: " + usage)
fmt.Println("Description: " + desc)
fmt.Println("Options:")
if !flagset.HasFlags() {
fmt.Println(" No flags defined")
}
flagset.PrintDefaults()
}
flagset.Parse(args)
}
func dialSocket() {
if _, err := os.Stat(path.Join(runstatedir, "esvm/esvm.sock")); err != nil {
log.Fatalf("Could not find esvm.sock! Error: %s\n", err)
}
var err error
conn, err = net.Dial("unix", path.Join(runstatedir, "esvm/esvm.sock"))
if err != nil {
log.Fatalf("Failed to connect to esvm.sock! Error: %s\n", err)
}
if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
log.Fatalf("Failed to set write deadline! Error: %s\n", err)
}
}
func readAllConn(conn net.Conn) ([]byte, error) {
var buf bytes.Buffer
for {
dataChunk := make([]byte, 1024)
n, err := conn.Read(dataChunk)
if err != nil && err != io.EOF {
return nil, err
}
buf.Write(dataChunk[:n])
if n < 1024 {
break
}
}
return buf.Bytes(), nil
}
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"os"
"path"
"slices"
"strings"
"gopkg.in/yaml.v3"
)
func isServiceEnabled(service string) (bool, int) {
for stage, services := range readEnabledServices() {
if slices.Contains(services, service) {
return true, stage
}
}
return false, 0
}
func setServiceEnabled(service string, stage int) error {
// Get current service enabled status
_, s := isServiceEnabled(service)
// Return if service is already in correct state
if s == stage {
return nil
}
EnabledServices := readEnabledServices()
// Remove service from current stage
EnabledServices[s] = slices.DeleteFunc(EnabledServices[s], func(name string) bool {
return name == service
})
if len(EnabledServices[s]) == 0 {
delete(EnabledServices, s)
}
// Add service to stage
if stage != 0 {
EnabledServices[stage] = append(EnabledServices[stage], service)
}
// Save enabled services to file
data, err := yaml.Marshal(EnabledServices)
if err != nil {
return err
}
err = os.WriteFile(path.Join(sysconfdir, "esvm/enabled-services.yml"), data, 0644)
if err != nil {
return err
}
return nil
}
func readEnabledServices() (EnabledServices map[int][]string) {
EnabledServices = make(map[int][]string)
data, err := os.ReadFile(path.Join(sysconfdir, "esvm/enabled-services.yml"))
if err != nil {
return EnabledServices
}
err = yaml.Unmarshal(data, &EnabledServices)
if err != nil {
// Assume old plain text format
for _, service := range strings.Split(strings.TrimSpace(string(data)), "\n") {
EnabledServices[3] = append(EnabledServices[3], service)
}
// Update enabled-services.yml file
data, err := yaml.Marshal(EnabledServices)
if err != nil {
return EnabledServices
}
err = os.WriteFile(path.Join(sysconfdir, "esvm/enabled-services.yml"), data, 0644)
if err != nil {
return EnabledServices
}
return EnabledServices
}
return EnabledServices
}
+8
View File
@@ -0,0 +1,8 @@
module enit
go 1.23.4
require (
github.com/mitchellh/go-ps v1.0.0
golang.org/x/sys v0.31.0
)
+4
View File
@@ -0,0 +1,4 @@
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+91 -43
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"bufio"
"flag" "flag"
"fmt" "fmt"
"log" "log"
@@ -12,6 +13,8 @@ import (
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
"github.com/mitchellh/go-ps"
) )
// Build-time variables // Build-time variables
@@ -78,42 +81,42 @@ func mountVirtualFilesystems() {
// Mount /proc // Mount /proc
if err := mount("proc", "/proc", "proc", commonOptions+",nodev,noexec", false); err != nil { if err := mount("proc", "/proc", "proc", commonOptions+",nodev,noexec", false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /proc: %s", err)
} }
// Mount /sys // Mount /sys
if err := mount("sys", "/sys", "sysfs", commonOptions+",nodev,noexec", false); err != nil { if err := mount("sys", "/sys", "sysfs", commonOptions+",nodev,noexec", false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /sys: %s", err)
} }
// Mount /dev // Mount /dev
if err := mount("dev", "/dev", "devtmpfs", commonOptions+",mode=755,inode64", false); err != nil { if err := mount("dev", "/dev", "devtmpfs", commonOptions+",mode=755,inode64", false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /dev: %s", err)
} }
// Mount /run // Mount /run
if err := mount("run", "/run", "tmpfs", commonOptions+",nodev,mode=755,inode64", false); err != nil { if err := mount("run", "/run", "tmpfs", commonOptions+",nodev,mode=755,inode64", false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /run: %s", err)
} }
// Mount /dev/pts // Mount /dev/pts
if err := mount("devpts", "/dev/pts", "devpts", commonOptions+",gid=5,mode=620,ptmxmode=000", true); err != nil { if err := mount("devpts", "/dev/pts", "devpts", commonOptions+",gid=5,mode=620,ptmxmode=000", true); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /dev/pts: %s", err)
} }
// Mount /dev/shm // Mount /dev/shm
if err := mount("shm", "/dev/shm", "tmpfs", commonOptions+",nodev,inode64", true); err != nil { if err := mount("shm", "/dev/shm", "tmpfs", commonOptions+",nodev,inode64", true); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /dev/shm: %s", err)
} }
// Mount securityfs // Mount securityfs
if err := mount("securityfs", "/sys/kernel/security", "securityfs", commonOptions, false); err != nil { if err := mount("securityfs", "/sys/kernel/security", "securityfs", commonOptions, false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /sys/kernel/security: %s", err)
} }
// Mount cgroups v2 // Mount cgroups v2
if err := mount("cgroup2", "/sys/fs/cgroup", "cgroup2", commonOptions+",noexec,nsdelegate,memory_recursiveprot", false); err != nil { if err := mount("cgroup2", "/sys/fs/cgroup", "cgroup2", commonOptions+",noexec,nsdelegate,memory_recursiveprot", false); err != nil {
panic(err) printErrorAndReboot("Error: could not mount /sys/fs/cgroup: %s", err)
} }
fmt.Println("Done.") fmt.Println("Done.")
@@ -122,9 +125,8 @@ func mountVirtualFilesystems() {
func mountFilesystems() { func mountFilesystems() {
fmt.Print("Mounting fstab entries... ") fmt.Print("Mounting fstab entries... ")
if err := mountFstabEntries(); err != nil { if err, line := mountFstabEntries(); err != nil {
log.Println("Could not mount fstab entries!") printErrorAndReboot("Error: could not mount fstab entry on line %d: %s", line, err)
panic(err)
} }
fmt.Println("Done.") fmt.Println("Done.")
@@ -138,8 +140,7 @@ func startServiceManager() {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Start() err := cmd.Start()
if err != nil { if err != nil {
log.Println("Could not initialize service manager!") printErrorAndReboot("Error: could not initialize service manager: %s", err)
panic(err)
} }
serviceManagerPid = cmd.Process.Pid serviceManagerPid = cmd.Process.Pid
@@ -149,37 +150,73 @@ func startServiceManager() {
func stopServiceManager() { func stopServiceManager() {
fmt.Println("Stopping service manager... ") fmt.Println("Stopping service manager... ")
err := syscall.Kill(serviceManagerPid, syscall.SIGTERM) process, _ := os.FindProcess(serviceManagerPid)
if err != nil {
// Send SIGTERM signal to service manager
if err := process.Signal(syscall.SIGTERM); err != nil {
log.Println("Could not stop service manager!") log.Println("Could not stop service manager!")
syscall.Kill(serviceManagerPid, syscall.SIGKILL)
return
} }
// Check if service manager has stopped gracefully, otherwise send sigkill on timeout // Check if service manager has stopped gracefully, otherwise send sigkill on timeout
exit := false exited := make(chan bool)
for timeout := time.After(60 * time.Second); ; { go func() {
if exit { for {
break if err := process.Signal(syscall.Signal(0)); err != nil {
}
select {
case <-timeout:
log.Println("Could not stop service manager!")
err := syscall.Kill(serviceManagerPid, syscall.SIGKILL)
if err != nil {
log.Println("Could not stop service manager!")
}
exit = true
default:
waitZombieProcesses()
p, err := os.FindProcess(serviceManagerPid)
if err != nil {
exit = true
break break
} }
err = p.Signal(syscall.Signal(0))
if err != nil {
exit = true
}
} }
exited <- true
}()
for {
select {
case <-exited:
fmt.Println("Done.")
return
case <-time.After(300 * time.Second):
log.Println("Could not stop service manager!")
syscall.Kill(serviceManagerPid, syscall.SIGKILL)
return
default:
waitZombieProcesses()
}
}
}
func killProcesses() {
fmt.Print("Killing processes... ")
// Send sigterm to all processes
processes, err := ps.Processes()
if err != nil {
return
}
for _, process := range processes {
sid, _, _ := syscall.Syscall(syscall.SYS_GETSID, uintptr(process.Pid()), 0, 0)
if process.Pid() == 1 || sid == 1 {
continue
}
syscall.Kill(process.Pid(), syscall.SIGTERM)
}
time.Sleep(1 * time.Second)
// Send sigkill to remaining processes
processes, err = ps.Processes()
if err != nil {
return
}
for _, process := range processes {
sid, _, _ := syscall.Syscall(syscall.SYS_GETSID, uintptr(process.Pid()), 0, 0)
if process.Pid() == 1 || sid == 1 {
continue
}
syscall.Kill(process.Pid(), syscall.SIGKILL)
} }
fmt.Println("Done.") fmt.Println("Done.")
@@ -215,16 +252,12 @@ func waitZombieProcesses() {
func catchSignals() { func catchSignals() {
sigc := make(chan os.Signal, 1) sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGUSR1, syscall.SIGTERM, syscall.SIGINT, syscall.SIGCHLD) signal.Notify(sigc, syscall.SIGUSR1, syscall.SIGTERM, syscall.SIGINT, syscall.SIGCHLD)
defer close(sigc)
defer signal.Stop(sigc)
for { for {
switch <-sigc { switch <-sigc {
case syscall.SIGUSR1: case syscall.SIGUSR1:
close(sigc)
signal.Stop(sigc) signal.Stop(sigc)
shutdownSystem() shutdownSystem()
case syscall.SIGTERM, syscall.SIGINT: case syscall.SIGTERM, syscall.SIGINT:
close(sigc)
signal.Stop(sigc) signal.Stop(sigc)
rebootSystem() rebootSystem()
case syscall.SIGCHLD: case syscall.SIGCHLD:
@@ -237,9 +270,13 @@ func shutdownSystem() {
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
stopServiceManager() stopServiceManager()
killProcesses()
unmountFilesystems()
remountRootReadonly()
fmt.Println("Syncing disks...") fmt.Print("Syncing disks... ")
syscall.Sync() syscall.Sync()
fmt.Println("Done.")
fmt.Println("Sending shutdown syscall...") fmt.Println("Sending shutdown syscall...")
err := syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF) err := syscall.Reboot(syscall.LINUX_REBOOT_CMD_POWER_OFF)
@@ -252,9 +289,13 @@ func rebootSystem() {
fmt.Println("Rebooting...") fmt.Println("Rebooting...")
stopServiceManager() stopServiceManager()
killProcesses()
unmountFilesystems()
remountRootReadonly()
fmt.Println("Syncing disks...") fmt.Print("Syncing disks... ")
syscall.Sync() syscall.Sync()
fmt.Println("Done.")
fmt.Println("Sending reboot syscall...") fmt.Println("Sending reboot syscall...")
err := syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART) err := syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART)
@@ -262,3 +303,10 @@ func rebootSystem() {
panic(err) panic(err)
} }
} }
func printErrorAndReboot(format string, v ...any) {
log.Printf(format, v...)
fmt.Println("Press 'Enter' to reboot...")
bufio.NewReader(os.Stdin).ReadBytes('\n')
rebootSystem()
}
+341
View File
@@ -0,0 +1,341 @@
package main
import (
"errors"
"fmt"
"log"
"os"
"slices"
"strings"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/unix"
)
var flagsEquivalence = map[string]uintptr{
"dirsync": unix.MS_DIRSYNC,
"lazytime": unix.MS_LAZYTIME,
"noatime": unix.MS_NOATIME,
"nodev": unix.MS_NODEV,
"nodiratime": unix.MS_NODIRATIME,
"noexec": unix.MS_NOEXEC,
"nosuid": unix.MS_NOSUID,
"ro": unix.MS_RDONLY,
"rw": 0,
"relatime": unix.MS_RELATIME,
"silent": unix.MS_SILENT,
"strictatime": unix.MS_STRICTATIME,
"sync": unix.MS_SYNCHRONOUS,
"defaults": 0,
}
// Split string flags to mount flags and mount data
func convertMountOptions(options string) (flags []uintptr, data string, extra []string) {
for _, flag := range strings.Split(options, ",") {
if unixFlag, ok := flagsEquivalence[flag]; ok {
flags = append(flags, unixFlag)
} else {
if flag == "noauto" || flag == "nofail" {
extra = append(extra, flag)
} else if data == "" {
data = flag
} else {
data += "," + flag
}
}
}
return flags, data, extra
}
// Combine a unix flag slice or array into a single uintptr
func combineUnixFlags(flagsSlice []uintptr) (flags uintptr) {
flags = 0
for _, flag := range flagsSlice {
flags |= flag
}
return flags
}
// Check whether a certain path is a mountpoint
func isMountpoint(mountpoint string) bool {
if mountpoint != "/" {
mountpoint = strings.TrimRight(mountpoint, "/")
}
if _, err := os.Stat("/proc/mounts"); err != nil {
return false
}
bytes, err := os.ReadFile("/proc/mounts")
if err != nil {
return false
}
for _, line := range strings.Split(string(bytes), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if strings.Split(line, " ")[1] == mountpoint {
return true
}
}
return false
}
func mount(source, target, fstype string, options string, mkdir bool) error {
flags, data, _ := convertMountOptions(options)
if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) {
flags = append(flags, unix.MS_REMOUNT)
}
if mkdir {
err := os.MkdirAll(target, 0755)
if err != nil {
return err
}
}
if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil {
return err
}
return nil
}
func mountFstabEntries() (error, int) {
if _, err := os.Stat("/etc/fstab"); os.IsNotExist(err) {
return nil, 0
} else if err != nil {
return err, 0
}
bytes, err := os.ReadFile("/etc/fstab")
if err != nil {
return err, 0
}
swapPriority := -2
for i, line := range strings.Split(string(bytes), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") || line == "" {
continue
}
// Get fields from line
fields := []string{}
sb := &strings.Builder{}
quoted := false
for _, r := range line {
if r == '"' {
quoted = !quoted
} else if !quoted && r == ' ' {
str := sb.String()
if len(strings.TrimSpace(str)) > 0 {
fields = append(fields, sb.String())
}
sb.Reset()
} else {
sb.WriteRune(r)
}
}
if sb.Len() > 0 {
fields = append(fields, sb.String())
}
if len(fields) < 4 {
return fmt.Errorf("Not enough fields"), i + 1
}
source := fields[0]
target := fields[1]
fstype := fields[2]
options := fields[3]
// Replace device prefixes
if cutSource, ok := strings.CutPrefix(source, "LABEL="); ok {
source = "/dev/disk/by-label/" + strings.ReplaceAll(cutSource, " ", "\\x20")
} else if cutSource, ok := strings.CutPrefix(source, "UUID="); ok {
source = "/dev/disk/by-uuid/" + cutSource
} else if cutSource, ok := strings.CutPrefix(source, "PARTLABEL="); ok {
source = "/dev/disk/by-partlabel/" + cutSource
} else if cutSource, ok := strings.CutPrefix(source, "PARTUUID="); ok {
source = "/dev/disk/by-partuuid/" + cutSource
}
flags, data, extra := convertMountOptions(options)
if slices.Contains(extra, "noauto") {
continue
}
if fstype == "swap" {
b := append([]byte(source), 0)
const SwapFlagPrioShift = 0
const SwapFlagPrioMask = 0x7fff
_, _, err := unix.Syscall(unix.SYS_SWAPON, uintptr(unsafe.Pointer(&b[0])), uintptr((swapPriority<<SwapFlagPrioShift)&SwapFlagPrioMask), 0)
swapPriority--
if err != 0 {
if slices.Contains(extra, "nofail") {
fmt.Printf("Warning: could not mount fstab entry on line %d: swapon syscall returned non-zero exit code: %d\n", i+1, err)
} else {
return fmt.Errorf("swapon syscall returned non-zero exit code: %d", err), i + 1
}
}
continue
}
if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) {
flags = append(flags, unix.MS_REMOUNT)
}
if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil {
if slices.Contains(extra, "nofail") {
log.Printf("Warning: could not mount fstab entry on line %d: %s\n", i+1, err)
} else {
return err, i + 1
}
}
}
return nil, 0
}
func unmountFilesystems() {
// Disable all swap memory
data, err := os.ReadFile("/proc/swaps")
if err != nil {
log.Fatal(err)
}
for i, entry := range strings.Split(string(data), "\n") {
if i == 0 {
continue
}
entry = strings.TrimSpace(entry)
if len(entry) == 0 {
continue
}
mountpoint := strings.Fields(entry)[0]
// Unmount swap at mountpoint
fmt.Printf("Disabling swap at %s... ", mountpoint)
b := append([]byte(mountpoint), 0)
_, _, err := unix.Syscall(unix.SYS_SWAPOFF, uintptr(unsafe.Pointer(&b[0])), 0, 0)
if err == 0 {
fmt.Println("Done.")
} else {
fmt.Printf("Error: %s\n", err.Error())
}
}
data, err = os.ReadFile("/proc/self/mountinfo")
if err != nil {
log.Fatal(err)
}
// Unmount filesystems
entries := strings.Split(string(data), "\n")
slices.Reverse(entries)
for _, entry := range entries {
entry = strings.TrimSpace(entry)
if len(entry) == 0 {
continue
}
// Get entry fields
fields := strings.Fields(entry)
mountpoint := fields[4]
filesystem := ""
for i := 6; i < len(fields); i++ {
if fields[i] == "-" {
filesystem = fields[i+1]
break
}
}
// Skip root filesystem
if mountpoint == "/" {
continue
}
// Skip root and ignored filesystems
ignoredFilesystems := []string{
"devtmpfs",
"proc",
"sysfs",
"tmpfs",
}
if slices.Contains(ignoredFilesystems, filesystem) {
continue
}
// Unmount filesystem at mountpoint
fmt.Printf("Unmounting %s...", mountpoint)
err := unix.Unmount(mountpoint, 0)
if errors.Is(err, syscall.EBUSY) {
fmt.Println(" Busy.")
time.Sleep(1 * time.Second)
} else if err != nil {
fmt.Printf(" Error: %s\n", err.Error())
} else {
fmt.Println(" Done.")
}
}
}
func remountRootReadonly() {
fmt.Print("Remounting root as read-only...")
data, err := os.ReadFile("/proc/self/mountinfo")
if err != nil {
log.Fatal(err)
}
filesystem := ""
source := ""
fsData := ""
// Get root filesystems
entries := strings.Split(string(data), "\n")
slices.Reverse(entries)
for _, entry := range entries {
entry = strings.TrimSpace(entry)
if len(entry) == 0 {
continue
}
// Get entry fields
fields := strings.Fields(entry)
mountpoint := fields[4]
for i := 6; i < len(fields); i++ {
if fields[i] == "-" {
filesystem = fields[i+1]
source = fields[i+2]
fsData = fields[i+3]
break
}
}
if mountpoint == "/" {
break
}
}
err = unix.Mount(source, "/", filesystem, syscall.MS_RDONLY|syscall.MS_REMOUNT, fsData)
if errors.Is(err, syscall.EBUSY) {
fmt.Println(" Busy.")
} else if err != nil {
fmt.Printf(" Error: %s\n", err.Error())
} else {
fmt.Println(" Done.")
}
}
View File
View File
+227
View File
@@ -0,0 +1,227 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"maps"
"net"
"os"
"os/signal"
"path"
"slices"
"strings"
"syscall"
"time"
)
// Build-time variables
var version = "dev"
var runtimeServiceDir string
var serviceConfigDir string
var logger *log.Logger
var socket net.Listener
func main() {
// Parse flags
printVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
if *printVersion || flag.NArg() != 2 {
fmt.Printf("Enit Service Manager version %s\n", version)
os.Exit(0)
}
if os.Getppid() != 1 {
fmt.Println("Esvm must be run by PID 1!")
os.Exit(1)
}
// Setup main logger
err := setupESVMLogger()
if err != nil {
log.Printf("Error: could not setup main ESVM logger: %s\n", err)
logger = log.Default()
}
// Set directory variables
runtimeServiceDir = flag.Arg(0)
serviceConfigDir = flag.Arg(1)
Init()
if err != nil {
}
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigc
Destroy()
os.Exit(0)
}()
for {
listenToSocket()
}
}
func setupESVMLogger() error {
// Create esvm log directory
err := os.MkdirAll("/var/log/esvm", 0755)
if err != nil {
return err
}
// Create esvm old log directory
err = os.MkdirAll("/var/log/esvm/old", 0755)
if err != nil {
return err
}
// Move old log file
if _, err := os.Stat("/var/log/esvm/esvm.log"); err == nil {
os.Rename("/var/log/esvm/esvm.log", "/var/log/esvm/old/esvm.log")
}
// Open new log file
loggerFile, err := os.OpenFile("/var/log/esvm/esvm.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
// Setup multiwriter
w := io.MultiWriter(loggerFile, os.Stderr)
// Initialize logger and print a header line
logger = log.New(w, "[ESVM] ", log.Lshortfile|log.LstdFlags)
_, err = loggerFile.WriteString("------ " + time.Now().Format(time.UnixDate) + " ------\n")
return nil
}
func Init() {
logger.Println("Initializing ESVM...")
if _, err := os.Stat(runtimeServiceDir); err == nil {
logger.Fatalf("Error: could not initialize ESVM: %s", fmt.Errorf("runtime service directory %s already exists", runtimeServiceDir))
}
err := os.MkdirAll(runtimeServiceDir, 0755)
if err != nil {
logger.Fatalf("Error: could not initialize ESVM: %s", err)
}
socket, err = initSocket()
if err != nil {
logger.Fatalf("Error: could not initialize ESVM: %s", err)
}
if stat, err := os.Stat(serviceConfigDir); err != nil || !stat.IsDir() {
logger.Println("ESVM initialized successfully!")
return
}
dirEntries, err := os.ReadDir(path.Join(serviceConfigDir, "services"))
if err != nil {
logger.Fatalf("Error: Could not initialize ESVM: %s", err)
}
// Read and initialize service files
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".esv") {
filepath := path.Join(serviceConfigDir, "services", entry.Name())
LoadService(filepath)
}
}
// Read enabled services
EnabledServices := ReadEnabledServices()
// Start enabled services
stages := slices.Collect(maps.Keys(EnabledServices))
slices.Sort(stages)
for stage := 1; stage <= stages[len(stages)-1]; stage++ {
logger.Printf("Starting stage %d services...", stage)
services := EnabledServices[stage]
remainingServices := len(services)
for remainingServices != 0 {
for _, serviceName := range services {
service := GetServiceByName(serviceName)
if service == nil {
remainingServices--
continue
}
err := service.StartService()
if err != nil {
logger.Printf("Error: could not start service (%s): %s", service.Name, err)
}
remainingServices--
}
}
}
logger.Println("ESVM initialized successfully!")
}
func Reload() {
logger.Println("Reloading all ESVM services...")
dirEntries, err := os.ReadDir(path.Join(serviceConfigDir, "services"))
if err != nil {
logger.Fatalf("Error: Could not initialize ESVM: %s", err)
}
// Read and load service files
servicesToRemove := slices.Clone(Services)
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".esv") {
filepath := path.Join(serviceConfigDir, "services", entry.Name())
LoadService(filepath)
servicesToRemove = slices.DeleteFunc(servicesToRemove, func(sv *EnitService) bool {
return sv.Filepath == filepath
})
}
}
// Reload services that had their esv file removed
for _, service := range servicesToRemove {
LoadService(service.Filepath)
}
logger.Println("All ESVM services have been reloaded!")
}
func Destroy() {
logger.Println("Stopping all ESVM services...")
// Loop through all started services in reverse
for i := len(startedServicesOrder) - 1; i >= 0; i-- {
// Get service by name
service := GetServiceByName(startedServicesOrder[i])
if service == nil {
continue
}
// Stop service
if err := service.StopService(); err != nil {
logger.Printf("Error: could not stop service (%s): %s", service.Name, err)
}
}
logger.Println("All ESVM services have stopped!")
}
func GetServiceByName(name string) *EnitService {
for _, service := range Services {
if service.Name == name {
return service
}
}
return nil
}
+528
View File
@@ -0,0 +1,528 @@
package main
import (
"crypto/sha256"
"fmt"
"io"
"os"
"os/exec"
"os/user"
"path"
"slices"
"strconv"
"strings"
"syscall"
"time"
"gopkg.in/yaml.v3"
)
type EnitServiceState uint8
const (
EnitServiceUnknown EnitServiceState = iota
EnitServiceUnloaded
EnitServiceStarting
EnitServiceRunning
EnitServiceStopped
EnitServiceCrashed
EnitServiceCompleted
)
var EnitServiceStateNames map[EnitServiceState]string = map[EnitServiceState]string{
EnitServiceUnknown: "unknown",
EnitServiceUnloaded: "unloaded",
EnitServiceStarting: "starting",
EnitServiceRunning: "running",
EnitServiceStopped: "stopped",
EnitServiceCrashed: "crashed",
EnitServiceCompleted: "completed",
}
type EnitService struct {
Name string `yaml:"name"`
Description string `yaml:"description,omitempty"`
Type string `yaml:"type"`
StartCmd string `yaml:"start_cmd"`
CrashOnSafeExit bool `yaml:"crash_on_safe_exit"`
StopCmd string `yaml:"stop_cmd,omitempty"`
User string `yaml:"user,omitempty"`
Restart string `yaml:"restart,omitempty"`
ReadyFd int `yaml:"ready_fd"`
Setpgid bool `yaml:"setpgid"`
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)
var startedServicesOrder = make([]string, 0)
func (service *EnitService) GetProcess() *os.Process {
process, _ := os.FindProcess(service.processID)
return process
}
func (service *EnitService) GetLogFile() (file *os.File, err error) {
// Create esvm log directory
err = os.MkdirAll("/var/log/esvm", 0755)
if err != nil {
return nil, err
}
// Create esvm old log directory
err = os.MkdirAll("/var/log/esvm/old", 0755)
if err != nil {
return nil, err
}
// Move old log file
if _, err := os.Stat(path.Join("/var/log/esvm/", service.Name+".log")); err == nil {
os.Rename(path.Join("/var/log/esvm/", service.Name+".log"), path.Join("/var/log/esvm/old", service.Name+".log"))
}
// Open new log file
file, err = os.OpenFile(path.Join("/var/log/esvm/", service.Name+".log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return nil, err
}
// Initialize logger and print a header line
_, err = file.WriteString("------ " + time.Now().Format(time.UnixDate) + " ------\n")
if err != nil {
file.Close()
return nil, err
}
return file, nil
}
func LoadService(filepath string) {
bytes, err := os.ReadFile(filepath)
checksum := sha256.Sum256(bytes)
var serviceToReload *EnitService
// Check if service is already loaded
for _, service := range Services {
if service.Filepath != filepath {
continue
}
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
serviceToReload = service
break
}
if serviceToReload == nil {
logger.Printf("Loading service (%s)...\n", filepath)
} else {
logger.Printf("Reloading service (%s)...\n", filepath)
}
if os.IsNotExist(err) {
Services = slices.DeleteFunc(Services, func(sv *EnitService) bool {
if sv.Filepath == filepath {
logger.Printf("Service (%s) has been removed\n", sv.Name)
return true
}
return false
})
return
} else if err != nil {
logger.Printf("Error: Could not read service file (%s)", filepath)
return
}
newService := EnitService{
Name: "",
Description: "",
Type: "",
StartCmd: "",
StopCmd: "",
User: "",
Restart: "",
Setpgid: true,
CrashOnSafeExit: true,
LogOutput: true,
Filepath: filepath,
filepathChecksum: sha256.Sum256(bytes),
restartCount: 0,
stopChannel: make(chan bool),
state: EnitServiceUnloaded,
}
if serviceToReload != nil {
newService.restartCount = serviceToReload.restartCount
newService.stopChannel = serviceToReload.stopChannel
newService.state = serviceToReload.state
}
if err := yaml.Unmarshal(bytes, &newService); err != nil {
logger.Printf("Error: could not read service file %s", filepath)
return
}
for _, sv := range Services {
if sv.Name == newService.Name && sv != serviceToReload {
logger.Printf("Error: service with name (%s) has already been loaded", newService.Name)
return
}
}
switch newService.Type {
case "simple", "background":
default:
logger.Printf("Error: unknown service type (%s)", newService.Type)
return
}
switch newService.Restart {
case "true", "always":
default:
newService.Restart = "false"
}
for i, sv := range Services {
if sv == serviceToReload {
Services[i] = &newService
logger.Printf("Service (%s) has been reloaded!\n", newService.Name)
return
}
}
Services = append(Services, &newService)
logger.Printf("Service (%s) has been loaded!\n", newService.Name)
}
func (service *EnitService) StartService() (err error) {
if service == nil {
return nil
}
if service.state == EnitServiceRunning {
return nil
}
logger.Printf("Starting service (%s)...\n", service.Name)
// Get log file if service logs output
var logFile *os.File
if service.LogOutput {
logFile, err = service.GetLogFile()
if err != nil {
return err
}
}
cmd := exec.Command("/bin/sh", "-c", "exec "+service.StartCmd)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: service.Setpgid, Pgid: 0}
// Setup service log file
if logFile != nil {
cmd.Stdout = logFile
cmd.Stderr = logFile
}
// Setup command credentials
if service.User != "" && service.User != "root" {
// Lookup user in /etc/passwd
u, err := user.Lookup(service.User)
if err != nil {
return err
}
// Get user id and group id
uid, err := strconv.Atoi(u.Uid)
if err != nil {
return err
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return err
}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}
}
// Setup command pipes
var pipeReader, pipeWriter *os.File
if service.ReadyFd > 2 {
pipeReader, pipeWriter, err = os.Pipe()
if err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
err := pipeReader.SetDeadline(time.Now().Add(10 * time.Second))
if err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
for i := 3; i < service.ReadyFd; i++ {
cmd.ExtraFiles = append(cmd.ExtraFiles, nil)
}
cmd.ExtraFiles = append(cmd.ExtraFiles, pipeWriter)
}
if err := cmd.Start(); err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
return err
}
pid := cmd.Process.Pid
service.processID = cmd.Process.Pid
service.state = EnitServiceStarting
// Wait for data from pipe
if pipeReader != nil {
buffer := make([]byte, 1)
_, err := io.ReadAtLeast(pipeReader, buffer, 1)
if err != nil {
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
// Kill process and children
syscall.Kill(-pid, syscall.SIGKILL)
service.processID = 0
service.state = EnitServiceCrashed
return err
}
}
service.state = EnitServiceRunning
go func() {
err := cmd.Wait()
// Close log file if not nil
if logFile != nil {
logFile.Close()
}
select {
case <-service.stopChannel:
service.restartCount = 0
default:
// Kill remaining child processes
syscall.Kill(-pid, syscall.SIGKILL)
if service.Type == "simple" && err == nil {
service.restartCount = 0
if strings.TrimSpace(service.StopCmd) == "" {
service.state = EnitServiceCompleted
// Reload service if needed
if service.shouldReload {
LoadService(service.Filepath)
}
}
return
}
if !service.CrashOnSafeExit {
logger.Printf("Service (%s) has exited\n", service.Name)
service.state = EnitServiceStopped
} else {
logger.Printf("Service (%s) has crashed!\n", service.Name)
service.state = EnitServiceCrashed
}
// Reload service if needed
if service.shouldReload {
LoadService(service.Filepath)
if GetServiceByName(service.Name) == nil {
return
}
service = GetServiceByName(service.Name)
}
if service.Restart == "always" {
_ = service.StartService()
} else if service.Restart == "true" && service.restartCount < 5 {
service.restartCount++
_ = service.StartService()
}
}
service.processID = 0
}()
// Add to started services order slice
if !slices.Contains(startedServicesOrder, service.Name) {
startedServicesOrder = append(startedServicesOrder, service.Name)
}
logger.Printf("Service (%s) has started!\n", service.Name)
return nil
}
func (service *EnitService) StopService() error {
if service.state != EnitServiceRunning {
return nil
}
logger.Printf("Stopping service (%s)...", service.Name)
pid := service.processID
newServiceStatus := EnitServiceCrashed
defer func() {
// Kill remaining child processes
if pid != 0 {
syscall.Kill(-pid, syscall.SIGKILL)
}
service.state = newServiceStatus
service.processID = 0
// Reload service if needed
if service.shouldReload {
LoadService(service.Filepath)
if GetServiceByName(service.Name) == nil {
return
}
service = GetServiceByName(service.Name)
}
}()
if strings.TrimSpace(service.StopCmd) == "" {
if err := service.GetProcess().Signal(syscall.Signal(0)); err != nil {
newServiceStatus = EnitServiceStopped
logger.Printf("Service (%s) has stopped (Process already dead)", service.Name)
return nil
}
go func() { service.stopChannel <- true }()
// Send SIGTERM signal to process
if err := service.GetProcess().Signal(syscall.SIGTERM); err != nil {
service.GetProcess().Signal(syscall.SIGKILL)
return fmt.Errorf("could not stop process gracefully")
}
} else {
go func() { service.stopChannel <- true }()
cmd := exec.Command("/bin/sh", "-c", service.StopCmd)
// Setup command credentials
if service.User != "" && service.User != "root" {
// Lookup user in /etc/passwd
u, err := user.Lookup(service.User)
if err != nil {
return err
}
// Get user id and group id
uid, err := strconv.Atoi(u.Uid)
if err != nil {
return err
}
gid, err := strconv.Atoi(u.Gid)
if err != nil {
return err
}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}
}
if err := cmd.Run(); err != nil {
return err
}
}
if service.Type == "background" {
// Check if the process has stopped gracefully, otherwise send sigkill on timeout
exited := make(chan bool)
go func() {
for {
if err := syscall.Kill(pid, syscall.Signal(0)); err != nil {
break
}
}
exited <- true
}()
select {
case <-exited:
case <-time.After(15 * time.Second):
service.GetProcess().Signal(syscall.SIGKILL)
return fmt.Errorf("could not stop process gracefully")
}
}
newServiceStatus = EnitServiceStopped
logger.Printf("Service (%s) has stopped!\n", service.Name)
return nil
}
func (service *EnitService) RestartService() error {
if err := service.StopService(); err != nil {
return err
}
// Get service from list in case of a reload
if GetServiceByName(service.Name) == nil {
return fmt.Errorf("service was removed")
}
service = GetServiceByName(service.Name)
if err := service.StartService(); err != nil {
return err
}
return nil
}
func ReadEnabledServices() (EnabledServices map[int][]string) {
EnabledServices = make(map[int][]string)
data, err := os.ReadFile(path.Join(serviceConfigDir, "enabled-services.yml"))
if err != nil {
return EnabledServices
}
err = yaml.Unmarshal(data, &EnabledServices)
if err != nil {
return EnabledServices
}
return EnabledServices
}
+261
View File
@@ -0,0 +1,261 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"path"
)
var commandHandlers = make(map[string]func(conn net.Conn, jsonData map[string]any))
func initSocket() (socket net.Listener, err error) {
socket, err = net.Listen("unix", path.Join(runtimeServiceDir, "esvm.sock"))
if err != nil {
return nil, err
}
// Register command handlers
commandHandlers["reload"] = handleReloadServicesCommand
commandHandlers["start"] = handleStartServiceCommand
commandHandlers["stop"] = handleStopServiceCommand
commandHandlers["restart"] = handleRestartServiceCommand
commandHandlers["status"] = handleStatusServiceCommand
commandHandlers["list"] = handleListServicesCommand
return socket, nil
}
func listenToSocket() {
conn, err := socket.Accept()
if err != nil {
logger.Println("Could not accept socket connection!")
return
}
// Handle the connection in a separate goroutine.
go func(conn net.Conn) {
defer conn.Close()
// Read data from the connection.
data, err := readAllConn(conn)
if err != nil {
logger.Fatalf("Could not read data from socket! Error: %s\n", err)
return
}
// Decoode JSON data
var jsonData map[string]any
err = json.Unmarshal(data, &jsonData)
if err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Invalid JSON")))
return
}
// Get command to execute
command, ok := jsonData["command"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'command' field missing")))
return
}
// Get command handler
commandHandler, ok := commandHandlers[command.(string)]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("command (%s) has not been implemented", command.(string))))
return
}
commandHandler(conn, jsonData)
}(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"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'service' field missing")))
return
}
// Ensure service exists
service := GetServiceByName(serviceName.(string))
if service == nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) not found", serviceName.(string))))
return
}
// Start the service
if err := service.StartService(); err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) could not be started", serviceName.(string))))
return
}
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) has started sucessfully", serviceName.(string))))
}
func handleStopServiceCommand(conn net.Conn, jsonData map[string]any) {
// Get service name from json data
serviceName, ok := jsonData["service"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'service' field missing")))
return
}
// Ensure service exists
service := GetServiceByName(serviceName.(string))
if service == nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) not found", serviceName.(string))))
return
}
// Stop the service
if err := service.StopService(); err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) could not be stopped", serviceName.(string))))
return
}
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) has stopped sucessfully", serviceName.(string))))
}
func handleRestartServiceCommand(conn net.Conn, jsonData map[string]any) {
// Get service name from json data
serviceName, ok := jsonData["service"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'service' field missing")))
return
}
// Ensure service exists
service := GetServiceByName(serviceName.(string))
if service == nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) not found", serviceName.(string))))
return
}
// Restart the service
if err := service.RestartService(); err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) could not be restarted", serviceName.(string))))
return
}
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) has restarted sucessfully", serviceName.(string))))
}
func handleStatusServiceCommand(conn net.Conn, jsonData map[string]any) {
// Get service name from json data
serviceName, ok := jsonData["service"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'service' field missing")))
return
}
// Ensure service exists
service := GetServiceByName(serviceName.(string))
if service == nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) not found", serviceName.(string))))
return
}
statusMap := make(map[string]any)
statusMap["name"] = service.Name
statusMap["description"] = service.Description
statusMap["state"] = EnitServiceStateNames[service.state]
statusMap["process_id"] = service.processID
// Encode map to json string
newJsonData, err := json.Marshal(statusMap)
if err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Could not encode JSON data")))
return
}
conn.Write(newJsonData)
}
func handleListServicesCommand(conn net.Conn, _ map[string]any) {
servicesMap := make(map[string]any)
servicesMap["services"] = make([]map[string]any, 0)
// Loop through each service
for _, service := range Services {
statusMap := make(map[string]any)
statusMap["name"] = service.Name
statusMap["description"] = service.Description
statusMap["state"] = EnitServiceStateNames[service.state]
statusMap["process_id"] = service.processID
servicesMap["services"] = append(servicesMap["services"].([]map[string]any), statusMap)
}
// Encode map to json string
newJsonData, err := json.Marshal(servicesMap)
if err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Could not encode JSON data")))
return
}
conn.Write(newJsonData)
}
func wrapErrorInJson(err error) []byte {
// Wrap error in struct
type jsonErrorStruct struct {
Error string `json:"error"`
}
jsonError := jsonErrorStruct{
Error: err.Error(),
}
// Encode struct to json string
jsonData, _err := json.Marshal(jsonError)
if _err != nil {
return nil
}
return jsonData
}
func wrapSuccessMsgInJson(msg string) []byte {
// Wrap message in struct
type jsonSuccessStruct struct {
Success string `json:"success"`
}
jsonSuccess := jsonSuccessStruct{
Success: msg,
}
// Encode struct to json string
jsonData, _err := json.Marshal(jsonSuccess)
if _err != nil {
return nil
}
return jsonData
}
func readAllConn(conn net.Conn) ([]byte, error) {
var buf bytes.Buffer
for {
dataChunk := make([]byte, 1024)
n, err := conn.Read(dataChunk)
if err != nil && err != io.EOF {
return nil, err
}
buf.Write(dataChunk[:n])
if n < 1024 {
break
}
}
return buf.Bytes(), nil
}