Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.

Commit ad77180

Browse files
authored
Fix bug
1 parent 8dfcd15 commit ad77180

1 file changed

Lines changed: 153 additions & 124 deletions

File tree

Sources/prostore/install/installApp.swift

Lines changed: 153 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -145,144 +145,172 @@ final class LocalStaticHTTPServer {
145145
}
146146
}
147147

148-
// Start HTTP (or HTTPS if tlsIdentity is provided) on given port. Serves static files from rootDir.
149-
func start(host: NWEndpoint.Host = .ipv4(IPv4Address("127.0.0.1")!),
150-
port: UInt16 = 7404,
151-
rootDir: URL,
152-
tlsIdentity: sec_identity_t? = nil) throws -> UInt16
153-
{
154-
InstallLogger.shared.log("Starting HTTP server with port: \(port), tlsIdentity: \(tlsIdentity != nil ? "present" : "nil")")
155-
self.rootDirectory = rootDir
156-
self.serverStarted = false
157-
158-
// Clear any existing connections
159-
connectionsLock.lock()
160-
activeConnections.removeAll()
161-
connectionsLock.unlock()
162-
163-
// Create TCP params and attach TLS options if identity provided
164-
let tcpOptions = NWProtocolTCP.Options()
165-
let tlsOptions: NWProtocolTLS.Options? = {
166-
guard let identity = tlsIdentity else {
167-
InstallLogger.shared.log("No TLS identity provided, using HTTP")
168-
return nil
169-
}
170-
InstallLogger.shared.log("Configuring TLS options with provided identity")
171-
let options = NWProtocolTLS.Options()
172-
// configure TLS min/max
173-
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12)
174-
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, .TLSv13)
175-
// set local identity (the sec_identity_t)
176-
sec_protocol_options_set_local_identity(options.securityProtocolOptions, identity)
177-
return options
178-
}()
179-
180-
let params: NWParameters
181-
if let tls = tlsOptions {
182-
params = NWParameters(tls: tls, tcp: tcpOptions)
183-
isTLS = true
184-
InstallLogger.shared.log("Server configured with TLS (HTTPS)")
185-
} else {
186-
params = NWParameters(tls: nil, tcp: tcpOptions)
187-
isTLS = false
188-
InstallLogger.shared.log("Server configured without TLS (HTTP)")
148+
func start(host: NWEndpoint.Host = .ipv4(IPv4Address("127.0.0.1")!),
149+
port: UInt16 = 7404,
150+
rootDir: URL,
151+
tlsIdentity: sec_identity_t? = nil) throws -> UInt16
152+
{
153+
InstallLogger.shared.log("Starting HTTP server with port: \(port), tlsIdentity: \(tlsIdentity != nil ? "present" : "nil")")
154+
self.rootDirectory = rootDir
155+
self.serverStarted = false
156+
157+
// If there is an existing listener, stop it first so the port gets freed.
158+
if listener != nil {
159+
InstallLogger.shared.log("An existing listener was found — stopping it before starting a new one")
160+
stop()
161+
// give the OS a tiny moment to free the port
162+
Thread.sleep(forTimeInterval: 0.05)
163+
}
164+
165+
// (No longer blindly removing activeConnections here — stop() handles that)
166+
167+
// Create TCP params and attach TLS options if identity provided
168+
let tcpOptions = NWProtocolTCP.Options()
169+
let tlsOptions: NWProtocolTLS.Options? = {
170+
guard let identity = tlsIdentity else {
171+
InstallLogger.shared.log("No TLS identity provided, using HTTP")
172+
return nil
189173
}
174+
InstallLogger.shared.log("Configuring TLS options with provided identity")
175+
let options = NWProtocolTLS.Options()
176+
sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12)
177+
sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, .TLSv13)
178+
sec_protocol_options_set_local_identity(options.securityProtocolOptions, identity)
179+
return options
180+
}()
181+
182+
let params: NWParameters
183+
if let tls = tlsOptions {
184+
params = NWParameters(tls: tls, tcp: tcpOptions)
185+
isTLS = true
186+
InstallLogger.shared.log("Server configured with TLS (HTTPS)")
187+
} else {
188+
params = NWParameters(tls: nil, tcp: tcpOptions)
189+
isTLS = false
190+
InstallLogger.shared.log("Server configured without TLS (HTTP)")
191+
}
190192

