mirror of
https://github.com/swift-server/swift-aws-lambda-runtime.git
synced 2026-05-03 07:22:27 +00:00
170 lines
5.4 KiB
Swift
170 lines
5.4 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftAWSLambdaRuntime open source project
|
|
//
|
|
// Copyright SwiftAWSLambdaRuntime project authors
|
|
// Copyright (c) Amazon.com, Inc. or its affiliates.
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import Dispatch
|
|
import Foundation
|
|
import Synchronization
|
|
|
|
@available(macOS 15.0, *)
|
|
struct Utils {
|
|
@discardableResult
|
|
static func execute(
|
|
executable: URL,
|
|
arguments: [String],
|
|
customWorkingDirectory: URL? = .none,
|
|
logLevel: ProcessLogLevel
|
|
) throws -> String {
|
|
if logLevel >= .debug {
|
|
print("\(executable.path()) \(arguments.joined(separator: " "))")
|
|
if let customWorkingDirectory {
|
|
print("Working directory: \(customWorkingDirectory.path())")
|
|
}
|
|
}
|
|
|
|
let fd = dup(1)
|
|
let stdout = fdopen(fd, "rw")
|
|
defer { if let so = stdout { fclose(so) } }
|
|
|
|
// We need to use an unsafe transfer here to get the fd into our Sendable closure.
|
|
// This transfer is fine, because we write to the variable from a single SerialDispatchQueue here.
|
|
// We wait until the process is run below process.waitUntilExit().
|
|
// This means no further writes to output will happen.
|
|
// This makes it save for us to read the output
|
|
struct UnsafeTransfer<Value>: @unchecked Sendable {
|
|
let value: Value
|
|
}
|
|
|
|
let outputMutex = Mutex("")
|
|
let outputSync = DispatchGroup()
|
|
let outputQueue = DispatchQueue(label: "AWSLambdaPackager.output")
|
|
let unsafeTransfer = UnsafeTransfer(value: stdout)
|
|
let outputHandler = { @Sendable (data: Data?) in
|
|
dispatchPrecondition(condition: .onQueue(outputQueue))
|
|
|
|
outputSync.enter()
|
|
defer { outputSync.leave() }
|
|
|
|
guard
|
|
let _output = data.flatMap({
|
|
String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"]))
|
|
}), !_output.isEmpty
|
|
else {
|
|
return
|
|
}
|
|
|
|
outputMutex.withLock { output in
|
|
output += _output + "\n"
|
|
}
|
|
|
|
switch logLevel {
|
|
case .silent:
|
|
break
|
|
case .debug(let outputIndent), .output(let outputIndent):
|
|
print(String(repeating: " ", count: outputIndent), terminator: "")
|
|
print(_output)
|
|
fflush(unsafeTransfer.value)
|
|
}
|
|
}
|
|
|
|
let pipe = Pipe()
|
|
|
|
let process = Process()
|
|
process.standardOutput = pipe
|
|
process.standardError = pipe
|
|
process.executableURL = executable
|
|
process.arguments = arguments
|
|
if let customWorkingDirectory {
|
|
process.currentDirectoryURL = URL(fileURLWithPath: customWorkingDirectory.path())
|
|
}
|
|
|
|
// Read from the pipe on a background thread using a manual read loop.
|
|
// We avoid FileHandle.readabilityHandler because on Linux its setter
|
|
// triggers _bridgeAnythingToObjectiveC / swift_dynamicCast which can
|
|
// crash with a SIGSEGV during concurrent Swift runtime metadata resolution.
|
|
let readFileHandle = pipe.fileHandleForReading
|
|
outputSync.enter()
|
|
outputQueue.async {
|
|
defer { outputSync.leave() }
|
|
// Read in a loop until EOF
|
|
while true {
|
|
let data = readFileHandle.availableData
|
|
if data.isEmpty {
|
|
break // EOF
|
|
}
|
|
outputHandler(data)
|
|
}
|
|
}
|
|
|
|
try process.run()
|
|
process.waitUntilExit()
|
|
|
|
// wait for output to be fully processed
|
|
outputSync.wait()
|
|
|
|
let output = outputMutex.withLock { $0 }
|
|
|
|
if process.terminationStatus != 0 {
|
|
// print output on failure and if not already printed
|
|
if logLevel < .output {
|
|
print(output)
|
|
fflush(stdout)
|
|
}
|
|
throw ProcessError.processFailed([executable.path()] + arguments, process.terminationStatus)
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
enum ProcessError: Error, CustomStringConvertible {
|
|
case processFailed([String], Int32)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case .processFailed(let arguments, let code):
|
|
return "\(arguments.joined(separator: " ")) failed with code \(code)"
|
|
}
|
|
}
|
|
}
|
|
|
|
enum ProcessLogLevel: Comparable {
|
|
case silent
|
|
case output(outputIndent: Int)
|
|
case debug(outputIndent: Int)
|
|
|
|
var naturalOrder: Int {
|
|
switch self {
|
|
case .silent:
|
|
return 0
|
|
case .output:
|
|
return 1
|
|
case .debug:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
static var output: Self {
|
|
.output(outputIndent: 2)
|
|
}
|
|
|
|
static var debug: Self {
|
|
.debug(outputIndent: 2)
|
|
}
|
|
|
|
static func < (lhs: ProcessLogLevel, rhs: ProcessLogLevel) -> Bool {
|
|
lhs.naturalOrder < rhs.naturalOrder
|
|
}
|
|
}
|
|
}
|