-
Notifications
You must be signed in to change notification settings - Fork 2
Mstoker/windows #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Mstoker/windows #29
Changes from all commits
7bffa89
a8b2597
823d084
c8e578b
8e8fe46
11bc4e5
95db2f7
db011ee
a94cee1
0d8bffa
1c07607
9d42f59
e15096a
2421c16
cc42896
c7c277d
34adb6a
682b4c2
f3f5baf
64e0663
89cc4eb
a237e3a
1ecf7cf
719aae0
cd0f26c
9867681
31134d9
033008e
b76bff0
ee5c22d
ecff70e
36e4873
a664f1c
2cbe817
58cb9ae
0d839eb
3a6f01e
6c2cc3a
00bdce7
2e18bff
e6e833e
14666ec
34102fa
2b5e647
964079f
697ca2c
037cc65
c2f00e6
964ff82
0eee4a8
50fab3c
1a6d320
56bc6b7
854d783
a5e1cee
1fa73d5
4e1cb32
e3674c7
6e3d2cd
c132fab
bb2201f
6683e4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,3 +11,4 @@ vendor | |
.bundle | ||
docs | ||
docs.tar.xz | ||
.vscode |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,11 +34,11 @@ extension Command { | |
} | ||
|
||
/// Run the command asynchronously, and return a stream open on process's stdout | ||
public func asyncStream() -> FileHandle { | ||
let pipe = Pipe() | ||
let write = pipe.fileHandleForWriting | ||
_ = async(fdMap: [ .stdout: write.fd ]) | ||
close(write.fileDescriptor) | ||
public func asyncStream() -> FDFileHandle { | ||
let pipe = FDPipe() | ||
_ = async(fdMap: [ .stdout: pipe.fileHandleForWriting.fileDescriptor ]) | ||
pipe.fileHandleForWriting.close() | ||
// TODO: Previously, this returned a FileHandle, but the test using it fails without returning an FDFileHandle, because otherwise, on return, the FDFileHandle is deallocate and calls close(). Can maintain liveness of the FDFileHandle in some way without changing the interface of this call? | ||
return pipe.fileHandleForReading | ||
} | ||
|
||
|
@@ -68,11 +68,10 @@ extension Command { | |
/// - Throws: if command fails | ||
/// - Returns: output as Data | ||
public func runData() throws -> Data { | ||
let pipe = Pipe() | ||
let write = pipe.fileHandleForWriting | ||
let result = async(fdMap: [ .stdout: write.fd ]) | ||
close(write.fileDescriptor) | ||
let data = pipe.fileHandleForReading.readDataToEndOfFile() | ||
let pipe = FDPipe() | ||
let result = async(fdMap: [ .stdout: pipe.fileHandleForWriting.fileDescriptor ]) | ||
pipe.fileHandleForWriting.close() | ||
let data = pipe.fileHandleForReading.handle.readDataToEndOfFile() | ||
try result.succeed() | ||
return data | ||
} | ||
|
@@ -89,6 +88,7 @@ extension Command { | |
throw InvalidString(data: data, encoding: encoding) | ||
} | ||
guard let trimStop = string.lastIndex(where: { $0 != "\n" }) else { | ||
// TODO: Shoudn't this return `string`? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the string is all newlines and should be completely trimmed. |
||
return "" | ||
} | ||
return String(string[...trimStop]) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,7 @@ | ||
import Foundation | ||
#if os(Windows) | ||
import WinSDK | ||
#endif | ||
|
||
/// Represents an external program invocation. It is the lowest-level command, that will spawn a subprocess when run. | ||
public class ExternalCommand: Command, CustomStringConvertible { | ||
|
@@ -9,7 +12,7 @@ public class ExternalCommand: Command, CustomStringConvertible { | |
|
||
public let description: String | ||
|
||
/// like "set -x", this will cause all external commands to print themselves when they run | ||
/// Like "set -x", this will cause all external commands to print themselves when they run | ||
public static var verbose: Bool = false | ||
|
||
internal var spawner: ProcessSpawner | ||
|
@@ -50,6 +53,8 @@ public class ExternalCommand: Command, CustomStringConvertible { | |
self.spawner = PosixSpawn() | ||
#elseif canImport(Glibc) | ||
self.spawner = LinuxSpawn() | ||
#elseif canImport(ucrt) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is mismatched with the import statement at the top of the file. They should both have the same #condition. |
||
self.spawner = WindowsSpawn() | ||
#endif | ||
} | ||
|
||
|
@@ -58,34 +63,60 @@ public class ExternalCommand: Command, CustomStringConvertible { | |
|
||
let name: String | ||
var command: Command | ||
let pid: pid_t | ||
let process: ProcessInformation | ||
|
||
private var _exitCode: Int32? | ||
private var _exitSemaphore = DispatchSemaphore(value: 0) | ||
private var _exitContinuations: [() -> Void] = [] | ||
|
||
init(command: ExternalCommand, pid: pid_t) { | ||
init(command: ExternalCommand, process: ProcessInformation) { | ||
self.command = command | ||
self.name = command.command | ||
self.pid = pid | ||
self.process = process | ||
|
||
command.spawner.reapAsync(pid: pid, queue: Result.reaperQueue) { [weak self] exitCode in | ||
command.spawner.reapAsync(process: process, queue: Result.reaperQueue) { [weak self] exitCode in | ||
self?._exitCode = exitCode | ||
self?._exitSemaphore.signal() | ||
self?._exitContinuations.forEach { $0() } | ||
self?._exitContinuations = [] | ||
} | ||
|
||
try? kill(signal: SIGCONT) | ||
try? command.spawner.resume(process: process) | ||
} | ||
|
||
var isRunning: Bool { | ||
Result.reaperQueue.sync { _exitCode == nil } | ||
} | ||
|
||
static let NtSuspendProcess: (ProcessInformation) -> Int32 = { | ||
// TODO: Tie into (undocumented) NtSuspendProcess() syscall | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be a noop |
||
return { _ in 0 } | ||
}() | ||
|
||
static let NtResumeProcess: (ProcessInformation) -> Int32 = { | ||
// TODO: Tie into (undocumented) NtResumeProcess() syscall | ||
return { _ in 0 } | ||
}() | ||
|
||
func kill(signal: Int32) throws { | ||
guard Foundation.kill(pid, signal) == 0 else { | ||
#if os(Windows) | ||
// TODO: figure out how to do this in-executable? | ||
if signal == SIGTERM || signal == SIGKILL { | ||
try cmd("taskkill.exe", "/F", "/pid", "\(process.id)").run() | ||
} else if signal == SIGSTOP { | ||
// Method borrowed from https://github.com/giampaolo/psutil/blob/a7e70bb66d5823f2cdcdf0f950bdbf26875058b4/psutil/arch/windows/proc.c#L539 | ||
// See also https://ntopcode.wordpress.com/2018/01/16/anatomy-of-the-thread-suspension-mechanism-in-windows-windows-internals/ | ||
_ = Self.NtSuspendProcess(process) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These errors shouldn't be ignored |
||
} else if signal == SIGCONT { | ||
_ = Self.NtResumeProcess(process) | ||
} else { | ||
throw PlatformError.killUnsupportedOnWindows | ||
} | ||
#else | ||
guard Foundation.kill(process.id, signal) == 0 else { | ||
throw SyscallError(name: "kill", command: command, errno: errno) | ||
} | ||
#endif | ||
} | ||
|
||
func succeed() throws { try defaultSucceed(name: name) } | ||
|
@@ -125,22 +156,22 @@ public class ExternalCommand: Command, CustomStringConvertible { | |
fdMap: fdMap, | ||
pathResolve: true | ||
) { | ||
case .success(let pid): | ||
return Result(command: self, pid: pid) | ||
case .success(let process): | ||
return Result(command: self, process: process) | ||
case .error(let err): | ||
return SyscallError(name: "launching \"\(command)\"", command: self, errno: err) | ||
} | ||
} | ||
} | ||
|
||
/// Convenience function for creating an extternal command. Does **not** run the command. | ||
/// Convenience function for creating an external command. Does **not** run the command. | ||
/// - Parameter command: The executable to run | ||
/// - Parameter arguments: The command line arguments to pass. No substitution is performed | ||
public func cmd(_ command: String, arguments: [String], addEnv: [String: String] = [:]) -> Command { | ||
ExternalCommand(command, arguments: arguments, addEnv: addEnv) | ||
} | ||
|
||
/// Convenience function for creating an extternal command. Does **not** run the command. | ||
/// Convenience function for creating an external command. Does **not** run the command. | ||
/// - Parameter command: The executable to run | ||
/// - Parameter arguments: The command line arguments to pass. No substitution is performed | ||
public func cmd(_ command: String, _ arguments: String..., addEnv: [String: String] = [:]) -> Command { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,50 @@ | ||||||
import Foundation | ||||||
|
||||||
/// A version of `FileHandle` that can be accessed by handle or fd at the same time | ||||||
public class FDFileHandle: CustomDebugStringConvertible { | ||||||
public let fileDescriptor: FileDescriptor | ||||||
public let handle: FileHandle | ||||||
private let closeOnDealloc: Bool | ||||||
private var isClosed: Bool | ||||||
|
||||||
public convenience init(fileDescriptor: FileDescriptor, closeOnDealloc: Bool) { | ||||||
// Construct the handle around the fd, but do not use closeOnDealloc, as this closes the fd! | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
let handle = FileHandle(fileDescriptor: fileDescriptor.rawValue, closeOnDealloc: false) | ||||||
self.init(fileDescriptor: fileDescriptor, handle: handle, closeOnDealloc: closeOnDealloc) | ||||||
} | ||||||
|
||||||
public init(fileDescriptor: FileDescriptor, handle: FileHandle, closeOnDealloc: Bool) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a strange interface to provide publicly. Is this needed? |
||||||
self.fileDescriptor = fileDescriptor | ||||||
self.handle = handle | ||||||
self.closeOnDealloc = closeOnDealloc | ||||||
self.isClosed = false | ||||||
// print("Created FDFileHandle for \(debugDescription)") | ||||||
} | ||||||
|
||||||
public func close() { | ||||||
precondition(!isClosed) | ||||||
close_fd(fileDescriptor) | ||||||
isClosed = true | ||||||
// print("Closed FDFileHandle for \(debugDescription)") | ||||||
} | ||||||
|
||||||
public var debugDescription: String { | ||||||
return "fd: \(fileDescriptor.rawValue) handle: \(handle) closeOnDealloc: \(closeOnDealloc)" | ||||||
} | ||||||
|
||||||
deinit { | ||||||
if closeOnDealloc && !isClosed { | ||||||
close() | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
fileprivate func close_fd(_ fileDescriptor: FileDescriptor) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be named "closeFD" or better yet, just inlined into "close" |
||||||
#if os(Windows) | ||||||
printOSCall("_close", fileDescriptor.rawValue) | ||||||
_close(fileDescriptor.rawValue) | ||||||
#else | ||||||
printOSCall("close", fileDescriptor.rawValue) | ||||||
close(fileDescriptor.rawValue) | ||||||
#endif | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import Foundation | ||
#if os(Windows) | ||
import WinSDK | ||
import ucrt | ||
#endif | ||
|
||
/// A version of `Pipe` that works more uniformly between windows and posix | ||
public class FDPipe { | ||
public let fileHandleForReading: FDFileHandle | ||
public let fileHandleForWriting: FDFileHandle | ||
|
||
public init() { | ||
#if os(Windows) | ||
// Adapted from libuv: | ||
// https://github.com/libuv/libuv/blob/34db4c21b1f3182a74091d927b10bb9830ef6717/src/win/pipe.c#L249 | ||
let uniquePipeName = "\\\\.\\pipe\\swsh-\(UUID().uuidString)-\(GetCurrentProcessId())" | ||
let pipeName: UnsafeMutablePointer<CChar>? = uniquePipeName.withCString(encodedAs: UTF8.self) { _strdup($0) } | ||
defer { free(pipeName) } | ||
let pipeMode = DWORD(PIPE_TYPE_BYTE) | DWORD(PIPE_READMODE_BYTE) | DWORD(PIPE_WAIT) | ||
let serverAccess = | ||
DWORD(PIPE_ACCESS_INBOUND) | | ||
DWORD(PIPE_ACCESS_OUTBOUND) | | ||
DWORD(WRITE_DAC) | | ||
DWORD(FILE_FLAG_FIRST_PIPE_INSTANCE) | ||
let clientAccess = | ||
DWORD(GENERIC_READ) | | ||
// DWORD(FILE_READ_ATTRIBUTES) | | ||
DWORD(GENERIC_WRITE) | | ||
// DWORD(FILE_WRITE_ATTRIBUTES) | | ||
DWORD(WRITE_DAC) | ||
|
||
printOSCall("CreateNamedPipeA", pipeName, serverAccess, pipeMode, 1, 65536, 65536, 0, nil) | ||
let serverPipe = CreateNamedPipeA( | ||
/* lpName */ pipeName, | ||
/* dwOpenMode */ serverAccess, | ||
/* dwPipeMode */ pipeMode, | ||
/* nMaxInstances */ 1, | ||
/* nOutBufferSize */ 65536, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably be a hex literal |
||
/* nInBufferSize */ 65536, | ||
/* nDefaultTimeOut */ 0, | ||
/* lpSecurityAttributes */ nil | ||
) | ||
guard serverPipe != INVALID_HANDLE_VALUE else { | ||
fatalError("Server pipe creation failed with error: \(WindowsSpawnImpl.Error(systemError: GetLastError()))") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This library shouldn't ever crash There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this init should throw since we aren't constrained by Pipe's API |
||
} | ||
|
||
var clientSecurityAttributes = SECURITY_ATTRIBUTES() | ||
clientSecurityAttributes.nLength = DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size) | ||
clientSecurityAttributes.lpSecurityDescriptor = nil | ||
clientSecurityAttributes.bInheritHandle = true | ||
printOSCall("CreateFileA", pipeName, clientAccess, 0, "ptr(\(clientSecurityAttributes))", "OPEN_EXISTING", 0, nil) | ||
let clientPipe = CreateFileA( | ||
/* lpFileName */ pipeName, | ||
/* dwDesiredAccess */ clientAccess, | ||
/* dwShareMode */ 0, | ||
/* lpSecurityAttributes */ &clientSecurityAttributes, | ||
/* dwCreationDisposition */ DWORD(OPEN_EXISTING), | ||
/* dwFlagsAndAttributes */ 0, | ||
/* hTemplateFile */ nil | ||
) | ||
guard clientPipe != INVALID_HANDLE_VALUE else { | ||
fatalError("Client pipe creation failed with error: \(WindowsSpawnImpl.Error(systemError: GetLastError()))") | ||
} | ||
|
||
printOSCall("ConnectNamedPipe", serverPipe, nil) | ||
guard ConnectNamedPipe(serverPipe, nil) || GetLastError() == ERROR_PIPE_CONNECTED else { | ||
fatalError("Pipe connection failed with error: \(WindowsSpawnImpl.Error(systemError: GetLastError()))") | ||
} | ||
|
||
printOSCall("_open_osfhandle", clientPipe, 0) | ||
let fileDescriptorForReading = FileDescriptor(_open_osfhandle(.init(bitPattern: clientPipe), _O_APPEND)) | ||
printOSCall("_open_osfhandle", serverPipe, 0) | ||
let fileDescriptorForWriting = FileDescriptor(_open_osfhandle(.init(bitPattern: serverPipe), _O_APPEND)) | ||
fileHandleForReading = FDFileHandle(fileDescriptor: fileDescriptorForReading, closeOnDealloc: true) | ||
fileHandleForWriting = FDFileHandle(fileDescriptor: fileDescriptorForWriting, closeOnDealloc: true) | ||
|
||
#else | ||
let pipe = Pipe() | ||
fileHandleForReading = FDFileHandle( | ||
fileDescriptor: FileDescriptor(pipe.fileHandleForReading.fileDescriptor), | ||
handle: pipe.fileHandleForReading, | ||
closeOnDealloc: true | ||
) | ||
fileHandleForWriting = FDFileHandle( | ||
fileDescriptor: FileDescriptor(pipe.fileHandleForWriting.fileDescriptor), | ||
handle: pipe.fileHandleForWriting, | ||
closeOnDealloc: true | ||
) | ||
#endif | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To do that, just duplicate the handle