191-
let nwPort = NWEndpoint.Port(rawValue: port) ?? NWEndpoint.Port(integerLiteral: 0)
192-
let listener: NWListener
193+
let requestedPort = port
194+
var listener: NWListener
195+
// Try requested port first; if it fails try ephemeral port 0
196+
do {
197+
let nwPort = NWEndpoint.Port(rawValue: requestedPort) ?? NWEndpoint.Port(integerLiteral: 0)
198+
listener = try NWListener(using: params, on: nwPort)
199+
InstallLogger.shared.log("NWListener created successfully on port \(requestedPort)")
200+
} catch {
201+
InstallLogger.shared.logWarning("NWListener init failed for port \(requestedPort): \(error.localizedDescription). Trying ephemeral port (0).")
193202
do {
194-
listener = try NWListener(using: params, on: nwPort)
195-
InstallLogger.shared.log("NWListener created successfully")
203+
listener = try NWListener(using: params, on: NWEndpoint.Port(integerLiteral: 0))
204+
InstallLogger.shared.log("NWListener created on ephemeral port (0) as fallback")
196205
} catch {
197-
InstallLogger.shared.logError("NWListener init failed: \(error.localizedDescription)")
206+
InstallLogger.shared.logError("NWListener init failed even on ephemeral port: \(error.localizedDescription)")
198207
throw InstallAppError.serverStartFailed("NWListener init failed: \(error.localizedDescription)")
199208
}
209+
}
200210

201-
// Create a semaphore to wait for server to start
202-
let startSemaphore = DispatchSemaphore(value: 0)
203-
var startError: Error?
204-
205-
listener.newConnectionHandler = { [weak self] connection in
206-
guard let self = self else { return }
207-
InstallLogger.shared.logDebug("New connection received")
208-
self.handleConnection(connection)
209-
}
210-
211-
listener.stateUpdateHandler = { newState in
212-
switch newState {
213-
case .ready:
214-
InstallLogger.shared.log("Server listener is READY")
215-
self.serverStarted = true
216-
startSemaphore.signal()
217-
case .failed(let err):
218-
InstallLogger.shared.logError("Listener failed: \(String(describing: err))")
219-
startError = err
220-
startSemaphore.signal()
221-
case .cancelled:
222-
InstallLogger.shared.log("Listener cancelled")
223-
startSemaphore.signal()
224-
case .waiting(let reason):
225-
InstallLogger.shared.log("Listener waiting: \(reason)")
226-
case .setup:
227-
InstallLogger.shared.log("Listener in setup state")
228-
@unknown default:
229-
InstallLogger.shared.log("Unknown listener state: \(newState)")
230-
}
231-
}
232-
233-
listener.start(queue: queue)
234-
self.listener = listener
235-
InstallLogger.shared.log("Listener started on queue")
211+
// Create a semaphore to wait for server to start
212+
let startSemaphore = DispatchSemaphore(value: 0)
213+
var startError: Error?
214+
215+
listener.newConnectionHandler = { [weak self] connection in
216+
guard let self = self else { return }
217+
InstallLogger.shared.logDebug("New connection received")
218+
self.handleConnection(connection)
219+
}
220+
221+
listener.stateUpdateHandler = { [weak self] newState in
222+
guard let self = self else { return }
223+
switch newState {
224+
case .ready:
225+
InstallLogger.shared.log("Server listener is READY")
226+
self.serverStarted = true
227+
startSemaphore.signal()
228+
case .failed(let err):
229+
InstallLogger.shared.logError("Listener failed: \(String(describing: err))")
230+
startError = err
231+
startSemaphore.signal()
232+
case .cancelled:
233+
InstallLogger.shared.log("Listener cancelled")
234+
// ensure serverStarted is false when cancelled
235+
self.serverStarted = false
236+
startSemaphore.signal()
237+
case .waiting(let reason):
238+
InstallLogger.shared.log("Listener waiting: \(reason)")
239+
case .setup:
240+
InstallLogger.shared.log("Listener in setup state")
241+
@unknown default:
242+
InstallLogger.shared.log("Unknown listener state: \(newState)")
243+
}
244+
}
245+
246+
listener.start(queue: queue)
247+
self.listener = listener
248+
InstallLogger.shared.log("Listener started on queue")
249+
250+
// Wait for server to start (with timeout)
251+
InstallLogger.shared.log("Waiting for server to become ready...")
252+
let timeoutResult = startSemaphore.wait(timeout: .now() + 10.0)
253+
254+
if timeoutResult == .timedOut {
255+
InstallLogger.shared.logError("Server start timeout after 10 seconds")
256+
// Cancel and cleanup local listener reference if still present
257+
listener.cancel()
258+
self.listener = nil
259+
throw InstallAppError.serverStartFailed("Server start timeout")
260+
}
261+
262+
if let error = startError {
263+
InstallLogger.shared.logError("Server failed to start: \(error.localizedDescription)")
264+
// Cancel and cleanup
265+
listener.cancel()
266+
self.listener = nil
267+
throw InstallAppError.serverStartFailed("Server failed to start: \(error.localizedDescription)")
268+
}
269+
270+
if !serverStarted {
271+
InstallLogger.shared.logError("Server not started (serverStarted flag is false)")
272+
listener.cancel()
273+
self.listener = nil
274+
throw InstallAppError.serverStartFailed("Server not started")
275+
}
276+
277+
// if we started with port 0 (ephemeral), get the actual port
278+
let actualPort: UInt16
279+
if let localEndpoint = listener.port {
280+
actualPort = UInt16(localEndpoint.rawValue)
281+
InstallLogger.shared.log("Server bound to port: \(actualPort)")
282+
} else {
283+
actualPort = requestedPort
284+
InstallLogger.shared.log("Using requested port: \(actualPort)")
285+
}
236286

