Compare commits

..

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

5 changed files with 274 additions and 247 deletions

View File

@ -1,26 +1,5 @@
# Changelog: @go/starter # Changelog: @go/starter
## v1.5.3 (2026-06-07)
- **重大架构更迭: 引入 Start/Wait 编排模式与生命周期钩子**:
- **废弃 `Run()`**: 彻底移除黑盒式的 `Run()` 方法,改为显式的 `Start()` (解析与非阻塞启动) 与 `Wait()` (阻塞与信号处理) 模式。
- **生命周期钩子**: `Register` 现返回 `*managedService` 指针,支持链式注册 `OnStarting`, `OnStarted`, `OnStopping`, `OnStopped` 回调。
- **中间状态编排**: 允许开发者在 `Start()` 之后、`Wait()` 之前执行自定义初始化逻辑(如跨服务的业务对齐)。
## v1.5.2 (2026-06-05)
- **可观测性升级: 中心化生命周期审计与 TraceID 链路追踪**:
- **TraceID 分层**: 为 `starter` 编排层引入了独立的长短 ID 体系。
- Starter 级8位短 ID代表整个启动/停止批次的编排上下文。
- 服务级10位长 ID自动派发给每个子服务的 `Start` 方法,确保服务内部日志能与 Starter 状态精准关联。
- **状态聚合**: 所有的 `service [name] starting / started / stopping / stopped` 日志现由 `starter` 统一负责,并显式记录跨层级的 `trace` ID。
- **语义统一**: 所有 Starter 核心消息增加 `[starter]` 前缀标识。
- **架构清理**: 移除了内部对 `log-writer` 的自动注册逻辑,将核心组件的启停权责彻底交还给应用开发者。
## v1.5.1 (2026-06-04)
- **依赖升级**: 全面对接 `@go` 基础设施 v1.5.1。
## v1.5.0 (2026-05-10)
- **基础设施对齐**: 全局对齐至 v1.5.0。
## v1.0.1 (2026-05-12) ## v1.0.1 (2026-05-12)
### 🚀 Features ### 🚀 Features

View File

