Compare commits

..

No commits in common. "main" and "v1.0.1" have entirely different histories.
main ... v1.0.1

8 changed files with 193 additions and 121 deletions

15
CHANGELOG-LATEST.md Normal file
View File

@ -0,0 +1,15 @@
## v1.0.1 (2026-05-12)
### 🚀 Features
- **Secure IPC**: SHA256 token-based authentication for Unix Domain Sockets.
- **Precise Signaling**: `kill <svc_name> <signal_num>` command.
- **Trace ID Propagation**: Shared Trace ID for service startup logs.
- **Enhanced `status`**: Secured detailed health reporting.
### 🧹 Cleanup
- **Minimalist API**: `Register`, `Run`, `AddCommand`, `SetAppInfo`.
- **Automated PID**: System temp directory placement.
### 🛠 Improvements
- **Race Condition Fix**: Delayed IPC server activation until startup completion.
- **Infrastructure Alignment**: `cast.To[T]`, `timer.Retry`, and `id`.

142
CODE-SUMMARY.md Normal file
View File

@ -0,0 +1,142 @@
### starter > starter.go
```go
var (
// Default configuration
appName = filepath.Base(os.Args[0])
appVersion = "1.0.1"
// Internal state
commands = make(map[string]*command)
// New Service registry
services = make(map[int][]*managedService)
startedPriorities []int
// Flags
flagSet = flag.NewFlagSet(appName, flag.ContinueOnError)
// IPC Security
ipcSecret = "apigo-starter-secret-2026"
)
// Service defines the lifecycle of a component managed by the starter.
type Service interface {
// Start starts the service. It should block until the service is ready.
Start(ctx context.Context, logger *log.Logger) error
// Stop stops the service. It should block until the service is cleaned up.
Stop(ctx context.Context) error
// Health returns the health status of the service.
Health() error
}
// Reloader defines an optional interface for services that support configuration reloading.
type Reloader interface {
Reload() error
}
// UserSignalHandler defines an optional interface for services that handle custom user signals.
type UserSignalHandler interface {
// HandleUserSignal handles a custom signal. Return true if the signal was handled.
HandleUserSignal(sig os.Signal) bool
}
type managedService struct {
Name string
svc Service
priority int
startTimeout time.Duration
stopTimeout time.Duration
}
type command struct {
name string
desc string
fn func()
}
func init()
// Register adds a service to be managed by the starter.
func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration)
// SetAppInfo sets the application name and version.
func SetAppInfo(name, version string)
// AddCommand adds a custom command.
func AddCommand(name, desc string, fn func())
// Run parses arguments and executes the service.
func Run()
func showHelp()
func runForeground()
func startServices(ctx context.Context) error
func stopServices()
func reloadServices()
func handleUserSignal(svcName *string, sig os.Signal) bool
func serveIPC(l net.Listener)
func getInternalStatus() string
func startCmd()
func stopCmd()
func restartCmd()
func statusCmd()
func killCmd()
func callIPC(pid int, cmd string) (string, error)
func getIPCToken(pid int) string
func getPidPath() string
func getSockPath() string
func savePid(p int)
func loadPid() int
func removePid()
func isProcessRunning(p int) bool
```

View File

