Skip to content

Latest commit

 

History

History
549 lines (484 loc) · 13.2 KB

File metadata and controls

549 lines (484 loc) · 13.2 KB
mkdir -p project/internal/domain
mkdir -p project/internal/ports/input
mkdir -p project/internal/ports/output
mkdir -p project/internal/ports/filesystem
mkdir -p project/internal/ports/process
mkdir -p project/internal/adapters/input
mkdir -p project/internal/adapters/output
mkdir -p project/internal/adapters/filesystem
mkdir -p project/internal/adapters/process
project/
├── go.mod
├── main.go
└── internal/
    ├── domain/
    │   └── daemon_service.go
    ├── ports/
    │   ├── input/
    │   │   └── cli_provider.go
    │   ├── output/
    │   │   └── printer.go
    │   ├── filesystem/
    │   │   └── manager.go
    │   └── process/
    │       └── manager.go
    └── adapters/
        ├── input/
        │   └── cli_adapter.go
        ├── output/
        │   └── console_printer.go
        ├── filesystem/
        │   └── os_manager.go
        └── process/
            └── os_manager.go

internal/ports/input/cli_provider.go

package input

// CLIProvider provides command-line input details.
type CLIProvider interface {
    GetCommand() string
    GetBinPath() string
    NewSudoError() error
}

internal/ports/output/printer.go

package output

// Printer is the output port for printing messages.
type Printer interface {
    Println(a ...interface{})
    Printf(format string, a ...interface{})
}

internal/ports/filesystem/manager.go

package filesystem

import "os"

// FileManager is the port for file system operations.
type FileManager interface {
    Stat(path string) (os.FileInfo, error)
    OpenFile(path string, flag int, perm os.FileMode) (io.WriteCloser, error)
    Remove(path string) error
    MkdirAll(path string, perm os.FileMode) error
    ReadFile(path string) ([]byte, error)
}

internal/ports/process/manager.go

package process

import "os"

// ProcessManager is the port for process operations.
type ProcessManager interface {
    Start(executable string, args []string, stdout io.Writer, stderr io.Writer, dir string) (*os.Process, error)
    FindProcess(pid int) (*os.Process, error)
    Signal(process *os.Process, sig os.Signal) error
}

internal/adapters/input/cli_adapter.go

package input

import (
    "fmt"

    ports "project/internal/ports/input"
)

// CLIAdapter is the adapter for command-line input.
type CLIAdapter struct {
    bin string
    cmd string
}

// NewCLIAdapter creates a new CLIAdapter.
func NewCLIAdapter(bin, cmd string) ports.CLIProvider {
    return &CLIAdapter{
        bin: bin,
        cmd: cmd,
    }
}

// GetCommand implements CLIProvider.
func (c *CLIAdapter) GetCommand() string {
    return c.cmd
}

// GetBinPath implements CLIProvider.
func (c *CLIAdapter) GetBinPath() string {
    return c.bin
}

// NewSudoError implements CLIProvider.
func (c *CLIAdapter) NewSudoError() error {
    return fmt.Errorf("try `sudo %s %s`", c.bin, c.cmd)
}

internal/adapters/output/console_printer.go

package output

import (
    "fmt"

    ports "project/internal/ports/output"
)

// ConsolePrinter is the adapter for console output.
type ConsolePrinter struct{}

// NewConsolePrinter creates a new ConsolePrinter.
func NewConsolePrinter() ports.Printer {
    return &ConsolePrinter{}
}

// Println implements Printer.
func (c *ConsolePrinter) Println(a ...interface{}) {
    fmt.Println(a...)
}

// Printf implements Printer.
func (c *ConsolePrinter) Printf(format string, a ...interface{}) {
    fmt.Printf(format, a...)
}

internal/adapters/filesystem/os_manager.go

package filesystem

import (
    "io"
    "os"

    ports "project/internal/ports/filesystem"
)

// OSFileManager is the adapter for OS file operations.
type OSFileManager struct{}

// NewOSFileManager creates a new OSFileManager.
func NewOSFileManager() ports.FileManager {
    return &OSFileManager{}
}

// Stat implements FileManager.
func (o *OSFileManager) Stat(path string) (os.FileInfo, error) {
    return os.Stat(path)
}

// OpenFile implements FileManager.
func (o *OSFileManager) OpenFile(path string, flag int, perm os.FileMode) (io.WriteCloser, error) {
    return os.OpenFile(path, flag, perm)
}

// Remove implements FileManager.
func (o *OSFileManager) Remove(path string) error {
    return os.Remove(path)
}

