init
This commit is contained in:
commit
b38c39030b
22 changed files with 1925 additions and 0 deletions
149
internal/app/systemd_service.go
Normal file
149
internal/app/systemd_service.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a *App) renderSystemdForProject(ctx context.Context, p Project) error {
|
||||
envVars, err := a.listEnvVars(ctx, p.UserID, p.ID)
|
||||
if err != nil {
|
||||
if err.Error() == "project not found" {
|
||||
return err
|
||||
}
|
||||
envVars = []EnvVar{}
|
||||
}
|
||||
projectDir := filepath.Join(a.cfg.ProjectRoot, slugify(p.UserID), p.Workspace, p.Slug)
|
||||
if err := os.MkdirAll(projectDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := filepath.Join(projectDir, "data")
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
socketPath := filepath.Join(projectDir, "app.sock")
|
||||
envPath := filepath.Join(projectDir, ".env")
|
||||
|
||||
values := map[string]string{}
|
||||
for _, v := range envVars {
|
||||
values[v.Key] = v.Value
|
||||
}
|
||||
values["LISTEN_NETWORK"] = "unix"
|
||||
values["LISTEN_ADDRESS"] = socketPath
|
||||
|
||||
var b strings.Builder
|
||||
keys := make([]string, 0, len(values))
|
||||
for k := range values {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
b.WriteString(k)
|
||||
b.WriteString("=")
|
||||
b.WriteString(escapeEnvValue(values[k]))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if err := os.WriteFile(envPath, []byte(b.String()), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unit := fmt.Sprintf(`[Unit]
|
||||
Description=Project %s
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=%s
|
||||
EnvironmentFile=%s
|
||||
ExecStart=%s
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
`, p.Name, dataDir, envPath, a.cfg.DefaultRunCommand)
|
||||
|
||||
homeDir, err := homeForUnixUser(p.UnixUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
serviceDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||
if err := os.MkdirAll(serviceDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
servicePath := filepath.Join(serviceDir, p.ServiceName)
|
||||
if err := os.WriteFile(servicePath, []byte(unit), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "daemon-reload")
|
||||
_ = runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "enable", "--now", p.ServiceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func homeForUnixUser(username string) (string, error) {
|
||||
u, err := user.Lookup(username)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup unix user %q failed: %w", username, err)
|
||||
}
|
||||
if u.HomeDir == "" {
|
||||
return "", fmt.Errorf("unix user %q has no home directory", username)
|
||||
}
|
||||
return u.HomeDir, nil
|
||||
}
|
||||
|
||||
type ServiceStatus struct {
|
||||
Unit string `json:"unit"`
|
||||
ActiveState string `json:"active_state"`
|
||||
SubState string `json:"sub_state"`
|
||||
MainPID int `json:"main_pid"`
|
||||
ExecStatus int `json:"exec_status"`
|
||||
Result string `json:"result"`
|
||||
FragmentPath string `json:"fragment_path"`
|
||||
}
|
||||
|
||||
func (a *App) getServiceStatus(p Project) (ServiceStatus, error) {
|
||||
out, err := runCmdOut(
|
||||
"sudo", "-n", "-u", p.UnixUser,
|
||||
"systemctl", "--user", "show", p.ServiceName, "--no-pager",
|
||||
"--property=ActiveState,SubState,MainPID,ExecMainStatus,Result,FragmentPath",
|
||||
)
|
||||
if err != nil {
|
||||
return ServiceStatus{}, err
|
||||
}
|
||||
status := ServiceStatus{Unit: p.ServiceName}
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if line == "" || !strings.Contains(line, "=") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
k, v := parts[0], parts[1]
|
||||
switch k {
|
||||
case "ActiveState":
|
||||
status.ActiveState = v
|
||||
case "SubState":
|
||||
status.SubState = v
|
||||
case "MainPID":
|
||||
status.MainPID, _ = strconv.Atoi(v)
|
||||
case "ExecMainStatus":
|
||||
status.ExecStatus, _ = strconv.Atoi(v)
|
||||
case "Result":
|
||||
status.Result = v
|
||||
case "FragmentPath":
|
||||
status.FragmentPath = v
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (a *App) getServiceLogs(p Project, lines int) (string, error) {
|
||||
return runCmdOut(
|
||||
"sudo", "-n", "-u", p.UnixUser,
|
||||
"journalctl", "--user", "-u", p.ServiceName, "--no-pager", "-n", strconv.Itoa(lines), "-o", "short-iso",
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue