Add probo-agent binary, installer, and CI
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -25,26 +25,37 @@
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { color: #f5f5f7; }
|
||||
code, pre {
|
||||
background: #3a3a3c;
|
||||
color: #f5f5f7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Installation complete</h1>
|
||||
|
||||
<p>
|
||||
The <code>probo-agent</code> binary is installed and the launchd
|
||||
unit is loaded. If the installer found a pre-staged
|
||||
configuration file at <code>/tmp/probo-agent.conf</code>, the
|
||||
device is already enrolled and the agent is running.
|
||||
The <code>probo-agent</code> binary is installed. The Probo icon should
|
||||
appear in the menu bar (or at your next login). To enroll, open the menu
|
||||
and choose <strong>Enroll via…</strong>, then pick
|
||||
<strong>United States</strong>, <strong>European Union</strong>, or
|
||||
<strong>Self hosted…</strong>. Finish enrollment in your browser; when
|
||||
it succeeds, the menu shows <strong>Connected</strong> and the agent
|
||||
service starts automatically.
|
||||
</p>
|
||||
|
||||
<h2>Enroll this device manually</h2>
|
||||
<p>
|
||||
If you installed without a pre-staged configuration, finish the
|
||||
setup from a Terminal:
|
||||
If MDM already enrolled the device during installation, the menu bar
|
||||
helper should already show <strong>Connected</strong>.
|
||||
</p>
|
||||
|
||||
<p>Administrators can also finish setup from a Terminal:</p>
|
||||
<pre>sudo probo-agent install \
|
||||
--server https://app.getprobo.com \
|
||||
--enrollment-token <TOKEN></pre>
|
||||
--server https://your-probo-host.example.com \
|
||||
--enrollment-token <ENROLLMENT_TOKEN></pre>
|
||||
|
||||
<h2>Inspect the agent</h2>
|
||||
<pre>sudo probo-agent status
|
||||
|
||||
@@ -22,41 +22,57 @@
|
||||
pre { padding: 8px 10px; overflow-x: auto; }
|
||||
ul { margin: 4px 0 8px 18px; padding: 0; }
|
||||
li { margin: 2px 0; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { color: #f5f5f7; }
|
||||
code, pre {
|
||||
background: #3a3a3c;
|
||||
color: #f5f5f7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to the Probo Device Posture Agent</h1>
|
||||
<p>
|
||||
This installer adds <code>probo-agent</code> to your Mac and starts it
|
||||
as a system service. The agent reports device posture — disk
|
||||
encryption, screen lock, firewall, OS version, and similar
|
||||
signals — back to your Probo workspace over HTTPS.
|
||||
This installer adds <code>probo-agent</code>, the menu bar helper, and
|
||||
the <code>probo://</code> enrollment app to your Mac. After enrollment,
|
||||
the agent reports device posture — disk encryption, screen lock,
|
||||
firewall, OS version, and similar signals — to your Probo
|
||||
workspace over HTTPS.
|
||||
</p>
|
||||
|
||||
<h2>What the installer does</h2>
|
||||
<ul>
|
||||
<li>Installs the <code>probo-agent</code> binary to
|
||||
<code>/usr/local/bin/probo-agent</code>.</li>
|
||||
<li>Registers the launchd unit
|
||||
<code>com.probo.agent</code> in
|
||||
<code>/Library/LaunchDaemons</code>.</li>
|
||||
<li>Installs <code>Probo Agent.app</code> to
|
||||
<code>/Applications</code> so browser enrollment deep links
|
||||
(<code>probo://</code>) work.</li>
|
||||
<li>Registers the menu bar helper LaunchAgent
|
||||
<code>com.probo.agent.tray</code> in
|
||||
<code>/Library/LaunchAgents</code>.</li>
|
||||
<li>Creates the persistent state directory
|
||||
<code>/var/lib/probo-agent</code> (root-owned, mode 0700).</li>
|
||||
<li>Enrolls the device automatically when an admin has pre-staged
|
||||
<code>/tmp/probo-agent.conf</code> (typically via an MDM).</li>
|
||||
<li>Enrolls the device and starts the
|
||||
<code>com.probo.agent</code> LaunchDaemon when an admin has
|
||||
pre-staged <code>/tmp/probo-agent.conf</code> (typically via
|
||||
MDM). Otherwise, finish enrollment from the menu bar icon.</li>
|
||||
</ul>
|
||||
|
||||
<h2>What you will need</h2>
|
||||
<ul>
|
||||
<li>Administrator privileges on this Mac.</li>
|
||||
<li>The Probo server URL (e.g.
|
||||
<code>https://app.getprobo.com</code>).</li>
|
||||
<li>The Probo server URL for your deployment (the URL of your Probo
|
||||
console, e.g. <code>https://your-probo-host.example.com</code>).
|
||||
Hosted Probo workspaces use the URL shown in your browser when you
|
||||
sign in.</li>
|
||||
<li>A device enrollment token issued by a workspace administrator.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Click <strong>Continue</strong> to review the license, then
|
||||
<strong>Install</strong> to proceed.
|
||||
<strong>Install</strong> to proceed. After installation, enroll from
|
||||
the menu bar unless MDM already staged a configuration file.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
# script; consumers can chain `productsign` and `xcrun notarytool`
|
||||
# afterwards.
|
||||
#
|
||||
# Must run on macOS: pkgbuild and productbuild are Apple-only tools.
|
||||
# Must run on macOS: pkgbuild, productbuild, and swift build are
|
||||
# Apple-only tools. The build also compiles Probo Agent.app (the
|
||||
# probo:// URL handler) from enroll-ui/.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -67,6 +69,10 @@ if ! command -v pkgbuild >/dev/null 2>&1 || ! command -v productbuild >/dev/null
|
||||
echo "error: pkgbuild and productbuild are required (run on macOS)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v swift >/dev/null 2>&1; then
|
||||
echo "error: swift is required to build Probo Agent.app (run on macOS)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STAGE="$(mktemp -d -t probo-agent-pkg)"
|
||||
trap 'rm -rf "${STAGE}"' EXIT
|
||||
@@ -78,6 +84,12 @@ mkdir -p "${PAYLOAD}/usr/local/bin" "${SCRIPTS}" "${RESOURCES}"
|
||||
|
||||
install -m 0755 "${BINARY}" "${PAYLOAD}/usr/local/bin/probo-agent"
|
||||
|
||||
mkdir -p "${PAYLOAD}/Applications"
|
||||
"${SCRIPT_DIR}/enroll-ui/build-app.sh" \
|
||||
--arch "${ARCH}" \
|
||||
--version "${VERSION}" \
|
||||
--output "${PAYLOAD}/Applications"
|
||||
|
||||
install -m 0755 "${SCRIPT_DIR}/scripts/postinstall" "${SCRIPTS}/postinstall"
|
||||
|
||||
cp "${SCRIPT_DIR}/Resources/welcome.html" "${RESOURCES}/welcome.html"
|
||||
|
||||
2
cmd/probo-agent/installer/macos/enroll-ui/.gitignore
vendored
Normal file
2
cmd/probo-agent/installer/macos/enroll-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.build/
|
||||
.swiftpm/
|
||||
45
cmd/probo-agent/installer/macos/enroll-ui/Info.plist.tmpl
Normal file
45
cmd/probo-agent/installer/macos/enroll-ui/Info.plist.tmpl
Normal file
@@ -0,0 +1,45 @@
|
||||
<?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">
|
||||
<!--
|
||||
Info.plist for the Probo Agent URL handler app bundle.
|
||||
|
||||
Placeholders are substituted by build-app.sh:
|
||||
|
||||
@@VERSION@@ agent version, e.g. 0.1.0
|
||||
-->
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>probo-agent-url-handler</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.getprobo.agent.url-handler</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Probo Agent</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Probo Agent</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>@@VERSION@@</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>@@VERSION@@</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>11.0</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>Probo Enrollment</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>probo</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
15
cmd/probo-agent/installer/macos/enroll-ui/Package.swift
Normal file
15
cmd/probo-agent/installer/macos/enroll-ui/Package.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
// swift-tools-version: 5.9
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "probo-agent-url-handler",
|
||||
platforms: [
|
||||
.macOS(.v11),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "probo-agent-url-handler",
|
||||
path: "URLHandlerSources"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,201 @@
|
||||
import AppKit
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
// Fixed install location written by the macOS PKG postinstall script
|
||||
// (cmd/probo-agent/installer/macos/scripts/postinstall, BINARY).
|
||||
private let agentExecutablePath = "/usr/local/bin/probo-agent"
|
||||
|
||||
private enum EnrollmentCallbackState: String, Codable {
|
||||
case success
|
||||
case failure
|
||||
}
|
||||
|
||||
private struct EnrollmentCallbackPayload: Codable {
|
||||
let state: EnrollmentCallbackState
|
||||
let message: String?
|
||||
}
|
||||
|
||||
private enum EnrollmentCallbackStore {
|
||||
static var statusFileURL: URL {
|
||||
FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("probo-agent-enrollment-status.json")
|
||||
}
|
||||
|
||||
static var lockFileURL: URL {
|
||||
FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("probo-agent-enrollment-ui.lock")
|
||||
}
|
||||
|
||||
static func isWizardRunning() -> Bool {
|
||||
guard
|
||||
let data = try? Data(contentsOf: lockFileURL),
|
||||
let pidText = String(data: data, encoding: .utf8)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
let pid = Int32(pidText),
|
||||
pid > 0
|
||||
else {
|
||||
return false
|
||||
}
|
||||
|
||||
return kill(pid, 0) == 0
|
||||
}
|
||||
|
||||
static func writeStatus(
|
||||
state: EnrollmentCallbackState,
|
||||
message: String?
|
||||
) {
|
||||
let payload = EnrollmentCallbackPayload(state: state, message: message)
|
||||
guard let data = try? JSONEncoder().encode(payload) else {
|
||||
return
|
||||
}
|
||||
|
||||
try? data.write(to: statusFileURL, options: [.atomic])
|
||||
}
|
||||
}
|
||||
|
||||
private final class URLHandlerApp: NSObject, NSApplicationDelegate {
|
||||
private var didReceiveURL = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
NSAppleEventManager.shared().setEventHandler(
|
||||
self,
|
||||
andSelector: #selector(handleGetURLEvent(_:withReplyEvent:)),
|
||||
forEventClass: AEEventClass(kInternetEventClass),
|
||||
andEventID: AEEventID(kAEGetURL)
|
||||
)
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in
|
||||
if !self.didReceiveURL {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleGetURLEvent(
|
||||
_ event: NSAppleEventDescriptor,
|
||||
withReplyEvent replyEvent: NSAppleEventDescriptor
|
||||
) {
|
||||
guard let rawURL = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
|
||||
reportFailure("Enrollment link is missing.")
|
||||
return
|
||||
}
|
||||
|
||||
guard !didReceiveURL else { return }
|
||||
|
||||
didReceiveURL = true
|
||||
runEnrollment(for: rawURL)
|
||||
}
|
||||
|
||||
private func runEnrollment(for rawURL: String) {
|
||||
let shouldNotifyWizard = EnrollmentCallbackStore.isWizardRunning()
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: agentExecutablePath)
|
||||
process.arguments = ["enroll-url", rawURL]
|
||||
|
||||
let output = Pipe()
|
||||
process.standardOutput = output
|
||||
process.standardError = output
|
||||
|
||||
var outputData = Data()
|
||||
let readHandle = output.fileHandleForReading
|
||||
let readDone = DispatchSemaphore(value: 0)
|
||||
|
||||
readHandle.readabilityHandler = { handle in
|
||||
let chunk = handle.availableData
|
||||
if chunk.isEmpty {
|
||||
handle.readabilityHandler = nil
|
||||
readDone.signal()
|
||||
return
|
||||
}
|
||||
outputData.append(chunk)
|
||||
}
|
||||
|
||||
defer { readHandle.readabilityHandler = nil }
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
readDone.signal()
|
||||
DispatchQueue.main.async {
|
||||
self.reportFailure(
|
||||
self.sanitizedFailureMessage(error.localizedDescription),
|
||||
shouldNotifyWizard: shouldNotifyWizard
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
process.waitUntilExit()
|
||||
readDone.wait()
|
||||
|
||||
guard process.terminationStatus == 0 else {
|
||||
let message = String(data: outputData, encoding: .utf8)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.reportFailure(
|
||||
self.sanitizedFailureMessage(message),
|
||||
shouldNotifyWizard: shouldNotifyWizard
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if shouldNotifyWizard {
|
||||
EnrollmentCallbackStore.writeStatus(state: .success, message: nil)
|
||||
}
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reportFailure(_ message: String, shouldNotifyWizard: Bool = EnrollmentCallbackStore.isWizardRunning()) {
|
||||
if shouldNotifyWizard {
|
||||
EnrollmentCallbackStore.writeStatus(state: .failure, message: message)
|
||||
NSApp.terminate(nil)
|
||||
return
|
||||
}
|
||||
|
||||
showError(message)
|
||||
}
|
||||
|
||||
private func sanitizedFailureMessage(_ raw: String?) -> String {
|
||||
guard let raw else {
|
||||
return "Enrollment failed. Please try again."
|
||||
}
|
||||
|
||||
let normalized = raw.lowercased()
|
||||
if normalized.contains("already enrolled") {
|
||||
return "This device is already enrolled."
|
||||
}
|
||||
if normalized.contains("key") && normalized.contains("missing") {
|
||||
return "Device API key is missing or invalid."
|
||||
}
|
||||
|
||||
return "Enrollment failed. Please try again."
|
||||
}
|
||||
|
||||
private func showError(_ message: String) {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Enrollment failed"
|
||||
alert.informativeText = message
|
||||
alert.alertStyle = .warning
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
alert.runModal()
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication.shared
|
||||
private let delegate = URLHandlerApp()
|
||||
app.delegate = delegate
|
||||
app.setActivationPolicy(.accessory)
|
||||
app.run()
|
||||
97
cmd/probo-agent/installer/macos/enroll-ui/build-app.sh
Executable file
97
cmd/probo-agent/installer/macos/enroll-ui/build-app.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build Probo Agent.app — a headless macOS app bundle that registers
|
||||
# the probo:// URL scheme and forwards enrollment links to probo-agent.
|
||||
#
|
||||
# Required arguments:
|
||||
# --arch amd64 or arm64
|
||||
# --version Agent version, e.g. 0.1.0
|
||||
# --output Parent directory; creates "Probo Agent.app" inside it
|
||||
#
|
||||
# Must run on macOS with the Swift toolchain (swift build).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
ARCH=""
|
||||
VERSION=""
|
||||
OUTPUT=""
|
||||
APP_NAME="Probo Agent.app"
|
||||
EXECUTABLE_NAME="probo-agent-url-handler"
|
||||
|
||||
usage() {
|
||||
sed -ne '/^#/!q; s/^# \{0,1\}//; 2,$ p' < "$0"
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown flag: $1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "${ARCH}" ]; then
|
||||
echo "error: --arch (amd64|arm64) is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
case "${ARCH}" in
|
||||
amd64) SWIFT_ARCH="x86_64" ;;
|
||||
arm64) SWIFT_ARCH="arm64" ;;
|
||||
*) echo "error: unsupported --arch '${ARCH}' (want amd64 or arm64)" >&2; exit 2 ;;
|
||||
esac
|
||||
if [ -z "${VERSION}" ]; then
|
||||
echo "error: --version is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -z "${OUTPUT}" ]; then
|
||||
echo "error: --output is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! command -v swift >/dev/null 2>&1; then
|
||||
echo "error: swift is required (run on macOS with Xcode or Swift toolchain)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_DIR="$(mktemp -d -t probo-agent-url-handler-build)"
|
||||
trap 'rm -rf "${BUILD_DIR}"' EXIT
|
||||
|
||||
pushd "${SCRIPT_DIR}" >/dev/null
|
||||
swift build -c release --arch "${SWIFT_ARCH}" --scratch-path "${BUILD_DIR}"
|
||||
BIN_DIR="$(swift build -c release --arch "${SWIFT_ARCH}" --scratch-path "${BUILD_DIR}" --show-bin-path)"
|
||||
BINARY="${BIN_DIR}/${EXECUTABLE_NAME}"
|
||||
popd >/dev/null
|
||||
|
||||
if [ ! -x "${BINARY}" ]; then
|
||||
echo "error: release binary not found at ${BINARY}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APP_ROOT="${OUTPUT}/${APP_NAME}"
|
||||
CONTENTS="${APP_ROOT}/Contents"
|
||||
MACOS="${CONTENTS}/MacOS"
|
||||
PLIST="${CONTENTS}/Info.plist"
|
||||
|
||||
rm -rf "${APP_ROOT}"
|
||||
mkdir -p "${MACOS}"
|
||||
|
||||
install -m 0755 "${BINARY}" "${MACOS}/${EXECUTABLE_NAME}"
|
||||
|
||||
sed \
|
||||
-e "s|@@VERSION@@|${VERSION}|g" \
|
||||
"${SCRIPT_DIR}/Info.plist.tmpl" > "${PLIST}"
|
||||
|
||||
if ! plutil -lint "${PLIST}" >/dev/null; then
|
||||
echo "error: rendered Info.plist failed plutil -lint" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q '<string>probo</string>' "${PLIST}"; then
|
||||
echo "error: Info.plist is missing probo URL scheme" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Built ${APP_ROOT}"
|
||||
@@ -12,14 +12,17 @@
|
||||
#
|
||||
# We intentionally do not abort the install if enrollment fails:
|
||||
# the binary is laid down regardless, and the operator can finish
|
||||
# enrollment with `sudo probo-agent install ...` from Terminal.
|
||||
# enrollment from the menu bar helper.
|
||||
|
||||
set -u
|
||||
|
||||
LOG_FILE="/var/log/probo-agent-install.log"
|
||||
BINARY="/usr/local/bin/probo-agent"
|
||||
STATE_DIR="/var/lib/probo-agent"
|
||||
RUN_DIR="/var/run/probo-agent"
|
||||
CONF_FILE="/tmp/probo-agent.conf"
|
||||
TRAY_LABEL="com.probo.agent.tray"
|
||||
TRAY_PLIST_NAME="${TRAY_LABEL}.plist"
|
||||
|
||||
# Mirror everything to the install log. We keep stdout/stderr open
|
||||
# too so failures still surface in macOS Installer.app's log pane.
|
||||
@@ -39,11 +42,123 @@ mkdir -p "${STATE_DIR}"
|
||||
chown root:wheel "${STATE_DIR}"
|
||||
chmod 0700 "${STATE_DIR}"
|
||||
|
||||
mkdir -p "${RUN_DIR}"
|
||||
chown root:wheel "${RUN_DIR}"
|
||||
chmod 0755 "${RUN_DIR}"
|
||||
|
||||
register_tray_launchagent() {
|
||||
local current_user user_uid agents_dir plist_path
|
||||
|
||||
agents_dir="/Library/LaunchAgents"
|
||||
plist_path="${agents_dir}/${TRAY_PLIST_NAME}"
|
||||
|
||||
mkdir -p "${agents_dir}"
|
||||
cat > "${plist_path}" <<EOF
|
||||
<?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>${TRAY_LABEL}</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>${BINARY}</string>
|
||||
<string>tray</string>
|
||||
<string>--run-dir</string>
|
||||
<string>${RUN_DIR}</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
chmod 0644 "${plist_path}"
|
||||
echo "Installed tray LaunchAgent at ${plist_path}."
|
||||
|
||||
bootstrap_tray_for_user() {
|
||||
local username="$1"
|
||||
local user_uid
|
||||
|
||||
if [ -z "${username}" ] || \
|
||||
[ "${username}" = "root" ] || \
|
||||
[ "${username}" = "loginwindow" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
user_uid="$(id -u "${username}" 2>/dev/null || true)"
|
||||
if [ -z "${user_uid}" ]; then
|
||||
echo "warning: cannot resolve uid for ${username}; skipping tray bootstrap."
|
||||
return 1
|
||||
fi
|
||||
|
||||
launchctl bootout "gui/${user_uid}/${TRAY_LABEL}" 2>/dev/null || true
|
||||
if ! launchctl bootstrap "gui/${user_uid}" "${plist_path}"; then
|
||||
echo "warning: could not start tray helper for ${username}; it will start at next GUI login."
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Started tray LaunchAgent for ${username}."
|
||||
return 0
|
||||
}
|
||||
|
||||
started_any=false
|
||||
seen_users=" "
|
||||
|
||||
for username in $(users 2>/dev/null || true); do
|
||||
case "${seen_users}" in
|
||||
*" ${username} "*) continue ;;
|
||||
esac
|
||||
seen_users="${seen_users}${username} "
|
||||
|
||||
if bootstrap_tray_for_user "${username}"; then
|
||||
started_any=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${started_any}" = false ]; then
|
||||
current_user=$(stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||
if bootstrap_tray_for_user "${current_user}"; then
|
||||
started_any=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${started_any}" = false ]; then
|
||||
echo "No active GUI session found; tray helper will start at next GUI login."
|
||||
fi
|
||||
}
|
||||
|
||||
register_enrollment_url_scheme() {
|
||||
local app_path lsregister
|
||||
|
||||
app_path="/Applications/Probo Agent.app"
|
||||
if [ ! -d "${app_path}" ]; then
|
||||
echo "warning: ${app_path} not found; cannot register probo:// URL scheme."
|
||||
return 0
|
||||
fi
|
||||
|
||||
lsregister="/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||
if [ ! -x "${lsregister}" ]; then
|
||||
echo "warning: lsregister is unavailable; URL scheme registration skipped."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! "${lsregister}" -f "${app_path}"; then
|
||||
echo "warning: failed to register probo:// URL scheme."
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "Registered probo:// URL scheme."
|
||||
}
|
||||
|
||||
# An admin (or MDM) may stage /tmp/probo-agent.conf to drive an
|
||||
# unattended enrollment. Recognized keys (shell-style):
|
||||
#
|
||||
# PROBO_SERVER_URL=https://app.getprobo.com
|
||||
# PROBO_ENROLLMENT_TOKEN=<token>
|
||||
# PROBO_SERVER_URL=https://your-probo-host.example.com
|
||||
# PROBO_ENROLLMENT_TOKEN=<enrollment-token>
|
||||
# PROBO_NO_AUTO_UPDATE=true
|
||||
#
|
||||
# Parse KEY=VALUE lines without sourcing or eval so a crafted conf
|
||||
@@ -61,7 +176,7 @@ if [ -f "${CONF_FILE}" ]; then
|
||||
echo "Found ${CONF_FILE}, attempting unattended enrollment."
|
||||
|
||||
CONF_SERVER=""
|
||||
CONF_TOKEN=""
|
||||
CONF_ENROLLMENT_TOKEN=""
|
||||
CONF_NOUPDATE=""
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line%%#*}"
|
||||
@@ -74,7 +189,7 @@ if [ -f "${CONF_FILE}" ]; then
|
||||
CONF_SERVER="$(strip_conf_value "${line#PROBO_SERVER_URL=}")"
|
||||
;;
|
||||
PROBO_ENROLLMENT_TOKEN=*)
|
||||
CONF_TOKEN="$(strip_conf_value "${line#PROBO_ENROLLMENT_TOKEN=}")"
|
||||
CONF_ENROLLMENT_TOKEN="$(strip_conf_value "${line#PROBO_ENROLLMENT_TOKEN=}")"
|
||||
;;
|
||||
PROBO_NO_AUTO_UPDATE=*)
|
||||
CONF_NOUPDATE="$(strip_conf_value "${line#PROBO_NO_AUTO_UPDATE=}")"
|
||||
@@ -82,32 +197,38 @@ if [ -f "${CONF_FILE}" ]; then
|
||||
esac
|
||||
done < "${CONF_FILE}"
|
||||
|
||||
if [ -z "${CONF_SERVER}" ] || [ -z "${CONF_TOKEN}" ]; then
|
||||
if [ -z "${CONF_SERVER}" ] || [ -z "${CONF_ENROLLMENT_TOKEN}" ]; then
|
||||
echo "warning: ${CONF_FILE} is missing PROBO_SERVER_URL or PROBO_ENROLLMENT_TOKEN; skipping enrollment."
|
||||
else
|
||||
EXTRA_FLAGS=()
|
||||
# Build argv from the first element so "${INSTALL_ARGS[@]}"
|
||||
# is never empty — macOS /bin/bash 3.2 treats an unset empty
|
||||
# array as unbound under `set -u`.
|
||||
INSTALL_ARGS=(
|
||||
install
|
||||
--server "${CONF_SERVER}"
|
||||
--enrollment-token "${CONF_ENROLLMENT_TOKEN}"
|
||||
)
|
||||
case "${CONF_NOUPDATE}" in
|
||||
1|true|TRUE|yes|YES) EXTRA_FLAGS+=("--no-auto-update") ;;
|
||||
1|true|TRUE|yes|YES) INSTALL_ARGS+=(--no-auto-update) ;;
|
||||
esac
|
||||
|
||||
if "${BINARY}" install \
|
||||
--server "${CONF_SERVER}" \
|
||||
--enrollment-token "${CONF_TOKEN}" \
|
||||
"${EXTRA_FLAGS[@]}"; then
|
||||
if "${BINARY}" "${INSTALL_ARGS[@]}"; then
|
||||
echo "Device enrolled and service installed."
|
||||
else
|
||||
echo "warning: probo-agent install failed; the binary is in place and can be re-run by an admin."
|
||||
fi
|
||||
fi
|
||||
|
||||
# The token in the conf file is sensitive; clear it whatever
|
||||
# The enrollment token in the conf file is sensitive; clear it
|
||||
# the outcome so a successful install does not leave secrets
|
||||
# in /tmp.
|
||||
rm -f "${CONF_FILE}"
|
||||
else
|
||||
echo "No ${CONF_FILE} found; skipping automatic enrollment."
|
||||
echo "Finish setup with: sudo ${BINARY} install --server <URL> --enrollment-token <TOKEN>"
|
||||
echo "No ${CONF_FILE} found; enrollment can be completed from the menu bar icon."
|
||||
fi
|
||||
|
||||
register_tray_launchagent
|
||||
register_enrollment_url_scheme
|
||||
|
||||
echo "=== postinstall done ==="
|
||||
exit 0
|
||||
|
||||
9
cmd/probo-agent/installer/macos/scripts/preinstall
Normal file
9
cmd/probo-agent/installer/macos/scripts/preinstall
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# probo-agent macOS PKG preinstall script.
|
||||
#
|
||||
# Enrollment is handled by the menu bar helper after installation.
|
||||
# MDM may still pre-stage /tmp/probo-agent.conf for unattended
|
||||
# enrollment in postinstall.
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user