// MkdirAll implements FileManager.
func (o *OSFileManager) MkdirAll(path string, perm os.FileMode) error {
    return os.MkdirAll(path, perm)
}

// ReadFile implements FileManager.
func (o *OSFileManager) ReadFile(path string) ([]byte, error) {
    return os.ReadFile(path)
}

internal/adapters/process/os_manager.go

package process

import (
    "io"
    "os"
    "os/exec"

    ports "project/internal/ports/process"
)

// OSProcessManager is the adapter for OS process operations.
type OSProcessManager struct{}

// NewOSProcessManager creates a new OSProcessManager.
func NewOSProcessManager() ports.ProcessManager {
    return &OSProcessManager{}
}

// Start implements ProcessManager.
func (o *OSProcessManager) Start(executable string, args []string, stdout io.Writer, stderr io.Writer, dir string) (*os.Process, error) {
    cmd := exec.Command(executable, args...)
    cmd.Stdout = stdout
    cmd.Stderr = stderr
    cmd.Dir = dir
    if err := cmd.Start(); err != nil {
        return nil, err
    }
    return cmd.Process, nil
}

// FindProcess implements ProcessManager.
func (o *OSProcessManager) FindProcess(pid int) (*os.Process, error) {
    return os.FindProcess(pid)
}

// Signal implements ProcessManager.
func (o *OSProcessManager) Signal(process *os.Process, sig os.Signal) error {
    return process.Signal(sig)
}

internal/domain/daemon_service.go

package domain

import (
    "fmt"
    "io"
    "os"
    "path/filepath"
    "strconv"
    "syscall"
    "time"

    input "project/internal/ports/input"
    filesystem "project/internal/ports/filesystem"
    process "project/internal/ports/process"
)

const (
    initdFile    = "/etc/init.d/mydaemon"
    varDir       = "/var/mydaemon/"
    pidFile      = "mydaemon.pid"
    outFile      = "mydaemon.log"
    errFile      = "mydaemon.err"
    initdContent = `#!/bin/sh
### BEGIN INIT INFO
# Provides: mydaemon
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Custom daemon
# Description: Enable service provided by daemon.
### END INIT INFO
"%s" $1
`
)

// DaemonService is the core domain service for daemon management.
type DaemonService struct {
    provider input.CLIProvider
    fm       filesystem.FileManager
    pm       process.ProcessManager
}

// NewDaemonService creates a new DaemonService.
func NewDaemonService(provider input.CLIProvider, fm filesystem.FileManager, pm process.ProcessManager) *DaemonService {
    return &DaemonService{
        provider: provider,
        fm:       fm,
        pm:       pm,
    }
}

// Status returns the daemon status.
type Status struct {
    PID    int
    Active bool
}

// Install installs the daemon.
func (ds *DaemonService) Install() error {
    _, err := ds.fm.Stat(initdFile)
    if err == nil {
        return fmt.Errorf("Already installed")
    }
    f, err := ds.fm.OpenFile(initdFile, os.O_CREATE|os.O_WRONLY, 0755)
    if err != nil {
        if os.IsPermission(err) {
            return ds.provider.NewSudoError()
        }
        return err
    }
    defer f.Close()
    bin := ds.provider.GetBinPath()
    content := fmt.Sprintf(initdContent, bin)
    if _, err = io.WriteString(f, content); err != nil {
        return err
    }
    return nil
}

// Uninstall uninstalls the daemon.
func (ds *DaemonService) Uninstall() error {
    _, err := ds.fm.Stat(initdFile)
    if err != nil {
        if os.IsNotExist(err) {
            return fmt.Errorf("not installed")
        }
        return err
    }
    if err = ds.fm.Remove(initdFile); err != nil {
        if os.IsPermission(err) {
            return ds.provider.NewSudoError()
        }
        return err
    }
    return nil
}

// Status checks the daemon status.
func (ds *DaemonService) Status() (*Status, error) {
    pidPath := filepath.Join(varDir, pidFile)
    b, err := ds.fm.ReadFile(pidPath)
    if err != nil {
        if os.IsNotExist(err) {
            return &Status{PID: 0, Active: false}, nil
        }
        return nil, err
    }
    pid, err := strconv.Atoi(string(b))
    if err != nil {
        return nil, fmt.Errorf("Invalid PID value: %s", string(b))
    }
    p, err := ds.pm.FindProcess(pid)
    if err != nil {
        return nil, err
    }
    err = ds.pm.Signal(p, syscall.Signal(0))
    if err != nil {
        ds.fm.Remove(pidPath)
        return &Status{PID: pid, Active: false}, nil
    }
    return &Status{PID: pid, Active: true}, nil
}

