Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/Commands/Serve.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,31 @@ struct Serve: AsyncParsableCommand {

func run() async throws {
let server = await Server(port: port)

Logger.info("Starting server", metadata: ["port": "\(port)"])
try await server.start()

// Using custom error handling to prevent ArgumentParser from printing additional error messages
do {
try await server.start()
} catch let error as PortError {
// For port errors, just log once with the suggestion
let suggestedPort = port + 1

// Create a user-friendly error message that includes the suggestion
let message = """
\(error.localizedDescription)
Try using a different port: lume serve --port \(suggestedPort)
"""

// Log the message (without the "ERROR:" prefix that ArgumentParser will add)
Logger.error(message)

// Exit with a custom code to prevent ArgumentParser from printing the error again
Foundation.exit(1)
} catch {
// For other errors, log once
Logger.error("Failed to start server", metadata: ["error": error.localizedDescription])
throw error
}
}
}
145 changes: 145 additions & 0 deletions src/Server/Server.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import Foundation
import Network
import Darwin

// MARK: - Error Types
enum PortError: Error, LocalizedError {
case alreadyInUse(port: UInt16)

var errorDescription: String? {
switch self {
case .alreadyInUse(let port):
return "Port \(port) is already in use by another process"
}
}
}

// MARK: - Server Class
@MainActor
Expand Down Expand Up @@ -145,11 +158,129 @@ final class Server {
]
}

// MARK: - Port Utilities
private func isPortAvailable(port: Int) async -> Bool {
// Create a socket
let socketFD = socket(AF_INET, SOCK_STREAM, 0)
if socketFD == -1 {
return false
}

// Set socket options to allow reuse
var value: Int32 = 1
if setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &value, socklen_t(MemoryLayout<Int32>.size)) == -1 {
close(socketFD)
return false
}

// Set up the address structure
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = UInt16(port).bigEndian
addr.sin_addr.s_addr = INADDR_ANY.bigEndian

// Bind to the port
let bindResult = withUnsafePointer(to: &addr) { addrPtr in
addrPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { addrPtr in
Darwin.bind(socketFD, addrPtr, socklen_t(MemoryLayout<sockaddr_in>.size))
}
}

// Clean up
close(socketFD)

// If bind failed, the port is in use
return bindResult == 0
}

// MARK: - Server Lifecycle
func start() async throws {
// First check if the port is already in use
if !(await isPortAvailable(port: Int(port.rawValue))) {
// Don't log anything here, just throw the error
throw PortError.alreadyInUse(port: port.rawValue)
}

let parameters = NWParameters.tcp
listener = try NWListener(using: parameters, on: port)

// Create an actor to safely manage state transitions
actor StartupState {
var error: Error?
var isComplete = false

func setError(_ error: Error) {
self.error = error
self.isComplete = true
}

func setComplete() {
self.isComplete = true
}

func checkStatus() -> (isComplete: Bool, error: Error?) {
return (isComplete, error)
}
}

let startupState = StartupState()

// Set up a state update handler to detect port binding errors
listener?.stateUpdateHandler = { state in
Task {
switch state {
case .setup:
// Initial state, no action needed
Logger.info("Listener setup", metadata: ["port": "\(self.port.rawValue)"])
break
case .waiting(let error):
// Log the full error details to see what we're getting
Logger.error("Listener waiting", metadata: [
"error": error.localizedDescription,
"debugDescription": error.debugDescription,
"localizedDescription": error.localizedDescription,
"port": "\(self.port.rawValue)"
])

// Check for different port in use error messages
if error.debugDescription.contains("Address already in use") ||
error.localizedDescription.contains("in use") ||
error.localizedDescription.contains("address already in use") {
Logger.error("Port conflict detected", metadata: ["port": "\(self.port.rawValue)"])
await startupState.setError(PortError.alreadyInUse(port: self.port.rawValue))
} else {
// Wait for a short period to see if the listener recovers
// Some network errors are transient
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second

// If we're still waiting after delay, consider it an error
if case .waiting = await self.listener?.state {
await startupState.setError(error)
}
}
case .failed(let error):
// Log the full error details
Logger.error("Listener failed", metadata: [
"error": error.localizedDescription,
"debugDescription": error.debugDescription,
"port": "\(self.port.rawValue)"
])
await startupState.setError(error)
case .ready:
// Listener successfully bound to port
Logger.info("Listener ready", metadata: ["port": "\(self.port.rawValue)"])
await startupState.setComplete()
case .cancelled:
// Listener was cancelled
Logger.info("Listener cancelled", metadata: ["port": "\(self.port.rawValue)"])
break
@unknown default:
Logger.info("Unknown listener state", metadata: ["state": "\(state)", "port": "\(self.port.rawValue)"])
break
}
}
}

listener?.newConnectionHandler = { [weak self] connection in
Task { @MainActor [weak self] in
guard let self else { return }
Expand All @@ -158,6 +289,20 @@ final class Server {
}

listener?.start(queue: .main)

// Wait for either successful startup or an error
var status: (isComplete: Bool, error: Error?) = (false, nil)
repeat {
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
status = await startupState.checkStatus()
} while !status.isComplete

// If there was a startup error, throw it
if let error = status.error {
self.stop()
throw error
}

isRunning = true

Logger.info("Server started", metadata: ["port": "\(port.rawValue)"])
Expand Down