@@ -6,12 +6,12 @@ import (
6
6
"errors"
7
7
"fmt"
8
8
"io"
9
+ "log/slog"
9
10
"os"
10
11
"os/exec"
11
12
"slices"
12
13
"strconv"
13
14
"strings"
14
- "sync"
15
15
"syscall"
16
16
"time"
17
17
)
@@ -24,8 +24,10 @@ type cmd struct {
24
24
Stdout io.Writer
25
25
Stderr io.Writer
26
26
27
- execCmd * exec.Cmd
28
- execCmdOnce sync.Once
27
+ execCmd * exec.Cmd
28
+ err error
29
+ dur time.Duration
30
+ logger * slog.Logger
29
31
}
30
32
31
33
func command (args ... any ) * cmd {
@@ -35,69 +37,141 @@ func command(args ...any) *cmd {
35
37
"--extra-experimental-features" , "ca-derivations" ,
36
38
"--option" , "experimental-features" , "nix-command flakes fetch-closure" ,
37
39
}, args ... ),
40
+ logger : slog .Default (),
38
41
}
39
42
return cmd
40
43
}
41
44
42
45
func (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
45
56
}
46
57
47
58
func (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
50
69
}
51
70
52
71
func (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 ... )
54
117
}
55
118
56
119
func (c * cmd ) String () string {
57
120
return c .Args .String ()
58
121
}
59
122
60
123
func (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
+ }
90
127
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
96
148
}
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
101
175
return c .execCmd
102
176
}
103
177
0 commit comments