13 Commits
16 changed files with 303 additions and 54 deletions
+18 -11
View File
@@ -1,12 +1,14 @@
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)
# Build-time variables
VERSION ?= $(shell git describe --tags --dirty) VERSION ?= $(shell git describe --tags --dirty)
build: build:
@@ -15,18 +17,23 @@ build:
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
View File
@@ -2,7 +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 setpgid: false
-1
View File
@@ -2,7 +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 setpgid: false
-1
View File
@@ -2,7 +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 setpgid: false
-1
View File
@@ -2,7 +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 setpgid: false
-1
View File
@@ -2,7 +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 setpgid: false
-1
View File
@@ -2,7 +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 setpgid: false
-2
View File
@@ -2,5 +2,3 @@ name: lo-interface
description: Enable loopback interface on boot description: Enable loopback interface on boot
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
+53 -17
View File
@@ -35,8 +35,12 @@ func handleServiceSubcommand() {
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:]) 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 // Dial esvm socket
dialSocket() err := dialSocket()
if err == nil {
defer conn.Close() defer conn.Close()
} else {
log.Fatalf("Error: %s", err)
}
startStopRestartService(subcommand) startStopRestartService(subcommand)
case "enable", "disable": case "enable", "disable":
@@ -53,8 +57,12 @@ func handleServiceSubcommand() {
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s status <options> <service>", os.Args[1]), "Show service status", os.Args[3:]) setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s status <options> <service>", os.Args[1]), "Show service status", os.Args[3:])
// Dial esvm socket // Dial esvm socket
dialSocket() err := dialSocket()
if err == nil {
defer conn.Close() defer conn.Close()
} else {
log.Fatalf("Error: %s", err)
}
showServiceStatus() showServiceStatus()
case "list": case "list":
@@ -64,8 +72,12 @@ func handleServiceSubcommand() {
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "List all services", os.Args[3:]) setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "List all services", os.Args[3:])
// Dial esvm socket // Dial esvm socket
dialSocket() err := dialSocket()
if err == nil {
defer conn.Close() defer conn.Close()
} else {
log.Fatalf("Error: %s", err)
}
listAllServices() listAllServices()
case "reload": case "reload":
@@ -75,8 +87,12 @@ func handleServiceSubcommand() {
setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "Reload all services", os.Args[3:]) setupFlagsAndHelp(currentFlagSet, fmt.Sprintf("ectl %s reload <options>", os.Args[1]), "Reload all services", os.Args[3:])
// Dial esvm socket // Dial esvm socket
dialSocket() err := dialSocket()
if err == nil {
defer conn.Close() defer conn.Close()
} else {
log.Fatalf("Error: %s", err)
}
reloadAllServices() reloadAllServices()
default: default:
@@ -174,6 +190,18 @@ func enableDisableService(subcommand string) {
verb = "disabled" verb = "disabled"
} }
// Ensure service exists
if stage != 0 {
if !serviceExists(service) {
if printJson {
fmt.Printf("{\"error\":\"Service (%s) does not exist\"}\n", verb)
} else {
fmt.Printf("Service (%s) does not exist\n", verb)
}
os.Exit(1)
}
}
// Return if service is already enabled // Return if service is already enabled
if _, enabledStage := isServiceEnabled(service); enabledStage == stage { if _, enabledStage := isServiceEnabled(service); enabledStage == stage {
if printJson { if printJson {
@@ -285,7 +313,7 @@ func showServiceStatus() {
} else { } else {
fmt.Printf("Enabled: %t\n", serviceEnabled) fmt.Printf("Enabled: %t\n", serviceEnabled)
} }
if serviceState == "running" { if serviceState == "running" && processID > 0 {
fmt.Printf("Process ID: %d\n", processID) fmt.Printf("Process ID: %d\n", processID)
} }
} }
@@ -319,12 +347,6 @@ func listAllServices() {
return return
} }
// Print json data if flag is set
if printJson {
fmt.Println(string(data))
return
}
// Decoode JSON data // Decoode JSON data
var returnedJsonData map[string]any var returnedJsonData map[string]any
err = json.Unmarshal(data, &returnedJsonData) err = json.Unmarshal(data, &returnedJsonData)
@@ -336,12 +358,24 @@ func listAllServices() {
log.Fatal(err) 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) { for _, serviceMap := range returnedJsonData["services"].([]any) {
serviceName := serviceMap.(map[string]any)["name"].(string) serviceName := serviceMap.(map[string]any)["name"].(string)
serviceDescription := serviceMap.(map[string]any)["description"].(string) serviceDescription := serviceMap.(map[string]any)["description"].(string)
serviceState := serviceMap.(map[string]any)["state"].(string) serviceState := serviceMap.(map[string]any)["state"].(string)
serviceEnabled := serviceMap.(map[string]any)["is_enabled"].(bool) serviceEnabled := serviceMap.(map[string]any)["is_enabled"].(bool)
serviceStage := int(serviceMap.(map[string]any)["stage"].(float64)) serviceStage := int(serviceMap.(map[string]any)["stage"].(int))
processID := int(serviceMap.(map[string]any)["process_id"].(float64)) processID := int(serviceMap.(map[string]any)["process_id"].(float64))
fmt.Printf("Name: %s\n", serviceName) fmt.Printf("Name: %s\n", serviceName)
@@ -352,7 +386,7 @@ func listAllServices() {
} else { } else {
fmt.Printf("Enabled: %t\n", serviceEnabled) fmt.Printf("Enabled: %t\n", serviceEnabled)
} }
if serviceState == "running" { if serviceState == "running" && processID > 0 {
fmt.Printf("Process ID: %d\n", processID) fmt.Printf("Process ID: %d\n", processID)
} }
fmt.Println() fmt.Println()
@@ -438,20 +472,22 @@ func setupFlagsAndHelp(flagset *flag.FlagSet, usage, desc string, args []string)
flagset.Parse(args) flagset.Parse(args)
} }
func dialSocket() { func dialSocket() error {
if _, err := os.Stat(path.Join(runstatedir, "esvm/esvm.sock")); err != nil { if _, err := os.Stat(path.Join(runstatedir, "esvm/esvm.sock")); err != nil {
log.Fatalf("Could not find esvm.sock! Error: %s\n", err) return fmt.Errorf("could not find socket! Error: %s", err)
} }
var err error var err error
conn, err = net.Dial("unix", path.Join(runstatedir, "esvm/esvm.sock")) conn, err = net.Dial("unix", path.Join(runstatedir, "esvm/esvm.sock"))
if err != nil { if err != nil {
log.Fatalf("Failed to connect to esvm.sock! Error: %s\n", err) return fmt.Errorf("could not connect to socket! Error: %s", err)
} }
if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil { if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
log.Fatalf("Failed to set write deadline! Error: %s\n", err) return fmt.Errorf("failed to set socket deadline! Error: %s", err)
} }
return nil
} }
func readAllConn(conn net.Conn) ([]byte, error) { func readAllConn(conn net.Conn) ([]byte, error) {
+24
View File
@@ -9,6 +9,30 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
func serviceExists(service string) bool {
dirEntries, err := os.ReadDir(path.Join(sysconfdir, "esvm/services"))
if err != nil {
return false
}
for _, entry := range dirEntries {
var dataMap map[string]any
data, err := os.ReadFile(path.Join(sysconfdir, "esvm/services", entry.Name()))
err = yaml.Unmarshal(data, &dataMap)
if err != nil {
return false
}
if name, ok := dataMap["name"]; ok && name == service {
return true
}
}
return false
}
func isServiceEnabled(service string) (bool, int) { func isServiceEnabled(service string) (bool, int) {
for stage, services := range readEnabledServices() { for stage, services := range readEnabledServices() {
if slices.Contains(services, service) { if slices.Contains(services, service) {
+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
}
+19 -2
View File
@@ -51,6 +51,8 @@ func main() {
mountVirtualFilesystems() mountVirtualFilesystems()
// Mount filesystems in fstab // Mount filesystems in fstab
mountFilesystems() mountFilesystems()
// Run sysctl
initSysctl()
// Set hostname // Set hostname
setHostname() setHostname()
// Start service manager // Start service manager
@@ -136,8 +138,6 @@ 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 {
printErrorAndReboot("Error: could not initialize service manager: %s", err) printErrorAndReboot("Error: could not initialize service manager: %s", err)
@@ -222,6 +222,23 @@ func killProcesses() {
fmt.Println("Done.") fmt.Println("Done.")
} }
func initSysctl() {
// Return if sysctl cannot be found
if _, err := os.Stat("/sbin/sysctl"); err != nil {
return
}
fmt.Print("Running sysctl...")
err := exec.Command("/sbin/sysctl", "--system").Run()
if err != nil {
log.Println("Failed!")
return
}
fmt.Println("Done.")
}
func setHostname() { func setHostname() {
fmt.Print("Setting hostname... ") fmt.Print("Setting hostname... ")
+65 -11
View File
@@ -158,23 +158,77 @@ func mountFstabEntries() (error, int) {
fstype := fields[2] fstype := fields[2]
options := fields[3] options := fields[3]
// Replace device prefixes // Convert mount options
if cutSource, ok := strings.CutPrefix(source, "LABEL="); ok {
source = "/dev/disk/by-label/" + strings.ReplaceAll(cutSource, " ", "\\x20")
} else if cutSource, ok := strings.CutPrefix(source, "UUID="); ok {
source = "/dev/disk/by-uuid/" + cutSource
} else if cutSource, ok := strings.CutPrefix(source, "PARTLABEL="); ok {
source = "/dev/disk/by-partlabel/" + cutSource
} else if cutSource, ok := strings.CutPrefix(source, "PARTUUID="); ok {
source = "/dev/disk/by-partuuid/" + cutSource
}
flags, data, extra := convertMountOptions(options) flags, data, extra := convertMountOptions(options)
// Skip if noauto flag is set
if slices.Contains(extra, "noauto") { 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
+8
View File
@@ -326,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()
@@ -339,7 +345,9 @@ func (service *EnitService) StartService() (err error) {
service.restartCount = 0 service.restartCount = 0
default: default:
// Kill remaining child processes // Kill remaining child processes
if pid != 0 {
syscall.Kill(-pid, syscall.SIGKILL) syscall.Kill(-pid, syscall.SIGKILL)
}
if service.Type == "simple" && err == nil { if service.Type == "simple" && err == nil {
service.restartCount = 0 service.restartCount = 0