From 4ae311b14e39a9e6a4f325276fe177a463980b2f Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:16:18 +0200 Subject: [PATCH 1/3] close-league-client-in-game: don't block closes right after boot last_close started at 0.0 and the cooldown compares it with time.monotonic(), which counts from boot. For the first 30 s of uptime every close was refused as "in cooldown". That made the tests flaky on freshly booted CI runners (seen on PR #76). Use None for "never closed". Co-Authored-By: Claude Opus 5.5 --- tests/test_close_league_client_in_game.py | 14 +++++++++++++- tools/close-league-client-in-game.py | 8 +++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/test_close_league_client_in_game.py b/tests/test_close_league_client_in_game.py index aeddd38..c72db0f 100644 --- a/tests/test_close_league_client_in_game.py +++ b/tests/test_close_league_client_in_game.py @@ -91,6 +91,17 @@ class InGameCloseTests(unittest.TestCase): self.assertIn('closed the client UI for the running match (2 processes: 123,124)', self.output.getvalue()) + def test_match_soon_after_boot_still_closes_the_client_ui(self): + # time.monotonic() counts from boot; a fresh machine (or CI runner) is + # still inside the first cooldown window. + self.client_ui(123) + self.process(125, GAME) + with patch.object(in_game.time, 'monotonic', return_value=5.0): + result, calls = self.run_once() + self.assertEqual(result, 0) + self.assertEqual(sorted(calls), sorted([(123, in_game.signal.SIGTERM), + (123, in_game.signal.SIGKILL)])) + def test_marker_in_arguments_does_not_make_a_process_client_ui(self): self.client_ui(123) self.process(124, CRASH_HANDLER, args=CRASH_ARGUMENTS) @@ -209,7 +220,8 @@ class InGameCloseTests(unittest.TestCase): def test_close_decision_covers_limits_and_cooldown(self): state = {'game': True, 'ui': {'123': 1}} - self.assertEqual(in_game.close_decision(state, 0, 0.0, 100.0), 'close') + self.assertEqual(in_game.close_decision(state, 0, None, 100.0), 'close') + self.assertEqual(in_game.close_decision(state, 0, None, 5.0), 'close') self.assertIsNone(in_game.close_decision(state, 0, 90.0, 100.0)) self.assertEqual(in_game.close_decision(state, in_game.MAX_CLOSES, 0.0, 100.0), 'limit') self.assertIsNone(in_game.close_decision({'game': False, 'ui': {'123': 1}}, 0, 0.0, 100.0)) diff --git a/tools/close-league-client-in-game.py b/tools/close-league-client-in-game.py index 3a7b008..b44e13f 100755 --- a/tools/close-league-client-in-game.py +++ b/tools/close-league-client-in-game.py @@ -160,7 +160,9 @@ def close_decision(state, closes, last_close, now): return None if closes >= MAX_CLOSES: return 'limit' - if now - last_close < COOLDOWN_SECONDS: + # last_close is None until the first close: monotonic time counts from boot, + # so a 0.0 start would block closes for the first COOLDOWN_SECONDS of uptime. + if last_close is not None and now - last_close < COOLDOWN_SECONDS: return None return 'close' @@ -189,7 +191,7 @@ def supervise(proc_root=Path('/proc'), prefix=None, once=False, dry_run=False, o 'opt out with RCLOL_CLOSE_CLIENT_IN_GAME=0' % prefix, file=out, flush=True) in_game = False closes = 0 - last_close = 0.0 + last_close = None limit_reported = False seen = False idle_since = None @@ -203,7 +205,7 @@ def supervise(proc_root=Path('/proc'), prefix=None, once=False, dry_run=False, o if state['game'] and not in_game: in_game = True closes = 0 - last_close = 0.0 + last_close = None limit_reported = False print('close-league-client-in-game: a match started', file=out, flush=True) elif not state['game'] and in_game: From fd8732d4bc0f5fee24e66fe0055e5e869f5ac3df Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:17:22 +0200 Subject: [PATCH 2/3] update.sh: replace the shared serial and UUID automatically Older setups shipped serial C02ZL0GVMD6N and UUID 6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B to every install. After installing, update.sh now runs configure-darling-platform.py --replace-known-defaults, which provisions the prefix only when its saved identity still has one of those values. Other custom values (imported serials/UUIDs, model, board, manufacturer, ROM, MLB) are kept; prefixes with no saved identity or only custom values are not touched. provision() also drops a standalone system-id or provisioning_UDID derived from the shared UUID instead of keeping it. Co-Authored-By: Claude Opus 5.5 --- docs/PLATFORM_IDENTITY.md | 10 ++++++-- scripts/update.sh | 9 +++++++ tests/test_platform_provisioning.py | 40 +++++++++++++++++++++++++++++ tools/configure-darling-platform.py | 30 +++++++++++++++++++--- 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/PLATFORM_IDENTITY.md b/docs/PLATFORM_IDENTITY.md index 3d132ea..111afa5 100644 --- a/docs/PLATFORM_IDENTITY.md +++ b/docs/PLATFORM_IDENTITY.md @@ -90,7 +90,12 @@ python3 tools/configure-darling-platform.py --prefix /absolute/prefix --provisio Launch Darling normally afterwards. This requires the updated iokitd from this PR; the config tool alone cannot update an older running daemon. Provisioning -is never called automatically by installation or launch. +is never called automatically by installation or launch. The one exception is +`scripts/update.sh`: after installing, it runs +`configure-darling-platform.py --replace-known-defaults --write`, which +provisions the prefix only if its saved identity still contains the shared +serial or UUID below. Prefixes without saved identity files, or whose saved +values are all custom, are left byte-for-byte unchanged. Missing serials use 12 characters and missing MLBs use 17 characters, sampled with Python's `secrets` module from `0123456789ABCDEFGHJKLMNPQRSTUVWXYZ` (Base-34). @@ -106,7 +111,8 @@ example with your chosen unicast OUI). It only affects a missing ROM. An OUI does not make a generated identity Apple-issued. Provisioning replaces the known shared serial `C02ZL0GVMD6N` and UUID -`6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B`, along with the latter's derived fields. +`6F3B2A1E-8C4D-4E5F-9A7B-1D2C3E4F5A6B`, along with the latter's derived fields +(including a standalone `system-id` or `provisioning_UDID` derived from it). It preserves other existing custom values, including imported serials and UUIDs that do not follow the generated formats. A standalone custom `system-id` is preserved by deriving its UUID instead of generating a different one. Model, diff --git a/scripts/update.sh b/scripts/update.sh index f8e997c..62d0c5c 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -7,6 +7,13 @@ set -eu repo=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) cd "$repo" +# Older setups shipped one serial and UUID shared by every install. Give a prefix that still +# has them its own identity; any other saved values, and prefixes without them, are left alone. +replace_shared_identity() { + python3 build/runtime/tools/stop-darling-runtime.py + python3 tools/configure-darling-platform.py --replace-known-defaults --write || \ + echo 'Could not replace the shared platform identity; run tools/configure-darling-platform.py --provision --write' >&2 +} if [ -n "${DARLING_WORKSPACE:-}" ]; then python3 scripts/nix-idle.py # Initial setup and upgrades use the same supported user-owned install path. @@ -21,6 +28,7 @@ if [ -n "${DARLING_WORKSPACE:-}" ]; then cmake --install build/darling-src/build python3 scripts/nix-state.py stamp-base ./scripts/install-runtime.sh + replace_shared_identity echo 'Rootless update installed. In this shell use scripts/run.sh; otherwise exit this shell and start darling-lol.' exit 0 fi @@ -45,4 +53,5 @@ PY python3 build/runtime/tools/stop-darling-runtime.py sudo cmake --install build/darling-src/build ./scripts/install-runtime.sh +replace_shared_identity echo 'Update installed. Start Darling with: sh scripts/run.sh' diff --git a/tests/test_platform_provisioning.py b/tests/test_platform_provisioning.py index 48bd0c0..ce195b5 100644 --- a/tests/test_platform_provisioning.py +++ b/tests/test_platform_provisioning.py @@ -157,3 +157,43 @@ class ProvisioningChecks(unittest.TestCase): self.assertEqual(platform.provision(supplied)['IOPlatformUUID'], value['IOPlatformUUID']) with self.assertRaises(ValueError): platform.validate({'IOPlatformUUID': value['IOPlatformUUID'], 'provisioning_UDID': str(uuid.uuid4())}) + + def test_replace_known_defaults_only_touches_shared_identities(self): + replace = [sys.executable, str(SCRIPT), '--prefix', str(self.prefix), '--runtime-root', + str(self.runtime), '--replace-known-defaults', '--write'] + # A prefix without identity files is not created or provisioned. + result = subprocess.run(replace, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(self.prefix.exists()) + # A custom identity stays byte-for-byte unchanged. + custom = {'IOPlatformSerialNumber': 'MY-SERIAL', 'model': 'CustomModel'} + platform.save(self.prefix, custom) + canonical = platform.destination(self.prefix) + before = canonical.read_bytes() + result = subprocess.run(replace, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(canonical.read_bytes(), before) + self.assertFalse((self.prefix / 'private/etc/darling-smbios.conf').exists()) + # The shared values are replaced; custom fields survive and a rerun is stable. + platform.save(self.prefix, {**custom, 'IOPlatformSerialNumber': platform.KNOWN_SERIAL, + 'IOPlatformUUID': platform.KNOWN_UUID}) + result = subprocess.run(replace, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + value = platform.load(canonical) + self.assertNotEqual(value['IOPlatformSerialNumber'], platform.KNOWN_SERIAL) + self.assertNotEqual(value['IOPlatformUUID'], platform.KNOWN_UUID) + self.assertFalse(platform.has_known_defaults(value)) + self.assertEqual(value['model'], 'CustomModel') + self.assertNotIn('board-id', value) + self.assertNotIn('manufacturer', value) + self.assertEqual(platform.load(self.prefix / 'private/etc/darling-smbios.conf'), value) + after = canonical.read_bytes() + subprocess.run(replace, capture_output=True, text=True, check=True) + self.assertEqual(canonical.read_bytes(), after) + + def test_standalone_fields_derived_from_known_uuid_are_replaced(self): + known_id = uuid.UUID(platform.KNOWN_UUID).bytes_le + for supplied in [{'system-id': known_id}, {'provisioning_UDID': platform.KNOWN_UUID}]: + value = platform.provision(supplied) + self.assertNotEqual(value['IOPlatformUUID'], platform.KNOWN_UUID) + self.assertNotEqual(value['system-id'], known_id) diff --git a/tools/configure-darling-platform.py b/tools/configure-darling-platform.py index 7332dca..9b3408a 100644 --- a/tools/configure-darling-platform.py +++ b/tools/configure-darling-platform.py @@ -182,13 +182,23 @@ def save(prefix, properties): return atomic_write(destination(prefix), encoded) +def has_known_defaults(properties): + """True when an identity still carries the shared serial, UUID, or a field derived from that UUID.""" + return (properties.get('IOPlatformSerialNumber') == KNOWN_SERIAL + or str(properties.get('IOPlatformUUID', '')).upper() == KNOWN_UUID + or str(properties.get('provisioning_UDID', '')).upper() == KNOWN_UUID + or properties.get('system-id') == uuid.UUID(KNOWN_UUID).bytes_le) + + def provision(properties, rom_oui=None): """Fill missing identifiers, replacing only the two known shared defaults.""" result = validate(properties) if result.get('IOPlatformSerialNumber') == KNOWN_SERIAL: result.pop('IOPlatformSerialNumber') - if result.get('IOPlatformUUID', '').upper() == KNOWN_UUID: - result.pop('IOPlatformUUID') + if (result.get('IOPlatformUUID', '').upper() == KNOWN_UUID + or result.get('provisioning_UDID', '').upper() == KNOWN_UUID + or result.get('system-id') == uuid.UUID(KNOWN_UUID).bytes_le): + result.pop('IOPlatformUUID', None) result.pop('system-id', None) result.pop('provisioning_UDID', None) for key, length in [('IOPlatformSerialNumber', 12), ('mlb-serial-number', 17)]: @@ -274,8 +284,14 @@ def main(argv=None): parser.add_argument('--provision', action='store_true', help='Generate missing synthetic identifiers and provision prefix metadata') parser.add_argument('--runtime-root', type=Path, help='Runtime supplying SystemVersion.plist (default: saved SDK or /usr/local/libexec/darling)') parser.add_argument('--rom-oui', help='Explicit three-byte unicast OUI for a missing ROM; default is locally administered') + parser.add_argument('--replace-known-defaults', action='store_true', + help='Provision only if the prefix identity still has the shared serial or UUID; otherwise change nothing') parser.add_argument('--write', action='store_true', help='Write validated overrides to a stopped prefix (otherwise preview only)') args = parser.parse_args(argv) + if args.replace_known_defaults: + if args.source or args.set: + parser.error('--replace-known-defaults cannot be combined with --import or --set') + args.provision = True config = Path(__file__).resolve().parents[1] / 'build/config.json' saved = json.loads(config.read_text()) if config.exists() else {} prefix = args.prefix or Path(os.environ.get('DPREFIX') or saved.get('prefix', str(Path.home() / '.darling'))) @@ -288,11 +304,19 @@ def main(argv=None): rom_oui = bytes.fromhex(args.rom_oui.replace(':', '').replace('-', '')) if len(rom_oui) != 3 or rom_oui[0] & 1: raise ValueError('--rom-oui requires exactly three unicast bytes') + identity_files = [checked_path(prefix, 'private/etc/darling-smbios.conf'), destination(prefix)] + if args.replace_known_defaults and not any(path.exists() for path in identity_files): + print('No saved platform identity; nothing to replace.') + return 0 with prefix_lock(prefix) if args.write else nullcontext(): path = destination(prefix) properties = {} + if args.replace_known_defaults and not any( + path.exists() and has_known_defaults(load(path)) for path in identity_files): + print('Platform identity has no shared defaults; nothing changed.') + return 0 if args.provision: - legacy = checked_path(prefix, 'private/etc/darling-smbios.conf') + legacy = identity_files[0] if legacy.exists(): properties.update(load(legacy)) if path.exists(): From c068e32152bd6b83b7e81dcef515bff980720b37 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:22:07 +0200 Subject: [PATCH 3/3] install-runtime.sh: provision a platform identity for new prefixes When install-runtime.sh creates the prefix (first-time setup, regular or Nix), it now runs configure-darling-platform.py --provision --write once the runtime is stopped, so each new install gets its own synthetic serial, MLB, UUID and ROM. A prefix counts as new the same way Darling decides it: the directory did not exist before the first `darling shell`. Existing prefixes keep their identity on reinstall and update. Co-Authored-By: Claude Opus 5.5 --- docs/PLATFORM_IDENTITY.md | 17 ++++++++++------- scripts/install-runtime.sh | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/PLATFORM_IDENTITY.md b/docs/PLATFORM_IDENTITY.md index 111afa5..5f25849 100644 --- a/docs/PLATFORM_IDENTITY.md +++ b/docs/PLATFORM_IDENTITY.md @@ -1,9 +1,12 @@ # Explicit platform identity overrides -By default, Darling keeps the existing host-derived UUID and `DARLING-` serial. -No override file is generated by installation or launch. Import mode uses values supplied by the user. Explicit `--provision` mode can -generate synthetic identifiers; it does not issue authentic Apple identities -or change CPU, GPU, or security metadata. +First-time setup (`scripts/install-runtime.sh` creating a new prefix) runs +`--provision --write` once, so each new prefix gets its own synthetic identity. +A prefix without saved identity files keeps the host-derived UUID and +`DARLING-` serial; launching never generates an override. Import mode uses +values supplied by the user. `--provision` generates synthetic identifiers; it +does not issue authentic Apple identities or change CPU, GPU, or security +metadata. Preview an OpenCore `config.plist`, a flat GenSMBIOS JSON file, or an existing `darling-smbios.conf` without changing the selected prefix: @@ -89,9 +92,9 @@ python3 tools/configure-darling-platform.py --prefix /absolute/prefix --provisio ``` Launch Darling normally afterwards. This requires the updated iokitd from this -PR; the config tool alone cannot update an older running daemon. Provisioning -is never called automatically by installation or launch. The one exception is -`scripts/update.sh`: after installing, it runs +PR; the config tool alone cannot update an older running daemon. Besides +first-time setup above, provisioning runs automatically in one other case: +after installing, `scripts/update.sh` runs `configure-darling-platform.py --replace-known-defaults --write`, which provisions the prefix only if its saved identity still contains the shared serial or UUID below. Prefixes without saved identity files, or whose saved diff --git a/scripts/install-runtime.sh b/scripts/install-runtime.sh index 677b2ab..9628760 100755 --- a/scripts/install-runtime.sh +++ b/scripts/install-runtime.sh @@ -16,6 +16,9 @@ if [ -d build/venv/bin ]; then PATH="$repo/build/venv/bin:$PATH"; export PATH; f # A server killed by the base install leaves a stale .init.pid that makes darling shell fail. python3 build/runtime/tools/ensure-darling-runtime.py # Initialize the normal user-owned prefix before installing overrides. +# Darling treats a missing prefix directory as new; only then is an identity generated below. +new_prefix= +[ -e "$prefix_dir" ] || new_prefix=1 darling shell /usr/bin/true # The prefix has no /private/tmp of its own, so the guest falls through to the root-owned # installed layer and every write there (the Riot installer's temp files) fails with EACCES. @@ -26,6 +29,12 @@ chmod 1777 "$prefix/private/tmp" python3 build/runtime/tools/ensure-darling-root-certificates.py || \ echo 'Darling system root store not installed; the launcher retries it' >&2 python3 build/runtime/tools/stop-darling-runtime.py +# Give a new prefix its own synthetic serial, MLB, UUID and ROM instead of the host-derived defaults. +# Existing prefixes keep their identity; update.sh only replaces the old shared serial and UUID. +if [ -n "$new_prefix" ]; then + python3 tools/configure-darling-platform.py --prefix "$prefix_dir" --provision --write || \ + echo 'Platform identity not provisioned; run tools/configure-darling-platform.py --provision --write' >&2 +fi sh build/runtime/tools/install-riot-presentation-runtime.sh sh build/runtime/tools/install-riot-gpu-runtime.sh # darling-lol perf: build and install optimized libc/libc++/libplatform.