Ensure server restarts on password change.

This commit is contained in:
Peter Steinberger 2025-06-19 18:15:44 +02:00
parent 99392b53a4
commit 2c276fc67c
3 changed files with 110 additions and 36 deletions

View file

@ -32,7 +32,15 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware where
}
// Check if password protection is enabled
guard UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled") else {
let passwordEnabled = UserDefaults.standard.bool(forKey: "dashboardPasswordEnabled")
// Check if enabled state changed and clear cache if needed
if await passwordCache.shouldRecache(currentEnabledState: passwordEnabled) {
await passwordCache.clear()
logger.info("Password enabled state changed, cleared cache")
}
guard passwordEnabled else {
// No password protection, allow request
return try await next(request, context)
}
@ -112,6 +120,7 @@ struct LazyBasicAuthMiddleware<Context: RequestContext>: RouterMiddleware where
/// Actor to manage password caching in a thread-safe way
private actor PasswordCache {
private var cachedPassword: String?
private var lastEnabledState: Bool?
func getPassword() -> String? {
cachedPassword
@ -123,5 +132,14 @@ private actor PasswordCache {
func clear() {
cachedPassword = nil
lastEnabledState = nil
}
func shouldRecache(currentEnabledState: Bool) -> Bool {
if lastEnabledState != currentEnabledState {
lastEnabledState = currentEnabledState
return true
}
return false
}
}

View file

@ -143,7 +143,7 @@ final class RustServer: ServerProtocol {
if let fileSize = attributes[.size] as? NSNumber {
logger.info("tty-fwd binary size: \(fileSize.intValue) bytes")
}
// Log binary architecture info
logContinuation?.yield(ServerLogEntry(
level: .debug,
@ -175,7 +175,7 @@ final class RustServer: ServerProtocol {
let webPublicPath = URL(fileURLWithPath: resourcesPath).appendingPathComponent("web/public")
let webPublicExists = FileManager.default.fileExists(atPath: webPublicPath.path)
logger.info("Web public directory at \(webPublicPath.path) exists: \(webPublicExists)")
if !webPublicExists {
logger.error("Web public directory NOT FOUND at: \(webPublicPath.path)")
logContinuation?.yield(ServerLogEntry(
@ -242,21 +242,21 @@ final class RustServer: ServerProtocol {
do {
// Start the process (this just launches it and returns immediately)
try await processHandler.runProcess(process)
// Mark server as running
isRunning = true
logger.info("Rust server process started")
// Give the process a moment to start before checking for early failures
try await Task.sleep(for: .milliseconds(100))
// Check if process exited immediately (indicating failure)
if !process.isRunning {
isRunning = false
let exitCode = process.terminationStatus
logger.error("Process exited immediately with code: \(exitCode)")
// Try to read any error output
var errorDetails = "Exit code: \(exitCode)"
if let stderrPipe = self.stderrPipe {
@ -265,16 +265,16 @@ final class RustServer: ServerProtocol {
errorDetails += "\nError: \(errorOutput.trimmingCharacters(in: .whitespacesAndNewlines))"
}
}
logContinuation?.yield(ServerLogEntry(
level: .error,
message: "Server failed to start: \(errorDetails)",
source: .rust
))
throw RustServerError.processFailedToStart
}
logger.info("Rust server process started, performing health check...")
logContinuation?.yield(ServerLogEntry(level: .info, message: "Performing health check...", source: .rust))
@ -318,7 +318,7 @@ final class RustServer: ServerProtocol {
}
} catch {
isRunning = false
// Log more detailed error information
let errorMessage: String
if let rustError = error as? RustServerError {
@ -331,7 +331,7 @@ final class RustServer: ServerProtocol {
} else {
errorMessage = String(describing: error)
}
logger.error("Failed to start Rust server: \(errorMessage)")
logContinuation?.yield(ServerLogEntry(
level: .error,

View file

@ -43,6 +43,34 @@ struct DashboardSettingsView: View {
DashboardAccessMode(rawValue: accessModeString) ?? .localhost
}
// MARK: - Helper Methods
/// Handles server-specific password updates (adding, changing, or removing passwords)
static func updateServerForPasswordChange(action: PasswordAction, logger: Logger) async {
let serverManager = ServerManager.shared
if serverManager.serverMode == .rust {
// Rust server requires restart to apply password changes
logger.info("Restarting Rust server to \(action.logMessage)")
await serverManager.restart()
} else {
// Hummingbird server just needs cache clear
await serverManager.clearAuthCache()
}
}
enum PasswordAction {
case apply
case remove
var logMessage: String {
switch self {
case .apply: "apply new password"
case .remove: "remove password protection"
}
}
}
var body: some View {
NavigationStack {
Form {
@ -54,7 +82,8 @@ struct DashboardSettingsView: View {
passwordError: $passwordError,
passwordSaved: $passwordSaved,
dashboardKeychain: dashboardKeychain,
savePassword: savePassword
savePassword: savePassword,
logger: logger
)
ServerConfigurationSection(
@ -156,15 +185,37 @@ struct DashboardSettingsView: View {
password = ""
confirmPassword = ""
// Clear cached password in LazyBasicAuthMiddleware
Task {
await ServerManager.shared.clearAuthCache()
// Check if we need to switch to network mode
let needsNetworkModeSwitch = accessMode == .localhost
if needsNetworkModeSwitch {
// Switch to network mode first (this updates ServerManager.bindAddress)
accessModeString = DashboardAccessMode.network.rawValue
}
// When password is set for the first time, automatically switch to network mode
if accessMode == .localhost {
accessModeString = DashboardAccessMode.network.rawValue
restartServerWithNewBindAddress()
// Handle server-specific password update
Task {
let serverManager = ServerManager.shared
if needsNetworkModeSwitch {
// If switching to network mode, update bind address before restart
serverManager.bindAddress = DashboardAccessMode.network.bindAddress
// Always restart when switching to network mode (both server types need it)
logger.info("Restarting server to apply new password and network mode")
await serverManager.restart()
// Wait for server to be ready
try? await Task.sleep(for: .seconds(1))
await MainActor.run {
SessionMonitor.shared.stopMonitoring()
SessionMonitor.shared.startMonitoring()
}
} else {
// Just password change, no network mode switch
await DashboardSettingsView.updateServerForPasswordChange(action: .apply, logger: logger)
}
}
} else {
passwordError = "Failed to save password to keychain"
@ -302,6 +353,7 @@ private struct SecuritySection: View {
@Binding var passwordSaved: Bool
let dashboardKeychain: DashboardKeychain
let savePassword: () -> Void
let logger: Logger
var body: some View {
Section {
@ -315,9 +367,13 @@ private struct SecuritySection: View {
_ = dashboardKeychain.deletePassword()
showPasswordFields = false
passwordSaved = false
// Clear cached password in LazyBasicAuthMiddleware
// Handle server-specific password removal
Task {
await ServerManager.shared.clearAuthCache()
await DashboardSettingsView.updateServerForPasswordChange(
action: .remove,
logger: logger
)
}
}
}
@ -546,12 +602,12 @@ private struct AccessModeView: View {
private struct PortConfigurationView: View {
@Binding var serverPort: String
let restartServerWithNewPort: (Int) -> Void
@State private var portNumber: Int = 4_020
@State private var portConflict: PortConflict?
@State private var isCheckingPort = false
@State private var alternativePorts: [Int] = []
private let serverManager = ServerManager.shared
private let logger = Logger(subsystem: "com.steipete.VibeTunnel", category: "PortConfiguration")
@ -619,7 +675,7 @@ private struct PortConfigurationView: View {
}
}
}
// Port conflict warning
if let conflict = portConflict {
VStack(alignment: .leading, spacing: 6) {
@ -627,18 +683,18 @@ private struct PortConfigurationView: View {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
.font(.caption)
Text("Port \(conflict.port) is used by \(conflict.process.name)")
.font(.caption)
.foregroundColor(.orange)
}
if !conflict.alternativePorts.isEmpty {
HStack(spacing: 4) {
Text("Try port:")
.font(.caption)
.foregroundStyle(.secondary)
ForEach(conflict.alternativePorts.prefix(3), id: \.self) { port in
Button(String(port)) {
serverPort = String(port)
@ -648,7 +704,7 @@ private struct PortConfigurationView: View {
.buttonStyle(.link)
.font(.caption)
}
Button("Choose...") {
showPortPicker()
}
@ -668,7 +724,7 @@ private struct PortConfigurationView: View {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(.red)
.font(.caption)
Text("Server failed to start")
.font(.caption)
.foregroundColor(.red)
@ -680,17 +736,17 @@ private struct PortConfigurationView: View {
}
}
}
private func checkPortAvailability(_ port: Int) async {
isCheckingPort = true
defer { isCheckingPort = false }
// Only check if it's not the port we're already successfully using
if serverManager.isRunning && Int(serverManager.port) == port {
portConflict = nil
return
}
if let conflict = await PortConflictResolver.shared.detectConflict(on: port) {
// Only show warning for non-VibeTunnel processes
// tty-fwd and other VibeTunnel instances will be auto-killed by ServerManager
@ -707,7 +763,7 @@ private struct PortConfigurationView: View {
alternativePorts = []
}
}
private func forceQuitConflictingProcess(_ conflict: PortConflict) async {
do {
try await PortConflictResolver.shared.resolveConflict(conflict)
@ -719,7 +775,7 @@ private struct PortConfigurationView: View {
logger.error("Failed to force quit: \(error)")
}
}
private func showPortPicker() {
// TODO: Implement port picker dialog
// For now, just cycle through alternatives