33 Commits
Author SHA1 Message Date
EnumDev 075b5a8bad Create README.md 2025-12-25 15:25:35 +02:00
EnumDev 542f9987e1 other: remove obsolete fields from default services 2025-12-25 15:06:32 +02:00
EnumDev 48b9e3512a other: update Makefile 2025-12-25 15:02:46 +02:00
EnumDev f69ca4d02c enit: switch away from using udev for fstab device prefixes 2025-12-25 11:10:17 +02:00
EnumDev 84de96ceca enit: do not set stdout and stderr for service manager 2025-12-25 09:23:31 +02:00
EnumDev 2c97e87261 Add optional rc-local service for running the /etc/rc.local script at boot 2025-12-18 12:29:18 +02:00
EnumDev 49541f6f76 ectl: hide service PID if it is 0 2025-12-11 13:07:11 +02:00
EnumDev fc738e6f4e esvm: set service PID to 0 for simple services with a stop command 2025-12-11 13:06:17 +02:00
EnumDev 87ea28bfa3 ectl: fix broken 'list' subcommand 2025-12-10 21:22:41 +02:00
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
23 changed files with 1395 additions and 674 deletions
+20 -25
View File
@@ -1,44 +1,39 @@
SHELL := /bin/bash # Installation paths
PREFIX ?= /usr/local PREFIX ?= /usr/local
SBINDIR ?= $(PREFIX)/sbin SBINDIR ?= $(PREFIX)/sbin
SYSCONFDIR ?= $(PREFIX)/etc SYSCONFDIR ?= $(PREFIX)/etc
LOCALSTATEDIR ?= $(PREFIX)/var LOCALSTATEDIR ?= $(PREFIX)/var
RUNSTATEDIR ?= $(LOCALSTATEDIR)/run RUNSTATEDIR ?= $(LOCALSTATEDIR)/run
# Compilers and tools
GO ?= $(shell type -a -P go | head -n 1) GO ?= $(shell type -a -P go | head -n 1)
# Set version variable # Build-time variables
ifeq ($(VERSION),) VERSION ?= $(shell git describe --tags --dirty)
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 build
cd src/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 src/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 src/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 build/esvm
mkdir -p $(DESTDIR)$(SBINDIR) # Create directories
mkdir -p $(DESTDIR)$(SYSCONFDIR)/esvm/services install -d $(DESTDIR)$(SBINDIR)
cp build/enit $(DESTDIR)$(SBINDIR)/enit # Install binaries
cp build/esvm $(DESTDIR)$(SBINDIR)/esvm install -m755 build/{enit,esvm,ectl} -t $(DESTDIR)$(SBINDIR)
cp build/ectl $(DESTDIR)$(SBINDIR)/ectl
install-services: install-services:
mkdir -p $(DESTDIR)$(SYSCONFDIR)/esvm/services # Create directory
cp services/* -t $(DESTDIR)$(SYSCONFDIR)/esvm/services install -d $(DESTDIR)$(SYSCONFDIR)/esvm/services
# Install services
install -m644 services/*.esv -t $(DESTDIR)$(SYSCONFDIR)/esvm/services
uninstall:
-rm -f $(DESTDIR)$(SBINDIR)/{enit,esvm,ectl}
-rm -rf $(DESTDIR)$(SYSCONFDIR)/esvm
clean: clean:
rm -r build/ rm -r build/
.PHONY: build .PHONY: build install install-services uninstall clean
+24
View File
@@ -0,0 +1,24 @@
# Enit
## The official init system for Tide Linux
### Project Information
Enit is an init system developed for the Tide Linux project. It features easy-to-use commands for power and service management and yaml-based service configuration
### Installation Guide
#### Using a package manager
- Tide Linux: Enit is pre-installed as the default init system through the `enit` package
#### Building from source
- Download `go` from your package manager or from the go website
- Download `make` from your package manager
- Run the following command to compile the project
```
make PREFIX=/usr SYSCONFDIR=/etc LOCALSTATEDIR=/var
```
- Run the following command to install Enit into your system. You may also append a DESTDIR variable at the end of this line if you wish to install in a different root directory
```
make install PREFIX=/usr
make install-config PREFIX=/usr SYSCONFDIR=/etc
```
### Post installation
- Set the default init system in your bootloader/boot-manager by appending `init=/usr/sbin/enit` to your kernel command-line parameters. Alternatively symlink `/usr/sbin/enit` to `/sbin/init`
- You may find additional service files in the [Tide Linux repositories](https://git.enumerated.dev/tide-linux). Some service files will likely need to be modified to run in your distribution
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty1
description: Start virtual terminal on tty1 description: Start virtual terminal on tty1
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty --noclear tty1 start_cmd: /usr/bin/setsid /sbin/agetty --noclear tty1
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty2
description: Start virtual terminal on tty2 description: Start virtual terminal on tty2
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty tty2 start_cmd: /usr/bin/setsid /sbin/agetty tty2
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty3
description: Start virtual terminal on tty3 description: Start virtual terminal on tty3
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty tty3 start_cmd: /usr/bin/setsid /sbin/agetty tty3
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty4
description: Start virtual terminal on tty4 description: Start virtual terminal on tty4
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty tty4 start_cmd: /usr/bin/setsid /sbin/agetty tty4
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty5
description: Start virtual terminal on tty5 description: Start virtual terminal on tty5
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty tty5 start_cmd: /usr/bin/setsid /sbin/agetty tty5
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
+1 -1
View File
@@ -2,6 +2,6 @@ name: agetty-tty6
description: Start virtual terminal on tty6 description: Start virtual terminal on tty6
type: background type: background
start_cmd: /usr/bin/setsid /sbin/agetty tty6 start_cmd: /usr/bin/setsid /sbin/agetty tty6
exit_method: kill
crash_on_safe_exit: false crash_on_safe_exit: false
restart: always restart: always
setpgid: false
-3
View File
@@ -1,7 +1,4 @@
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
restart: false
+4
View File
@@ -0,0 +1,4 @@
name: rc-local
description: Run the /etc/rc.local script at boot
type: simple
start_cmd: /etc/rc.local
+5
View File
@@ -1,3 +1,8 @@
module ectl module ectl
go 1.23.4 go 1.23.4
require (
github.com/spf13/pflag v1.0.10
gopkg.in/yaml.v3 v3.0.1
)
+6
View File
@@ -0,0 +1,6 @@
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+22 -344
View File
@@ -1,17 +1,10 @@
package main package main
import ( import (
"encoding/json"
"flag"
"fmt" "fmt"
"io"
"log" "log"
"net"
"os" "os"
"path"
"strconv"
"syscall" "syscall"
"time"
) )
// Build-time variables // Build-time variables
@@ -19,358 +12,43 @@ var version = "dev"
var sysconfdir = "/etc/" var sysconfdir = "/etc/"
var runstatedir = "/var/run/" var runstatedir = "/var/run/"
var conn net.Conn
func main() { func main() {
// Set and parse flags // Show usage if no arguments specified
printVersion := flag.Bool("version", false, "print version and exit") if len(os.Args) == 1 {
printJson := flag.Bool("json", false, "print output in json format")
flag.Parse()
// Dial esvm socket
dialSocket()
defer conn.Close()
if flag.NArg() < 1 {
printUsage() printUsage()
os.Exit(1) return
} }
if *printVersion || flag.Args()[0] == "version" { subcommand := os.Args[1]
switch subcommand {
case "v", "version":
fmt.Printf("Enit Control version %s\n", version) fmt.Printf("Enit Control version %s\n", version)
return case "shutdown", "poweroff", "halt":
} 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) err := syscall.Kill(1, syscall.SIGUSR1)
if err != nil { if err != nil {
log.Fatalf("Could not send shutdown signal! Error: %s\n", err) log.Fatalf("Could not send shutdown signal! Error: %s\n", err)
} }
return case "reboot", "restart", "reset":
} else if flag.Args()[0] == "reboot" || flag.Args()[0] == "restart" || flag.Args()[0] == "reset" {
err := syscall.Kill(1, syscall.SIGTERM) err := syscall.Kill(1, syscall.SIGTERM)
if err != nil { if err != nil {
log.Fatalf("Could not send shutdown signal! Error: %s\n", err) log.Fatalf("Could not send reboot 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.Arg(1) == "start" || flag.Arg(1) == "stop" || flag.Arg(1) == "restart" {
// Ensure service name argument has been set
if len(flag.Args()) <= 2 {
fmt.Printf("Usage: ectl service %s <service>\n", flag.Args()[1])
return
}
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: flag.Arg(1),
Service: flag.Arg(2),
}
// 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 == io.EOF {
return
}
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) == "enable" || flag.Arg(1) == "disable" {
// Ensure service name argument has been set
if len(flag.Args()) <= 2 {
fmt.Printf("Usage: ectl service %s <service> [stage]\n", flag.Args()[1])
return
}
// Get service stage
stage := 3
if len(flag.Args()) > 3 {
flagStr := flag.Arg(3)
_stage, err := strconv.ParseInt(flagStr, 10, 32)
if err != nil {
log.Fatalf("Error: could not parse stage number: %s", err)
}
stage = int(_stage)
} else if flag.Arg(1) == "disable" {
stage = 0
}
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
Stage int `json:"stage"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: "set_enabled",
Service: flag.Arg(2),
Stage: stage,
}
// 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 == io.EOF {
return
}
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.Args()[1] == "status" {
// Ensure service name argument has been set
if len(flag.Args()) <= 2 {
fmt.Printf("Usage: ectl service %s <service>\n", flag.Args()[1])
return
}
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: flag.Arg(1),
Service: flag.Arg(2),
}
// 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 == io.EOF {
return
}
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)
}
serviceState := returnedJsonData["state"].(string)
serviceEnabled := returnedJsonData["is_enabled"].(bool)
serviceStage := int(returnedJsonData["stage"].(float64))
processID := int(returnedJsonData["process_id"].(float64))
fmt.Printf("Name: %s\n", flag.Arg(2))
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)
}
return
} else if flag.Arg(1) == "list" {
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
}
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 == io.EOF {
return
}
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)
}
for _, serviceMap := range returnedJsonData["services"].([]any) {
serviceName := serviceMap.(map[string]any)["name"].(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("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()
}
return
} }
case "sv", "service":
handleServiceSubcommand()
default:
printUsage()
os.Exit(1)
} }
printUsage()
os.Exit(1)
} }
func printUsage() { func printUsage() {
fmt.Println("Available sucommands:") fmt.Println("Usage: ectl <subcommand> [options]")
fmt.Println("ectl version | Show enit version") fmt.Println("Description: Shutdown, reboot and manage system services")
fmt.Println("ectl shutdown/poweroff/halt | Shutdown the system") fmt.Println("Sucommands:")
fmt.Println("ectl reboot/restart | Reboot the system") fmt.Println(" v, version Show enit version")
fmt.Println("ectl help | Show command explanations") fmt.Println(" shutdown, poweroff, halt Shutdown the system")
fmt.Println("ectl sv/service start <service> | Start a service") fmt.Println(" reboot, restart, reset Reboot the system")
fmt.Println("ectl sv/service stop <service> | Stop a service") fmt.Println(" sv, service Manage system services")
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
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(5 * time.Second)); err != nil {
log.Fatalf("Failed to set write deadline! Error: %s\n", err)
}
} }
+482
View File
@@ -0,0 +1,482 @@
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" && processID > 0 {
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
}
// 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)
}
// Set is_enabled and stage fields in json data
for _, serviceMap := range returnedJsonData["services"].([]any) {
serviceMap.(map[string]any)["is_enabled"], serviceMap.(map[string]any)["stage"] = isServiceEnabled(serviceMap.(map[string]any)["name"].(string))
}
// Print json data if flag is set
if printJson {
data, _ = json.Marshal(returnedJsonData)
fmt.Println(string(data))
return
}
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"].(int))
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" && processID > 0 {
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
}
+83
View File
@@ -0,0 +1,83 @@
package main
import (
"os/exec"
"strings"
)
type BlockDevice struct {
Device string
UUID string
PartUUID string
Label string
PartLabel string
Type string
}
func GetBlockDevices() []BlockDevice {
cmd := exec.Command("/sbin/blkid")
out, err := cmd.Output()
if err != nil {
return make([]BlockDevice, 0)
}
blockDevices := make([]BlockDevice, 0)
for _, line := range strings.Split(string(out), "\n") {
line := strings.TrimSpace(line)
if len(line) == 0 {
continue
}
lineSplit := strings.SplitN(line, ": ", 2)
if len(lineSplit) != 2 {
return make([]BlockDevice, 0)
}
device := lineSplit[0]
line = lineSplit[1]
fields := []string{}
sb := &strings.Builder{}
quoted := false
for _, r := range line {
if r == '"' {
quoted = !quoted
} else if !quoted && r == ' ' {
fields = append(fields, sb.String())
sb.Reset()
} else {
sb.WriteRune(r)
}
}
if sb.Len() > 0 {
fields = append(fields, sb.String())
}
bd := BlockDevice{Device: device}
for _, field := range fields {
fieldSplit := strings.SplitN(field, "=", 2)
if len(fieldSplit) != 2 {
return make([]BlockDevice, 0)
}
fieldName := fieldSplit[0]
fieldValue := fieldSplit[1]
switch fieldName {
case "UUID":
bd.UUID = fieldValue
case "PARTUUID":
bd.PartUUID = fieldValue
case "LABEL":
bd.Label = fieldValue
case "PARTLABEL":
bd.PartLabel = fieldValue
case "TYPE":
bd.Type = fieldValue
}
}
blockDevices = append(blockDevices, bd)
}
return blockDevices
}
+4 -1
View File
@@ -2,4 +2,7 @@ module enit
go 1.23.4 go 1.23.4
require golang.org/x/sys v0.31.0 require (
github.com/mitchellh/go-ps v1.0.0
golang.org/x/sys v0.31.0
)
+2
View File
@@ -1,2 +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 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+68 -22
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.")
@@ -134,12 +136,9 @@ func startServiceManager() {
fmt.Print("Initializing service manager... ") fmt.Print("Initializing service manager... ")
cmd := exec.Command("/sbin/esvm", path.Join(runstatedir, "esvm"), path.Join(sysconfdir, "esvm")) cmd := exec.Command("/sbin/esvm", path.Join(runstatedir, "esvm"), path.Join(sysconfdir, "esvm"))
cmd.Stdout = os.Stdout
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
@@ -174,7 +173,7 @@ func stopServiceManager() {
case <-exited: case <-exited:
fmt.Println("Done.") fmt.Println("Done.")
return return
case <-time.After(60 * time.Second): case <-time.After(300 * time.Second):
log.Println("Could not stop service manager!") log.Println("Could not stop service manager!")
syscall.Kill(serviceManagerPid, syscall.SIGKILL) syscall.Kill(serviceManagerPid, syscall.SIGKILL)
return return
@@ -185,6 +184,42 @@ func stopServiceManager() {
} }
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.")
}
func setHostname() { func setHostname() {
fmt.Print("Setting hostname... ") fmt.Print("Setting hostname... ")
@@ -215,16 +250,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 +268,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 +287,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 +301,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()
}
+253 -18
View File
@@ -1,10 +1,14 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"log"
"os" "os"
"slices" "slices"
"strings" "strings"
"syscall"
"time"
"unsafe" "unsafe"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -28,12 +32,14 @@ var flagsEquivalence = map[string]uintptr{
} }
// Split string flags to mount flags and mount data // Split string flags to mount flags and mount data
func convertMountOptions(options string) (flags []uintptr, data string) { func convertMountOptions(options string) (flags []uintptr, data string, extra []string) {
for _, flag := range strings.Split(options, ",") { for _, flag := range strings.Split(options, ",") {
if unixFlag, ok := flagsEquivalence[flag]; ok { if unixFlag, ok := flagsEquivalence[flag]; ok {
flags = append(flags, unixFlag) flags = append(flags, unixFlag)
} else { } else {
if data == "" { if flag == "noauto" || flag == "nofail" {
extra = append(extra, flag)
} else if data == "" {
data = flag data = flag
} else { } else {
data += "," + flag data += "," + flag
@@ -41,7 +47,7 @@ func convertMountOptions(options string) (flags []uintptr, data string) {
} }
} }
return flags, data return flags, data, extra
} }
// Combine a unix flag slice or array into a single uintptr // Combine a unix flag slice or array into a single uintptr
@@ -84,7 +90,7 @@ func isMountpoint(mountpoint string) bool {
} }
func mount(source, target, fstype string, options string, mkdir bool) error { func mount(source, target, fstype string, options string, mkdir bool) error {
flags, data := convertMountOptions(options) flags, data, _ := convertMountOptions(options)
if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) { if isMountpoint(target) && !slices.Contains(flags, unix.MS_REMOUNT) {
flags = append(flags, unix.MS_REMOUNT) flags = append(flags, unix.MS_REMOUNT)
@@ -104,37 +110,125 @@ func mount(source, target, fstype string, options string, mkdir bool) error {
return nil return nil
} }
func mountFstabEntries() error { func mountFstabEntries() (error, int) {
if _, err := os.Stat("/etc/fstab"); os.IsNotExist(err) { if _, err := os.Stat("/etc/fstab"); os.IsNotExist(err) {
return nil return nil, 0
} else if err != nil { } else if err != nil {
return err return err, 0
} }
bytes, err := os.ReadFile("/etc/fstab") bytes, err := os.ReadFile("/etc/fstab")
if err != nil { if err != nil {
return err return err, 0
} }
swapPriority := -2 swapPriority := -2
for _, line := range strings.Split(string(bytes), "\n") { for i, line := range strings.Split(string(bytes), "\n") {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") || line == "" { if strings.HasPrefix(line, "#") || line == "" {
continue continue
} }
source := strings.Split(line, " ")[0] // Get fields from line
target := strings.Split(line, " ")[1] fields := []string{}
fstype := strings.Split(line, " ")[2] sb := &strings.Builder{}
options := strings.Split(line, " ")[3] 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]
flags, data := convertMountOptions(options) // Convert mount options
flags, data, extra := convertMountOptions(options)
if slices.Contains(strings.Split(data, ","), "noauto") { // Skip if noauto flag is set
if slices.Contains(extra, "noauto") {
continue continue
} }
// Get block devices
blockDevices := GetBlockDevices()
// Replace device prefixes
if strings.Contains(source, "=") {
fieldSplit := strings.SplitN(source, "=", 2)
if len(fieldSplit) != 2 {
if slices.Contains(extra, "nofail") {
fmt.Printf("Warning: could not mount fstab entry on line %d: Formatting error\n", i+1)
continue
} else {
return fmt.Errorf("Formatting error"), i + 1
}
}
fieldName := fieldSplit[0]
fieldValue := fieldSplit[1]
// Reset source field
source = ""
for _, bd := range blockDevices {
bdField := ""
switch fieldName {
case "LABEL":
bdField = bd.Label
case "UUID":
bdField = bd.UUID
case "PARTLABEL":
bdField = bd.PartLabel
case "PARTUUID":
bdField = bd.PartUUID
default:
if slices.Contains(extra, "nofail") {
fmt.Printf("Warning: could not mount fstab entry on line %d: Formatting error\n", i+1)
continue
} else {
return fmt.Errorf("Formatting error"), i + 1
}
}
if bdField == "" {
continue
}
if bdField == fieldValue {
source = bd.Device
break
}
}
// Ensure source was set
if source == "" {
if slices.Contains(extra, "nofail") {
fmt.Printf("Warning: could not mount fstab entry on line %d: could not resolve %s=\"%s\"\n", i+1, fieldName, fieldValue)
continue
} else {
return fmt.Errorf("could not resolve %s=\"%s\"", fieldName, fieldValue), i + 1
}
}
}
if fstype == "swap" { if fstype == "swap" {
b := append([]byte(source), 0) b := append([]byte(source), 0)
const SwapFlagPrioShift = 0 const SwapFlagPrioShift = 0
@@ -142,7 +236,11 @@ func mountFstabEntries() error {
_, _, err := unix.Syscall(unix.SYS_SWAPON, uintptr(unsafe.Pointer(&b[0])), uintptr((swapPriority<<SwapFlagPrioShift)&SwapFlagPrioMask), 0) _, _, err := unix.Syscall(unix.SYS_SWAPON, uintptr(unsafe.Pointer(&b[0])), uintptr((swapPriority<<SwapFlagPrioShift)&SwapFlagPrioMask), 0)
swapPriority-- swapPriority--
if err != 0 { if err != 0 {
return fmt.Errorf("swapon syscall returned none-zero error code: %d", err) 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 continue
} }
@@ -152,9 +250,146 @@ func mountFstabEntries() error {
} }
if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil { if err := unix.Mount(source, target, fstype, combineUnixFlags(flags), data); err != nil {
return err 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 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.")
}
} }
+35 -65
View File
@@ -14,8 +14,6 @@ import (
"strings" "strings"
"syscall" "syscall"
"time" "time"
"gopkg.in/yaml.v3"
) )
// Build-time variables // Build-time variables
@@ -135,67 +133,13 @@ func Init() {
// Read and initialize service files // Read and initialize service files
for _, entry := range dirEntries { for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".esv") { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".esv") {
logger.Printf("Initializing service (%s)...\n", entry.Name()) filepath := path.Join(serviceConfigDir, "services", entry.Name())
bytes, err := os.ReadFile(path.Join(serviceConfigDir, "services", entry.Name())) LoadService(filepath)
if err != nil {
logger.Printf("Error: Could not read service file (%s)", path.Join(serviceConfigDir, "services", entry.Name()))
continue
}
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,
}
if err := yaml.Unmarshal(bytes, &service); err != nil {
logger.Printf("Error: could not read service file %s", path.Join(serviceConfigDir, "services", entry.Name()))
continue
}
for _, sv := range Services {
if sv.Name == service.Name {
logger.Printf("Error: service with name (%s) has already been initialized", service.Name)
}
}
switch service.Type {
case "simple", "background":
default:
logger.Printf("Error: unknown service type (%s)", service.Type)
continue
}
switch service.ExitMethod {
case "stop_command", "kill":
default:
logger.Printf("Error: unknown exit method (%s)\n", service.ExitMethod)
continue
}
switch service.Restart {
case "true", "always":
default:
service.Restart = "false"
}
Services = append(Services, &service)
logger.Printf("Service (%s) has been initialized!\n", service.Name)
} }
} }
// Read enabled services // Read enabled services
ReadEnabledServices() EnabledServices := ReadEnabledServices()
// Start enabled services // Start enabled services
stages := slices.Collect(maps.Keys(EnabledServices)) stages := slices.Collect(maps.Keys(EnabledServices))
@@ -213,13 +157,11 @@ func Init() {
continue continue
} }
if len(service.GetUnmetDependencies()) == 0 { err := service.StartService()
err := service.StartService() if err != nil {
if err != nil { logger.Printf("Error: could not start service (%s): %s", service.Name, err)
logger.Printf("Error: could not start service (%s): %s", service.Name, err)
}
remainingServices--
} }
remainingServices--
} }
} }
} }
@@ -227,6 +169,34 @@ func Init() {
logger.Println("ESVM initialized successfully!") 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() { func Destroy() {
logger.Println("Stopping all ESVM services...") logger.Println("Stopping all ESVM services...")
+251 -114
View File
@@ -1,12 +1,15 @@
package main package main
import ( import (
"crypto/sha256"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path" "path"
"slices" "slices"
"strconv"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@@ -37,46 +40,29 @@ var EnitServiceStateNames map[EnitServiceState]string = map[EnitServiceState]str
} }
type EnitService struct { type EnitService struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Description string `yaml:"description,omitempty"` Description string `yaml:"description,omitempty"`
Dependencies []string `yaml:"dependencies,omitempty"` Type string `yaml:"type"`
Type string `yaml:"type"` StartCmd string `yaml:"start_cmd"`
StartCmd string `yaml:"start_cmd"` CrashOnSafeExit bool `yaml:"crash_on_safe_exit"`
ExitMethod string `yaml:"exit_method"` StopCmd string `yaml:"stop_cmd,omitempty"`
CrashOnSafeExit bool `yaml:"crash_on_safe_exit"` User string `yaml:"user,omitempty"`
StopCmd string `yaml:"stop_cmd,omitempty"` Restart string `yaml:"restart,omitempty"`
Restart string `yaml:"restart,omitempty"` ReadyFd int `yaml:"ready_fd"`
ReadyFd int `yaml:"ready_fd"` Setpgid bool `yaml:"setpgid"`
LogOutput bool `yaml:"log_output,omitempty"` LogOutput bool `yaml:"log_output,omitempty"`
state EnitServiceState Filepath string
processID int filepathChecksum [32]byte
restartCount int state EnitServiceState
stopChannel chan bool processID int
restartCount int
stopChannel chan bool
shouldReload bool
} }
var Services = make([]*EnitService, 0) var Services = make([]*EnitService, 0)
var EnabledServices = make(map[int][]string)
var startedServicesOrder = make([]string, 0) var startedServicesOrder = make([]string, 0)
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 { func (service *EnitService) GetProcess() *os.Process {
process, _ := os.FindProcess(service.processID) process, _ := os.FindProcess(service.processID)
@@ -117,6 +103,113 @@ func (service *EnitService) GetLogFile() (file *os.File, err error) {
return file, nil 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) { func (service *EnitService) StartService() (err error) {
if service == nil { if service == nil {
return nil return nil
@@ -137,10 +230,39 @@ func (service *EnitService) StartService() (err error) {
} }
cmd := exec.Command("/bin/sh", "-c", "exec "+service.StartCmd) 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 { if logFile != nil {
cmd.Stdout = logFile cmd.Stdout = logFile
cmd.Stderr = 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 var pipeReader, pipeWriter *os.File
if service.ReadyFd > 2 { if service.ReadyFd > 2 {
pipeReader, pipeWriter, err = os.Pipe() pipeReader, pipeWriter, err = os.Pipe()
@@ -168,6 +290,7 @@ func (service *EnitService) StartService() (err error) {
} }
cmd.ExtraFiles = append(cmd.ExtraFiles, pipeWriter) cmd.ExtraFiles = append(cmd.ExtraFiles, pipeWriter)
} }
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
// Close log file if not nil // Close log file if not nil
if logFile != nil { if logFile != nil {
@@ -177,6 +300,7 @@ func (service *EnitService) StartService() (err error) {
return err return err
} }
pid := cmd.Process.Pid
service.processID = cmd.Process.Pid service.processID = cmd.Process.Pid
service.state = EnitServiceStarting service.state = EnitServiceStarting
@@ -190,8 +314,8 @@ func (service *EnitService) StartService() (err error) {
logFile.Close() logFile.Close()
} }
// Kill process // Kill process and children
cmd.Process.Kill() syscall.Kill(-pid, syscall.SIGKILL)
service.processID = 0 service.processID = 0
service.state = EnitServiceCrashed service.state = EnitServiceCrashed
@@ -202,6 +326,12 @@ func (service *EnitService) StartService() (err error) {
service.state = EnitServiceRunning service.state = EnitServiceRunning
// Set PID to 0 for simple services with a stop command
if service.Type == "simple" && service.StopCmd != "" {
pid = 0
service.processID = 0
}
go func() { go func() {
err := cmd.Wait() err := cmd.Wait()
@@ -214,12 +344,20 @@ func (service *EnitService) StartService() (err error) {
case <-service.stopChannel: case <-service.stopChannel:
service.restartCount = 0 service.restartCount = 0
default: default:
// Kill remaining child processes
if pid != 0 {
syscall.Kill(-pid, syscall.SIGKILL)
}
if service.Type == "simple" && err == nil { if service.Type == "simple" && err == nil {
service.restartCount = 0 service.restartCount = 0
if service.ExitMethod != "stop_command" { if strings.TrimSpace(service.StopCmd) == "" {
service.state = EnitServiceCompleted service.state = EnitServiceCompleted
} else {
service.state = EnitServiceRunning // Reload service if needed
if service.shouldReload {
LoadService(service.Filepath)
}
} }
return return
} }
@@ -231,6 +369,15 @@ func (service *EnitService) StartService() (err error) {
service.state = EnitServiceCrashed 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" { if service.Restart == "always" {
_ = service.StartService() _ = service.StartService()
} else if service.Restart == "true" && service.restartCount < 5 { } else if service.Restart == "true" && service.restartCount < 5 {
@@ -258,14 +405,29 @@ func (service *EnitService) StopService() error {
} }
logger.Printf("Stopping service (%s)...", service.Name) logger.Printf("Stopping service (%s)...", service.Name)
pid := service.processID
newServiceStatus := EnitServiceCrashed newServiceStatus := EnitServiceCrashed
defer func() { defer func() {
// Kill remaining child processes
if pid != 0 {
syscall.Kill(-pid, syscall.SIGKILL)
}
service.state = newServiceStatus service.state = newServiceStatus
service.processID = 0 service.processID = 0
// Reload service if needed
if service.shouldReload {
LoadService(service.Filepath)
if GetServiceByName(service.Name) == nil {
return
}
service = GetServiceByName(service.Name)
}
}() }()
if service.ExitMethod == "kill" { if strings.TrimSpace(service.StopCmd) == "" {
if err := service.GetProcess().Signal(syscall.Signal(0)); err != nil { if err := service.GetProcess().Signal(syscall.Signal(0)); err != nil {
newServiceStatus = EnitServiceStopped newServiceStatus = EnitServiceStopped
logger.Printf("Service (%s) has stopped (Process already dead)", service.Name) logger.Printf("Service (%s) has stopped (Process already dead)", service.Name)
@@ -279,12 +441,46 @@ func (service *EnitService) StopService() error {
service.GetProcess().Signal(syscall.SIGKILL) service.GetProcess().Signal(syscall.SIGKILL)
return fmt.Errorf("could not stop process gracefully") 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 // Check if the process has stopped gracefully, otherwise send sigkill on timeout
exited := make(chan bool) exited := make(chan bool)
go func() { go func() {
for { for {
if err := service.GetProcess().Signal(syscall.Signal(0)); err != nil { if err := syscall.Kill(pid, syscall.Signal(0)); err != nil {
break break
} }
} }
@@ -293,15 +489,10 @@ func (service *EnitService) StopService() error {
select { select {
case <-exited: case <-exited:
case <-time.After(5 * time.Second): case <-time.After(15 * time.Second):
service.GetProcess().Signal(syscall.SIGKILL) service.GetProcess().Signal(syscall.SIGKILL)
return fmt.Errorf("could not stop process gracefully") return fmt.Errorf("could not stop process gracefully")
} }
} else {
cmd := exec.Command("/bin/sh", "-c", service.StopCmd)
if err := cmd.Run(); err != nil {
return err
}
} }
newServiceStatus = EnitServiceStopped newServiceStatus = EnitServiceStopped
@@ -315,6 +506,12 @@ func (service *EnitService) RestartService() error {
return err 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 { if err := service.StartService(); err != nil {
return err return err
} }
@@ -322,78 +519,18 @@ func (service *EnitService) RestartService() error {
return nil return nil
} }
// Functions will be rewritten at some point to allow enabling unloaded services func ReadEnabledServices() (EnabledServices map[int][]string) {
EnabledServices = make(map[int][]string)
func (service *EnitService) isEnabled() (bool, int) { data, err := os.ReadFile(path.Join(serviceConfigDir, "enabled-services.yml"))
for stage, services := range EnabledServices {
if slices.Contains(services, service.Name) {
return true, stage
}
}
return false, 0
}
func (service *EnitService) SetEnabled(stage int) error {
// Get current service enabled status
_, s := service.isEnabled()
// Return if service is already in correct state
if s == stage {
return nil
}
// Remove service from current stage
EnabledServices[s] = slices.DeleteFunc(EnabledServices[s], func(name string) bool {
return name == service.Name
})
if len(EnabledServices[s]) == 0 {
delete(EnabledServices, s)
}
// Add service to stage
if stage != 0 {
EnabledServices[stage] = append(EnabledServices[stage], service.Name)
}
// Save enabled services to file
data, err := yaml.Marshal(EnabledServices)
if err != nil { if err != nil {
return err return EnabledServices
}
err = os.WriteFile(path.Join(serviceConfigDir, "enabled_services"), data, 0644)
if err != nil {
return err
}
return nil
}
func ReadEnabledServices() error {
data, err := os.ReadFile(path.Join(serviceConfigDir, "enabled_services"))
if err != nil {
return err
} }
err = yaml.Unmarshal(data, &EnabledServices) err = yaml.Unmarshal(data, &EnabledServices)
if err != nil { if err != nil {
// Assume old plain text format return EnabledServices
for _, service := range strings.Split(strings.TrimSpace(string(data)), "\n") {
EnabledServices[3] = append(EnabledServices[3], service)
}
// Update enabled_services file
data, err := yaml.Marshal(EnabledServices)
if err != nil {
return err
}
err = os.WriteFile(path.Join(serviceConfigDir, "enabled_services"), data, 0644)
if err != nil {
return err
}
return nil
} }
return nil return EnabledServices
} }
+36 -70
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -17,10 +18,10 @@ func initSocket() (socket net.Listener, err error) {
} }
// Register command handlers // Register command handlers
commandHandlers["reload"] = handleReloadServicesCommand
commandHandlers["start"] = handleStartServiceCommand commandHandlers["start"] = handleStartServiceCommand
commandHandlers["stop"] = handleStopServiceCommand commandHandlers["stop"] = handleStopServiceCommand
commandHandlers["restart"] = handleRestartServiceCommand commandHandlers["restart"] = handleRestartServiceCommand
commandHandlers["set_enabled"] = handleSetEnabledServiceCommand
commandHandlers["status"] = handleStatusServiceCommand commandHandlers["status"] = handleStatusServiceCommand
commandHandlers["list"] = handleListServicesCommand commandHandlers["list"] = handleListServicesCommand
@@ -31,27 +32,23 @@ func listenToSocket() {
conn, err := socket.Accept() conn, err := socket.Accept()
if err != nil { if err != nil {
logger.Println("Could not accept socket connection!") logger.Println("Could not accept socket connection!")
panic(err) return
} }
// Handle the connection in a separate goroutine. // Handle the connection in a separate goroutine.
go func(conn net.Conn) { go func(conn net.Conn) {
defer conn.Close() defer conn.Close()
// Create a buffer for incoming data.
buf := make([]byte, 4096)
// Read data from the connection. // Read data from the connection.
n, err := conn.Read(buf) data, err := readAllConn(conn)
if err == io.EOF {
return
}
if err != nil { if err != nil {
logger.Fatalf("Could not read data from socket! Error: %s\n", err)
return return
} }
// Decoode JSON data // Decoode JSON data
var jsonData map[string]any var jsonData map[string]any
err = json.Unmarshal(buf[:n], &jsonData) err = json.Unmarshal(data, &jsonData)
if err != nil { if err != nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Invalid JSON"))) conn.Write(wrapErrorInJson(fmt.Errorf("Invalid JSON")))
return return
@@ -74,6 +71,13 @@ func listenToSocket() {
}(conn) }(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) { func handleStartServiceCommand(conn net.Conn, jsonData map[string]any) {
// Get service name from json data // Get service name from json data
serviceName, ok := jsonData["service"] serviceName, ok := jsonData["service"]
@@ -146,65 +150,6 @@ func handleRestartServiceCommand(conn net.Conn, jsonData map[string]any) {
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) has restarted sucessfully", serviceName.(string)))) conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) has restarted sucessfully", serviceName.(string))))
} }
func handleSetEnabledServiceCommand(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
}
// Get service stage from json json data
_serviceStage, ok := jsonData["stage"]
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'stage' field missing")))
return
}
serviceStage, ok := _serviceStage.(float64)
if !ok {
conn.Write(wrapErrorInJson(fmt.Errorf("'stage' field is not a number")))
return
}
// Ensure service exists
service := GetServiceByName(serviceName.(string))
if service == nil {
conn.Write(wrapErrorInJson(fmt.Errorf("Service (%s) not found", serviceName.(string))))
return
}
// Get current service enabled status
_, s := service.isEnabled()
// Return if service is already in correct state
if s == int(serviceStage) {
if serviceStage == 0 {
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) is already disabled", serviceName.(string))))
} else {
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) is already enabled", serviceName.(string))))
}
return
}
// Enable service
err := service.SetEnabled(int(serviceStage))
if err != nil {
if serviceStage == 0 {
conn.Write(wrapErrorInJson(fmt.Errorf("Could not disable service! Error: %s", err)))
} else {
conn.Write(wrapErrorInJson(fmt.Errorf("Could not enable service! Error: %s", err)))
}
return
}
if serviceStage == 0 {
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) was disabled sucessfully", serviceName.(string))))
} else {
conn.Write(wrapSuccessMsgInJson(fmt.Sprintf("Service (%s) was enabled sucessfully", serviceName.(string))))
}
}
func handleStatusServiceCommand(conn net.Conn, jsonData map[string]any) { func handleStatusServiceCommand(conn net.Conn, jsonData map[string]any) {
// Get service name from json data // Get service name from json data
serviceName, ok := jsonData["service"] serviceName, ok := jsonData["service"]
@@ -222,9 +167,9 @@ func handleStatusServiceCommand(conn net.Conn, jsonData map[string]any) {
statusMap := make(map[string]any) statusMap := make(map[string]any)
statusMap["name"] = service.Name statusMap["name"] = service.Name
statusMap["description"] = service.Description
statusMap["state"] = EnitServiceStateNames[service.state] statusMap["state"] = EnitServiceStateNames[service.state]
statusMap["process_id"] = service.processID statusMap["process_id"] = service.processID
statusMap["is_enabled"], statusMap["stage"] = service.isEnabled()
// Encode map to json string // Encode map to json string
newJsonData, err := json.Marshal(statusMap) newJsonData, err := json.Marshal(statusMap)
@@ -244,9 +189,9 @@ func handleListServicesCommand(conn net.Conn, _ map[string]any) {
for _, service := range Services { for _, service := range Services {
statusMap := make(map[string]any) statusMap := make(map[string]any)
statusMap["name"] = service.Name statusMap["name"] = service.Name
statusMap["description"] = service.Description
statusMap["state"] = EnitServiceStateNames[service.state] statusMap["state"] = EnitServiceStateNames[service.state]
statusMap["process_id"] = service.processID statusMap["process_id"] = service.processID
statusMap["is_enabled"], statusMap["stage"] = service.isEnabled()
servicesMap["services"] = append(servicesMap["services"].([]map[string]any), statusMap) servicesMap["services"] = append(servicesMap["services"].([]map[string]any), statusMap)
} }
@@ -293,3 +238,24 @@ func wrapSuccessMsgInJson(msg string) []byte {
} }
return jsonData 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
}