Skip to content

Commit e1d6be8

Browse files
feat: generate manpages from the CLI definitions (#105)
* chore: delete previously geenrated manpage for `schedctl status` * build(deps): add `urfave/cli` docgen tool to deps * feat: generate manpages from the CLI definitions * docs: add manpages generated by our tool * build: add a doc generation and a drift check make target * ci: check whether manpages need to be regenerated * fix(docgen): avoid gosec linting on the docgen tool, file permissions are ok
1 parent 13ef27e commit e1d6be8

13 files changed

Lines changed: 541 additions & 42 deletions

File tree

.github/workflows/go.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ jobs:
3030
version: v2.11.3
3131
args: --build-tags containers_image_openpgp
3232

33+
- name: Check generated manpages are up to date
34+
run: make man-check
35+
3336
build:
3437
name: Build and test
3538
runs-on: ubuntu-latest

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ test:
1414
lint:
1515
golangci-lint run
1616

17+
.PHONY: man man-check
18+
man:
19+
go run -tags containers_image_openpgp ./cmd/gen-man -out dist/man
20+
21+
man-check:
22+
@tmp=$$(mktemp -d); \
23+
trap 'rm -rf "$$tmp"' EXIT; \
24+
go run -tags containers_image_openpgp ./cmd/gen-man -out "$$tmp" || exit $$?; \
25+
diff -ruN dist/man "$$tmp" || { \
26+
echo; \
27+
echo 'manpages out of date — run `make man` and commit the result'; \
28+
exit 1; \
29+
}
30+
1731
clean:
1832
rm -rf schedctl
1933

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
.SH EXIT STATUS
2+
.TP
3+
.B 0
4+
A scheduler is running normally, or no scheduler is running. Both
5+
states are reported with a clear message and exit zero.
6+
.TP
7+
.B 1
8+
Generic error. The container runtime could not be queried, the
9+
.B \-\-output
10+
value is invalid, or another setup-level failure occurred.
11+
.TP
12+
.B 2
13+
Discrepancy between schedctl's view and the kernel's view. The
14+
.B status
15+
field in the report identifies which kind of discrepancy was
16+
detected (see
17+
.B STATUS VALUES
18+
below).
19+
.SH STATUS VALUES
20+
The
21+
.B status
22+
field takes one of the following stable string values. These
23+
values are part of the JSON schema and will not be renamed without
24+
a schema version bump.
25+
.TP
26+
.B idle
27+
No managed scheduler and the kernel reports no attached ops. Exit 0.
28+
.TP
29+
.B running
30+
A managed scheduler is running and the kernel reports a matching
31+
attached ops. Exit 0.
32+
.TP
33+
.B orphaned-kernel-state
34+
The kernel reports a sched_ext ops attached, but schedctl is not
35+
managing any scheduler. Indicates a scheduler started outside
36+
schedctl, or schedctl-managed state was lost. Exit 2.
37+
.TP
38+
.B managed-detached
39+
schedctl is managing a scheduler container but the kernel reports
40+
no ops attached. Likely the container is running but has not
41+
attached its BPF program, or it crashed without cleaning up. Exit 2.
42+
.TP
43+
.B managed-mismatch
44+
schedctl is managing a scheduler container, the kernel reports
45+
a different ops attached than the one schedctl expected. Exit 2.
46+
.TP
47+
.B multiple-managed
48+
schedctl is managing more than one scheduler container. Only one
49+
sched_ext scheduler can be attached at a time. Exit 2.
50+
.SH JSON OUTPUT
51+
.B \-\-output json
52+
emits a single JSON document on stdout. The schema is versioned
53+
through the
54+
.B schema_version
55+
field; consumers should reject documents whose
56+
.B schema_version
57+
does not match a version they understand.
58+
.PP
59+
.B Top-level fields:
60+
.RS
61+
.TP
62+
.B schema_version
63+
String. The current value is
64+
.BR "1" .
65+
Bumped on any breaking change.
66+
.TP
67+
.B status
68+
String. One of the values listed under
69+
.BR "STATUS VALUES" .
70+
.TP
71+
.B driver
72+
String. The container runtime that was queried
73+
.RB ( podman
74+
or
75+
.BR containerd ).
76+
.TP
77+
.B scheduler
78+
Object. Present iff schedctl is managing exactly one scheduler. See
79+
.B "Scheduler object"
80+
below. Omitted otherwise.
81+
.TP
82+
.B kernel
83+
Object. Always present. See
84+
.B "Kernel object"
85+
below.
86+
.TP
87+
.B discrepancy
88+
String. Present iff
89+
.B status
90+
indicates a discrepancy. Carries a human-readable description.
91+
Omitted when
92+
.B status
93+
is
94+
.B idle
95+
or
96+
.BR running .
97+
.RE
98+
.PP
99+
.B Scheduler object:
100+
.RS
101+
.TP
102+
.B name
103+
String. The container/scheduler name as schedctl knows it
104+
(e.g.
105+
.IR scx_rusty ).
106+
.TP
107+
.B container_id
108+
String. The container ID assigned by the runtime.
109+
.TP
110+
.B image
111+
String. Image reference the container was started from.
112+
.TP
113+
.B image_id
114+
String. Image digest. Omitted when the runtime did not surface one.
115+
.TP
116+
.B pid
117+
Number. Host PID of the container's primary process.
118+
.TP
119+
.B started_at
120+
String, RFC 3339 timestamp in UTC.
121+
.RE
122+
.PP
123+
.B Kernel object:
124+
.RS
125+
.TP
126+
.B supported
127+
Boolean.
128+
.B true
129+
iff
130+
.I /sys/kernel/sched_ext
131+
exists.
132+
.TP
133+
.B enabled
134+
Boolean.
135+
.B true
136+
iff
137+
.I /sys/kernel/sched_ext/state
138+
reads as
139+
.IR enabled .
140+
.TP
141+
.B ops
142+
String. Contents of
143+
.IR /sys/kernel/sched_ext/root/ops ,
144+
or the empty string when no ops are attached.
145+
.RE
146+
.SH EXAMPLES
147+
.PP
148+
Human-readable status:
149+
.PP
150+
.RS
151+
.nf
152+
$ schedctl status
153+
scheduler running: scx_rusty
154+
driver: podman
155+
container: 3a7f...
156+
image: ghcr.io/sched-ext/scx_rusty:latest
157+
digest: sha256:abcd...
158+
pid: 4242
159+
started: 2026-05-02T10:30:00Z
160+
kernel: enabled, ops=rusty
161+
.fi
162+
.RE
163+
.PP
164+
JSON output for scripts:
165+
.PP
166+
.RS
167+
.nf
168+
$ schedctl status --output json | jq -r .status
169+
running
170+
.fi
171+
.RE
172+
.SH SEE ALSO
173+
.BR schedctl (1),
174+
.BR schedctl-run (1),
175+
.BR schedctl-stop (1)

cmd/gen-man/main.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"runtime"
9+
"strings"
10+
11+
docs "github.com/urfave/cli-docs/v3"
12+
"github.com/urfave/cli/v3"
13+
14+
cmd "schedctl/cmd/schedctl"
15+
)
16+
17+
func main() {
18+
out := flag.String("out", "dist/man", "output directory")
19+
flag.Parse()
20+
21+
if err := os.MkdirAll(*out, 0o755); err != nil {
22+
fail(err)
23+
}
24+
25+
extrasDir := extrasDir()
26+
27+
root := cmd.NewRootCmd()
28+
29+
if err := writeMan(root, "schedctl", filepath.Join(*out, "schedctl.1"), extrasDir); err != nil {
30+
fail(err)
31+
}
32+
33+
persistent := persistentFlags(root)
34+
35+
for _, sub := range root.Commands {
36+
name := "schedctl-" + sub.Name
37+
sub.Name = name
38+
sub.UsageText = "schedctl " + strings.TrimPrefix(name, "schedctl-")
39+
if sub.ArgsUsage != "" {
40+
sub.UsageText += " " + sub.ArgsUsage
41+
}
42+
sub.Flags = append(sub.Flags, persistent...)
43+
if err := writeMan(sub, name, filepath.Join(*out, name+".1"), extrasDir); err != nil {
44+
fail(err)
45+
}
46+
}
47+
}
48+
49+
func writeMan(c *cli.Command, page, path, extrasDir string) error {
50+
body, err := docs.ToManWithSection(c, 1)
51+
if err != nil {
52+
return fmt.Errorf("render %s: %w", page, err)
53+
}
54+
extras, err := readExtras(extrasDir, page)
55+
if err != nil {
56+
return err
57+
}
58+
if extras != "" {
59+
if !strings.HasSuffix(body, "\n") {
60+
body += "\n"
61+
}
62+
body += extras
63+
}
64+
return os.WriteFile(path, []byte(body), 0o644) //nolint:gosec
65+
}
66+
67+
func readExtras(dir, page string) (string, error) {
68+
path := filepath.Join(dir, page+".troff")
69+
data, err := os.ReadFile(path)
70+
if err != nil {
71+
if os.IsNotExist(err) {
72+
return "", nil
73+
}
74+
return "", fmt.Errorf("read extras %s: %w", path, err)
75+
}
76+
return string(data), nil
77+
}
78+
79+
func persistentFlags(c *cli.Command) []cli.Flag {
80+
var out []cli.Flag
81+
for _, f := range c.Flags {
82+
if lf, ok := f.(interface{ IsLocal() bool }); ok && lf.IsLocal() {
83+
continue
84+
}
85+
out = append(out, f)
86+
}
87+
return out
88+
}
89+
90+
func extrasDir() string {
91+
_, file, _, ok := runtime.Caller(0)
92+
if !ok {
93+
return "extras"
94+
}
95+
return filepath.Join(filepath.Dir(file), "extras")
96+
}
97+
98+
func fail(err error) {
99+
fmt.Fprintln(os.Stderr, "gen-man:", err)
100+
os.Exit(1)
101+
}

