fix(sync): package user service helpers

This commit is contained in:
fy59 2026-09-17 15:47:15 +02:00
parent cc87f26b98
commit dd8c91a946
6 changed files with 113 additions and 23 deletions

View file

@ -2,6 +2,10 @@
## Unreleased — 0.1.2 ## Unreleased — 0.1.2
- Packaged `trainlog-sync-once` and `trainlog-syncd` with the relocatable sync
candidate so user services cannot mix a versioned install with live-checkout
helpers during rollout.
- Closed the native candidate warning gate with checked path and HTTP response - Closed the native candidate warning gate with checked path and HTTP response
bounds, strict GCC and Clang builds, full Clang ASan/UBSan validation, and a bounds, strict GCC and Clang builds, full Clang ASan/UBSan validation, and a
stable concurrent object-I/O MTP double. Rebuilt the desktop, current Android stable concurrent object-I/O MTP double. Rebuilt the desktop, current Android

View file

@ -14,6 +14,9 @@ Clang ASan/UBSan build passes it with leak detection. Bounded path assembly and
the Web sync accepted response reject insufficient capacity without publishing the Web sync accepted response reject insufficient capacity without publishing
partial output. Fresh desktop, current-Android and schema-v17 bridge candidates partial output. Fresh desktop, current-Android and schema-v17 bridge candidates
were rebuilt from commit `2c5a8be2827e31fc5be2accb65073bfeb79d6068`. were rebuilt from commit `2c5a8be2827e31fc5be2accb65073bfeb79d6068`.
The rollout packager now keeps the legacy request daemon and one-shot helper in
the same relocatable, hashed bundle as the Web generation coordinator, avoiding
mixed-checkout user-service execution during the controlled rollout.
Snapshot date: **2026-09-17**. Snapshot date: **2026-09-17**.

View file

@ -22,6 +22,10 @@ the separately authorized rollout are complete.
1. Build and validate one revision, then run `python3 1. Build and validate one revision, then run `python3
tools/package_sync_candidate.py --output <new-private-directory>`. Its tools/package_sync_candidate.py --output <new-private-directory>`. Its
inventory records hashes, schemas, protocols, entry points and dependencies. inventory records hashes, schemas, protocols, entry points and dependencies.
The relocatable bundle includes `trainlog`, `trainlog-sync-once`,
`trainlog-syncd`, the production generation MTP adapter and their matching
Python codecs. User units must point only into one versioned bundle; they
must not combine a packaged binary with helpers from a live checkout.
2. `assembleDebug` is development evidence only. Rollout requires the existing 2. `assembleDebug` is development evidence only. Rollout requires the existing
release signing identity, verification of its public certificate and a valid release signing identity, verification of its public certificate and a valid
increasing `versionCode`; this mission reads no signing secret. increasing `versionCode`; this mission reads no signing secret.

View file

