Fix setup: shell PATH, restart race, Tailscale wait, macOS 27 grants
This commit is contained in:
+8
-1
@@ -78,8 +78,15 @@ func installCmd() {
|
||||
return
|
||||
}
|
||||
// bootout kills every process the service started, which includes this one when an agent runs it.
|
||||
reload := exec.Command("/bin/sh", "-c", `launchctl bootout "$1"; launchctl bootstrap "$2" "$3"`, "sh", service, domain, plistPath)
|
||||
// It returns before the old server exits, and bootstrap fails while the job is still tearing down.
|
||||
reload := exec.Command("/bin/sh", "-c", `launchctl bootout "$1"; while launchctl print "$1" >/dev/null 2>&1; do sleep 1; done; launchctl bootstrap "$2" "$3"`, "sh", service, domain, plistPath)
|
||||
reload.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
logFile, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
|
||||
if err != nil {
|
||||
fatal("open log", "err", err)
|
||||
}
|
||||
reload.Stdout = logFile
|
||||
reload.Stderr = logFile
|
||||
err = reload.Start()
|
||||
if err != nil {
|
||||
fatal("restart service", "err", err)
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ Every build is signed for distribution outside the App Store: the team's Develop
|
||||
|
||||
- Startup waits until the app's tailnet node lists the server's node among its peers, not just until it is `Running`: a node starting from libtailscale's netmap cache reports `Running` with no peers before control sends the real netmap, and a dial made then fails.
|
||||
- Every request reaches the tailnet through byre's own SOCKS5 proxy on a loopback port (`Proxy.swift`), which dials with `tailscale_dial`, not libtailscale's `tailscale_loopback`. iOS reclaims a suspended app's listening sockets, and tsnet's proxy stops for good on the first failed accept, leaving the phone unable to reach any server. byre's proxy binds again on the same port each time the app returns to the foreground.
|
||||
- Run byre on this Mac and the Server tab run `byre install` through the user's login shell, since `byre install` copies its `PATH` into the service and the app's own `PATH` from launchd finds none of `claude`, `codex`, `agy` or `npx`.
|
||||
- Run byre on this Mac and the Server tab run `byre install` through the user's shell as an interactive login shell (`-l -i`), since `byre install` copies its `PATH` into the service and the app's own `PATH` from launchd finds none of `claude`, `codex`, `agy` or `npx`. It must be interactive: claude's installer has the user add `~/.local/bin` to `.zshrc`, which a non-interactive login shell doesn't read.
|
||||
- Signing a new server's Tailscale node in for the first time can take well over a minute before its ts.net address answers (DNS and its first HTTPS certificate), so Run byre on this Mac waits up to 2 minutes for it.
|
||||
- SwiftUI's window restoration is off, but the scene keeps the id `main` that earlier builds used: AppKit restores their saved window by that id, and SwiftUI then opens no launch window at all when the id names no scene.
|
||||
|
||||
## Views
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ Closing a task deletes its branch, worktree and snapshot refs, so a task continu
|
||||
|
||||
## Running as a service
|
||||
|
||||
The service always runs from the app: `/Applications/Byre.app/Contents/Helpers/byre install`, which the app's Server tab and connection screen run. `byre install` takes `PATH` from the shell it is run in, since the service's `PATH` must find `claude`, `codex`, `agy`, `git`, `curl` and `npx`. When the service is already loaded, `install` hands `launchctl bootout` and `bootstrap` to a detached `sh` and exits: `bootout` kills every process in the service's process groups, which includes `byre install` itself when an agent byre runs is the one running it.
|
||||
The service always runs from the app: `/Applications/Byre.app/Contents/Helpers/byre install`, which the app's Server tab and connection screen run. `byre install` takes `PATH` from the shell it is run in, since the service's `PATH` must find `claude`, `codex`, `agy`, `git`, `curl` and `npx`. When the service is already loaded, `install` hands `launchctl bootout` and `bootstrap` to a detached `sh` and exits: `bootout` kills every process in the service's process groups, which includes `byre install` itself when an agent byre runs is the one running it. `bootout` returns once it has sent SIGTERM, not once the server has exited, and a `bootstrap` while the old job is still tearing down fails with `Bootstrap failed: 5: Input/output error`, leaving no service at all; so the `sh` waits until `launchctl print` no longer finds the service before `bootstrap`, and writes its output to `~/Library/Logs/byre.log`.
|
||||
|
||||
`byre serve` exits when its executable is replaced (logging `executable replaced, exiting`) and launchd starts the new one. A missing path counts as not yet replaced, so the moment in `install.sh` between removing the old bundle and moving the new one in doesn't trigger it. The service log is owner-only (0600), including existing logs: Tailscale can put a sign-in URL there. Logs go to `~/Library/Logs/byre.log`; `launchctl print gui/$(id -u)/run.fc.byre` shows the service.
|
||||
|
||||
@@ -123,7 +123,7 @@ byre never signs with a key on the machine it runs on: every git command it runs
|
||||
- **Orion.** Orion ignores key events that carry only a Unicode string, so typing posts each ASCII character as its key code. Some apps list `confirm` but ignore it, Orion's address bar among them; send Return instead. `do JavaScript` needs Orion's Develop → "Allow JavaScript from Apple Events".
|
||||
- **Chrome.** Chrome refuses remote debugging on its default profile, so byre's Chrome is its own profile (`~/Library/Application Support/byre/chrome`), a separate Chrome beside any the human runs, and it keeps its logins: the human signs in there once. Chrome comes from `brew install --cask google-chrome`; macOS's "downloaded from the Internet" prompt on its first launch must be answered Open (or `osascript -e 'tell application "System Events" to tell process "CoreServicesUIAgent" to click button "Open" of window 1'`), and until it is, Chrome cannot accept debugging commands.
|
||||
- **Chrome's trust boundary.** Use Chrome's loopback debugging port with byre's separate persistent profile and Chrome's default browser-origin checks; never add `--remote-allow-origins=*`. Other local OS accounts can reach that debugger and control its logged-in sessions. This is an accepted risk on the personal Macs byre runs on, as is access to the app's local preview forwards; do not add account-isolating brokers for these listeners. Chrome must be launched with `--remote-debugging-port=0` so it records its port and browser WebSocket path in the profile’s `DevToolsActivePort` file. After switching from the pipe or fixed-port transport, quit the old byre-profile Chrome once so the next tool call can start it with that discovery file. The saved profile and logins stay in place.
|
||||
- **Permissions.** Agents' processes run as byre's descendants, so macOS privacy controls (TCC) hold byre responsible for them: the Screen Recording, Accessibility and Automation grants in System Settings → Privacy & Security are byre's, made by clicking at the Mac's screen, and each app scripted for the first time asks for its own Automation grant. Without Accessibility an untrusted process just sees no windows and no menu bar. Accessibility can list byre switched off; only its switch being on counts. TCC keys grants on the signing identifier and team, and every build is signed with Developer ID, so grants carry over from build to build. On a client Mac the grants belong to byre's app; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. The Desktop, Documents, Downloads and iCloud Drive folders each ask for their own Files & Folders grant the first time a process byre is responsible for reads them. `byre-hands check` prints the grants of the process that runs it, listing each of those folders so their prompts come up during the check rather than mid-turn; the Server and Settings tabs run it.
|
||||
- **Permissions.** Agents' processes run as byre's descendants, so macOS privacy controls (TCC) hold byre responsible for them: the Screen Recording, Device Control and Data Access (macOS 27's name for Accessibility) and Automation grants in System Settings → Privacy & Security are byre's, made by clicking at the Mac's screen, and each app scripted for the first time asks for its own Automation grant. Without Device Control and Data Access an untrusted process just sees no windows and no menu bar. A Mac running a server lists byre twice: the server's helper, a bare executable shown by its file name `byre` with a terminal icon, and the app, `Byre` with its icon; `byre-hands` names the app when the app is among its ancestor processes, and the server otherwise. Device Control and Data Access can list byre switched off; only its switch being on counts. TCC keys grants on the signing identifier and team, and every build is signed with Developer ID, so grants carry over from build to build. On a client Mac the grants belong to byre's app; `NSAppleEventsUsageDescription` in `mac/Info.plist` is what lets macOS ask for the Automation grant. The Desktop, Documents, Downloads and iCloud Drive folders each ask for their own Files & Folders grant the first time a process byre is responsible for reads them. `byre-hands check` prints the grants of the process that runs it, listing each of those folders so their prompts come up during the check rather than mid-turn; the Server and Settings tabs run it.
|
||||
|
||||
## Terminals
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func appElement(_ pid: pid_t) -> AXUIElement {
|
||||
|
||||
func requireTrust() throws {
|
||||
if !AXIsProcessTrusted() {
|
||||
throw ToolError("byre lacks Accessibility, so no app's tree can be read or acted on; the human must switch byre on in System Settings → Privacy & Security → Accessibility")
|
||||
throw ToolError("byre lacks \(deviceControl), so no app's tree can be read or acted on; the human must switch \(grantee) on in System Settings → Privacy & Security → \(deviceControl)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,24 @@ import ApplicationServices
|
||||
import ScreenCaptureKit
|
||||
|
||||
let orionBundle = "com.kagi.kagimacOS"
|
||||
let deviceControl = "Device Control and Data Access"
|
||||
|
||||
// The server and the app each run byre-hands, and System Settings lists them as two entries: the server's helper by its file name, the app by its own.
|
||||
let grantee: String = {
|
||||
var pid = getppid()
|
||||
while pid > 1 {
|
||||
var buf = [CChar](repeating: 0, count: Int(MAXPATHLEN))
|
||||
if proc_pidpath(pid, &buf, UInt32(buf.count)) > 0, String(cString: buf).hasSuffix(".app/Contents/MacOS/Byre") {
|
||||
return "Byre (with byre's app icon)"
|
||||
}
|
||||
var info = proc_bsdinfo()
|
||||
if proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, Int32(MemoryLayout<proc_bsdinfo>.size)) <= 0 {
|
||||
break
|
||||
}
|
||||
pid = pid_t(info.pbi_ppid)
|
||||
}
|
||||
return "byre (with the black terminal icon)"
|
||||
}()
|
||||
|
||||
func permission(_ name: String, _ status: String, _ detail: String, _ pane: String) -> [String: String] {
|
||||
return ["name": name, "status": status, "detail": detail, "settings": "x-apple.systempreferences:com.apple.preference.security?Privacy_\(pane)"]
|
||||
@@ -14,26 +32,26 @@ func checkScreenRecording() async -> [String: String] {
|
||||
return permission("Screen Recording", "granted", "Listed the screen (displays: \(content.displays.count), windows: \(content.windows.count)).", "ScreenCapture")
|
||||
} catch {
|
||||
CGRequestScreenCaptureAccess()
|
||||
return permission("Screen Recording", "missing", "Listing the screen failed (\(error.localizedDescription)). Switch byre on in Screen Recording.", "ScreenCapture")
|
||||
return permission("Screen Recording", "missing", "Listing the screen failed (\(error.localizedDescription)). Switch \(grantee) on in Screen Recording.", "ScreenCapture")
|
||||
}
|
||||
}
|
||||
|
||||
func checkAccessibility() -> [String: String] {
|
||||
if !AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary) {
|
||||
return permission("Accessibility", "missing", "Switch byre on in Accessibility.", "Accessibility")
|
||||
return permission(deviceControl, "missing", "Switch \(grantee) on in \(deviceControl).", "Accessibility")
|
||||
}
|
||||
guard let finder = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.finder").first else {
|
||||
return permission("Accessibility", "unchecked", "Granted, but Finder isn't running to read its menu bar.", "Accessibility")
|
||||
return permission(deviceControl, "unchecked", "Granted, but Finder isn't running to read its menu bar.", "Accessibility")
|
||||
}
|
||||
do {
|
||||
guard let bar = try attr(appElement(finder.processIdentifier), kAXMenuBarAttribute) else {
|
||||
return permission("Accessibility", "missing", "Granted, but reading Finder's menu bar returned nothing. Switch byre off and on again in Accessibility.", "Accessibility")
|
||||
return permission(deviceControl, "missing", "Granted, but reading Finder's menu bar returned nothing. Switch \(grantee) off and on again in \(deviceControl).", "Accessibility")
|
||||
}
|
||||
return permission("Accessibility", "granted", "Read Finder's menu bar (menus: \(try children(bar as! AXUIElement).count)).", "Accessibility")
|
||||
return permission(deviceControl, "granted", "Read Finder's menu bar (menus: \(try children(bar as! AXUIElement).count)).", "Accessibility")
|
||||
} catch let e as ToolError {
|
||||
return permission("Accessibility", "missing", e.message, "Accessibility")
|
||||
return permission(deviceControl, "missing", e.message, "Accessibility")
|
||||
} catch {
|
||||
return permission("Accessibility", "missing", "\(error)", "Accessibility")
|
||||
return permission(deviceControl, "missing", "\(error)", "Accessibility")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +62,7 @@ func checkOrion() -> [String: String] {
|
||||
let target = NSAppleEventDescriptor(bundleIdentifier: orionBundle)
|
||||
let status = AEDeterminePermissionToAutomateTarget(target.aeDesc, typeWildCard, typeWildCard, true)
|
||||
if status != noErr {
|
||||
return permission("Automation of Orion", "missing", "macOS refused Apple events to Orion (\(status)). Switch Orion on under byre in Automation.", "Automation")
|
||||
return permission("Automation of Orion", "missing", "macOS refused Apple events to Orion (\(status)). Switch Orion on under \(grantee) in Automation.", "Automation")
|
||||
}
|
||||
do {
|
||||
let windows = try osa("tell application \"Orion\" to count windows").int32Value
|
||||
@@ -65,7 +83,7 @@ func checkFolder(_ name: String, _ path: String) -> [String: String] {
|
||||
let entries = try FileManager.default.contentsOfDirectory(atPath: dir)
|
||||
return permission(name, "granted", "Listed \(dir) (entries: \(entries.count)).", "FilesAndFolders")
|
||||
} catch {
|
||||
return permission(name, "missing", "Listing \(dir) failed (\(error.localizedDescription)). Switch \(name) on under byre in Files & Folders.", "FilesAndFolders")
|
||||
return permission(name, "missing", "Listing \(dir) failed (\(error.localizedDescription)). Switch \(name) on under \(grantee) in Files & Folders.", "FilesAndFolders")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ func (m *Manager) clientTools(l *live) []mcp.Tool {
|
||||
Description: "Call one of your hands tools (screen_list, capture, ax_tree, act, browser, chrome) on a client Mac instead of the Mac byre runs on: " +
|
||||
"tool is the tool's name and arguments exactly what that tool takes. Window ids, refs, displays and Chrome tabs are that Mac's, so start with screen_list (or chrome tabs) there. " +
|
||||
"Every agent using that Mac shares one set of refs, so read the tree again if another agent may have. " +
|
||||
"A call fails until the human has given byre's app Screen Recording and Accessibility (and Automation of Orion, for browser) on that Mac; say so when it does. " +
|
||||
"A call fails until the human has given byre's app Screen Recording and Device Control and Data Access (and Automation of Orion, for browser) on that Mac; say so when it does. " +
|
||||
"chrome needs Google Chrome installed on that Mac; its trace files are saved there, so fetch one with client_download.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
|
||||
@@ -11,14 +11,15 @@ Go one step at a time: check, say in a line or two what is missing and what it i
|
||||
Agents run on the Mac byre runs on, the one your own Bash runs on. Check its tools with:
|
||||
|
||||
```
|
||||
"$SHELL" -l -c '/Applications/Byre.app/Contents/Helpers/byre check'
|
||||
"$SHELL" -l -i -c '/Applications/Byre.app/Contents/Helpers/byre check'
|
||||
```
|
||||
|
||||
It prints one JSON line: `ready` (false while something blocking is missing) and `checks`, each with `name`, `need` (`required`; `agent`, where one of claude, codex and agy is enough; or `optional`), `for` (what it is used for), `ok`, `command` (what installs it), `fix` (what to do after) and `blocking`.
|
||||
|
||||
- Run each missing tool's `command` yourself through the login shell, `"$SHELL" -l -c '<command>'`. `brew` must be there first; installing Homebrew asks for the human's password, so ask them to run its installer in a terminal on that Mac (the app's Server tab there lists each missing tool with a Run in Terminal button). `xcode-select --install` opens a macOS dialog on that Mac's screen for the human to click Install.
|
||||
- Sign-in needs the human at a terminal on that Mac: `claude`, `codex login` or `agy`, each run once. `claude auth status` (JSON, `loggedIn`) and `codex login status` show whether one is signed in; agy has no status command, so ask.
|
||||
- The service's `PATH` is fixed when it is installed, and your own shell has the same one. When a tool is found through the login shell but not by `/Applications/Byre.app/Contents/Helpers/byre check` run directly, reinstall the service from the login shell so it picks the new `PATH` up: `"$SHELL" -l -c '/Applications/Byre.app/Contents/Helpers/byre install'`. That restarts byre and cuts your turn off; you resume with a restart message, so say before running it that you will carry on afterwards.
|
||||
- The service's `PATH` is fixed when it is installed, and your own shell has the same one. It comes from an interactive login shell (`-l -i`), since claude's installer has the human add `~/.local/bin` to `.zshrc`. When a tool is found through that shell but not by `/Applications/Byre.app/Contents/Helpers/byre check` run directly, reinstall the service from it so it picks the new `PATH` up: `"$SHELL" -l -i -c '/Applications/Byre.app/Contents/Helpers/byre install'`. That restarts byre and cuts your turn off; you resume with a restart message, so say before running it that you will carry on afterwards. When you resume, check with `byre check` before reinstalling again: the restart usually took.
|
||||
- `byre install` and the app's Server tab (Turn off, Turn on) are the only ways to restart byre's service. Never run `launchctl` on it yourself, and never tell the human to.
|
||||
- Google Chrome, for agents' browser tools, asks "downloaded from the Internet" on its first launch, and can't be driven until the human answers Open.
|
||||
|
||||
## Permissions on the server's Mac
|
||||
@@ -32,7 +33,7 @@ d=$(mktemp -d) && cp /Applications/Byre.app/Contents/Helpers/byre-hands "$d/" &&
|
||||
It prints a JSON array, one entry per grant: `name`, `status` (`granted`, `missing`, or `unchecked` when the test couldn't run, such as Automation of Orion while Orion isn't running), `detail` (what to switch on) and `settings`, the `x-apple.systempreferences` URL of its System Settings pane.
|
||||
|
||||
- A missing folder grant makes macOS show a prompt on that Mac's screen during the check, and the check waits until it is answered, so tell the human to be at that Mac before you run it, and give the call a timeout of a few minutes.
|
||||
- For Screen Recording, Accessibility and Automation, open the pane for the human with `open '<settings>'` and tell them which switch to turn on. Accessibility can list byre switched off; only the switch being on counts.
|
||||
- For Screen Recording, Device Control and Data Access (what macOS 27 calls Accessibility) and Automation, open the pane for the human with `open '<settings>'` and tell them which switch to turn on. On a Mac that runs a server, the panes list two entries: `byre` with a black terminal icon is the server, whose grants agents on that Mac use, and `Byre` with byre's app icon is the app, whose grants client tools on that Mac use. The check's `detail` names the one it checked. Device Control and Data Access can list byre switched off; only the switch being on counts.
|
||||
- Check again after the human says they've done it.
|
||||
|
||||
## Client Macs
|
||||
|
||||
@@ -31,7 +31,7 @@ const leadGuidance = `You are the human's lead agent in byre, shown in the left
|
||||
- Reviews: after each new snapshot of a task, the stages of its project's review workflow (a saved workflow whose stages are single agents, set per project with set_review_workflow) review the change, each a reviewer agent on its stage's model; a stage after others gets their reviews. A project with no review workflow isn't reviewed. Once every stage has reviewed the snapshot, the task's agent gets all their findings in one message. The results are in the task's transcript, where its agent answers each finding with respond_to_finding (fixing, or ignoring with a reason), and read_transcript shows each review's findings. They are for the human to look at when they want; mention one only when its findings matter to what the human is deciding.
|
||||
- Dig into context with list_tasks, read_transcript, read_inbox and read_status (vendor rate limits and agent health), and with your own Read, Grep and Bash on a task's worktree path (from list_tasks). Don't edit files in task worktrees; ask the task's agent with send_message.
|
||||
- You can create tasks, start TODO items, message, interrupt and compact task agents, and merge or discard tasks. Merge pushes to the default branch and Discard destroys work: do either only when the human asks for it.
|
||||
- Setup: when the human asks you to set byre up, or about the tools or macOS permissions its agents need, read the setup knowledge-base article and walk them through it, running its checks and fixes yourself with your own Bash on this Mac and with list_clients and client_bash on their Macs.
|
||||
- Setup: when the human asks you to set byre up, or about the tools or macOS permissions its agents need, read the setup knowledge-base article and walk them through it, running its checks and fixes yourself with your own Bash on this Mac and with list_clients and client_bash on their Macs. Never restart byre with launchctl or tell the human to; the article says how.
|
||||
- Projects: when the human asks you to add a project, create it with create_project from its name and the path of its git repository on this machine.
|
||||
- Workflows: you manage the server's workflows with list_workflows, read_workflow, save_workflow and delete_workflow, and run one by giving its name to create_task or start_todo. A workflow is a tree of steps run in order, each a stage, an order or a parallel; every stage runs as its own task on the stage's model once the step before it has finished, so stages fan out (a parallel after a stage) and fan in (a stage after a parallel); a stage's agent calls finish_stage to hand its output to the stages after it, signals tell you when a stage starts and when the workflow finishes, and the workflow finishes once every stage has; byre then discards the run's other stage tasks, leaving the task of each last stage (one per branch when it ends in a parallel) to merge. Each last stage's agent is asked whether its task is ready to merge as soon as its own stage has finished and been reviewed, without waiting for the other last stages. Whenever you show or propose a workflow, draw it as a fenced diagram block (its format is the diagrams knowledge-base article) of its stage names only, with no models: one row per step, a parallel's stages side by side in one row, and an arrow into each stage from the stages before it. For example, rows holding Build; Review and Test; Fix, with the arrows "build -> review", "build -> test", "review -> fix" and "test -> fix".
|
||||
- Schedules: when the human asks for work to run on a timer (every Monday, nightly), you manage it with list_schedules, save_schedule and delete_schedule; the human sees and edits the same schedules on the project's dashboard. Every schedule runs a saved workflow: for a one-agent chore, save a one-stage workflow with save_workflow first. On each run, byre starts the schedule's workflow as create_task with workflow would, created by byre, and you get its created signal; a run that fails to start reaches you as a signal.
|
||||
|
||||
@@ -343,10 +343,11 @@ enum Server {
|
||||
return try JSONDecoder().decode(TailnetInfo.self, from: data)
|
||||
}
|
||||
|
||||
// byre install copies its PATH into the service, which needs the login shell's to find claude, codex, agy and npx.
|
||||
// byre install copies its PATH into the service, which needs the interactive login shell's to find claude, codex, agy and npx:
|
||||
// installers such as claude's have the user add their directory to .zshrc, which only an interactive shell reads.
|
||||
private static func helper(_ command: String) async throws -> (status: Int32, output: String) {
|
||||
let shell = unsafe String(cString: getpwuid(getuid()).pointee.pw_shell)
|
||||
return try await execute(shell, ["-l", "-c", "exec \"$BYRE_HELPER\" \(command)"], env: ["BYRE_HELPER": serverHelper])
|
||||
return try await execute(shell, ["-l", "-i", "-c","exec \"$BYRE_HELPER\" \(command)"], env: ["BYRE_HELPER": serverHelper])
|
||||
}
|
||||
|
||||
static func checks() async throws -> Checks {
|
||||
|
||||
@@ -347,8 +347,8 @@ final class ConnectForm {
|
||||
}
|
||||
}
|
||||
group.addTask {
|
||||
try await Task.sleep(for: .seconds(15))
|
||||
throw ProcessFailure(errorDescription: "byre's Tailscale node is signed in but \(origin.absoluteString) didn't answer within 15 s.")
|
||||
try await Task.sleep(for: .seconds(120))
|
||||
throw ProcessFailure(errorDescription: "byre's Tailscale node is signed in but \(origin.absoluteString) didn't answer within 2 minutes.")
|
||||
}
|
||||
defer {
|
||||
group.cancelAll()
|
||||
|
||||
Reference in New Issue
Block a user