bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// macOS drivers for computer:*.
|
||||
//
|
||||
// Inventory: `system_profiler SPApplicationsDataType -json` — one fixed call,
|
||||
// the same list the "About This Mac → Applications" report shows, with name,
|
||||
// version, path and the signing identity (the publisher). Shelling out once
|
||||
// beats reading a few hundred Info.plist files, half of them binary plists.
|
||||
//
|
||||
// Windows: one osascript against System Events listing every window of every
|
||||
// foreground process with its pid and whether the process is frontmost. Two
|
||||
// permissions can refuse it, and macOS names them by error code on stderr:
|
||||
// -1743 is AUTOMATION (privatae-bastion may not send Apple Events to System
|
||||
// Events), -25211 / "assistive access" is ACCESSIBILITY. The report names the
|
||||
// pane that fixes the code seen, in the user's terms.
|
||||
|
||||
const (
|
||||
computerObserveSupported = true
|
||||
computerInventoryKind = "system_profiler"
|
||||
computerWindowsKind = "osascript+system-events"
|
||||
)
|
||||
|
||||
func listInstalledApps() ([]computerApp, error) {
|
||||
out, err := runFixed("system_profiler", "SPApplicationsDataType", "-json")
|
||||
if err != nil {
|
||||
return nil, errors.New("system_profiler failed: " + execFailure(err))
|
||||
}
|
||||
return parseSystemProfiler(out)
|
||||
}
|
||||
|
||||
// parseSystemProfiler reads the JSON report. Apps under /System are Apple's
|
||||
// own and skipped: the map is what the user installed and can be asked to
|
||||
// open, not the OS. Signing identity "Developer ID Application: X (TEAM)"
|
||||
// becomes publisher X.
|
||||
func parseSystemProfiler(raw []byte) ([]computerApp, error) {
|
||||
var doc struct {
|
||||
Apps []struct {
|
||||
Name string `json:"_name"`
|
||||
Version string `json:"version"`
|
||||
Path string `json:"path"`
|
||||
Obtained string `json:"obtained_from"`
|
||||
SignedBy []string `json:"signed_by"`
|
||||
} `json:"SPApplicationsDataType"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
||||
return nil, errors.New("system_profiler output unreadable: " + err.Error())
|
||||
}
|
||||
var apps []computerApp
|
||||
for _, a := range doc.Apps {
|
||||
if strings.HasPrefix(a.Path, "/System/") || a.Obtained == "apple" {
|
||||
continue
|
||||
}
|
||||
apps = append(apps, computerApp{Name: a.Name, Version: a.Version, Path: a.Path,
|
||||
Publisher: publisherFromSigner(a.SignedBy), Source: "applications"})
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func publisherFromSigner(signed []string) string {
|
||||
for _, s := range signed {
|
||||
s = strings.TrimSpace(s)
|
||||
if i := strings.Index(s, ":"); i >= 0 {
|
||||
s = strings.TrimSpace(s[i+1:])
|
||||
}
|
||||
if i := strings.LastIndex(s, " ("); i > 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
if s != "" && !strings.HasPrefix(s, "Apple") {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// One script, one process spawn. Columns are tab-separated per line:
|
||||
// pid \t process name \t frontmost \t window title. An untitled window's
|
||||
// name is `missing value`, which cannot be concatenated — it becomes "".
|
||||
const windowsScript = `tell application "System Events"
|
||||
set out to ""
|
||||
repeat with p in (every process whose background only is false)
|
||||
set pn to name of p
|
||||
set fm to frontmost of p
|
||||
set pi to unix id of p
|
||||
repeat with w in (every window of p)
|
||||
set wn to name of w
|
||||
if wn is missing value then set wn to ""
|
||||
set out to out & pi & tab & pn & tab & fm & tab & wn & linefeed
|
||||
end repeat
|
||||
end repeat
|
||||
return out
|
||||
end tell`
|
||||
|
||||
func listWindows() ([]computerWindow, error) {
|
||||
out, err := runFixed("osascript", "-e", windowsScript)
|
||||
if err != nil {
|
||||
return nil, errors.New(osascriptReason(execFailure(err)))
|
||||
}
|
||||
return parseSystemEventsWindows(string(out)), nil
|
||||
}
|
||||
|
||||
// execFailure is the text a failed exec.Cmd.Output() actually left behind:
|
||||
// its stderr, when there is one, else the exit status. Output() folds stderr
|
||||
// into *exec.ExitError.Stderr and (*ExitError).Error() says only "exit status
|
||||
// 1" — reading err.Error() alone loses the reason.
|
||||
func execFailure(err error) string {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && len(ee.Stderr) > 0 {
|
||||
return strings.TrimSpace(string(ee.Stderr))
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// osascriptReason turns the stderr macOS wrote into the pane the user opens.
|
||||
func osascriptReason(stderr string) string {
|
||||
low := strings.ToLower(stderr)
|
||||
switch {
|
||||
case strings.Contains(stderr, "-1743"):
|
||||
return "macOS Automation permission not granted: allow privatae-bastion to control " +
|
||||
"System Events (System Settings → Privacy & Security → Automation) — the screen cannot be read"
|
||||
case strings.Contains(stderr, "-25211"), strings.Contains(low, "assistive"):
|
||||
return "macOS Accessibility permission not granted to privatae-bastion " +
|
||||
"(System Settings → Privacy & Security → Accessibility) — the screen cannot be read"
|
||||
}
|
||||
return "osascript failed: " + stderr
|
||||
}
|
||||
|
||||
func parseSystemEventsWindows(text string) []computerWindow {
|
||||
var wins []computerWindow
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
f := strings.SplitN(line, "\t", 4)
|
||||
if len(f) < 4 {
|
||||
continue
|
||||
}
|
||||
title := strings.TrimSpace(f[3])
|
||||
if title == "" {
|
||||
continue
|
||||
}
|
||||
pid, _ := strconv.Atoi(strings.TrimSpace(f[0]))
|
||||
wins = append(wins, computerWindow{Title: title, PID: pid, App: strings.TrimSpace(f[1]),
|
||||
Focused: strings.TrimSpace(f[2]) == "true"})
|
||||
}
|
||||
return wins
|
||||
}
|
||||
Reference in New Issue
Block a user