99 "io"
1010 "net/http"
1111 "os"
12+ "os/exec"
1213 "path/filepath"
1314 "sort"
1415 "strings"
@@ -65,10 +66,20 @@ type daemonOptions struct {
6566}
6667
6768type daemonControlOptions struct {
68- StatePath string
69- Status bool
70- Stop bool
71- Tick bool
69+ StatePath string
70+ Status bool
71+ Stop bool
72+ Tick bool
73+ Start bool
74+ Restart bool
75+ HeartbeatInterval time.Duration
76+ }
77+
78+ type daemonProcessStartOptions struct {
79+ WorkingDirectory string
80+ SessionID contracts.ID
81+ StatePath string
82+ Heartbeat time.Duration
7283}
7384
7485type repeatedStringFlag []string
@@ -95,6 +106,9 @@ func run(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
95106 daemonStatus := flags .Bool ("daemon-status" , false , "print daemon status and exit" )
96107 daemonStop := flags .Bool ("daemon-stop" , false , "stop the daemon recorded in daemon state" )
97108 daemonTick := flags .Bool ("daemon-tick" , false , "trigger one daemon schedule tick" )
109+ daemonStart := flags .Bool ("daemon-start" , false , "start daemon in the background" )
110+ daemonRestart := flags .Bool ("daemon-restart" , false , "restart daemon in the background" )
111+ daemonSession := flags .String ("daemon-session" , "" , "daemon session id" )
98112 daemonStatePath := flags .String ("daemon-state" , "" , "daemon state path" )
99113 cwd := flags .String ("cwd" , "" , "working directory" )
100114 printMode := flags .Bool ("print" , false , "print response and exit" )
@@ -154,12 +168,18 @@ func run(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) int
154168 fmt .Fprintf (stderr , "ccgo: %v\n " , err )
155169 return 1
156170 }
157- if * daemonStatus || * daemonStop || * daemonTick {
171+ if strings .TrimSpace (* daemonSession ) != "" {
172+ state .SetSessionID (contracts .ID (strings .TrimSpace (* daemonSession )))
173+ }
174+ if * daemonStatus || * daemonStop || * daemonTick || * daemonStart || * daemonRestart {
158175 return runDaemonControl (context .Background (), state , daemonControlOptions {
159- StatePath : strings .TrimSpace (* daemonStatePath ),
160- Status : * daemonStatus ,
161- Stop : * daemonStop ,
162- Tick : * daemonTick ,
176+ StatePath : strings .TrimSpace (* daemonStatePath ),
177+ Status : * daemonStatus ,
178+ Stop : * daemonStop ,
179+ Tick : * daemonTick ,
180+ Start : * daemonStart ,
181+ Restart : * daemonRestart ,
182+ HeartbeatInterval : * daemonHeartbeat ,
163183 }, stdout , stderr )
164184 }
165185 if * daemonMode || * daemonOnce {
@@ -265,9 +285,11 @@ func runChromeNativeHost(stdin io.Reader, stdout io.Writer, stderr io.Writer) in
265285 }
266286}
267287
288+ var startDaemonProcess = startDaemonProcessDefault
289+
268290func runDaemonControl (ctx context.Context , state * bootstrap.State , options daemonControlOptions , stdout io.Writer , stderr io.Writer ) int {
269291 actionCount := 0
270- for _ , enabled := range []bool {options .Status , options .Stop , options .Tick } {
292+ for _ , enabled := range []bool {options .Status , options .Stop , options .Tick , options . Start , options . Restart } {
271293 if enabled {
272294 actionCount ++
273295 }
@@ -276,6 +298,13 @@ func runDaemonControl(ctx context.Context, state *bootstrap.State, options daemo
276298 fmt .Fprintf (stderr , "ccgo daemon: daemon control actions are mutually exclusive\n " )
277299 return 2
278300 }
301+ if options .Start || options .Restart {
302+ if err := startOrRestartDaemon (ctx , state , options , stdout ); err != nil {
303+ fmt .Fprintf (stderr , "ccgo daemon: %v\n " , err )
304+ return 1
305+ }
306+ return 0
307+ }
279308 statePath , err := resolveDaemonStatePath (state , options .StatePath )
280309 if err != nil {
281310 fmt .Fprintf (stderr , "ccgo daemon: %v\n " , err )
@@ -306,6 +335,138 @@ func runDaemonControl(ctx context.Context, state *bootstrap.State, options daemo
306335 return 2
307336}
308337
338+ func startOrRestartDaemon (ctx context.Context , state * bootstrap.State , options daemonControlOptions , stdout io.Writer ) error {
339+ if options .HeartbeatInterval <= 0 {
340+ return errors .New ("daemon heartbeat interval must be positive" )
341+ }
342+ runner , err := state .ConversationRunner ()
343+ if err != nil {
344+ return err
345+ }
346+ if runner .SessionPath == "" && runner .SessionID != "" {
347+ runner .SessionPath = session .TranscriptPath (runner .WorkingDirectory , runner .SessionID )
348+ }
349+ existingPath , err := resolveDaemonStatePath (state , options .StatePath )
350+ if err != nil {
351+ return err
352+ }
353+ existingState , err := daemonpkg .LoadState (existingPath )
354+ if err != nil {
355+ return err
356+ }
357+ existingRuntime := daemonpkg .RuntimeStateAt (existingState , time .Now ().UTC (), 2 * time .Minute )
358+ if options .Restart && existingState .GeneratedAt != "" && existingRuntime == daemonpkg .RuntimeRunning && strings .TrimSpace (existingState .Endpoint ) != "" {
359+ if err := stopDaemon (ctx , io .Discard , existingPath ); err != nil {
360+ return err
361+ }
362+ } else if options .Start && existingRuntime == daemonpkg .RuntimeRunning && existingState .GeneratedAt != "" {
363+ return writeAlreadyRunningDaemon (stdout , existingPath , existingState )
364+ }
365+
366+ sessionID := contracts .NewID ()
367+ sessionPath := session .TranscriptPath (runner .WorkingDirectory , sessionID )
368+ statePath := daemonpkg .SessionStatePath (sessionPath , sessionID )
369+ pid , err := startDaemonProcess (ctx , daemonProcessStartOptions {
370+ WorkingDirectory : runner .WorkingDirectory ,
371+ SessionID : sessionID ,
372+ StatePath : statePath ,
373+ Heartbeat : options .HeartbeatInterval ,
374+ })
375+ if err != nil {
376+ return err
377+ }
378+ startCtx , cancel := context .WithTimeout (ctx , 2 * time .Second )
379+ defer cancel ()
380+ started := waitForDaemonRuntimeState (startCtx , statePath , daemonpkg .RuntimeRunning )
381+ if started .RuntimeState != daemonpkg .RuntimeRunning {
382+ return fmt .Errorf ("daemon start did not report running state: %s" , statePath )
383+ }
384+ lines := []string {
385+ "ccgo daemon started" ,
386+ "session_id=" + string (sessionID ),
387+ "state_path=" + statePath ,
388+ "runtime_state=" + daemonpkg .RuntimeRunning ,
389+ }
390+ if started .PID > 0 {
391+ lines = append (lines , fmt .Sprintf ("pid=%d" , started .PID ))
392+ } else if pid > 0 {
393+ lines = append (lines , fmt .Sprintf ("pid=%d" , pid ))
394+ }
395+ if started .Endpoint != "" {
396+ lines = append (lines , "endpoint=" + started .Endpoint )
397+ }
398+ if started .GeneratedAt != "" {
399+ lines = append (lines , "generated_at=" + started .GeneratedAt )
400+ }
401+ _ , err = fmt .Fprintln (stdout , strings .Join (lines , "\n " ))
402+ return err
403+ }
404+
405+ func writeAlreadyRunningDaemon (stdout io.Writer , statePath string , state daemonpkg.State ) error {
406+ lines := []string {
407+ "ccgo daemon already running" ,
408+ "state_path=" + statePath ,
409+ "runtime_state=" + daemonpkg .RuntimeRunning ,
410+ }
411+ if state .PID > 0 {
412+ lines = append (lines , fmt .Sprintf ("pid=%d" , state .PID ))
413+ }
414+ if state .Endpoint != "" {
415+ lines = append (lines , "endpoint=" + state .Endpoint )
416+ }
417+ _ , err := fmt .Fprintln (stdout , strings .Join (lines , "\n " ))
418+ return err
419+ }
420+
421+ func startDaemonProcessDefault (ctx context.Context , options daemonProcessStartOptions ) (int , error ) {
422+ executable , err := os .Executable ()
423+ if err != nil {
424+ return 0 , err
425+ }
426+ if strings .TrimSpace (options .WorkingDirectory ) == "" {
427+ return 0 , errors .New ("daemon working directory is unavailable" )
428+ }
429+ if options .SessionID == "" {
430+ return 0 , errors .New ("daemon session id is unavailable" )
431+ }
432+ logPath := options .StatePath + ".log"
433+ if err := os .MkdirAll (filepath .Dir (logPath ), 0o755 ); err != nil {
434+ return 0 , err
435+ }
436+ logFile , err := os .OpenFile (logPath , os .O_CREATE | os .O_WRONLY | os .O_APPEND , 0o600 )
437+ if err != nil {
438+ return 0 , err
439+ }
440+ defer logFile .Close ()
441+ args := []string {
442+ "--cwd" , options .WorkingDirectory ,
443+ "--daemon" ,
444+ "--daemon-session" , string (options .SessionID ),
445+ "--daemon-heartbeat" , options .Heartbeat .String (),
446+ }
447+ cmd := exec .Command (executable , args ... )
448+ cmd .Dir = options .WorkingDirectory
449+ cmd .Env = os .Environ ()
450+ cmd .Stdin = nil
451+ cmd .Stdout = logFile
452+ cmd .Stderr = logFile
453+ if ctx != nil {
454+ select {
455+ case <- ctx .Done ():
456+ return 0 , ctx .Err ()
457+ default :
458+ }
459+ }
460+ if err := cmd .Start (); err != nil {
461+ return 0 , err
462+ }
463+ pid := cmd .Process .Pid
464+ if err := cmd .Process .Release (); err != nil {
465+ return 0 , err
466+ }
467+ return pid , nil
468+ }
469+
309470func resolveDaemonStatePath (state * bootstrap.State , explicit string ) (string , error ) {
310471 if strings .TrimSpace (explicit ) != "" {
311472 return filepath .Clean (strings .TrimSpace (explicit )), nil
0 commit comments