@ -64,7 +64,6 @@ func main() {
- **Trace ID Propagation**: Automatically generates a shared Trace ID for all services during startup, ensuring log correlation.
- **Secure IPC**: Token-based Unix Domain Socket for `status` and `kill` commands.
- **Zero Configuration PID**: Automatic PID management in the system temporary directory.
- **Embedded Log Writer**: Automatically registered log writer service with high priority (-100).
## Commands

24
go.mod
View File

@ -3,21 +3,11 @@ module apigo.cc/go/starter
go 1.25.0
require (
apigo.cc/go/cast v1.3.3
apigo.cc/go/crypto v1.3.1
apigo.cc/go/file v1.3.2
apigo.cc/go/id v1.3.1
apigo.cc/go/log v1.3.4
apigo.cc/go/shell v1.3.1
apigo.cc/go/timer v1.3.1
)
require (
apigo.cc/go/config v1.3.1 // indirect
apigo.cc/go/encoding v1.3.1 // indirect
apigo.cc/go/rand v1.3.1 // indirect
apigo.cc/go/safe v1.3.1 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
apigo.cc/go/cast v1.3.0
apigo.cc/go/crypto v1.3.0
apigo.cc/go/file v1.3.0
apigo.cc/go/id v1.3.0
apigo.cc/go/log v1.3.0
apigo.cc/go/shell v1.3.0
apigo.cc/go/timer v1.3.0
)

37
go.sum
View File

@ -1,37 +0,0 @@
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg=
apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs=
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
apigo.cc/go/timer v1.3.1 h1:YMSusF1LfJYOf6tAW94Yipj3pHrX6QhfP7Rk3nGFT8k=
apigo.cc/go/timer v1.3.1/go.mod h1:kOnqTTX+zA4AH7SfC+LpUm4ZvS+DVyWWMqul/V5QWJs=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,28 +0,0 @@
package starter
import (
"apigo.cc/go/log"
"context"
"os"
)
// Service defines the lifecycle of a component managed by the starter.
type Service interface {
// Start starts the service. It should block until the service is ready.
Start(ctx context.Context, logger *log.Logger) error
// Stop stops the service. It should block until the service is cleaned up.
Stop(ctx context.Context) error
// Status returns the current status and health of the service.
Status() (string, error)
}
// Reloader defines an optional interface for services that support configuration reloading.
type Reloader interface {
Reload() error
}
// UserSignalHandler defines an optional interface for services that handle custom user signals.
type UserSignalHandler interface {
// HandleUserSignal handles a custom signal. Return true if the signal was handled.
HandleUserSignal(sig os.Signal) bool
}

View File

@ -77,11 +77,8 @@ func (s *mockService) Stop(ctx context.Context) error {
}
}
func (s *mockService) Status() (string, error) {
if s.healthErr != nil {
return "unhealthy", s.healthErr
}
return "ok", nil
func (s *mockService) Health() error {
return s.healthErr
}
func (s *mockService) Reload() error {

View File

@ -29,7 +29,6 @@ var (
// Default configuration
appName = filepath.Base(os.Args[0])
appVersion = "1.0.1"
appUsage = ""
// Internal state
commands = make(map[string]*command)
@ -45,6 +44,27 @@ var (
ipcSecret = "apigo-starter-secret-2026"
)
// Service defines the lifecycle of a component managed by the starter.
type Service interface {
// Start starts the service. It should block until the service is ready.
Start(ctx context.Context, logger *log.Logger) error
// Stop stops the service. It should block until the service is cleaned up.
Stop(ctx context.Context) error
// Health returns the health status of the service.
Health() error
}
// Reloader defines an optional interface for services that support configuration reloading.
type Reloader interface {
Reload() error
}
// UserSignalHandler defines an optional interface for services that handle custom user signals.
type UserSignalHandler interface {
// HandleUserSignal handles a custom signal. Return true if the signal was handled.
HandleUserSignal(sig os.Signal) bool
}
type managedService struct {
Name string
svc Service
@ -65,9 +85,6 @@ func init() {
AddCommand("restart", "Restart the service", restartCmd)
AddCommand("status", "Show service status", statusCmd)
AddCommand("kill", "Send signal to a specific service: kill <svc_name> <signal_num>", killCmd)
// Auto-register log writer service with high priority
Register("log-writer", log.WriterService, -100, 0, 0)
}
// Register adds a service to be managed by the starter.
@ -87,26 +104,11 @@ func SetAppInfo(name, version string) {
appVersion = version
}
// SetInfo is an alias for SetAppInfo.
func SetInfo(name, version string) {
SetAppInfo(name, version)
}
// SetUsage sets custom usage text to be displayed in help.
func SetUsage(text string) {
appUsage = text
}
// AddCommand adds a custom command.
func AddCommand(name, desc string, fn func()) {
commands[name] = &command{name: name, desc: desc, fn: fn}
}
// AddCmd is an alias for AddCommand.
func AddCmd(name, desc string, fn func()) {
AddCommand(name, desc, fn)
}
// Run parses arguments and executes the service.
func Run() {
flagSet.Usage = showHelp
@ -136,11 +138,8 @@ func Run() {
}
func showHelp() {
fmt.Printf("%s (%s)\n\n", appName, appVersion)
if appUsage != "" {
fmt.Printf("%s\n\n", appUsage)
}
fmt.Printf("Usage:\n %s [command] [options]\n\nCommands:\n", filepath.Base(os.Args[0]))
fmt.Printf("%s (%s)\n\nUsage:\n %s [command] [options]\n\nCommands:\n",
appName, appVersion, filepath.Base(os.Args[0]))
var names []string
for cmdName := range commands {
@ -377,16 +376,11 @@ func getInternalStatus() string {
for _, p := range priorities {
for _, ms := range services[p] {
statusMsg, err := ms.svc.Status()
indicator := shell.Green("OK")
if err != nil {
indicator = shell.Red(fmt.Sprintf("FAIL (%v)", err))
}
if statusMsg != "" {
out += fmt.Sprintf("[%d] %-20s %s (%s)\n", p, ms.Name, indicator, statusMsg)
} else {
out += fmt.Sprintf("[%d] %-20s %s\n", p, ms.Name, indicator)
status := shell.Green("OK")
if err := ms.svc.Health(); err != nil {
status = shell.Red(fmt.Sprintf("FAIL (%v)", err))
}
out += fmt.Sprintf("[%d] %-20s %s\n", p, ms.Name, status)
}
}
return out