PrintToVeraeTimeStamp/cmd/verae-print/menubar.go
George Lambert b342b9235e fix: prefer go.mod tree for packaging paths; CERT residual tests
- Avoid research checkout shadowing logo/script lookup
- Strengthen packaging/CERT tests for multihash rules
2026-10-13 21:18:23 -04:00

584 lines
18 KiB
Go

package main
// PT-W3 / PT-06 tray / menu-bar launcher.
// Ships a working presence without a multi-month native C++ tray:
// Windows → platform/windows/scripts/verae-tray.ps1 (NotifyIcon + /status poll)
// → Startup shortcut via menubar --install (or install-tray-startup.ps1)
// → Start Menu folder via build-tray-startmenu.ps1 (WiX/NSIS-free stub)
// macOS → platform/macos/menubar/run-menubar.sh (NSStatusItem via Swift)
// → durable copy under ~/Library/Application Support/Verae/menubar
// → LaunchAgent via menubar --install; optional .app via build-menubar-app.sh
// IPP POST stays pure; this only opens scripts that GET /status and /queue.
import (
"flag"
"fmt"
"github.com/VeraeLLC/PrintToVeraeTimeStamp/internal/statuslang"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
)
const (
menubarLaunchLabel = "com.verae.timestamping.menubar"
winStartupLnkName = "Verae Tray.lnk"
)
// runMenubar starts the platform tray/menu-bar helper, or prints install hints.
func runMenubar(args []string) int {
fs := flag.NewFlagSet("menubar", flag.ContinueOnError)
fs.SetOutput(os.Stderr)
addr := fs.String("addr", defaultHelperAddr(), "helper loopback address for /status and /queue")
install := fs.Bool("install", false, "install at login (Windows Startup shortcut; macOS LaunchAgent)")
uninstall := fs.Bool("uninstall", false, "remove login install (Windows Startup shortcut; macOS LaunchAgent)")
foreground := fs.Bool("foreground", false, "run attached (default: detach on Windows/macOS)")
if err := fs.Parse(args); err != nil {
return 2
}
_ = os.Setenv("VERAE_IPP_ADDR", *addr)
if *uninstall {
return menubarUninstall()
}
if *install {
return menubarInstall(*addr)
}
switch runtime.GOOS {
case "windows":
return startWindowsTray(*addr, *foreground)
case "darwin":
return startDarwinMenubar(*addr, *foreground)
default:
fmt.Fprintln(os.Stderr, "menubar: no tray script on this OS — use: verae-print tray-status --open")
fmt.Fprintln(os.Stderr, " loopback: GET http://"+*addr+"/queue and GET http://"+*addr+"/status")
return runTrayStatus([]string{"--open", "--addr", *addr})
}
}
func startWindowsTray(addr string, foreground bool) int {
script, err := locateWindowsTrayScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar: verae-tray.ps1 not found (%v)\n", err)
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
return runTrayStatus([]string{"--open", "--addr", addr})
}
ps := windowsPowerShell()
args := []string{
"-NoProfile", "-ExecutionPolicy", "Bypass", "-STA",
"-File", script,
"-Addr", addr,
}
cmd := exec.Command(ps, args...)
if foreground {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: tray exited: %v\n", err)
return 1
}
return 0
}
hideWindow(cmd)
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: start tray: %v\n", err)
return 1
}
fmt.Printf("Verae tray started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
fmt.Println("Install at login: verae-print menubar --install")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func startDarwinMenubar(addr string, foreground bool) int {
script, err := locateDarwinMenubarScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar: run-menubar.sh not found (%v)\n", err)
fmt.Fprintln(os.Stderr, "fallback: verae-print tray-status --open")
return runTrayStatus([]string{"--open", "--addr", addr})
}
_ = os.Chmod(script, 0o755)
cmd := exec.Command("/bin/bash", script)
cmd.Env = append(os.Environ(), "VERAE_IPP_ADDR="+addr)
if foreground {
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: %v\n", err)
return 1
}
return 0
}
if err := cmd.Start(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: start: %v\n", err)
return 1
}
fmt.Printf("Verae menu bar started (pid %d) · polls http://%s/status · queue http://%s/queue\n", cmd.Process.Pid, addr, addr)
fmt.Println("Install at login: verae-print menubar --install")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarInstall(addr string) int {
switch runtime.GOOS {
case "windows":
return menubarInstallWindows(addr)
case "darwin":
return menubarInstallDarwin(addr)
default:
fmt.Fprintln(os.Stderr, "menubar --install: no tray packaging on this OS")
fmt.Fprintln(os.Stderr, " use: verae-print tray-status --open")
return 2
}
}
func menubarUninstall() int {
switch runtime.GOOS {
case "windows":
return menubarUninstallWindows()
case "darwin":
return menubarUninstallDarwin()
default:
fmt.Fprintln(os.Stderr, "menubar --uninstall: no tray packaging on this OS")
return 2
}
}
// --- Windows Startup shortcut ------------------------------------------------
func menubarInstallWindows(addr string) int {
tray, err := locateWindowsTrayScript()
if err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
installer, err := locateWindowsStartupInstaller()
if err == nil {
ps := windowsPowerShell()
cmd := exec.Command(ps,
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", installer,
"-Addr", addr,
"-TrayScript", tray,
)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
return 0
}
// Fallback without install-tray-startup.ps1: create .lnk via PowerShell COM.
if err := writeWindowsStartupShortcut(tray, addr); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: %v\n", err)
return 1
}
fmt.Println("Installed Startup shortcut:", windowsStartupShortcutPath())
fmt.Println("Tray starts at next login. Run now: verae-print menubar")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarUninstallWindows() int {
installer, err := locateWindowsStartupInstaller()
if err == nil {
ps := windowsPowerShell()
cmd := exec.Command(ps,
"-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", installer,
"-Uninstall",
)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "menubar --uninstall: %v\n", err)
return 1
}
return 0
}
path := windowsStartupShortcutPath()
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println("Removed Startup shortcut:", path)
return 0
}
func windowsStartupShortcutPath() string {
appData := os.Getenv("APPDATA")
if appData == "" {
home, _ := os.UserHomeDir()
appData = filepath.Join(home, "AppData", "Roaming")
}
return filepath.Join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup", winStartupLnkName)
}
func writeWindowsStartupShortcut(trayScript, addr string) error {
ps := windowsPowerShell()
lnk := windowsStartupShortcutPath()
if err := os.MkdirAll(filepath.Dir(lnk), 0o755); err != nil {
return err
}
// Escape single quotes for PowerShell single-quoted strings.
esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
psExe := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
if _, err := os.Stat(psExe); err != nil {
psExe = "powershell.exe"
}
args := fmt.Sprintf(`-NoProfile -ExecutionPolicy Bypass -STA -WindowStyle Hidden -File "%s" -Addr %s`, trayScript, addr)
script := fmt.Sprintf(`
$ErrorActionPreference = 'Stop'
$wsh = New-Object -ComObject WScript.Shell
$sc = $wsh.CreateShortcut('%s')
$sc.TargetPath = '%s'
$sc.Arguments = '%s'
$sc.WorkingDirectory = '%s'
$sc.WindowStyle = 7
$sc.Description = 'Verae Timestamping tray — polls GET /status; hold id ≠ evidence.'
$sc.Save()
`, esc(lnk), esc(psExe), esc(args), esc(filepath.Dir(trayScript)))
cmd := exec.Command(ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%v: %s", err, strings.TrimSpace(string(out)))
}
return nil
}
func windowsPowerShell() string {
ps := filepath.Join(os.Getenv("WINDIR"), "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
if _, err := os.Stat(ps); err != nil {
return "powershell.exe"
}
return ps
}
func locateWindowsTrayScript() (string, error) {
script, err := findRepoFile("platform", "windows", "scripts", "verae-tray.ps1")
if err == nil {
return script, nil
}
// Installer layout next to the exe.
for _, alt := range []string{
filepath.Join(exeDirHint(), "scripts", "verae-tray.ps1"),
filepath.Join(exeDirHint(), "..", "scripts", "verae-tray.ps1"),
filepath.Join(exeDirHint(), "verae-tray.ps1"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
func locateWindowsStartupInstaller() (string, error) {
script, err := findRepoFile("platform", "windows", "scripts", "install-tray-startup.ps1")
if err == nil {
return script, nil
}
for _, alt := range []string{
filepath.Join(exeDirHint(), "scripts", "install-tray-startup.ps1"),
filepath.Join(exeDirHint(), "..", "scripts", "install-tray-startup.ps1"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
// --- macOS durable LaunchAgent -----------------------------------------------
func menubarSupportDir(home string) string {
return filepath.Join(home, "Library", "Application Support", "Verae", "menubar")
}
func menubarInstallDarwin(addr string) int {
srcScript, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
srcDir := filepath.Dir(srcScript)
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
support := menubarSupportDir(home)
if err := os.MkdirAll(support, 0o755); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
// Copy durable assets so LaunchAgent does not depend on the checkout path.
for _, name := range []string{"run-menubar.sh", "verae-menubar.swift", "com.verae.timestamping.menubar.plist"} {
src := filepath.Join(srcDir, name)
if !fileExists(src) {
continue
}
dst := filepath.Join(support, name)
if err := copyFile(src, dst); err != nil {
fmt.Fprintf(os.Stderr, "menubar --install: copy %s: %v\n", name, err)
return 1
}
}
script := filepath.Join(support, "run-menubar.sh")
_ = os.Chmod(script, 0o755)
// Best-effort compile so LaunchAgent does not need swift at login.
bin := filepath.Join(support, "verae-menubar")
swiftSrc := filepath.Join(support, "verae-menubar.swift")
if fileExists(swiftSrc) {
if path, lookErr := exec.LookPath("swiftc"); lookErr == nil {
c := exec.Command(path, "-O", "-o", bin, swiftSrc)
if out, err := c.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "menubar: swiftc optional compile failed (will use swift at run): %v\n%s\n", err, strings.TrimSpace(string(out)))
} else {
_ = os.Chmod(bin, 0o755)
}
}
}
logDir := filepath.Join(home, "Library", "Logs")
_ = os.MkdirAll(logDir, 0o755)
agents := filepath.Join(home, "Library", "LaunchAgents")
_ = os.MkdirAll(agents, 0o755)
plistPath := filepath.Join(agents, menubarLaunchLabel+".plist")
body := menubarPlistBody(script, logDir, addr)
// Prefer rewriting the template if present.
if tpl := filepath.Join(support, "com.verae.timestamping.menubar.plist"); fileExists(tpl) {
raw, err := os.ReadFile(tpl)
if err == nil {
s := string(raw)
s = strings.ReplaceAll(s, "MENUBAR_SCRIPT_PLACEHOLDER", script)
s = strings.ReplaceAll(s, "LOG_DIR_PLACEHOLDER", logDir)
// Ensure VERAE_IPP_ADDR matches --addr.
if strings.Contains(s, "<key>VERAE_IPP_ADDR</key>") {
// Replace the next <string>…</string> after VERAE_IPP_ADDR.
s = replaceEnvString(s, "VERAE_IPP_ADDR", addr)
}
body = s
}
}
if err := os.WriteFile(plistPath, []byte(body), 0o644); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
if err := launchctlLoadMenubar(plistPath); err != nil {
fmt.Fprintf(os.Stderr, "menubar: wrote %s but launchctl load failed: %v\n", plistPath, err)
fmt.Fprintln(os.Stderr, " load manually after login, or: verae-print menubar")
fmt.Fprintln(os.Stderr, " assets:", support)
return 1
}
fmt.Println("Installed LaunchAgent:", plistPath)
fmt.Println("Durable assets:", support)
fmt.Println("Menu bar polls http://" + addr + "/status · Open queue → /queue")
fmt.Println("Optional .app: platform/macos/menubar/build-menubar-app.sh")
fmt.Println(statuslang.EvidenceNote)
return 0
}
func menubarUninstallDarwin() int {
home, err := os.UserHomeDir()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
plistPath := filepath.Join(home, "Library", "LaunchAgents", menubarLaunchLabel+".plist")
_ = launchctlUnloadMenubar(plistPath)
if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, err)
return 1
}
// Leave Application Support assets; user may still run menubar manually.
fmt.Println("Removed LaunchAgent:", plistPath)
fmt.Println("Assets kept at:", menubarSupportDir(home), "(delete manually if desired)")
return 0
}
func menubarPlistBody(script, logDir, addr string) string {
return fmt.Sprintf(`<?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>%s</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>%s</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>ProcessType</key>
<string>Interactive</string>
<key>StandardOutPath</key>
<string>%s/verae-menubar.launchd.out.log</string>
<key>StandardErrorPath</key>
<string>%s/verae-menubar.launchd.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>VERAE_IPP_ADDR</key>
<string>%s</string>
</dict>
</dict>
</plist>
`, menubarLaunchLabel, script, logDir, logDir, addr)
}
func launchctlLoadMenubar(plistPath string) error {
uid := os.Getuid()
domain := "gui/" + strconv.Itoa(uid)
label := menubarLaunchLabel
// Modern path first (macOS 10.13+); fall back to load -w.
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
_ = exec.Command("launchctl", "unload", plistPath).Run()
if err := exec.Command("launchctl", "bootstrap", domain, plistPath).Run(); err == nil {
_ = exec.Command("launchctl", "enable", domain+"/"+label).Run()
_ = exec.Command("launchctl", "kickstart", "-k", domain+"/"+label).Run()
return nil
}
if err := exec.Command("launchctl", "load", "-w", plistPath).Run(); err != nil {
return err
}
return nil
}
func launchctlUnloadMenubar(plistPath string) error {
uid := os.Getuid()
domain := "gui/" + strconv.Itoa(uid)
label := menubarLaunchLabel
_ = exec.Command("launchctl", "bootout", domain+"/"+label).Run()
_ = exec.Command("launchctl", "unload", "-w", plistPath).Run()
return nil
}
func locateDarwinMenubarScript() (string, error) {
// Prefer durable install copy if present.
if home, err := os.UserHomeDir(); err == nil {
p := filepath.Join(menubarSupportDir(home), "run-menubar.sh")
if fileExists(p) {
return p, nil
}
}
script, err := findRepoFile("platform", "macos", "menubar", "run-menubar.sh")
if err == nil {
return script, nil
}
for _, alt := range []string{
filepath.Join(exeDirHint(), "menubar", "run-menubar.sh"),
filepath.Join(exeDirHint(), "..", "menubar", "run-menubar.sh"),
filepath.Join(exeDirHint(), "run-menubar.sh"),
} {
if fileExists(alt) {
abs, _ := filepath.Abs(alt)
return abs, nil
}
}
return "", err
}
// replaceEnvString replaces the <string> value immediately following <key>NAME</key>.
func replaceEnvString(plist, key, value string) string {
needle := "<key>" + key + "</key>"
i := strings.Index(plist, needle)
if i < 0 {
return plist
}
rest := plist[i+len(needle):]
start := strings.Index(rest, "<string>")
end := strings.Index(rest, "</string>")
if start < 0 || end < 0 || end < start {
return plist
}
return plist[:i+len(needle)] + rest[:start] + "<string>" + value + rest[end:]
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
tmp := dst + ".tmp"
out, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return err
}
_, copyErr := io.Copy(out, in)
closeErr := out.Close()
if copyErr != nil {
_ = os.Remove(tmp)
return copyErr
}
if closeErr != nil {
_ = os.Remove(tmp)
return closeErr
}
if st, err := os.Stat(src); err == nil {
_ = os.Chmod(tmp, st.Mode())
}
return os.Rename(tmp, dst)
}
func findRepoFile(parts ...string) (string, error) {
rel := filepath.Join(parts...)
// Prefer go.mod tree (repoRootHint) over bare relative/exe guesses so a
// sibling research checkout cannot shadow the Verae-Projects package tree.
candidates := []string{
filepath.Join(repoRootHint(), rel),
filepath.Join(".", rel),
filepath.Join(exeDirHint(), rel),
filepath.Join(exeDirHint(), "..", rel),
filepath.Join(exeDirHint(), "..", "..", rel),
}
// Walk up from cwd for go.mod (tests + nested cwd).
if wd, err := os.Getwd(); err == nil {
dir := wd
for i := 0; i < 8; i++ {
if fileExists(filepath.Join(dir, "go.mod")) {
candidates = append(candidates, filepath.Join(dir, rel))
break
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
}
var tried []string
seen := map[string]bool{}
for _, c := range candidates {
abs, err := filepath.Abs(c)
if err != nil {
continue
}
if seen[abs] {
continue
}
seen[abs] = true
tried = append(tried, abs)
if fileExists(abs) {
return abs, nil
}
}
return "", fmt.Errorf("not found: %s (tried %s)", rel, strings.Join(tried, ", "))
}
func exeDirHint() string {
exe, err := os.Executable()
if err != nil {
return "."
}
exe, _ = filepath.EvalSymlinks(exe)
return filepath.Dir(exe)
}