fix: service draait als uid 1000 (eddy), eigenaar van de exfat-SSD
Root was bad practice (oude staat); aiaiaiaiagent (uid 1001) kon geen chmod/utime op de exfat-mount (uid=1000) -> elke copy2-kopie faalde met EPERM (10-15, 0/68). Correcte laag is de identity-laag, niet de kopieer- primitive: User=eddy (mount-eigenaar). copy2 behouden (timestamps op doel). Matrix-secrets sherpa-token/eddy-report-room -> mode 640 group eddy + traverse op /home/aiaiaiaiagent zodat de notificatie werkt. Revert eerdere copyfile-workaround (dd46ad6/69d08ba): die maskeerde de mismatch in plaats van de principal te aligneren.
This commit is contained in:
parent
69d08ba8ba
commit
0461fbc074
3 changed files with 28 additions and 25 deletions
25
README.md
25
README.md
|
|
@ -48,13 +48,6 @@ SD-kaartlezer die midden in de transfer offline klapt, `I/O error`, `device offl
|
|||
verwijderd, wordt eerst een bestand van de kaart gelezen. Een onleesbare kaart
|
||||
resulteert in een duidelijke Matrix-"GESTOPT" en **niets** wordt gewist.
|
||||
- **Retry per bestand** — 3 pogingen met 2s pauze; vangt transiënte USB-glitches.
|
||||
- **`shutil.copyfile` i.p.v. `copy`/`copy2`** — `copy`/`copy2` doen na het kopiëren
|
||||
ook metadata-operaties op het doel (`chmod`, `utime`). Op de exfat-SSD
|
||||
(`uid=1000` → álle bestanden zijn eigendom van uid 1000, `allow_utime=0022`)
|
||||
worden die geweigerd voor een niet-eigenaar (EPERM); sinds de service als
|
||||
`aiaiaiaiagent` draait (voorheen root) faalde daardoor elke kopie. `copyfile`
|
||||
schrijft alleen data; mode is op exfat altijd 0777 en het opnamemoment staat al
|
||||
in de bestandsnaam — metadata-operaties voegen niets toe.
|
||||
- **Early-abort** — bij 5 opeenvolgende fouten stopt het script en stuurt het een
|
||||
Matrix-bericht met de laatste fout. Bronbestanden worden nooit verwijderd zonder
|
||||
geverifieerde kopie.
|
||||
|
|
@ -62,6 +55,14 @@ SD-kaartlezer die midden in de transfer offline klapt, `I/O error`, `device offl
|
|||
- **fsck.exfat** — na mount draait het mount-script een read-only health check;
|
||||
een dirty/defecte kaart wordt daarmee zichtbaar in syslog.
|
||||
- **Geen dubbele runs** — het mount-script skipt als de transfer al actief is.
|
||||
- **Service-user = data-eigenaar (uid 1000)** — de exfat-mounts draaien als
|
||||
`uid=1000,gid=1000`, dus álle bestanden op de SSD zijn eigendom van uid 1000
|
||||
(`eddy`). De unit draait als `User=eddy`; dat is noodzakelijk want copy2 doet
|
||||
`chmod`/`utime` op het doel, en dat mag alleen de eigenaar. Dit was eerder
|
||||
fout opgebouwd (eerst `root` — bad practice; daarna `aiaiaiaiagent` — leidde
|
||||
tot EPERM op élke kopie). Wie de unit-user aanpast: alleen naar een uid dat
|
||||
eigenaar is van de mount, anders breekt elke transfer met
|
||||
`Operation not permitted`.
|
||||
- **Stale-mount opruiming** — als de kaartlezer wordt uitgetrokken (of de kaart
|
||||
offline klapt) ruimt een udev REMOVE-trigger de mount op: de service wordt
|
||||
gestopt en `/mnt/sdcard` netjes ontkoppeld (lazy als dat mislukt). Zo blijft er
|
||||
|
|
@ -159,7 +160,7 @@ dmesg | grep -Ei "usb|sd[a-z]|device offline|I/O error"
|
|||
| `Geen MP4 bestanden gevonden` in log | Kaart leeg (transfer geslaagd) of verkeerde mapstructuur | Niks mis; controleer op de SSD of bestanden er zijn |
|
||||
| Transfer start niet na insteken | Service draait al (is-active gate) of device verscheen niet binnen 60s | Logs checken; als het device nooit komt, lezer opnieuw insteken |
|
||||
| Stale mount in `mount`-output | REMOVE-trigger miste (lezer trok in één keer beide devices weg) | Cleanup-script handmatig draaien (zie herstel) |
|
||||
| `Matrix notificatie mislukt` in log | sherpa-token/room-file ontbreken of Matrix-homeserver onbereikbaar | `~/whatsed-new/secrets/sherpa-token` + `eddy-report-room` controleren (mode 600), `https://whatsed.eddydevink.nl` checken |
|
||||
| `Matrix notificatie mislukt` in log | sherpa-token/room-file onleesbaar voor service-user of Matrix-homeserver onbereikbaar | `~/whatsed-new/secrets/sherpa-token` + `eddy-report-room` controleren (mode 640, group eddy; service draait als uid 1000), `https://whatsed.eddydevink.nl` checken |
|
||||
| Abort: onvoldoende vrije ruimte | SSD vol, opschonen hielp niet | Oude datum-mappen handmatig opschonen of SSD uitbreiden |
|
||||
|
||||
### Herstelprocedures
|
||||
|
|
@ -232,8 +233,8 @@ watchdog-rapportage, Home Assistant en Uptime Kuma):
|
|||
|---|---|
|
||||
| API | `https://whatsed.eddydevink.nl/_matrix/client/v3` |
|
||||
| Bot-account | `@sherpa:whatsed.eddydevink.nl` (compat-token `mct_…`) |
|
||||
| Tokenbestand | `/home/aiaiaiaiagent/whatsed-new/secrets/sherpa-token` (mode 600) |
|
||||
| Roombestand | `/home/aiaiaiaiagent/whatsed-new/secrets/eddy-report-room` (mode 600) |
|
||||
| Tokenbestand | `/home/aiaiaiaiagent/whatsed-new/secrets/sherpa-token` (mode 640, group eddy — leesbaar door service-user eddy) |
|
||||
| Roombestand | `/home/aiaiaiaiagent/whatsed-new/secrets/eddy-report-room` (mode 640, group eddy) |
|
||||
| Bericht | `PUT /rooms/<id>/send/m.room.message/…` met `m.text`-event |
|
||||
|
||||
Token en room-ID blijven buiten Git; de bestanden worden bij elke notificatie gelezen.
|
||||
|
|
@ -294,7 +295,9 @@ After=mnt-sdcard.mount
|
|||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/dashcam-transfer
|
||||
User=aiaiaiaiagent
|
||||
# uid 1000 (eddy): eigenaar van de exfat-SSD (mount uid=1000) — copy2 heeft
|
||||
# eigenaarschap nodig voor chmod/utime. Niet root, niet een andere user.
|
||||
User=eddy
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
|
|
|||
|
|
@ -150,16 +150,16 @@ def ensure_free_space(needed_bytes, target_base):
|
|||
|
||||
def copy_file(src, dst, attempts=MAX_RETRIES, delay=RETRY_DELAY):
|
||||
"""Copy src → dst with retry. Raises the last OSError when all attempts fail."""
|
||||
# copy2 preserveert timestamps (os.utime) en mode (os.chmod) op het doel.
|
||||
# Dat vereist eigenaarschap van het doelbestand: de exfat-mount draait als
|
||||
# uid=1000 en álle bestanden op de SSD zijn eigendom van uid 1000. De unit
|
||||
# draait daarom als User=eddy (uid 1000) — NIET als root, NIET als een
|
||||
# andere gebruiker (chmod/utime → EPERM voor niet-eigenaars). Bestand →
|
||||
# copyfile omwisselen zou deze invariant verbergen en opnieuw breken.
|
||||
last_error = None
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
# copyfile (niet copy/copy2): die doen ook metadata-operaties op het
|
||||
# doel — chmod en utime. Op exfat met mountuid=1000 zijn álle bestanden
|
||||
# eigendom van uid 1000, en allow_utime=0022 + chmod-toestand weigeren
|
||||
# dat voor de niet-eigenaar service-gebruiker (EPERM op elke kopie).
|
||||
# copyfile schrijft alleen data; mode is op exfat altijd 0777 en het
|
||||
# opnamemoment staat al in de bestandsnaam (YYYYMMDDHHMMSS).
|
||||
shutil.copyfile(src, dst)
|
||||
shutil.copy2(src, dst)
|
||||
return
|
||||
except OSError as e:
|
||||
last_error = e
|
||||
|
|
|
|||
|
|
@ -85,22 +85,22 @@ class CopyFileTest(unittest.TestCase):
|
|||
self.assertEqual(os.path.getsize(self.dst), 100)
|
||||
|
||||
def test_retries_then_succeeds(self):
|
||||
real_copy = dashcam.shutil.copyfile
|
||||
real_copy2 = dashcam.shutil.copy2
|
||||
calls = {"n": 0}
|
||||
|
||||
def flaky(s, d):
|
||||
calls["n"] += 1
|
||||
if calls["n"] < 3:
|
||||
raise OSError(errno.EIO, "I/O error")
|
||||
real_copy(s, d)
|
||||
real_copy2(s, d)
|
||||
|
||||
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=flaky):
|
||||
with mock.patch.object(dashcam.shutil, "copy2", side_effect=flaky):
|
||||
dashcam.copy_file(self.src, self.dst, attempts=3, delay=0)
|
||||
self.assertEqual(calls["n"], 3)
|
||||
self.assertEqual(os.path.getsize(self.dst), 100)
|
||||
|
||||
def test_raises_after_exhausting_attempts(self):
|
||||
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=OSError(errno.EIO, "I/O error")):
|
||||
with mock.patch.object(dashcam.shutil, "copy2", side_effect=OSError(errno.EIO, "I/O error")):
|
||||
with self.assertRaises(OSError):
|
||||
dashcam.copy_file(self.src, self.dst, attempts=2, delay=0)
|
||||
|
||||
|
|
@ -155,11 +155,11 @@ class TransferFilesTest(unittest.TestCase):
|
|||
def test_verification_mismatch_counts_as_error_and_keeps_source(self):
|
||||
files = [self._make("20260622050346_0000001F.MP4", 100)]
|
||||
|
||||
def bad_copy(s, d):
|
||||
def bad_copy2(s, d):
|
||||
with open(d, "wb") as f:
|
||||
f.write(b"0" * 50)
|
||||
|
||||
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=bad_copy), mock.patch.object(
|
||||
with mock.patch.object(dashcam.shutil, "copy2", side_effect=bad_copy2), mock.patch.object(
|
||||
dashcam.time, "sleep"
|
||||
):
|
||||
ok, errors = dashcam.transfer_files(files, self.src, self.base)
|
||||
|
|
@ -172,7 +172,7 @@ class TransferFilesTest(unittest.TestCase):
|
|||
for i in range(dashcam.MAX_CONSECUTIVE_ERRORS + 3)
|
||||
]
|
||||
with mock.patch.object(
|
||||
dashcam.shutil, "copyfile", side_effect=OSError(errno.EIO, "I/O error")
|
||||
dashcam.shutil, "copy2", side_effect=OSError(errno.EIO, "I/O error")
|
||||
), mock.patch.object(dashcam.time, "sleep"):
|
||||
with self.assertRaises(dashcam.TransferAborted) as ctx:
|
||||
dashcam.transfer_files(files, self.src, self.base)
|
||||
|
|
|
|||
Loading…
Reference in a new issue