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
- 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
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

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
partial output. Fresh desktop, current-Android and schema-v17 bridge candidates
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**.

View file

@ -22,6 +22,10 @@ the separately authorized rollout are complete.
1. Build and validate one revision, then run `python3
tools/package_sync_candidate.py --output <new-private-directory>`. Its
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
release signing identity, verification of its public certificate and a valid
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
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
skipped, zero failures/errors**. The skips are optional, externally supplied
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
ROOT=Path(__file__).resolve().parents[1]
ROOT = Path(__file__).resolve().parents[1]
class DeploymentToolsTest(unittest.TestCase):
def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self):
with tempfile.TemporaryDirectory() as directory:
root=Path(directory);source=root/"source.db";backup=root/"backup.db"
db=sqlite3.connect(source);db.execute("PRAGMA journal_mode=WAL");db.execute("PRAGMA foreign_keys=ON")
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()
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",))
db.close()
def test_sqlite_backup_captures_committed_wal_and_restores_invariants(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
source = root / "source.db"
backup = root / "backup.db"
database = sqlite3.connect(source)
database.execute("PRAGMA journal_mode=WAL")
database.execute("PRAGMA foreign_keys=ON")
database.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);"
)
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):
with tempfile.TemporaryDirectory() as directory:
output=Path(directory)/"candidate"
result=subprocess.run(["python3",str(ROOT/"tools/package_sync_candidate.py"),"--output",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(any(row["path"]=="tools/sync_peer_worker.py" for row in inventory["files"]))
def test_candidate_is_self_contained_and_hashed(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "candidate"
result = subprocess.run(
[
"python3",
str(ROOT / "tools/package_sync_candidate.py"),
"--output",
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]
RUNTIME_TOOLS = [
"trainlog_syncd.py",
"sync_orchestrator.py",
"sync_peer_worker.py",
"sync_generation_exchange.py",
@ -49,16 +50,20 @@ def main() -> int:
if args.output.exists():
raise RuntimeError("candidate output already exists")
binary = args.build / "tui/trainlog"
sync_once = args.build / "tui/trainlog-sync-once"
mtp_adapter = args.build / "tui/trainlog-generation-mtp-adapter"
if not binary.is_file():
raise RuntimeError("compiled trainlog executable is missing")
if not mtp_adapter.is_file():
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 / "libexec").mkdir(parents=True)
(args.output / "tools").mkdir(parents=True)
(args.output / "catalog").mkdir(parents=True)
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")
for name in RUNTIME_TOOLS:
shutil.copy2(ROOT / "tools" / name, args.output / "tools" / name)
@ -68,6 +73,14 @@ def main() -> int:
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'
)
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())
inventory = {
"format": "trainlog-sync-candidate-inventory",
@ -88,6 +101,8 @@ def main() -> int:
],
"entry_points": [
"bin/trainlog",
"bin/trainlog-sync-once",
"bin/trainlog-syncd",
"libexec/trainlog-generation-mtp-adapter",
"tools/sync_orchestrator.py",
],
@ -113,7 +128,10 @@ def main() -> int:
json.dumps(inventory, indent=2, sort_keys=True) + "\n"
)
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-sync-once", 0o755)
os.chmod(args.output / "libexec/trainlog-generation-mtp-adapter", 0o755)
return 0