/* This source file is part of the Swift.org open source project Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors Licensed under Apache License v2.0 with Runtime Library Exception See http://swift.org/LICENSE.txt for license information See http://swift.org/CONTRIBUTORS.txt for Swift project authors */ #if canImport(Glibc) @_exported import Glibc #elseif canImport(Musl) @_exported import Musl #elseif os(Windows) @_exported import CRT @_exported import WinSDK #else @_exported import Darwin.C #endif /// `CStringArray` represents a C null-terminated array of pointers to C strings. /// /// The lifetime of the C strings will correspond to the lifetime of the `CStringArray` /// instance so be careful about copying the buffer as it may contain dangling pointers. public final class CStringArray { /// The null-terminated array of C string pointers. public let cArray: [UnsafeMutablePointer?] /// Creates an instance from an array of strings. public init(_ array: [String]) { #if os(Windows) cArray = array.map({ $0.withCString({ _strdup($0) }) }) + [nil] #else cArray = array.map({ $0.withCString({ strdup($0) }) }) + [nil] #endif } deinit { for case let element? in cArray { free(element) } } } import Foundation #if os(Windows) import WinSDK #endif #if os(Windows) public let executableFileSuffix = ".exe" #else public let executableFileSuffix = "" #endif #if os(Windows) private func quote(_ arguments: [String]) -> String { func quote(argument: String) -> String { if !argument.contains(where: { " \t\n\"".contains($0) }) { return argument } // To escape the command line, we surround the argument with quotes. // However, the complication comes due to how the Windows command line // parser treats backslashes (\) and quotes ("). // // - \ is normally treated as a literal backslash // e.g. alpha\beta\gamma => alpha\beta\gamma // - The sequence \" is treated as a literal " // e.g. alpha\"beta => alpha"beta // // But then what if we are given a path that ends with a \? // // Surrounding alpha\beta\ with " would be "alpha\beta\" which would be // an unterminated string since it ends on a literal quote. To allow // this case the parser treats: // // - \\" as \ followed by the " metacharacter // - \\\" as \ followed by a literal " // // In general: // - 2n \ followed by " => n \ followed by the " metacharacter // - 2n + 1 \ followed by " => n \ followed by a literal " var quoted = "\"" var unquoted = argument.unicodeScalars while !unquoted.isEmpty { guard let firstNonBS = unquoted.firstIndex(where: { $0 != "\\" }) else { // String ends with a backslash (e.g. first\second\), escape all // the backslashes then add the metacharacter ". let count = unquoted.count quoted.append(String(repeating: "\\", count: 2 * count)) break } let count = unquoted.distance(from: unquoted.startIndex, to: firstNonBS) if unquoted[firstNonBS] == "\"" { // This is a string of \ followed by a " (e.g. first\"second). // Escape the backslashes and the quote. quoted.append(String(repeating: "\\", count: 2 * count + 1)) } else { // These are just literal backslashes quoted.append(String(repeating: "\\", count: count)) } quoted.append(String(unquoted[firstNonBS])) // Drop the backslashes and the following character unquoted.removeFirst(count + 1) } quoted.append("\"") return quoted } return arguments.map(quote(argument:)).joined(separator: " ") } #endif /// Replace the current process image with a new process image. /// /// - Parameters: /// - path: Absolute path to the executable. /// - args: The executable arguments. public func exec(path: String, args: [String]) throws -> Never { let cArgs = CStringArray(args) #if os(Windows) var hJob: HANDLE hJob = CreateJobObjectA(nil, nil) if hJob == HANDLE(bitPattern: 0) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } defer { CloseHandle(hJob) } let hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1) if hPort == HANDLE(bitPattern: 0) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } var acpAssociation: JOBOBJECT_ASSOCIATE_COMPLETION_PORT = JOBOBJECT_ASSOCIATE_COMPLETION_PORT() acpAssociation.CompletionKey = hJob acpAssociation.CompletionPort = hPort if !SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, &acpAssociation, DWORD(MemoryLayout.size)) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } var eliLimits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = JOBOBJECT_EXTENDED_LIMIT_INFORMATION() eliLimits.BasicLimitInformation.LimitFlags = DWORD(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE) | DWORD(JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK) if !SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &eliLimits, DWORD(MemoryLayout.size)) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } var siInfo: STARTUPINFOW = STARTUPINFOW() siInfo.cb = DWORD(MemoryLayout.size) var piInfo: PROCESS_INFORMATION = PROCESS_INFORMATION() try quote(args).withCString(encodedAs: UTF16.self) { pwszCommandLine in if !CreateProcessW(nil, UnsafeMutablePointer(mutating: pwszCommandLine), nil, nil, false, DWORD(CREATE_SUSPENDED) | DWORD(CREATE_NEW_PROCESS_GROUP), nil, nil, &siInfo, &piInfo) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } } defer { CloseHandle(piInfo.hThread) } defer { CloseHandle(piInfo.hProcess) } if !AssignProcessToJobObject(hJob, piInfo.hProcess) { throw SystemError.exec(Int32(GetLastError()), path: path, args: args) } _ = ResumeThread(piInfo.hThread) var dwCompletionCode: DWORD = 0 var ulCompletionKey: ULONG_PTR = 0 var lpOverlapped: LPOVERLAPPED? repeat { } while GetQueuedCompletionStatus(hPort, &dwCompletionCode, &ulCompletionKey, &lpOverlapped, INFINITE) && !(ulCompletionKey == ULONG_PTR(UInt(bitPattern: hJob)) && dwCompletionCode == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) var dwExitCode: DWORD = DWORD(bitPattern: -1) _ = GetExitCodeProcess(piInfo.hProcess, &dwExitCode) _exit(Int32(bitPattern: dwExitCode)) #elseif (!canImport(Darwin) || os(macOS)) guard execv(path, cArgs.cArray) != -1 else { throw SystemError.exec(errno, path: path, args: args) } fatalError("unreachable") #else fatalError("not implemented") #endif } @_disfavoredOverload @available(*, deprecated, message: "Use the overload which returns Never") public func exec(path: String, args: [String]) throws { try exec(path: path, args: args) } // MARK: TSCUtility function for searching for executables /// Create a list of AbsolutePath search paths from a string, such as the PATH environment variable. /// /// - Parameters: /// - pathString: The path string to parse. /// - currentWorkingDirectory: The current working directory, the relative paths will be converted to absolute paths /// based on this path. /// - Returns: List of search paths. public func getEnvSearchPaths( pathString: String?, currentWorkingDirectory: AbsolutePath? ) -> [AbsolutePath] { // Compute search paths from PATH variable. #if os(Windows) let pathSeparator: Character = ";" #else let pathSeparator: Character = ":" #endif return (pathString ?? "").split(separator: pathSeparator).map(String.init).compactMap({ pathString in if let cwd = currentWorkingDirectory { return try? AbsolutePath(validating: pathString, relativeTo: cwd) } return try? AbsolutePath(validating: pathString) }) } /// Lookup an executable path from an environment variable value, current working /// directory or search paths. Only return a value that is both found and executable. /// /// This method searches in the following order: /// * If env value is a valid absolute path, return it. /// * If env value is relative path, first try to locate it in current working directory. /// * Otherwise, in provided search paths. /// /// - Parameters: /// - filename: The name of the file to find. /// - currentWorkingDirectory: The current working directory to look in. /// - searchPaths: The additional search paths to look in if not found in cwd. /// - Returns: Valid path to executable if present, otherwise nil. public func lookupExecutablePath( filename value: String?, currentWorkingDirectory: AbsolutePath? = localFileSystem.currentWorkingDirectory, searchPaths: [AbsolutePath] = [] ) -> AbsolutePath? { // We should have a value to continue. guard let value = value, !value.isEmpty else { return nil } var paths: [AbsolutePath] = [] if let cwd = currentWorkingDirectory, let path = try? AbsolutePath(validating: value, relativeTo: cwd) { // We have a value, but it could be an absolute or a relative path. paths.append(path) } else if let absPath = try? AbsolutePath(validating: value) { // Current directory not being available is not a problem // for the absolute-specified paths. paths.append(absPath) } // Ensure the value is not a path. if !value.contains("/") { // Try to locate in search paths. paths.append(contentsOf: searchPaths.map({ $0.appending(component: value) })) } return paths.first(where: { localFileSystem.isExecutableFile($0) }) } /// A wrapper for Range to make it Codable. /// /// Technically, we can use conditional conformance and make /// stdlib's Range Codable but since extensions leak out, it /// is not a good idea to extend types that you don't own. /// /// Range conformance will be added soon to stdlib so we can remove /// this type in the future. public struct CodableRange where Bound: Comparable & Codable { /// The underlying range. public let range: Range /// Create a CodableRange instance. public init(_ range: Range) { self.range = range } } extension CodableRange: Sendable where Bound: Sendable {} extension CodableRange: Codable { private enum CodingKeys: String, CodingKey { case lowerBound, upperBound } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(range.lowerBound, forKey: .lowerBound) try container.encode(range.upperBound, forKey: .upperBound) } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let lowerBound = try container.decode(Bound.self, forKey: .lowerBound) let upperBound = try container.decode(Bound.self, forKey: .upperBound) self.init(Range(uncheckedBounds: (lowerBound, upperBound))) } } extension AbsolutePath { /// File URL created from the normalized string representation of the path. public var asURL: Foundation.URL { return URL(fileURLWithPath: pathString) } public init(_ url: URL) throws { try self.init(validating: url.path) } } // FIXME: Eliminate or find a proper place for this. public enum SystemError: Error { case chdir(Int32, String) case close(Int32) case exec(Int32, path: String, args: [String]) case pipe(Int32) case posix_spawn(Int32, [String]) case read(Int32) case setenv(Int32, String) case stat(Int32, String) case symlink(Int32, String, dest: String) case unsetenv(Int32, String) case waitpid(Int32) } extension SystemError: CustomStringConvertible { public var description: String { func strerror(_ errno: Int32) -> String { #if os(Windows) let cap = 128 var buf = [Int8](repeating: 0, count: cap) let _ = strerror_s(&buf, 128, errno) return "\(String(cString: buf)) (\(errno))" #else var cap = 64 while cap <= 16 * 1024 { var buf = [Int8](repeating: 0, count: cap) let err = strerror_r(errno, &buf, buf.count) if err == EINVAL { return "Unknown error \(errno)" } if err == ERANGE { cap *= 2 continue } if err != 0 { fatalError("strerror_r error: \(err)") } return "\(String(cString: buf)) (\(errno))" } fatalError("strerror_r error: \(ERANGE)") #endif } switch self { case .chdir(let errno, let path): return "chdir error: \(strerror(errno)): \(path)" case .close(let err): let errorMessage: String if err == -1 { // if the return code is -1, we need to consult the global `errno` errorMessage = strerror(errno) } else { errorMessage = strerror(err) } return "close error: \(errorMessage)" case .exec(let errno, let path, let args): let joinedArgs = args.joined(separator: " ") return "exec error: \(strerror(errno)): \(path) \(joinedArgs)" case .pipe(let errno): return "pipe error: \(strerror(errno))" case .posix_spawn(let errno, let args): return "posix_spawn error: \(strerror(errno)), `\(args)`" case .read(let errno): return "read error: \(strerror(errno))" case .setenv(let errno, let key): return "setenv error: \(strerror(errno)): \(key)" case .stat(let errno, _): return "stat error: \(strerror(errno))" case .symlink(let errno, let path, let dest): return "symlink error: \(strerror(errno)): \(path) -> \(dest)" case .unsetenv(let errno, let key): return "unsetenv error: \(strerror(errno)): \(key)" case .waitpid(let errno): return "waitpid error: \(strerror(errno))" } } } extension SystemError: CustomNSError { public var errorUserInfo: [String : Any] { return [NSLocalizedDescriptionKey: self.description] } } /// Memoizes a costly computation to a cache variable. func memoize(to cache: inout T?, build: () throws -> T) rethrows -> T { if let value = cache { return value } else { let value = try build() cache = value return value } }