feat(mac): reuse running gateway
This commit is contained in:
@@ -196,7 +196,7 @@ struct CritterStatusLabel: View {
|
|||||||
switch self.gatewayStatus {
|
switch self.gatewayStatus {
|
||||||
case .failed, .stopped:
|
case .failed, .stopped:
|
||||||
!self.isPaused
|
!self.isPaused
|
||||||
case .starting, .restarting, .running:
|
case .starting, .restarting, .running, .attachedExisting:
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,21 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
case starting
|
case starting
|
||||||
case running(pid: Int32)
|
case running(pid: Int32)
|
||||||
case restarting
|
case restarting
|
||||||
|
case attachedExisting(details: String?)
|
||||||
case failed(String)
|
case failed(String)
|
||||||
|
|
||||||
var label: String {
|
var label: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .stopped: "Stopped"
|
case .stopped: return "Stopped"
|
||||||
case .starting: "Starting…"
|
case .starting: return "Starting…"
|
||||||
case let .running(pid): "Running (pid \(pid))"
|
case let .running(pid): return "Running (pid \(pid))"
|
||||||
case .restarting: "Restarting…"
|
case .restarting: return "Restarting…"
|
||||||
case let .failed(reason): "Failed: \(reason)"
|
case let .attachedExisting(details):
|
||||||
|
if let details, !details.isEmpty {
|
||||||
|
return "Using existing gateway (\(details))"
|
||||||
|
}
|
||||||
|
return "Using existing gateway"
|
||||||
|
case let .failed(reason): return "Failed: \(reason)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,6 +42,7 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
@Published private(set) var log: String = ""
|
@Published private(set) var log: String = ""
|
||||||
@Published private(set) var restartCount: Int = 0
|
@Published private(set) var restartCount: Int = 0
|
||||||
@Published private(set) var environmentStatus: GatewayEnvironmentStatus = .checking
|
@Published private(set) var environmentStatus: GatewayEnvironmentStatus = .checking
|
||||||
|
@Published private(set) var existingGatewayDetails: String?
|
||||||
|
|
||||||
private var execution: Execution?
|
private var execution: Execution?
|
||||||
private var desiredActive = false
|
private var desiredActive = false
|
||||||
@@ -63,9 +70,17 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
self.status = .failed("Too many crashes; giving up")
|
self.status = .failed("Too many crashes; giving up")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.status = self.status == .restarting ? .restarting : .starting
|
|
||||||
Task.detached { [weak self] in
|
if self.status != .restarting {
|
||||||
|
self.status = .starting
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to latch onto an already-running gateway to avoid spawning a duplicate.
|
||||||
|
Task { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
if await self.attachExistingGatewayIfAvailable() {
|
||||||
|
return
|
||||||
|
}
|
||||||
await self.spawnGateway()
|
await self.spawnGateway()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +88,7 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
func stop() {
|
func stop() {
|
||||||
self.desiredActive = false
|
self.desiredActive = false
|
||||||
self.stopping = true
|
self.stopping = true
|
||||||
|
self.existingGatewayDetails = nil
|
||||||
guard let execution else {
|
guard let execution else {
|
||||||
self.status = .stopped
|
self.status = .stopped
|
||||||
return
|
return
|
||||||
@@ -90,7 +106,40 @@ final class GatewayProcessManager: ObservableObject {
|
|||||||
|
|
||||||
// MARK: - Internals
|
// MARK: - Internals
|
||||||
|
|
||||||
|
/// Attempt to connect to an already-running gateway on the configured port.
|
||||||
|
/// If successful, mark status as attached and skip spawning a new process.
|
||||||
|
private func attachExistingGatewayIfAvailable() async -> Bool {
|
||||||
|
let port = GatewayEnvironment.gatewayPort()
|
||||||
|
guard let url = URL(string: "ws://127.0.0.1:\(port)") else { return false }
|
||||||
|
let token = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"]
|
||||||
|
let channel = GatewayChannel()
|
||||||
|
await channel.configure(url: url, token: token)
|
||||||
|
do {
|
||||||
|
let data = try await channel.request(method: "health", params: nil)
|
||||||
|
let details: String
|
||||||
|
if let snap = decodeHealthSnapshot(from: data) {
|
||||||
|
let linked = snap.web.linked ? "linked" : "not linked"
|
||||||
|
let authAge = snap.web.authAgeMs.flatMap(msToAge) ?? "unknown age"
|
||||||
|
details = "port \(port), \(linked), auth \(authAge)"
|
||||||
|
} else {
|
||||||
|
details = "port \(port), health probe succeeded"
|
||||||
|
}
|
||||||
|
self.existingGatewayDetails = details
|
||||||
|
self.status = .attachedExisting(details: details)
|
||||||
|
self.appendLog("[gateway] using existing instance: \(details)\n")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// No reachable gateway (or token mismatch) — fall through to spawn.
|
||||||
|
self.existingGatewayDetails = nil
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func spawnGateway() async {
|
private func spawnGateway() async {
|
||||||
|
if self.status != .restarting {
|
||||||
|
self.status = .starting
|
||||||
|
}
|
||||||
|
self.existingGatewayDetails = nil
|
||||||
let resolution = GatewayEnvironment.resolveGatewayCommand()
|
let resolution = GatewayEnvironment.resolveGatewayCommand()
|
||||||
await MainActor.run { self.environmentStatus = resolution.status }
|
await MainActor.run { self.environmentStatus = resolution.status }
|
||||||
guard let command = resolution.command else {
|
guard let command = resolution.command else {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SwiftUI
|
|||||||
struct GeneralSettings: View {
|
struct GeneralSettings: View {
|
||||||
@ObservedObject var state: AppState
|
@ObservedObject var state: AppState
|
||||||
@ObservedObject private var healthStore = HealthStore.shared
|
@ObservedObject private var healthStore = HealthStore.shared
|
||||||
|
@ObservedObject private var gatewayManager = GatewayProcessManager.shared
|
||||||
@State private var isInstallingCLI = false
|
@State private var isInstallingCLI = false
|
||||||
@State private var cliStatus: String?
|
@State private var cliStatus: String?
|
||||||
@State private var cliInstalled = false
|
@State private var cliInstalled = false
|
||||||
@@ -278,6 +279,12 @@ struct GeneralSettings: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case let .attachedExisting(details) = self.gatewayManager.status {
|
||||||
|
Text(details ?? "Using existing gateway instance")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Button {
|
Button {
|
||||||
Task { await self.installGateway() }
|
Task { await self.installGateway() }
|
||||||
|
|||||||
Reference in New Issue
Block a user