mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
- Replace backtick syntax for hyphenated property names with CodingKeys enum - Change `stream-out` to streamOut with proper CodingKeys mapping - Ensures compatibility with older Swift versions that don't support backtick property names
138 lines
No EOL
4.3 KiB
Swift
138 lines
No EOL
4.3 KiB
Swift
//
|
|
// SessionMonitor.swift
|
|
// VibeTunnel
|
|
//
|
|
// Created by Assistant on 6/16/25.
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
|
|
/// Monitors tty-fwd sessions and provides real-time session count
|
|
@MainActor
|
|
@Observable
|
|
class SessionMonitor {
|
|
static let shared = SessionMonitor()
|
|
|
|
var sessionCount: Int = 0
|
|
var sessions: [String: SessionInfo] = [:]
|
|
var lastError: String?
|
|
|
|
private var monitoringTask: Task<Void, Never>?
|
|
private let refreshInterval: TimeInterval = 5.0 // Check every 5 seconds
|
|
private var serverPort: Int
|
|
|
|
struct SessionInfo: Codable {
|
|
let cmdline: [String]
|
|
let cwd: String
|
|
let exit_code: Int?
|
|
let name: String
|
|
let pid: Int
|
|
let started_at: String
|
|
let status: String
|
|
let stdin: String
|
|
let streamOut: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case cmdline, cwd, name, pid, status, stdin
|
|
case exit_code = "exit_code"
|
|
case started_at = "started_at"
|
|
case streamOut = "stream-out"
|
|
}
|
|
|
|
var isRunning: Bool {
|
|
status == "running"
|
|
}
|
|
}
|
|
|
|
private init() {
|
|
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
|
self.serverPort = port > 0 ? port : 4020
|
|
}
|
|
|
|
func startMonitoring() {
|
|
stopMonitoring()
|
|
|
|
// Update port from UserDefaults in case it changed
|
|
let port = UserDefaults.standard.integer(forKey: "serverPort")
|
|
self.serverPort = port > 0 ? port : 4020
|
|
|
|
// Start monitoring task
|
|
monitoringTask = Task {
|
|
// Initial fetch
|
|
await fetchSessions()
|
|
|
|
// Set up periodic fetching
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(for: .seconds(refreshInterval))
|
|
if !Task.isCancelled {
|
|
await fetchSessions()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stopMonitoring() {
|
|
monitoringTask?.cancel()
|
|
monitoringTask = nil
|
|
}
|
|
|
|
@MainActor
|
|
private func fetchSessions() async {
|
|
do {
|
|
// First check if server is running
|
|
let healthURL = URL(string: "http://127.0.0.1:\(serverPort)/health")!
|
|
let healthRequest = URLRequest(url: healthURL, timeoutInterval: 2.0)
|
|
|
|
do {
|
|
let (_, healthResponse) = try await URLSession.shared.data(for: healthRequest)
|
|
guard let httpResponse = healthResponse as? HTTPURLResponse,
|
|
httpResponse.statusCode == 200 else {
|
|
// Server not running
|
|
self.sessions = [:]
|
|
self.sessionCount = 0
|
|
self.lastError = nil
|
|
return
|
|
}
|
|
} catch {
|
|
// Server not reachable
|
|
self.sessions = [:]
|
|
self.sessionCount = 0
|
|
self.lastError = nil
|
|
return
|
|
}
|
|
|
|
// Server is running, fetch sessions
|
|
let url = URL(string: "http://127.0.0.1:\(serverPort)/sessions")!
|
|
let request = URLRequest(url: url, timeoutInterval: 5.0)
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse,
|
|
httpResponse.statusCode == 200 else {
|
|
self.lastError = "Failed to fetch sessions"
|
|
return
|
|
}
|
|
|
|
// Parse JSON response
|
|
let sessionsData = try JSONDecoder().decode([String: SessionInfo].self, from: data)
|
|
self.sessions = sessionsData
|
|
|
|
// Count only running sessions
|
|
self.sessionCount = sessionsData.values.filter { $0.isRunning }.count
|
|
self.lastError = nil
|
|
|
|
} catch {
|
|
// Don't set error for connection issues when server is likely not running
|
|
if !(error is URLError) {
|
|
self.lastError = "Error fetching sessions: \(error.localizedDescription)"
|
|
}
|
|
// Clear sessions on error
|
|
self.sessions = [:]
|
|
self.sessionCount = 0
|
|
}
|
|
}
|
|
|
|
func refreshNow() async {
|
|
await fetchSessions()
|
|
}
|
|
} |