@ -1333,6 +1333,12 @@ JDK 17. The real Firefox/object-I/O-boundary MTP conversation also passes after
formatting. A source comparison against the pre-readability commit confirms formatting. A source comparison against the pre-readability commit confirms
that the scoped test names and assertion counts are unchanged. that the scoped test names and assertion counts are unchanged.
The deployment-tools regression also constructs the relocatable candidate and
requires its inventory to include the packaged `trainlog-sync-once` binary,
the `trainlog-syncd` launcher and daemon source, and the generation worker.
This prevents a user unit from silently executing a helper from a different
checkout than the installed desktop candidate.
The isolated JDK 17 harness reports **220 Android tests: 215 passed, five The isolated JDK 17 harness reports **220 Android tests: 215 passed, five
skipped, zero failures/errors**. The skips are optional, externally supplied skipped, zero failures/errors**. The skips are optional, externally supplied
copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`, copy gates: `BridgeBackupMigrationTest.verifiedSchemaSeventeenBridgeBackupMigratesThroughCurrentOwner`,

View file

@ -1,29 +1,84 @@
import json, sqlite3, subprocess, tempfile, unittest import json
import sqlite3
import subprocess
import tempfile
import unittest
from pathlib import Path from pathlib import Path
ROOT=Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
class DeploymentToolsTest(unittest.TestCase): class DeploymentToolsTest(unittest.TestCase):
def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self): def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self):
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
root=Path(directory);source=root/"source.db";backup=root/"backup.db" root = Path(directory)
db=sqlite3.connect(source);db.execute("PRAGMA journal_mode=WAL");db.execute("PRAGMA foreign_keys=ON") source = root / "source.db"
db.executescript("CREATE TABLE parent(id INTEGER PRIMARY KEY);CREATE TABLE child(parent_id INTEGER REFERENCES parent);INSERT INTO parent VALUES(7);INSERT INTO child VALUES(7);");db.commit() backup = root / "backup.db"
result=subprocess.run(["python3",str(ROOT/"tools/backup_trainlog_sqlite.py"),str(source),str(backup)],capture_output=True,text=True) database = sqlite3.connect(source)
self.assertEqual(result.returncode,0,result.stderr) database.execute("PRAGMA journal_mode=WAL")
with sqlite3.connect(backup) as restored: database.execute("PRAGMA foreign_keys=ON")
self.assertEqual(restored.execute("SELECT * FROM child").fetchall(),[(7,)]) database.executescript(
self.assertEqual(restored.execute("PRAGMA integrity_check").fetchone(),("ok",)) "CREATE TABLE parent(id INTEGER PRIMARY KEY);"
db.close() "CREATE TABLE child(parent_id INTEGER REFERENCES parent);"
"INSERT INTO parent VALUES(7);"
"INSERT INTO child VALUES(7);"
)
database.commit()
result = subprocess.run(
[
"python3",
str(ROOT / "tools/backup_trainlog_sqlite.py"),
str(source),
str(backup),
],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
with sqlite3.connect(backup) as restored:
self.assertEqual(
restored.execute("SELECT * FROM child").fetchall(),
[(7,)],
)
self.assertEqual(
restored.execute("PRAGMA integrity_check").fetchone(),
("ok",),
)
database.close()
def test_candidate_is_self_contained_and_hashed(self): def test_candidate_is_self_contained_and_hashed(self):
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
output=Path(directory)/"candidate" output = Path(directory) / "candidate"
result=subprocess.run(["python3",str(ROOT/"tools/package_sync_candidate.py"),"--output",str(output)],capture_output=True,text=True) result = subprocess.run(
self.assertEqual(result.returncode,0,result.stderr) [
inventory=json.loads((output/"candidate-inventory.json").read_text()) "python3",
self.assertEqual(inventory["product_version"],"0.1.2") str(ROOT / "tools/package_sync_candidate.py"),
self.assertTrue((output/"bin/trainlog").is_file()) "--output",
self.assertTrue(any(row["path"]=="tools/sync_peer_worker.py" for row in inventory["files"])) str(output),
],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
inventory = json.loads((output / "candidate-inventory.json").read_text())
self.assertEqual(inventory["product_version"], "0.1.2")
self.assertTrue((output / "bin/trainlog").is_file())
self.assertTrue((output / "bin/trainlog-sync-once").is_file())
self.assertTrue((output / "bin/trainlog-syncd").is_file())
self.assertTrue((output / "libexec/trainlog-sync-once").is_file())
self.assertTrue(
any(
row["path"] == "tools/trainlog_syncd.py"
for row in inventory["files"]
)
)
self.assertTrue(
any(
row["path"] == "tools/sync_peer_worker.py"
for row in inventory["files"]
)
)
if __name__=="__main__": unittest.main()
if __name__ == "__main__":
unittest.main()

View file

@ -11,6 +11,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
RUNTIME_TOOLS = [ RUNTIME_TOOLS = [
"trainlog_syncd.py",
"sync_orchestrator.py", "sync_orchestrator.py",
"sync_peer_worker.py", "sync_peer_worker.py",
"sync_generation_exchange.py", "sync_generation_exchange.py",
@ -49,16 +50,20 @@ def main() -> int:
if args.output.exists(): if args.output.exists():
raise RuntimeError("candidate output already exists") raise RuntimeError("candidate output already exists")
binary = args.build / "tui/trainlog" binary = args.build / "tui/trainlog"
sync_once = args.build / "tui/trainlog-sync-once"
mtp_adapter = args.build / "tui/trainlog-generation-mtp-adapter" mtp_adapter = args.build / "tui/trainlog-generation-mtp-adapter"
if not binary.is_file(): if not binary.is_file():
raise RuntimeError("compiled trainlog executable is missing") raise RuntimeError("compiled trainlog executable is missing")
if not mtp_adapter.is_file(): if not mtp_adapter.is_file():
raise RuntimeError("compiled generation MTP adapter is missing") raise RuntimeError("compiled generation MTP adapter is missing")
if not sync_once.is_file():
raise RuntimeError("compiled legacy synchronization helper is missing")
(args.output / "bin").mkdir(parents=True) (args.output / "bin").mkdir(parents=True)
(args.output / "libexec").mkdir(parents=True) (args.output / "libexec").mkdir(parents=True)
(args.output / "tools").mkdir(parents=True) (args.output / "tools").mkdir(parents=True)
(args.output / "catalog").mkdir(parents=True) (args.output / "catalog").mkdir(parents=True)
shutil.copy2(binary, args.output / "libexec/trainlog") shutil.copy2(binary, args.output / "libexec/trainlog")
shutil.copy2(sync_once, args.output / "libexec/trainlog-sync-once")
shutil.copy2(mtp_adapter, args.output / "libexec/trainlog-generation-mtp-adapter") shutil.copy2(mtp_adapter, args.output / "libexec/trainlog-generation-mtp-adapter")
for name in RUNTIME_TOOLS: for name in RUNTIME_TOOLS:
shutil.copy2(ROOT / "tools" / name, args.output / "tools" / name) shutil.copy2(ROOT / "tools" / name, args.output / "tools" / name)
@ -68,6 +73,14 @@ def main() -> int:
launcher.write_text( launcher.write_text(
'#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\nexport TRAINLOG_SYNC_TOOLS_DIR="$root/tools"\nexport TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"\nexec "$root/libexec/trainlog" "$@"\n' '#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\nexport TRAINLOG_SYNC_TOOLS_DIR="$root/tools"\nexport TRAINLOG_SYNC_MTP_ADAPTER="$root/libexec/trainlog-generation-mtp-adapter"\nexec "$root/libexec/trainlog" "$@"\n'
) )
sync_once_launcher = args.output / "bin/trainlog-sync-once"
sync_once_launcher.write_text(
'#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\nexec "$root/libexec/trainlog-sync-once" "$@"\n'
)
syncd_launcher = args.output / "bin/trainlog-syncd"
syncd_launcher.write_text(
'#!/bin/sh\nset -eu\nroot=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)\nexec python3 "$root/tools/trainlog_syncd.py" --sync-once "$root/bin/trainlog-sync-once" "$@"\n'
)
files = sorted(path for path in args.output.rglob("*") if path.is_file()) files = sorted(path for path in args.output.rglob("*") if path.is_file())
inventory = { inventory = {
"format": "trainlog-sync-candidate-inventory", "format": "trainlog-sync-candidate-inventory",
@ -88,6 +101,8 @@ def main() -> int:
], ],
"entry_points": [ "entry_points": [
"bin/trainlog", "bin/trainlog",
"bin/trainlog-sync-once",
"bin/trainlog-syncd",
"libexec/trainlog-generation-mtp-adapter", "libexec/trainlog-generation-mtp-adapter",
"tools/sync_orchestrator.py", "tools/sync_orchestrator.py",
], ],
@ -113,7 +128,10 @@ def main() -> int:
json.dumps(inventory, indent=2, sort_keys=True) + "\n" json.dumps(inventory, indent=2, sort_keys=True) + "\n"
) )
os.chmod(args.output / "bin/trainlog", 0o755) os.chmod(args.output / "bin/trainlog", 0o755)
os.chmod(args.output / "bin/trainlog-sync-once", 0o755)
os.chmod(args.output / "bin/trainlog-syncd", 0o755)
os.chmod(args.output / "libexec/trainlog", 0o755) os.chmod(args.output / "libexec/trainlog", 0o755)
os.chmod(args.output / "libexec/trainlog-sync-once", 0o755)
os.chmod(args.output / "libexec/trainlog-generation-mtp-adapter", 0o755) os.chmod(args.output / "libexec/trainlog-generation-mtp-adapter", 0o755)
return 0 return 0