Skip to content

Commit bd10954

Browse files
committed
WinUIBackend: Attach to parent console on launch (fixes stdout/err)
stdout and stderr used to go nowhere for apps compiled as GUI apps (i.e. linked with `/SUBSYSTEM:WINDOWS`). They now go to the parent's console.
1 parent 8416e83 commit bd10954

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

Sources/WinUIBackend/Console.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import WinSDK
2+
import Foundation
3+
4+
extension WinUIBackend {
5+
/// Attaches the app's standard IO streams to the parent's console.
6+
///
7+
/// This allows the stdout/stderr of SwiftCrossUI GUI apps to be
8+
/// viewed by starting them from the command line, even when they're
9+
/// built and linked as /SUBSYSTEM:WINDOWS apps (GUI apps). Without
10+
/// this fix the output of GUI apps is basically impossible to access.
11+
///
12+
/// Adapted from: https://stackoverflow.com/a/55875595/8268001
13+
static func attachToParentConsole() throws {
14+
try Self.releaseConsole()
15+
// -1 attaches to parent's console
16+
if AttachConsole(DWORD(bitPattern: -1)) {
17+
try Self.adjustConsoleBuffer(1024)
18+
try Self.redirectConsoleIO()
19+
}
20+
}
21+
22+
/// Releases existing files associated with the app's standard IO streams.
23+
private static func releaseConsole() throws {
24+
var fp = UnsafeMutablePointer<FILE>?.none
25+
guard
26+
freopen_s(&fp, "NUL:", "r", stdin) == 0,
27+
freopen_s(&fp, "NUL:", "w", stdout) == 0,
28+
freopen_s(&fp, "NUL:", "w", stdout) == 0,
29+
FreeConsole()
30+
else {
31+
throw Error(message: "Failed to release existing console")
32+
}
33+
}
34+
35+
/// Redirect the application's standard IO streams to the current console.
36+
private static func redirectConsoleIO() throws {
37+
var fp = UnsafeMutablePointer<FILE>?.none
38+
guard
39+
freopen_s(&fp, "CONIN$", "r", stdin) == 0,
40+
freopen_s(&fp, "CONOUT$", "w", stdout) == 0,
41+
freopen_s(&fp, "CONOUT$", "w", stderr) == 0
42+
else {
43+
throw Error(message: "Failed to redirect console IO")
44+
}
45+
}
46+
47+
/// Adjusts the size of the app's console output buffer.
48+
private static func adjustConsoleBuffer(_ minLength: SHORT) throws {
49+
let handle = GetStdHandle(STD_OUTPUT_HANDLE)
50+
var consoleInfo = CONSOLE_SCREEN_BUFFER_INFO();
51+
guard GetConsoleScreenBufferInfo(handle, &consoleInfo) else {
52+
throw Error(message: "Failed to get console screen buffer info")
53+
}
54+
if consoleInfo.dwSize.Y < minLength {
55+
consoleInfo.dwSize.Y = minLength
56+
}
57+
guard SetConsoleScreenBufferSize(handle, consoleInfo.dwSize) else {
58+
throw Error(message: "Failed to set console screen buffer size")
59+
}
60+
}
61+
}

Sources/WinUIBackend/WinUIBackend.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,26 @@ public final class WinUIBackend: AppBackend {
6666
internalState = InternalState()
6767
}
6868

69+
struct Error: LocalizedError {
70+
var message: String
71+
72+
var errorDescription: String? {
73+
message
74+
}
75+
}
76+
6977
public func runMainLoop(_ callback: @escaping () -> Void) {
78+
do {
79+
try Self.attachToParentConsole()
80+
} catch {
81+
// We essentially just ignore if this fails because it's just a QoL
82+
// debugging feature, and if it fails then any warning we print likely
83+
// won't get seen anyway. But I don't trust my Windows knowledge enough
84+
// to assert that it's impossible to view logs on failure, so let's
85+
// print a warning anyway.
86+
print("Warning: Failed to attach to parent console: \(error.localizedDescription)")
87+
}
88+
7089
WinUIApplication.callback = { application in
7190
// Toggle Switch has annoying default 'internal margins' (not Control
7291
// margins that we can set directly) that we can luckily get rid of by

0 commit comments

Comments
 (0)