|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +import Foundation |
| 14 | +import LSPLogging |
| 15 | +import SwiftExtensions |
| 16 | + |
| 17 | +import struct TSCBasic.AbsolutePath |
| 18 | +import class TSCBasic.Process |
| 19 | +import enum TSCBasic.ProcessEnv |
| 20 | +import struct TSCBasic.ProcessEnvironmentBlock |
| 21 | +import struct TSCBasic.ProcessResult |
| 22 | + |
| 23 | +#if os(Windows) |
| 24 | +import WinSDK |
| 25 | +#endif |
| 26 | + |
| 27 | +extension Process { |
| 28 | + /// Wait for the process to exit. If the task gets cancelled, during this time, send a `SIGINT` to the process. |
| 29 | + @discardableResult |
| 30 | + public func waitUntilExitSendingSigIntOnTaskCancellation() async throws -> ProcessResult { |
| 31 | + return try await withTaskCancellationHandler { |
| 32 | + try await waitUntilExit() |
| 33 | + } onCancel: { |
| 34 | + signal(SIGINT) |
| 35 | + } |
| 36 | + } |
| 37 | + |
| 38 | + /// Launches a new process with the given parameters. |
| 39 | + /// |
| 40 | + /// - Important: If `workingDirectory` is not supported on this platform, this logs an error and falls back to launching the |
| 41 | + /// process without the working directory set. |
| 42 | + private static func launch( |
| 43 | + arguments: [String], |
| 44 | + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, |
| 45 | + workingDirectory: AbsolutePath?, |
| 46 | + outputRedirection: OutputRedirection = .collect, |
| 47 | + startNewProcessGroup: Bool = true, |
| 48 | + loggingHandler: LoggingHandler? = .none |
| 49 | + ) throws -> Process { |
| 50 | + let process = |
| 51 | + if let workingDirectory { |
| 52 | + Process( |
| 53 | + arguments: arguments, |
| 54 | + environmentBlock: environmentBlock, |
| 55 | + workingDirectory: workingDirectory, |
| 56 | + outputRedirection: outputRedirection, |
| 57 | + startNewProcessGroup: startNewProcessGroup, |
| 58 | + loggingHandler: loggingHandler |
| 59 | + ) |
| 60 | + } else { |
| 61 | + Process( |
| 62 | + arguments: arguments, |
| 63 | + environmentBlock: environmentBlock, |
| 64 | + outputRedirection: outputRedirection, |
| 65 | + startNewProcessGroup: startNewProcessGroup, |
| 66 | + loggingHandler: loggingHandler |
| 67 | + ) |
| 68 | + } |
| 69 | + do { |
| 70 | + try process.launch() |
| 71 | + } catch Process.Error.workingDirectoryNotSupported where workingDirectory != nil { |
| 72 | + // TODO (indexing): We need to figure out how to set the working directory on all platforms. |
| 73 | + logger.error( |
| 74 | + "Working directory not supported on the platform. Launching process without working directory \(workingDirectory!.pathString)" |
| 75 | + ) |
| 76 | + return try Process.launch( |
| 77 | + arguments: arguments, |
| 78 | + environmentBlock: environmentBlock, |
| 79 | + workingDirectory: nil, |
| 80 | + outputRedirection: outputRedirection, |
| 81 | + startNewProcessGroup: startNewProcessGroup, |
| 82 | + loggingHandler: loggingHandler |
| 83 | + ) |
| 84 | + } |
| 85 | + return process |
| 86 | + } |
| 87 | + |
| 88 | + /// Runs a new process with the given parameters and waits for it to exit, sending SIGINT if this task is cancelled. |
| 89 | + /// |
| 90 | + /// The process's priority tracks the priority of the current task. |
| 91 | + @discardableResult |
| 92 | + public static func run( |
| 93 | + arguments: [String], |
| 94 | + environmentBlock: ProcessEnvironmentBlock = ProcessEnv.block, |
| 95 | + workingDirectory: AbsolutePath?, |
| 96 | + outputRedirection: OutputRedirection = .collect, |
| 97 | + startNewProcessGroup: Bool = true, |
| 98 | + loggingHandler: LoggingHandler? = .none |
| 99 | + ) async throws -> ProcessResult { |
| 100 | + let process = try Self.launch( |
| 101 | + arguments: arguments, |
| 102 | + environmentBlock: environmentBlock, |
| 103 | + workingDirectory: workingDirectory, |
| 104 | + outputRedirection: outputRedirection, |
| 105 | + startNewProcessGroup: startNewProcessGroup, |
| 106 | + loggingHandler: loggingHandler |
| 107 | + ) |
| 108 | + return try await withTaskPriorityChangedHandler(initialPriority: Task.currentPriority) { @Sendable in |
| 109 | + setProcessPriority(pid: process.processID, newPriority: Task.currentPriority) |
| 110 | + return try await process.waitUntilExitSendingSigIntOnTaskCancellation() |
| 111 | + } taskPriorityChanged: { |
| 112 | + setProcessPriority(pid: process.processID, newPriority: Task.currentPriority) |
| 113 | + } |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +/// Set the priority of the given process to a value that's equivalent to `newPriority` on the current OS. |
| 118 | +private func setProcessPriority(pid: Process.ProcessID, newPriority: TaskPriority) { |
| 119 | + #if os(Windows) |
| 120 | + guard let handle = OpenProcess(UInt32(PROCESS_SET_INFORMATION), /*bInheritHandle*/ false, UInt32(pid)) else { |
| 121 | + logger.error("Failed to get process handle for \(pid) to change its priority: \(GetLastError())") |
| 122 | + return |
| 123 | + } |
| 124 | + defer { |
| 125 | + CloseHandle(handle) |
| 126 | + } |
| 127 | + if !SetPriorityClass(handle, UInt32(newPriority.windowsProcessPriority)) { |
| 128 | + logger.error("Failed to set process priority of \(pid) to \(newPriority.rawValue): \(GetLastError())") |
| 129 | + } |
| 130 | + #elseif canImport(Darwin) |
| 131 | + // `setpriority` is only able to decrease a process's priority and cannot elevate it. Since Swift task’s priorities |
| 132 | + // can only be elevated, this means that we can effectively only change a process's priority once, when it is created. |
| 133 | + // All subsequent calls to `setpriority` will fail. Because of this, don't log an error. |
| 134 | + setpriority(PRIO_PROCESS, UInt32(pid), newPriority.posixProcessPriority) |
| 135 | + #else |
| 136 | + setpriority(__priority_which_t(PRIO_PROCESS.rawValue), UInt32(pid), newPriority.posixProcessPriority) |
| 137 | + #endif |
| 138 | +} |
| 139 | + |
| 140 | +fileprivate extension TaskPriority { |
| 141 | + #if os(Windows) |
| 142 | + var windowsProcessPriority: Int32 { |
| 143 | + if self >= .high { |
| 144 | + // SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the normal priority class. |
| 145 | + return NORMAL_PRIORITY_CLASS |
| 146 | + } |
| 147 | + if self >= .medium { |
| 148 | + return BELOW_NORMAL_PRIORITY_CLASS |
| 149 | + } |
| 150 | + return IDLE_PRIORITY_CLASS |
| 151 | + } |
| 152 | + #else |
| 153 | + var posixProcessPriority: Int32 { |
| 154 | + if self >= .high { |
| 155 | + // SourceKit-LSP’s request handling runs at `TaskPriority.high`, which corresponds to the base 0 niceness value. |
| 156 | + return 0 |
| 157 | + } |
| 158 | + if self >= .medium { |
| 159 | + return 5 |
| 160 | + } |
| 161 | + if self >= .low { |
| 162 | + return 10 |
| 163 | + } |
| 164 | + return 15 |
| 165 | + } |
| 166 | + #endif |
| 167 | +} |
0 commit comments