vibetunnel/VibeTunnel/Core/Services/SessionMonitor.swift
Peter Steinberger 722402d116 Set up comprehensive CI workflows for Swift, Rust, and Node.js
- Create separate workflow files for each language
- Swift: macOS-15 runner with SwiftLint/SwiftFormat, build, and test
- Rust: Multi-platform build with rustfmt/clippy and coverage
- Node.js: TypeScript linting, build, test, and security audit
- Update main CI workflow to use the new language-specific workflows
- Remove old lint.yml workflow (integrated into language workflows)
- Apply code formatting to ensure CI passes
2025-06-17 01:33:48 +02:00

153 lines
4.9 KiB
Swift

import Foundation
import Observation
/// Monitors tty-fwd sessions and provides real-time session count.
///
/// `SessionMonitor` is a singleton that periodically polls the local server to track active terminal sessions.
/// It maintains a count of running sessions and provides detailed information about each session.
/// The monitor automatically starts and stops based on server lifecycle events.
@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
/// Information about a terminal session.
///
/// Contains detailed metadata about a tty-fwd session including its process information,
/// status, and I/O stream paths.
struct SessionInfo: Codable {
let cmdline: [String]
let cwd: String
let exitCode: Int?
let name: String
let pid: Int
let startedAt: String
let status: String
let stdin: String
let streamOut: String
enum CodingKeys: String, CodingKey {
case cmdline
case cwd
case name
case pid
case status
case stdin
case exitCode = "exit_code"
case startedAt = "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 : 4_020
}
func startMonitoring() {
stopMonitoring()
// Update port from UserDefaults in case it changed
let port = UserDefaults.standard.integer(forKey: "serverPort")
self.serverPort = port > 0 ? port : 4_020
// 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
guard let healthURL = URL(string: "http://127.0.0.1:\(serverPort)/api/health") else {
self.sessions = [:]
self.sessionCount = 0
self.lastError = nil
return
}
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
guard let url = URL(string: "http://127.0.0.1:\(serverPort)/sessions") else {
self.lastError = "Invalid URL"
return
}
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.count { $0.isRunning }
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()
}
}