dist/man/schedctl-doctor.1

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.nh
2+
.TH schedctl-doctor 1
3+
4+
.SH NAME
5+
schedctl-doctor \- check host readiness for sched_ext schedulers
6+
7+
8+
.SH SYNOPSIS
9+
schedctl-doctor
10+
11+
.EX
12+
[--driver|-d]=[value]
13+
[--output|-o]=[value]
14+
.EE
15+
16+
17+
.SH DESCRIPTION
18+
Run sanity checks against the host (kernel, capabilities, container runtime). Exits non-zero when any blocking check fails.
19+
20+
.PP
21+
\fBUsage\fP:
22+
23+
.EX
24+
schedctl doctor
25+
.EE
26+
27+
28+
.SH GLOBAL OPTIONS
29+
\fB--driver, -d\fP="": container runtime to use: containerd, podman (default: "podman")
30+
31+
.PP
32+
\fB--output, -o\fP="": output format: text, json (default: "text")

dist/man/schedctl-list.1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.nh
2+
.TH schedctl-list 1
3+
4+
.SH NAME
5+
schedctl-list \- list available schedulers
6+
7+
8+
.SH SYNOPSIS
9+
schedctl-list
10+
11+
.EX
12+
[--driver|-d]=[value]
13+
.EE
14+
15+
.PP
16+
\fBUsage\fP:
17+
18+
.EX
19+
schedctl list
20+
.EE
21+
22+
23+
.SH GLOBAL OPTIONS
24+
\fB--driver, -d\fP="": container runtime to use: containerd, podman (default: "podman")

dist/man/schedctl-ps.1

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.nh
2+
.TH schedctl-ps 1
3+
4+
.SH NAME
5+
schedctl-ps \- list running schedulers
6+
7+
8+
.SH SYNOPSIS
9+
schedctl-ps
10+
11+
.EX
12+
[--driver|-d]=[value]
13+
.EE
14+
15+
.PP
16+
\fBUsage\fP:
17+
18+
.EX
19+
schedctl ps
20+
.EE
21+
22+
23+
.SH GLOBAL OPTIONS
24+
\fB--driver, -d\fP="": container runtime to use: containerd, podman (default: "podman")

0 commit comments

Comments
 (0)