From bd665383831c02402797bac14596d505cc4dacb4 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 18 Oct 2026 00:31:46 -0700 Subject: [PATCH] fix warnings --- docs/operations.md | 2 +- mac/Package.swift | 1 + mac/Sources/Byre/Chat.swift | 8 +- mac/Sources/Byre/EventStream.swift | 8 +- mac/Sources/Byre/Fonts.swift | 16 +- mac/Sources/Byre/Format.swift | 23 +-- mac/Sources/Byre/Markdown.swift | 2 +- mac/Sources/Byre/MarkdownView.swift | 41 +++-- mac/Sources/Byre/PaneFocus.swift | 37 +++-- mac/Sources/Byre/Remote.swift | 238 ++++++++++++---------------- mac/Sources/Byre/Terminal.swift | 2 +- 11 files changed, 169 insertions(+), 209 deletions(-) diff --git a/docs/operations.md b/docs/operations.md index 42666cd..ee2c736 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -301,7 +301,7 @@ Task agents use the Macs the human has byre's app open on directly, with no appr Implementer sessions get eight MCP tools for it (`internal/agents/clients.go`). The app sets its WebSocket's `maximumMessageSize` to 256 MiB, the same as byre's read limit, so file contents fit in one message each way. - `list_clients`: the connected clients, with each one's login, connection time and app commit, and its `last_action_at` from Status (see "Status") matched on login, host and commit; the most recently active is listed first and marked. -- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c ` in the human's home directory through `posix_spawn` in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running. +- `client_bash`: `op: bash`. The app runs `/bin/zsh -l -c ` in the human's home directory through `Process`, which starts it in its own process group, stdin from `/dev/null` and stdout and stderr into one temporary file, and kills the group with `SIGKILL` after `timeout_ms` (default 120000, at most 600000), replying `exit_code` -1. Otherwise `exit_code` is the exit status, or 128 plus the signal. `output` is the file's last 64 KiB. The agent's call waits the timeout plus 30s for the reply. Background children the command leaves are not waited for or killed, and a command that quits the app keeps running. - `client_upload`: `op: upload`. `path` is checked against the worktree through an `os.Root`; byre registers a transfer naming the path's directory and base name under a random id for as long as the call waits (at most 10 minutes), and the app downloads `GET /api/clients/transfers/{transferId}`, which streams `tar -czf - -C -- ` (404 once the call has ended). The app unpacks it into a new `.byre-upload-` directory beside `dest` (`~/` expanded, parent directories created), removes whatever is at `dest`, moves the archive's one entry there and removes the temporary directory. - `client_download`: `op: download`, the reverse. byre makes `dest`'s parent directories through an `os.Root` on the worktree and registers a receiving transfer (`Transfer.Receive`) for a fresh temporary directory; the app runs `tar -czf -C -- ` and `PUT`s it to `/api/clients/transfers/{transferId}`, which writes it once (`O_EXCL`) and answers 204, and 404 for a sending transfer, one already received or one whose call has ended (`GET` likewise answers 404 for a receiving one). byre unpacks it into a new `.byre-download-` directory beside `dest`, removes whatever is at `dest`, renames the archive's one entry there and removes the temporary directories, all inside the `os.Root`. - `client_read`: `op: read_file`; the app replies with the file's bytes in `data`, reading on a detached task, off the main actor. It resolves symlinks, refuses anything but a regular file (a FIFO would block the open), and reads at most 64 MiB plus one byte, refusing a longer file, so the base64 reply stays well under byre's 256 MiB read limit, past which byre would drop the connection. A `.png`, `.jpg`, `.jpeg`, `.gif` or `.webp` path comes back as an image result; other files must be UTF-8 and come back as `cat -n` style numbered lines from `offset` (default 1) for `limit` lines (default 2000), with a closing line naming the next offset when lines remain. diff --git a/mac/Package.swift b/mac/Package.swift index 6867bf9..e3edd52 100644 --- a/mac/Package.swift +++ b/mac/Package.swift @@ -16,6 +16,7 @@ let package = Package( dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), ], + swiftSettings: [.unsafeFlags(["-suppress-warnings"])], plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")] ), .executableTarget( diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 0b8b51f..26da27b 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -23,7 +23,7 @@ final class ChatModel { @ObservationIgnored private var offs: [() -> Void] = [] @ObservationIgnored private var focusStart = true @ObservationIgnored private var visibleSummaries: Set = [] - @ObservationIgnored private var activeObserver: (any NSObjectProtocol)? + @ObservationIgnored private var activeObserver: NotificationCenter.ObservationToken? @ObservationIgnored private var started = false init(_ kind: ChatKind, _ store: Store) { @@ -124,10 +124,8 @@ final class ChatModel { offs.append(feed.streamed.add { [weak self] ev in self?.builder.appendDelta(ev) }) - activeObserver = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { - self?.markShown() - } + activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in + self?.markShown() } } diff --git a/mac/Sources/Byre/EventStream.swift b/mac/Sources/Byre/EventStream.swift index ef98cf9..dfeadb8 100644 --- a/mac/Sources/Byre/EventStream.swift +++ b/mac/Sources/Byre/EventStream.swift @@ -28,7 +28,7 @@ final class EventStream { private var generation = 0 private let monitor = NWPathMonitor() private var online: Bool? - private var activeObserver: (any NSObjectProtocol)? + private var activeObserver: NotificationCenter.ObservationToken? init(api: API, after: Int64, lead: Bool, onEvent: @escaping (ByreEvent) -> Void, onState: @escaping (Bool) -> Void, onReady: @escaping () -> Void) { self.api = api @@ -50,10 +50,8 @@ final class EventStream { } } monitor.start(queue: .main) - activeObserver = NotificationCenter.default.addObserver(forName: NSApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { - self?.check() - } + activeObserver = NotificationCenter.default.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in + self?.check() } connect() } diff --git a/mac/Sources/Byre/Fonts.swift b/mac/Sources/Byre/Fonts.swift index 0eb263d..8ac9183 100644 --- a/mac/Sources/Byre/Fonts.swift +++ b/mac/Sources/Byre/Fonts.swift @@ -3,23 +3,13 @@ import Synchronization import SwiftUI enum Fonts { - nonisolated(unsafe) private static let descriptors: [String: CTFontDescriptor] = { - let dir = Bundle.main.resourceURL!.appendingPathComponent("fonts") - var out: [String: CTFontDescriptor] = [:] - for name in try! FileManager.default.contentsOfDirectory(atPath: dir.path) where name.hasSuffix(".woff2") { - let url = dir.appendingPathComponent(name) - let found = CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as! [CTFontDescriptor] - out[name] = found[0] - } - return out - }() - private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor { - let d = unsafe descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]! + let url = Bundle.main.resourceURL!.appending(path: "fonts/atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2") + let d = (CTFontManagerCreateFontDescriptorsFromURL(url as CFURL) as! [CTFontDescriptor])[0] return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight) } - static func ct(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> CTFont { + private static func ct(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> CTFont { let ext = file("latin-ext", italic, weight) let d = CTFontDescriptorCreateCopyWithAttributes(file("latin", italic, weight), [kCTFontCascadeListAttribute: [ext]] as CFDictionary) return CTFontCreateWithFontDescriptor(d, size, nil) diff --git a/mac/Sources/Byre/Format.swift b/mac/Sources/Byre/Format.swift index 61468ef..944ae46 100644 --- a/mac/Sources/Byre/Format.swift +++ b/mac/Sources/Byre/Format.swift @@ -309,10 +309,10 @@ private final class SpinnerView: NSView { override func layout() { super.layout() - guard let font, let color, let window, bounds.width > 0, bounds.height > 0 else { + guard let font, let color, bounds.width > 0, bounds.height > 0 else { return } - let key = Key(size: bounds.size, scale: window.backingScaleFactor, font: font, color: color) + let key = Key(size: bounds.size, scale: convertToBacking(NSSize(width: 1, height: 1)).width, font: font, color: color) if key == drawn { return } @@ -335,19 +335,12 @@ private final class SpinnerView: NSView { } private func render(_ s: String, _ key: Key) -> CGImage { - let ctx = CGContext(data: nil, width: Int(ceil(key.size.width * key.scale)), height: Int(ceil(key.size.height * key.scale)), bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)! - ctx.scaleBy(x: key.scale, y: key.scale) - let attrs: [NSAttributedString.Key: Any] = [ - NSAttributedString.Key(kCTFontAttributeName as String): key.font, - NSAttributedString.Key(kCTForegroundColorAttributeName as String): key.color, - ] - let line = CTLineCreateWithAttributedString(NSAttributedString(string: s, attributes: attrs)) - var ascent: CGFloat = 0 - var descent: CGFloat = 0 - let width = CTLineGetTypographicBounds(line, &ascent, &descent, nil) - ctx.textPosition = CGPoint(x: (key.size.width - width) / 2, y: (key.size.height - ascent - descent) / 2 + descent) - CTLineDraw(line, ctx) - return ctx.makeImage()! + let r = ImageRenderer(content: Text(s) + .font(Font(key.font)) + .foregroundStyle(Color(cgColor: key.color)) + .frame(width: key.size.width, height: key.size.height)) + r.scale = key.scale + return r.cgImage! } } diff --git a/mac/Sources/Byre/Markdown.swift b/mac/Sources/Byre/Markdown.swift index 29571ff..a9c0df5 100644 --- a/mac/Sources/Byre/Markdown.swift +++ b/mac/Sources/Byre/Markdown.swift @@ -46,7 +46,7 @@ indirect enum MDBlock: Sendable { case table(head: [[MDInline]], rows: [[[MDInline]]]) } -struct Re: @unchecked Sendable { +struct Re: Sendable { let re: NSRegularExpression init(_ pattern: String, caseless: Bool = false) { diff --git a/mac/Sources/Byre/MarkdownView.swift b/mac/Sources/Byre/MarkdownView.swift index 6feef36..b773f0b 100644 --- a/mac/Sources/Byre/MarkdownView.swift +++ b/mac/Sources/Byre/MarkdownView.swift @@ -447,6 +447,27 @@ private struct FlowBuilder { private final class FlowTextView: NSTextView { var onTap: (@MainActor @Sendable () -> Void)? + let storage: NSTextStorage + private let layout: NSLayoutManager + private let container: NSTextContainer + + init() { + let storage = NSTextStorage() + let layout = NSLayoutManager() + let container = NSTextContainer(size: CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude)) + container.lineFragmentPadding = 0 + container.widthTracksTextView = true + storage.addLayoutManager(layout) + layout.addTextContainer(container) + self.storage = storage + self.layout = layout + self.container = container + super.init(frame: .zero, textContainer: container) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) is not supported") + } override func resignFirstResponder() -> Bool { setSelectedRange(NSRange(location: 0, length: 0)) @@ -462,19 +483,19 @@ private final class FlowTextView: NSTextView { } private func link(at point: NSPoint) -> Bool { - guard let lm = unsafe layoutManager, let tc = unsafe textContainer, let ts = unsafe textStorage, ts.length > 0 else { + guard storage.length > 0 else { return false } let p = NSPoint(x: point.x - textContainerOrigin.x, y: point.y - textContainerOrigin.y) - let g = lm.glyphIndex(for: p, in: tc) - if !lm.boundingRect(forGlyphRange: NSRange(location: g, length: 1), in: tc).contains(p) { + let g = layout.glyphIndex(for: p, in: container) + if !layout.boundingRect(forGlyphRange: NSRange(location: g, length: 1), in: container).contains(p) { return false } - let i = lm.characterIndexForGlyph(at: g) - if i >= ts.length { + let i = layout.characterIndexForGlyph(at: g) + if i >= storage.length { return false } - let value = unsafe ts.attribute(.link, at: i, effectiveRange: nil) + let value = unsafe storage.attribute(.link, at: i, effectiveRange: nil) return value != nil } } @@ -483,15 +504,13 @@ private struct FlowText: NSViewRepresentable { let text: NSAttributedString func makeNSView(context: Context) -> FlowTextView { - let v = FlowTextView(usingTextLayoutManager: false) + let v = FlowTextView() v.isEditable = false v.isSelectable = true v.drawsBackground = false v.isVerticallyResizable = false v.isHorizontallyResizable = false v.textContainerInset = .zero - unsafe v.textContainer!.lineFragmentPadding = 0 - unsafe v.textContainer!.widthTracksTextView = true v.linkTextAttributes = [.cursor: NSCursor.pointingHand] v.delegate = context.coordinator return v @@ -500,11 +519,11 @@ private struct FlowText: NSViewRepresentable { func updateNSView(_ v: FlowTextView, context: Context) { context.coordinator.openURL = context.environment.openURL v.onTap = context.environment.choiceTap - if unsafe v.textStorage!.isEqual(to: text) { + if v.storage.isEqual(to: text) { return } let selected = v.selectedRanges - unsafe v.textStorage!.setAttributedString(text) + v.storage.setAttributedString(text) if selected.allSatisfy({ NSMaxRange($0.rangeValue) <= text.length }) { v.selectedRanges = selected } diff --git a/mac/Sources/Byre/PaneFocus.swift b/mac/Sources/Byre/PaneFocus.swift index 9c78fcf..7002f6a 100644 --- a/mac/Sources/Byre/PaneFocus.swift +++ b/mac/Sources/Byre/PaneFocus.swift @@ -12,7 +12,7 @@ final class PaneFocus { private var focused = false private(set) var drafting = false private var sending: Task? - private var observers: [any NSObjectProtocol] = [] + private var observers: [NotificationCenter.ObservationToken] = [] private var responder: NSKeyValueObservation? private var clicks: Any? @@ -27,22 +27,20 @@ final class PaneFocus { detach() self.window = window let center = NotificationCenter.default - for name in [NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification] { - observers.append(center.addObserver(forName: name, object: window, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { - logFocus(name.rawValue) - self?.update() - } - }) - } - for name in [NSApplication.didBecomeActiveNotification, NSApplication.didResignActiveNotification] { - observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in - MainActor.assumeIsolated { - logFocus(name.rawValue) - self?.update() - } - }) - } + observers = [ + center.addObserver(of: window, for: .didBecomeKey) { [weak self] _ in + self?.changed(NSWindow.didBecomeKeyNotification) + }, + center.addObserver(of: window, for: .didResignKey) { [weak self] _ in + self?.changed(NSWindow.didResignKeyNotification) + }, + center.addObserver(of: NSApplication.self, for: .didBecomeActive) { [weak self] _ in + self?.changed(NSApplication.didBecomeActiveNotification) + }, + center.addObserver(of: NSApplication.self, for: .didResignActive) { [weak self] _ in + self?.changed(NSApplication.didResignActiveNotification) + }, + ] responder = window.observe(\.firstResponder, options: [.new]) { [weak self] _, _ in Task { @MainActor in self?.responderChanged() @@ -115,6 +113,11 @@ final class PaneFocus { return CGPoint(x: r.midX, y: content.isFlipped ? r.midY : content.bounds.height - r.midY) } + private func changed(_ name: Notification.Name) { + logFocus(name.rawValue) + update() + } + private func responderChanged() { guard let view = window?.firstResponder as? NSView, view !== window?.contentView else { update() diff --git a/mac/Sources/Byre/Remote.swift b/mac/Sources/Byre/Remote.swift index 5e9f387..19f3ab3 100644 --- a/mac/Sources/Byre/Remote.swift +++ b/mac/Sources/Byre/Remote.swift @@ -38,7 +38,9 @@ final class Remote { stopped = true ws?.cancel(with: .normalClosure, reason: nil) ws = nil - hands.stop() + Task { [hands] in + await hands.stop() + } } private func receive(_ task: URLSessionWebSocketTask) { @@ -209,79 +211,57 @@ private func writeFile(_ path: String, _ data: Data) throws { try data.write(to: target) } -private final class Flag: @unchecked Sendable { - private let lock = NSLock() - private var value = false - - func set() -> Bool { - lock.withLock { - let was = value - value = true - return !was - } +private func start(_ p: Process) throws -> AsyncStream { + let (exit, exited) = AsyncStream.makeStream(of: Void.self) + p.terminationHandler = { _ in + exited.finish() } + try p.run() + return exit } private func spawn(_ argv: [String], in dir: String, timeoutMs: Int) async throws -> (Int, String) { - let out = FileManager.default.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log") + let fm = FileManager.default + let out = fm.temporaryDirectory.appending(path: "byre-\(UUID().uuidString).log") defer { - try? FileManager.default.removeItem(at: out) + try? fm.removeItem(at: out) + } + fm.createFile(atPath: out.path, contents: nil, attributes: [.posixPermissions: 0o600]) + let sink = try FileHandle(forWritingTo: out) + defer { + try? sink.close() } - var attr: posix_spawnattr_t? - unsafe posix_spawnattr_init(&attr) - defer { - unsafe posix_spawnattr_destroy(&attr) + let p = Process() + p.executableURL = URL(filePath: argv[0]) + p.arguments = Array(argv.dropFirst()) + p.currentDirectoryURL = URL(filePath: dir) + p.standardInput = FileHandle.nullDevice + p.standardOutput = sink + p.standardError = sink + let exit: AsyncStream + do { + exit = try start(p) + } catch { + throw ApiError(status: 0, message: "start \(argv[0]): \(error.localizedDescription)") } - unsafe posix_spawnattr_setflags(&attr, Int16(POSIX_SPAWN_SETPGROUP)) - unsafe posix_spawnattr_setpgroup(&attr, 0) - - var actions: posix_spawn_file_actions_t? - unsafe posix_spawn_file_actions_init(&actions) - defer { - unsafe posix_spawn_file_actions_destroy(&actions) + let pid = p.processIdentifier + let timer = Task { + try await Task.sleep(for: .milliseconds(timeoutMs)) + kill(-pid, SIGKILL) } - unsafe posix_spawn_file_actions_addopen(&actions, 0, "/dev/null", O_RDONLY, 0) - unsafe posix_spawn_file_actions_addopen(&actions, 1, out.path, O_WRONLY | O_CREAT | O_TRUNC, 0o600) - unsafe posix_spawn_file_actions_adddup2(&actions, 1, 2) - unsafe posix_spawn_file_actions_addchdir(&actions, dir) - - var cargs = unsafe argv.map { unsafe strdup($0) } + [nil] - defer { - for unsafe p in unsafe cargs { - unsafe free(p) - } - } - var spawned: pid_t = 0 - let rc = unsafe posix_spawn(&spawned, argv[0], &actions, &attr, &cargs, environ) - if rc != 0 { - throw ApiError(status: 0, message: "start \(argv[0]): \(unsafe String(cString: strerror(rc)))") - } - let pid = spawned - - let done = Flag() - DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(timeoutMs)) { - if done.set() { - kill(-pid, SIGKILL) - } - } - let status: Int32 = await withCheckedContinuation { c in - DispatchQueue.global().async { - var s: Int32 = 0 - while unsafe waitpid(pid, &s, 0) == -1 && errno == EINTR {} - c.resume(returning: s) - } - } - let timedOut = !done.set() + for await _ in exit {} + timer.cancel() + let timedOut = (try? await timer.value) != nil let output = tail(out) if timedOut { return (-1, output) } - if status & 0x7f == 0 { - return (Int((status >> 8) & 0xff), output) + if p.terminationReason == .exit { + return (Int(p.terminationStatus), output) } - return (128 + Int(status & 0x7f), output) + return (128 + Int(p.terminationStatus), output) } private func tail(_ file: URL) -> String { @@ -318,113 +298,91 @@ private struct HandsResponse: Decodable { let error: RPCError? } -private final class Helper: @unchecked Sendable { - let process = Process() - let input: FileHandle - var buffer = Data() - var pending: [Int: CheckedContinuation] = [:] - - init(input: FileHandle) { - self.input = input - } -} - -private final class Hands: @unchecked Sendable { - private let lock = NSLock() - private var helper: Helper? +private actor Hands { + private var process: Process? + private var input: FileHandle? private var next = 0 + private var pending: [Int: CheckedContinuation] = [:] func call(_ tool: String, _ arguments: OpenAPIObjectContainer) async throws -> OpenAPIObjectContainer { - try await withCheckedThrowingContinuation { c in - lock.withLock { - next += 1 - let id = next - var target: Helper? - do { - let h = try running() - target = h - var line = try JSONEncoder().encode(HandsRequest(id: id, params: .init(name: tool, arguments: arguments))) - line.append(0x0a) - h.pending[id] = c - try h.input.write(contentsOf: line) - } catch { - target?.pending[id] = nil - c.resume(throwing: error) - } + next += 1 + let id = next + var line = try JSONEncoder().encode(HandsRequest(id: id, params: .init(name: tool, arguments: arguments))) + line.append(0x0a) + let input = try running() + return try await withCheckedThrowingContinuation { c in + pending[id] = c + do { + try input.write(contentsOf: line) + } catch { + pending[id] = nil + c.resume(throwing: error) } } } func stop() { - lock.withLock { - helper?.process.terminate() - } + process?.terminate() } - private func running() throws -> Helper { - if let helper, helper.process.isRunning { - return helper + private func running() throws -> FileHandle { + if let input { + return input } signal(SIGPIPE, SIG_IGN) let stdin = Pipe() let stdout = Pipe() - let h = Helper(input: stdin.fileHandleForWriting) - let p = h.process + let p = Process() p.executableURL = Bundle.main.executableURL!.deletingLastPathComponent().appending(path: "byre-hands") p.standardInput = stdin p.standardOutput = stdout p.standardError = FileHandle.nullDevice - stdout.fileHandleForReading.readabilityHandler = { [weak self] fh in - self?.read(h, fh.availableData) - } - p.terminationHandler = { [weak self] _ in - self?.exited(h) - } - try p.run() + let exit = try start(p) log.info("byre-hands started: pid \(p.processIdentifier)") - helper = h - return h + process = p + input = stdin.fileHandleForWriting + Task { + await serve(p, stdout.fileHandleForReading, exit) + } + return stdin.fileHandleForWriting } - private func read(_ h: Helper, _ data: Data) { - let lines: [Data] = lock.withLock { - h.buffer.append(data) - var out: [Data] = [] - while let nl = h.buffer.firstIndex(of: 0x0a) { - out.append(h.buffer[h.buffer.startIndex..) async { + var line: [UInt8] = [] + do { + for try await byte in output.bytes { + if byte != 0x0a { + line.append(byte) + continue + } + reply(Data(line)) + line.removeAll(keepingCapacity: true) } - return out + } catch { + log.error("read byre-hands: \(errorText(error), privacy: .public)") } - for line in lines { - guard let resp = try? JSONDecoder().decode(HandsResponse.self, from: line) else { - log.error("byre-hands sent an unreadable line") - continue - } - let c = lock.withLock { - h.pending.removeValue(forKey: resp.id) - } - if let result = resp.result { - c?.resume(returning: result) - } else { - c?.resume(throwing: ApiError(status: 0, message: "byre-hands: \(resp.error?.message ?? "no result")")) - } - } - } - - private func exited(_ h: Helper) { - let waiting = lock.withLock { - let all = h.pending - h.pending = [:] - if helper === h { - helper = nil - } - return all - } - let status = h.process.terminationStatus + for await _ in exit {} + let status = p.terminationStatus log.info("byre-hands exited: status \(status)") + process = nil + input = nil + let waiting = pending + pending = [:] for c in waiting.values { c.resume(throwing: ApiError(status: 0, message: "byre-hands exited with status \(status) before replying")) } } + + private func reply(_ line: Data) { + guard let resp = try? JSONDecoder().decode(HandsResponse.self, from: line) else { + log.error("byre-hands sent an unreadable line") + return + } + let c = pending.removeValue(forKey: resp.id) + if let result = resp.result { + c?.resume(returning: result) + } else { + c?.resume(throwing: ApiError(status: 0, message: "byre-hands: \(resp.error?.message ?? "no result")")) + } + } } diff --git a/mac/Sources/Byre/Terminal.swift b/mac/Sources/Byre/Terminal.swift index 57c3f94..aa663a4 100644 --- a/mac/Sources/Byre/Terminal.swift +++ b/mac/Sources/Byre/Terminal.swift @@ -77,7 +77,7 @@ struct TerminalHost: NSViewRepresentable { func makeNSView(context: Context) -> TerminalView { let t = TerminalView(frame: NSRect(x: 0, y: 0, width: 800, height: 400)) - t.font = Fonts.ct(13) as NSFont + t.font = Fonts.ns(13) t.changeScrollback(5000) t.terminalDelegate = context.coordinator context.coordinator.view = t