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/processproject/
├── 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