//go:build linux package dashboard import ( "context" "errors" "fmt" "github.com/nezhahq/nezha/integration/agentcompat/internal/client" processharness "github.com/nezhahq/nezha/integration/agentcompat/internal/process" ) func (dashboard *Dashboard) StopProcess(ctx context.Context) (RuntimeIdentity, error) { dashboard.lifecycleMu.Lock() defer dashboard.lifecycleMu.Unlock() dashboard.stateMu.Lock() process := dashboard.currentProcess dashboard.currentProcess = nil dashboard.supervisor = nil dashboard.stateMu.Unlock() if process == nil { return RuntimeIdentity{}, errors.New("dashboard process is not running") } if process.receiptConn != nil { _ = process.receiptConn.Close() } if process.httpTransport != nil { process.httpTransport.CloseIdleConnections() } if process.tlsTransport != nil { process.tlsTransport.CloseIdleConnections() } if err := process.supervisor.Stop(ctx); err != nil { return process.identity, fmt.Errorf("stop dashboard process: %w", err) } process.record = process.supervisor.CleanupRecord() return process.identity, nil } func (dashboard *Dashboard) StartProcess(ctx context.Context) (RuntimeIdentity, error) { dashboard.lifecycleMu.Lock() defer dashboard.lifecycleMu.Unlock() dashboard.stateMu.Lock() if dashboard.currentProcess != nil { dashboard.stateMu.Unlock() return RuntimeIdentity{}, errors.New("dashboard process is already running") } dashboard.generation++ generation := dashboard.generation dashboard.stateMu.Unlock() dashboard.receiptMu.Lock() dashboard.receiptAccepted = false dashboard.receiptAcceptedCount = 0 dashboard.receiptGeneration = 0 dashboard.receiptMu.Unlock() process, err := dashboard.startGeneration(ctx, generation) if err != nil { return RuntimeIdentity{}, err } dashboard.stateMu.Lock() dashboard.currentProcess = process dashboard.supervisor = process.supervisor dashboard.processes = append(dashboard.processes, process) dashboard.stateMu.Unlock() return process.identity, nil } func (dashboard *Dashboard) FixtureIdentity() FixtureIdentity { identity := FixtureIdentity{WorkspaceRoot: dashboard.workspace.Root(), ConfigPath: dashboard.configPath, DatabasePath: dashboard.databasePath, BinaryPath: dashboard.binaryPath} if dashboard.httpListener != nil { identity.HTTP = dashboard.httpListener.Identity() } if dashboard.receiptListener != nil { identity.Receipt = dashboard.receiptListener.Identity() } if dashboard.httpsListener != nil { identity.HTTPS = dashboard.httpsListener.Identity() } return identity } func (dashboard *Dashboard) RuntimeIdentity() RuntimeIdentity { dashboard.stateMu.Lock() defer dashboard.stateMu.Unlock() if dashboard.currentProcess == nil { return RuntimeIdentity{} } return dashboard.currentProcess.identity } func (dashboard *Dashboard) Restart(ctx context.Context) error { if _, err := dashboard.StopProcess(ctx); err != nil { return err } _, err := dashboard.StartProcess(ctx) return err } func (dashboard *Dashboard) cleanupOnCancellation(ctx context.Context) { select { case <-ctx.Done(): dashboard.cleanupOnce.Do(func() { go dashboard.cleanup(context.WithoutCancel(ctx)) }) case <-dashboard.cleanupDone: } } func (dashboard *Dashboard) cleanup(ctx context.Context) { defer close(dashboard.cleanupDone) var stopError error cleanupReceipt := dashboard.cleanupProcesses(ctx, &stopError) if err := dashboard.workspace.Close(); err != nil { stopError = errors.Join(stopError, fmt.Errorf("close dashboard workspace: %w", err)) cleanupReceipt = processharness.NewCleanupReceipt(append(cleanupReceipt.Processes, processharness.CleanupRecord{Name: "dashboard-workspace", Error: client.Redact(err.Error())})) } dashboard.cleanupMu.Lock() dashboard.cleanupError = stopError dashboard.cleanupReceipt = cleanupReceipt dashboard.cleanupMu.Unlock() } func (dashboard *Dashboard) cleanupProcesses(ctx context.Context, stopError *error) processharness.CleanupReceipt { cleanupReceipt := processharness.CleanupReceipt{} dashboard.stateMu.Lock() processes := append([]*dashboardGeneration(nil), dashboard.processes...) legacySupervisor := dashboard.supervisor dashboard.stateMu.Unlock() if len(processes) == 0 && legacySupervisor != nil { if err := legacySupervisor.Stop(ctx); err != nil { *stopError = errors.Join(*stopError, fmt.Errorf("stop dashboard process: %w", err)) } return processharness.NewCleanupReceipt([]processharness.CleanupRecord{legacySupervisor.CleanupRecord()}) } for _, process := range processes { if process.receiptConn != nil { _ = process.receiptConn.Close() } if process.httpTransport != nil { process.httpTransport.CloseIdleConnections() } if process.tlsTransport != nil { process.tlsTransport.CloseIdleConnections() } stopContext, cancel := context.WithTimeout(ctx, failedStartCleanupTimeout) if err := process.supervisor.Stop(stopContext); err != nil { *stopError = errors.Join(*stopError, fmt.Errorf("stop dashboard process: %w", err)) } cancel() process.record = process.supervisor.CleanupRecord() if process.record.Forced { *stopError = errors.Join(*stopError, errors.New("dashboard required forced SIGKILL cleanup")) } cleanupReceipt.Processes = append(cleanupReceipt.Processes, process.record) } return processharness.NewCleanupReceipt(cleanupReceipt.Processes) }