From 1321f664222bb1ab41923b20f1e0796dd3682c37 Mon Sep 17 00:00:00 2001 From: amo Date: Sat, 18 Jul 2026 14:06:08 +0200 Subject: [PATCH 1/3] Add native Windows driver profile support --- docs/linbofs-windows-driver-profiles.md | 588 +++++++++++++++++++++ src/linbofs/usr/bin/linbo_driverpostsync | 617 +++++++++++++++++++++++ 2 files changed, 1205 insertions(+) create mode 100644 docs/linbofs-windows-driver-profiles.md create mode 100755 src/linbofs/usr/bin/linbo_driverpostsync diff --git a/docs/linbofs-windows-driver-profiles.md b/docs/linbofs-windows-driver-profiles.md new file mode 100644 index 0000000..4738c09 --- /dev/null +++ b/docs/linbofs-windows-driver-profiles.md @@ -0,0 +1,588 @@ +# Native Windows driver profiles for LINBO + +- Status: implementation workbench; first reference-server end-to-end test passed +- Target branch: `main` +- Planning base: `e17d008` (`main` and `7.4`, `linuxmuster-linbo7` 7.4.6) +- Related components: `linuxmuster-tools7`, `linuxmuster-api7` + +## Decision + +Move the stable client-side driver deployment runtime into `linbofs`, but keep +one small, generated `.driverpostsync` companion per image. + +The per-image file cannot be moved completely into `linbofs`: `linbofs` is a +global boot filesystem, while image assignments and profile lists are dynamic +server state. Embedding that state would require an `update-linbofs` rebuild +after every assignment and would expose unrelated image state to every client. + +The target split is therefore: + +- `linuxmuster-linbo7` owns one static `linbo_driverpostsync` runtime; +- `linuxmuster-tools7` remains the source of truth for profile assignments; +- Tools renders a minimal per-image dispatcher containing only the validated + image name and the sorted profile names; +- the already existing LINBO download and execution path transports and + sources that dispatcher; +- `linuxmuster-api7` remains unchanged. + +This moves the already tested client-side implementation into LINBO; it does +not introduce a second, parallel implementation. + +## Workbench implementation + +The first implementation now exists on this branch as +`src/linbofs/usr/bin/linbo_driverpostsync`. It uses the positional contract +described below, keeps the existing transfer, matching, cache, Windows task +and registry behavior, and adds fail-closed validation for direct command +invocations. The existing `linbo_download_image` and `linbo_sync` files remain +unchanged. + +A dependent linuxmuster-tools workbench branch replaces the generated full +runtime with the small dispatcher only. That change must not be released +before a LINBO package containing this executable has reached every client. +The linuxmuster API does not need a corresponding code change. + +## Existing contracts to reuse + +| Concern | Existing implementation | Decision | +|---|---|---| +| Companion download | `src/linbofs/usr/bin/linbo_download_image` downloads `driverpostsync` | Reuse unchanged | +| Companion execution | `src/linbofs/usr/bin/linbo_sync` sources the hook after the regular postsync while `/mnt` and `/cache` are mounted | Reuse unchanged in the first PR | +| LINBO environment | `/usr/share/linbo/shell_functions`, `/.env`, `/conf/linbo` | Source and reuse | +| Interrupt handling | existing traps from `shell_functions`; the current driver runtime uses direct `rsync --timeout=120` calls | Preserve for the extraction; evaluate `interruptible` separately | +| Logging | `/tmp/linbo.log`, `sendlog`, and the existing driver log copied to `/cache` | Reuse | +| Offline registry | `/usr/bin/linbo_patch_registry` and the existing `reged` payload | Reuse | +| Driver profiles | `/srv/linbo/drivers/` managed by `LinboDriverManager` | Keep server-side | +| Image assignments | manager-owned `image.conf` files and shared lock | Keep server-side | +| Matching rules | canonical manager-owned `match.conf` files | Keep the existing format | +| Windows installation | task/marker contract expected by the current Tools runtime, but not delivered by LINBO 7.4.6 | Keep as an external prerequisite or separate bootstrap PR; preserve the fallback | +| HTTP access | existing thin linuxmuster API endpoints | No API change | + +The download and source support was merged before this proposal. It must not +be implemented again under another name. + +## Target architecture + +```mermaid +flowchart TD + A["linuxmuster-tools7
LinboDriverManager"] --> B["image.conf assignments"] + B --> C["sorted profiles for one image"] + C --> D["small generated dispatcher
<image>.driverpostsync"] + D -->|"existing rsync companion download"| E["/cache/<image>.driverpostsync"] + E -->|"existing source in linbo_sync"| F["linbofs executable
linbo_driverpostsync"] + F --> G["download assigned match.conf files"] + G --> H["match local DMI"] + H --> I["download matching payloads only"] + I --> J["/mnt/Drivers/LINBO"] + J --> K["pre-provisioned SYSTEM task or
administrative RunOnce fallback"] +``` + +### Server state + +```text +/srv/linbo/drivers/ +├── lenovo-l14-gen5/ +│ ├── match.conf +│ ├── image.conf +│ └── ... extracted driver payload including at least one INF +└── dell-7490/ + ├── match.conf + ├── image.conf + └── ... extracted driver payload including at least one INF + +/srv/linbo/images/windows-11/ +├── windows-11.qcow2 +└── windows-11.driverpostsync # small generated dispatcher +``` + +### Client state + +```text +linbofs +└── /usr/bin/linbo_driverpostsync # static runtime + +/cache +├── windows-11.driverpostsync # dynamic dispatcher +├── linbo-driverprofiles/windows-11/ # last-known-good payload cache +└── linbo-driverpostsync.log + +/mnt # mounted Windows target +└── Drivers/LINBO/ + ├── pnputil-install.cmd + └── ... matched driver payloads +``` + +### One image with many hardware classes + +An image with 15 hardware classes still has exactly one +`windows-11.driverpostsync`. Its dispatcher passes 15 validated profile names +to the static runtime. The client downloads the 15 small `match.conf` files, +compares them with its own DMI values and downloads only the payloads of the +matching profiles. Neither the Windows image nor `linbofs` contains all 15 +driver payloads. + +## Rejected alternatives + +### Embed profile assignments in `linbofs` + +Rejected because assignments are dynamic and image-specific. Every profile +change would otherwise require a global `update-linbofs`, and clients would +receive state for unrelated images. + +### Remove `.driverpostsync` and scan every profile from the client + +Rejected because it would create a second assignment resolver. The client +would have to discover all server profiles and interpret every `image.conf`, +duplicating the authoritative server-side manager and increasing transfer and +failure scope. + +### Add a new manifest, API route or service + +Rejected because the existing image companion already transports exactly the +small amount of dynamic state required by the runtime. A new protocol would +duplicate an accepted LINBO mechanism. + +### Put profile lists in `start.conf` + +Rejected because it would add another configuration owner and require changes +to provisioning and start.conf parsing. The existing `image.conf` assignments +and generated companion already provide this relationship transactionally. + +## Responsibility boundary + +### Remains in `linuxmuster-tools7` + +- school-aware inventory access through the existing `Devices(school)` + provider; +- profile CRUD and canonical `match.conf` generation; +- server-side payload, INF, path, permission, size and collision validation; +- persistent `image.conf` assignments; +- locking, atomic writes, rollback and image lifecycle integration; +- deriving the sorted, deduplicated profile list for an image; +- `.driverpostsync` ownership checks and atomic publication; +- cleanup tombstone publication; +- reconciliation after restore, upgrade or repair; +- future archive import orchestration. + +### Moves into `linuxmuster-linbo7` + +- reading the real client DMI vendor and product; +- metadata-first download of assigned `match.conf` files; +- strict client-side parsing and matching of canonical rules; +- last-known-good metadata and payload cache handling; +- downloading only matching driver payloads; +- last-known-good cache activation for matched payloads; +- staging into the mounted Windows partition; +- generation of the deterministic `pnputil-install.cmd` file; +- checking the pre-provisioned SYSTEM-task marker; +- applying or removing the existing administrative RunOnce fallback with + `linbo_patch_registry`; +- driver-specific runtime logging and cleanup. + +### Remains outside both components + +The Windows SYSTEM task is a golden-image bootstrap. LINBO 7.4.6 does not +deliver it. For the current implementation it must be installed manually in +the golden image; a supported delivery path requires a separate bootstrap PR +and an explicit merge and rollout order. LINBO has no existing mechanism that +can safely create this scheduled task offline with the required Windows +permissions, so this runtime proposal does not invent one. Without the task +and marker pair, the current administrative RunOnce fallback remains the only +installation path. + +## Runtime command contract + +Add one executable to the LINBO client filesystem: + +```text +/usr/bin/linbo_driverpostsync +``` + +Proposed interface: + +```text +linbo_driverpostsync [ ...] +``` + +- one or more profiles run the normal matching and deployment path; +- zero profiles run the cleanup/tombstone path; +- image and profile values are passed as separate arguments, never through + `eval` or a parsed manifest; +- the client validates all path components again and fails closed; +- profile ordering is already deterministic on the server, but the runtime + must not depend on ordering for correctness; +- the executable sources `/usr/share/linbo/shell_functions` like the existing + `linbo_*` commands. + +Expected exit status for the behavior-preserving extraction: + +| Status | Meaning | +|---:|---| +| `0` | Completed successfully or skipped a non-Windows target | +| `1` | Invalid invocation or a matching, transfer, staging or cache operation failed | + +As in the current hook, a non-zero `linbo_patch_registry` result is logged as +a warning and does not by itself change the runtime status. Some LINBO 7.4 +versions do not return a reliable registry-helper status; making it fatal +would be a separate failure-semantics change. + +The first implementation should preserve the current full-hook outcome and +logging before changing any failure semantics in `linbo_sync`. + +At `e17d008`, `linbo_sync` sources `.driverpostsync` without assigning a +non-zero hook status to its own `RC`; the following `mk_boot` result can mask +that status. The executable's return value is therefore useful for direct +diagnostics and the dispatcher, but is not guaranteed to become the overall +sync result. Changing that behavior requires a separate, explicit decision. + +## Generated dispatcher contract + +For assigned profiles, Tools should eventually render only a small sourced +hook similar to: + +```sh +#!/bin/sh +# Managed-By: linuxmusterTools.linbo.driver_hooks v1 +# Image: windows-11 +# Profiles: dell-7490, lenovo-l14-gen5 + +if ! command -v linbo_driverpostsync >/dev/null 2>&1; then + echo "LINBO driver runtime is missing." >&2 + return 1 +fi + +linbo_driverpostsync "windows-11" "dell-7490" "lenovo-l14-gen5" +return $? +``` + +For an image without assigned profiles, Tools must publish a dispatcher that +calls the same executable with only the image argument: + +```sh +linbo_driverpostsync "windows-11" +return $? +``` + +The empty dispatcher is required. Deleting the hook is not enough because a +client may still have an older hook and driver payload in its cache. + +The existing managed ownership header should remain stable during the +refactoring. No `.driverpostsync.d` convention or second manifest format is +introduced. + +## Runtime sequence + +The static runtime should preserve the sequence already implemented by the +current generated hook: + +1. Validate the image and every profile argument. +2. Confirm that `/mnt` contains a Windows installation; return success for a + non-Windows target. +3. Create the per-image cache and metadata staging locations below `/cache`. +4. Read and normalize `sys_vendor` and `product_name` from sysfs. +5. Download only each assigned profile's small, non-empty `match.conf` into a + staging file and atomically replace the active copy. A failed transfer + leaves the previous active copy in place. As in the current hook, parsing + happens after activation; rejecting malformed metadata before activation + would be a separate hardening change. +6. Parse only canonical `vendor` and `product` entries and compare them with + local DMI data. +7. Download full payloads only for matching profiles into staging + directories. +8. After a successful rsync, swap the staged profile cache into place and + retain last-known-good payloads when transfer or activation fails. The + current client hook does not perform another complete payload-shape + validation; adding one is a separate hardening change. +9. Recreate `/mnt/Drivers/LINBO` with the current `rm`, `mkdir` and `cp` + sequence and copy the matched cached payloads. An atomic Windows-target + swap is not part of the behavior-preserving extraction. +10. Generate a deterministic `pnputil-install.cmd` for all staged INF files. +11. If the pre-provisioned SYSTEM task and readiness marker are present, + clear the fallback RunOnce value. Otherwise request the existing + administrative fallback through `linbo_patch_registry`. +12. Persist the driver log below `/cache`; the parent `linbo_sync` retains its + existing `sendlog` behavior. + +With no profile arguments and a recognized Windows target, the runtime removes +the managed Windows target, the image-specific driver cache and the managed +RunOnce fallback. Like the normal profile path, it returns successfully +without changing state when `/mnt` is not a Windows target. It must not remove +unrelated administrator files. + +## Behavior extraction versus later hardening + +The first LINBO implementation is a code-ownership refactoring. It must not +quietly combine that move with unrelated runtime semantics. In particular, +the following are useful follow-up candidates but not part of the initial +extraction unless the maintainer explicitly expands the scope: + +- validate a newly downloaded `match.conf` before replacing cached metadata; +- enforce a client-side `match.conf` size limit; +- rescan the complete payload shape before activating an rsync staging tree; +- replace `/mnt/Drivers/LINBO` atomically instead of using `rm`, `mkdir` and + `cp`; +- replace direct rsync calls with `interruptible`; +- make a non-zero `linbo_patch_registry` result fatal instead of preserving + the current warning-only behavior; +- propagate a driver runtime failure into the overall `linbo_sync` status. +- remove orphaned hidden `.staging-*` and `.previous-*` payload directories + belonging to profiles that are no longer assigned or no longer match. The + extracted runtime deliberately preserves the current cleanup behavior; this + inherited cache-leak edge case should be fixed in a focused hardening change. + +Keeping these decisions separate makes old and new runtime output directly +comparable and keeps the first upstream review focused. + +## Packaging + +The LINBO PR should add the runtime directly at: + +```text +src/linbofs/usr/bin/linbo_driverpostsync +``` + +It must be executable. The existing build already copies `src/linbofs/` into +the linbofs root, and `update-linbofs` already rebuilds `/srv/linbo/linbofs64` +from the packaged template. No new daemon, service, port, package dependency +or `linbofs.apps` entry is expected: the runtime uses commands already present +in `linbofs`. + +The initial LINBO PR should not modify either `linbo_download_image` or +`linbo_sync`; both required hooks already exist. A change to propagate a +driver-runtime failure into the overall sync result is a separate behavioral +decision and should not be hidden in this refactoring. + +Do not guess a future package version or introduce a new package coupling by +assumption. The maintainers must decide whether the dispatcher rollout is +guarded through a Debian relationship, a coordinated release or another +existing packaging mechanism. A server package relationship alone cannot +prove that an already running client has loaded the new `linbofs`. + +## Cross-repository implementation plan + +### PR 1: `linuxmuster-linbo7` + +Scope: + +- add the static runtime under `src/linbofs/usr/bin`; +- preserve the current generated-hook behavior exactly; +- verify it is executable and included in the built linbofs template; +- define portable behavior cases derived from the current Tools runtime tests + and run them in the development workbench; do not add a new LINBO test + framework without maintainer agreement; +- document the runtime command contract; +- do not change profile storage, API behavior or Windows bootstrap delivery. + +This PR is backward compatible: existing large `.driverpostsync` hooks keep +working because the existing download/source path is unchanged. + +### PR 2: `linuxmuster-tools7` + +This PR starts only after a LINBO package containing the runtime is available. + +Scope: + +- replace the large full and tombstone templates with the minimal dispatcher; +- keep existing assignment resolution, sorting, locking, ownership checks, + transactional publication and reconciliation; +- keep the current managed header recognizable during migration; +- implement the release guard chosen with the maintainers, using the real + released LINBO version if that guard is a package relationship; +- keep tests for storage, assignments, rollback and dispatcher rendering; +- keep portable runtime cases aligned without creating a cross-repository test + dependency. + +### `linuxmuster-api7` + +No change is required. Every endpoint continues to call the same public +`LinboDriverManager` methods. + +## Compatibility matrix + +| LINBO client runtime | Per-image hook | Result | +|---|---|---| +| Old | Existing full hook | Supported | +| New | Existing full hook | Supported during rollout | +| New | Small dispatcher | Target state | +| Old | Small dispatcher | Unsupported; dispatcher fails because the executable is missing | + +The unsupported combination must be prevented by release order and a client +fleet rollout gate. A server package relationship may be one part of that +decision, but it does not update clients that are already running an older +`linbofs`. Runtime detection in the small dispatcher remains an explicit last +line of defence, not the primary upgrade mechanism. + +## Rollout sequence + +1. Record representative full-hook and tombstone behavior cases from the + current Tools implementation. +2. Implement the LINBO executable by extracting that behavior, not by writing + a second matching path. +3. Run the portable behavior cases against both the old rendered hook and the + new executable and require equivalent observable results. +4. Build and install the LINBO package on the reference server. +5. Rebuild `linbofs64` through the existing package/configuration path. +6. Boot representative test clients and verify that `linbo_driverpostsync` is + present in the running `linbofs`. +7. Test the new LINBO runtime while Tools still emits full hooks. +8. Release the LINBO package. +9. Roll out the new `linbofs` to the client fleet. Reboot or otherwise replace + every still-running old LINBO session that may execute a thin dispatcher. +10. Complete an explicit fleet readiness gate before changing any managed + hook to the small dispatcher. +11. Change Tools to emit the small dispatcher and apply the release guard + agreed with the maintainers. +12. Run `reconcile_driver_hooks()` once to rewrite all managed hooks. +13. Test one image with multiple assigned hardware classes and verify that + only the DMI-matching payloads are transferred. +14. Remove the old large template from Tools only after end-to-end acceptance. + +Normal profile creation or assignment changes never require +`update-linbofs`. Only changes to the static runtime require a rebuilt +`linbofs64`. + +## Rollback + +Before downgrading LINBO, restore a Tools version that still renders full +hooks and reconcile all managed hooks. Only then downgrade the LINBO package. + +If a dispatcher has already been published to an old client, it must fail +visibly before changing the Windows target. Without the static helper, that +client cannot redeploy even an existing driver cache. This is an unsupported +combination that the fleet gate must prevent, not a degraded operating mode. +Whether the hook failure also changes the overall `linbo_sync` result is the +separate failure-semantics decision listed below; it must not be changed +implicitly by the runtime extraction. + +## Verification plan + +### Static and package checks + +- shell syntax check using the same shell capabilities available in linbofs; +- no commands outside the existing linbofs payload; +- executable bit preserved in the packaged template; +- built `linbofs64` contains `/usr/bin/linbo_driverpostsync`; +- `linbo_download_image` and `linbo_sync` remain unchanged in the first PR; +- no new network service or listening port. + +### Portable runtime behavior cases + +- case-sensitive exact vendor match with case-sensitive product substring; +- multiple products and the existing explicit wildcard behavior; +- no matching profile; +- multiple matching profiles for one image; +- invalid image or profile arguments; +- malformed `match.conf`; +- failed metadata download with valid last-known-good metadata; +- failed payload refresh with valid last-known-good payload; +- interrupted or failed staged transfer; +- deterministic INF batch generation; +- repeated identical execution is idempotent; +- non-Windows target is skipped; +- canonical and legacy task/marker pairs, including path case variants; +- missing task uses the existing RunOnce fallback; +- `pnputil` success statuses `0`, `259`, `1641` and `3010` remove the batch; +- other `pnputil` failures retain the batch for retry; +- zero profiles removes only managed state; +- missing executable in the dispatcher fails visibly. + +### Reference-server acceptance + +- build from the exact commit under review; +- deploy using the normal package and `update-linbofs` flow; +- inspect the resulting initramfs instead of assuming package inclusion; +- assign at least two profiles to the same image; +- verify one generated dispatcher and its deterministic profile order; +- run `Sync + Start` on two different hardware classes; +- verify only matching payloads under `C:\Drivers\LINBO`; +- with the separate bootstrap prerequisite installed, verify SYSTEM-task + execution, driver log and task result; otherwise verify the documented + administrative RunOnce fallback; +- remove all assignments, reconcile and verify cleanup; +- repeat after a server reboot and a client offline-cache scenario. + +### Workbench verification result (2026-07-18) + +The extracted runtime passed a portable BusyBox/`ash` behavior matrix with +13 scenarios and 91 assertions. The matrix covered argument validation, +Windows-target detection, matching and non-matching profiles, malformed +metadata, last-known-good metadata and payload fallback, canonical and legacy +task markers, RunOnce fallback, registry-helper warnings, repeat execution, +no-INF behavior and the zero-profile cleanup tombstone. + +The first package-level end-to-end run was performed on a restored +linuxmuster.net 7.4 reference server with an active LINBO client: + +- a derivative of the installed `linuxmuster-linbo7` 7.4.5 package added only + `usr/bin/linbo_driverpostsync` to the packaged linbofs template; +- the normal package post-install path rebuilt `/srv/linbo/linbofs64`; +- the executable copies in the package template, the generated `linbofs64` + and the running client were byte-identical with SHA-256 + `09180bdc8a1a64e9d27ce0dca0dbc8284013255440a71a9fe9dc865cea4b00c7`; +- a test Tools package rendered one 387-byte dispatcher for + `win11_pro_edu`, containing two deterministically sorted profiles; +- the running client reported DMI vendor `QEMU` and product + `Standard PC (Q35 + ICH9, 2009)`; +- both small `match.conf` files were transferred, but only the QEMU payload + was downloaded into the image-scoped cache and copied to the temporary + Windows target; +- the non-matching Lenovo payload was absent from both payload cache and + Windows target; +- the generated `pnputil-install.cmd` used CRLF line endings and contained + exactly the matching INF fixture; +- the dispatcher and `linbo_driverpostsync` both returned zero; +- after removing both assignments, the generated 277-byte tombstone removed + only the managed target and the cache for `win11_pro_edu`; an unrelated + image cache remained intact and the tombstone returned zero; +- all test profiles and the temporary client target were removed afterwards, + and the pre-test client cache was restored. + +The Windows target was deliberately represented by an unmounted temporary +directory for this extraction test. Consequently, `linbo_patch_registry` +reported a warning because no real SOFTWARE hive existed; the preserved +warning-only behavior returned zero as designed. A later acceptance run still +has to cover `Sync + Start` against a real Windows partition, the prepared +SYSTEM task, two physical hardware classes, reboot persistence and an offline +last-known-good cache scenario. + +## Acceptance criteria + +- there is exactly one client runtime implementation; +- generated hooks contain configuration only, not hundreds of lines of + duplicated runtime code; +- one image can reference many hardware profiles through one dispatcher; +- profile changes require neither an image rebuild nor `update-linbofs`; +- client DMI determines which assigned payloads are downloaded; +- old full hooks continue to work on the new LINBO version; +- thin hooks are never released before the client fleet runtime gate; +- API behavior and payload storage contracts do not change; +- cleanup remains explicit and safe for cached clients; +- existing LINBO functions and tools are reused wherever available. + +## Explicit non-goals + +- no client-side scan of every server-side `image.conf`; +- no second assignment resolver in LINBO; +- no new manifest or `.driverpostsync.d` format; +- no new API endpoint, service or database; +- no WebUI or archive-upload work; +- no change to the image-name-with-dots limitation in this refactoring; +- no automatic invention of a Windows scheduled task; +- no unrelated LINBO sync, image or firmware fixes; +- no 7.3 backport in the `main` PR. + +## Decisions required before upstream release + +1. Confirm the executable name and positional argument contract with the + LINBO maintainer. +2. Decide the accepted location for focused runtime tests because this + repository currently has no general shell-test harness. +3. Decide separately whether a driver-runtime failure should set the overall + `linbo_sync` return code. +4. Agree on the release guard with the maintainers; only if it is a package + relationship, record the real released LINBO version before changing it. +5. Define how fleet readiness is established before Tools publishes thin + dispatchers to clients that may still be running an old `linbofs`. +6. Confirm the packaging and merge order for the Windows golden-image SYSTEM + task; it remains outside this LINBO runtime PR and is not present in LINBO + 7.4.6. diff --git a/src/linbofs/usr/bin/linbo_driverpostsync b/src/linbofs/usr/bin/linbo_driverpostsync new file mode 100755 index 0000000..384486c --- /dev/null +++ b/src/linbofs/usr/bin/linbo_driverpostsync @@ -0,0 +1,617 @@ +#!/bin/sh +# +# linbo_driverpostsync +# Deploys image-assigned Windows driver profiles selected by client DMI. +# ahmed.alani@netzint.de +# 20260718 +# + + +#### functions begin #### + +usage(){ + local RC="$1" + echo + echo "Deploys matching Windows driver profiles after image synchronization." + echo + echo "Usage: linbo_driverpostsync [ ...]" + echo + echo "Calling the command without profiles removes its managed driver state." + echo + exit "$RC" +} + + +persist_driver_log(){ + cp "$DRIVERPOSTSYNC_LOG" /cache/linbo-driverpostsync.log 2>/dev/null +} + + +driver_log(){ + echo "$*" | tee -a "$DRIVERPOSTSYNC_LOG" +} + + +request_registry_action(){ + local action="$1" + local inf_count="$2" + + case "$action" in + task) + cat > "$DRIVERPOSTSYNC_REGISTRY_FILE" << 'REG' +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce] +"LinboDriverInstall"=- +"!LinboDriverInstall"=- +REG + driver_log "SYSTEM startup task detected; legacy RunOnce values will be removed." + ;; + fallback) + cat > "$DRIVERPOSTSYNC_REGISTRY_FILE" << 'REG' +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce] +"LinboDriverInstall"=- +"!LinboDriverInstall"="cmd.exe /d /s /c C:\\Drivers\\LINBO\\pnputil-install.cmd" +REG + driver_log "Warning: SYSTEM startup task missing; !LinboDriverInstall fallback requires an administrator logon." + ;; + remove) + cat > "$DRIVERPOSTSYNC_REGISTRY_FILE" << 'REG' +Windows Registry Editor Version 5.00 + +[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce] +"LinboDriverInstall"=- +"!LinboDriverInstall"=- +REG + ;; + *) + driver_log "Invalid registry action: $action" + return 1 + ;; + esac + + linbo_patch_registry "$DRIVERPOSTSYNC_REGISTRY_FILE" > "$DRIVERPOSTSYNC_REGISTRY_LOG" 2>&1 + DRIVERPOSTSYNC_REGISTRY_RC=$? + tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_REGISTRY_LOG" + if [ "$DRIVERPOSTSYNC_REGISTRY_RC" = "0" ]; then + case "$action" in + task) + driver_log "Driver auto-install: $inf_count INF files, persistent SYSTEM startup task ready" + ;; + fallback) + driver_log "RunOnce fallback registered for $inf_count INF files; administrator logon required" + ;; + remove) + if [ "$DRIVERPOSTSYNC_CLEANUP" = "1" ]; then + driver_log "Removed stale LinboDriverInstall RunOnce values." + else + driver_log "Removed stale LinboDriverInstall RunOnce values" + fi + ;; + esac + else + if [ "$action" = "remove" ] && [ "$DRIVERPOSTSYNC_CLEANUP" = "1" ]; then + driver_log "Warning: linbo_patch_registry returned $DRIVERPOSTSYNC_REGISTRY_RC while removing RunOnce; some LINBO 7.4 releases do not provide a reliable exit status, inspect helper output." + else + driver_log "Warning: linbo_patch_registry returned $DRIVERPOSTSYNC_REGISTRY_RC while requesting RunOnce $action; some LINBO 7.4 releases do not provide a reliable exit status, inspect helper output." + fi + fi + rm -f "$DRIVERPOSTSYNC_REGISTRY_FILE" "$DRIVERPOSTSYNC_REGISTRY_LOG" + return 0 +} + + +valid_image_name(){ + local value="$1" + local length + + case "$value" in + ""|*[!a-zA-Z0-9_-]*) return 1 ;; + esac + length=$(printf '%s' "$value" | wc -c) + [ "$length" -le 100 ] +} + + +valid_profile_name(){ + local value="$1" + local length + local stem + + case "$value" in + [a-zA-Z0-9]*) ;; + *) return 1 ;; + esac + case "$value" in + *[!a-zA-Z0-9._-]*) return 1 ;; + esac + length=$(printf '%s' "$value" | wc -c) + [ "$length" -le 100 ] || return 1 + case "$value" in + *.) return 1 ;; + esac + stem=$(printf '%s' "$value" | sed 's/\..*$//' | tr A-Z a-z) + case "$stem" in + aux|con|nul|prn|com[1-9]|lpt[1-9]) return 1 ;; + esac + [ "$(printf '%s' "$value" | tr A-Z a-z)" != "pnputil-install.cmd" ] +} + + +windows_target_present(){ + [ -d "$DRIVERPOSTSYNC_WINDOWS_ROOT/Windows/System32/config" ] || + [ -d "$DRIVERPOSTSYNC_WINDOWS_ROOT/WINDOWS/System32/config" ] || + [ -d "$DRIVERPOSTSYNC_WINDOWS_ROOT/windows/System32/config" ] +} + + +cleanup_managed_state(){ + driver_log "No driver profiles assigned; clearing managed target and image cache." + if ! rm -rf "$DRIVERPOSTSYNC_TARGET" "$DRIVERPOSTSYNC_CACHE"; then + driver_log "Failed to clear managed target or image cache." + DRIVERPOSTSYNC_RC=1 + fi + request_registry_action remove 0 +} + + +detect_system_task(){ + local WINDOWS_DIR + local PROGRAMDATA_DIR + local TASK_CANDIDATE + local MARKER_CANDIDATE + + DRIVERPOSTSYNC_TASK_READY=0 + + # Prefer the canonical native task. Task and marker are checked as a pair so + # a partial migration cannot be mistaken for a ready SYSTEM installation. + for WINDOWS_DIR in Windows WINDOWS windows; do + TASK_CANDIDATE="$DRIVERPOSTSYNC_WINDOWS_ROOT/$WINDOWS_DIR/System32/Tasks/LINBO-Driver-Install" + [ -f "$TASK_CANDIDATE" ] || continue + for PROGRAMDATA_DIR in ProgramData PROGRAMDATA programdata; do + MARKER_CANDIDATE="$DRIVERPOSTSYNC_WINDOWS_ROOT/$PROGRAMDATA_DIR/LINBO/Drivers/startup-task-ready" + if [ -f "$MARKER_CANDIDATE" ] && + [ "$(cat "$MARKER_CANDIDATE" 2>/dev/null)" = "LINBO SYSTEM driver startup task v1" ]; then + DRIVERPOSTSYNC_TASK_READY=1 + return 0 + fi + done + done + + # Existing golden images keep working during migration. Legacy names are + # read-only compatibility inputs. + for WINDOWS_DIR in Windows WINDOWS windows; do + TASK_CANDIDATE="$DRIVERPOSTSYNC_WINDOWS_ROOT/$WINDOWS_DIR/System32/Tasks/LINBO-Patchless-Driver-Install" + [ -f "$TASK_CANDIDATE" ] || continue + for PROGRAMDATA_DIR in ProgramData PROGRAMDATA programdata; do + MARKER_CANDIDATE="$DRIVERPOSTSYNC_WINDOWS_ROOT/$PROGRAMDATA_DIR/LINBO-Patchless/startup-task-ready" + if [ -f "$MARKER_CANDIDATE" ] && + [ "$(cat "$MARKER_CANDIDATE" 2>/dev/null)" = "LINBO-Patchless SYSTEM startup task v1" ]; then + DRIVERPOSTSYNC_TASK_READY=1 + return 0 + fi + done + done + + return 1 +} + +#### functions end #### + + +[ "$#" -ge 1 ] || usage 1 + +# get environment +source /usr/share/linbo/shell_functions +echo "### $timestamp $(basename "$0") $@ ###" + +DRIVERPOSTSYNC_IMAGE="$1" +shift + +if ! valid_image_name "$DRIVERPOSTSYNC_IMAGE"; then + echo "Invalid LINBO driver image name: $DRIVERPOSTSYNC_IMAGE" >&2 + exit 1 +fi + +DRIVERPOSTSYNC_PROFILE_KEYS="|" +for FOLDER in "$@"; do + if ! valid_profile_name "$FOLDER"; then + echo "Invalid LINBO driver profile name: $FOLDER" >&2 + exit 1 + fi + PROFILE_KEY=$(printf '%s' "$FOLDER" | tr A-Z a-z) + case "$DRIVERPOSTSYNC_PROFILE_KEYS" in + *"|$PROFILE_KEY|"*) + echo "Duplicate or case-colliding LINBO driver profile name: $FOLDER" >&2 + exit 1 + ;; + esac + DRIVERPOSTSYNC_PROFILE_KEYS="$DRIVERPOSTSYNC_PROFILE_KEYS$PROFILE_KEY|" +done +unset DRIVERPOSTSYNC_PROFILE_KEYS PROFILE_KEY + +DRIVERPOSTSYNC_RC=0 +DRIVERPOSTSYNC_LOG="/tmp/linbo-driverpostsync.log" +DRIVERPOSTSYNC_RSYNC_LOG="/tmp/linbo-driverpostsync-rsync.$$" +DRIVERPOSTSYNC_CACHE="/cache/linbo-driverprofiles/$DRIVERPOSTSYNC_IMAGE" +DRIVERPOSTSYNC_MATCH_CACHE="$DRIVERPOSTSYNC_CACHE/.match" +DRIVERPOSTSYNC_WINDOWS_ROOT="/mnt" +DRIVERPOSTSYNC_TARGET="$DRIVERPOSTSYNC_WINDOWS_ROOT/Drivers/LINBO" +DRIVERPOSTSYNC_BATCH_FILE="$DRIVERPOSTSYNC_TARGET/pnputil-install.cmd" +DRIVERPOSTSYNC_BATCH_TEMP="" +DRIVERPOSTSYNC_REGISTRY_FILE="/tmp/linbo-driver-install.$$.reg" +DRIVERPOSTSYNC_REGISTRY_LOG="/tmp/linbo-driverpostsync-registry.$$" +DRIVERPOSTSYNC_CLEANUP=0 + +if [ "$#" -eq 0 ]; then + DRIVERPOSTSYNC_CLEANUP=1 + echo "=== LINBO Driver Postsync tombstone for $DRIVERPOSTSYNC_IMAGE $(date) ===" > "$DRIVERPOSTSYNC_LOG" +else + echo "=== LINBO Driver Postsync for $DRIVERPOSTSYNC_IMAGE $(date) ===" > "$DRIVERPOSTSYNC_LOG" +fi + +if ! windows_target_present; then + if [ "$DRIVERPOSTSYNC_CLEANUP" = "1" ]; then + driver_log "Non-Windows target detected; cleanup tombstone skipped." + else + driver_log "Non-Windows target detected; driver integration skipped." + fi + persist_driver_log + exit 0 +fi + +# An invocation without profile arguments is the explicit cleanup tombstone. +if [ "$#" -eq 0 ]; then + cleanup_managed_state + persist_driver_log + exit "$DRIVERPOSTSYNC_RC" +fi + +if ! mkdir -p "$DRIVERPOSTSYNC_CACHE" "$DRIVERPOSTSYNC_MATCH_CACHE"; then + driver_log "Failed to create the driver profile cache." + persist_driver_log + exit 1 +fi + +# Read client DMI before downloading any large driver payload. Every assigned +# profile contributes only its small match.conf first. +SYS_VENDOR=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') +PRODUCT_NAME=$(cat /sys/class/dmi/id/product_name 2>/dev/null | tr -d '\n\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') +driver_log "DMI: vendor='$SYS_VENDOR' product='$PRODUCT_NAME'" + +MATCHED_FOLDERS="" +rm -f "$DRIVERPOSTSYNC_MATCH_CACHE/.staging-"*.conf + +driver_log "Syncing match metadata for image-assigned profiles..." +for FOLDER in "$@"; do + ACTIVE_CONF="$DRIVERPOSTSYNC_MATCH_CACHE/$FOLDER.conf" + STAGING_CONF="$DRIVERPOSTSYNC_MATCH_CACHE/.staging-$FOLDER-$$.conf" + rm -f "$STAGING_CONF" + + if rsync -a --timeout=120 "$LINBOSERVER::linbo/drivers/$FOLDER/match.conf" "$STAGING_CONF" > "$DRIVERPOSTSYNC_RSYNC_LOG" 2>&1 && + [ -f "$STAGING_CONF" ] && [ -s "$STAGING_CONF" ]; then + tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_RSYNC_LOG" + if mv "$STAGING_CONF" "$ACTIVE_CONF"; then + driver_log " Synced match metadata: $FOLDER" + else + rm -f "$STAGING_CONF" + driver_log " Failed to activate match metadata: $FOLDER; using cached metadata when available" + DRIVERPOSTSYNC_RC=1 + fi + else + tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_RSYNC_LOG" + rm -f "$STAGING_CONF" + driver_log " Failed to sync match metadata: $FOLDER; using cached metadata when available" + DRIVERPOSTSYNC_RC=1 + fi + + CONF="$ACTIVE_CONF" + [ -f "$CONF" ] || continue + + # Parse the canonical match.conf format. + CONF_VENDOR="" + CONF_PRODUCTS="" + IN_MATCH=0 + FOUND_MATCH_SECTION=0 + FOUND_VENDOR=0 + FOUND_PRODUCT=0 + INVALID_MATCH_ENTRY=0 + while IFS= read -r line || [ -n "$line" ]; do + line=$(printf '%s\n' "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + case "$line" in + \#*|\;*|"") continue ;; + \[*\]) + SECTION_NAME=$(printf '%s\n' "$line" | sed 's/^\[//;s/\]$//') + case "$SECTION_NAME" in + ""|*[!a-zA-Z0-9_.-]*) + INVALID_MATCH_ENTRY=1 + IN_MATCH=0 + continue + ;; + esac + case "$SECTION_NAME" in + [mM][aA][tT][cC][hH]) + if [ "$FOUND_MATCH_SECTION" = "1" ]; then + INVALID_MATCH_ENTRY=1 + IN_MATCH=0 + else + IN_MATCH=1 + FOUND_MATCH_SECTION=1 + fi + ;; + *) IN_MATCH=0 ;; + esac + continue + ;; + \[*) + INVALID_MATCH_ENTRY=1 + IN_MATCH=0 + continue + ;; + esac + [ "$IN_MATCH" = "0" ] && continue + + case "$line" in + *=*) ;; + *) INVALID_MATCH_ENTRY=1; continue ;; + esac + + key=$(printf '%s\n' "$line" | sed 's/[[:space:]]*=.*//;s/[[:space:]]*$//' | tr '[:upper:]' '[:lower:]') + value=$(printf '%s\n' "$line" | sed 's/^[^=]*=[[:space:]]*//;s/[[:space:]]*$//') + + case "$value" in + "") INVALID_MATCH_ENTRY=1; continue ;; + "*") ;; + *[!a-zA-Z0-9\ .,\(\)/_+#-]*) INVALID_MATCH_ENTRY=1; continue ;; + esac + + case "$key" in + vendor) + if [ "$FOUND_VENDOR" = "1" ]; then + INVALID_MATCH_ENTRY=1 + continue + fi + CONF_VENDOR="$value" + FOUND_VENDOR=1 + ;; + product) + CONF_PRODUCTS="$CONF_PRODUCTS|$value" + FOUND_PRODUCT=1 + ;; + *) INVALID_MATCH_ENTRY=1 ;; + esac + done < "$CONF" + + if [ "$FOUND_MATCH_SECTION" != "1" ] || [ "$FOUND_VENDOR" != "1" ] || + [ "$FOUND_PRODUCT" != "1" ] || [ -z "$CONF_VENDOR" ] || + [ "$INVALID_MATCH_ENTRY" != "0" ]; then + driver_log " $FOLDER: invalid match.conf (explicit [match], one vendor, and at least one non-empty product are required; use product = * as wildcard), skipping" + DRIVERPOSTSYNC_RC=1 + continue + fi + + # Vendor matching is exact unless an explicit wildcard is configured. + if [ "$CONF_VENDOR" != "*" ] && [ "$CONF_VENDOR" != "$SYS_VENDOR" ]; then + continue + fi + + # Product values are case-sensitive substrings or an explicit wildcard. + PRODUCTS_LEFT=${CONF_PRODUCTS#|} + while [ -n "$PRODUCTS_LEFT" ]; do + case "$PRODUCTS_LEFT" in + *"|"*) + pat=${PRODUCTS_LEFT%%|*} + PRODUCTS_LEFT=${PRODUCTS_LEFT#*|} + ;; + *) + pat="$PRODUCTS_LEFT" + PRODUCTS_LEFT="" + ;; + esac + [ -z "$pat" ] && continue + if [ "$pat" = "*" ]; then + driver_log " $FOLDER: vendor criteria matched, wildcard product" + MATCHED_FOLDERS="$MATCHED_FOLDERS $FOLDER" + break + fi + case "$PRODUCT_NAME" in + *"$pat"*) + driver_log " $FOLDER: matched (product contains '$pat')" + MATCHED_FOLDERS="$MATCHED_FOLDERS $FOLDER" + break + ;; + esac + done +done + +# Drop metadata for profiles no longer assigned to this image. +for CACHED_CONF in "$DRIVERPOSTSYNC_MATCH_CACHE"/*.conf; do + [ -f "$CACHED_CONF" ] || continue + CACHED_FILE=$(basename "$CACHED_CONF") + CACHED_NAME=$(printf '%s\n' "$CACHED_FILE" | sed 's/\.conf$//') + KEEP=0 + for FOLDER in "$@"; do + [ "$CACHED_NAME" = "$FOLDER" ] && KEEP=1 && break + done + [ "$KEEP" = "1" ] || rm -f "$CACHED_CONF" +done +rm -f "$DRIVERPOSTSYNC_RSYNC_LOG" + +driver_log "Matched folders: $MATCHED_FOLDERS" + +driver_log "Syncing full payload for matched profiles only..." +for FOLDER in $MATCHED_FOLDERS; do + ACTIVE_PROFILE="$DRIVERPOSTSYNC_CACHE/$FOLDER" + STAGING_PROFILE="$DRIVERPOSTSYNC_CACHE/.staging-$FOLDER-$$" + PREVIOUS_PROFILE="$DRIVERPOSTSYNC_CACHE/.previous-$FOLDER-$$" + + # Recover last-known-good state after an interrupted cache swap. + for CRASH_BACKUP in "$DRIVERPOSTSYNC_CACHE/.previous-$FOLDER-"*; do + [ -d "$CRASH_BACKUP" ] || continue + if [ ! -d "$ACTIVE_PROFILE" ]; then + if mv "$CRASH_BACKUP" "$ACTIVE_PROFILE"; then + driver_log " Recovered last-known-good cache: $FOLDER" + else + driver_log " Failed to recover cached profile: $FOLDER" + DRIVERPOSTSYNC_RC=1 + fi + else + rm -rf "$CRASH_BACKUP" + fi + done + rm -rf "$DRIVERPOSTSYNC_CACHE/.staging-$FOLDER-"* "$STAGING_PROFILE" "$PREVIOUS_PROFILE" + + if ! mkdir -p "$STAGING_PROFILE"; then + driver_log " Failed to create staging cache: $FOLDER" + DRIVERPOSTSYNC_RC=1 + continue + fi + + if rsync -a --delete --timeout=120 "$LINBOSERVER::linbo/drivers/$FOLDER/" "$STAGING_PROFILE/" > "$DRIVERPOSTSYNC_RSYNC_LOG" 2>&1; then + tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_RSYNC_LOG" + SWAP_OK=1 + if [ -d "$ACTIVE_PROFILE" ] && ! mv "$ACTIVE_PROFILE" "$PREVIOUS_PROFILE"; then + driver_log " Failed to stage previous cache: $FOLDER" + SWAP_OK=0 + fi + if [ "$SWAP_OK" = "1" ] && mv "$STAGING_PROFILE" "$ACTIVE_PROFILE"; then + rm -rf "$PREVIOUS_PROFILE" + driver_log " Synced matched profile: $FOLDER" + else + rm -rf "$STAGING_PROFILE" + if [ -d "$PREVIOUS_PROFILE" ] && [ ! -d "$ACTIVE_PROFILE" ]; then + if ! mv "$PREVIOUS_PROFILE" "$ACTIVE_PROFILE"; then + driver_log " Failed to restore last-known-good cache: $FOLDER" + fi + fi + driver_log " Failed to activate synced profile: $FOLDER; keeping last-known-good cache" + DRIVERPOSTSYNC_RC=1 + fi + else + tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_RSYNC_LOG" + rm -rf "$STAGING_PROFILE" + driver_log " Failed to sync matched profile: $FOLDER; keeping last-known-good cache" + DRIVERPOSTSYNC_RC=1 + fi +done +rm -f "$DRIVERPOSTSYNC_RSYNC_LOG" + +# Retain payloads for this client's current hardware matches only. +for CACHED_DIR in "$DRIVERPOSTSYNC_CACHE"/*; do + [ -d "$CACHED_DIR" ] || continue + CACHED_NAME=$(basename "$CACHED_DIR") + KEEP=0 + for FOLDER in $MATCHED_FOLDERS; do + [ "$CACHED_NAME" = "$FOLDER" ] && KEEP=1 && break + done + [ "$KEEP" = "1" ] || rm -rf "$CACHED_DIR" +done + +# Recreate the managed Windows driver target. +DRIVERPOSTSYNC_INSTALL_READY=1 +if ! rm -rf "$DRIVERPOSTSYNC_TARGET"; then + driver_log "Failed to clear managed Windows driver target; installation disabled." + DRIVERPOSTSYNC_INSTALL_READY=0 + DRIVERPOSTSYNC_RC=1 +elif ! mkdir -p "$DRIVERPOSTSYNC_TARGET"; then + driver_log "Failed to create managed Windows driver target; installation disabled." + DRIVERPOSTSYNC_INSTALL_READY=0 + DRIVERPOSTSYNC_RC=1 +fi + +if [ "$DRIVERPOSTSYNC_INSTALL_READY" = "1" ]; then + for FOLDER in $MATCHED_FOLDERS; do + SRC="$DRIVERPOSTSYNC_CACHE/$FOLDER" + if [ -d "$SRC" ]; then + FILE_COUNT=$(find "$SRC" -type f ! -name "match.conf" ! -name "image.conf" ! -name "driver-map.json" ! -name "driver-manifest.json" ! -name ".extracting" 2>/dev/null | wc -l) + if [ "$FILE_COUNT" -gt 0 ]; then + driver_log " Copying drivers from: $FOLDER ($FILE_COUNT files)" + DEST="$DRIVERPOSTSYNC_TARGET/$FOLDER" + if mkdir -p "$DEST" && + cp -R "$SRC/." "$DEST/" && + rm -f "$DEST/match.conf" "$DEST/image.conf" "$DEST/driver-map.json" "$DEST/driver-manifest.json" "$DEST/.extracting"; then + driver_log " Copied driver payload: $FOLDER" + else + driver_log " Failed to copy driver payload: $FOLDER; installation disabled" + rm -rf "$DEST" + DRIVERPOSTSYNC_INSTALL_READY=0 + DRIVERPOSTSYNC_RC=1 + break + fi + else + driver_log " $FOLDER: no driver files, skipping copy" + fi + fi + done +fi + +if [ "$DRIVERPOSTSYNC_INSTALL_READY" != "1" ]; then + if ! rm -f "$DRIVERPOSTSYNC_BATCH_FILE"; then + driver_log "Warning: could not remove stale pnputil-install.cmd." + fi +fi + +# Publish a retryable installer only after the complete payload is ready. +INF_COUNT=0 +if [ "$DRIVERPOSTSYNC_INSTALL_READY" = "1" ]; then + INF_COUNT=$(find "$DRIVERPOSTSYNC_TARGET" -iname '*.inf' 2>/dev/null | wc -l) +fi + +DRIVERPOSTSYNC_BATCH_READY=0 +if [ "$INF_COUNT" -gt 0 ]; then + DRIVERPOSTSYNC_BATCH_TEMP=$(mktemp "$DRIVERPOSTSYNC_TARGET/.pnputil-install.cmd.tmp.XXXXXX" 2>/dev/null) + if [ -n "$DRIVERPOSTSYNC_BATCH_TEMP" ] && + printf '@echo off\r\n' > "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'setlocal EnableExtensions\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'set "LINBO_LOG_DIR=%%ProgramData%%\\LINBO\\Drivers"\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if not exist "%%LINBO_LOG_DIR%%" mkdir "%%LINBO_LOG_DIR%%"\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'set "LINBO_LOG=%%LINBO_LOG_DIR%%\\driver-install.log"\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf '>>"%%LINBO_LOG%%" echo [%%DATE%% %%TIME%%] Starting LINBO driver installation.\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf '"%%SystemRoot%%\\System32\\pnputil.exe" /add-driver C:\\Drivers\\LINBO\\*.inf /subdirs /install >>"%%LINBO_LOG%%" 2>&1\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'set "LINBO_RC=%%ERRORLEVEL%%"\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if "%%LINBO_RC%%"=="0" goto success\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if "%%LINBO_RC%%"=="259" goto no_action\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if "%%LINBO_RC%%"=="1641" goto success\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if "%%LINBO_RC%%"=="3010" goto success\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf '>>"%%LINBO_LOG%%" echo [%%DATE%% %%TIME%%] pnputil failed with exit code %%LINBO_RC%%; installer retained for retry.\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'exit /b %%LINBO_RC%%\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf ':no_action\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf '>>"%%LINBO_LOG%%" echo [%%DATE%% %%TIME%%] pnputil completed with exit code 259: no matching device or a better/newer driver is already active.\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'goto cleanup\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf ':success\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf '>>"%%LINBO_LOG%%" echo [%%DATE%% %%TIME%%] Driver installation succeeded with exit code %%LINBO_RC%%.\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf ':cleanup\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'del "%%~f0" >>"%%LINBO_LOG%%" 2>&1\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'if exist "%%~f0" exit /b 1\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + printf 'exit /b 0\r\n' >> "$DRIVERPOSTSYNC_BATCH_TEMP" && + mv "$DRIVERPOSTSYNC_BATCH_TEMP" "$DRIVERPOSTSYNC_BATCH_FILE"; then + DRIVERPOSTSYNC_BATCH_TEMP="" + DRIVERPOSTSYNC_BATCH_READY=1 + else + driver_log "Failed to create pnputil-install.cmd; installation disabled." + [ -z "$DRIVERPOSTSYNC_BATCH_TEMP" ] || rm -f "$DRIVERPOSTSYNC_BATCH_TEMP" + rm -f "$DRIVERPOSTSYNC_BATCH_FILE" + DRIVERPOSTSYNC_INSTALL_READY=0 + DRIVERPOSTSYNC_RC=1 + fi +fi + +detect_system_task + +if [ "$DRIVERPOSTSYNC_BATCH_READY" = "1" ] && [ "$DRIVERPOSTSYNC_TASK_READY" = "1" ]; then + request_registry_action task "$INF_COUNT" +elif [ "$DRIVERPOSTSYNC_BATCH_READY" = "1" ]; then + request_registry_action fallback "$INF_COUNT" +else + if [ "$DRIVERPOSTSYNC_INSTALL_READY" != "1" ]; then + driver_log "Driver payload unavailable — clearing RunOnce fallbacks." + else + driver_log "No INF files found — clearing RunOnce fallbacks." + fi + request_registry_action remove 0 +fi + +persist_driver_log +exit "$DRIVERPOSTSYNC_RC" From 26d4c872548e578c4e4f18aa9f41d1cb82323fd6 Mon Sep 17 00:00:00 2001 From: amo Date: Mon, 20 Jul 2026 09:57:12 +0200 Subject: [PATCH 2/3] Exclude driver profile metadata from payloads --- src/linbofs/usr/bin/linbo_driverpostsync | 30 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/linbofs/usr/bin/linbo_driverpostsync b/src/linbofs/usr/bin/linbo_driverpostsync index 384486c..2d4d16a 100755 --- a/src/linbofs/usr/bin/linbo_driverpostsync +++ b/src/linbofs/usr/bin/linbo_driverpostsync @@ -140,6 +140,18 @@ valid_profile_name(){ } +sync_profile_payload(){ + rsync \ + --exclude="/match.conf" \ + --exclude="/image.conf" \ + --exclude="/.match.conf.bak.*" \ + --exclude="/.image.conf.bak.*" \ + --exclude="/.match.conf.*.tmp" \ + --exclude="/.image.conf.*.tmp" \ + "$@" +} + + windows_target_present(){ [ -d "$DRIVERPOSTSYNC_WINDOWS_ROOT/Windows/System32/config" ] || [ -d "$DRIVERPOSTSYNC_WINDOWS_ROOT/WINDOWS/System32/config" ] || @@ -469,7 +481,7 @@ for FOLDER in $MATCHED_FOLDERS; do continue fi - if rsync -a --delete --timeout=120 "$LINBOSERVER::linbo/drivers/$FOLDER/" "$STAGING_PROFILE/" > "$DRIVERPOSTSYNC_RSYNC_LOG" 2>&1; then + if sync_profile_payload -a --delete --timeout=120 "$LINBOSERVER::linbo/drivers/$FOLDER/" "$STAGING_PROFILE/" > "$DRIVERPOSTSYNC_RSYNC_LOG" 2>&1; then tee -a "$DRIVERPOSTSYNC_LOG" < "$DRIVERPOSTSYNC_RSYNC_LOG" SWAP_OK=1 if [ -d "$ACTIVE_PROFILE" ] && ! mv "$ACTIVE_PROFILE" "$PREVIOUS_PROFILE"; then @@ -525,13 +537,23 @@ if [ "$DRIVERPOSTSYNC_INSTALL_READY" = "1" ]; then for FOLDER in $MATCHED_FOLDERS; do SRC="$DRIVERPOSTSYNC_CACHE/$FOLDER" if [ -d "$SRC" ]; then - FILE_COUNT=$(find "$SRC" -type f ! -name "match.conf" ! -name "image.conf" ! -name "driver-map.json" ! -name "driver-manifest.json" ! -name ".extracting" 2>/dev/null | wc -l) + FILE_COUNT=$(find "$SRC" -type f \ + ! -path "$SRC/match.conf" \ + ! -path "$SRC/image.conf" \ + ! -path "$SRC/.match.conf.bak.*" \ + ! -path "$SRC/.image.conf.bak.*" \ + ! -path "$SRC/.match.conf.*.tmp" \ + ! -path "$SRC/.image.conf.*.tmp" \ + ! -path "$SRC/driver-map.json" \ + ! -path "$SRC/driver-manifest.json" \ + ! -path "$SRC/.extracting" \ + 2>/dev/null | wc -l) if [ "$FILE_COUNT" -gt 0 ]; then driver_log " Copying drivers from: $FOLDER ($FILE_COUNT files)" DEST="$DRIVERPOSTSYNC_TARGET/$FOLDER" if mkdir -p "$DEST" && - cp -R "$SRC/." "$DEST/" && - rm -f "$DEST/match.conf" "$DEST/image.conf" "$DEST/driver-map.json" "$DEST/driver-manifest.json" "$DEST/.extracting"; then + sync_profile_payload -r "$SRC/" "$DEST/" && + rm -f "$DEST/match.conf" "$DEST/image.conf" "$DEST"/.match.conf.bak.* "$DEST"/.image.conf.bak.* "$DEST"/.match.conf.*.tmp "$DEST"/.image.conf.*.tmp "$DEST/driver-map.json" "$DEST/driver-manifest.json" "$DEST/.extracting"; then driver_log " Copied driver payload: $FOLDER" else driver_log " Failed to copy driver payload: $FOLDER; installation disabled" From 719231a3936ac16021114b58cc2744f968f3611d Mon Sep 17 00:00:00 2001 From: amo Date: Wed, 22 Jul 2026 14:06:41 +0200 Subject: [PATCH 3/3] test: cover driver postsync name validation --- docs/linbofs-windows-driver-profiles.md | 33 ++++++++----- tests/shell/README.md | 18 +++++-- tests/shell/test_linbo_driverpostsync.sh | 60 ++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 tests/shell/test_linbo_driverpostsync.sh diff --git a/docs/linbofs-windows-driver-profiles.md b/docs/linbofs-windows-driver-profiles.md index 4738c09..ebe59b3 100644 --- a/docs/linbofs-windows-driver-profiles.md +++ b/docs/linbofs-windows-driver-profiles.md @@ -1,8 +1,8 @@ # Native Windows driver profiles for LINBO - Status: implementation workbench; first reference-server end-to-end test passed -- Target branch: `main` -- Planning base: `e17d008` (`main` and `7.4`, `linuxmuster-linbo7` 7.4.6) +- Target branch: `7.4` +- Planning base: `3b3c3f4` (`7.4`, `linuxmuster-linbo7` 7.4.8) - Related components: `linuxmuster-tools7`, `linuxmuster-api7` ## Decision @@ -367,9 +367,13 @@ Scope: - add the static runtime under `src/linbofs/usr/bin`; - preserve the current generated-hook behavior exactly; - verify it is executable and included in the built linbofs template; -- define portable behavior cases derived from the current Tools runtime tests - and run them in the development workbench; do not add a new LINBO test - framework without maintainer agreement; +- cover the pure image and profile name validators with the existing + `tests/shell` harness under both `dash` and BusyBox `ash`; +- keep the inline `match.conf` parser as a documented Wave 2 test candidate; + isolating its rsync, cache and filesystem dependencies requires dedicated + stubs or fixtures and is not part of this behavior-preserving extraction; +- continue to run the remaining portable behavior cases derived from the + current Tools runtime tests in the development workbench; - document the runtime command contract; - do not change profile storage, API behavior or Windows bootstrap delivery. @@ -467,6 +471,13 @@ implicitly by the runtime extraction. ### Portable runtime behavior cases +The in-repository Wave 1 tests use `tests/shell/test_linbo_driverpostsync.sh` +to extract `valid_image_name()` and `valid_profile_name()` without executing +the runtime's top-level filesystem and network operations. The inline +`match.conf` parser and the following end-to-end behavior cases remain Wave 2 +because they require controlled rsync, cache, filesystem and registry stubs or +integration fixtures: + - case-sensitive exact vendor match with case-sensitive product substring; - multiple products and the existing explicit wildcard behavior; - no matching profile; @@ -569,20 +580,18 @@ last-known-good cache scenario. - no change to the image-name-with-dots limitation in this refactoring; - no automatic invention of a Windows scheduled task; - no unrelated LINBO sync, image or firmware fixes; -- no 7.3 backport in the `main` PR. +- no 7.3 backport in this PR. ## Decisions required before upstream release 1. Confirm the executable name and positional argument contract with the LINBO maintainer. -2. Decide the accepted location for focused runtime tests because this - repository currently has no general shell-test harness. -3. Decide separately whether a driver-runtime failure should set the overall +2. Decide separately whether a driver-runtime failure should set the overall `linbo_sync` return code. -4. Agree on the release guard with the maintainers; only if it is a package +3. Agree on the release guard with the maintainers; only if it is a package relationship, record the real released LINBO version before changing it. -5. Define how fleet readiness is established before Tools publishes thin +4. Define how fleet readiness is established before Tools publishes thin dispatchers to clients that may still be running an old `linbofs`. -6. Confirm the packaging and merge order for the Windows golden-image SYSTEM +5. Confirm the packaging and merge order for the Windows golden-image SYSTEM task; it remains outside this LINBO runtime PR and is not present in LINBO 7.4.6. diff --git a/tests/shell/README.md b/tests/shell/README.md index 6de0daf..7f2b3b4 100644 --- a/tests/shell/README.md +++ b/tests/shell/README.md @@ -4,11 +4,12 @@ Lightweight unit tests for individual functions inside `src/linbofs/usr/bin/` scripts, using [shunit2](https://github.com/kward/shunit2) (vendored as a single file in `shunit2`, no external dependency). -linbofs scripts run under busybox `ash` on real clients, and this repo has no -general shell-test harness (see `docs/proposal-shell-test-harness.de.md` for -the full design rationale). Tests run under both `dash` (`/bin/sh` in the -`lmndev-runner` build container) and `busybox ash`, since those are the two -shells linbofs code actually has to work under. +linbofs scripts run under busybox `ash` on real clients. This directory +provides the repository's general shell-test harness (see +`docs/proposal-shell-test-harness.de.md` for the full design rationale). Tests +run under both `dash` (`/bin/sh` in the `lmndev-runner` build container) and +`busybox ash`, since those are the two shells linbofs code actually has to +work under. ## Running the tests locally @@ -99,6 +100,8 @@ only because it's already used in production. today without stubs: - `convert_size()` (`linbo_partition`) - covered by `test_linbo_partition.sh`. +- `valid_image_name()` and `valid_profile_name()` (`linbo_driverpostsync`) - + covered by `test_linbo_driverpostsync.sh`. **Wave 2** - functions that need stubs or fixtures before they're unit-testable; tracked here rather than forced or used as an excuse to refactor the code @@ -108,6 +111,11 @@ first: from `shell_functions`, which read real `start.conf` files and sometimes block devices. Needs those helpers stubbed, or a fixture variant of `shell_functions`. +- The inline `match.conf` parsing and DMI matching in + `linbo_driverpostsync` combines downloaded or cached metadata, file reads + and local sysfs data. It needs fixtures and stubs before it can be + unit-tested; extracting it into a separate function is intentionally + outside this behavior-preserving runtime move. - `findcache()` (`linbo_mountcache`) - iterates `/dev/disk/by-id/*part*` and mounts real partitions. Not sensibly unit-testable without abstracting the device enumeration (e.g. an overridable variable instead of a hardcoded diff --git a/tests/shell/test_linbo_driverpostsync.sh b/tests/shell/test_linbo_driverpostsync.sh new file mode 100644 index 0000000..c5dcd2c --- /dev/null +++ b/tests/shell/test_linbo_driverpostsync.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# +# test_linbo_driverpostsync.sh +# ahmed.alani@netzint.de +# 20260722 +# +# Wave 1 tests: argument validation without filesystem, device or network +# access. + +SCRIPT="$(dirname "$0")/../../src/linbofs/usr/bin/linbo_driverpostsync" + +oneTimeSetUp() { + . "$(dirname "$0")/lib/extract_function.sh" + extract_function "$SCRIPT" valid_image_name valid_profile_name \ + > "$SHUNIT_TMPDIR/linbo_driverpostsync_validation.sh" + . "$SHUNIT_TMPDIR/linbo_driverpostsync_validation.sh" +} + +repeat_char() { + awk -v count="$1" 'BEGIN { for (i = 0; i < count; i++) printf "a" }' +} + +test_valid_image_name_accepts_supported_names() { + valid_image_name "win11" + assertEquals 0 $? + valid_image_name "win11_2026-test" + assertEquals 0 $? + valid_image_name "$(repeat_char 100)" + assertEquals 0 $? +} + +test_valid_image_name_rejects_unsafe_or_oversized_names() { + for value in "" "../win11" "win11.qcow2" "win 11" "win*" "$(repeat_char 101)"; do + valid_image_name "$value" + assertNotEquals "image '$value' should be rejected: " 0 $? + done +} + +test_valid_profile_name_accepts_supported_names() { + for value in "Lenovo-21L4" "intel.graphics" "com10" "$(repeat_char 100)"; do + valid_profile_name "$value" + assertEquals "profile '$value' should be accepted: " 0 $? + done +} + +test_valid_profile_name_rejects_unsafe_names() { + for value in "" ".hidden" "-profile" "_profile" "../profile" "profile/child" "profile name" "profile?" "profile." "$(repeat_char 101)"; do + valid_profile_name "$value" + assertNotEquals "profile '$value' should be rejected: " 0 $? + done +} + +test_valid_profile_name_rejects_windows_reserved_names() { + for value in "aux" "CON" "nul.inf" "PrN" "com1" "COM9.driver" "lpt1" "LPT9.inf" "pnputil-install.cmd" "PnPUtil-Install.CMD"; do + valid_profile_name "$value" + assertNotEquals "reserved profile '$value' should be rejected: " 0 $? + done +} + +. "$(dirname "$0")/shunit2"