-
Notifications
You must be signed in to change notification settings - Fork 6
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
Add an option to put WinAppDriver output in a file #151
Changes from 2 commits
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 |
---|---|---|
|
@@ -19,10 +19,16 @@ public class WinAppDriver: WebDriver { | |
|
||
private let httpWebDriver: HTTPWebDriver | ||
private let processTree: Win32ProcessTree? | ||
/// The write end of a pipe that is connected to the child process's stdin. | ||
private let childStdinHandle: HANDLE? | ||
|
||
private init(httpWebDriver: HTTPWebDriver, processTree: Win32ProcessTree? = nil) { | ||
private init( | ||
httpWebDriver: HTTPWebDriver, | ||
processTree: Win32ProcessTree? = nil, | ||
childStdinHandle: HANDLE? = nil) { | ||
self.httpWebDriver = httpWebDriver | ||
self.processTree = processTree | ||
self.childStdinHandle = childStdinHandle | ||
} | ||
|
||
public static func attach(ip: String = defaultIp, port: Int = defaultPort) -> WinAppDriver { | ||
|
@@ -34,12 +40,59 @@ public class WinAppDriver: WebDriver { | |
executablePath: String = defaultExecutablePath, | ||
ip: String = defaultIp, | ||
port: Int = defaultPort, | ||
waitTime: TimeInterval? = defaultStartWaitTime) throws -> WinAppDriver { | ||
|
||
waitTime: TimeInterval? = defaultStartWaitTime, | ||
outputFile: String? = nil) throws -> WinAppDriver { | ||
let processTree: Win32ProcessTree | ||
var childStdinHandle: HANDLE? = nil | ||
do { | ||
processTree = try Win32ProcessTree(path: executablePath, args: [ ip, String(port) ]) | ||
var launchOptions = ProcessLaunchOptions() | ||
if let outputFile = outputFile { | ||
// Open the output file for writing to the child stdout. | ||
var securityAttributes = SECURITY_ATTRIBUTES() | ||
securityAttributes.nLength = DWORD(MemoryLayout<SECURITY_ATTRIBUTES>.size) | ||
securityAttributes.bInheritHandle = true | ||
launchOptions.stdoutHandle = try outputFile.withCString(encodedAs: UTF16.self) { | ||
outputFile throws in | ||
Steelskin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
CreateFileW( | ||
UnsafeMutablePointer<WCHAR>(mutating: outputFile), DWORD(GENERIC_WRITE), | ||
DWORD(FILE_SHARE_READ), &securityAttributes, | ||
DWORD(OPEN_ALWAYS), DWORD(FILE_ATTRIBUTE_NORMAL), nil) | ||
} | ||
if launchOptions.stdoutHandle == INVALID_HANDLE_VALUE { | ||
// Failed to open the output file for writing. | ||
throw Win32Error.getLastError(apiName: "CreateFileW") | ||
} | ||
|
||
// Use the same handle for stderr. | ||
launchOptions.stderrHandle = launchOptions.stdoutHandle | ||
|
||
// WinAppDriver will close immediately if no stdin is provided so create a dummy | ||
// pipe here to keep stdin open until the child process is closed. | ||
var childReadInputHandle: HANDLE? | ||
if !CreatePipe(&childReadInputHandle, &childStdinHandle, &securityAttributes, 0) { | ||
CloseHandle(launchOptions.stdoutHandle) | ||
Steelskin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw Win32Error.getLastError(apiName: "CreatePipe") | ||
} | ||
launchOptions.stdinHandle = childReadInputHandle | ||
|
||
// Also use the parent console to stop spurious new consoles from spawning. | ||
launchOptions.spawnNewConsole = false | ||
} | ||
|
||
// Close our handles when the process has launched. The child process keeps a copy. | ||
defer { | ||
Steelskin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if let handle = launchOptions.stdoutHandle { | ||
CloseHandle(handle) | ||
} | ||
if let handle = launchOptions.stdinHandle { | ||
CloseHandle(handle) | ||
} | ||
} | ||
|
||
processTree = try Win32ProcessTree( | ||
path: executablePath, args: [ip, String(port)], options: launchOptions) | ||
} catch let error as Win32Error { | ||
CloseHandle(childStdinHandle) | ||
throw StartError(message: "Call to Win32 \(error.apiName) failed with error code \(error.errorCode).") | ||
} | ||
|
||
|
@@ -55,7 +108,10 @@ public class WinAppDriver: WebDriver { | |
} | ||
} | ||
|
||
return WinAppDriver(httpWebDriver: httpWebDriver, processTree: processTree) | ||
return WinAppDriver( | ||
httpWebDriver: httpWebDriver, | ||
processTree: processTree, | ||
childStdinHandle: childStdinHandle) | ||
} | ||
|
||
deinit { | ||
|
@@ -66,6 +122,9 @@ public class WinAppDriver: WebDriver { | |
assertionFailure("WinAppDriver did not terminate within the expected time: \(error).") | ||
} | ||
} | ||
if let childStdinHandle { | ||
CloseHandle(childStdinHandle) | ||
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. Do we need to keep the stdin write handle until here if just to close it? And since we have it, we could send
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. If we close the handle, this closes |
||
} | ||
} | ||
|
||
@discardableResult | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import TestsCommon | ||
import WinSDK | ||
import XCTest | ||
|
||
@testable import WebDriver | ||
@testable import WinAppDriver | ||
|
||
class AppDriverOptionsTest: XCTestCase { | ||
func tempFileName() -> String { | ||
return FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) | ||
.appendingPathExtension("txt").path | ||
} | ||
|
||
/// Tests that redirecting stdout to a file works. | ||
func testStdoutRedirectToFile() throws { | ||
let outputFile = try { | ||
Steelskin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Start a new instance of msinfo32 and write the output to a file. | ||
let outputFile = tempFileName() | ||
let _ = try MSInfo32App( | ||
winAppDriver: WinAppDriver.start( | ||
outputFile: outputFile | ||
)) | ||
return outputFile | ||
}() | ||
|
||
// Read the output file. | ||
let output = try String(contentsOfFile: outputFile, encoding: .utf16LittleEndian) | ||
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. Does WinAppDriver write as utf-16 when redirected to a file? That's unusual 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. It writes UTF-16 either way. It's usually not noticeable because Windows Terminal handles either. 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. Terminals are supposed to decode stdout according to the 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. Probably witchcraft involved: PS > $output = & 'C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe'
PS > [system.Text.Encoding]::ASCII.GetBytes($output)
87
0
105
0
[...]
PS > $output = echo potato
PS > [system.Text.Encoding]::ASCII.GetBytes($output)
112
111
116
97
116
111 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. I don't know if that's a valid test but it is puzzling! |
||
|
||
// Delete the file. | ||
try FileManager.default.removeItem(atPath: outputFile) | ||
|
||
XCTAssert(output.contains("msinfo32")) | ||
} | ||
} |
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.
Suggestion for more compact
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.
We should not set these handles to
INVALID_HANDLE_VALUE
becauseINVALID_HANDLE_VALUE
(= -1 = 0xfff...) is not actually invalid but is the current process handle. So here, we use the current process input/output handles as a fallback, which makes theredirectStdHandle
logic more complex than it should be.GetStdHandle()
will returnNULL
if the standard input/output devices have been closed, which is also a valid value to set thestdoutHandle
option to if you don't want the child process to write anything, for instance. IIUCNULL HANDLE
(=0) is a different value from Swiftnil
, since thestdoutHandle
in theProcessLaunchOptions
struct is an optional.