@ -45,31 +45,21 @@ func (s *MyService) Reload() error {
func main() { func main() {
starter.SetAppInfo("myapp", "1.0.0") starter.SetAppInfo("myapp", "1.0.0")
// Register with priority 1 and optional hooks // Register with priority 1
starter.Register("myservice", &MyService{}, 1, 0, 0).OnStarted(func() { starter.Register("myservice", &MyService{}, 1, 0, 0)
fmt.Println("MyService is ready!")
})
if err := starter.Start(); err != nil { starter.Run()
panic(err)
}
// Custom initialization logic here
starter.Wait()
} }
``` ```
## Interfaces ## Interfaces
- **`Service`**: Core lifecycle interface (`Start`, `Stop`, `Status`). - **`Service`**: Core lifecycle interface (`Start`, `Stop`, `Health`).
- **`Reloader`**: Optional interface for services that support `Reload()` (triggered by `SIGHUP`). - **`Reloader`**: Optional interface for services that support `Reload()` (triggered by `SIGHUP`).
- **`UserSignalHandler`**: Optional interface for services that handle custom user signals (`SIGUSR1`, `SIGUSR2` or via `kill` command). - **`UserSignalHandler`**: Optional interface for services that handle custom user signals (`SIGUSR1`, `SIGUSR2` or via `kill` command).
## Features ## Features
- **Lifecycle Hooks**: `OnStarting`, `OnStarted`, `OnStopping`, `OnStopped` hooks for precise coordination.
- **Start/Wait Pattern**: Replaces `Run()` to allow custom logic between service startup and blocking.
- **Tiered Startup/Shutdown**: Concurrent execution within priority levels, serial execution across them. - **Tiered Startup/Shutdown**: Concurrent execution within priority levels, serial execution across them.
- **Trace ID Propagation**: Automatically generates a shared Trace ID for all services during startup, ensuring log correlation. - **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. - **Secure IPC**: Token-based Unix Domain Socket for `status` and `kill` commands.

27
go.mod
View File

@ -3,22 +3,21 @@ module apigo.cc/go/starter
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/crypto v1.5.0 apigo.cc/go/cast v1.3.3
apigo.cc/go/id v1.5.0 apigo.cc/go/crypto v1.3.1
apigo.cc/go/log v1.5.5 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/jsmod v1.5.0 // indirect
require ( require (
apigo.cc/go/cast v1.5.0 // indirect apigo.cc/go/config v1.3.1 // indirect
apigo.cc/go/config v1.5.1 // indirect apigo.cc/go/encoding v1.3.1 // indirect
apigo.cc/go/encoding v1.5.0 // indirect apigo.cc/go/rand v1.3.1 // indirect
apigo.cc/go/file v1.5.0 // indirect apigo.cc/go/safe v1.3.1 // indirect
apigo.cc/go/rand v1.5.0 // indirect golang.org/x/crypto v0.51.0 // indirect
apigo.cc/go/safe v1.5.0 // indirect golang.org/x/sys v0.44.0 // indirect
apigo.cc/go/shell v1.5.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

51
go.sum
View File

@ -1,34 +1,35 @@
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE= apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8= apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.5.1 h1:rpj7oCzlsDV3f2/YK3Pb+CHbfr2DL5Vyyv6VNkobJP4= apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.5.1/go.mod h1:jdMiDLPa9gzB8/FFZvm9jOopUqdxb7XSX+0OeWcZZUM= apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/crypto v1.5.0 h1:Nxz7a6VKCdvaF258IU0NkjQyureOLxfR308Sy2iftUI= apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg=
apigo.cc/go/crypto v1.5.0/go.mod h1:F9M6nXv+5328r1ZwbTvI6fcr8VdgqHVzALOcsdv6ntE= apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs=
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I= apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA= apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw= apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0= apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/id v1.5.0 h1:MjNWPhBhDsoXaLeJDv/0wfJmVMU9EvOs8pWYfsTQ6e8= apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.5.0/go.mod h1:qhu4a1/KLc/XcBpcsRu+mXZt7U7Wvd9zMcPs4VspuPA= apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec= apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/log v1.5.5 h1:AFU7d7AQxkpgDHl7SnlEwd6yzGSFAlnrrjbrNDQnQHI= apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk= apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM= apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.5.0 h1:WLDMMqUU0INeaBDmQsTPr0h/NfB2RknAtiJ5NL467+Q= apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
apigo.cc/go/shell v1.5.0/go.mod h1:rYHA77d5hEsQHcJrbAWf1pHy0sxayeJ0gU55LA/JWQk= 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 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -11,13 +11,18 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"sort" "sort"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
"apigo.cc/go/cast"
"apigo.cc/go/crypto" "apigo.cc/go/crypto"
"apigo.cc/go/file"
"apigo.cc/go/id" "apigo.cc/go/id"
"apigo.cc/go/log" "apigo.cc/go/log"
"apigo.cc/go/shell"
"apigo.cc/go/timer"
) )
var ( var (
@ -38,13 +43,6 @@ var (
// IPC Security // IPC Security
ipcSecret = "apigo-starter-secret-2026" ipcSecret = "apigo-starter-secret-2026"
// Starter Logger
starterLogger *log.Logger
// Signal handling
sigs chan os.Signal
cancelCtx context.CancelFunc
) )
type managedService struct { type managedService struct {
@ -53,30 +51,6 @@ type managedService struct {
priority int priority int
startTimeout time.Duration startTimeout time.Duration
stopTimeout time.Duration stopTimeout time.Duration
onStarting []func()
onStarted []func()
onStopping []func()
onStopped []func()
}
func (ms *managedService) OnStarting(fn func()) *managedService {
ms.onStarting = append(ms.onStarting, fn)
return ms
}
func (ms *managedService) OnStarted(fn func()) *managedService {
ms.onStarted = append(ms.onStarted, fn)
return ms
}
func (ms *managedService) OnStopping(fn func()) *managedService {
ms.onStopping = append(ms.onStopping, fn)
return ms
}
func (ms *managedService) OnStopped(fn func()) *managedService {
ms.onStopped = append(ms.onStopped, fn)
return ms
} }
type command struct { type command struct {
@ -91,27 +65,20 @@ func init() {
AddCommand("restart", "Restart the service", restartCmd) AddCommand("restart", "Restart the service", restartCmd)
AddCommand("status", "Show service status", statusCmd) AddCommand("status", "Show service status", statusCmd)
AddCommand("kill", "Send signal to a specific service: kill <svc_name> <signal_num>", killCmd) AddCommand("kill", "Send signal to a specific service: kill <svc_name> <signal_num>", killCmd)
}
// getStarterLogger returns the singleton logger for the starter context. // Auto-register log writer service with high priority
func getStarterLogger() *log.Logger { Register("log-writer", log.WriterService, -100, 0, 0)
if starterLogger == nil {
starterLogger = log.DefaultLogger.New(id.Get8Bytes4KPerSecond())
}
return starterLogger
} }
// Register adds a service to be managed by the starter. // Register adds a service to be managed by the starter.
func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) *managedService { func Register(name string, svc Service, priority int, startTimeout, stopTimeout time.Duration) {
ms := &managedService{ services[priority] = append(services[priority], &managedService{
Name: name, Name: name,
svc: svc, svc: svc,
priority: priority, priority: priority,
startTimeout: startTimeout, startTimeout: startTimeout,
stopTimeout: stopTimeout, stopTimeout: stopTimeout,
} })
services[priority] = append(services[priority], ms)
return ms
} }
// SetAppInfo sets the application name and version. // SetAppInfo sets the application name and version.
@ -120,6 +87,11 @@ func SetAppInfo(name, version string) {
appVersion = version 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. // SetUsage sets custom usage text to be displayed in help.
func SetUsage(text string) { func SetUsage(text string) {
appUsage = text appUsage = text
@ -130,10 +102,13 @@ func AddCommand(name, desc string, fn func()) {
commands[name] = &command{name: name, desc: desc, fn: fn} commands[name] = &command{name: name, desc: desc, fn: fn}
} }
// TODO 使用 Start / Wait 代替 Run方便在启动后做初始化操作或者支持注册某服务的 OnStarting / OnStarted / OnStopping / OnStopped 事件 // AddCmd is an alias for AddCommand.
func AddCmd(name, desc string, fn func()) {
AddCommand(name, desc, fn)
}
// Start parses arguments and starts the services. // Run parses arguments and executes the service.
func Start() error { func Run() {
flagSet.Usage = showHelp flagSet.Usage = showHelp
if len(os.Args) > 1 { if len(os.Args) > 1 {
arg := os.Args[1] arg := os.Args[1]
@ -141,46 +116,87 @@ func Start() error {
// Subcommand detected, parse flags after the command // Subcommand detected, parse flags after the command
_ = flagSet.Parse(os.Args[2:]) _ = flagSet.Parse(os.Args[2:])
cmd.fn() cmd.fn()
os.Exit(0) return
} }
// Check for help/version // Check for help/version
switch arg { switch arg {
case "-h", "--help", "help": case "-h", "--help", "help":
showHelp() showHelp()
os.Exit(0) return
case "-v", "--version", "version": case "-v", "--version", "version":
fmt.Printf("%s version %s\n", appName, appVersion) fmt.Printf("%s version %s\n", appName, appVersion)
os.Exit(0) return
} }
} }
// No starter command, treat all as app flags // No starter command, treat all as app flags
_ = flagSet.Parse(os.Args[1:]) _ = flagSet.Parse(os.Args[1:])
runForeground()
}
// Setup signal handling for graceful shutdown func showHelp() {
sigs = make(chan os.Signal, 1) fmt.Printf("%s (%s)\n\n", appName, appVersion)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2) if appUsage != "" {
fmt.Printf("%s\n\n", appUsage)
}
fmt.Printf("Usage:\n %s [command] [options]\n\nCommands:\n", filepath.Base(os.Args[0]))
var ctx context.Context var names []string
ctx, cancelCtx = context.WithCancel(context.Background()) for cmdName := range commands {
names = append(names, cmdName)
}
sort.Strings(names)
for _, cmdName := range names {
fmt.Printf(" %-10s %s\n", cmdName, commands[cmdName].desc)
}
fmt.Println("\nOptions:")
flagSet.PrintDefaults()
fmt.Println("\nIf no command is provided, the service runs in the foreground.")
}
func runForeground() {
pid := os.Getpid()
savePid(pid)
defer removePid()
// Prepare IPC listener but don't serve yet to avoid race conditions during startup
sockPath := getSockPath()
_ = os.Remove(sockPath)
l, err := net.Listen("unix", sockPath)
if err == nil {
defer func() {
_ = l.Close()
_ = os.Remove(sockPath)
}()
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Setup signal handling
sigChan := make(chan os.Signal, 10)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGUSR1, syscall.SIGUSR2)
// Start registered services
if err := startServices(ctx); err != nil { if err := startServices(ctx); err != nil {
getStarterLogger().Error(fmt.Sprintf("[starter] start services failed: %v", err)) log.DefaultLogger.Error(fmt.Sprintf("Start services failed: %v", err))
return err stopServices()
} log.DefaultLogger.Error("Service failed to start, exiting.")
return nil os.Exit(1)
}
// Wait blocks until a termination signal is received.
func Wait() {
if sigs == nil {
return return
} }
for { // Service started successfully, now expose IPC
sig := <-sigs if l != nil {
go serveIPC(l)
}
for sig := range sigChan {
if sig == syscall.SIGHUP { if sig == syscall.SIGHUP {
log.DefaultLogger.Info("Received SIGHUP. Reloading...")
reloadServices() reloadServices()
continue continue
} }
@ -188,20 +204,18 @@ func Wait() {
// Handle user custom signals // Handle user custom signals
if sig == syscall.SIGUSR1 || sig == syscall.SIGUSR2 { if sig == syscall.SIGUSR1 || sig == syscall.SIGUSR2 {
if !handleUserSignal(nil, sig) { if !handleUserSignal(nil, sig) {
getStarterLogger().Info(fmt.Sprintf("[starter] received signal %v, but no service handled it.", sig)) log.DefaultLogger.Info(fmt.Sprintf("Received signal %v, but no service handled it.", sig))
} }
continue continue
} }
getStarterLogger().Info(fmt.Sprintf("[starter] received signal: %v, shutting down...", sig)) log.DefaultLogger.Info(fmt.Sprintf("Received signal: %v. Shutting down...", sig))
break break
} }
if cancelCtx != nil { cancel() // Trigger context cancellation
cancelCtx() // Trigger context cancellation
}
stopServices() stopServices()
getStarterLogger().Info("[starter] shutdown complete") log.DefaultLogger.Info("Shutdown complete.")
} }
func startServices(ctx context.Context) error { func startServices(ctx context.Context) error {
@ -211,6 +225,9 @@ func startServices(ctx context.Context) error {
} }
sort.Ints(priorities) sort.Ints(priorities)
// Generate a shared logger with trace ID for all services startup
logger := log.DefaultLogger.New(id.Get8Bytes4KPerSecond())
for _, p := range priorities { for _, p := range priorities {
svcs := services[p] svcs := services[p]
var wg sync.WaitGroup var wg sync.WaitGroup
@ -226,23 +243,8 @@ func startServices(ctx context.Context) error {
sctx, cancel = context.WithTimeout(ctx, ms.startTimeout) sctx, cancel = context.WithTimeout(ctx, ms.startTimeout)
defer cancel() defer cancel()
} }
if err := ms.svc.Start(sctx, logger); err != nil {
// Each service gets its own unique 10-byte trace ID for its internal logs
serviceTraceId := id.Get10Bytes14MPerSecond()
serviceLogger := log.DefaultLogger.New(serviceTraceId)
// Log using starter's logger (8-byte ID) but include service's trace ID in extra fields
getStarterLogger().Info(fmt.Sprintf("service [%s] starting", ms.Name), "trace", serviceTraceId)
for _, fn := range ms.onStarting {
fn()
}
if err := ms.svc.Start(sctx, serviceLogger); err != nil {
errChan <- fmt.Errorf("service [%s] start error: %w", ms.Name, err) errChan <- fmt.Errorf("service [%s] start error: %w", ms.Name, err)
} else {
getStarterLogger().Info(fmt.Sprintf("service [%s] started", ms.Name), "trace", serviceTraceId)
for _, fn := range ms.onStarted {
fn()
}
} }
}(ms) }(ms)
} }
@ -262,7 +264,6 @@ func stopServices() {
sort.Slice(startedPriorities, func(i, j int) bool { sort.Slice(startedPriorities, func(i, j int) bool {
return startedPriorities[i] > startedPriorities[j] return startedPriorities[i] > startedPriorities[j]
}) })
for _, p := range startedPriorities { for _, p := range startedPriorities {
svcs := services[p] svcs := services[p]
var wg sync.WaitGroup var wg sync.WaitGroup
@ -276,17 +277,8 @@ func stopServices() {
sctx, cancel = context.WithTimeout(sctx, ms.stopTimeout) sctx, cancel = context.WithTimeout(sctx, ms.stopTimeout)
defer cancel() defer cancel()
} }
getStarterLogger().Info(fmt.Sprintf("service [%s] stopping", ms.Name))
for _, fn := range ms.onStopping {
fn()
}
if err := ms.svc.Stop(sctx); err != nil { if err := ms.svc.Stop(sctx); err != nil {
getStarterLogger().Error(fmt.Sprintf("service [%s] stop error: %v", ms.Name, err)) log.DefaultLogger.Error(fmt.Sprintf("service [%s] stop error: %v", ms.Name, err))
} else {
getStarterLogger().Info(fmt.Sprintf("service [%s] stopped", ms.Name))
for _, fn := range ms.onStopped {
fn()
}
} }
}(ms) }(ms)
} }
@ -296,12 +288,11 @@ func stopServices() {
} }
func reloadServices() { func reloadServices() {
getStarterLogger().Info("[starter] reloading all services...")
for _, p := range startedPriorities { for _, p := range startedPriorities {
for _, ms := range services[p] { for _, ms := range services[p] {
if r, ok := ms.svc.(Reloader); ok { if r, ok := ms.svc.(Reloader); ok {
if err := r.Reload(); err != nil { if err := r.Reload(); err != nil {
getStarterLogger().Error(fmt.Sprintf("service [%s] reload error: %v", ms.Name, err)) log.DefaultLogger.Error(fmt.Sprintf("service [%s] reload error: %v", ms.Name, err))
} }
} }
} }
@ -312,7 +303,7 @@ func handleUserSignal(svcName *string, sig os.Signal) bool {
handled := false handled := false
for _, p := range startedPriorities { for _, p := range startedPriorities {
for _, ms := range services[p] { for _, ms := range services[p] {
if svcName != nil && *svcName != ms.Name { if svcName != nil && ms.Name != *svcName {
continue continue
} }
if h, ok := ms.svc.(UserSignalHandler); ok { if h, ok := ms.svc.(UserSignalHandler); ok {
@ -325,103 +316,168 @@ func handleUserSignal(svcName *string, sig os.Signal) bool {
return handled return handled
} }
func showHelp() { func serveIPC(l net.Listener) {
fmt.Printf("%s (%s)\n\n", appName, appVersion) for {
if appUsage != "" { conn, err := l.Accept()
fmt.Printf("%s\n\n", appUsage) if err != nil {
return
}
go func(c net.Conn) {
defer c.Close()
data := make([]byte, 4096)
n, err := c.Read(data)
if err != nil || n == 0 {
return
} }
fmt.Printf("Usage:\n %s [command] [options]\n\nCommands:\n", filepath.Base(os.Args[0]))
var names []string // Protocol: TOKEN COMMAND ARGS...
for cmdName := range commands { parts := strings.Split(string(data[:n]), " ")
names = append(names, cmdName) if len(parts) < 2 {
return
} }
sort.Strings(names)
for _, name := range names { token := parts[0]
fmt.Printf(" %-10s %s\n", name, commands[name].desc) if token != getIPCToken(os.Getpid()) {
_, _ = c.Write([]byte("Error: Unauthorized"))
return
} }
fmt.Println()
cmd := parts[1]
args := parts[2:]
switch cmd {
case "status":
_, _ = c.Write([]byte(getInternalStatus()))
case "kill":
if len(args) < 2 {
_, _ = c.Write([]byte("Error: Missing arguments for kill"))
return
}
svcName := args[0]
sigNum := cast.Int(args[1])
if handleUserSignal(&svcName, syscall.Signal(sigNum)) {
_, _ = c.Write([]byte(fmt.Sprintf("Signal %d sent to %s", sigNum, svcName)))
} else {
_, _ = c.Write([]byte(fmt.Sprintf("Error: Service %s not found or didn't handle signal", svcName)))
}
default:
_, _ = c.Write([]byte("Error: Unknown command"))
}
}(conn)
}
}
func getInternalStatus() string {
var out string
var priorities []int
for p := range services {
priorities = append(priorities, p)
}
sort.Ints(priorities)
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)
}
}
}
return out
} }
func startCmd() { func startCmd() {
pid := getPid() pid := loadPid()
if pid != 0 && isProcessRunning(pid) { if pid > 0 && isProcessRunning(pid) {
getStarterLogger().Info(fmt.Sprintf("[starter] %s is already running (PID %d)", appName, pid)) log.DefaultLogger.Info(fmt.Sprintf("%s is already running (PID %d)", appName, pid))
return return
} }
cmd := exec.Command(os.Args[0]) args := []string{}
cmd.Args = append([]string{os.Args[0]}, flagSet.Args()...) for i := 1; i < len(os.Args); i++ {
cmd.Stdout = nil if os.Args[i] != "start" {
cmd.Stderr = nil args = append(args, os.Args[i])
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} }
if err := cmd.Start(); err != nil {
getStarterLogger().Error(fmt.Sprintf("[starter] failed to start %s: %v", appName, err))
return
} }
_ = os.WriteFile(getPidPath(), []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644) cmd := exec.Command(os.Args[0], args...)
getStarterLogger().Info(fmt.Sprintf("[starter] %s started (PID %d)", appName, cmd.Process.Pid)) err := cmd.Start()
if err != nil {
log.DefaultLogger.Error(fmt.Sprintf("Failed to start %s: %v", appName, err))
os.Exit(1)
}
log.DefaultLogger.Info(fmt.Sprintf("%s started (PID %d)", appName, cmd.Process.Pid))
} }
func stopCmd() { func stopCmd() {
pid := getPid() pid := loadPid()
if pid == 0 || !isProcessRunning(pid) { if pid <= 0 || !isProcessRunning(pid) {
getStarterLogger().Info(fmt.Sprintf("[starter] %s is not running", appName)) log.DefaultLogger.Info(fmt.Sprintf("%s is not running", appName))
return return
} }
getStarterLogger().Info(fmt.Sprintf("[starter] stopping %s (PID %d)...", appName, pid)) process, _ := os.FindProcess(pid)
_ = syscall.Kill(pid, syscall.SIGTERM) log.DefaultLogger.Info(fmt.Sprintf("Stopping %s (PID %d)...", appName, pid))
_ = process.Signal(syscall.SIGTERM)
for i := 0; i < 30; i++ { err := timer.Retry(func() error {
if !isProcessRunning(pid) { if isProcessRunning(pid) {
return fmt.Errorf("still running")
}
return nil
}, timer.WithMaxRetries(25), timer.WithBackoff(200*time.Millisecond, 1.0))
if err == nil {
log.DefaultLogger.Info("Stopped OK")
removePid() removePid()
getStarterLogger().Info(fmt.Sprintf("[starter] %s stopped", appName))
return return
} }
time.Sleep(500 * time.Millisecond)
} log.DefaultLogger.Info("Stop timeout, killing...")
getStarterLogger().Warning(fmt.Sprintf("[starter] %s failed to stop gracefully, killing...", appName)) _ = process.Kill()
_ = syscall.Kill(pid, syscall.SIGKILL)
removePid() removePid()
} }
func restartCmd() { func restartCmd() {
stopCmd() stopCmd()
_ = timer.Retry(func() error { return nil }, timer.WithMaxRetries(1), timer.WithBackoff(500*time.Millisecond, 1.0))
startCmd() startCmd()
} }
func statusCmd() { func statusCmd() {
pid := getPid() pid := loadPid()
if pid == 0 || !isProcessRunning(pid) { isRunning := pid > 0 && isProcessRunning(pid)
fmt.Printf("%s is NOT running\n", appName) if isRunning {
return fmt.Printf("%s is %s (PID %d)\n", appName, shell.Green("running"), pid)
}
fmt.Printf("%s is running (PID %d)\n\n", appName, pid)
res, err := callIPC(pid, "status") res, err := callIPC(pid, "status")
if err == nil { if err == nil {
fmt.Println("Services Status:") fmt.Println("\nServices:")
fmt.Println(res) fmt.Print(res)
}
} else {
fmt.Printf("%s is %s\n", appName, shell.Red("not running"))
} }
} }
func killCmd() { func killCmd() {
if flagSet.NArg() < 2 { if len(flagSet.Args()) < 2 {
fmt.Println("Usage: kill <service_name> <signal_num>") fmt.Println("Usage: kill <service_name> <signal_num>")
return return
} }
pid := loadPid()
if pid <= 0 || !isProcessRunning(pid) {
fmt.Println("Error: process not running")
return
}
svcName := flagSet.Arg(0) svcName := flagSet.Arg(0)
sigNum := flagSet.Arg(1) sigNum := flagSet.Arg(1)
pid := getPid()
if pid == 0 || !isProcessRunning(pid) {
fmt.Println("Application is not running")
return
}
res, err := callIPC(pid, fmt.Sprintf("kill %s %s", svcName, sigNum)) res, err := callIPC(pid, fmt.Sprintf("kill %s %s", svcName, sigNum))
if err != nil { if err != nil {
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
@ -431,24 +487,24 @@ func killCmd() {
} }
func callIPC(pid int, cmd string) (string, error) { func callIPC(pid int, cmd string) (string, error) {
sockPath := getSockPath() conn, err := net.Dial("unix", getSockPath())
conn, err := net.Dial("unix", sockPath)
if err != nil { if err != nil {
return "", err return "", err
} }
defer conn.Close() defer conn.Close()
token := generateToken(pid) token := getIPCToken(pid)
_, _ = conn.Write([]byte(fmt.Sprintf("%s %s", token, cmd))) _, _ = conn.Write([]byte(fmt.Sprintf("%s %s", token, cmd)))
buf, err := io.ReadAll(conn) data, err := io.ReadAll(conn)
if err != nil { if err != nil {
return "", err return "", err
} }
return string(buf), nil return string(data), nil
} }
func generateToken(pid int) string { func getIPCToken(pid int) string {
// Use Sha256 for better security
return crypto.Sha256ToHex([]byte(fmt.Sprintf("%s:%d", ipcSecret, pid))) return crypto.Sha256ToHex([]byte(fmt.Sprintf("%s:%d", ipcSecret, pid)))
} }
@ -460,14 +516,16 @@ func getSockPath() string {
return filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s.sock", appName, appVersion)) return filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s.sock", appName, appVersion))
} }
func getPid() int { func savePid(p int) {
data, err := os.ReadFile(getPidPath()) _ = file.Write(getPidPath(), cast.To[string](p))
}
func loadPid() int {
data, err := file.Read(getPidPath())
if err != nil { if err != nil {
return 0 return 0
} }
var pid int return cast.To[int](data)
fmt.Sscanf(string(data), "%d", &pid)
return pid
} }
func removePid() { func removePid() {