From 2cd113efa09c8e08fce270ff6dba0bbf4ea3afbb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 08:55:55 +0000 Subject: [PATCH 1/3] Fix Foundation.Process pipe deadlock vulnerability in AutomationExecutor Co-authored-by: NSEvent <44446865+NSEvent@users.noreply.github.com> --- .Jules/sentinel.md | 4 ++++ TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.Jules/sentinel.md b/.Jules/sentinel.md index 70f67594..3d52446e 100644 --- a/.Jules/sentinel.md +++ b/.Jules/sentinel.md @@ -18,3 +18,7 @@ **Vulnerability:** The application was falling back to storing OBS passwords as plaintext strings inside exported/saved JSON models (`Macro.swift` and `SystemCommand.swift`) if saving to the macOS Keychain failed. **Learning:** Saving secrets to unencrypted formats simply because secure storage fails is a critical anti-pattern known as "failing open" that results in data exposure. **Prevention:** Always fail securely. If secure storage operations fail, discard the sensitive credential in memory rather than writing it insecurely to disk, even if it requires the user to re-authenticate later. +## 2026-06-26 - [Foundation.Process Pipe Deadlock] +**Vulnerability:** Execution frameworks using `Foundation.Process` to run shell commands or scripts were vulnerable to a pipe deadlock and Denial of Service (DoS) if the child process output exceeded the OS pipe buffer (~64KB). +**Learning:** Calling `process.waitUntilExit()` before fully reading the child process's output pipe causes the parent to block waiting for the child, while the child blocks waiting for the parent to drain the pipe buffer. +**Prevention:** Always read data from the pipe (e.g., `pipe.fileHandleForReading.readDataToEndOfFile()`) *before* calling `process.waitUntilExit()` to ensure the pipe is drained as the child writes. diff --git a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift index 59854205..1b8c2bcd 100644 --- a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift +++ b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift @@ -354,9 +354,10 @@ public final class AutomationExecutor { } catch { return .failure("\(name) launch failed: \(error.localizedDescription)") } - process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + let output = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" From a7e51b061f46c5534c83526ad0a67599580eba7b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:18:47 +0000 Subject: [PATCH 2/3] Fix Foundation.Process pipe deadlock vulnerability in AutomationExecutor Co-authored-by: NSEvent <44446865+NSEvent@users.noreply.github.com> --- parse_ControllerService.py | 11 ++++++++ parse_OBSWebSocketLiveIntegrationTests.py | 11 ++++++++ parse_ScriptEngine.py | 11 ++++++++ parse_ScriptEngineSecurityTests.py | 11 ++++++++ parse_ScriptEngine_lines.py | 33 +++++++++++++++++++++++ parse_ScriptEngine_lines2.py | 30 +++++++++++++++++++++ parse_StreamDeckProfileParser.py | 11 ++++++++ parse_SystemCommandExecutor.py | 11 ++++++++ parse_UniversalControlMouseRelay.py | 11 ++++++++ parse_swift.py | 27 +++++++++++++++++++ parse_swift_runInTerminal.py | 27 +++++++++++++++++++ parse_swift_runOSAScript.py | 27 +++++++++++++++++++ parse_swift_runShell.py | 27 +++++++++++++++++++ parse_swift_shellRunner.py | 27 +++++++++++++++++++ parse_swift_systemSetting.py | 27 +++++++++++++++++++ parse_swift_testDeadlock.py | 30 +++++++++++++++++++++ parse_tests.py | 8 ++++++ print_test_content.py | 30 +++++++++++++++++++++ test_parse.swift | 16 +++++++++++ test_script.py | 27 +++++++++++++++++++ 20 files changed, 413 insertions(+) create mode 100644 parse_ControllerService.py create mode 100644 parse_OBSWebSocketLiveIntegrationTests.py create mode 100644 parse_ScriptEngine.py create mode 100644 parse_ScriptEngineSecurityTests.py create mode 100644 parse_ScriptEngine_lines.py create mode 100644 parse_ScriptEngine_lines2.py create mode 100644 parse_StreamDeckProfileParser.py create mode 100644 parse_SystemCommandExecutor.py create mode 100644 parse_UniversalControlMouseRelay.py create mode 100644 parse_swift.py create mode 100644 parse_swift_runInTerminal.py create mode 100644 parse_swift_runOSAScript.py create mode 100644 parse_swift_runShell.py create mode 100644 parse_swift_shellRunner.py create mode 100644 parse_swift_systemSetting.py create mode 100644 parse_swift_testDeadlock.py create mode 100644 parse_tests.py create mode 100644 print_test_content.py create mode 100644 test_parse.swift create mode 100644 test_script.py diff --git a/parse_ControllerService.py b/parse_ControllerService.py new file mode 100644 index 00000000..daaa094f --- /dev/null +++ b/parse_ControllerService.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Controller/ControllerService+SteamHID.swift', 'r') as f: + content = f.read() + +pattern = r'process\.waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_OBSWebSocketLiveIntegrationTests.py b/parse_OBSWebSocketLiveIntegrationTests.py new file mode 100644 index 00000000..4acbf9bd --- /dev/null +++ b/parse_OBSWebSocketLiveIntegrationTests.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift', 'r') as f: + content = f.read() + +pattern = r'process\.waitUntilExit\(\)|waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_ScriptEngine.py b/parse_ScriptEngine.py new file mode 100644 index 00000000..2794d20e --- /dev/null +++ b/parse_ScriptEngine.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: + content = f.read() + +pattern = r'waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_ScriptEngineSecurityTests.py b/parse_ScriptEngineSecurityTests.py new file mode 100644 index 00000000..8d4ccb59 --- /dev/null +++ b/parse_ScriptEngineSecurityTests.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapperTests/ScriptEngineSecurityTests.swift', 'r') as f: + content = f.read() + +pattern = r'waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_ScriptEngine_lines.py b/parse_ScriptEngine_lines.py new file mode 100644 index 00000000..e3d480c9 --- /dev/null +++ b/parse_ScriptEngine_lines.py @@ -0,0 +1,33 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+shell\s*[^)]*\)' +match = re.search(pattern, content) +if not match: + pattern = r'let\s+shell\s*:\s*@convention' + match = re.search(pattern, content) + +start_index = match.end() + +while content[start_index] != '{': + start_index += 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_ScriptEngine_lines2.py b/parse_ScriptEngine_lines2.py new file mode 100644 index 00000000..6c8a3bca --- /dev/null +++ b/parse_ScriptEngine_lines2.py @@ -0,0 +1,30 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: + content = f.read() + +pattern = r'let\s+shellAsync\s*:\s*@convention' +match = re.search(pattern, content) + +start_index = match.end() + +while content[start_index] != '{': + start_index += 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_StreamDeckProfileParser.py b/parse_StreamDeckProfileParser.py new file mode 100644 index 00000000..bd3c8354 --- /dev/null +++ b/parse_StreamDeckProfileParser.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Profile/StreamDeckProfileParser.swift', 'r') as f: + content = f.read() + +pattern = r'process\.waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_SystemCommandExecutor.py b/parse_SystemCommandExecutor.py new file mode 100644 index 00000000..9ed7fe3e --- /dev/null +++ b/parse_SystemCommandExecutor.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Input/SystemCommandExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_UniversalControlMouseRelay.py b/parse_UniversalControlMouseRelay.py new file mode 100644 index 00000000..5130436e --- /dev/null +++ b/parse_UniversalControlMouseRelay.py @@ -0,0 +1,11 @@ +import re + +with open('XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift', 'r') as f: + content = f.read() + +pattern = r'process\.waitUntilExit\(\)' +for match in re.finditer(pattern, content): + start = max(0, match.start() - 200) + end = min(len(content), match.end() + 200) + print(f"--- MATCH AT {match.start()} ---") + print(content[start:end]) diff --git a/parse_swift.py b/parse_swift.py new file mode 100644 index 00000000..859c49cf --- /dev/null +++ b/parse_swift.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+runProcess\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_runInTerminal.py b/parse_swift_runInTerminal.py new file mode 100644 index 00000000..8ba5f6a3 --- /dev/null +++ b/parse_swift_runInTerminal.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+runShellInTerminal\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_runOSAScript.py b/parse_swift_runOSAScript.py new file mode 100644 index 00000000..75431d66 --- /dev/null +++ b/parse_swift_runOSAScript.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+runOSAScript\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_runShell.py b/parse_swift_runShell.py new file mode 100644 index 00000000..22a7f095 --- /dev/null +++ b/parse_swift_runShell.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+runShell\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_shellRunner.py b/parse_swift_shellRunner.py new file mode 100644 index 00000000..802a0274 --- /dev/null +++ b/parse_swift_shellRunner.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'class\s+ShellCommandRunner\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_systemSetting.py b/parse_swift_systemSetting.py new file mode 100644 index 00000000..a5b62866 --- /dev/null +++ b/parse_swift_systemSetting.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+runSystemSetting\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_swift_testDeadlock.py b/parse_swift_testDeadlock.py new file mode 100644 index 00000000..cc792679 --- /dev/null +++ b/parse_swift_testDeadlock.py @@ -0,0 +1,30 @@ +import re + +with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+testShellCommandDrainsLargeOutputWithoutDeadlocking' +match = re.search(pattern, content) + +start_index = match.end() + +while content[start_index] != '{': + start_index += 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/parse_tests.py b/parse_tests.py new file mode 100644 index 00000000..3b171f9f --- /dev/null +++ b/parse_tests.py @@ -0,0 +1,8 @@ +import re + +with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: + content = f.read() + +funcs = re.findall(r'func\s+(test\w+)', content) +for func in funcs: + print(func) diff --git a/print_test_content.py b/print_test_content.py new file mode 100644 index 00000000..cc792679 --- /dev/null +++ b/print_test_content.py @@ -0,0 +1,30 @@ +import re + +with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+testShellCommandDrainsLargeOutputWithoutDeadlocking' +match = re.search(pattern, content) + +start_index = match.end() + +while content[start_index] != '{': + start_index += 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 diff --git a/test_parse.swift b/test_parse.swift new file mode 100644 index 00000000..3a347ad3 --- /dev/null +++ b/test_parse.swift @@ -0,0 +1,16 @@ + do { + try process.run() + } catch { + return .failure("\(name) launch failed: \(error.localizedDescription)") + } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + + let output = String(data: data, encoding: .utf8)? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + + if process.terminationStatus == 0 { + return .success(success) + } + return .failure(output.isEmpty ? "\(name) exit \(process.terminationStatus)" : output) diff --git a/test_script.py b/test_script.py new file mode 100644 index 00000000..576947b3 --- /dev/null +++ b/test_script.py @@ -0,0 +1,27 @@ +import re + +with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: + content = f.read() + +pattern = r'func\s+childPIDs\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' +match = re.search(pattern, content) + +start_index = match.end() - 1 + +count = 1 +i = start_index + 1 +in_string = False + +while i < len(content): + if content[i] == '"' and (i == 0 or content[i-1] != '\\'): + in_string = not in_string + + if not in_string: + if content[i] == '{': + count += 1 + elif content[i] == '}': + count -= 1 + if count == 0: + print(content[match.start():i+1]) + break + i += 1 From 644ce0751823c7f69f0e24042a6ffcb044f06e39 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:42:52 +0000 Subject: [PATCH 3/3] Fix Foundation.Process pipe deadlock vulnerability in all usages Co-authored-by: NSEvent <44446865+NSEvent@users.noreply.github.com> --- .../AutomationExecutor.swift | 2 +- .../Input/UniversalControlMouseRelay.swift | 3 +- .../OBSWebSocketLiveIntegrationTests.swift | 8 ++++- parse_ControllerService.py | 11 ------- parse_OBSWebSocketLiveIntegrationTests.py | 11 ------- parse_ScriptEngine.py | 11 ------- parse_ScriptEngineSecurityTests.py | 11 ------- parse_ScriptEngine_lines.py | 33 ------------------- parse_ScriptEngine_lines2.py | 30 ----------------- parse_StreamDeckProfileParser.py | 11 ------- parse_SystemCommandExecutor.py | 11 ------- parse_UniversalControlMouseRelay.py | 11 ------- parse_swift.py | 27 --------------- parse_swift_runInTerminal.py | 27 --------------- parse_swift_runOSAScript.py | 27 --------------- parse_swift_runShell.py | 27 --------------- parse_swift_shellRunner.py | 27 --------------- parse_swift_systemSetting.py | 27 --------------- parse_swift_testDeadlock.py | 30 ----------------- parse_tests.py | 8 ----- print_test_content.py | 30 ----------------- test_parse.swift | 16 --------- test_script.py | 27 --------------- 23 files changed, 9 insertions(+), 417 deletions(-) delete mode 100644 parse_ControllerService.py delete mode 100644 parse_OBSWebSocketLiveIntegrationTests.py delete mode 100644 parse_ScriptEngine.py delete mode 100644 parse_ScriptEngineSecurityTests.py delete mode 100644 parse_ScriptEngine_lines.py delete mode 100644 parse_ScriptEngine_lines2.py delete mode 100644 parse_StreamDeckProfileParser.py delete mode 100644 parse_SystemCommandExecutor.py delete mode 100644 parse_UniversalControlMouseRelay.py delete mode 100644 parse_swift.py delete mode 100644 parse_swift_runInTerminal.py delete mode 100644 parse_swift_runOSAScript.py delete mode 100644 parse_swift_runShell.py delete mode 100644 parse_swift_shellRunner.py delete mode 100644 parse_swift_systemSetting.py delete mode 100644 parse_swift_testDeadlock.py delete mode 100644 parse_tests.py delete mode 100644 print_test_content.py delete mode 100644 test_parse.swift delete mode 100644 test_script.py diff --git a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift index 1b8c2bcd..bb8eff77 100644 --- a/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift +++ b/TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift @@ -732,11 +732,11 @@ private final class ShellCommandRunner: @unchecked Sendable { pgrep.standardError = FileHandle.nullDevice do { try pgrep.run() - pgrep.waitUntilExit() } catch { return [] } let data = pipe.fileHandleForReading.readDataToEndOfFile() + pgrep.waitUntilExit() let output = String(data: data, encoding: .utf8) ?? "" return output .split(whereSeparator: \.isNewline) diff --git a/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift b/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift index 40e97b37..15ecd7cb 100644 --- a/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift +++ b/XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift @@ -1405,10 +1405,9 @@ final class UniversalControlMouseRelay: @unchecked Sendable { NSLog("[UCMouseRelay] Could not run tailscale status: %@", String(describing: error)) return [] } + let data = pipe.fileHandleForReading.readDataToEndOfFile() process.waitUntilExit() guard process.terminationStatus == 0 else { return [] } - - let data = pipe.fileHandleForReading.readDataToEndOfFile() guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let peers = object["Peer"] as? [String: Any] else { return [] diff --git a/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift b/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift index 722ba696..c2543442 100644 --- a/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift +++ b/XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift @@ -167,9 +167,9 @@ private enum OBSMediaMTXManager { which.standardOutput = outPipe which.standardError = Pipe() try? which.run() + let data = outPipe.fileHandleForReading.readDataToEndOfFile() which.waitUntilExit() if which.terminationStatus == 0 { - let data = outPipe.fileHandleForReading.readDataToEndOfFile() if let path = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty, @@ -246,6 +246,12 @@ private enum OBSMediaMTXManager { } catch { return false } + if let outPipe = p.standardOutput as? Pipe { + _ = outPipe.fileHandleForReading.readDataToEndOfFile() + } + if let errPipe = p.standardError as? Pipe { + _ = errPipe.fileHandleForReading.readDataToEndOfFile() + } p.waitUntilExit() return p.terminationStatus == 0 } diff --git a/parse_ControllerService.py b/parse_ControllerService.py deleted file mode 100644 index daaa094f..00000000 --- a/parse_ControllerService.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Controller/ControllerService+SteamHID.swift', 'r') as f: - content = f.read() - -pattern = r'process\.waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_OBSWebSocketLiveIntegrationTests.py b/parse_OBSWebSocketLiveIntegrationTests.py deleted file mode 100644 index 4acbf9bd..00000000 --- a/parse_OBSWebSocketLiveIntegrationTests.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapperTests/OBSWebSocketLiveIntegrationTests.swift', 'r') as f: - content = f.read() - -pattern = r'process\.waitUntilExit\(\)|waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_ScriptEngine.py b/parse_ScriptEngine.py deleted file mode 100644 index 2794d20e..00000000 --- a/parse_ScriptEngine.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: - content = f.read() - -pattern = r'waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_ScriptEngineSecurityTests.py b/parse_ScriptEngineSecurityTests.py deleted file mode 100644 index 8d4ccb59..00000000 --- a/parse_ScriptEngineSecurityTests.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapperTests/ScriptEngineSecurityTests.swift', 'r') as f: - content = f.read() - -pattern = r'waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_ScriptEngine_lines.py b/parse_ScriptEngine_lines.py deleted file mode 100644 index e3d480c9..00000000 --- a/parse_ScriptEngine_lines.py +++ /dev/null @@ -1,33 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+shell\s*[^)]*\)' -match = re.search(pattern, content) -if not match: - pattern = r'let\s+shell\s*:\s*@convention' - match = re.search(pattern, content) - -start_index = match.end() - -while content[start_index] != '{': - start_index += 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_ScriptEngine_lines2.py b/parse_ScriptEngine_lines2.py deleted file mode 100644 index 6c8a3bca..00000000 --- a/parse_ScriptEngine_lines2.py +++ /dev/null @@ -1,30 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Scripting/ScriptEngine.swift', 'r') as f: - content = f.read() - -pattern = r'let\s+shellAsync\s*:\s*@convention' -match = re.search(pattern, content) - -start_index = match.end() - -while content[start_index] != '{': - start_index += 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_StreamDeckProfileParser.py b/parse_StreamDeckProfileParser.py deleted file mode 100644 index bd3c8354..00000000 --- a/parse_StreamDeckProfileParser.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Profile/StreamDeckProfileParser.swift', 'r') as f: - content = f.read() - -pattern = r'process\.waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_SystemCommandExecutor.py b/parse_SystemCommandExecutor.py deleted file mode 100644 index 9ed7fe3e..00000000 --- a/parse_SystemCommandExecutor.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Input/SystemCommandExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_UniversalControlMouseRelay.py b/parse_UniversalControlMouseRelay.py deleted file mode 100644 index 5130436e..00000000 --- a/parse_UniversalControlMouseRelay.py +++ /dev/null @@ -1,11 +0,0 @@ -import re - -with open('XboxControllerMapper/XboxControllerMapper/Services/Input/UniversalControlMouseRelay.swift', 'r') as f: - content = f.read() - -pattern = r'process\.waitUntilExit\(\)' -for match in re.finditer(pattern, content): - start = max(0, match.start() - 200) - end = min(len(content), match.end() + 200) - print(f"--- MATCH AT {match.start()} ---") - print(content[start:end]) diff --git a/parse_swift.py b/parse_swift.py deleted file mode 100644 index 859c49cf..00000000 --- a/parse_swift.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+runProcess\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_runInTerminal.py b/parse_swift_runInTerminal.py deleted file mode 100644 index 8ba5f6a3..00000000 --- a/parse_swift_runInTerminal.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+runShellInTerminal\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_runOSAScript.py b/parse_swift_runOSAScript.py deleted file mode 100644 index 75431d66..00000000 --- a/parse_swift_runOSAScript.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+runOSAScript\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_runShell.py b/parse_swift_runShell.py deleted file mode 100644 index 22a7f095..00000000 --- a/parse_swift_runShell.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+runShell\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_shellRunner.py b/parse_swift_shellRunner.py deleted file mode 100644 index 802a0274..00000000 --- a/parse_swift_shellRunner.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'class\s+ShellCommandRunner\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_systemSetting.py b/parse_swift_systemSetting.py deleted file mode 100644 index a5b62866..00000000 --- a/parse_swift_systemSetting.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+runSystemSetting\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_swift_testDeadlock.py b/parse_swift_testDeadlock.py deleted file mode 100644 index cc792679..00000000 --- a/parse_swift_testDeadlock.py +++ /dev/null @@ -1,30 +0,0 @@ -import re - -with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+testShellCommandDrainsLargeOutputWithoutDeadlocking' -match = re.search(pattern, content) - -start_index = match.end() - -while content[start_index] != '{': - start_index += 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/parse_tests.py b/parse_tests.py deleted file mode 100644 index 3b171f9f..00000000 --- a/parse_tests.py +++ /dev/null @@ -1,8 +0,0 @@ -import re - -with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: - content = f.read() - -funcs = re.findall(r'func\s+(test\w+)', content) -for func in funcs: - print(func) diff --git a/print_test_content.py b/print_test_content.py deleted file mode 100644 index cc792679..00000000 --- a/print_test_content.py +++ /dev/null @@ -1,30 +0,0 @@ -import re - -with open('TriggerKit/Tests/TriggerKitRuntimeTests/AutomationExecutorTests.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+testShellCommandDrainsLargeOutputWithoutDeadlocking' -match = re.search(pattern, content) - -start_index = match.end() - -while content[start_index] != '{': - start_index += 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1 diff --git a/test_parse.swift b/test_parse.swift deleted file mode 100644 index 3a347ad3..00000000 --- a/test_parse.swift +++ /dev/null @@ -1,16 +0,0 @@ - do { - try process.run() - } catch { - return .failure("\(name) launch failed: \(error.localizedDescription)") - } - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - process.waitUntilExit() - - let output = String(data: data, encoding: .utf8)? - .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - - if process.terminationStatus == 0 { - return .success(success) - } - return .failure(output.isEmpty ? "\(name) exit \(process.terminationStatus)" : output) diff --git a/test_script.py b/test_script.py deleted file mode 100644 index 576947b3..00000000 --- a/test_script.py +++ /dev/null @@ -1,27 +0,0 @@ -import re - -with open('TriggerKit/Sources/TriggerKitRuntime/AutomationExecutor.swift', 'r') as f: - content = f.read() - -pattern = r'func\s+childPIDs\s*\([^)]*\)\s*(async)?\s*(throws)?\s*->\s*[^{]*{' -match = re.search(pattern, content) - -start_index = match.end() - 1 - -count = 1 -i = start_index + 1 -in_string = False - -while i < len(content): - if content[i] == '"' and (i == 0 or content[i-1] != '\\'): - in_string = not in_string - - if not in_string: - if content[i] == '{': - count += 1 - elif content[i] == '}': - count -= 1 - if count == 0: - print(content[match.start():i+1]) - break - i += 1