diff --git a/cmd/serve_nonwindows.go b/cmd/serve_nonwindows.go new file mode 100644 index 000000000..031966433 --- /dev/null +++ b/cmd/serve_nonwindows.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package cmd + +import ( + "github.com/pocketbase/pocketbase" +) + +func Serve(app *pocketbase.PocketBase) error { + return app.Start() +} diff --git a/cmd/serve_windows.go b/cmd/serve_windows.go new file mode 100644 index 000000000..4179f1f8d --- /dev/null +++ b/cmd/serve_windows.go @@ -0,0 +1,57 @@ +//go:build windows +// +build windows + +package cmd + +import ( + "fmt" + + "github.com/pocketbase/pocketbase" + "github.com/pocketbase/pocketbase/core" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/eventlog" +) + +type winscHandler struct { + pb *pocketbase.PocketBase + elog *eventlog.Log +} + +func (h *winscHandler) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { + go func() { + if err := h.pb.Start(); err != nil { + h.elog.Error(999, fmt.Sprintf("Start failed: %v", err)) + } + }() + + s <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown} + for { + select { + case c := <-r: + switch c.Cmd { + case svc.Interrogate: + s <- c.CurrentStatus + case svc.Stop, svc.Shutdown: + event := new(core.TerminateEvent) + event.App = h.pb + h.pb.OnTerminate().Trigger(event, func(e *core.TerminateEvent) error { + return e.App.ResetBootstrapState() + }) + s <- svc.Status{State: svc.Stopped} + return false, 0 + default: + h.elog.Warning(998, fmt.Sprintf("unexpected control request: %v", c.Cmd)) + } + } + } +} + +func Serve(app *pocketbase.PocketBase) error { + if isWinsc, _ := svc.IsWindowsService(); isWinsc { + elog, _ := eventlog.Open(winscName) + defer elog.Close() + return svc.Run(winscName, &winscHandler{pb: app, elog: elog}) + } + + return app.Start() +} diff --git a/cmd/winsc_nonwindows.go b/cmd/winsc_nonwindows.go new file mode 100644 index 000000000..9e3bb0ea4 --- /dev/null +++ b/cmd/winsc_nonwindows.go @@ -0,0 +1,18 @@ +//go:build !windows +// +build !windows + +package cmd + +import ( + "github.com/pocketbase/pocketbase/core" + "github.com/spf13/cobra" +) + +func NewWinscCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "winsc", + Short: "Install/Uninstall Windows service (Not supported on non-Windows OS)", + } + + return command +} diff --git a/cmd/winsc_windows.go b/cmd/winsc_windows.go new file mode 100644 index 000000000..8a0a05ef4 --- /dev/null +++ b/cmd/winsc_windows.go @@ -0,0 +1,189 @@ +//go:build windows +// +build windows + +package cmd + +import ( + "fmt" + "log/slog" + "os" + "time" + + "github.com/pocketbase/pocketbase/core" + "github.com/spf13/cobra" + "golang.org/x/sys/windows/svc" + "golang.org/x/sys/windows/svc/eventlog" + "golang.org/x/sys/windows/svc/mgr" +) + +const winscName = "certimate" + +func NewWinscCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "winsc", + Short: "Install/Uninstall Windows service", + } + + command.AddCommand(winscInstallCommand(app)) + command.AddCommand(winscUninstallCommand(app)) + command.AddCommand(winscStartCommand(app)) + command.AddCommand(winscStopCommand(app)) + + return command +} + +func winscInstallCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "install [args...]", + Example: "winsc install", + Run: func(cmd *cobra.Command, args []string) { + srvPath, err := os.Executable() + if err != nil { + srvPath = os.Args[0] + } + + srvArgs := []string{"serve"} + srvArgs = append(srvArgs, args...) + + manager, err := mgr.Connect() + if err != nil { + slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) + return + } + defer manager.Disconnect() + + config := mgr.Config{ + DisplayName: "Certimate", + Description: "https://github.com/certimate-go/certimate", + StartType: mgr.StartAutomatic, + } + service, err := manager.CreateService(winscName, srvPath, config, srvArgs...) + if err != nil { + slog.Error(fmt.Sprintf("failed to create service: %v", err)) + return + } + defer service.Close() + + eventlog.InstallAsEventCreate(winscName, eventlog.Error|eventlog.Warning|eventlog.Info) + slog.Info(fmt.Sprintf("service '%s' installed", winscName)) + + if err := service.Start(); err != nil { + slog.Warn(fmt.Sprintf("failed to start service: %v", err)) + } + + slog.Info(fmt.Sprintf("service '%s' started", winscName)) + }, + DisableFlagParsing: true, + } + + return command +} + +func winscUninstallCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "uninstall", + Example: "winsc uninstall", + Run: func(cmd *cobra.Command, args []string) { + manager, err := mgr.Connect() + if err != nil { + slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) + return + } + defer manager.Disconnect() + + service, err := manager.OpenService(winscName) + if err != nil { + slog.Error(fmt.Sprintf("failed to open service: %v", err)) + return + } + defer service.Close() + + status, err := service.Query() + if err == nil && status.State != svc.Stopped { + _, err = service.Control(svc.Stop) + if err != nil { + slog.Warn(fmt.Sprintf("failed to stop service: %v", err)) + } + + time.Sleep(3 * time.Second) + slog.Info(fmt.Sprintf("service '%s' stopped", winscName)) + } + + if err = service.Delete(); err != nil { + slog.Error(fmt.Sprintf("failed to delete service: %v", err)) + return + } + + eventlog.Remove(winscName) + slog.Info(fmt.Sprintf("service '%s' uninstalled", winscName)) + }, + } + + return command +} + +func winscStartCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "start", + Example: "winsc start", + Run: func(cmd *cobra.Command, args []string) { + manager, err := mgr.Connect() + if err != nil { + slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) + return + } + defer manager.Disconnect() + + service, err := manager.OpenService(winscName) + if err != nil { + slog.Error(fmt.Sprintf("failed to open service: %v", err)) + return + } + defer service.Close() + + if err := service.Start(); err != nil { + slog.Error(fmt.Sprintf("failed to start service: %v", err)) + return + } + + slog.Info(fmt.Sprintf("service '%s' started", winscName)) + }, + } + + return command +} + +func winscStopCommand(app core.App) *cobra.Command { + command := &cobra.Command{ + Use: "stop", + Example: "winsc stop", + Run: func(cmd *cobra.Command, args []string) { + manager, err := mgr.Connect() + if err != nil { + slog.Error(fmt.Sprintf("failed to connect to service manager: %v", err)) + return + } + defer manager.Disconnect() + + service, err := manager.OpenService(winscName) + if err != nil { + slog.Error(fmt.Sprintf("failed to open service: %v", err)) + return + } + defer service.Close() + + status, err := service.Query() + if err == nil && status.State != svc.Stopped { + _, err = service.Control(svc.Stop) + if err != nil { + slog.Warn(fmt.Sprintf("failed to stop service: %v", err)) + } + + time.Sleep(3 * time.Second) + slog.Info(fmt.Sprintf("service '%s' stopped", winscName)) + } + }, + } + + return command +} diff --git a/go.mod b/go.mod index d475d21fd..4c05262a7 100644 --- a/go.mod +++ b/go.mod @@ -218,7 +218,7 @@ require ( golang.org/x/net v0.46.0 // indirect golang.org/x/oauth2 v0.32.0 // indirect golang.org/x/sync v0.17.0 - golang.org/x/sys v0.37.0 // indirect + golang.org/x/sys v0.37.0 golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.14.0 // indirect diff --git a/internal/workflow/dispatcher/dispatcher.go b/internal/workflow/dispatcher/dispatcher.go index 65d61ce29..9f8ff769b 100644 --- a/internal/workflow/dispatcher/dispatcher.go +++ b/internal/workflow/dispatcher/dispatcher.go @@ -206,7 +206,7 @@ func (wd *workflowDispatcher) tryExecuteAsync(task *taskInfo) { defer func() { if r := recover(); r != nil { wd.syslog.Error(fmt.Sprintf("workflow dispatcher panic: %v", r), slog.String("workflowId", task.WorkflowId), slog.String("runId", task.RunId)) - slog.Default().Error(fmt.Sprintf("workflow dispatcher panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", task.WorkflowId), slog.String("runId", task.RunId)) + slog.Error(fmt.Sprintf("workflow dispatcher panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", task.WorkflowId), slog.String("runId", task.RunId)) if workflowRun != nil { workflowRun.Status = domain.WorkflowRunStatusTypeFailed diff --git a/internal/workflow/engine/engine.go b/internal/workflow/engine/engine.go index aea61bfa4..8309f051a 100644 --- a/internal/workflow/engine/engine.go +++ b/internal/workflow/engine/engine.go @@ -60,7 +60,7 @@ func (we *workflowEngine) Invoke(ctx context.Context, execution WorkflowExecutio if r := recover(); r != nil { we.fireOnErrorHooks(ctx, fmt.Errorf("workflow engine panic: %v", r)) we.syslog.Error(fmt.Sprintf("workflow engine panic: %v", r), slog.String("workflowId", execution.WorkflowId), slog.String("runId", execution.RunId)) - slog.Default().Error(fmt.Sprintf("workflow engine panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", execution.WorkflowId), slog.String("runId", execution.RunId)) + slog.Error(fmt.Sprintf("workflow engine panic: %v, stack trace: %s", r, string(debug.Stack())), slog.String("workflowId", execution.WorkflowId), slog.String("runId", execution.RunId)) } }() diff --git a/main.go b/main.go index 007f96ef0..dfea711cb 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ func main() { }) app.RootCmd.AddCommand(cmd.NewInternalCommand(app)) + app.RootCmd.AddCommand(cmd.NewWinscCommand(app)) app.OnServe().BindFunc(func(e *core.ServeEvent) error { scheduler.Register() @@ -72,7 +73,7 @@ func main() { return e.Next() }) - if err := app.Start(); err != nil { + if err := cmd.Serve(app); err != nil { slog.Error("[CERTIMATE] Start failed.", slog.Any("error", err)) } }