179 lines
4.7 KiB
Go
179 lines
4.7 KiB
Go
package launcher
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
)
|
|
|
|
var (
|
|
executablePath = os.Executable
|
|
launchManaged = runManagedApp
|
|
newSupervisorFn = newSupervisor
|
|
launchBackendFn = launchRestartableBackend
|
|
waitForHealthFn = waitForHealth
|
|
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 {
|
|
backendPort, err := resolvePorts(opts)
|
|
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
|
|
}
|
|
if err := ensureDataDir(); err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
|
|
logLevel := resolveLogLevel(opts)
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
type managedAppConfig struct {
|
|
Header string
|
|
Mode string
|
|
Backend string
|
|
BackendCWD string
|
|
Ports portConfig
|
|
LogLevel string
|
|
Opts Options
|
|
}
|
|
|
|
func resolveLogLevel(opts Options) string {
|
|
if logLevel := os.Getenv("KANDEV_LOG_LEVEL"); logLevel != "" {
|
|
return logLevel
|
|
}
|
|
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 {
|
|
ignoreBrokenPipeSignal()
|
|
logStartup(cfg.Header, cfg.Ports, resolveDatabasePath(), cfg.LogLevel)
|
|
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 := backendEnv(cfg.Ports, cfg.LogLevel, resolveConsoleLogLevel(cfg.Opts), cfg.Opts.Debug, healthToken, nil)
|
|
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: resolveHomeDir(),
|
|
Supervisor: supervisor,
|
|
})
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
shutdownDebugf("runManagedApp backend launched")
|
|
fmt.Println("[kandev] starting backend...")
|
|
if err := waitForHealthFn(ctx, cfg.Ports.BackendURL, backend, healthTimeout(healthTimeoutReleaseMS), healthToken, dumpLogs); err != nil {
|
|
supervisor.shutdown("backend health failure")
|
|
fmt.Fprintln(os.Stderr, "[kandev] "+err.Error())
|
|
return 1
|
|
}
|
|
fmt.Printf("[kandev] backend ready at %s\n", cfg.Ports.BackendURL)
|
|
|
|
if cfg.Opts.Headless {
|
|
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) {
|
|
fmt.Println("[kandev] " + header)
|
|
fmt.Println("[kandev] url:", ports.BackendURL)
|
|
hosts := networkAddressesForBindHost(listHostNetworkAddresses(), os.Getenv("KANDEV_SERVER_HOST"))
|
|
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 exists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|