54d507b8d7
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
251 lines
7.0 KiB
Go
251 lines
7.0 KiB
Go
package launcher
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"github.com/kandev/kandev/internal/common/config"
|
|
)
|
|
|
|
var (
|
|
executablePath = os.Executable
|
|
launchManaged = runManagedApp
|
|
newSupervisorFn = newSupervisor
|
|
launchBackendFn = launchRestartableBackend
|
|
waitForHealthFn = waitForHealth
|
|
waitForReadyFn = waitForReady
|
|
attachSignalsFn = func(supervisor *processSupervisor) {
|
|
supervisor.attachSignals()
|
|
}
|
|
startParentWatchFn = func(supervisor *processSupervisor) *parentWatchdog {
|
|
watchdog := newParentWatchdogFromEnv(supervisor.shutdown, launcherExit)
|
|
watchdog.start()
|
|
return watchdog
|
|
}
|
|
)
|
|
|
|
func runStart(ctx context.Context, opts Options) int {
|
|
startupConfig, err := loadBootstrapConfig()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
backendPort, err := resolvePorts(opts, startupConfig)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 2
|
|
}
|
|
ports, err := pickPorts(backendPort, backendPortSource(opts))
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
endpoints, err := resolveBackendEndpoints(startupConfig, ports.BackendPort)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
ports.BackendURL = endpoints.accessURL
|
|
if err := ensureDataDirForConfig(startupConfig); err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
|
|
logLevel := resolveLogLevelForConfig(opts, startupConfig)
|
|
|
|
self, err := executablePath()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
return launchManaged(ctx, managedAppConfig{
|
|
Header: "start mode: using local build",
|
|
Mode: "start",
|
|
Backend: self,
|
|
BackendCWD: filepath.Dir(self),
|
|
Ports: ports,
|
|
LogLevel: logLevel,
|
|
Opts: opts,
|
|
Startup: startupConfig,
|
|
Endpoints: endpoints,
|
|
})
|
|
}
|
|
|
|
type managedAppConfig struct {
|
|
Header string
|
|
Mode string
|
|
Backend string
|
|
BackendCWD string
|
|
Ports portConfig
|
|
LogLevel string
|
|
Opts Options
|
|
Startup *config.Config
|
|
Endpoints backendEndpointSet
|
|
}
|
|
|
|
func resolveLogLevel(opts Options, configs ...*config.Config) string {
|
|
if len(configs) > 0 {
|
|
return resolveLogLevelForConfig(opts, configs[0])
|
|
}
|
|
return resolveLogLevelForConfig(opts, nil)
|
|
}
|
|
|
|
func resolveLogLevelForConfig(opts Options, cfg *config.Config) string {
|
|
if logLevel := os.Getenv("KANDEV_LOG_LEVEL"); logLevel != "" {
|
|
return logLevel
|
|
}
|
|
if configSourceIsExplicit(cfg, "logging.level") && cfg.Logging.Level != "" {
|
|
return cfg.Logging.Level
|
|
}
|
|
switch {
|
|
case opts.Debug:
|
|
return "debug"
|
|
default:
|
|
return "info"
|
|
}
|
|
}
|
|
|
|
func resolveConsoleLogLevel(opts Options) string {
|
|
if opts.Verbose {
|
|
return "info"
|
|
}
|
|
return "warn"
|
|
}
|
|
|
|
func runManagedApp(ctx context.Context, cfg managedAppConfig) int {
|
|
if cfg.Endpoints.accessURL == "" {
|
|
if cfg.Startup != nil {
|
|
endpoints, err := resolveBackendEndpoints(cfg.Startup, cfg.Ports.BackendPort)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
cfg.Endpoints = endpoints
|
|
} else {
|
|
cfg.Endpoints = endpointSetForAccessURL(cfg.Ports.BackendURL)
|
|
}
|
|
}
|
|
cfg.Ports.BackendURL = cfg.Endpoints.accessURL
|
|
ignoreBrokenPipeSignal()
|
|
logStartup(cfg.Header, cfg.Ports, resolveDatabasePathForConfig(cfg.Startup), cfg.LogLevel, serverHostForConfig(cfg.Startup))
|
|
setLauncherShutdownDebug(cfg.Opts.Debug || os.Getenv("KANDEV_SHUTDOWN_DEBUG") == "1")
|
|
shutdownDebugf("runManagedApp start mode=%q backend=%q backend_cwd=%q debug=%t", cfg.Mode, cfg.Backend, cfg.BackendCWD, cfg.Opts.Debug)
|
|
|
|
supervisor := newSupervisorFn()
|
|
attachSignalsFn(supervisor)
|
|
shutdownDebugf("runManagedApp signal handler attached")
|
|
parentWatchdog := startParentWatchFn(supervisor)
|
|
if parentWatchdog != nil {
|
|
defer parentWatchdog.stop()
|
|
}
|
|
healthToken, err := launchHealthToken()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
env := backendEnvForConfig(cfg.Ports, cfg.LogLevel, resolveConsoleLogLevel(cfg.Opts), cfg.Opts.Debug, healthToken, nil, cfg.Startup)
|
|
backend, dumpLogs, err := launchBackendFn(backendLaunchConfig{
|
|
Command: cfg.Backend,
|
|
Args: []string{"__backend"},
|
|
CWD: cfg.BackendCWD,
|
|
Env: env,
|
|
Quiet: false,
|
|
Ports: cfg.Ports,
|
|
Mode: cfg.Mode,
|
|
HomeDir: resolveHomeDirForConfig(cfg.Startup),
|
|
Supervisor: supervisor,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
shutdownDebugf("runManagedApp backend launched")
|
|
fmt.Println("[kandev] starting backend...")
|
|
readyURL, err := waitForHealthFn(ctx, cfg.Endpoints, backend, healthTimeoutForConfig(healthTimeoutReleaseMS, cfg.Startup), healthToken, dumpLogs)
|
|
if err != nil {
|
|
supervisor.shutdown("backend health failure")
|
|
fmt.Fprintln(os.Stderr, formatStartupFailure(err, cfg.Endpoints, cfg.Startup, backendLogPathForConfig(cfg.Startup), startupFailureStoppedBackend(err)))
|
|
return 1
|
|
}
|
|
if readyURL != "" {
|
|
cfg.Ports.BackendURL = readyURL
|
|
}
|
|
shutdownDebugf("runManagedApp backend healthy, waiting for readiness")
|
|
if err := waitForReadyFn(ctx, cfg.Ports.BackendURL, backend); err != nil {
|
|
supervisor.shutdown("backend readiness failure")
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
fmt.Printf("[kandev] backend ready at %s\n", cfg.Ports.BackendURL)
|
|
|
|
if !shouldOpenBrowser(cfg.Opts, cfg.Startup) {
|
|
fmt.Printf("[kandev] ready (headless) at %s\n", cfg.Ports.BackendURL)
|
|
return waitForAppExit(supervisor, backend)
|
|
}
|
|
fmt.Println("[kandev] open: " + cfg.Ports.BackendURL)
|
|
openBrowser(cfg.Ports.BackendURL)
|
|
return waitForAppExit(supervisor, backend)
|
|
}
|
|
|
|
func logStartup(header string, ports portConfig, dbPath, logLevel string, serverHosts ...string) {
|
|
fmt.Println("[kandev] " + header)
|
|
fmt.Println("[kandev] url:", ports.BackendURL)
|
|
serverHost := os.Getenv("KANDEV_SERVER_HOST")
|
|
if len(serverHosts) > 0 && serverHosts[0] != "" {
|
|
serverHost = serverHosts[0]
|
|
}
|
|
hosts := networkAddressesForBindHost(listHostNetworkAddresses(), serverHost)
|
|
for _, url := range networkURLsForPort(ports.BackendPort, hosts) {
|
|
fmt.Println("[kandev] network:", url)
|
|
}
|
|
fmt.Println("[kandev] mcp:", ports.BackendURL+"/mcp")
|
|
if dbPath != "" {
|
|
fmt.Println("[kandev] db:", dbPath)
|
|
}
|
|
if logLevel != "" {
|
|
fmt.Println("[kandev] log level:", logLevel)
|
|
}
|
|
}
|
|
|
|
func openBrowser(url string) {
|
|
if os.Getenv("KANDEV_NO_BROWSER") == "1" {
|
|
return
|
|
}
|
|
var cmd *exec.Cmd
|
|
switch {
|
|
case os.Getenv("OS") == "Windows_NT":
|
|
cmd = exec.Command(cmdExe, "/c", "start", "", url)
|
|
case runtime.GOOS == "darwin":
|
|
cmd = exec.Command("open", url)
|
|
default:
|
|
cmd = exec.Command("xdg-open", url)
|
|
}
|
|
_ = cmd.Start()
|
|
}
|
|
|
|
func serverHostForConfig(cfg *config.Config) string {
|
|
if cfg != nil && configSourceIsExplicit(cfg, "server.host") {
|
|
return cfg.Server.Host
|
|
}
|
|
return os.Getenv("KANDEV_SERVER_HOST")
|
|
}
|
|
|
|
func shouldOpenBrowser(opts Options, cfg *config.Config) bool {
|
|
if opts.Headless {
|
|
return false
|
|
}
|
|
if cfg != nil {
|
|
return !cfg.Launcher.NoBrowser
|
|
}
|
|
return os.Getenv("KANDEV_NO_BROWSER") != "1"
|
|
}
|
|
|
|
func exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|