feat(mac): add tailscale settings
This commit is contained in:
150
apps/macos/Sources/Clawdis/TailscaleService.swift
Normal file
150
apps/macos/Sources/Clawdis/TailscaleService.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import Observation
|
||||
import os
|
||||
|
||||
/// Manages Tailscale integration and status checking.
|
||||
@Observable
|
||||
@MainActor
|
||||
final class TailscaleService {
|
||||
static let shared = TailscaleService()
|
||||
|
||||
/// Tailscale local API endpoint.
|
||||
private static let tailscaleAPIEndpoint = "http://100.100.100.100/api/data"
|
||||
|
||||
/// API request timeout in seconds.
|
||||
private static let apiTimeoutInterval: TimeInterval = 5.0
|
||||
|
||||
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "tailscale")
|
||||
|
||||
/// Indicates if the Tailscale app is installed on the system.
|
||||
private(set) var isInstalled = false
|
||||
|
||||
/// Indicates if Tailscale is currently running.
|
||||
private(set) var isRunning = false
|
||||
|
||||
/// The Tailscale hostname for this device (e.g., "my-mac.tailnet.ts.net").
|
||||
private(set) var tailscaleHostname: String?
|
||||
|
||||
/// The Tailscale IPv4 address for this device.
|
||||
private(set) var tailscaleIP: String?
|
||||
|
||||
/// Error message if status check fails.
|
||||
private(set) var statusError: String?
|
||||
|
||||
private init() {
|
||||
Task { await self.checkTailscaleStatus() }
|
||||
}
|
||||
|
||||
func checkAppInstallation() -> Bool {
|
||||
let installed = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app")
|
||||
self.logger.info("Tailscale app installed: \(installed)")
|
||||
return installed
|
||||
}
|
||||
|
||||
private struct TailscaleAPIResponse: Codable {
|
||||
let status: String
|
||||
let deviceName: String
|
||||
let tailnetName: String
|
||||
let iPv4: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case status = "Status"
|
||||
case deviceName = "DeviceName"
|
||||
case tailnetName = "TailnetName"
|
||||
case iPv4 = "IPv4"
|
||||
}
|
||||
}
|
||||
|
||||
private func fetchTailscaleStatus() async -> TailscaleAPIResponse? {
|
||||
guard let url = URL(string: Self.tailscaleAPIEndpoint) else {
|
||||
self.logger.error("Invalid Tailscale API URL")
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.timeoutIntervalForRequest = Self.apiTimeoutInterval
|
||||
let session = URLSession(configuration: configuration)
|
||||
|
||||
let (data, response) = try await session.data(from: url)
|
||||
guard let httpResponse = response as? HTTPURLResponse,
|
||||
httpResponse.statusCode == 200
|
||||
else {
|
||||
self.logger.warning("Tailscale API returned non-200 status")
|
||||
return nil
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
return try decoder.decode(TailscaleAPIResponse.self, from: data)
|
||||
} catch {
|
||||
self.logger.debug("Failed to fetch Tailscale status: \(String(describing: error))")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func checkTailscaleStatus() async {
|
||||
self.isInstalled = self.checkAppInstallation()
|
||||
guard self.isInstalled else {
|
||||
self.isRunning = false
|
||||
self.tailscaleHostname = nil
|
||||
self.tailscaleIP = nil
|
||||
self.statusError = "Tailscale is not installed"
|
||||
return
|
||||
}
|
||||
|
||||
if let apiResponse = await fetchTailscaleStatus() {
|
||||
self.isRunning = apiResponse.status.lowercased() == "running"
|
||||
|
||||
if self.isRunning {
|
||||
let deviceName = apiResponse.deviceName
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: " ", with: "-")
|
||||
let tailnetName = apiResponse.tailnetName
|
||||
.replacingOccurrences(of: ".ts.net", with: "")
|
||||
.replacingOccurrences(of: ".tailscale.net", with: "")
|
||||
|
||||
self.tailscaleHostname = "\(deviceName).\(tailnetName).ts.net"
|
||||
self.tailscaleIP = apiResponse.iPv4
|
||||
self.statusError = nil
|
||||
|
||||
self.logger.info(
|
||||
"Tailscale running host=\(self.tailscaleHostname ?? "nil") ip=\(self.tailscaleIP ?? "nil")")
|
||||
} else {
|
||||
self.tailscaleHostname = nil
|
||||
self.tailscaleIP = nil
|
||||
self.statusError = "Tailscale is not running"
|
||||
}
|
||||
} else {
|
||||
self.isRunning = false
|
||||
self.tailscaleHostname = nil
|
||||
self.tailscaleIP = nil
|
||||
self.statusError = "Please start the Tailscale app"
|
||||
self.logger.info("Tailscale API not responding; app likely not running")
|
||||
}
|
||||
}
|
||||
|
||||
func openTailscaleApp() {
|
||||
if let url = URL(string: "file:///Applications/Tailscale.app") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func openAppStore() {
|
||||
if let url = URL(string: "https://apps.apple.com/us/app/tailscale/id1475387142") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func openDownloadPage() {
|
||||
if let url = URL(string: "https://tailscale.com/download/macos") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
func openSetupGuide() {
|
||||
if let url = URL(string: "https://tailscale.com/kb/1017/install/") {
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user