From 59fc6374d16c22408f50478d82fba19feab14665 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 2 May 2026 17:39:03 +0300 Subject: Add Darwin goprecords upload client for task i --- docs/upload-client.md | 97 +++++++++++++++++++++- internal/cli/upload_client_darwin_script_test.go | 101 +++++++++++++++++++++++ scripts/goprecords-upload-client-darwin.fish | 95 +++++++++++++++++++++ 3 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 internal/cli/upload_client_darwin_script_test.go create mode 100755 scripts/goprecords-upload-client-darwin.fish diff --git a/docs/upload-client.md b/docs/upload-client.md index 943d09d..dff3abf 100644 --- a/docs/upload-client.md +++ b/docs/upload-client.md @@ -4,7 +4,12 @@ uptimed record files to a running `goprecords` daemon. It works on FreeBSD, Linux, and OpenBSD, and runs as root or as a regular user. -A copy is also kept in `contrib/` for backward compatibility. +`scripts/goprecords-upload-client-darwin.fish` is a fish-shell variant for +macOS (Darwin). It handles both Intel (`/usr/local/var/uptimed`) and Apple +Silicon (`/opt/homebrew/var/uptimed`) Homebrew prefixes and uses macOS-native +commands (`sw_vers`, `sysctl`) for OS and CPU identification. + +A copy of the POSIX script is also kept in `contrib/` for backward compatibility. ## Prerequisites @@ -16,6 +21,8 @@ running before setting up uploads. | FreeBSD | `pkg install curl uptimed` | | Rocky Linux / Fedora | `sudo dnf install curl uptimed` | | OpenBSD | `pkg_add curl uptimed` | +| macOS (Intel) | `brew install curl uptimed` | +| macOS (Apple Silicon) | `brew install curl uptimed` | ## Token path @@ -81,6 +88,21 @@ install -m 700 scripts/goprecords-upload-client.sh \ ~/.local/bin/goprecords-upload-client.sh ``` +**macOS (user session)** + +The Darwin variant is a fish script and runs as a regular user (no root needed +for Homebrew-installed `uptimed`): + +```sh +install -m 700 scripts/goprecords-upload-client-darwin.fish \ + ~/.local/bin/goprecords-upload-client-darwin.fish +``` + +Ensure `fish` is on `PATH` (it is when installed via Homebrew). The script +requires `curl`, `uptimed`, and `uprecords` — all provided by `brew install uptimed`. +Set `GOPRECORDS_RECORDS_FILE` only when your `uptimed` records file is outside +the standard Homebrew paths. + ## Step 3 — Store the token **FreeBSD / Linux (root)** @@ -113,6 +135,17 @@ umask 077 echo 'TOKEN' > ~/.config/goprecords-upload-earth/token ``` +**macOS (user session)** + +Replace `mymac` with the `GOPRECORDS_HOST` value you chose: + +```sh +mkdir -p ~/.config/goprecords-upload-mymac +chmod 700 ~/.config/goprecords-upload-mymac +echo 'TOKEN' > ~/.config/goprecords-upload-mymac/token +chmod 600 ~/.config/goprecords-upload-mymac/token +``` + ## Step 4 — Automate ### FreeBSD — hourly cron @@ -221,6 +254,62 @@ GOPRECORDS_HOST=blowfish /usr/local/bin/goprecords-upload-client.sh Adjust `GOPRECORDS_HOST` for each OpenBSD host (`fishfinger`, etc.). +### macOS — hourly LaunchAgent + +Create `~/Library/LaunchAgents/org.buetow.goprecords-upload.plist`. +Replace `mymac` with your `GOPRECORDS_HOST` value and adjust the path to +`fish` if your Homebrew prefix differs: + +```xml + + + + + Label + org.buetow.goprecords-upload + ProgramArguments + + /opt/homebrew/bin/fish + /Users/paul/.local/bin/goprecords-upload-client-darwin.fish + + EnvironmentVariables + + GOPRECORDS_HOST + mymac + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + + StartInterval + 3600 + StandardOutPath + /tmp/goprecords-upload.log + StandardErrorPath + /tmp/goprecords-upload.err + RunAtLoad + + + +``` + +Load it with: + +```sh +launchctl load ~/Library/LaunchAgents/org.buetow.goprecords-upload.plist +``` + +For Intel Macs, replace `/opt/homebrew/bin/fish` with `/usr/local/bin/fish`. + +### macOS — fish shell supersync integration + +If you use the `supersync` fish function from the dotfiles, you can call the +upload script directly from your shell session by adding a call to your +`supersync` function or running it manually: + +```fish +GOPRECORDS_HOST=mymac fish ~/.local/bin/goprecords-upload-client-darwin.fish +``` + ## Step 5 — Test one run **FreeBSD** @@ -246,3 +335,9 @@ doas env GOPRECORDS_HOST=blowfish /usr/local/bin/goprecords-upload-client.sh ```sh GOPRECORDS_HOST=earth ~/.local/bin/goprecords-upload-client.sh ``` + +**macOS (user session)** + +```sh +GOPRECORDS_HOST=mymac fish ~/.local/bin/goprecords-upload-client-darwin.fish +``` diff --git a/internal/cli/upload_client_darwin_script_test.go b/internal/cli/upload_client_darwin_script_test.go new file mode 100644 index 0000000..6505b23 --- /dev/null +++ b/internal/cli/upload_client_darwin_script_test.go @@ -0,0 +1,101 @@ +package cli + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestDarwinFishUploadClient(t *testing.T) { + fish, err := exec.LookPath("fish") + if err != nil { + t.Skip("fish not installed") + } + + dir := t.TempDir() + binDir := filepath.Join(dir, "bin") + bodyDir := filepath.Join(dir, "bodies") + configDir := filepath.Join(dir, "config") + tokenDir := filepath.Join(configDir, "goprecords-upload-mymac") + for _, path := range []string{binDir, bodyDir, tokenDir} { + if err := os.MkdirAll(path, 0o755); err != nil { + t.Fatal(err) + } + } + + recordsPath := filepath.Join(dir, "records") + if err := os.WriteFile(recordsPath, []byte("records-body\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(tokenDir, "token"), []byte("secret-token\n"), 0o600); err != nil { + t.Fatal(err) + } + + writeExecutable(t, filepath.Join(binDir, "curl"), `#!/bin/sh +for arg do + if [ "$prev" = "--data-binary" ]; then data=${arg#@}; fi + prev=$arg + url=$arg +done +kind=${url##*/} +printf '%s\n' "$*" >> "$GOPRECORDS_TEST_LOG" +cp "$data" "$GOPRECORDS_TEST_BODY_DIR/$kind" +`) + writeExecutable(t, filepath.Join(binDir, "uprecords"), `#!/bin/sh +if [ "$1" = "-a" ] && [ "$2" = "-m" ]; then + printf 'all records\n' +else + printf '%s\n' '-> current boot' +fi +`) + writeExecutable(t, filepath.Join(binDir, "sw_vers"), `#!/bin/sh +printf 'ProductName: macOS\nProductVersion: 14.0\n' +`) + writeExecutable(t, filepath.Join(binDir, "sysctl"), `#!/bin/sh +printf 'hw.model: MacBookPro\nhw.ncpu: 8\nhw.machine: arm64\n' +`) + + logPath := filepath.Join(dir, "curl.log") + cmd := exec.Command(fish, "goprecords-upload-client-darwin.fish") + cmd.Dir = filepath.Join("..", "..", "scripts") + cmd.Env = append(os.Environ(), + "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"), + "HOME="+dir, + "XDG_CONFIG_HOME="+configDir, + "GOPRECORDS_HOST=mymac", + "GOPRECORDS_BASE_URL=https://example.test", + "GOPRECORDS_RECORDS_FILE="+recordsPath, + "GOPRECORDS_TEST_LOG="+logPath, + "GOPRECORDS_TEST_BODY_DIR="+bodyDir, + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("fish client failed: %v\n%s", err, out) + } + + logData, err := os.ReadFile(logPath) + if err != nil { + t.Fatal(err) + } + logText := string(logData) + if got := strings.Count(logText, "Authorization: Bearer secret-token"); got != 5 { + t.Fatalf("Authorization header count = %d, want 5\n%s", got, logText) + } + for _, kind := range []string{"records", "txt", "cur.txt", "os.txt", "cpuinfo.txt"} { + if !strings.Contains(logText, "https://example.test/upload/mymac/"+kind) { + t.Fatalf("missing upload for %s\n%s", kind, logText) + } + if _, err := os.Stat(filepath.Join(bodyDir, kind)); err != nil { + t.Fatalf("missing body for %s: %v", kind, err) + } + } +} + +func writeExecutable(t *testing.T, path, content string) { + t.Helper() + if err := os.WriteFile(path, []byte(content), 0o755); err != nil { + t.Fatal(err) + } +} diff --git a/scripts/goprecords-upload-client-darwin.fish b/scripts/goprecords-upload-client-darwin.fish new file mode 100755 index 0000000..c64d273 --- /dev/null +++ b/scripts/goprecords-upload-client-darwin.fish @@ -0,0 +1,95 @@ +#!/usr/bin/env fish + +if test -z "$GOPRECORDS_HOST" + echo "goprecords-upload-client-darwin: GOPRECORDS_HOST is not set" >&2 + exit 1 +end + +set -g _gpr_base_url $GOPRECORDS_BASE_URL +if test -z "$_gpr_base_url" + set -g _gpr_base_url "https://goprecords.f3s.buetow.org" +end + +set -l config_dir $XDG_CONFIG_HOME +if test -z "$config_dir" + set config_dir "$HOME/.config" +end +set -g _gpr_token_file $GOPRECORDS_TOKEN_FILE +if test -z "$_gpr_token_file" + set -g _gpr_token_file "$config_dir/goprecords-upload-$GOPRECORDS_HOST/token" +end + +if not test -r "$_gpr_token_file" + echo "goprecords-upload-client-darwin: cannot read $_gpr_token_file" >&2 + exit 1 +end + +set -g _gpr_token (string trim (cat "$_gpr_token_file")) + +function _upload + set -l kind $argv[1] + set -l file $argv[2] + if not test -f "$file" + echo "goprecords-upload-client-darwin: skip $kind (no $file)" >&2 + return 0 + end + curl -fsS -X PUT --data-binary "@$file" \ + -H "Authorization: Bearer $_gpr_token" \ + "$_gpr_base_url/upload/$GOPRECORDS_HOST/$kind" +end + +function _upload_required + set -l kind $argv[1] + set -l file $argv[2] + _upload "$kind" "$file"; or exit 1 +end + +function _find_records + if test -n "$GOPRECORDS_RECORDS_FILE" + if test -f "$GOPRECORDS_RECORDS_FILE" + echo "$GOPRECORDS_RECORDS_FILE" + return 0 + end + echo "goprecords-upload-client-darwin: no uptimed records file at GOPRECORDS_RECORDS_FILE=$GOPRECORDS_RECORDS_FILE" >&2 + exit 1 + end + for p in /usr/local/var/uptimed/records /opt/homebrew/var/uptimed/records + if test -f "$p" + echo $p + return 0 + end + end + echo "goprecords-upload-client-darwin: no uptimed records file found" >&2 + exit 1 +end + +set -l records_path (_find_records) +set -g _gpr_tmp (mktemp /tmp/goprecords-darwin.XXXXXX) + +function _cleanup_tmp --on-event fish_exit + if test -n "$_gpr_tmp" + rm -f "$_gpr_tmp" + end +end + +_upload_required records "$records_path" + +if command -v uprecords >/dev/null 2>&1 + uprecords -a -m 100 >"$_gpr_tmp" + _upload_required txt "$_gpr_tmp" + + uprecords -a | grep '^->' >"$_gpr_tmp" 2>/dev/null; or true + if test -s "$_gpr_tmp" + _upload_required cur.txt "$_gpr_tmp" + end +end + +if command -v sw_vers >/dev/null 2>&1 + sw_vers >"$_gpr_tmp" +else + uname -a >"$_gpr_tmp" +end +_upload_required os.txt "$_gpr_tmp" + +sysctl hw.model hw.ncpu hw.machine >"$_gpr_tmp" 2>/dev/null; or uname -a >"$_gpr_tmp" +_upload_required cpuinfo.txt "$_gpr_tmp" -- cgit v1.2.3