// Start starts the daemon.
func (ds *DaemonService) Start() (int, error) {
    if err := ds.fm.MkdirAll(varDir, 0755); err != nil {
        if os.IsPermission(err) {
            return 0, ds.provider.NewSudoError()
        }
        return 0, err
    }
    outPath := filepath.Join(varDir, outFile)
    errPath := filepath.Join(varDir, errFile)
    stdout, err := ds.fm.OpenFile(outPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        return 0, err
    }
    stderr, err := ds.fm.OpenFile(errPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        stdout.Close()
        return 0, err
    }
    proc, err := ds.pm.Start(ds.provider.GetBinPath(), []string{"run"}, stdout, stderr, "/")
    if err != nil {
        stdout.Close()
        stderr.Close()
        return 0, err
    }
    pidPath := filepath.Join(varDir, pidFile)
    if err := ds.writePID(pidPath, proc.Pid); err != nil {
        ds.pm.Signal(proc, os.Kill)
        return 0, err
    }
    return proc.Pid, nil
}

// Stop stops the daemon.
func (ds *DaemonService) Stop() (int, error) {
    pidPath := filepath.Join(varDir, pidFile)
    pid, err := ds.getPID(pidPath)
    if err != nil {
        if os.IsNotExist(err) {
            return 0, nil
        }
        return 0, err
    }
    p, err := ds.pm.FindProcess(pid)
    if err != nil {
        return 0, nil
    }
    if err = ds.pm.Signal(p, os.Kill); err != nil {
        return 0, err
    }
    if err = ds.fm.Remove(pidPath); err != nil {
        return 0, err
    }
    return pid, nil
}

// Run runs the daemon loop.
func (ds *DaemonService) Run() error {
    for {
        time.Sleep(1 * time.Second)
    }
}

func (ds *DaemonService) writePID(path string, pid int) error {
    f, err := ds.fm.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()
    _, err = fmt.Fprintf(f, "%d", pid)
    return err
}

func (ds *DaemonService) getPID(path string) (int, error) {
    b, err := ds.fm.ReadFile(path)
    if err != nil {
        return 0, err
    }
    pid, err := strconv.Atoi(string(b))
    if err != nil {
        return 0, fmt.Errorf("Invalid PID value: %s", string(b))
    }
    return pid, nil
}

main.go

package main

import (
    "fmt"
    "os"
    "path/filepath"

    "project/internal/adapters/filesystem"
    "project/internal/adapters/input"
    "project/internal/adapters/output"
    "project/internal/adapters/process"
    "project/internal/domain"
)

func main() {
    bin, err := filepath.Abs(os.Args[0])
    if err != nil {
        fmt.Printf("Error getting bin path: %v\n", err)
        os.Exit(1)
    }
    var cmd string
    if len(os.Args) > 1 {
        cmd = os.Args[1]
    }
    cliProvider := input.NewCLIAdapter(bin, cmd)
    printer := output.NewConsolePrinter()
    fileManager := filesystem.NewOSFileManager()
    procManager := process.NewOSProcessManager()
    service := domain.NewDaemonService(cliProvider, fileManager, procManager)

    switch cmd {
    case "run":
        _ = service.Run()
    case "install":
        err = service.Install()
        if err == nil {
            printer.Printf("Daemon %s installed\n", bin)
        } else {
            printer.Printf("install error: %v\n", err)
        }
    case "uninstall":
        err = service.Uninstall()
        if err == nil {
            printer.Printf("Daemon %s removed\n", bin)
        } else {
            printer.Printf("uninstall error: %v\n", err)
        }
    case "status":
        status, err := service.Status()
        if err != nil {
            printer.Printf("status error: %v\n", err)
            return
        }
        if !status.Active {
            if status.PID != 0 {
                printer.Printf("%d not found - removing PID file...\n", status.PID)
            }
            printer.Println("status: not active")
        } else {
            printer.Printf("status: active - pid %d\n", status.PID)
        }
    case "start":
        pid, err := service.Start()
        if err != nil {
            printer.Printf("start error: %v\n", err)
        } else {
            printer.Printf("Started with PID %d\n", pid)
        }
    case "stop":
        pid, err := service.Stop()
        if err != nil {
            printer.Printf("stop error: %v\n", err)
        } else if pid != 0 {
            printer.Printf("Stopped PID %d\n", pid)
        }
    default:
        printer.Printf("usage: %s run|install|uninstall|status|start|stop\n", bin)
    }
}

go.mod

module project

go 1.21