diff --git a/desktop/NxNativeEasy/build_session.py b/desktop/NxNativeEasy/build_session.py index 0d7ace9..d605a08 100644 --- a/desktop/NxNativeEasy/build_session.py +++ b/desktop/NxNativeEasy/build_session.py @@ -4,5 +4,8 @@ import os import sys if __name__ == '__main__': - os.setsid() + # Foundation Process already starts a separate process group on macOS. + # A group leader cannot call setsid(); its existing group is safe to cancel. + if os.getpgrp() != os.getpid(): + os.setsid() os.execv('/bin/bash', ['bash', *sys.argv[1:]]) diff --git a/desktop/release.json b/desktop/release.json index 31c6b80..187f9a0 100644 --- a/desktop/release.json +++ b/desktop/release.json @@ -1,5 +1,5 @@ { - "version": "1.4.1", - "build": 6, + "version": "1.4.2", + "build": 7, "minimumMacOSMajor": 26 } diff --git a/docs/agents/symptoms.md b/docs/agents/symptoms.md index ba62edf..5dc0ed9 100644 --- a/docs/agents/symptoms.md +++ b/docs/agents/symptoms.md @@ -5,6 +5,7 @@ One line per symptom in the [subsystem references](../reference/): what you see ## Packing - `decompress NSZ` stops immediately with `nsz: error: unrecognized arguments: --keys …`. → [NSZ decompression rejects `--keys`](../reference/packing.md#nsz-decompression-rejects---keys) +- clicking Prepare game stops before any build tools start, with `PermissionError: [Errno 1] Operation not permitted` at `build_session.py` `os.setsid()`. → [Prepare game fails at `os.setsid()`](../reference/packing.md#prepare-game-fails-at-ossetsid) ## Rewriter diff --git a/docs/personal-ios.md b/docs/personal-ios.md index c713f59..6940dee 100644 --- a/docs/personal-ios.md +++ b/docs/personal-ios.md @@ -292,3 +292,8 @@ tools and system build dependencies were already installed; initial installation of those system components, browser quarantine prompts, a fresh macOS user account and game compilation/gameplay were not exercised by this check. No game packages, keys or saves were used or modified. + +The clean-install check above did not start game preparation. A later user report +exposed a Foundation worker-launch failure, fixed and reproduced in 1.4.2; see +[Prepare game fails at os.setsid](reference/packing.md#prepare-game-fails-at-ossetsid). +The regression suite now exercises the actual Foundation launch mechanism. diff --git a/docs/reference/packing.md b/docs/reference/packing.md index c455192..fd69e06 100644 --- a/docs/reference/packing.md +++ b/docs/reference/packing.md @@ -8,3 +8,12 @@ - **Cause:** `nsz` 4.6.1 has no `--keys` argument. It loads `keys.txt` beside `sys.argv[0]`, or falls back to `~/.switch/prod.keys`, while importing its modules, before parsing arguments ([4.6.1 key loader](https://github.com/nicoboss/nsz/blob/4.6.1/nsz/nut/Keys.py)). Newer upstream code supports `--keys`; staging `keys.txt` also supports 4.6.1. - **Fix:** `scripts/build_ipa_from_nsp.py` `decompress_nsz` links the file selected by `-k` as `keys.txt` in the conversion's temporary folder and runs `nsz -D` there, with `sys.argv[0]` set to `nsz` before import. Input and output paths are absolute. The temporary folder and link are removed on success or failure; the user's default keys are untouched. - **Found on:** [Super Mario Odyssey](../../dm/games/0100000000010000/README.md), converting its v0 NSZ. Applies to NSZ and XCZ inputs for every game. + +## Desktop build worker + +### Prepare game fails at `os.setsid()` + +- **Symptom:** clicking Prepare game stops before any build tools start, with `PermissionError: [Errno 1] Operation not permitted` at `build_session.py` `os.setsid()`. +- **Cause:** Foundation `Process` already launches the worker as its own process-group leader on macOS. POSIX rejects `setsid()` for a group leader. A normal Python subprocess launch did not expose this difference, so the original regression test missed the GUI path. +- **Fix:** reuse the existing group when `os.getpgrp() == os.getpid()`; call `setsid()` only for a worker that still shares its parent's group. Cancellation still signals the worker's separate group. Do not suppress unrelated permission failures. Added regression coverage for Foundation `Process`, an existing group leader, an existing session leader and a normal subprocess launch, including cancellation. +- **Found on:** a user's clean desktop installation, before game detection; reproduced locally with a Swift Foundation launcher. Applies to every Mac/iPhone game preparation, independent of the game, keys or saves. Released in 1.4.2/build 7. diff --git a/scripts/tests/test_desktop_session.py b/scripts/tests/test_desktop_session.py index 81fb004..627e5d7 100644 --- a/scripts/tests/test_desktop_session.py +++ b/scripts/tests/test_desktop_session.py @@ -13,19 +13,51 @@ ROOT=Path(__file__).resolve().parents[2] @unittest.skipUnless(sys.platform=='darwin','macOS desktop worker') class DesktopSession(unittest.TestCase): def test_worker_has_separate_group_and_cancels(self): + self.check_worker() + + def test_worker_already_group_leader(self): + self.check_worker(preexec_fn=os.setpgrp) + + def test_worker_already_session_leader(self): + self.check_worker(start_new_session=True) + + def test_foundation_process_launch_and_cancellation(self): + with tempfile.TemporaryDirectory() as temporary: + root=Path(temporary);swift=root/'launch.swift';executable=root/'launch' + swift.write_text('''import Foundation +let task=Process() +task.executableURL=URL(fileURLWithPath:CommandLine.arguments[1]) +task.arguments=Array(CommandLine.arguments[2...4]) +try task.run() +try String(task.processIdentifier).write(toFile:CommandLine.arguments[5],atomically:true,encoding:.utf8) +task.waitUntilExit() +exit(task.terminationStatus) +''') + subprocess.run(['xcrun','swiftc',str(swift),'-o',str(executable)],check=True) + self.check_worker(foundation=executable) + + def check_worker(self,foundation=None,**options): with tempfile.TemporaryDirectory() as temporary: path=Path(temporary);marker=path/'ready' script=path/'worker.sh' script.write_text('echo ready > "$1"\nsleep 30\n') - task=subprocess.Popen([sys.executable,str(ROOT/'desktop/NxNativeEasy/build_session.py'),str(script),str(marker)]) + arguments=[sys.executable,str(ROOT/'desktop/NxNativeEasy/build_session.py'),str(script),str(marker)] + pidfile=path/'pid' + if foundation:arguments=[str(foundation),*arguments,str(pidfile)] + task=subprocess.Popen(arguments,**options) + worker_pid=None try: deadline=time.monotonic()+5 - while not marker.exists() and time.monotonic()