summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/upload-client.md97
-rw-r--r--internal/cli/upload_client_darwin_script_test.go101
-rwxr-xr-xscripts/goprecords-upload-client-darwin.fish95
3 files changed, 292 insertions, 1 deletions
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
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>org.buetow.goprecords-upload</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/opt/homebrew/bin/fish</string>
+ <string>/Users/paul/.local/bin/goprecords-upload-client-darwin.fish</string>
+ </array>
+ <key>EnvironmentVariables</key>
+ <dict>
+ <key>GOPRECORDS_HOST</key>
+ <string>mymac</string>
+ <key>PATH</key>
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
+ </dict>
+ <key>StartInterval</key>
+ <integer>3600</integer>
+ <key>StandardOutPath</key>
+ <string>/tmp/goprecords-upload.log</string>
+ <key>StandardErrorPath</key>
+ <string>/tmp/goprecords-upload.err</string>
+ <key>RunAtLoad</key>
+ <true/>
+</dict>
+</plist>
+```
+
+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"