Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
0xTim committed Jun 4, 2024
2 parents 111be08 + 12e9b41 commit b439fb1
Show file tree
Hide file tree
Showing 13 changed files with 332 additions and 182 deletions.
1 change: 1 addition & 0 deletions Sources/Development/routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ public func routes(_ app: Application) throws {
return [cred1]
}

@Sendable
func opaqueRouteTester(_ req: Request) async throws -> some AsyncResponseEncodable {
"Hello World"
}
Expand Down
38 changes: 20 additions & 18 deletions Sources/Vapor/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ public final class Application: Sendable {
_ eventLoopGroupProvider: EventLoopGroupProvider = .singleton
) {
self.init(environment, eventLoopGroupProvider, async: false)
self.asyncCommands.use(self.servers.command, as: "serve", isDefault: true)
DotEnvFile.load(for: environment, on: .shared(self.eventLoopGroup), fileio: self.fileio, logger: self.logger)
}

Expand Down Expand Up @@ -155,12 +156,12 @@ public final class Application: Sendable {
self.servers.use(.http)
self.clients.initialize()
self.clients.use(.http)
self.asyncCommands.use(self.servers.command, as: "serve", isDefault: true)
self.asyncCommands.use(RoutesCommand(), as: "routes")
}

public static func make(_ environment: Environment = .development, _ eventLoopGroupProvider: EventLoopGroupProvider = .singleton) async throws -> Application {
let app = Application(environment, eventLoopGroupProvider, async: true)
await app.asyncCommands.use(app.servers.asyncCommand, as: "serve", isDefault: true)
await DotEnvFile.load(for: app.environment, fileio: app.fileio, logger: app.logger)
return app
}
Expand Down Expand Up @@ -247,12 +248,15 @@ public final class Application: Sendable {

/// Called when the applications starts up, will trigger the lifecycle handlers. The asynchronous version of ``boot()``
public func asyncBoot() async throws {
self.isBooted.withLockedValue { booted in
guard !booted else {
return
}
booted = true
/// Skip the boot process if already booted
guard !self.isBooted.withLockedValue({
var result = true
swap(&$0, &result)
return result
}) else {
return
}

for handler in self.lifecycle.handlers {
try await handler.willBootAsync(self)
}
Expand All @@ -268,8 +272,11 @@ public final class Application: Sendable {

self.logger.trace("Shutting down providers")
self.lifecycle.handlers.reversed().forEach { $0.shutdown(self) }

triggerShutdown()
self.lifecycle.handlers = []

self.logger.trace("Clearing Application storage")
self.storage.shutdown()
self.storage.clear()

switch self.eventLoopGroupProvider {
case .shared:
Expand All @@ -295,8 +302,11 @@ public final class Application: Sendable {
for handler in self.lifecycle.handlers.reversed() {
await handler.shutdownAsync(self)
}

triggerShutdown()
self.lifecycle.handlers = []

self.logger.trace("Clearing Application storage")
await self.storage.asyncShutdown()
self.storage.clear()

switch self.eventLoopGroupProvider {
case .shared:
Expand All @@ -313,14 +323,6 @@ public final class Application: Sendable {
self._didShutdown.withLockedValue { $0 = true }
self.logger.trace("Application shutdown complete")
}

private func triggerShutdown() {
self.lifecycle.handlers = []

self.logger.trace("Clearing Application storage")
self.storage.shutdown()
self.storage.clear()
}

deinit {
self.logger.trace("Application deinitialized, goodbye!")
Expand Down
2 changes: 1 addition & 1 deletion Sources/Vapor/Client/ClientResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import NIOCore
import NIOHTTP1
import Foundation

public struct ClientResponse {
public struct ClientResponse: Sendable {
public var status: HTTPStatus
public var headers: HTTPHeaders
public var body: ByteBuffer?
Expand Down
11 changes: 11 additions & 0 deletions Sources/Vapor/Commands/ServeCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public final class ServeCommand: AsyncCommand, Sendable {
self.box.withLockedValue { $0 = box }
}

@available(*, noasync, message: "Use the async asyncShutdown() method instead.")
func shutdown() {
var box = self.box.withLockedValue { $0 }
box.didShutdown = true
Expand All @@ -115,6 +116,16 @@ public final class ServeCommand: AsyncCommand, Sendable {
self.box.withLockedValue { $0 = box }
}

func asyncShutdown() async {
var box = self.box.withLockedValue { $0 }
box.didShutdown = true
box.running?.stop()
await box.server?.shutdown()
box.signalSources.forEach { $0.cancel() } // clear refs
box.signalSources = []
self.box.withLockedValue { $0 = box }
}

deinit {
assert(self.box.withLockedValue({ $0.didShutdown }), "ServeCommand did not shutdown before deinit")
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Vapor/HTTP/Client/Application+HTTP+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ extension Application.HTTP {
configuration: self.configuration,
backgroundActivityLogger: self.application.logger
)
self.application.storage.set(Key.self, to: new) {
try $0.syncShutdown()
self.application.storage.setFirstTime(Key.self, to: new, onShutdown: { try $0.syncShutdown() }) {
try await $0.shutdown()
}
return new
}
Expand Down
15 changes: 15 additions & 0 deletions Sources/Vapor/Server/Application+Servers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ extension Application {
self.storage.makeServer.withLockedValue { $0 = .init(factory: makeServer) }
}

@available(*, noasync, renamed: "asyncCommand", message: "Use the async property instead.")
public var command: ServeCommand {
if let existing = self.application.storage.get(CommandKey.self) {
return existing
Expand All @@ -62,6 +63,20 @@ extension Application {
return new
}
}

public var asyncCommand: ServeCommand {
get async {
if let existing = self.application.storage.get(CommandKey.self) {
return existing
} else {
let new = ServeCommand()
await self.application.storage.setWithAsyncShutdown(CommandKey.self, to: new) {
await $0.asyncShutdown()
}
return new
}
}
}

let application: Application

Expand Down
26 changes: 13 additions & 13 deletions Sources/Vapor/URLEncodedForm/URLEncodedFormSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import struct Foundation.CharacterSet
struct URLEncodedFormSerializer: Sendable {
let splitVariablesOn: Character
let splitKeyValueOn: Character

/// Create a new form-urlencoded data parser.
init(splitVariablesOn: Character = "&", splitKeyValueOn: Character = "=") {
self.splitVariablesOn = splitVariablesOn
self.splitKeyValueOn = splitKeyValueOn
}

func serialize(_ data: URLEncodedFormData, codingPath: [CodingKey] = []) throws -> String {
var entries: [String] = []
let key = try codingPath.toURLEncodedKey()
Expand All @@ -21,20 +21,20 @@ struct URLEncodedFormSerializer: Sendable {
}
}
for (key, child) in data.children {
entries.append(try serialize(child, codingPath: codingPath + [_CodingKey(stringValue: key) as CodingKey]))
try entries.append(serialize(child, codingPath: codingPath + [_CodingKey(stringValue: key) as CodingKey]))
}
return entries.joined(separator: String(splitVariablesOn))
}

struct _CodingKey: CodingKey {
var stringValue: String

init(stringValue: String) {
self.stringValue = stringValue
}

var intValue: Int?

init?(intValue: Int) {
self.intValue = intValue
self.stringValue = intValue.description
Expand All @@ -47,9 +47,9 @@ extension Array where Element == CodingKey {
if count < 1 {
return ""
}
return try self[0].stringValue.urlEncoded(codingPath: self) + self[1...].map({ (key: CodingKey) -> String in
return try "[" + key.stringValue.urlEncoded(codingPath: self) + "]"
}).joined()
return try self[0].stringValue.urlEncoded(codingPath: self) + self[1...].map { (key: CodingKey) -> String in
try "[" + key.stringValue.urlEncoded(codingPath: self) + "]"
}.joined()
}
}

Expand All @@ -72,10 +72,10 @@ extension String {

/// Characters allowed in form-urlencoded data.
private enum Characters {
// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
static let allowedCharacters: CharacterSet = {
var allowed = CharacterSet.urlQueryAllowed
// these symbols are reserved for url-encoded form
allowed.remove(charactersIn: "?&=[];+")
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "*-._")
return allowed
}()
}
59 changes: 59 additions & 0 deletions Sources/Vapor/Utilities/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,25 @@ public struct Storage: Sendable {
struct Value<T: Sendable>: AnyStorageValue {
var value: T
var onShutdown: (@Sendable (T) throws -> ())?
var onAsyncShutdown: (@Sendable (T) async throws -> ())?
func shutdown(logger: Logger) {
do {
try self.onShutdown?(self.value)
} catch {
logger.warning("Could not shutdown \(T.self): \(error)")
}
}
func asyncShutdown(logger: Logger) async {
do {
if let onAsyncShutdown {
try await onAsyncShutdown(self.value)
} else {
try self.onShutdown?(self.value)
}
} catch {
logger.warning("Could not shutdown \(T.self): \(error)")
}
}
}

/// The logger provided to shutdown closures.
Expand Down Expand Up @@ -79,6 +91,7 @@ public struct Storage: Sendable {
/// Set or remove a value for a given key, optionally providing a shutdown closure for the value.
///
/// If a key that has a shutdown closure is removed by this method, the closure **is** invoked.
@available(*, noasync, message: "Use the async setWithAsyncShutdown() method instead.", renamed: "setWithAsyncShutdown")
public mutating func set<Key>(
_ key: Key.Type,
to value: Key.Value?,
Expand All @@ -94,20 +107,66 @@ public struct Storage: Sendable {
existing.shutdown(logger: self.logger)
}
}

/// Set or remove a value for a given key, optionally providing an async shutdown closure for the value.
///
/// If a key that has a shutdown closure is removed by this method, the closure **is** invoked.
public mutating func setWithAsyncShutdown<Key>(
_ key: Key.Type,
to value: Key.Value?,
onShutdown: (@Sendable (Key.Value) async throws -> ())? = nil
) async
where Key: StorageKey
{
let key = ObjectIdentifier(Key.self)
if let value = value {
self.storage[key] = Value(value: value, onShutdown: nil, onAsyncShutdown: onShutdown)
} else if let existing = self.storage[key] {
self.storage[key] = nil
await existing.asyncShutdown(logger: self.logger)
}
}

// Provides a way to set an async shutdown with an async call to avoid breaking the API
// This must not be called when a value alraedy exists in storage
mutating func setFirstTime<Key>(
_ key: Key.Type,
to value: Key.Value?,
onShutdown: (@Sendable (Key.Value) throws -> ())? = nil,
onAsyncShutdown: (@Sendable (Key.Value) async throws -> ())? = nil
)
where Key: StorageKey
{
let key = ObjectIdentifier(Key.self)
precondition(self.storage[key] == nil, "You must not call this when a value already exists in storage")
if let value {
self.storage[key] = Value(value: value, onShutdown: onShutdown, onAsyncShutdown: onAsyncShutdown)
}
}

/// For every key in the container having a shutdown closure, invoke the closure. Designed to
/// be invoked during an explicit app shutdown process or in a reference type's `deinit`.
@available(*, noasync, message: "Use the async asyncShutdown() method instead.")
public func shutdown() {
self.storage.values.forEach {
$0.shutdown(logger: self.logger)
}
}

/// For every key in the container having a shutdown closure, invoke the closure. Designed to
/// be invoked during an explicit app shutdown process or in a reference type's `deinit`.
public func asyncShutdown() async {
for value in self.storage.values {
await value.asyncShutdown(logger: self.logger)
}
}
}

/// ``Storage`` uses this protocol internally to generically invoke shutdown closures for arbitrarily-
/// typed key values.
protocol AnyStorageValue: Sendable {
func shutdown(logger: Logger)
func asyncShutdown(logger: Logger) async
}

/// A key used to store values in a ``Storage`` must conform to this protocol.
Expand Down
22 changes: 16 additions & 6 deletions Sources/Vapor/Utilities/URI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,17 @@ public struct URI: CustomStringConvertible, ExpressibleByStringInterpolation, Ha
}

public var string: String {
#if canImport(Darwin)
self.components?.string ?? ""
#else
// On Linux, URLComponents incorrectly treats `;` as *not* allowed in the path component.
self.components?.string?.replacingOccurrences(of: "%3B", with: ";") ?? ""
#endif
if urlPathAllowedIsBroken {
// On Linux and in older Xcode versions, URLComponents incorrectly treats `;` as *not* allowed in the path component.
let string = self.components?.string ?? ""
return string.replacingOccurrences(
of: "%3B", with: ";",
options: .literal, // N.B.: `rangeOfPath` never actually returns `nil`
range: self.components?.rangeOfPath ?? (string.startIndex..<string.startIndex)
)
} else {
return self.components?.string ?? ""
}
}

// See `ExpressibleByStringInterpolation.init(stringLiteral:)`.
Expand Down Expand Up @@ -331,3 +336,8 @@ extension CharacterSet {
.urlPathAllowed.union(.init(charactersIn: ";"))
}
}

/// On Linux and in older Xcode versions, URLComponents incorrectly treats `;` as *not* allowed in the path component.
private let urlPathAllowedIsBroken: Bool = {
CharacterSet.urlPathAllowed != CharacterSet.urlCorrectPathAllowed
}()
Loading

0 comments on commit b439fb1

Please sign in to comment.