Project: Systems Mini-Tools

Updated

September 8, 2026

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/systool

Dispatch 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 id

2. 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 restarts

Stretch: 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.txt

4. 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 a

Exit 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 busy

See chapter 148 for Acquire. Wire:

go run ./cmd/systool once -lock /tmp/job.lock -- echo only-one

6. 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/healthz

7. 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

  • id prints pid/uid/cwd
  • watchdog restarts false until Ctrl-C
  • atomic-write leaves no torn file if killed mid-run (manual chaos)
  • lines works in a pipe with jq or wc
  • once second instance fails while first sleeps
  • serve-sock answers curl via --unix-socket
  • tailf prints 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, …).