refactor/kvconf 3421f16

main.go RawHistoryFolder

1// Command neferafk is the NeferAFK absence daemon and its private tools.
2package main
3
4import (
5	"context"
6	"errors"
7	"flag"
8	"fmt"
9	"io"
10	"io/fs"
11	"os"
12	"os/signal"
13	"syscall"
14
15	"github.com/bnema/neferafk/internal/adapters/auth"
16	"github.com/bnema/neferafk/internal/adapters/config"
17	"github.com/bnema/neferafk/internal/adapters/control"
18	"github.com/bnema/neferafk/internal/adapters/visual"
19	"github.com/bnema/neferafk/internal/app"
20	"github.com/bnema/zerowrap"
21)
22
23const (
24	exitOK          = 0
25	exitFailure     = 1
26	exitUsage       = 2
27	exitUnsupported = 3
28)
29
30// version is set at build time with -ldflags "-X main.version=...".
31var version = "dev"
32
33const usage = `usage: neferafk <command> [flags]
34
35commands:
36  run [--config FILE]   run the absence daemon
37  lock                  ask the running daemon to lock now
38  status                print the running daemon's state
39  validate-config FILE  check a configuration file and exit
40  version               print the version
41  visual                internal: fade/lock process (fd 3 commands in, fd 4 events out)
42`
43
44func main() { os.Exit(execute(os.Args[1:], os.Stdout, os.Stderr)) }
45
46func execute(args []string, stdout, stderr io.Writer) int {
47	if len(args) == 0 {
48		fmt.Fprint(stderr, usage)
49		return exitUsage
50	}
51	cmd, rest := args[0], args[1:]
52	switch cmd {
53	case "run":
54		return runDaemon(rest, stderr)
55	case "lock", "status":
56		return runControl(cmd, rest, stdout, stderr)
57	case "visual":
58		return runVisual(rest, os.NewFile(3, "visual-cmd"), os.NewFile(4, "visual-events"), stderr)
59	case "auth-worker": // hidden: spawned by the visual process only
60		return runAuthWorker(rest, os.NewFile(3, "auth-in"), os.NewFile(4, "auth-out"), stderr)
61	case "version":
62		if len(rest) != 0 {
63			fmt.Fprint(stderr, usage)
64			return exitUsage
65		}
66		fmt.Fprintln(stdout, version)
67		return exitOK
68	case "validate-config":
69		if len(rest) != 1 {
70			fmt.Fprint(stderr, usage)
71			return exitUsage
72		}
73		if _, err := config.Load(rest[0]); err != nil {
74			if errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrPermission) {
75				fmt.Fprintf(stderr, "neferafk: %v\n", err) // already names the file
76			} else {
77				fmt.Fprintf(stderr, "neferafk: %s: %v\n", rest[0], err)
78			}
79			return exitFailure
80		}
81		return exitOK
82	case "-h", "--help", "help":
83		fmt.Fprint(stdout, usage)
84		return exitOK
85	}
86	fmt.Fprintf(stderr, "neferafk: unknown command %q\n%s", cmd, usage)
87	return exitUsage
88}
89
90func runDaemon(args []string, stderr io.Writer) int {
91	fs := flag.NewFlagSet("run", flag.ContinueOnError)
92	fs.SetOutput(stderr)
93	cfg := fs.String("config", "", "configuration file (default $XDG_CONFIG_HOME/neferafk/config)")
94	if err := fs.Parse(args); err != nil {
95		return exitUsage
96	}
97	if fs.NArg() != 0 {
98		fmt.Fprintln(stderr, "neferafk run: unexpected arguments")
99		return exitUsage
100	}
101	exe, err := os.Executable()
102	if err != nil {
103		fmt.Fprintf(stderr, "neferafk: %v\n", err)
104		return exitFailure
105	}
106	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
107	defer stop()
108	// Compositors usually start the daemon with stderr discarded, so it also
109	// logs to $XDG_STATE_HOME/neferafk/logs/daemon.log (never secrets).
110	log, closeLog, err := zerowrap.NewWithFile(zerowrap.Config{Output: stderr},
111		zerowrap.FileConfig{Enabled: true, AppName: "neferafk", Name: "daemon", MaxSize: 5, MaxBackups: 1})
112	if err != nil {
113		log = zerowrap.New(zerowrap.Config{Output: stderr})
114		closeLog = func() {}
115		log.Warn().Str("component", "main").Err(err).Msg("file logging disabled")
116	}
117	defer closeLog()
118	err = app.RunDaemon(ctx, app.DaemonOptions{ConfigPath: *cfg, Executable: exe, Log: log})
119	var unsup *app.UnsupportedError
120	switch {
121	case err == nil:
122		return exitOK
123	case errors.As(err, &unsup):
124		log.Error().Str("component", "main").Strs("missing", unsup.Missing).Msg("unsupported environment")
125		fmt.Fprintf(stderr, "neferafk: %v\n", unsup)
126		return exitUnsupported
127	default:
128		log.Error().Str("component", "main").Err(err).Msg("daemon failed")
129		fmt.Fprintf(stderr, "neferafk: %v\n", err)
130		return exitFailure
131	}
132}
133
134func runControl(cmd string, args []string, stdout, stderr io.Writer) int {
135	if len(args) != 0 {
136		fmt.Fprintf(stderr, "neferafk %s: unexpected arguments\n", cmd)
137		return exitUsage
138	}
139	path, err := control.DefaultPath()
140	if err != nil {
141		fmt.Fprintf(stderr, "neferafk: %v\n", err)
142		return exitFailure
143	}
144	reply, err := control.Do(context.Background(), path, cmd)
145	if err != nil {
146		fmt.Fprintf(stderr, "neferafk %s: %v\n", cmd, err)
147		return exitFailure
148	}
149	fmt.Fprintln(stdout, reply)
150	return exitOK
151}
152
153// Descriptors the daemon hands down by exec ExtraFiles: fd 3 in, fd 4 out.
154// Go clears close-on-exec on them in the child, so anything we spawn later
155// (`pass`, gpg) would inherit the secret input pipe and the result output pipe
156// and could read secrets or forge results. Mark them before doing anything.
157const (
158	inheritedIn  = 3
159	inheritedOut = 4
160)
161
162func markInheritedCloexec() {
163	syscall.CloseOnExec(inheritedIn)
164	syscall.CloseOnExec(inheritedOut)
165}
166
167// runAuthWorker serves one lock's authentication on the private pipes handed
168// down by the visual process (fd 3: bootstrap + frames in, fd 4: frames out).
169// Nothing typed ever passes through the policy daemon.
170func runAuthWorker(args []string, in, out *os.File, stderr io.Writer) int {
171	if len(args) != 0 || in == nil || out == nil {
172		fmt.Fprintln(stderr, "neferafk auth-worker: internal command")
173		return exitUsage
174	}
175	markInheritedCloexec() // before anything is read or spawned
176	if err := auth.HardenProcess(); err != nil {
177		fmt.Fprintln(stderr, "neferafk auth-worker: hardening failed")
178		return exitFailure
179	}
180	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
181	defer stop()
182	return serveAuth(ctx, in, out, stderr)
183}
184
185// serveAuth reads the bootstrap record and runs the worker loop.
186func serveAuth(ctx context.Context, in io.Reader, out io.Writer, stderr io.Writer) int {
187	bootstrap, err := auth.ReadBootstrap(in)
188	if err != nil {
189		fmt.Fprintln(stderr, "neferafk auth-worker: invalid bootstrap")
190		return exitFailure
191	}
192	if err := auth.Run(ctx, in, out, bootstrap); err != nil {
193		fmt.Fprintln(stderr, "neferafk auth-worker: session ended without success")
194		return exitFailure
195	}
196	return exitOK
197}
198
199// runVisual serves the fade/lock process spawned by the daemon: commands on fd
200// 3, lifecycle events on fd 4. An active lock outlives the daemon and SIGTERM;
201// it ends only after a successful unlock.
202func runVisual(args []string, in, out *os.File, stderr io.Writer) int {
203	if len(args) != 0 || in == nil || out == nil {
204		fmt.Fprintln(stderr, "neferafk visual: internal command")
205		return exitUsage
206	}
207	markInheritedCloexec() // the auth worker is spawned with its own pipes only
208	// The visual process holds the typed secret and the PIN bootstrap: no core
209	// dumps and no ptrace/proc-mem access from same-uid processes.
210	if err := auth.HardenProcess(); err != nil {
211		fmt.Fprintln(stderr, "neferafk visual: hardening failed")
212		return exitFailure
213	}
214	exe, err := visual.ExecutablePath()
215	if err != nil {
216		fmt.Fprintf(stderr, "neferafk visual: %v\n", err)
217		return exitFailure
218	}
219	ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
220	defer stop()
221	log := zerowrap.New(zerowrap.Config{Output: stderr})
222	if err := visual.Run(ctx, visual.Config{Commands: in, Events: out, Executable: exe, Log: log}); err != nil {
223		fmt.Fprintf(stderr, "neferafk visual: %v\n", err)
224		return exitFailure
225	}
226	return exitOK
227}

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 2

Tags 9

↑↓ moveenter openesc close