import FoundationEssentials import Hummingbird import Logging /// How the binary was started: `--socket --db ` on Carloku, /// or `--listen 127.0.0.1: --db ` for local runs and tests. public struct Options: Sendable, Equatable { public enum Address: Sendable, Equatable { case socket(String) case listen(host: String, port: Int) } public var address: Address public var database: String /// `CARLOS_HOST`, the instance's hostname, if the platform set it. public var carlosHost: String? public struct UsageError: Error, CustomStringConvertible { public var description: String } public static let usage = "usage: todo (--socket | --listen 127.0.0.1:) --db " public init(arguments: [String], environment: [String: String]) throws { var socket: String? var listen: String? var database: String? var rest = arguments[...] while let flag = rest.popFirst() { guard let value = rest.popFirst() else { throw UsageError(description: "\(flag) needs a value\n\(Self.usage)") } switch flag { case "--socket", "-socket": socket = value case "--listen", "-listen": listen = value case "--db", "-db": database = value default: throw UsageError(description: "unknown flag \(flag)\n\(Self.usage)") } } guard let database else { throw UsageError(description: Self.usage) } self.database = database switch (socket, listen) { case (let path?, nil): address = .socket(path) case (nil, let listen?): guard let colon = listen.lastIndex(of: ":"), listen[..; nothing else is bound") } address = .listen(host: "127.0.0.1", port: port) default: throw UsageError(description: Self.usage) } carlosHost = environment["CARLOS_HOST"].flatMap { $0.isEmpty ? nil : $0 } } /// The origin a browser sends with the app's own form posts. var origin: String? { switch address { case .listen(let host, let port): "http://\(host):\(port)" case .socket: carlosHost.map { "https://\($0)" } } } /// On a socket the platform's edge serves the app over https. var servedOverHTTPS: Bool { if case .socket = address { return true } return false } } public enum Server { /// Builds the router for a store; the tests drive this directly. public static func router(store: Store, options: Options, version: String = buildVersion) -> Router { let router = Router() router.add(middleware: OriginCheck(origin: options.origin)) Routes(store: store, visitors: Visitors(store: store, secure: options.servedOverHTTPS), version: version) .register(on: router) return router } /// Serves until SIGTERM or SIGINT, then closes the database and returns. public static func run(_ options: Options) async throws { var logger = Logger(label: "todo-swift") logger.logLevel = .info let store = try await Store.open(path: options.database) let address: BindAddress switch options.address { case .socket(let path): // A socket file left behind by an earlier process would make bind fail. try? FileManager.default.removeItem(atPath: path) address = .unixDomainSocket(path: path) case .listen(let host, let port): address = .hostname(host, port: port) } let app = Application( router: router(store: store, options: options), configuration: .init(address: address), logger: logger ) do { try await app.runService() } catch { try? await store.close() throw error } try await store.close() if case .socket(let path) = options.address { try? FileManager.default.removeItem(atPath: path) } } }