detached 6af0a61

chroot.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close