Project: Systems Mini-Tools
Project: Systems Mini-Tools
Overview
Build systool, a multi-command systems utility that practices this part end-to-end: process info, signals, atomic files, pipelines, locks, and local IPC. Keep each verb small.
| Command | Skills |
|---|---|
id |
pid, uid, cwd, env peek |
watchdog |
run a child; restart on exit; SIGTERM drain |
atomic-write |
write-temp-rename |
lines |
stdin filter pipeline |
once |
flock single-instance runner |
serve-sock |
HTTP over Unix socket |
tailf |
poll file growth (simple) |
Stdlib first; use golang.org/x/sys/unix only for flock.
Scaffold
mkdir -p systool/cmd/systool systool/internal/app
cd systool
go mod init example.com/systoolDispatch pattern: same as CLI part (FlagSet per command + NotifyContext).
1. id — process context
func cmdID(stdout io.Writer) error {
wd, err := os.Getwd()
if err != nil {
return err
}
host, _ := os.Hostname()
fmt.Fprintf(stdout, "pid\t%d\n", os.Getpid())
fmt.Fprintf(stdout, "ppid\t%d\n", os.Getppid())
fmt.Fprintf(stdout, "uid\t%d\n", os.Getuid())
fmt.Fprintf(stdout, "cwd\t%s\n", wd)
fmt.Fprintf(stdout, "host\t%s\n", host)
if h, err := os.UserHomeDir(); err == nil {
fmt.Fprintf(stdout, "home\t%s\n", h)
}
return nil
}go run ./cmd/systool id2. watchdog — supervise a child
func cmdWatchdog(ctx context.Context, args []string) error {
fs := flag.NewFlagSet("watchdog", flag.ContinueOnError)
backoff := fs.Duration("backoff", time.Second, "restart delay")
if err := fs.Parse(args); err != nil {
return err
}
if fs.NArg() < 1 {
return fmt.Errorf("usage: systool watchdog [--] <cmd> [args]")
}
for {
if err := ctx.Err(); err != nil {
return err
}
cmd := exec.CommandContext(ctx, fs.Arg(0), fs.Args()[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
fmt.Fprintf(os.Stderr, "watchdog: child exited: %v\n", err)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(*backoff):
}
}
}go run ./cmd/systool watchdog -backoff 500ms -- sleep 2
# Ctrl-C should stop restartsStretch: max restarts; exponential backoff; don’t restart on exit 0.
3. atomic-write
func atomicWrite(path string, data []byte, mode os.FileMode) error {
dir := filepath.Dir(path)
f, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return err
}
tmp := f.Name()
cleanup := true
defer func() {
if cleanup {
_ = os.Remove(tmp)
}
}()
if _, err := f.Write(data); err != nil {
f.Close()
return err
}
if err := f.Chmod(mode); err != nil {
f.Close()
return err
}
if err := f.Sync(); err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Rename(tmp, path); err != nil {
return err
}
cleanup = false
// optional: fsync directory for durability
return nil
}echo hello | go run ./cmd/systool atomic-write -path /tmp/x.txt4. lines — pipeline filter
func cmdLines(r io.Reader, w io.Writer, contains string) error {
sc := bufio.NewScanner(r)
for sc.Scan() {
line := sc.Text()
if contains == "" || strings.Contains(line, contains) {
fmt.Fprintln(w, line)
}
}
return sc.Err()
}printf 'a\nb\nac\n' | go run ./cmd/systool lines -c aExit 0 always if IO OK; use grep-like exit codes only if you document them.
5. once — single instance
// acquire flock; run command; release
// exit 75 if lock busySee chapter 148 for Acquire. Wire:
go run ./cmd/systool once -lock /tmp/job.lock -- echo only-one6. serve-sock — local HTTP
func cmdServeSock(ctx context.Context, path string) error {
_ = os.Remove(path)
ln, err := net.Listen("unix", path)
if err != nil {
return err
}
_ = os.Chmod(path, 0o600)
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
srv := &http.Server{Handler: mux}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()
err = srv.Serve(ln)
if err == http.ErrServerClosed {
return nil
}
return err
}go run ./cmd/systool serve-sock -path /tmp/systool.sock
curl --unix-socket /tmp/systool.sock http://localhost/healthz7. tailf — follow file growth
func tailf(ctx context.Context, path string, w io.Writer) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// seek end
_, _ = f.Seek(0, io.SeekEnd)
buf := make([]byte, 4096)
for {
n, err := f.Read(buf)
if n > 0 {
_, _ = w.Write(buf[:n])
}
if err == io.EOF {
select {
case <-ctx.Done():
return nil
case <-time.After(200 * time.Millisecond):
}
continue
}
if err != nil {
return err
}
}
}Not a full tail -F (no rotate handling)—enough to practice polls + signals.
Acceptance checklist
idprints pid/uid/cwd
watchdogrestartsfalseuntil Ctrl-C
atomic-writeleaves no torn file if killed mid-run (manual chaos)
linesworks in a pipe withjqorwc
oncesecond instance fails while first sleeps
serve-sockanswers curl via--unix-socket
tailfprints new lines as you append to a file
Layout recap
cmd/systool/main.go # signals + exit codes
internal/app/*.go # commands
internal/lock/flock_unix.go # build-tagged
What you practiced
| Chapter | Tool |
|---|---|
| 141 | watchdog, serve-sock shutdown |
| 142 | atomic-write |
| 143 | lines |
| 144 | id |
| 145 | pipes, stdout/stderr |
| 147 | serve-sock |
| 148 | once |
Ship systool as a learning binary; promote individual commands into dedicated tools when they grow.
Next project: 156 Systems ops toolkit (deadline, reload, cronish, healthsock, …).