Fix game preparation launched from Foundation process groups
This commit is contained in:
1 parent
8a6838d6b8
commit
4bc1fba541
6 files changed
+60
-10
No files matched your search
@@ -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:]])
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.4.1",
|
||||
"build": 6,
|
||||
"version": "1.4.2",
|
||||
"build": 7,
|
||||
"minimumMacOSMajor": 26
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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()<deadline:time.sleep(0.02)
|
||||
while (not marker.exists() or (foundation and not pidfile.exists())) and time.monotonic()<deadline:time.sleep(0.02)
|
||||
self.assertTrue(marker.exists())
|
||||
self.assertEqual(os.getpgid(task.pid),task.pid)
|
||||
self.assertNotEqual(os.getpgrp(),task.pid)
|
||||
os.killpg(task.pid,signal.SIGTERM)
|
||||
self.assertEqual(task.wait(timeout=5),-signal.SIGTERM)
|
||||
worker_pid=int(pidfile.read_text()) if foundation else task.pid
|
||||
self.assertEqual(os.getpgid(worker_pid),worker_pid)
|
||||
self.assertNotEqual(os.getpgrp(),worker_pid)
|
||||
os.killpg(worker_pid,signal.SIGTERM)
|
||||
self.assertEqual(task.wait(timeout=5),signal.SIGTERM if foundation else -signal.SIGTERM)
|
||||
finally:
|
||||
if task.poll() is None:
|
||||
os.killpg(task.pid,signal.SIGKILL);task.wait()
|
||||
if worker_pid is not None:
|
||||
try:os.killpg(worker_pid,signal.SIGKILL)
|
||||
except ProcessLookupError:pass
|
||||
task.kill();task.wait()
|
||||
Reference in new issue
Block a user