237-
// Wait for server to start (with timeout)
238-
InstallLogger.shared.log("Waiting for server to become ready...")
239-
let timeoutResult = startSemaphore.wait(timeout: .now() + 10.0)
240-
241-
if timeoutResult == .timedOut {
242-
InstallLogger.shared.logError("Server start timeout after 10 seconds")
243-
throw InstallAppError.serverStartFailed("Server start timeout")
244-
}
245-
246-
if let error = startError {
247-
InstallLogger.shared.logError("Server failed to start: \(error.localizedDescription)")
248-
throw InstallAppError.serverStartFailed("Server failed to start: \(error.localizedDescription)")
249-
}
250-
251-
if !serverStarted {
252-
InstallLogger.shared.logError("Server not started (serverStarted flag is false)")
253-
throw InstallAppError.serverStartFailed("Server not started")
254-
}
287+
InstallLogger.shared.logSuccess("Server successfully started on \(isTLS ? "https" : "http")://127.0.0.1:\(actualPort)")
288+
return actualPort
289+
}
255290

256-
// if we started with port 0 (ephemeral), get the actual port
257-
let actualPort: UInt16
258-
if let localEndpoint = listener.port {
259-
actualPort = UInt16(localEndpoint.rawValue)
260-
InstallLogger.shared.log("Server bound to port: \(actualPort)")
261-
} else {
262-
actualPort = port
263-
InstallLogger.shared.log("Using requested port: \(actualPort)")
264-
}
291+
func stop() {
292+
InstallLogger.shared.log("Stopping HTTP server")
265293

266-
InstallLogger.shared.logSuccess("Server successfully started on \(isTLS ? "https" : "http")://127.0.0.1:\(actualPort)")
267-
return actualPort
294+
connectionsLock.lock()
295+
// cancel all tracked connections
296+
for wrapper in activeConnections {
297+
wrapper.connection.cancel()
268298
}
299+
activeConnections.removeAll()
300+
connectionsLock.unlock()
269301

270-
func stop() {
271-
InstallLogger.shared.log("Stopping HTTP server")
272-
273-
connectionsLock.lock()
274-
for wrapper in activeConnections {
275-
wrapper.connection.cancel()
276-
}
277-
278-
activeConnections.removeAll()
279-
connectionsLock.unlock()
280-
281-
listener?.cancel()
282-
listener = nil
283-
serverStarted = false
302+
// cancel the listener and remove reference
303+
if let l = listener {
304+
InstallLogger.shared.log("Cancelling listener")
305+
l.cancel()
306+
// give the OS a brief moment to actually close the socket so a subsequent start on the same port won't immediately fail
307+
Thread.sleep(forTimeInterval: 0.05)
284308
}
285309

310+
listener = nil
311+
serverStarted = false
312+
}
313+
286314
// Very minimal GET-only static file handler.
287315
private func handleConnection(_ connection: NWConnection) {
288316
connectionsLock.lock()
@@ -905,3 +933,4 @@ public func installApp(from ipaURL: URL) throws {
905933
InstallLogger.shared.log("Summary written to: \(summaryURL.path)")
906934
}
907935

936+

0 commit comments

Comments
 (0)