1//nolint:errcheck
2package system
3
4import (
5 "context"
6 "fmt"
7 "os"
8 "os/exec"
9 "strings"
10 "time"
11)
12
13// ChrootExec executes a command inside a chroot environment
14func ChrootExec(logPath, mountPoint, command string, args ...string) error {
15 // Construct the command to run in chroot
16 fullCommand := command
17 if len(args) > 0 {
18 fullCommand = command + " " + strings.Join(args, " ")
19 }
20
21 result := RunLogged(logPath, "arch-chroot", mountPoint, "bash", "-c", fullCommand)
22 if result.Error != nil {
23 return fmt.Errorf("failed to execute in chroot: %w", result.Error)
24 }
25 return nil
26}
27
28// ChrootExecWithOutput executes a command inside a chroot environment and captures output
29func ChrootExecWithOutput(logPath, mountPoint, command string, args ...string) (string, error) {
30 // Construct the command to run in chroot
31 fullCommand := command
32 if len(args) > 0 {
33 fullCommand = command + " " + strings.Join(args, " ")
34 }
35
36 result := RunLogged(logPath, "arch-chroot", mountPoint, "bash", "-c", fullCommand)
37 if result.Error != nil {
38 return "", fmt.Errorf("failed to execute in chroot: %w", result.Error)
39 }
40 return result.Output, nil
41}
42
43// ChrootExecWithOutputAndContext executes a command inside chroot with context and captures output
44func ChrootExecWithOutputAndContext(ctx context.Context, logPath, mountPoint, command string, args ...string) (string, error) {
45 // Construct the command to run in chroot
46 fullCommand := command
47 if len(args) > 0 {
48 fullCommand = command + " " + strings.Join(args, " ")
49 }
50
51 cmd := exec.CommandContext(ctx, "arch-chroot", mountPoint, "bash", "-c", fullCommand)
52
53 // Capture output
54 output, err := cmd.CombinedOutput()
55
56 // Also log to file
57 if logPath != "" {
58 logFile, logErr := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
59 if logErr == nil {
60 defer func() {
61 if err := logFile.Close(); err != nil {
62 fmt.Fprintf(os.Stderr, "failed to close log file: %v\n", err)
63 }
64 }()
65 if _, err := logFile.Write(output); err != nil {
66 fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
67 }
68 }
69 }
70
71 if err != nil {
72 // Check if it was a context deadline exceeded
73 if ctx.Err() == context.DeadlineExceeded {
74 return string(output), fmt.Errorf("command timed out: %w", err)
75 }
76 return string(output), fmt.Errorf("failed to execute in chroot: %w", err)
77 }
78
79 return string(output), nil
80}
81
82// ChrootExecWithContext executes a command inside chroot with timeout support via context
83// The context deadline will terminate the command if it takes too long
84func ChrootExecWithContext(ctx context.Context, logPath, mountPoint, command string) error {
85 cmd := exec.CommandContext(ctx, "arch-chroot", mountPoint, "bash", "-c", command)
86
87 // Set up logging
88 if logPath != "" {
89 logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
90 if err == nil {
91 defer func() {
92 if closeErr := logFile.Close(); closeErr != nil {
93 fmt.Fprintf(os.Stderr, "failed to close log file: %v\n", closeErr)
94 }
95 }()
96 cmd.Stdout = logFile
97 cmd.Stderr = logFile
98 }
99 }
100
101 // Execute the command
102 if err := cmd.Run(); err != nil {
103 // Check if it was a context deadline exceeded
104 if ctx.Err() == context.DeadlineExceeded {
105 return fmt.Errorf("command timed out: %w", err)
106 }
107 return fmt.Errorf("failed to execute in chroot: %w", err)
108 }
109
110 return nil
111}
112
113// ChrootExecWithStdin executes a command inside chroot with stdin input
114// This is secure for passing sensitive data like passwords without exposing them in process listings
115func ChrootExecWithStdin(logPath, mountPoint, command, stdin string) error {
116 cmd := exec.Command("arch-chroot", mountPoint, "bash", "-c", command)
117
118 // Set up stdin pipe
119 stdinPipe, err := cmd.StdinPipe()
120 if err != nil {
121 return fmt.Errorf("failed to create stdin pipe: %w", err)
122 }
123
124 // Set up logging
125 if logPath != "" {
126 logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
127 if err == nil {
128 defer func() {
129 if closeErr := logFile.Close(); closeErr != nil {
130 fmt.Fprintf(os.Stderr, "failed to close log file: %v\n", closeErr)
131 }
132 }()
133 cmd.Stdout = logFile
134 cmd.Stderr = logFile
135 }
136 }
137
138 // Start the command
139 if err := cmd.Start(); err != nil {
140 return fmt.Errorf("failed to start command: %w", err)
141 }
142
143 // Write to stdin and close pipe
144 if _, err := stdinPipe.Write([]byte(stdin)); err != nil {
145 return fmt.Errorf("failed to write to stdin: %w", err)
146 }
147 if err := stdinPipe.Close(); err != nil {
148 return fmt.Errorf("failed to close stdin: %w", err)
149 }
150
151 // Wait for command to complete
152 if err := cmd.Wait(); err != nil {
153 return fmt.Errorf("failed to execute in chroot: %w", err)
154 }
155
156 return nil
157}
158
159// ChrootPacman runs pacman inside the chroot
160func ChrootPacman(logPath, mountPoint, operation string, packages ...string) error {
161 args := append([]string{operation, "--noconfirm"}, packages...)
162 command := "pacman " + strings.Join(args, " ")
163
164 result := RunLogged(logPath, "arch-chroot", mountPoint, "bash", "-c", command)
165 if result.Error != nil {
166 return fmt.Errorf("failed to run pacman in chroot: %w", result.Error)
167 }
168 return nil
169}
170
171// ChrootSystemctl runs systemctl inside the chroot
172func ChrootSystemctl(logPath, mountPoint, action, service string) error {
173 command := fmt.Sprintf("systemctl %s %s", action, service)
174
175 result := RunLogged(logPath, "arch-chroot", mountPoint, "bash", "-c", command)
176 if result.Error != nil {
177 return fmt.Errorf("failed to run systemctl in chroot: %w", result.Error)
178 }
179 return nil
180}
181
182// Unmount unmounts a filesystem
183func Unmount(logPath, mountPoint string) error {
184 result := RunLogged(logPath, "umount", "-R", mountPoint)
185 if result.Error != nil {
186 return fmt.Errorf("failed to unmount %s: %w", mountPoint, result.Error)
187 }
188 return nil
189}
190
191// DefaultChrootSession represents a persistent chroot session
192type DefaultChrootSession struct {
193 cmd *exec.Cmd
194 stdin *os.File
195 stdoutPipe *os.File
196 stderrPipe *os.File
197 logFile *os.File
198 logPath string
199 mountPoint string
200 ctx context.Context
201 cancel context.CancelFunc
202 outputChan chan string
203 errorChan chan error
204 closed bool
205}
206
207// BeginSession starts a persistent chroot session
208func BeginSession(logPath, mountPoint string) (*DefaultChrootSession, error) {
209 return BeginSessionWithContext(context.Background(), logPath, mountPoint)
210}
211
212// BeginSessionWithContext starts a persistent chroot session with context support
213func BeginSessionWithContext(ctx context.Context, logPath, mountPoint string) (*DefaultChrootSession, error) {
214 // Create a cancellable context
215 sessionCtx, cancel := context.WithCancel(ctx)
216
217 // Start arch-chroot with bash
218 cmd := exec.CommandContext(sessionCtx, "arch-chroot", mountPoint, "bash", "--norc", "--noprofile")
219
220 // Create pipes for stdin, stdout, stderr
221 stdinPipe, err := cmd.StdinPipe()
222 if err != nil {
223 cancel()
224 return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
225 }
226
227 stdoutPipe, err := cmd.StdoutPipe()
228 if err != nil {
229 cancel()
230 return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
231 }
232
233 stderrPipe, err := cmd.StderrPipe()
234 if err != nil {
235 cancel()
236 return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
237 }
238
239 // Open log file
240 var logFile *os.File
241 if logPath != "" {
242 logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
243 if err != nil {
244 cancel()
245 return nil, fmt.Errorf("failed to open log file: %w", err)
246 }
247 }
248
249 // Start the command
250 if err := cmd.Start(); err != nil {
251 if logFile != nil {
252 if closeErr := logFile.Close(); closeErr != nil {
253 fmt.Fprintf(os.Stderr, "failed to close log file: %v\n", closeErr)
254 }
255 }
256 cancel()
257 return nil, fmt.Errorf("failed to start chroot session: %w", err)
258 }
259
260 session := &DefaultChrootSession{
261 cmd: cmd,
262 stdin: stdinPipe.(*os.File),
263 stdoutPipe: stdoutPipe.(*os.File),
264 stderrPipe: stderrPipe.(*os.File),
265 logFile: logFile,
266 logPath: logPath,
267 mountPoint: mountPoint,
268 ctx: sessionCtx,
269 cancel: cancel,
270 outputChan: make(chan string, 100),
271 errorChan: make(chan error, 1),
272 closed: false,
273 }
274
275 // Start goroutines to handle output
276 go session.handleOutput()
277 go session.handleError()
278
279 return session, nil
280}
281
282// handleOutput continuously reads from stdout and logs it
283func (s *DefaultChrootSession) handleOutput() {
284 buf := make([]byte, 4096)
285 for {
286 n, err := s.stdoutPipe.Read(buf)
287 if n > 0 {
288 data := string(buf[:n])
289 if s.logFile != nil {
290 if _, err := s.logFile.WriteString(data); err != nil {
291 fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
292 }
293 if err := s.logFile.Sync(); err != nil {
294 fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
295 }
296 }
297 s.outputChan <- data
298 }
299 if err != nil {
300 close(s.outputChan)
301 return
302 }
303 }
304}
305
306// handleError continuously reads from stderr and logs it
307func (s *DefaultChrootSession) handleError() {
308 buf := make([]byte, 4096)
309 for {
310 n, err := s.stderrPipe.Read(buf)
311 if n > 0 {
312 data := string(buf[:n])
313 if s.logFile != nil {
314 if _, err := s.logFile.WriteString(data); err != nil {
315 fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
316 }
317 if err := s.logFile.Sync(); err != nil {
318 fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
319 }
320 }
321 }
322 if err != nil {
323 return
324 }
325 }
326}
327
328// Exec executes a command in the chroot session
329func (s *DefaultChrootSession) Exec(command string) error {
330 return s.ExecWithContext(s.ctx, command)
331}
332
333// ExecWithOutput executes a command and returns its output
334func (s *DefaultChrootSession) ExecWithOutput(command string) (string, error) {
335 return s.ExecWithOutputAndContext(s.ctx, command)
336}
337
338// ExecWithContext executes a command with context support
339func (s *DefaultChrootSession) ExecWithContext(ctx context.Context, command string) error {
340 _, err := s.ExecWithOutputAndContext(ctx, command)
341 return err
342}
343
344// ExecWithOutputAndContext executes a command with context and returns output
345func (s *DefaultChrootSession) ExecWithOutputAndContext(ctx context.Context, command string) (string, error) {
346 if s.closed {
347 return "", fmt.Errorf("session is closed")
348 }
349
350 // Clear the output channel
351 for len(s.outputChan) > 0 {
352 <-s.outputChan
353 }
354
355 // Write the command followed by a marker
356 marker := fmt.Sprintf("ARCHUP_CMD_DONE_%d", os.Getpid())
357 fullCommand := fmt.Sprintf("%s\necho \"%s_$?\"\n", command, marker)
358
359 if _, err := s.stdin.WriteString(fullCommand); err != nil {
360 return "", fmt.Errorf("failed to write command: %w", err)
361 }
362
363 // Collect output until we see the marker
364 var output strings.Builder
365 var exitCode string
366
367 for {
368 select {
369 case <-ctx.Done():
370 return output.String(), fmt.Errorf("command context cancelled: %w", ctx.Err())
371 case data, ok := <-s.outputChan:
372 if !ok {
373 return output.String(), fmt.Errorf("session stdout closed unexpectedly")
374 }
375
376 // Check if output contains the marker
377 if strings.Contains(data, marker) {
378 // Extract exit code from marker line
379 lines := strings.Split(data, "\n")
380 for _, line := range lines {
381 if strings.HasPrefix(line, marker+"_") {
382 exitCode = strings.TrimPrefix(line, marker+"_")
383 exitCode = strings.TrimSpace(exitCode)
384
385 // Add any output before the marker
386 beforeMarker := strings.Split(data, marker)[0]
387 output.WriteString(beforeMarker)
388
389 // Check exit code
390 if exitCode != "0" {
391 return output.String(), fmt.Errorf("command exited with code %s", exitCode)
392 }
393 return output.String(), nil
394 }
395 }
396 } else {
397 output.WriteString(data)
398 }
399 }
400 }
401}
402
403// Close terminates the session and cleans up resources
404func (s *DefaultChrootSession) Close() error {
405 if s.closed {
406 return nil
407 }
408 s.closed = true
409
410 // Send exit command
411 if s.stdin != nil {
412 if _, err := s.stdin.WriteString("exit\n"); err != nil {
413 fmt.Fprintf(os.Stderr, "failed to write exit to stdin: %v\n", err)
414 }
415 if err := s.stdin.Close(); err != nil {
416 fmt.Fprintf(os.Stderr, "failed to close stdin: %v\n", err)
417 }
418 }
419
420 // Wait for the command to finish with timeout
421 done := make(chan error, 1)
422 go func() {
423 done <- s.cmd.Wait()
424 }()
425
426 select {
427 case <-time.After(5 * time.Second):
428 // Force kill if it doesn't exit gracefully
429 if s.cmd.Process != nil {
430 if err := s.cmd.Process.Kill(); err != nil {
431 fmt.Fprintf(os.Stderr, "failed to kill process: %v\n", err)
432 }
433 }
434 case <-done:
435 // Command exited normally
436 }
437
438 // Close pipes and log file
439 if s.stdoutPipe != nil {
440 if err := s.stdoutPipe.Close(); err != nil {
441 fmt.Fprintf(os.Stderr, "failed to close stdout pipe: %v\n", err)
442 }
443 }
444 if s.stderrPipe != nil {
445 if err := s.stderrPipe.Close(); err != nil {
446 fmt.Fprintf(os.Stderr, "failed to close stderr pipe: %v\n", err)
447 }
448 }
449 if s.logFile != nil {
450 if err := s.logFile.Close(); err != nil {
451 fmt.Fprintf(os.Stderr, "failed to close log file: %v\n", err)
452 }
453 }
454
455 // Cancel context
456 s.cancel()
457
458 return nil
459}