10 Commits
11 changed files with 692 additions and 608 deletions
+1 -13
View File
@@ -7,19 +7,7 @@ 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
-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
+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=
+21 -399
View File
@@ -1,18 +1,10 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"flag"
"fmt" "fmt"
"io"
"log" "log"
"net"
"os" "os"
"path"
"strconv"
"syscall" "syscall"
"time"
) )
// Build-time variables // Build-time variables
@@ -20,413 +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 case "sv", "service":
} else if flag.Args()[0] == "service" || flag.Args()[0] == "sv" { handleServiceSubcommand()
if len(flag.Args()) <= 1 { default:
fmt.Println("Usage: ectl service <reload/start/stop/enable/disable/status/list> [service]")
return
}
if flag.Arg(1) == "reload" {
type ServiceCommandJsonStruct struct {
Command string `json:"command"`
Service string `json:"service"`
}
serviceCommandJson := ServiceCommandJsonStruct{
Command: flag.Arg(1),
}
// Encode struct to json string
jsonData, err := json.Marshal(serviceCommandJson)
if err != nil {
log.Fatalf("Could not encode JSON data! Error: %s\n", err)
}
_, err = conn.Write(jsonData)
if err != nil {
log.Fatalf("Could not write JSON data to socket! Error: %s\n", err)
}
// 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!")
}
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)
}
// 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!")
}
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)
}
// 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!")
}
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)
}
// 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)
}
serviceState := returnedJsonData["state"].(string)
serviceDescription := returnedJsonData["description"].(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("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)
}
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)
}
// 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()
}
return
}
}
printUsage() printUsage()
os.Exit(1) 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)
}
}
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
} }
+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
}
+1 -1
View File
@@ -175,7 +175,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
+3 -25
View File
@@ -280,25 +280,14 @@ func unmountFilesystems() {
// Unmount filesystem at mountpoint // Unmount filesystem at mountpoint
fmt.Printf("Unmounting %s...", mountpoint) fmt.Printf("Unmounting %s...", mountpoint)
tries := 0
for {
err := unix.Unmount(mountpoint, 0) err := unix.Unmount(mountpoint, 0)
if errors.Is(err, syscall.EBUSY) { if errors.Is(err, syscall.EBUSY) {
fmt.Print(".") fmt.Println(" Busy.")
tries++
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if tries >= 60 {
unix.Unmount(mountpoint, syscall.MNT_FORCE)
fmt.Println(" Timeout.")
break
}
} else if err != nil { } else if err != nil {
fmt.Printf(" Error: %s\n", err.Error()) fmt.Printf(" Error: %s\n", err.Error())
break
} else { } else {
fmt.Println(" Done.") fmt.Println(" Done.")
break
}
} }
} }
} }
@@ -341,23 +330,12 @@ func remountRootReadonly() {
} }
} }
tries := 0 err = unix.Mount(source, "/", filesystem, syscall.MS_RDONLY|syscall.MS_REMOUNT, fsData)
for {
err := unix.Mount(source, "/", filesystem, syscall.MS_RDONLY|syscall.MS_REMOUNT, fsData)
if errors.Is(err, syscall.EBUSY) { if errors.Is(err, syscall.EBUSY) {
fmt.Print(".") fmt.Println(" Busy.")
tries++
time.Sleep(1 * time.Second)
if tries >= 60 {
fmt.Println(" Timeout.")
break
}
} else if err != nil { } else if err != nil {
fmt.Printf(" Error: %s\n", err.Error()) fmt.Printf(" Error: %s\n", err.Error())
break
} else { } else {
fmt.Println(" Done.") fmt.Println(" Done.")
break
}
} }
} }
+64 -80
View File
@@ -6,8 +6,10 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"os/user"
"path" "path"
"slices" "slices"
"strconv"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@@ -42,9 +44,9 @@ type EnitService struct {
Description string `yaml:"description,omitempty"` Description string `yaml:"description,omitempty"`
Type string `yaml:"type"` Type string `yaml:"type"`
StartCmd string `yaml:"start_cmd"` StartCmd string `yaml:"start_cmd"`
ExitMethod string `yaml:"exit_method"`
CrashOnSafeExit bool `yaml:"crash_on_safe_exit"` CrashOnSafeExit bool `yaml:"crash_on_safe_exit"`
StopCmd string `yaml:"stop_cmd,omitempty"` StopCmd string `yaml:"stop_cmd,omitempty"`
User string `yaml:"user,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"` Setpgid bool `yaml:"setpgid"`
@@ -154,8 +156,8 @@ func LoadService(filepath string) {
Description: "", Description: "",
Type: "", Type: "",
StartCmd: "", StartCmd: "",
ExitMethod: "",
StopCmd: "", StopCmd: "",
User: "",
Restart: "", Restart: "",
Setpgid: true, Setpgid: true,
CrashOnSafeExit: true, CrashOnSafeExit: true,
@@ -190,13 +192,6 @@ func LoadService(filepath string) {
return return
} }
switch newService.ExitMethod {
case "stop_command", "kill":
default:
logger.Printf("Error: unknown exit method (%s)\n", newService.ExitMethod)
return
}
switch newService.Restart { switch newService.Restart {
case "true", "always": case "true", "always":
default: default:
@@ -236,10 +231,38 @@ 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} 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()
@@ -267,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 {
@@ -319,19 +343,14 @@ func (service *EnitService) StartService() (err error) {
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
// Reload service if needed // Reload service if needed
if service.shouldReload { if service.shouldReload {
LoadService(service.Filepath) LoadService(service.Filepath)
if GetServiceByName(service.Name) == nil {
return
} }
} }
} else {
service.state = EnitServiceRunning
}
return return
} }
if !service.CrashOnSafeExit { if !service.CrashOnSafeExit {
@@ -400,7 +419,7 @@ func (service *EnitService) StopService() error {
} }
}() }()
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)
@@ -418,11 +437,37 @@ func (service *EnitService) StopService() error {
go func() { service.stopChannel <- true }() go func() { service.stopChannel <- true }()
cmd := exec.Command("/bin/sh", "-c", service.StopCmd) 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 { if err := cmd.Run(); err != nil {
return err 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() {
@@ -436,10 +481,11 @@ 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")
} }
}
newServiceStatus = EnitServiceStopped newServiceStatus = EnitServiceStopped
logger.Printf("Service (%s) has stopped!\n", service.Name) logger.Printf("Service (%s) has stopped!\n", service.Name)
@@ -465,80 +511,18 @@ func (service *EnitService) RestartService() error {
return nil return nil
} }
func (service *EnitService) isEnabled() (bool, int) {
for stage, services := range ReadEnabledServices() {
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
}
EnabledServices := ReadEnabledServices()
// 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 {
return err
}
err = os.WriteFile(path.Join(serviceConfigDir, "enabled_services"), data, 0644)
if err != nil {
return err
}
return nil
}
func ReadEnabledServices() (EnabledServices map[int][]string) { func ReadEnabledServices() (EnabledServices map[int][]string) {
EnabledServices = make(map[int][]string) EnabledServices = make(map[int][]string)
data, err := os.ReadFile(path.Join(serviceConfigDir, "enabled_services")) data, err := os.ReadFile(path.Join(serviceConfigDir, "enabled-services.yml"))
if err != nil { if err != nil {
return EnabledServices return EnabledServices
} }
err = yaml.Unmarshal(data, &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 file
data, err := yaml.Marshal(EnabledServices)
if err != nil {
return EnabledServices
}
err = os.WriteFile(path.Join(serviceConfigDir, "enabled_services"), data, 0644)
if err != nil { if err != nil {
return EnabledServices return EnabledServices
} }
return EnabledServices return EnabledServices
} }
return EnabledServices
}
-62
View File
@@ -22,7 +22,6 @@ func initSocket() (socket net.Listener, err error) {
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
@@ -151,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"]
@@ -230,7 +170,6 @@ func handleStatusServiceCommand(conn net.Conn, jsonData map[string]any) {
statusMap["description"] = service.Description 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)
@@ -253,7 +192,6 @@ func handleListServicesCommand(conn net.Conn, _ map[string]any) {
statusMap["description"] = service.Description 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)
} }