mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Migrating PR from gravitational/teleport repo (#232)
Export-Source-Commit: ead22275e58458e55a2316e849ff5fed138eea27
This commit is contained in:
committed by
Gus Rivera
parent
f3a8e939f4
commit
6d906dc57b
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2700"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "LogBackends"
|
||||||
|
BuildableName = "LogBackends"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnabled = "No">
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "LogBackends"
|
||||||
|
BuildableName = "LogBackends"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
@@ -20,7 +20,7 @@ import PackageDescription
|
|||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "TeleportKit",
|
name: "TeleportKit",
|
||||||
platforms: [.iOS(.v26)],
|
platforms: [.iOS(.v26), .macOS(.v26)],
|
||||||
products: [
|
products: [
|
||||||
.library(name: "SystemClients", targets: ["SystemClients"]),
|
.library(name: "SystemClients", targets: ["SystemClients"]),
|
||||||
.library(name: "LogBackends", targets: ["LogBackends"]),
|
.library(name: "LogBackends", targets: ["LogBackends"]),
|
||||||
@@ -29,6 +29,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", .upToNextMajor(from: "1.14.0")),
|
.package(url: "https://github.com/pointfreeco/swift-dependencies", .upToNextMajor(from: "1.14.0")),
|
||||||
.package(url: "https://github.com/pointfreeco/swift-sharing", .upToNextMajor(from: "2.9.1")),
|
.package(url: "https://github.com/pointfreeco/swift-sharing", .upToNextMajor(from: "2.9.1")),
|
||||||
.package(url: "https://github.com/apple/swift-log", .upToNextMajor(from: "1.14.0")),
|
.package(url: "https://github.com/apple/swift-log", .upToNextMajor(from: "1.14.0")),
|
||||||
|
.package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.6.0")),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
@@ -42,7 +43,19 @@ let package = Package(
|
|||||||
.target(
|
.target(
|
||||||
name: "LogBackends",
|
name: "LogBackends",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
.collections,
|
||||||
.logging,
|
.logging,
|
||||||
|
.dependencies,
|
||||||
|
"SystemClients",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "LogBackendsTests",
|
||||||
|
dependencies: [
|
||||||
|
.dependencies,
|
||||||
|
.logging,
|
||||||
|
"LogBackends",
|
||||||
|
"SystemClients",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -68,6 +81,10 @@ extension Target.Dependency {
|
|||||||
name: "Logging",
|
name: "Logging",
|
||||||
package: "swift-log",
|
package: "swift-log",
|
||||||
)
|
)
|
||||||
|
fileprivate static let collections: Self = .product(
|
||||||
|
name: "Collections",
|
||||||
|
package: "swift-collections",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Build Settings
|
// MARK: - Build Settings
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see http://www.gnu.org/licenses/
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Dependencies
|
||||||
import Foundation
|
import Foundation
|
||||||
public import Logging
|
public import Logging
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ public import Logging
|
|||||||
public struct ConsoleLogHandler: LogHandler {
|
public struct ConsoleLogHandler: LogHandler {
|
||||||
public let label: String
|
public let label: String
|
||||||
|
|
||||||
|
@Dependency(\.date.now)
|
||||||
|
private var now
|
||||||
|
|
||||||
private let timestampFormmater = Date.ISO8601FormatStyle(
|
private let timestampFormmater = Date.ISO8601FormatStyle(
|
||||||
dateSeparator: .dash,
|
dateSeparator: .dash,
|
||||||
dateTimeSeparator: .space,
|
dateTimeSeparator: .space,
|
||||||
@@ -46,11 +50,7 @@ public struct ConsoleLogHandler: LogHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func log(event: LogEvent) {
|
public func log(event: LogEvent) {
|
||||||
let timestamp = timestampFormmater.format(.now)
|
let logMessage = LogFormatter.format(label: label, event: event, handlerMetadata: metadata, timestamp: now)
|
||||||
let metadataToLog = metadata.merging(event.metadata ?? [:]) { _, new in new }
|
print(logMessage)
|
||||||
|
|
||||||
let output = "\(timestamp) \(event.level.formatted) \(event.file):\(event.line) | \(event.message) \(metadataToLog.formatted)"
|
|
||||||
|
|
||||||
print(output)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
|
||||||
|
/// LogFormatter doesn't currently maintain any local state, so it's simplest to implement it as an enum with static
|
||||||
|
/// functions.
|
||||||
|
enum LogFormatter {
|
||||||
|
private static let timestampFormatter = Date.ISO8601FormatStyle(
|
||||||
|
dateSeparator: .dash,
|
||||||
|
dateTimeSeparator: .space,
|
||||||
|
timeSeparator: .colon,
|
||||||
|
timeZoneSeparator: .colon,
|
||||||
|
includingFractionalSeconds: true,
|
||||||
|
timeZone: .autoupdatingCurrent,
|
||||||
|
)
|
||||||
|
|
||||||
|
/// Formats the log message
|
||||||
|
/// - Parameters:
|
||||||
|
/// - label: The label of the logger
|
||||||
|
/// - event: The event to log
|
||||||
|
/// - handlerMetadata: The metadata for the `LogHandler` (note: events carry their own metadata and this formatter
|
||||||
|
/// automatically coalesces them with the `LogHandler` metadata)
|
||||||
|
/// - timestamp: The time at which the logging event took place
|
||||||
|
/// - Returns: A formatted log message suitable for output to a debug console or writing to a file
|
||||||
|
static func format(
|
||||||
|
label: String,
|
||||||
|
event: LogEvent,
|
||||||
|
handlerMetadata: Logger.Metadata,
|
||||||
|
timestamp: Date,
|
||||||
|
) -> String {
|
||||||
|
let metadata = handlerMetadata.merging(event.metadata ?? [:]) { _, eventValue in eventValue }
|
||||||
|
var components: [String] = [
|
||||||
|
timestampFormatter.format(timestamp),
|
||||||
|
event.level.formatted,
|
||||||
|
"[\(label)]",
|
||||||
|
"\(event.file):\(event.line)",
|
||||||
|
"|",
|
||||||
|
"\(event.message)",
|
||||||
|
]
|
||||||
|
|
||||||
|
let formattedMetadata = metadata.formatted
|
||||||
|
if !formattedMetadata.isEmpty {
|
||||||
|
components.append(formattedMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = event.error {
|
||||||
|
components.append("❌ error=\(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return components.joined(separator: " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-6
@@ -25,12 +25,7 @@ extension Logger.Metadata {
|
|||||||
var formatted: String {
|
var formatted: String {
|
||||||
guard !isEmpty else { return "" }
|
guard !isEmpty else { return "" }
|
||||||
let metadataAsStrings = map { key, value in
|
let metadataAsStrings = map { key, value in
|
||||||
let prefix = if key.lowercased().localizedStandardContains("error") {
|
"🔸 \(key)=\(value)"
|
||||||
"❌"
|
|
||||||
} else {
|
|
||||||
"🔸"
|
|
||||||
}
|
|
||||||
return "\(prefix) \(key)=\(value)"
|
|
||||||
}
|
}
|
||||||
return metadataAsStrings.joined(separator: " ")
|
return metadataAsStrings.joined(separator: " ")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
|
||||||
|
/// A log handler that writes to a rotating file on disk.
|
||||||
|
public struct RotatingFileLogHandler: LogHandler {
|
||||||
|
public let label: String
|
||||||
|
|
||||||
|
@Dependency(\.date.now)
|
||||||
|
private var now
|
||||||
|
|
||||||
|
private let writer: RotatingFileWriter
|
||||||
|
|
||||||
|
public init(label: String, writer: RotatingFileWriter) {
|
||||||
|
self.label = label
|
||||||
|
self.writer = writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: LogHandler Conformance
|
||||||
|
|
||||||
|
public var logLevel: Logger.Level = .info
|
||||||
|
|
||||||
|
public var metadata: Logger.Metadata = [:]
|
||||||
|
|
||||||
|
public subscript(metadataKey key: String) -> Logger.Metadata.Value? {
|
||||||
|
get { metadata[key] }
|
||||||
|
set { metadata[key] = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
public func log(event: LogEvent) {
|
||||||
|
let logMessage = LogFormatter.format(label: label, event: event, handlerMetadata: metadata, timestamp: now)
|
||||||
|
writer.enqueue(logMessage: "\(logMessage)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Dependencies
|
||||||
|
import DequeModule
|
||||||
|
public import Foundation
|
||||||
|
import Synchronization
|
||||||
|
import SystemClients
|
||||||
|
|
||||||
|
/// Opens a file for writing which rotates to a new file whenever the original is at capacity.
|
||||||
|
public final class RotatingFileWriter: Sendable {
|
||||||
|
// MARK: Mutable State
|
||||||
|
|
||||||
|
/// An inbox that maintains the pending items to be written.
|
||||||
|
///
|
||||||
|
/// This mutext protects pending-item bookkeeping only. Filesystem work must never occur while holding this mutex,
|
||||||
|
/// keeping enqueue operations independent of slow I/O.
|
||||||
|
private let inbox = Mutex(Inbox())
|
||||||
|
|
||||||
|
/// A handle to the currently open/active file.
|
||||||
|
///
|
||||||
|
/// This mutex protects the active file state separately from the inbox mutext because it may be held during
|
||||||
|
/// synchronous filesystem operations.
|
||||||
|
private let fileState = Mutex(FileState())
|
||||||
|
|
||||||
|
// MARK: Immutable State
|
||||||
|
|
||||||
|
private let activeFileURL: URL
|
||||||
|
private let configuration: Configuration
|
||||||
|
private let queue = DispatchQueue(label: "com.goteleport.teleportkit.rotating-file-writer")
|
||||||
|
private let fileSystemClient: FileSystemClient
|
||||||
|
|
||||||
|
public convenience init(fileURL: URL) {
|
||||||
|
self.init(fileURL: fileURL, configuration: .live)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(fileURL: URL, configuration: Configuration) {
|
||||||
|
self.activeFileURL = fileURL
|
||||||
|
self.configuration = configuration
|
||||||
|
|
||||||
|
// Retrieving the dependency once during init is safe because the file system implementation should not
|
||||||
|
// change mid-process.
|
||||||
|
@Dependency(\.fileSystemClient)
|
||||||
|
var fileSystemClient
|
||||||
|
|
||||||
|
self.fileSystemClient = fileSystemClient
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Waits for all previously enqueued records to be processed and synchronizes the active file.
|
||||||
|
public func flush() async throws {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
// Enqueuing the continuation allows us to ensure all earlier items are processed before the active file is
|
||||||
|
// synchronized and the continuation resumes.
|
||||||
|
enqueue(item: .flush(continuation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enqueues a log message to be written to disk
|
||||||
|
func enqueue(logMessage: String) {
|
||||||
|
enqueue(item: .logMessage(logMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Inbox Processing
|
||||||
|
|
||||||
|
extension RotatingFileWriter {
|
||||||
|
/// Adds an item to the inbox and schedules processing if it isn't already scheduled or active.
|
||||||
|
private func enqueue(item: PendingItem) {
|
||||||
|
let shouldScheduleProcessing = inbox.withLock { inbox in
|
||||||
|
inbox.append(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard shouldScheduleProcessing else { return }
|
||||||
|
queue.async {
|
||||||
|
self.processInbox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top level inbox processing function that iterates through the pending items and performs their corresponding
|
||||||
|
/// operation.
|
||||||
|
private func processInbox() {
|
||||||
|
inbox.withLock { $0.beginProcessing() }
|
||||||
|
|
||||||
|
while let item = inbox.withLock({ $0.nextPendingItem() }) {
|
||||||
|
switch item {
|
||||||
|
case let .logMessage(record):
|
||||||
|
do {
|
||||||
|
try append(record: record)
|
||||||
|
} catch {
|
||||||
|
recordFailure(error)
|
||||||
|
}
|
||||||
|
case let .droppedRecordNotice(count):
|
||||||
|
do {
|
||||||
|
try append(droppedRecordNoticeFor: count)
|
||||||
|
} catch {
|
||||||
|
recordFailure(error)
|
||||||
|
}
|
||||||
|
case let .flush(continuation):
|
||||||
|
processFlushRequest(continuation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - File Operations
|
||||||
|
|
||||||
|
extension RotatingFileWriter {
|
||||||
|
/// Appends the record to the currently active file.
|
||||||
|
/// - Parameter record: The record to write to disk.
|
||||||
|
private func append(record: String) throws {
|
||||||
|
let record = truncateRecordIfNeeded(Data(record.utf8))
|
||||||
|
try openActiveFileIfNeeded()
|
||||||
|
try rotateActiveFileIfNeeded(forAppendingByteCount: record.count)
|
||||||
|
|
||||||
|
try fileState.withLock { fileState in
|
||||||
|
guard let fileClient = fileState.fileClient else { return }
|
||||||
|
_ = try fileClient.seekToEnd()
|
||||||
|
try fileClient.write(data: record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends a line to the log file indicating that some number of records were dropped, perhaps due to overflowing
|
||||||
|
/// the queue.
|
||||||
|
/// - Parameter count: The number of log records
|
||||||
|
private func append(droppedRecordNoticeFor count: Int) throws {
|
||||||
|
// TODO: Implement dropped record logging
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncates a record if its size exceeds the max size of a single log file.
|
||||||
|
/// - Parameter record: The record to truncate
|
||||||
|
/// - Returns: The record, truncated only if necessary
|
||||||
|
private func truncateRecordIfNeeded(_ record: Data) -> Data {
|
||||||
|
// TODO: Truncate the record
|
||||||
|
record
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a file for appending if no active file is yet open.
|
||||||
|
private func openActiveFileIfNeeded() throws {
|
||||||
|
try fileState.withLock { fileState in
|
||||||
|
guard fileState.fileClient == nil else { return }
|
||||||
|
|
||||||
|
try fileSystemClient.createDirectory(url: activeFileURL.deletingLastPathComponent())
|
||||||
|
if !fileSystemClient.fileExists(url: activeFileURL) {
|
||||||
|
try fileSystemClient.createFile(url: activeFileURL, contents: nil)
|
||||||
|
}
|
||||||
|
fileState.fileClient = try fileSystemClient.openFileForWriting(url: activeFileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotates the active file if needed.
|
||||||
|
///
|
||||||
|
/// Rotation is "needed" when appending a record of the indicated count would cause the log file to exceed its
|
||||||
|
/// maximum capacity.
|
||||||
|
/// - Parameter byteCount: The number of bytes we intend to append to the log file.
|
||||||
|
private func rotateActiveFileIfNeeded(forAppendingByteCount byteCount: Int) throws {
|
||||||
|
// TODO: Implement rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flushes all pending writes to disk via the file handle, ensuring all records are persisted.
|
||||||
|
/// - Parameter continuation: The continuation to resume when the synchronize operation is done.
|
||||||
|
private func processFlushRequest(_ continuation: CheckedContinuation<Void, any Error>) {
|
||||||
|
do {
|
||||||
|
try fileState.withLock { fileState in
|
||||||
|
try fileState.fileClient?.synchronize()
|
||||||
|
}
|
||||||
|
continuation.resume()
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recordFailure(_ error: any Error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Supporting Types
|
||||||
|
|
||||||
|
extension RotatingFileWriter {
|
||||||
|
struct Configuration {
|
||||||
|
static let live = Configuration(
|
||||||
|
maximumFileSize: 4 * 1024 * 1024,
|
||||||
|
maximumArchiveCount: 3,
|
||||||
|
)
|
||||||
|
|
||||||
|
/// The maximum size any individual file is allowed to be, in number of bytes
|
||||||
|
let maximumFileSize: Int
|
||||||
|
|
||||||
|
/// The maximum number of archived files. This does not include the file actively being written to.
|
||||||
|
///
|
||||||
|
/// For example, if the maximum archive count is 3, then at most the rotating file writer will govern 4 total
|
||||||
|
/// files: the 3 archive files and the one active file.
|
||||||
|
let maximumArchiveCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Inbox {
|
||||||
|
var pendingItems: Deque<PendingItem> = []
|
||||||
|
var processingState = ProcessingState.idle
|
||||||
|
|
||||||
|
/// Appends an item to the pendingItems queue and marks the inbox as scheduled for processing
|
||||||
|
/// - Parameter item: The item to queue up
|
||||||
|
/// - Returns: True if a new processing job should be started. False otherwise.
|
||||||
|
mutating func append(_ item: PendingItem) -> Bool {
|
||||||
|
pendingItems.append(item)
|
||||||
|
|
||||||
|
guard processingState == .idle else { return false }
|
||||||
|
processingState = .scheduled
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the inbox as being actively processed.
|
||||||
|
mutating func beginProcessing() {
|
||||||
|
assert(processingState == .scheduled)
|
||||||
|
processingState = .processing
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the next item from the queue. If the queue is empty, that means processing is done and we mark the
|
||||||
|
/// inbox as idle.
|
||||||
|
/// - Returns: The next item to process from the queue, if the queue is non-empty. Nil otherwise.
|
||||||
|
mutating func nextPendingItem() -> PendingItem? {
|
||||||
|
assert(processingState == .processing)
|
||||||
|
|
||||||
|
guard !pendingItems.isEmpty else {
|
||||||
|
processingState = .idle
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pendingItems.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumerates the possible states the inbox can be in with respect to processing.
|
||||||
|
///
|
||||||
|
/// Lifecycle: idle → scheduled → processing → idle.
|
||||||
|
///
|
||||||
|
/// Whenever the mutex is released, a nonempty inbox must have processing scheduled or active.
|
||||||
|
private enum ProcessingState {
|
||||||
|
case idle
|
||||||
|
case scheduled
|
||||||
|
case processing
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains mutable state that the worker needs in order to write to disk.
|
||||||
|
struct FileState {
|
||||||
|
var fileClient: WritableFileClient? = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An enumeration of the various items that can be enqueued in the inbox
|
||||||
|
private enum PendingItem {
|
||||||
|
/// A log message to be written to disk
|
||||||
|
case logMessage(String)
|
||||||
|
/// An indicator saying a certain number of records were dropped.
|
||||||
|
case droppedRecordNotice(Int)
|
||||||
|
/// A request to flush the current file handle to disk
|
||||||
|
case flush(CheckedContinuation<Void, any Error>)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import DependenciesMacros
|
||||||
|
public import Foundation
|
||||||
|
|
||||||
|
/// An interface to the filesystem operations used by TeleportKit.
|
||||||
|
///
|
||||||
|
/// As TeleportKit needs more filesystem capabilities, it's perfectly natural for this client to grow over time.
|
||||||
|
@DependencyClient
|
||||||
|
public struct FileSystemClient: Sendable {
|
||||||
|
public var createDirectory: @Sendable (_ url: URL) throws -> Void
|
||||||
|
public var fileExists: @Sendable (_ url: URL) -> Bool = { _ in false }
|
||||||
|
public var createFile: @Sendable (_ url: URL, _ contents: Data?) throws -> Void
|
||||||
|
public var openFileForWriting: @Sendable (_ url: URL) throws -> WritableFileClient
|
||||||
|
public var moveItem: @Sendable (_ sourceURL: URL, _ destinationURL: URL) throws -> Void
|
||||||
|
public var removeItem: @Sendable (_ url: URL) throws -> Void
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FileSystemClientError: Error {
|
||||||
|
/// File creation failure doesn't surface an error, but rather returns a `Bool`, so we have our own error
|
||||||
|
case couldNotCreateFile(path: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FileSystemClient {
|
||||||
|
public static let liveValue = FileSystemClient(
|
||||||
|
createDirectory: { url in
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
at: url,
|
||||||
|
withIntermediateDirectories: true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
fileExists: { url in
|
||||||
|
FileManager.default.fileExists(atPath: url.path)
|
||||||
|
},
|
||||||
|
createFile: { url, contents in
|
||||||
|
guard FileManager.default.createFile(atPath: url.path, contents: contents) else {
|
||||||
|
throw FileSystemClientError.couldNotCreateFile(path: url.path(percentEncoded: false))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFileForWriting: { url in
|
||||||
|
try WritableFileClient.liveValue(FileHandle(forWritingTo: url))
|
||||||
|
},
|
||||||
|
moveItem: { sourceURL, destinationURL in
|
||||||
|
try FileManager.default.moveItem(at: sourceURL, to: destinationURL)
|
||||||
|
},
|
||||||
|
removeItem: { url in
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import DependenciesMacros
|
||||||
|
public import Foundation
|
||||||
|
|
||||||
|
/// An interface to an open writable file.
|
||||||
|
///
|
||||||
|
/// This interface exists so that code which uses a writeable FileHandle can be tested. For documentation on what the
|
||||||
|
/// individual functions do, refer to the correspondingly named functions on `FileHandle`.
|
||||||
|
@DependencyClient
|
||||||
|
public struct WritableFileClient: Sendable {
|
||||||
|
public var seekToEnd: @Sendable () throws -> UInt64
|
||||||
|
public var write: @Sendable (_ data: Data) throws -> Void
|
||||||
|
public var synchronize: @Sendable () throws -> Void
|
||||||
|
public var close: @Sendable () throws -> Void
|
||||||
|
}
|
||||||
|
|
||||||
|
extension WritableFileClient {
|
||||||
|
static func liveValue(_ fileHandle: FileHandle) -> WritableFileClient {
|
||||||
|
WritableFileClient(
|
||||||
|
seekToEnd: {
|
||||||
|
try fileHandle.seekToEnd()
|
||||||
|
},
|
||||||
|
write: { data in
|
||||||
|
try fileHandle.write(contentsOf: data)
|
||||||
|
},
|
||||||
|
synchronize: {
|
||||||
|
try fileHandle.synchronize()
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
try fileHandle.close()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,4 +20,7 @@ import DependenciesMacros
|
|||||||
extension DependencyValues {
|
extension DependencyValues {
|
||||||
@DependencyEntry(liveValue: SerialNumberClient.liveValue)
|
@DependencyEntry(liveValue: SerialNumberClient.liveValue)
|
||||||
public nonisolated var serialNumberClient = SerialNumberClient()
|
public nonisolated var serialNumberClient = SerialNumberClient()
|
||||||
|
|
||||||
|
@DependencyEntry(liveValue: FileSystemClient.liveValue)
|
||||||
|
public nonisolated var fileSystemClient: FileSystemClient
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Logging
|
||||||
|
import SystemClients
|
||||||
|
import Testing
|
||||||
|
@testable import LogBackends
|
||||||
|
|
||||||
|
struct RotatingFileWriterTests {
|
||||||
|
@Test
|
||||||
|
func `flush appends all previously enqueued records in order`() async throws {
|
||||||
|
try await withTemporaryDirectory { directoryURL in
|
||||||
|
let fileURL = directoryURL.appending(path: "events.log")
|
||||||
|
let writer = makeWriter(fileURL: fileURL)
|
||||||
|
|
||||||
|
writer.enqueue(logMessage: "first\n")
|
||||||
|
writer.enqueue(logMessage: "second\n")
|
||||||
|
try await writer.flush()
|
||||||
|
|
||||||
|
let contents = try? String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
#expect(contents == "first\nsecond\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `records enqueued after a completed flush are processed`() async throws {
|
||||||
|
try await withTemporaryDirectory { directoryURL in
|
||||||
|
let fileURL = directoryURL.appending(path: "events.log")
|
||||||
|
let writer = makeWriter(fileURL: fileURL)
|
||||||
|
|
||||||
|
writer.enqueue(logMessage: "first\n")
|
||||||
|
try await writer.flush()
|
||||||
|
|
||||||
|
writer.enqueue(logMessage: "second\n")
|
||||||
|
try await writer.flush()
|
||||||
|
|
||||||
|
let contents = try? String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
#expect(contents == "first\nsecond\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `writing creates missing parent directories`() async throws {
|
||||||
|
try await withTemporaryDirectory { directoryURL in
|
||||||
|
let fileURL = directoryURL.appending(path: "nested/logs/events.log")
|
||||||
|
let writer = makeWriter(fileURL: fileURL)
|
||||||
|
|
||||||
|
writer.enqueue(logMessage: "record\n")
|
||||||
|
try await writer.flush()
|
||||||
|
|
||||||
|
let contents = try? String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
#expect(contents == "record\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func `handler sends its formatted record to the shared writer`() async throws {
|
||||||
|
try await withTemporaryDirectory { directoryURL in
|
||||||
|
let fileURL = directoryURL.appending(path: "events.log")
|
||||||
|
let writer = makeWriter(fileURL: fileURL)
|
||||||
|
let handler = withDependencies {
|
||||||
|
$0.date.now = Date(timeIntervalSince1970: 0)
|
||||||
|
} operation: {
|
||||||
|
RotatingFileLogHandler(label: "test.logger", writer: writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.log(event: LogEvent(
|
||||||
|
level: .info,
|
||||||
|
message: "hello from the handler",
|
||||||
|
metadata: nil,
|
||||||
|
source: nil,
|
||||||
|
file: "Module/File.swift",
|
||||||
|
function: "run()",
|
||||||
|
line: 42,
|
||||||
|
))
|
||||||
|
try await writer.flush()
|
||||||
|
|
||||||
|
let contents = try? String(contentsOf: fileURL, encoding: .utf8)
|
||||||
|
#expect(contents?.contains("hello from the handler") == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeWriter(fileURL: URL) -> RotatingFileWriter {
|
||||||
|
withDependencies {
|
||||||
|
$0.fileSystemClient = FileSystemClient.liveValue
|
||||||
|
} operation: {
|
||||||
|
RotatingFileWriter(fileURL: fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Teleport
|
||||||
|
// Copyright (C) 2026 Gravitational, Inc.
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see http://www.gnu.org/licenses/
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
func withTemporaryDirectory<Result>(
|
||||||
|
_ operation: (URL) async throws -> Result
|
||||||
|
) async throws -> Result {
|
||||||
|
let directoryURL = FileManager.default.temporaryDirectory
|
||||||
|
.appending(path: "TeleportKitTests-\(UUID())", directoryHint: .isDirectory)
|
||||||
|
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
defer {
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: directoryURL)
|
||||||
|
} catch {
|
||||||
|
Issue.record("Failed to remove temporary test directory at \(directoryURL.path): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await operation(directoryURL)
|
||||||
|
}
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0C94C8CE2FA35F3D00C13C9A /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C94C8CD2FA35ED900C13C9A /* libresolv.9.tbd */; };
|
0C94C8CE2FA35F3D00C13C9A /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C94C8CD2FA35ED900C13C9A /* libresolv.9.tbd */; };
|
||||||
CD65C8012FF711CE002FB1CD /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = CD65C8002FF711CE002FB1CD /* IdentifiedCollections */; };
|
CD65C8012FF711CE002FB1CD /* IdentifiedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = CD65C8002FF711CE002FB1CD /* IdentifiedCollections */; };
|
||||||
|
CD67F9AF302A74B20043D017 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CD67F9AE302A74B20043D017 /* Logging */; };
|
||||||
|
CD67F9B1302A74BD0043D017 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CD67F9B0302A74BD0043D017 /* Logging */; };
|
||||||
|
CD67F9B3302A76000043D017 /* Dependencies in Frameworks */ = {isa = PBXBuildFile; productRef = CD67F9B2302A76000043D017 /* Dependencies */; };
|
||||||
|
CD67F9B5302A76000043D017 /* DependenciesMacros in Frameworks */ = {isa = PBXBuildFile; productRef = CD67F9B4302A76000043D017 /* DependenciesMacros */; };
|
||||||
CD7999232FE33AE000133048 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD79991B2FE33AE000133048 /* Core.framework */; };
|
CD7999232FE33AE000133048 /* Core.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD79991B2FE33AE000133048 /* Core.framework */; };
|
||||||
CD7999242FE33AE000133048 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD79991B2FE33AE000133048 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
CD7999242FE33AE000133048 /* Core.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CD79991B2FE33AE000133048 /* Core.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
CD79992C2FE33F0600133048 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD79992B2FE33EFF00133048 /* libresolv.9.tbd */; };
|
CD79992C2FE33F0600133048 /* libresolv.9.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD79992B2FE33EFF00133048 /* libresolv.9.tbd */; };
|
||||||
@@ -23,6 +27,8 @@
|
|||||||
CDAA8BB22FFD55CC00E6CA87 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB12FFD55CC00E6CA87 /* GRDB */; };
|
CDAA8BB22FFD55CC00E6CA87 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB12FFD55CC00E6CA87 /* GRDB */; };
|
||||||
CDAA8BB52FFD5A1300E6CA87 /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB42FFD5A1300E6CA87 /* SQLiteData */; };
|
CDAA8BB52FFD5A1300E6CA87 /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB42FFD5A1300E6CA87 /* SQLiteData */; };
|
||||||
CDAA8BB72FFD5A1300E6CA87 /* SQLiteDataTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB62FFD5A1300E6CA87 /* SQLiteDataTestSupport */; };
|
CDAA8BB72FFD5A1300E6CA87 /* SQLiteDataTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = CDAA8BB62FFD5A1300E6CA87 /* SQLiteDataTestSupport */; };
|
||||||
|
CDEA38EA302CD5630004B164 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CDEA38E9302CD5630004B164 /* Collections */; };
|
||||||
|
CDEA38EC302CD5770004B164 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = CDEA38EB302CD5770004B164 /* Collections */; };
|
||||||
CDFCE7053022A9D4002E6773 /* LogBackends in Frameworks */ = {isa = PBXBuildFile; productRef = CDFCE7043022A9D4002E6773 /* LogBackends */; };
|
CDFCE7053022A9D4002E6773 /* LogBackends in Frameworks */ = {isa = PBXBuildFile; productRef = CDFCE7043022A9D4002E6773 /* LogBackends */; };
|
||||||
CDFCE7073022A9D4002E6773 /* LogBackends in Frameworks */ = {isa = PBXBuildFile; productRef = CDFCE7063022A9D4002E6773 /* LogBackends */; };
|
CDFCE7073022A9D4002E6773 /* LogBackends in Frameworks */ = {isa = PBXBuildFile; productRef = CDFCE7063022A9D4002E6773 /* LogBackends */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -102,10 +108,14 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CD67F9AF302A74B20043D017 /* Logging in Frameworks */,
|
||||||
CDFCE7053022A9D4002E6773 /* LogBackends in Frameworks */,
|
CDFCE7053022A9D4002E6773 /* LogBackends in Frameworks */,
|
||||||
CD9AEFAC3022782200E2FC14 /* SystemClients in Frameworks */,
|
CD9AEFAC3022782200E2FC14 /* SystemClients in Frameworks */,
|
||||||
CDAA8BB52FFD5A1300E6CA87 /* SQLiteData in Frameworks */,
|
CDAA8BB52FFD5A1300E6CA87 /* SQLiteData in Frameworks */,
|
||||||
|
CD67F9B5302A76000043D017 /* DependenciesMacros in Frameworks */,
|
||||||
0C94C8CE2FA35F3D00C13C9A /* libresolv.9.tbd in Frameworks */,
|
0C94C8CE2FA35F3D00C13C9A /* libresolv.9.tbd in Frameworks */,
|
||||||
|
CDEA38EA302CD5630004B164 /* Collections in Frameworks */,
|
||||||
|
CD67F9B3302A76000043D017 /* Dependencies in Frameworks */,
|
||||||
CDAA8BB22FFD55CC00E6CA87 /* GRDB in Frameworks */,
|
CDAA8BB22FFD55CC00E6CA87 /* GRDB in Frameworks */,
|
||||||
CD65C8012FF711CE002FB1CD /* IdentifiedCollections in Frameworks */,
|
CD65C8012FF711CE002FB1CD /* IdentifiedCollections in Frameworks */,
|
||||||
CD7999232FE33AE000133048 /* Core.framework in Frameworks */,
|
CD7999232FE33AE000133048 /* Core.framework in Frameworks */,
|
||||||
@@ -117,6 +127,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
CDEA38EC302CD5770004B164 /* Collections in Frameworks */,
|
||||||
|
CD67F9B1302A74BD0043D017 /* Logging in Frameworks */,
|
||||||
CDFCE7073022A9D4002E6773 /* LogBackends in Frameworks */,
|
CDFCE7073022A9D4002E6773 /* LogBackends in Frameworks */,
|
||||||
CD9AEFAE3022782900E2FC14 /* SystemClients in Frameworks */,
|
CD9AEFAE3022782900E2FC14 /* SystemClients in Frameworks */,
|
||||||
CD79992C2FE33F0600133048 /* libresolv.9.tbd in Frameworks */,
|
CD79992C2FE33F0600133048 /* libresolv.9.tbd in Frameworks */,
|
||||||
@@ -209,6 +221,10 @@
|
|||||||
CDAA8BB42FFD5A1300E6CA87 /* SQLiteData */,
|
CDAA8BB42FFD5A1300E6CA87 /* SQLiteData */,
|
||||||
CD9AEFAB3022782200E2FC14 /* SystemClients */,
|
CD9AEFAB3022782200E2FC14 /* SystemClients */,
|
||||||
CDFCE7043022A9D4002E6773 /* LogBackends */,
|
CDFCE7043022A9D4002E6773 /* LogBackends */,
|
||||||
|
CD67F9AE302A74B20043D017 /* Logging */,
|
||||||
|
CD67F9B2302A76000043D017 /* Dependencies */,
|
||||||
|
CD67F9B4302A76000043D017 /* DependenciesMacros */,
|
||||||
|
CDEA38E9302CD5630004B164 /* Collections */,
|
||||||
);
|
);
|
||||||
productName = Verify;
|
productName = Verify;
|
||||||
productReference = 0C94C8522FA232E600C13C9A /* Verify.app */;
|
productReference = 0C94C8522FA232E600C13C9A /* Verify.app */;
|
||||||
@@ -238,6 +254,8 @@
|
|||||||
CD7999342FE346B800133048 /* DependenciesMacros */,
|
CD7999342FE346B800133048 /* DependenciesMacros */,
|
||||||
CD9AEFAD3022782900E2FC14 /* SystemClients */,
|
CD9AEFAD3022782900E2FC14 /* SystemClients */,
|
||||||
CDFCE7063022A9D4002E6773 /* LogBackends */,
|
CDFCE7063022A9D4002E6773 /* LogBackends */,
|
||||||
|
CD67F9B0302A74BD0043D017 /* Logging */,
|
||||||
|
CDEA38EB302CD5770004B164 /* Collections */,
|
||||||
);
|
);
|
||||||
productName = Core;
|
productName = Core;
|
||||||
productReference = CD79991B2FE33AE000133048 /* Core.framework */;
|
productReference = CD79991B2FE33AE000133048 /* Core.framework */;
|
||||||
@@ -310,6 +328,8 @@
|
|||||||
CDAA8BB32FFD5A1200E6CA87 /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
CDAA8BB32FFD5A1200E6CA87 /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
||||||
CD920CEE301D13500027C579 /* XCRemoteSwiftPackageReference "swift-sharing" */,
|
CD920CEE301D13500027C579 /* XCRemoteSwiftPackageReference "swift-sharing" */,
|
||||||
CD921947302148B80027C579 /* XCLocalSwiftPackageReference "../TeleportKit" */,
|
CD921947302148B80027C579 /* XCLocalSwiftPackageReference "../TeleportKit" */,
|
||||||
|
CD67F9AD302A74B20043D017 /* XCRemoteSwiftPackageReference "swift-log" */,
|
||||||
|
CDEA38E8302CD5630004B164 /* XCRemoteSwiftPackageReference "swift-collections" */,
|
||||||
);
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 0C94C8532FA232E600C13C9A /* Products */;
|
productRefGroup = 0C94C8532FA232E600C13C9A /* Products */;
|
||||||
@@ -586,6 +606,8 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Verify/Info.plist;
|
INFOPLIST_FILE = Verify/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Teleport Verify";
|
INFOPLIST_KEY_CFBundleDisplayName = "Teleport Verify";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Teleport Verify uses the built-in QR code scanner to receive enrollment data from the Teleport Web UI";
|
||||||
|
INFOPLIST_KEY_NSFaceIDUsageDescription = "Teleport Verify uses Face ID to confirm your presence when proving this device's identity";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -634,6 +656,8 @@
|
|||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = Verify/Info.plist;
|
INFOPLIST_FILE = Verify/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Teleport Verify";
|
INFOPLIST_KEY_CFBundleDisplayName = "Teleport Verify";
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Teleport Verify uses the built-in QR code scanner to receive enrollment data from the Teleport Web UI";
|
||||||
|
INFOPLIST_KEY_NSFaceIDUsageDescription = "Teleport Verify uses Face ID to confirm your presence when proving this device's identity";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -872,6 +896,14 @@
|
|||||||
minimumVersion = 1.1.1;
|
minimumVersion = 1.1.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
CD67F9AD302A74B20043D017 /* XCRemoteSwiftPackageReference "swift-log" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apple/swift-log";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.14.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
CD7999312FE345D400133048 /* XCRemoteSwiftPackageReference "swift-dependencies" */ = {
|
CD7999312FE345D400133048 /* XCRemoteSwiftPackageReference "swift-dependencies" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/pointfreeco/swift-dependencies";
|
repositoryURL = "https://github.com/pointfreeco/swift-dependencies";
|
||||||
@@ -924,6 +956,14 @@
|
|||||||
minimumVersion = 0.63.3;
|
minimumVersion = 0.63.3;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
CDEA38E8302CD5630004B164 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apple/swift-collections.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.6.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -932,6 +972,26 @@
|
|||||||
package = CD65C7FF2FF711CE002FB1CD /* XCRemoteSwiftPackageReference "swift-identified-collections" */;
|
package = CD65C7FF2FF711CE002FB1CD /* XCRemoteSwiftPackageReference "swift-identified-collections" */;
|
||||||
productName = IdentifiedCollections;
|
productName = IdentifiedCollections;
|
||||||
};
|
};
|
||||||
|
CD67F9AE302A74B20043D017 /* Logging */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CD67F9AD302A74B20043D017 /* XCRemoteSwiftPackageReference "swift-log" */;
|
||||||
|
productName = Logging;
|
||||||
|
};
|
||||||
|
CD67F9B0302A74BD0043D017 /* Logging */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CD67F9AD302A74B20043D017 /* XCRemoteSwiftPackageReference "swift-log" */;
|
||||||
|
productName = Logging;
|
||||||
|
};
|
||||||
|
CD67F9B2302A76000043D017 /* Dependencies */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CD7999312FE345D400133048 /* XCRemoteSwiftPackageReference "swift-dependencies" */;
|
||||||
|
productName = Dependencies;
|
||||||
|
};
|
||||||
|
CD67F9B4302A76000043D017 /* DependenciesMacros */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CD7999312FE345D400133048 /* XCRemoteSwiftPackageReference "swift-dependencies" */;
|
||||||
|
productName = DependenciesMacros;
|
||||||
|
};
|
||||||
CD7999282FE33B7E00133048 /* SwiftLintBuildToolPlugin */ = {
|
CD7999282FE33B7E00133048 /* SwiftLintBuildToolPlugin */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CDDEB8752FE1F5F10052BC27 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
package = CDDEB8752FE1F5F10052BC27 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
||||||
@@ -997,6 +1057,16 @@
|
|||||||
package = CDDEB8752FE1F5F10052BC27 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
package = CDDEB8752FE1F5F10052BC27 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */;
|
||||||
productName = "plugin:SwiftLintBuildToolPlugin";
|
productName = "plugin:SwiftLintBuildToolPlugin";
|
||||||
};
|
};
|
||||||
|
CDEA38E9302CD5630004B164 /* Collections */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CDEA38E8302CD5630004B164 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
productName = Collections;
|
||||||
|
};
|
||||||
|
CDEA38EB302CD5770004B164 /* Collections */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CDEA38E8302CD5630004B164 /* XCRemoteSwiftPackageReference "swift-collections" */;
|
||||||
|
productName = Collections;
|
||||||
|
};
|
||||||
CDFCE7043022A9D4002E6773 /* LogBackends */ = {
|
CDFCE7043022A9D4002E6773 /* LogBackends */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CD921947302148B80027C579 /* XCLocalSwiftPackageReference "../TeleportKit" */;
|
package = CD921947302148B80027C579 /* XCLocalSwiftPackageReference "../TeleportKit" */;
|
||||||
|
|||||||
@@ -42,10 +42,7 @@ extension VerifyAppModel {
|
|||||||
landingViewModel.navigateToDeviceEnrollment(with: deepLink)
|
landingViewModel.navigateToDeviceEnrollment(with: deepLink)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.warning("Failed to parse deep link", metadata: [
|
logger.warning("Failed to parse deep link", error: error, metadata: ["scannedURL": "\(url)"])
|
||||||
"scannedURL": "\(url)",
|
|
||||||
"error": .string(String(describing: error)),
|
|
||||||
])
|
|
||||||
landingViewModel.showParserError(errorMessage: error.localizedDescription)
|
landingViewModel.showParserError(errorMessage: error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,9 +230,10 @@ extension SecureEnclaveCredentialStore {
|
|||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
if let error {
|
if let error {
|
||||||
logger.error("Could not create the Device Trust access-control policy", metadata: [
|
logger.error(
|
||||||
"error": "\(error.takeRetainedValue())",
|
"Could not create the Device Trust access-control policy",
|
||||||
])
|
error: error.takeRetainedValue(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
throw DeviceTrustCredentialError.accessControlCreationFailed
|
throw DeviceTrustCredentialError.accessControlCreationFailed
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ class EnrollDeviceViewModel {
|
|||||||
|
|
||||||
loadingState = .success("fake-token-\(cluster?.id.uuidString ?? "(nil)")")
|
loadingState = .success("fake-token-\(cluster?.id.uuidString ?? "(nil)")")
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to request enrollment token", metadata: [
|
logger.error("Failed to request enrollment token", error: error)
|
||||||
"error": .string(String(describing: error)),
|
|
||||||
])
|
|
||||||
loadingState = .failure(error)
|
loadingState = .failure(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -181,9 +181,7 @@ extension LandingViewModel {
|
|||||||
try deleteOperation().execute(db)
|
try deleteOperation().execute(db)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.warning("Failed to forget clusters", metadata: [
|
logger.warning("Failed to forget clusters", error: error)
|
||||||
"error": .string(String(describing: error)),
|
|
||||||
])
|
|
||||||
destination = .notice(AlertState(
|
destination = .notice(AlertState(
|
||||||
title: {
|
title: {
|
||||||
TextState("Could Not Forget Clusters")
|
TextState("Could Not Forget Clusters")
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Teleport Verify uses the built-in QR code scanner to receive enrollment data from the Teleport Web UI</string>
|
|
||||||
<key>NSFaceIDUsageDescription</key>
|
|
||||||
<string>Teleport Verify uses Face ID to confirm your presence when proving this device's identity</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -87,9 +87,7 @@ extension AppDatabase {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
fatalError("Database initialization failed: \(error)")
|
fatalError("Database initialization failed: \(error)")
|
||||||
#else
|
#else
|
||||||
logger.critical("Database initialization failed; falling back to in-memory database", metadata: [
|
logger.critical("Database initialization failed; falling back to in-memory database", error: error)
|
||||||
"error": .string(String(describing: error)),
|
|
||||||
])
|
|
||||||
database = makeInMemoryDatabase()
|
database = makeInMemoryDatabase()
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user