@@ -6,12 +6,12 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "os"
1011 "os/exec"
1112 "slices"
1213 "strconv"
1314 "strings"
14- "sync"
1515 "syscall"
1616 "time"
1717)
@@ -24,8 +24,10 @@ type cmd struct {
2424 Stdout io.Writer
2525 Stderr io.Writer
2626
27- execCmd * exec.Cmd
28- execCmdOnce sync.Once
27+ execCmd * exec.Cmd
28+ err error
29+ dur time.Duration
30+ logger * slog.Logger
2931}
3032
3133func command (args ... any ) * cmd {
@@ -35,69 +37,141 @@ func command(args ...any) *cmd {
3537 "--extra-experimental-features" , "ca-derivations" ,
3638 "--option" , "experimental-features" , "nix-command flakes fetch-closure" ,
3739 }, args ... ),
40+ logger : slog .Default (),
3841 }
3942 return cmd
4043}
4144
4245func (c * cmd ) CombinedOutput (ctx context.Context ) ([]byte , error ) {
43- out , err := c .initExecCommand (ctx ).CombinedOutput ()
44- return out , c .error (ctx , err )
46+ cmd := c .initExecCommand (ctx )
47+ c .logger .DebugContext (ctx , "nix command starting" , "cmd" , c )
48+
49+ start := time .Now ()
50+ out , err := cmd .CombinedOutput ()
51+ c .dur = time .Since (start )
52+
53+ c .err = c .error (ctx , err )
54+ c .logger .DebugContext (ctx , "nix command exited" , "cmd" , c )
55+ return out , c .err
4556}
4657
4758func (c * cmd ) Output (ctx context.Context ) ([]byte , error ) {
48- out , err := c .initExecCommand (ctx ).Output ()
49- return out , c .error (ctx , err )
59+ cmd := c .initExecCommand (ctx )
60+ c .logger .DebugContext (ctx , "nix command starting" , "cmd" , c )
61+
62+ start := time .Now ()
63+ out , err := cmd .Output ()
64+ c .dur = time .Since (start )
65+
66+ c .err = c .error (ctx , err )
67+ c .logger .DebugContext (ctx , "nix command exited" , "cmd" , c )
68+ return out , c .err
5069}
5170
5271func (c * cmd ) Run (ctx context.Context ) error {
53- return c .error (ctx , c .initExecCommand (ctx ).Run ())
72+ cmd := c .initExecCommand (ctx )
73+ c .logger .DebugContext (ctx , "nix command starting" , "cmd" , c )
74+
75+ start := time .Now ()
76+ err := cmd .Run ()
77+ c .dur = time .Since (start )
78+
79+ c .err = c .error (ctx , err )
80+ c .logger .DebugContext (ctx , "nix command exited" , "cmd" , c )
81+ return c .err
82+ }
83+
84+ func (c * cmd ) LogValue () slog.Value {
85+ attrs := []slog.Attr {
86+ slog .Any ("args" , c .Args ),
87+ }
88+ if c .execCmd == nil {
89+ return slog .GroupValue (attrs ... )
90+ }
91+ attrs = append (attrs , slog .String ("path" , c .execCmd .Path ))
92+
93+ var exitErr * exec.ExitError
94+ if errors .As (c .err , & exitErr ) {
95+ stderr := c .stderrExcerpt (exitErr .Stderr )
96+ if len (stderr ) != 0 {
97+ attrs = append (attrs , slog .String ("stderr" , stderr ))
98+ }
99+ }
100+ if proc := c .execCmd .Process ; proc != nil {
101+ attrs = append (attrs , slog .Int ("pid" , proc .Pid ))
102+ }
103+ if procState := c .execCmd .ProcessState ; procState != nil {
104+ if procState .Exited () {
105+ attrs = append (attrs , slog .Int ("code" , procState .ExitCode ()))
106+ }
107+ if status , ok := procState .Sys ().(syscall.WaitStatus ); ok && status .Signaled () {
108+ if status .Signaled () {
109+ attrs = append (attrs , slog .String ("signal" , status .Signal ().String ()))
110+ }
111+ }
112+ }
113+ if c .dur != 0 {
114+ attrs = append (attrs , slog .Duration ("dur" , c .dur ))
115+ }
116+ return slog .GroupValue (attrs ... )
54117}
55118
56119func (c * cmd ) String () string {
57120 return c .Args .String ()
58121}
59122
60123func (c * cmd ) initExecCommand (ctx context.Context ) * exec.Cmd {
61- c .execCmdOnce .Do (func () {
62- args := c .Args .StringSlice ()
63- c .execCmd = exec .CommandContext (ctx , args [0 ], args [1 :]... )
64- c .execCmd .Env = c .Env
65- c .execCmd .Stdin = c .Stdin
66- c .execCmd .Stdout = c .Stdout
67- c .execCmd .Stderr = c .Stderr
68-
69- c .execCmd .Cancel = func () error {
70- // Try to let Nix exit gracefully by sending an
71- // interrupt instead of the default behavior of killing
72- // it.
73- err := c .execCmd .Process .Signal (os .Interrupt )
74- if errors .Is (err , os .ErrProcessDone ) {
75- // Nix already exited; execCmd.Wait will use the
76- // exit code.
77- return err
78- }
79- if err != nil {
80- // We failed to send SIGINT, so kill the process
81- // instead.
82- //
83- // - If Nix already exited, Kill will return
84- // os.ErrProcessDone and execCmd.Wait will use
85- // the exit code.
86- // - Otherwise, execCmd.Wait will always return
87- // an error.
88- return c .execCmd .Process .Kill ()
89- }
124+ if c .execCmd != nil {
125+ return c .execCmd
126+ }
90127
91- // We sent the SIGINT successfully. It's still possible
92- // for Nix to exit successfully, so return
93- // os.ErrProcessDone so that execCmd.Wait uses the exit
94- // code instead of ctx.Err.
95- return os .ErrProcessDone
128+ args := c .Args .StringSlice ()
129+ c .execCmd = exec .CommandContext (ctx , args [0 ], args [1 :]... )
130+ c .execCmd .Env = c .Env
131+ c .execCmd .Stdin = c .Stdin
132+ c .execCmd .Stdout = c .Stdout
133+ c .execCmd .Stderr = c .Stderr
134+
135+ c .execCmd .Cancel = func () error {
136+ // Try to let Nix exit gracefully by sending an interrupt
137+ // instead of the default behavior of killing it.
138+ c .logger .DebugContext (ctx , "sending interrupt to nix process" , slog .Group ("cmd" ,
139+ "args" , c .Args ,
140+ "path" , c .execCmd .Path ,
141+ "pid" , c .execCmd .Process .Pid ,
142+ ))
143+ err := c .execCmd .Process .Signal (os .Interrupt )
144+ if errors .Is (err , os .ErrProcessDone ) {
145+ // Nix already exited; execCmd.Wait will use the exit
146+ // code.
147+ return err
96148 }
97- // Kill Nix if it doesn't exit within 15 seconds of Devbox
98- // sending an interrupt.
99- c .execCmd .WaitDelay = 15 * time .Second
100- })
149+ if err != nil {
150+ // We failed to send SIGINT, so kill the process
151+ // instead.
152+ //
153+ // - If Nix already exited, Kill will return
154+ // os.ErrProcessDone and execCmd.Wait will use
155+ // the exit code.
156+ // - Otherwise, execCmd.Wait will always return an
157+ // error.
158+ c .logger .ErrorContext (ctx , "error interrupting nix process, attempting to kill" ,
159+ "err" , err , slog .Group ("cmd" ,
160+ "args" , c .Args ,
161+ "path" , c .execCmd .Path ,
162+ "pid" , c .execCmd .Process .Pid ,
163+ ))
164+ return c .execCmd .Process .Kill ()
165+ }
166+
167+ // We sent the SIGINT successfully. It's still possible for Nix
168+ // to exit successfully, so return os.ErrProcessDone so that
169+ // execCmd.Wait uses the exit code instead of ctx.Err.
170+ return os .ErrProcessDone
171+ }
172+ // Kill Nix if it doesn't exit within 15 seconds of Devbox sending an
173+ // interrupt.
174+ c .execCmd .WaitDelay = 15 * time .Second
101175 return c .execCmd
102176}
103177
0 commit comments