Compare commits
11
Commits
1ebebb86e2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9385583955 | ||
|
|
2bdacaf37a | ||
|
|
6d63457737 | ||
|
|
aeeb5fdf3c | ||
|
|
ba8c8231ad | ||
|
|
d401538820 | ||
|
|
d65910a7c5 | ||
|
|
bf9871eb9e | ||
|
|
abe9873e7c | ||
|
|
af37c88162 | ||
|
|
eb363ec42d |
No files matched your search
@@ -0,0 +1,65 @@
|
|||||||
|
BasedOnStyle: LLVM
|
||||||
|
AccessModifierOffset: -4
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignConsecutiveAssignments: None
|
||||||
|
AlignOperands: Align
|
||||||
|
AllowAllArgumentsOnNextLine: false
|
||||||
|
AllowAllConstructorInitializersOnNextLine: false
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: false
|
||||||
|
AllowShortBlocksOnASingleLine: Always
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
AllowShortIfStatementsOnASingleLine: Always
|
||||||
|
AllowShortLambdasOnASingleLine: All
|
||||||
|
AllowShortLoopsOnASingleLine: true
|
||||||
|
AlwaysBreakAfterReturnType: None
|
||||||
|
AlwaysBreakTemplateDeclarations: Yes
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BraceWrapping:
|
||||||
|
AfterCaseLabel: false
|
||||||
|
AfterClass: false
|
||||||
|
AfterControlStatement: Never
|
||||||
|
AfterEnum: false
|
||||||
|
AfterFunction: false
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterUnion: false
|
||||||
|
BeforeCatch: false
|
||||||
|
BeforeElse: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: false
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakConstructorInitializers: BeforeColon
|
||||||
|
BreakInheritanceList: BeforeColon
|
||||||
|
ColumnLimit: 0
|
||||||
|
CompactNamespaces: false
|
||||||
|
ContinuationIndentWidth: 8
|
||||||
|
IndentCaseLabels: true
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentWidth: 4
|
||||||
|
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
|
MaxEmptyLinesToKeep: 2
|
||||||
|
NamespaceIndentation: All
|
||||||
|
ObjCSpaceAfterProperty: false
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
PointerAlignment: Right
|
||||||
|
ReflowComments: false
|
||||||
|
SpaceAfterCStyleCast: true
|
||||||
|
SpaceAfterLogicalNot: false
|
||||||
|
SpaceAfterTemplateKeyword: false
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCpp11BracedList: false
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: false
|
||||||
|
SpaceInEmptyParentheses: false
|
||||||
|
SpacesBeforeTrailingComments: 0
|
||||||
|
SpacesInAngles: false
|
||||||
|
SpacesInCStyleCastParentheses: false
|
||||||
|
SpacesInContainerLiterals: false
|
||||||
|
SpacesInParentheses: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
TabWidth: 4
|
||||||
|
UseTab: Never
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Reader-facing changes to the articles, architecture decisions, diagrams, and practical examples, newest first. New work is listed by change date; older entries summarize the Git history by commit date. Issue references are included where recorded in commit messages. File links open the current material, while commit links show the historical changes.
|
||||||
|
|
||||||
|
When adding or revising material, add a short entry under the relevant date with links to the affected files and any related issue or commit.
|
||||||
|
|
||||||
|
## 2026-10-20
|
||||||
|
|
||||||
|
- Added [chapter 06: Atomic Updates](docs/06-atomic-updates.md), covering preparation before activation, durable state, gaps between records, restart decisions, and the limits of package recovery.
|
||||||
|
- Added [ADR-018: Prepare and Verify Before Activation](adr/ADR-018-prepare-and-verify-before-activation.md) and [ADR-019: Recover Interrupted Updates From Durable State](adr/ADR-019-recover-interrupted-updates-from-durable-state.md).
|
||||||
|
- Added PlantUML sources for the [installation sequence](diagrams/plantuml/ab-installation-sequence.puml) and [restart decisions](diagrams/plantuml/ab-installation-recovery.puml). No new image previews are included.
|
||||||
|
- Added a [C++17 interrupted installation model](examples/simulations/interrupted-installation/readme.md), with reset points, three supplied boot-record write outcomes, recovery checks, and CMake/CTest support. It models completed durable operations in memory, not real power-loss-safe storage.
|
||||||
|
- Applied the root [.clang-format](.clang-format) to the new model and the existing [A/B trial boot example](examples/simulations/ab-trial-boot/readme.md). Existing C++ changes are formatting only.
|
||||||
|
- Updated the [README](readme.md), [series overview](docs/00-series-overview.md), [chapter 05 continuation](docs/05-storage-layout.md), and [diagram index](diagrams/readme.md).
|
||||||
|
|
||||||
|
## 2026-10-11
|
||||||
|
|
||||||
|
### Storage layout
|
||||||
|
|
||||||
|
- Added [chapter 05: Storage Layout](docs/05-storage-layout.md), covering system slots, persistent device data, shared-data compatibility, update and recovery state, storage capacity, and failure scenarios.
|
||||||
|
- Added [ADR-016: Separate System Software and Persistent Device Data](adr/ADR-016-separate-system-software-and-persistent-device-data.md) and [ADR-017: Keep Recovery State Outside Replaceable Software](adr/ADR-017-keep-recovery-state-outside-replaceable-software.md).
|
||||||
|
- Updated the [series overview](docs/00-series-overview.md) and the continuation from [chapter 04](docs/04-starting-an-updated-system-or-service.md).
|
||||||
|
|
||||||
|
History: [bf9871e](https://git.himro.ca/deeaitch/ota-reference-design/commit/bf9871eb9ee5f122f41750242dda12097a4364ad), which records closing [issue #2](https://git.himro.ca/deeaitch/ota-reference-design/issues/2).
|
||||||
|
|
||||||
|
### Diagrams and A/B simulation
|
||||||
|
|
||||||
|
- Added three diagrams for chapters 03–05, with editable PlantUML sources and PNG previews:
|
||||||
|
- OTA architecture: [source](diagrams/plantuml/ota-architecture.puml), [preview](diagrams/rendered/ota-architecture.png).
|
||||||
|
- A/B trial boot, confirmation, and recovery: [source](diagrams/plantuml/ab-trial-boot.puml), [preview](diagrams/rendered/ab-trial-boot.png).
|
||||||
|
- A/B storage layout: [source](diagrams/plantuml/ab-storage-layout.puml), [preview](diagrams/rendered/ab-storage-layout.png).
|
||||||
|
- Added [diagram rendering instructions](diagrams/readme.md) and links from the corresponding chapters.
|
||||||
|
- Added a [C++17 A/B trial boot simulation](examples/simulations/ab-trial-boot/readme.md) with six scenarios: successful confirmation, failed health checks, exhausted attempts, restart during trial, interrupted preparation, and recovery failure. The example includes saved boot state, [state and scenario checks](examples/simulations/ab-trial-boot/tests.cpp), and [CMake/CTest integration](examples/simulations/ab-trial-boot/CMakeLists.txt). It models logical transitions assuming valid completed writes; power-loss safety is outside its scope.
|
||||||
|
- Linked the practical material from the [README](readme.md) and [series overview](docs/00-series-overview.md).
|
||||||
|
|
||||||
|
History: [d65910a](https://git.himro.ca/deeaitch/ota-reference-design/commit/d65910a7c5157efecf61adf6b0d860845191a687), [d401538](https://git.himro.ca/deeaitch/ota-reference-design/commit/d401538820c2695ae545198642ec8e63e77ff906).
|
||||||
|
|
||||||
|
## 2026-10-05
|
||||||
|
|
||||||
|
- Added [chapter 04: Starting an Updated System or Service](docs/04-starting-an-updated-system-or-service.md), explaining startup, trial boot, health checks, confirmation, service recovery, and remembering update outcomes.
|
||||||
|
- Added [ADR-014: Trial Boot and Confirmation](adr/ADR-014-trial-boot-and-confirmation.md) and [ADR-015: Service Checks and Package Downgrade](adr/ADR-015-service-checks-and-package-downgrade.md), and clarified component rollback in [ADR-005](adr/ADR-005-package-based-updates.md).
|
||||||
|
- Added related ADR links to chapter 04 and updated the [learning path](docs/00-series-overview.md).
|
||||||
|
- Removed status and date fields from the [ADR template](adr/template.md) and existing decisions that contained them.
|
||||||
|
|
||||||
|
History: [eb363ec](https://git.himro.ca/deeaitch/ota-reference-design/commit/eb363ec42d2f5637a7b5c0f1e5ecf9830ff2c5f9) references [issue #1](https://git.himro.ca/deeaitch/ota-reference-design/issues/1); [af37c88](https://git.himro.ca/deeaitch/ota-reference-design/commit/af37c881625aa43e9aa75dd4241ed5ff019d6525), [abe9873](https://git.himro.ca/deeaitch/ota-reference-design/commit/abe9873e7c84e7e85cf14537750722968354d550).
|
||||||
|
|
||||||
|
## 2026-09-21
|
||||||
|
|
||||||
|
- Added and simplified [chapter 03: OTA System Architecture](docs/03-system-architecture.md), describing server and device responsibilities and the update flow.
|
||||||
|
- Consolidated the update-check decision into [ADR-011: Device-Initiated Update Checks](adr/ADR-011-device-initiated-update-checks.md). Added heading-only placeholders for [ADR-012: Server-Initiated Updates](adr/ADR-012-server-initiated-updates.md) and [ADR-013: Update Notifications with Device-Initiated Checks](adr/ADR-013-update-notifications-with-device-initiated-checks.md).
|
||||||
|
- Reworked [chapter 02: Choosing an OTA Update Strategy](docs/02-update_strategies.md) into a shorter comparison of update units and delivery mechanisms.
|
||||||
|
- Added decisions for [independent application updates](adr/ADR-007-independent-application-updates.md), [binary delta delivery](adr/ADR-008-binary-delta-delivery.md), [versioned filesystem trees](adr/ADR-009-versioned-filesystem-trees.md), and [hybrid strategies](adr/ADR-010-hybrid-update-strategies.md).
|
||||||
|
|
||||||
|
History: [98edac4](https://git.himro.ca/deeaitch/ota-reference-design/commit/98edac44538b42635feac2a5c9f153dc22439c41), [252a7e4](https://git.himro.ca/deeaitch/ota-reference-design/commit/252a7e4c273fdd9583950d20438fed87fcc812fa), [572d1f7](https://git.himro.ca/deeaitch/ota-reference-design/commit/572d1f7acc776d023b8d4c214b4bad52e062e918), [3c94145](https://git.himro.ca/deeaitch/ota-reference-design/commit/3c941454910a358c7978fc1c196051edc33ab552), [13a88b7](https://git.himro.ca/deeaitch/ota-reference-design/commit/13a88b7270f2c8bd825f170fab72c60e2190564b), [931efea](https://git.himro.ca/deeaitch/ota-reference-design/commit/931efeabc73152d2315cb8674da78ca932cf34ba), [1ebebb8](https://git.himro.ca/deeaitch/ota-reference-design/commit/1ebebb86e27e8881f53131f332c1a6b832f58c71).
|
||||||
|
|
||||||
|
## 2026-08-03
|
||||||
|
|
||||||
|
- Added the first version of [chapter 02: Choosing an OTA Update Strategy](docs/02-update_strategies.md).
|
||||||
|
- Documented [full-system image updates](adr/ADR-004-full-system-image-updates.md), [package-based updates](adr/ADR-005-package-based-updates.md), and [container-based updates](adr/ADR-006-container-based-updates.md) as architecture decisions.
|
||||||
|
- Revised the strategy discussion to cover application-level updates through package-based updates at that stage of the series.
|
||||||
|
- Added the [guide to architecture decision records](adr/readme.md).
|
||||||
|
|
||||||
|
History: [2b49cbe](https://git.himro.ca/deeaitch/ota-reference-design/commit/2b49cbe01003276841ad510a546b2831a0136d4b), [7a74d9f](https://git.himro.ca/deeaitch/ota-reference-design/commit/7a74d9fca35897db26840b4bc03a833d5b0e75a4), [155cca5](https://git.himro.ca/deeaitch/ota-reference-design/commit/155cca5eeb3a66f71ee1f58a793a0d4fece4994f), [be12197](https://git.himro.ca/deeaitch/ota-reference-design/commit/be12197ac07d7c9ca57089ec887ba6280edd0067), [37583d8](https://git.himro.ca/deeaitch/ota-reference-design/commit/37583d820a98d91aed763a57bbc2917e99817173), [2da217c](https://git.himro.ca/deeaitch/ota-reference-design/commit/2da217c0cefd736ed2485a77ff850b4c66294b47).
|
||||||
|
|
||||||
|
## 2026-07-29
|
||||||
|
|
||||||
|
- Started the repository with the [series overview](docs/00-series-overview.md) and [chapter 01: What Is an OTA Update?](docs/01-what-is-an-ota-update.md).
|
||||||
|
- Established the [repository name](adr/ADR-001-repository-name.md), [MIT license](adr/ADR-002-license.md), and [educational reference-design scope](adr/ADR-003-reference-design.md) in the first architecture decisions.
|
||||||
|
- Added the [ADR template](adr/template.md), organized decisions under `adr/`, and expanded the [README](readme.md) to explain the project.
|
||||||
|
|
||||||
|
History: [initial commit](https://git.himro.ca/deeaitch/ota-reference-design/commit/eae04d851bb8d461909cd26def0f52581657cda6), [series overview](https://git.himro.ca/deeaitch/ota-reference-design/commit/7a5e040c38d7c31d53cf0aaf719163ef7f93128d), [OTA introduction](https://git.himro.ca/deeaitch/ota-reference-design/commit/5b8566e69c91b0634c23b368305e3c14d41dfc24), [README update](https://git.himro.ca/deeaitch/ota-reference-design/commit/556163a92b18969d7210f460dc0cf918d8f25653).
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
# ADR-001: Use `ota-reference-design` as the Repository Name
|
# ADR-001: Use `ota-reference-design` as the Repository Name
|
||||||
|
|
||||||
* **Status:** Accepted
|
|
||||||
* **Date:** 2026-07-29
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
The project requires a repository name that communicates its purpose clearly without tying the design to a single hardware platform, implementation language, or update framework.
|
The project requires a repository name that communicates its purpose clearly without tying the design to a single hardware platform, implementation language, or update framework.
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# ADR-002: Use the MIT License
|
# ADR-002: Use the MIT License
|
||||||
|
|
||||||
* **Status:** Accepted
|
|
||||||
* **Date:** 2026-07-29
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
The project is intended to be an open educational and engineering reference for designing OTA update systems on embedded Linux.
|
The project is intended to be an open educational and engineering reference for designing OTA update systems on embedded Linux.
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# ADR-003: Build a Reference Design, Not a Production Framework
|
# ADR-003: Build a Reference Design, Not a Production Framework
|
||||||
|
|
||||||
* **Status:** Accepted
|
|
||||||
* **Date:** 2026-07-29
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
OTA update systems differ significantly between products.
|
OTA update systems differ significantly between products.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-004: Full-System Image Updates
|
# ADR-004: Full-System Image Updates
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Accepted
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
An OTA architecture must define the unit of update.
|
An OTA architecture must define the unit of update.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-005: Package-Based Updates
|
# ADR-005: Package-Based Updates
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Accepted
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
An OTA architecture must define the unit of update.
|
An OTA architecture must define the unit of update.
|
||||||
@@ -90,7 +86,7 @@ The package manager is responsible for:
|
|||||||
- verifying signatures and integrity;
|
- verifying signatures and integrity;
|
||||||
- executing installation scripts;
|
- executing installation scripts;
|
||||||
- replacing files;
|
- replacing files;
|
||||||
- retaining previously installed versions.
|
- retaining previous packages when configured to do so, or working with a separate local package store.
|
||||||
|
|
||||||
The OTA manager is responsible for update policy, including when updates are allowed, network restrictions, user approval, and operational conditions.
|
The OTA manager is responsible for update policy, including when updates are allowed, network restrictions, user approval, and operational conditions.
|
||||||
|
|
||||||
@@ -98,9 +94,11 @@ Using a package manager alone does not constitute a complete OTA solution.
|
|||||||
|
|
||||||
### Component Rollback
|
### Component Rollback
|
||||||
|
|
||||||
Each component can be upgraded and downgraded independently.
|
An independent component can be restored without reverting the entire system when the package manager supports downgrade and the previous version remains compatible with its dependencies, configuration, and data.
|
||||||
|
|
||||||
Most package managers retain previously installed versions, allowing an individual component to be rolled back without reverting the entire system.
|
The update design must keep the previous package available. Automatic retention must be checked for the selected package manager; it cannot be assumed.
|
||||||
|
|
||||||
|
[ADR-015: Service Checks and Package Downgrade](ADR-015-service-checks-and-package-downgrade.md) describes a recovery policy for a service that fails after an update. It covers startup checks, an explicit downgrade, a saved block against retrying the failed version, and reporting. That example requires local retention of the previous package and verified downgrade support.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-006: Container-Based Updates
|
# ADR-006: Container-Based Updates
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Accepted
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
An OTA architecture must define the unit of update.
|
An OTA architecture must define the unit of update.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-007: Independent Application Updates
|
# ADR-007: Independent Application Updates
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
An embedded Linux device may contain applications that change more frequently than its operating system. Releasing a complete system image for every application change can tie unrelated work to the same release schedule.
|
An embedded Linux device may contain applications that change more frequently than its operating system. Releasing a complete system image for every application change can tie unrelated work to the same release schedule.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-008: Binary Delta Delivery
|
# ADR-008: Binary Delta Delivery
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
A device may need a large artifact even though much of its content is already present in the installed version. Transferring the complete artifact can be expensive or slow on a constrained connection.
|
A device may need a large artifact even though much of its content is already present in the installed version. Transferring the complete artifact can be expensive or slow on a constrained connection.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-009: Versioned Filesystem Trees
|
# ADR-009: Versioned Filesystem Trees
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
A software platform can be released as a complete set of files and directories without representing every release as a separate block image.
|
A software platform can be released as a complete set of files and directories without representing every release as a separate block image.
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
# ADR-010: Hybrid Update Strategies
|
# ADR-010: Hybrid Update Strategies
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Proposed
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
An embedded Linux device may contain a slowly changing platform, frequently updated applications, and data maintained on a separate schedule.
|
An embedded Linux device may contain a slowly changing platform, frequently updated applications, and data maintained on a separate schedule.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# ADR-014: Trial Boot and Confirmation for System Updates
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A new system image can install correctly and still fail to start or run the main application. If the new system cannot start the OTA manager, that manager cannot request recovery.
|
||||||
|
|
||||||
|
This example assumes that the device can keep a previous working system while preparing a new one. Its boot control can select either system and read saved update state. An A/B layout, with two system slots, is one way to meet these conditions.
|
||||||
|
|
||||||
|
The device can check its essential functions locally. Installation, startup checks, and recovery must not require a continuous server connection.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Start a new system as a trial. Keep the previous working system available until the new system passes its required checks.
|
||||||
|
|
||||||
|
Boot control records each trial attempt before handing control to the new system. Allow a limited number of unconfirmed attempts. The platform design must set this limit and explain how an interrupted state write is handled.
|
||||||
|
|
||||||
|
Once the new system starts, local health checks test its essential functions within a defined time limit. Linux starting alone is not enough. If the checks pass, save confirmation and make the new system the normal boot choice.
|
||||||
|
|
||||||
|
If the checks fail, request a return to the previous system. If the new system never reaches the checks, boot control must select the previous system after the trial attempts are used up.
|
||||||
|
|
||||||
|
A hang must lead to a reset through a watchdog or another defined mechanism. A watchdog is a timer that can reset the device when software stops showing that it is alive. Its coverage must include the startup stages where the design requires automatic recovery.
|
||||||
|
|
||||||
|
After returning to the previous system, check its essential functions too. Save the outcome and report it when a connection is available. If the previous system also fails, follow a separately defined recovery policy rather than endlessly switching between versions.
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
- The device must recover even when the new system cannot start the OTA manager.
|
||||||
|
- A successful installation does not prove that the system works.
|
||||||
|
- Trial attempts must end within a defined limit.
|
||||||
|
- Local recovery must remain possible while the device is offline.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Select the New System Permanently After Installation
|
||||||
|
|
||||||
|
This needs less boot state, but installation checks cannot detect all startup and application failures. It does not provide the required automatic return to a working system.
|
||||||
|
|
||||||
|
### Let the OTA Manager Handle Every Failure
|
||||||
|
|
||||||
|
The manager can handle failures after it starts. It cannot handle a failure that prevents its own startup.
|
||||||
|
|
||||||
|
### Wait for Server Confirmation
|
||||||
|
|
||||||
|
Server approval may be useful for some products. It is not used here because these devices can check their essential functions locally and must recover without a server connection.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Early boot failures can trigger recovery without the OTA manager.
|
||||||
|
- The device keeps a previous system until the new one passes its checks.
|
||||||
|
- Offline devices can finish the trial and report later.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- Boot control, health checks, and reset behavior must work together.
|
||||||
|
- Saved attempt and confirmation state must survive power loss.
|
||||||
|
- Storage is needed for the previous system.
|
||||||
|
- Poor checks can accept a broken system or reject a working one.
|
||||||
|
- The previous system may fail if the new version changed shared data.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach when the platform supports selection of a retained system, reliable boot state, and a reset path for the failures being covered.
|
||||||
|
|
||||||
|
Retaining an old image does not guarantee recovery from damaged storage, failure of shared boot components, or incompatible data changes. Those cases need their own recovery design.
|
||||||
|
|
||||||
|
The platform must define the attempt limit, check time limit, confirmation owner, watchdog coverage, and behavior when recovery also fails. The exact storage format and bootloader commands belong in the platform example.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 04: Starting an Updated System or Service](../docs/04-starting-an-updated-system-or-service.md)
|
||||||
|
- [ADR-004: Full-System Image Updates](ADR-004-full-system-image-updates.md)
|
||||||
|
- [ADR-015: Service Checks and Package Downgrade](ADR-015-service-checks-and-package-downgrade.md)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# ADR-015: Service Checks and Package Downgrade
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
An independently updated service can fail after its new package is installed. It may crash, fail to become ready, or keep running without doing its job. Restarting it repeatedly may leave the device without an essential function.
|
||||||
|
|
||||||
|
This example uses a stable system platform and a service that can be updated independently. A separate supervisor can control installation and check the service. The supervisor remains able to run when the updated service fails.
|
||||||
|
|
||||||
|
Before an update starts, the device has the exact previous package available locally, with the information needed to verify it. The package manager supports installing that older version. Dependencies, installation scripts, configuration, and service data have been checked for downgrade compatibility.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
After installing the new package, start the service and check its required functions within a defined time limit. Keep the new version only if these checks pass.
|
||||||
|
|
||||||
|
If the service fails, save a block against another automatic installation of that failed version. Then stop the service, explicitly reinstall the previous package, start it, and check it again.
|
||||||
|
|
||||||
|
This explicit request to install an older version is a **forced downgrade**. It overrides the normal preference for a newer version. It does not skip verification or compatibility checks.
|
||||||
|
|
||||||
|
Save recovery progress so the supervisor can decide what to do after a restart. The supervisor must handle an interrupted installation or downgrade according to the package manager's recovery rules. Automatic updates must not start a competing installation while recovery is in progress.
|
||||||
|
|
||||||
|
If the previous service passes its checks, record that recovery succeeded. If installation, downgrade, or the checks fail, record the failed recovery and follow the device's recovery policy. Do not treat a completed package command as proof that the service works.
|
||||||
|
|
||||||
|
### Update Lock
|
||||||
|
|
||||||
|
Here, a **lock** means a saved rule that prevents another automatic attempt at the failed package version. It is separate from a temporary package-manager lock used during installation.
|
||||||
|
|
||||||
|
The rule applies to the failed version of this package on this device. It survives a restart and is checked on every automatic update path. Discovering the same release again does not remove it. It does not block unrelated packages or all future versions of this package.
|
||||||
|
|
||||||
|
A different version may be tried after the usual checks. An authorized operator may remove the rule to retry the same version after reviewing the failure. Removal must be recorded. A server may use device reports to pause a wider deployment, but that is a separate deployment decision.
|
||||||
|
|
||||||
|
### Reporting
|
||||||
|
|
||||||
|
Save the failed package version, the reason for failure, the recovery result, the version now running, and the active update lock. Send the report when a connection is available.
|
||||||
|
|
||||||
|
If the failed service provides network access, local recovery still runs. Reporting may have to wait until communication is restored.
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
- A package installation can succeed while the updated service fails.
|
||||||
|
- The device needs a local way to restore an essential service.
|
||||||
|
- The same failed version must not be installed again automatically.
|
||||||
|
- Operators need to distinguish update failure from recovery failure.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Keep Restarting the New Service
|
||||||
|
|
||||||
|
Restarts can help with a temporary fault. They do not fix a repeatable error in the new version. This design limits startup checks and restores the previous version when they fail.
|
||||||
|
|
||||||
|
### Restore the Package Without Blocking the Failed Version
|
||||||
|
|
||||||
|
The next automatic check could offer the same version and repeat the failure. A saved block prevents that cycle.
|
||||||
|
|
||||||
|
### Hold the Package at the Previous Version
|
||||||
|
|
||||||
|
Blocking all later versions until review can fit products with stricter approval rules. This example blocks only the failed version so a later fix can be considered through the normal update process.
|
||||||
|
|
||||||
|
### Restore the Entire System
|
||||||
|
|
||||||
|
A system rollback may fit tightly coupled software. It is not selected here because this service is an independent update unit on a stable platform.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- The device can restore the service without replacing the whole system.
|
||||||
|
- Recovery can run without a server connection.
|
||||||
|
- A restart does not remove the failure block.
|
||||||
|
- Reports show whether the previous service was actually restored.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- The previous package and its verification information need local storage.
|
||||||
|
- The supervisor, checks, saved state, and package manager must work together.
|
||||||
|
- Downgrade paths need testing, including scripts and data changes.
|
||||||
|
- Locks need explicit removal rules and operator visibility.
|
||||||
|
- Service downtime includes the failed attempt and recovery.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach for independently updated services when the platform and package manager support a tested downgrade path.
|
||||||
|
|
||||||
|
It does not make package replacement atomic or undo every action of an installation script. Restoring software does not automatically restore data. If the old version cannot use the resulting data, this recovery path is insufficient.
|
||||||
|
|
||||||
|
The product must define the service checks, their time limit, the supervisor's dependencies, and what happens if recovery fails. Package retention and downgrade support must be verified for the selected tools.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 04: Starting an Updated System or Service](../docs/04-starting-an-updated-system-or-service.md)
|
||||||
|
- [ADR-005: Package-Based Updates](ADR-005-package-based-updates.md)
|
||||||
|
- [ADR-014: Trial Boot and Confirmation for System Updates](ADR-014-trial-boot-and-confirmation.md)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# ADR-016: Separate System Software and Persistent Device Data
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
This example uses system images and an A/B layout. The device keeps the current system while preparing a new version in the other slot.
|
||||||
|
|
||||||
|
An image installer can replace all contents of its target area. Device identity, calibration, settings, and application data must survive that operation. Both retained system versions may need these files.
|
||||||
|
|
||||||
|
The device has enough storage for two system versions and a separate persistent data area. The product can define which data formats the retained versions support.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Store system software in the system slots. Store device identity, calibration, device settings, and application data outside the areas replaced by ordinary system image updates.
|
||||||
|
|
||||||
|
Keep the running system files read-only in this example. Programs write changing files to the persistent data area. The platform defines the locations and access rules for those files.
|
||||||
|
|
||||||
|
An image may include default configuration. Device settings are stored separately, with a defined rule for how the application combines them with the defaults. A normal system release must not overwrite device identity or calibration.
|
||||||
|
|
||||||
|
Share persistent data between the slots only when the supported recovery versions can use it. Before changing a data format, define and test compatibility or a restore path. If recovery uses a saved copy, define what happens to data written after that copy was made.
|
||||||
|
|
||||||
|
The product must state when returning to an older system stops being supported. Confirmation alone does not prove that later data changes remain compatible with that system.
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
- Installing a system image must not erase information that belongs to the device.
|
||||||
|
- Returning to the previous system must preserve a usable data path.
|
||||||
|
- The boundary between release files and changing device files must be clear.
|
||||||
|
- Normal application writes should not change the system software under trial.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Keep Device Data Inside Each System Image
|
||||||
|
|
||||||
|
This is a simple layout, but replacing an image would erase the data unless the installer copies it elsewhere first. The copying and restore steps would need their own failure handling.
|
||||||
|
|
||||||
|
### Keep a Separate Data Copy for Each Slot
|
||||||
|
|
||||||
|
This can help isolate data changes during a trial. It needs more storage and rules for copying data, choosing the active copy, and handling new records after a return to the old slot. It may be appropriate when shared data cannot support both versions.
|
||||||
|
|
||||||
|
### Keep One Writable System With All Data
|
||||||
|
|
||||||
|
This can suit package-based updates. It does not meet this example's requirement to prepare and replace a complete system image while retaining the current system.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- System image replacement preserves device-specific files.
|
||||||
|
- New application records can remain available after a return to the previous system.
|
||||||
|
- The areas that the image installer may change are easier to define.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- The platform must arrange paths and access to persistent files.
|
||||||
|
- Shared data needs compatibility rules for retained software versions.
|
||||||
|
- A shared data failure can affect both slots.
|
||||||
|
- Copies or backups used for data recovery need extra storage and cleanup rules.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach when system software is replaced as an image and device data must survive that replacement.
|
||||||
|
|
||||||
|
Separation may use partitions or other storage areas outside the installer's target. A directory inside a partition being overwritten is not sufficient.
|
||||||
|
|
||||||
|
This choice does not protect against failure of the whole storage device. It also does not make data writes safe under power loss or make migrations reversible. Those need separate mechanisms and tests.
|
||||||
|
|
||||||
|
Key storage and access protection need a security design. Placing identity files outside a system slot is only a rule about their lifetime.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 05: Storage Layout](../docs/05-storage-layout.md)
|
||||||
|
- [ADR-004: Full-System Image Updates](ADR-004-full-system-image-updates.md)
|
||||||
|
- [ADR-014: Trial Boot and Confirmation for System Updates](ADR-014-trial-boot-and-confirmation.md)
|
||||||
|
- [ADR-017: Keep Recovery State Outside Replaceable Software](ADR-017-keep-recovery-state-outside-replaceable-software.md)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# ADR-017: Keep Recovery State Outside Replaceable Software
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Trial boot and service downgrade both depend on saved state. After a restart, the device needs to know which version is under trial, whether it was confirmed, and whether recovery is in progress.
|
||||||
|
|
||||||
|
If that state exists only inside the software being replaced, installation or recovery may erase it. If boot control cannot read it before Linux starts, it cannot use it to handle an early startup failure.
|
||||||
|
|
||||||
|
This example assumes that the platform provides storage accessible to boot control and persistent storage accessible to the OTA manager or service supervisor. These may be different areas.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Keep state required for recovery outside the software areas that the relevant update or recovery operation replaces.
|
||||||
|
|
||||||
|
Keep boot selection, trial attempts, and confirmation in a location and format supported by boot control. The Linux component that requests a trial or confirms the system must use the platform's defined boot control interface.
|
||||||
|
|
||||||
|
Keep installation progress, service recovery progress, failed version blocks, and pending reports in persistent storage accessible to the OTA manager or supervisor. Package installation, removal scripts, and cleanup must preserve these records while they are needed.
|
||||||
|
|
||||||
|
Do not require one common file for every reader. Define who writes each record and how the readers agree on its meaning. Both retained software versions must understand any state they need after recovery.
|
||||||
|
|
||||||
|
Use a storage method that handles interrupted writes. Readers must detect invalid or incomplete records. The platform must define a recovery action when no valid record remains; unreadable state must not be interpreted as successful confirmation.
|
||||||
|
|
||||||
|
Define when each record may be removed. A report may be removed after its delivery is acknowledged. A failed version block follows the explicit removal policy in ADR-015. Recovery progress must remain available until the recorded operation has reached a known outcome.
|
||||||
|
|
||||||
|
## Decision Drivers
|
||||||
|
|
||||||
|
- Restarting or replacing software must not erase the information needed to recover.
|
||||||
|
- Boot control must make decisions before the OTA manager starts.
|
||||||
|
- Offline recovery must retain progress and results locally.
|
||||||
|
- Interrupted state writes must have a defined outcome.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Store State Only Inside the Target System Slot
|
||||||
|
|
||||||
|
Replacing that slot may erase the state. Boot control may also be unable to read it when the new system fails. This does not meet the recovery requirements.
|
||||||
|
|
||||||
|
### Store All State in the Application's Data Files
|
||||||
|
|
||||||
|
This may be convenient for reporting. It is unsuitable when boot control cannot read those files or when package operations can remove them. Separate records are used according to their readers and lifetime.
|
||||||
|
|
||||||
|
### Keep the Authoritative State Only on the Server
|
||||||
|
|
||||||
|
The server can track deployment progress, but it may be unreachable during boot or recovery. The device needs local state to complete those steps while offline.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Boot and service recovery retain the information they need across replacement and restart.
|
||||||
|
- Each reader has state it can access at the required time.
|
||||||
|
- Reports can wait locally for the server connection to return.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- The platform needs tested methods for saving and reading state under power loss.
|
||||||
|
- Boot control and Linux must agree on state formats and ownership.
|
||||||
|
- Multiple records may need coordination if an operation is interrupted between writes.
|
||||||
|
- Persistent storage needs space limits and retention rules.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach when recovery decisions depend on state that must survive software replacement or an interrupted operation.
|
||||||
|
|
||||||
|
This ADR defines where state belongs and who must read it. It does not select a filesystem, record format, or bootloader command. Those choices belong in the platform example and must be tested under power loss.
|
||||||
|
|
||||||
|
Separating state from software does not guarantee that the state is valid. It also does not make several writes one atomic operation. The update design must handle the intermediate states those writes can leave behind.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 05: Storage Layout](../docs/05-storage-layout.md)
|
||||||
|
- [ADR-014: Trial Boot and Confirmation for System Updates](ADR-014-trial-boot-and-confirmation.md)
|
||||||
|
- [ADR-015: Service Checks and Package Downgrade](ADR-015-service-checks-and-package-downgrade.md)
|
||||||
|
- [ADR-016: Separate System Software and Persistent Device Data](ADR-016-separate-system-software-and-persistent-device-data.md)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# ADR-018: Prepare and Verify Before Activation
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
This example uses two system slots. A is confirmed and running. B is inactive and may be replaced. There is no pending trial, and the installer has exclusive access to B.
|
||||||
|
|
||||||
|
Power may stop during installation. A and shared device data must remain usable. Boot control provides an interface for saving a trial request separately from normal installation progress.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Prepare B without changing the normal boot choice A. Save the target and release identity before writing. Complete the required storage operations and verify the installed contents against trusted release information before saving prepared state.
|
||||||
|
|
||||||
|
Request a trial through boot control only after the software and required preparation state have been saved. Boot control owns the trial target and attempts. Its storage method must have a defined result after interruption.
|
||||||
|
|
||||||
|
If an operation fails or its result is unknown, inspect saved state before retrying. After a restart, verify the installed release again before making a new activation request. Do not rewrite a slot already selected for a trial or running.
|
||||||
|
|
||||||
|
Keep confirmation separate. A prepared release can still fail startup checks. Preserve A and the data it needs during the supported recovery period.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Replace the Running Slot
|
||||||
|
|
||||||
|
This uses less storage, but an interrupted installation may damage the only runnable system. It requires another installation and recovery design.
|
||||||
|
|
||||||
|
### Select B Before Preparation Ends
|
||||||
|
|
||||||
|
This permits boot control to select incomplete software. It does not meet this example's recovery requirement.
|
||||||
|
|
||||||
|
### Treat Installation Success as Confirmation
|
||||||
|
|
||||||
|
Installation checks do not show that services start and work on the device. A trial remains necessary here.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- Interrupted preparation leaves A available.
|
||||||
|
- Activation has a clear precondition.
|
||||||
|
- Installation checks and startup checks have separate roles.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- Two system slots require space.
|
||||||
|
- Storage completion, verification, and state writes need explicit ordering.
|
||||||
|
- Shared data and shared boot components still need their own recovery rules.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach when a complete release can be prepared away from the running system. The platform must define write completion and boot-state recovery. This ADR does not select a filesystem or prove power-loss safety.
|
||||||
|
|
||||||
|
A package installed into the running filesystem does not gain this property without a separate preparation and activation mechanism.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 06: Atomic Updates](../docs/06-atomic-updates.md)
|
||||||
|
- [ADR-014: Trial Boot and Confirmation](ADR-014-trial-boot-and-confirmation.md)
|
||||||
|
- [ADR-019: Recover Interrupted Updates From Durable State](ADR-019-recover-interrupted-updates-from-durable-state.md)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# ADR-019: Recover Interrupted Updates From Durable State
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Boot control and the OTA manager save different records. A restart can happen after one record changes but before another does. For example, a saved trial request can exist while installation progress still says prepared.
|
||||||
|
|
||||||
|
This example must continue locally without a server connection. Both retained software versions must understand the records needed during recovery.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Assign one owner to each state transition. Boot control owns boot selection, trial attempts, and confirmation. The OTA manager owns installation progress and pending reports. Progress does not override boot state.
|
||||||
|
|
||||||
|
Save recovery information outside replaceable software as required by ADR-017. Define record validation, supported formats, and the action taken when a record is invalid. Use a storage method with a stated interruption contract and check write errors.
|
||||||
|
|
||||||
|
Before continuing an interrupted operation, read authoritative boot state and inspect the target release. Reconcile old progress with a saved boot request or outcome. Do not rewrite a pending or running target, reset trial attempts, or begin a fresh trial merely because a progress record is old.
|
||||||
|
|
||||||
|
For each operation, define whether to repeat it, verify its result, abandon it, or enter recovery. Bind progress to the release and target. A flag without this identity is not enough.
|
||||||
|
|
||||||
|
If no valid boot record remains, use the platform's separate recovery policy. Never infer confirmation from unreadable state. Retain results until reporting and recovery policies allow removal.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Restart Installation Without Reading Boot State
|
||||||
|
|
||||||
|
This may overwrite a selected target or reset the trial limit. It is unsuitable for the separate-record design used here.
|
||||||
|
|
||||||
|
### Put Every State Change in One Transaction
|
||||||
|
|
||||||
|
This can work if all readers and writers share suitable storage and transaction support. It is not assumed here because boot control and Linux may use different storage interfaces.
|
||||||
|
|
||||||
|
### Ask the Server What to Do After Every Restart
|
||||||
|
|
||||||
|
The server can help with deployment policy, but connectivity may be absent during boot and recovery. Local decisions must remain possible.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- A gap between state writes has a defined recovery path.
|
||||||
|
- Old progress does not undo a saved boot decision.
|
||||||
|
- Interrupted operations can be handled offline.
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- Record compatibility and ownership must be maintained across releases.
|
||||||
|
- Recovery needs more than a single progress flag.
|
||||||
|
- Storage behavior and interruption handling need platform tests.
|
||||||
|
|
||||||
|
## Applicability and Limits
|
||||||
|
|
||||||
|
Use this approach when an update spans several persistent records or operations. It does not make those writes one atomic transaction.
|
||||||
|
|
||||||
|
Checksums, redundant records, and file replacement are possible parts of a storage design. None is a complete power-loss solution without defined storage assumptions and tests. Package scripts and data migrations need their own repeat and recovery rules.
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
|
||||||
|
- [Chapter 06: Atomic Updates](../docs/06-atomic-updates.md)
|
||||||
|
- [ADR-017: Keep Recovery State Outside Replaceable Software](ADR-017-keep-recovery-state-outside-replaceable-software.md)
|
||||||
|
- [ADR-018: Prepare and Verify Before Activation](ADR-018-prepare-and-verify-before-activation.md)
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
# ADR-NNNN: Decision Title
|
# ADR-NNNN: Decision Title
|
||||||
|
|
||||||
* **Status:** Proposed
|
|
||||||
* **Date:** YYYY-MM-DD
|
|
||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
Describe the problem or architectural question that requires a decision.
|
Describe the problem or architectural question that requires a decision.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@startuml
|
||||||
|
title A/B installation and restart decisions
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam monochrome true
|
||||||
|
state "A confirmed\nB inactive; no pending trial" as Idle
|
||||||
|
state "Intent saved\nWriting B" as Writing
|
||||||
|
state "B fully written\nNot yet verified" as Written
|
||||||
|
state "B verified\nPrepared state saved" as Prepared
|
||||||
|
state "Trial request saved\nOTA progress may still be old" as Requested
|
||||||
|
state "Inspect saved boot state\nand target release" as Inspect
|
||||||
|
state "Separate recovery policy" as Recovery
|
||||||
|
state "Trial boot lifecycle\nSee existing trial boot diagram" as Trial
|
||||||
|
|
||||||
|
[*] --> Idle
|
||||||
|
Idle --> Writing : save release and target
|
||||||
|
Writing --> Written : complete storage writes
|
||||||
|
Written --> Prepared : verify expected contents; save prepared state
|
||||||
|
Prepared --> Requested : boot control saves trial request
|
||||||
|
Requested --> Trial : restart; consume attempt through boot control
|
||||||
|
Writing --> Inspect : restart
|
||||||
|
Written --> Inspect : restart
|
||||||
|
Prepared --> Inspect : restart
|
||||||
|
Requested --> Inspect : restart before entering trial lifecycle
|
||||||
|
Inspect --> Writing : A selected; B incomplete\nrestart preparation under policy
|
||||||
|
Inspect --> Prepared : A selected; B complete\nverify again; save prepared state
|
||||||
|
Inspect --> Requested : saved B trial found\nrepair progress; preserve attempt count
|
||||||
|
Inspect --> Recovery : no valid boot state
|
||||||
|
Written --> Idle : verification fails; abandon B
|
||||||
|
Recovery --> [*]
|
||||||
|
Trial --> [*]
|
||||||
|
|
||||||
|
note right of Inspect
|
||||||
|
Read boot state before writing B.
|
||||||
|
Never infer confirmation from progress.
|
||||||
|
A saved boot request wins over old progress.
|
||||||
|
end note
|
||||||
|
legend bottom
|
||||||
|
One A-to-B installation. No shared data migration or shared boot update.
|
||||||
|
Writes assume the platform's durability and record validation contract.
|
||||||
|
Trial outcomes and later updates belong to the trial boot lifecycle.
|
||||||
|
endlegend
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
@startuml
|
||||||
|
title A/B preparation and activation
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam monochrome true
|
||||||
|
participant "OTA manager" as Manager
|
||||||
|
participant Installer
|
||||||
|
database "Slot B" as B
|
||||||
|
database "OTA progress" as Progress
|
||||||
|
participant "Boot control" as Boot
|
||||||
|
|
||||||
|
note over Manager, Boot
|
||||||
|
A is confirmed. B is inactive. No earlier trial is pending.
|
||||||
|
One installer owns B. A and shared data are preserved.
|
||||||
|
end note
|
||||||
|
Manager -> Progress : Save intent: release and target B
|
||||||
|
Manager -> Installer : Prepare B
|
||||||
|
Installer -> B : Write release
|
||||||
|
Installer -> B : Complete required storage operations
|
||||||
|
Installer -> B : Read and verify installed contents
|
||||||
|
Installer --> Manager : Verified expected release
|
||||||
|
note over Manager, Boot
|
||||||
|
Reset before activation: boot A.
|
||||||
|
Inspect B and verify again before a new activation request.
|
||||||
|
end note
|
||||||
|
Manager -> Progress : Save prepared release B
|
||||||
|
Manager -> Boot : Save trial request for B
|
||||||
|
Boot -> Boot : Persist target and attempt limit
|
||||||
|
note right of Boot
|
||||||
|
Interrupted state write:
|
||||||
|
read valid old or new state,
|
||||||
|
or enter defined recovery.
|
||||||
|
end note
|
||||||
|
Boot --> Manager : Trial request saved
|
||||||
|
note over Manager, Boot
|
||||||
|
Reset here: boot control may start B.
|
||||||
|
Old OTA progress must not cause a rewrite or fresh attempts.
|
||||||
|
end note
|
||||||
|
Manager -> Progress : Save trial-requested result
|
||||||
|
Manager -> Boot : Restart into trial
|
||||||
|
ref over Manager, Boot
|
||||||
|
Existing A/B trial boot, checks, and confirmation diagram
|
||||||
|
end ref
|
||||||
|
legend bottom
|
||||||
|
Success path only. Any error stops forward progress and requires state inspection.
|
||||||
|
Arrows assume completed durable operations under the platform storage contract.
|
||||||
|
A saved trial request is not confirmation.
|
||||||
|
endlegend
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
@startuml
|
||||||
|
title A/B storage: logical areas and access responsibilities
|
||||||
|
left to right direction
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam monochrome true
|
||||||
|
|
||||||
|
actor "Installer" as installer
|
||||||
|
actor "Boot control" as boot
|
||||||
|
actor "OTA manager /\nconfirmation program" as manager
|
||||||
|
actor "Running system\nand applications" as runtime
|
||||||
|
|
||||||
|
rectangle "Device storage (logical areas, not a partition table)" {
|
||||||
|
rectangle "Shared boot components\nSeparate update and recovery method" as shared
|
||||||
|
database "Boot state\nNormal slot, trial target, attempts, confirmation" as state
|
||||||
|
rectangle "Slot A: retained system\nKernel + device tree + root filesystem" as A
|
||||||
|
rectangle "Slot B: update target\nKernel + device tree + root filesystem" as B
|
||||||
|
database "Persistent device data" as data {
|
||||||
|
rectangle "Identity and calibration\nRestricted access" as identity
|
||||||
|
rectangle "Configuration and application data\nMust support the retained version" as appdata
|
||||||
|
rectangle "OTA progress, failure blocks\nand pending reports" as progress
|
||||||
|
}
|
||||||
|
rectangle "Download and work area\nDisposable preparation files" as work
|
||||||
|
}
|
||||||
|
|
||||||
|
installer --> B : replace inactive slot
|
||||||
|
installer --> work : use temporary files
|
||||||
|
boot --> shared : execute
|
||||||
|
boot --> state : read / save before Linux starts
|
||||||
|
boot --> A : select matching components
|
||||||
|
boot --> B : select matching components
|
||||||
|
manager --> state : request trial / confirm\nthrough platform interface
|
||||||
|
manager --> progress : read / save
|
||||||
|
runtime --> appdata : read / write
|
||||||
|
runtime --> identity : authorized access only
|
||||||
|
|
||||||
|
legend bottom
|
||||||
|
A is running while B is prepared. Preserve A, boot state and device data.
|
||||||
|
Persistent device data survives replacement of either system slot.
|
||||||
|
Shared data is not rolled back by selecting A.
|
||||||
|
Both system versions must understand the recovery records they read.
|
||||||
|
Separate logical areas do not guarantee safe writes or whole-device recovery.
|
||||||
|
endlegend
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
@startuml
|
||||||
|
title A/B trial boot and recovery
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam monochrome true
|
||||||
|
|
||||||
|
state "A confirmed" as A
|
||||||
|
state "Prepare B\nA remains the normal boot choice" as Preparing
|
||||||
|
state "B prepared and verified" as Prepared
|
||||||
|
state "B trial pending\nSaved target and attempt limit" as Pending
|
||||||
|
state "B trial running\nUnconfirmed" as Trial
|
||||||
|
state "B confirmed" as Confirmed
|
||||||
|
state "Recovery to A pending" as Recovering
|
||||||
|
state "A recovery running" as Recovery
|
||||||
|
state "A recovery checked" as Recovered
|
||||||
|
state "Separate recovery required" as Failed
|
||||||
|
|
||||||
|
[*] --> A
|
||||||
|
A --> Preparing : begin installation
|
||||||
|
Preparing --> Preparing : reset during preparation; boot A\nB is not eligible for trial
|
||||||
|
Preparing --> A : abandon incomplete or failed preparation
|
||||||
|
Preparing --> Prepared : installation and verification complete
|
||||||
|
Prepared --> Pending : save trial request through boot control
|
||||||
|
Pending --> Trial : reset [attempts left > 0]; save attempts left - 1\nbefore handing control to B
|
||||||
|
Trial --> Confirmed : local checks pass within time limit;\nsave confirmation
|
||||||
|
Trial --> Recovering : local checks fail; save recovery request
|
||||||
|
Trial --> Pending : reset before confirmation\n(power loss or covered watchdog)
|
||||||
|
Pending --> Recovering : reset; no attempts left
|
||||||
|
Recovering --> Recovery : boot control selects A on reset
|
||||||
|
Recovery --> Recovered : local recovery checks pass;\nsave outcome
|
||||||
|
Recovery --> Failed : recovery checks fail or startup fails\n(platform recovery policy)
|
||||||
|
Confirmed --> [*]
|
||||||
|
Recovered --> [*]
|
||||||
|
Failed --> [*]
|
||||||
|
|
||||||
|
note right of Pending
|
||||||
|
Boot state is outside A and B.
|
||||||
|
Attempts are consumed before starting B.
|
||||||
|
Invalid records require a platform-defined
|
||||||
|
recovery path, never implicit confirmation.
|
||||||
|
end note
|
||||||
|
|
||||||
|
note right of Recovery
|
||||||
|
A must still be compatible with shared data.
|
||||||
|
Do not endlessly alternate failed systems.
|
||||||
|
end note
|
||||||
|
|
||||||
|
legend bottom
|
||||||
|
Preparation interruption assumes the installer preserves A and boot state.
|
||||||
|
State transitions assume valid durable records; torn writes need platform testing.
|
||||||
|
Checks and recovery are local; reports wait for connectivity.
|
||||||
|
endlegend
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@startuml
|
||||||
|
title OTA roles and device boundary
|
||||||
|
left to right direction
|
||||||
|
skinparam shadowing false
|
||||||
|
skinparam monochrome true
|
||||||
|
skinparam packageStyle rectangle
|
||||||
|
|
||||||
|
package "Release and deployment services" {
|
||||||
|
component "Build, test and publish" as release
|
||||||
|
component "Artifact storage" as artifacts
|
||||||
|
component "Deployment management" as deployment
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Device" {
|
||||||
|
component "OTA manager" as manager
|
||||||
|
component "Installer" as installer
|
||||||
|
component "Boot control" as boot
|
||||||
|
component "Local health checks" as checks
|
||||||
|
database "Persistent update progress\nand pending reports" as progress
|
||||||
|
}
|
||||||
|
|
||||||
|
release --> artifacts : publish artifact and\nverification information
|
||||||
|
release --> deployment : register release and compatibility
|
||||||
|
manager --> deployment
|
||||||
|
manager --> installer : authorize preparation
|
||||||
|
installer --> artifacts : download and verify
|
||||||
|
manager --> boot : request trial through\nplatform interface
|
||||||
|
checks --> boot : confirm after local checks pass\nor request recovery after failure
|
||||||
|
manager --> progress : save / resume progress;\nqueue reports while offline
|
||||||
|
manager --> checks : arrange checks after activation
|
||||||
|
|
||||||
|
legend bottom
|
||||||
|
OTA manager -> deployment management: request releases; report running version and outcome.
|
||||||
|
Boot control runs before the OTA manager and handles exhausted trial attempts.
|
||||||
|
Hangs need a covered reset path. Checks and recovery do not require the server.
|
||||||
|
Roles may be combined in one tool. This view uses a system update with trial boot.
|
||||||
|
endlegend
|
||||||
|
@enduml
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# OTA diagrams
|
||||||
|
|
||||||
|
PlantUML sources accompany chapters 03–06. The diagrams describe the example A/B design, not a required platform or partition table.
|
||||||
|
|
||||||
|
| View | Source | Preview |
|
||||||
|
|---|---|---|
|
||||||
|
| Roles and the server/device boundary | [PlantUML](plantuml/ota-architecture.puml) | [PNG](rendered/ota-architecture.png) |
|
||||||
|
| Trial boot, confirmation and recovery | [PlantUML](plantuml/ab-trial-boot.puml) | [PNG](rendered/ab-trial-boot.png) |
|
||||||
|
| Storage areas and access responsibilities | [PlantUML](plantuml/ab-storage-layout.puml) | [PNG](rendered/ab-storage-layout.png) |
|
||||||
|
| Preparation and activation sequence | [PlantUML](plantuml/ab-installation-sequence.puml) | Source only |
|
||||||
|
| Installation states and restart decisions | [PlantUML](plantuml/ab-installation-recovery.puml) | Source only |
|
||||||
|
|
||||||
|
From the repository root, regenerate previews with a local PlantUML installation and Graphviz:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
JAVA_TOOL_OPTIONS=-Djava.awt.headless=true plantuml -tpng -o ../rendered diagrams/plantuml/*.puml
|
||||||
|
```
|
||||||
|
|
||||||
|
Chapters 03–05 include existing previews. The chapter 06 diagrams are provided as sources only; generate previews locally when needed. No external rendering service is needed. These diagrams do not establish power-loss safety. The [A/B simulation](../examples/simulations/ab-trial-boot/readme.md) covers trial boot. The [interrupted installation model](../examples/simulations/interrupted-installation/readme.md) covers preparation, old progress, and supplied boot-record write outcomes.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -71,36 +71,54 @@ The emphasis is always on understanding the underlying design rather than memori
|
|||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
|
The structure below includes the current documentation, PlantUML diagrams, and A/B simulation. Hardware demonstrations and later chapters will be added as the practical material develops.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ota-reference-design/
|
ota-reference-design/
|
||||||
│
|
│
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── 00-series-overview.md
|
│ ├── 00-series-overview.md
|
||||||
│ ├── 01-ota-introduction.md
|
│ ├── 01-what-is-an-ota-update.md
|
||||||
│ ├── 02-update-strategies.md
|
│ ├── 02-update_strategies.md
|
||||||
|
│ ├── 03-system-architecture.md
|
||||||
|
│ ├── 04-starting-an-updated-system-or-service.md
|
||||||
|
│ ├── 05-storage-layout.md
|
||||||
|
│ ├── 06-atomic-updates.md
|
||||||
│ ├── ...
|
│ ├── ...
|
||||||
│
|
│
|
||||||
├── diagrams/
|
├── diagrams/
|
||||||
│ └── plantuml/
|
│ ├── plantuml/
|
||||||
|
│ └── rendered/
|
||||||
│
|
│
|
||||||
├── examples/
|
├── examples/
|
||||||
│ ├── raspberry-pi/
|
|
||||||
│ ├── qemu/
|
|
||||||
│ └── simulations/
|
│ └── simulations/
|
||||||
|
│ ├── ab-trial-boot/
|
||||||
|
│ └── interrupted-installation/
|
||||||
│
|
│
|
||||||
├── adr/
|
├── adr/
|
||||||
│ ├── ADR-001-repository-name.md
|
│ ├── ADR-001-repository-name.md
|
||||||
│ ├── ADR-002-license.md
|
│ ├── ADR-002-license.md
|
||||||
│ └── ADR-003-reference-design.md
|
│ └── ADR-003-reference-design.md
|
||||||
│
|
│
|
||||||
└── README.md
|
└── readme.md
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Learning Path
|
## Learning Path
|
||||||
|
|
||||||
The chapters are designed to build on each other.
|
The chapters are designed to build on each other. The current chapters are:
|
||||||
|
|
||||||
|
- [01. What Is an OTA Update?](01-what-is-an-ota-update.md)
|
||||||
|
- [02. Choosing an OTA Update Strategy](02-update_strategies.md)
|
||||||
|
- [03. OTA System Architecture](03-system-architecture.md)
|
||||||
|
- [04. Starting an Updated System or Service](04-starting-an-updated-system-or-service.md)
|
||||||
|
- [05. Storage Layout](05-storage-layout.md)
|
||||||
|
- [06. Atomic Updates](06-atomic-updates.md)
|
||||||
|
|
||||||
|
To catch up on additions and revisions, see the [changelog](../CHANGELOG.md), with dated summaries and links to chapters, diagrams, simulations, and related history.
|
||||||
|
|
||||||
|
The learning path below also includes planned chapters.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Introduction
|
Introduction
|
||||||
@@ -112,7 +130,7 @@ Update Strategies
|
|||||||
System Architecture
|
System Architecture
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Boot Process
|
System and Service Startup
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Storage Layout
|
Storage Layout
|
||||||
@@ -183,6 +201,8 @@ Where possible, theoretical discussions are accompanied by practical material, i
|
|||||||
|
|
||||||
The objective is to connect high-level architecture with practical implementation.
|
The objective is to connect high-level architecture with practical implementation.
|
||||||
|
|
||||||
|
Available now: [PlantUML diagrams](../diagrams/readme.md), a [C++ A/B trial boot simulation](../examples/simulations/ab-trial-boot/readme.md), and a [C++ interrupted installation model](../examples/simulations/interrupted-installation/readme.md). Chapters 03–05 have PNG previews; chapter 06 currently has sources only. Raspberry Pi and QEMU demonstrations are planned.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Intended Audience
|
## Intended Audience
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ Alongside the update files, it needs a few details: which version this is, which
|
|||||||
|
|
||||||
These are roles, not a shopping list of separate services. One tool may cover several of them. What matters is knowing who handles each step.
|
These are roles, not a shopping list of separate services. One tool may cover several of them. What matters is knowing who handles each step.
|
||||||
|
|
||||||
|
The following view shows the roles and the server/device boundary for the system-update example used in the next chapter.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[PlantUML source](../diagrams/plantuml/ota-architecture.puml)
|
||||||
|
|
||||||
## 3. One Update, Start to Finish
|
## 3. One Update, Start to Finish
|
||||||
|
|
||||||
Imagine a device running version 1. Version 2 is ready. In this example, the device can prepare the new system while keeping the previous one available for recovery.
|
Imagine a device running version 1. Version 2 is ready. In this example, the device can prepare the new system while keeping the previous one available for recovery.
|
||||||
@@ -62,4 +68,4 @@ If the update itself is interrupted, the device needs enough saved information t
|
|||||||
|
|
||||||
[ADR-011: Device-Initiated Update Checks](../adr/ADR-011-device-initiated-update-checks.md) explains how a device checks for updates at power-on, on a schedule, or at the operator’s request.
|
[ADR-011: Device-Initiated Update Checks](../adr/ADR-011-device-initiated-update-checks.md) explains how a device checks for updates at power-on, on a schedule, or at the operator’s request.
|
||||||
|
|
||||||
Next, we will follow the boot process and see what happens before Linux and the OTA manager start.
|
Next, we will follow [the startup of an updated system or service](04-starting-an-updated-system-or-service.md). We will look at recovery before the OTA manager starts, and at restoring an older package when an updated service fails.
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# 04. Starting an Updated System or Service
|
||||||
|
|
||||||
|
The previous chapter described who prepares, installs, and checks an update. Now we will follow what happens when the new software starts.
|
||||||
|
|
||||||
|
Installing files is only part of the work. The new software must start and do its job before the device decides to keep it.
|
||||||
|
|
||||||
|
We will use two examples: a system update with two system slots, and a package update for one service. These are examples of possible designs. Each needs a recovery mechanism suited to its platform.
|
||||||
|
|
||||||
|
## 1. From Power-On to the Application
|
||||||
|
|
||||||
|
A Linux device starts in several steps. A simple view is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Power on or reset
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Early platform code
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Bootloader selects and loads a system
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Linux kernel starts
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Root filesystem becomes available
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Init starts services
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Main application starts
|
||||||
|
```
|
||||||
|
|
||||||
|
The exact steps depend on the hardware. Early platform code prepares enough hardware to continue loading software. The **bootloader** loads the operating system and passes control to it.
|
||||||
|
|
||||||
|
The **kernel** is the core of Linux. It manages hardware and provides the basic functions that programs use. The **root filesystem** contains the files and directories needed by the system. **Init** is the first program Linux starts; it arranges the startup of services. A **service** is a program that runs to provide a function, such as communication or diagnostics.
|
||||||
|
|
||||||
|
The OTA manager starts later in this chain. If Linux cannot reach that point, the manager cannot repair the device. Recovery for an early failure must therefore work before the manager starts.
|
||||||
|
|
||||||
|
## 2. Which Software Will Start?
|
||||||
|
|
||||||
|
A system update may change the kernel, the root filesystem, or both. Some platforms also use a **device tree**, a description of hardware that helps the kernel use the device.
|
||||||
|
|
||||||
|
These parts must work together. Selecting a new root filesystem while loading an incompatible kernel can prevent the system from working.
|
||||||
|
|
||||||
|
The design must say which parts belong to a system version and how they are selected together. Later chapters will describe where these parts are stored.
|
||||||
|
|
||||||
|
A package update has a different starting point. Linux is already running. The device replaces an application package and starts the updated service on the existing system platform. It may need a service restart rather than a device reboot.
|
||||||
|
|
||||||
|
## 3. Installed Does Not Mean Working
|
||||||
|
|
||||||
|
Consider four different events:
|
||||||
|
|
||||||
|
| Event | What we know |
|
||||||
|
|---|---|
|
||||||
|
| Installed | The installation step has finished. |
|
||||||
|
| Started | The new system or service has started. |
|
||||||
|
| Checked | The required checks have passed. |
|
||||||
|
| Confirmed | The device has decided to keep the new version. |
|
||||||
|
|
||||||
|
A process can start and then crash. It can also keep running while failing to answer requests or read a required sensor.
|
||||||
|
|
||||||
|
A **health check** tests whether the software can do the work the device needs. For example, it might check that the main service answers a local request and can read a required device.
|
||||||
|
|
||||||
|
The checks need a time limit. Some services need time to become ready, but the device must not wait forever. The design must define what to check, how long to wait, and what counts as failure.
|
||||||
|
|
||||||
|
In these examples, the checks can run locally. Losing the server connection alone does not make the new software unhealthy. Reports can wait until the connection returns.
|
||||||
|
|
||||||
|
## 4. Example: Starting a New System in an A/B Layout
|
||||||
|
|
||||||
|
An **A/B layout** provides two places, called slots, for system software. In this example, slot A contains the working version. The installer prepares the new version in slot B while A keeps running.
|
||||||
|
|
||||||
|
The new version first gets a **trial boot**: a chance to start and pass the checks before it becomes the normal choice.
|
||||||
|
|
||||||
|
```text
|
||||||
|
A is running
|
||||||
|
|
|
||||||
|
Install new system in B
|
||||||
|
|
|
||||||
|
Select B for a trial boot
|
||||||
|
|
|
||||||
|
Restart into B
|
||||||
|
|
|
||||||
|
Check essential functions
|
||||||
|
|
|
||||||
|
+-- Pass --> Confirm B
|
||||||
|
|
|
||||||
|
+-- Fail --> Return to A and check it
|
||||||
|
```
|
||||||
|
|
||||||
|
The boot control mechanism limits the number of unconfirmed attempts. If B never reaches the program that confirms it, a later boot must be able to select A again.
|
||||||
|
|
||||||
|
This requires more than a choice between two slots. If B hangs, something must cause another reset. A **watchdog** is a timer that can reset the device when software stops showing that it is alive. Its setup must cover the failures the design intends to recover from.
|
||||||
|
|
||||||
|
The boot attempt state must be saved so a reset cannot erase the fact that an attempt was made. The device also needs a clear record of whether B has been confirmed.
|
||||||
|
|
||||||
|
Returning to a previous version is called **rollback**. Keeping A available makes rollback possible, but does not prove that A will still work. Shared data changed by B may also affect A.
|
||||||
|
|
||||||
|
[ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) explains when to choose this policy and what the platform must support.
|
||||||
|
|
||||||
|
The state diagram separates an explicit health-check failure from a startup failure that prevents the checks from running. It also shows the checks after returning to A.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[PlantUML source](../diagrams/plantuml/ab-trial-boot.puml) · [Runnable simulation and failure scenarios](../examples/simulations/ab-trial-boot/readme.md)
|
||||||
|
|
||||||
|
## 5. Example: Starting an Updated Service
|
||||||
|
|
||||||
|
Now imagine a device with a stable system platform and an independently updated communication service. Version 1 works. Version 2 arrives as a package.
|
||||||
|
|
||||||
|
A separate update supervisor controls this change. It can be part of the OTA manager. It must stay able to run when the communication service fails.
|
||||||
|
|
||||||
|
Before installation, the device keeps the exact version 1 package available locally, with the information needed to verify it. This example assumes that both versions work on the current platform and that the service's data remains usable by version 1.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Service version 1 is running
|
||||||
|
|
|
||||||
|
Install version 2 and restart the service
|
||||||
|
|
|
||||||
|
Check that version 2 can do its job
|
||||||
|
|
|
||||||
|
+-- Pass --> Keep version 2 and report success
|
||||||
|
|
|
||||||
|
+-- Fail --> Block another automatic attempt at version 2
|
||||||
|
|
|
||||||
|
Stop the failed service
|
||||||
|
|
|
||||||
|
Reinstall version 1
|
||||||
|
|
|
||||||
|
Start and check version 1
|
||||||
|
|
|
||||||
|
Save and report the result
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinstalling an older package is called a **downgrade**. Here, a **forced downgrade** means that the supervisor explicitly requests the previous version, even though the normal update policy prefers newer versions. It still verifies the package and checks compatibility.
|
||||||
|
|
||||||
|
The package manager must support this operation. The supervisor must also account for installation scripts and dependencies. Keeping an old package does not by itself make downgrade safe.
|
||||||
|
|
||||||
|
After recovery, the supervisor applies a **lock** that prevents another automatic installation of the failed version. Here, lock means a saved update rule, not a temporary lock used while writing files. The failure block is saved before recovery starts, so a restart cannot immediately trigger the same failed update again.
|
||||||
|
|
||||||
|
For this example, the rule blocks version 2 of this package on this device. Other designs may hold the package at version 1 until an operator reviews the failure. The scope of the lock and the conditions for removing it must be explicit.
|
||||||
|
|
||||||
|
If version 1 also fails its checks, recovery has failed. The supervisor records that outcome and follows the device's recovery policy. It must not report success just because the downgrade command finished.
|
||||||
|
|
||||||
|
[ADR-005: Package-Based Updates](../adr/ADR-005-package-based-updates.md) describes the conditions under which independent packages fit a stable system platform.
|
||||||
|
|
||||||
|
[ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) describes the recovery policy used here, including the scope of the update lock and the conditions for removing it.
|
||||||
|
|
||||||
|
## 6. Who Handles Each Failure?
|
||||||
|
|
||||||
|
| Failure | Who handles it in these examples? |
|
||||||
|
|---|---|
|
||||||
|
| New system cannot reach Linux services | Boot control uses the saved trial state to select the previous system on a later boot. |
|
||||||
|
| New system hangs | A watchdog causes a reset; boot control then uses the trial state. |
|
||||||
|
| Linux starts, but an essential function fails | Health checks request system recovery. |
|
||||||
|
| Updated service crashes or fails its checks | The separate supervisor downgrades it and blocks the failed version. |
|
||||||
|
| Previous system or service also fails | The device follows its recovery policy and records that recovery failed. |
|
||||||
|
| Server connection is lost | Local checks and recovery continue; reporting waits. |
|
||||||
|
|
||||||
|
These mechanisms must be connected deliberately. Two system slots alone do not detect a hang. A service manager that restarts a crashed process does not, by itself, restore the previous package.
|
||||||
|
|
||||||
|
## 7. Remembering What Happened
|
||||||
|
|
||||||
|
Both examples need information that survives a restart. This is called **persistent state**.
|
||||||
|
|
||||||
|
The device needs enough information to answer questions such as:
|
||||||
|
|
||||||
|
- Which version was working before the update?
|
||||||
|
- Which version is being tried now?
|
||||||
|
- Has it passed the required checks?
|
||||||
|
- Has recovery started or finished?
|
||||||
|
- Is this version blocked from another automatic attempt?
|
||||||
|
- Which result still needs to be reported?
|
||||||
|
|
||||||
|
Saving this information is part of the design. Power can fail while it is being written. After a restart, the device must be able to read a valid state and decide what to do next.
|
||||||
|
|
||||||
|
A useful report includes the failed version, the reason for failure, the recovery result, the version now running, and any update lock. If the device is offline, it saves the report and sends it later.
|
||||||
|
|
||||||
|
## 8. Recovering Software and Data
|
||||||
|
|
||||||
|
Returning to old software does not automatically restore old data.
|
||||||
|
|
||||||
|
For example, version 2 may change a database into a format that version 1 cannot read. The old system slot or package can still be present, yet recovery can fail.
|
||||||
|
|
||||||
|
The update design must keep data compatible, provide a tested way to restore it, or define another recovery path. The same question applies to configuration and changes made by package installation scripts.
|
||||||
|
|
||||||
|
## 9. Related Architecture Decisions
|
||||||
|
|
||||||
|
These Architecture Decision Records (ADRs) explain when to choose the approaches used in this chapter:
|
||||||
|
|
||||||
|
| Question | Further reading |
|
||||||
|
|---|---|
|
||||||
|
| When should the software system be updated as one image? | [ADR-004: Full-System Image Updates](../adr/ADR-004-full-system-image-updates.md) |
|
||||||
|
| When do independent packages fit a stable system platform? | [ADR-005: Package-Based Updates](../adr/ADR-005-package-based-updates.md) |
|
||||||
|
| When should a new system get a trial boot before it is confirmed? | [ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) |
|
||||||
|
| When should a failed service update trigger downgrade and an update lock? | [ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) |
|
||||||
|
|
||||||
|
## 10. What Comes Next?
|
||||||
|
|
||||||
|
Both examples follow the same pattern: start the new version, check its required functions, then keep it or recover. They use different mechanisms because a system boot and a service restart fail in different ways.
|
||||||
|
|
||||||
|
Next, we will look at [storage layout](05-storage-layout.md): where software versions, device data, and update state live, and what must remain available when an update fails.
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
# 05. Storage Layout: Software Versions, Persistent Data, and Update State
|
||||||
|
|
||||||
|
The previous chapter followed the startup of a new system or service. It also showed why the device may need to return to an older version.
|
||||||
|
|
||||||
|
That return is only possible if the needed software, data, and update state are still available.
|
||||||
|
|
||||||
|
A **storage layout** describes where these things live. It also defines which parts an update may change. We will use the same two examples: an A/B system update and an independent service package update.
|
||||||
|
|
||||||
|
## 1. Different Files Have Different Jobs
|
||||||
|
|
||||||
|
Before choosing partitions or directories, separate the contents by purpose.
|
||||||
|
|
||||||
|
| Contents | Example | What an update must preserve |
|
||||||
|
|---|---|---|
|
||||||
|
| System software | Kernel, system libraries, services | A usable previous version during a trial |
|
||||||
|
| Device identity and calibration | Device keys, sensor calibration | The information that belongs to this device |
|
||||||
|
| Configuration | Network settings, application settings | Settings needed by the versions that may run |
|
||||||
|
| Application data | Measurements, a local database | Data needed for operation and recovery |
|
||||||
|
| Update state | Trial target, confirmation, failure block | Enough information to choose the next safe action |
|
||||||
|
| Recovery files | Previous service package and verification information | The files needed to restore the previous service |
|
||||||
|
| Temporary files | Partial downloads, work files | Nothing essential; incomplete work may be discarded |
|
||||||
|
|
||||||
|
These groups do not all need separate partitions. A **partition** is a defined region of a storage device. Several groups may use different directories in one partition.
|
||||||
|
|
||||||
|
The important question is what an operation can replace or erase. If the installer writes a complete partition image, a separate directory inside that partition will not protect the files in it.
|
||||||
|
|
||||||
|
## 2. An Example A/B Layout
|
||||||
|
|
||||||
|
In this example, each system slot holds a matching kernel, device tree, and root filesystem. The boot control mechanism selects them together.
|
||||||
|
|
||||||
|
The device tree describes hardware for the kernel. The root filesystem holds the system files Linux needs. Some platforms store these parts differently. The example shows their roles, rather than a required partition table.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Device storage: logical areas
|
||||||
|
|
|
||||||
|
+-- Shared boot components
|
||||||
|
| `-- Code needed to select and load a system
|
||||||
|
|
|
||||||
|
+-- Boot state
|
||||||
|
| `-- Trial target, attempts left, confirmation
|
||||||
|
|
|
||||||
|
+-- System slot A
|
||||||
|
| `-- Kernel A + device tree A + root filesystem A
|
||||||
|
|
|
||||||
|
+-- System slot B
|
||||||
|
| `-- Kernel B + device tree B + root filesystem B
|
||||||
|
|
|
||||||
|
+-- Persistent device data
|
||||||
|
| +-- Device identity and calibration
|
||||||
|
| +-- Configuration and application data
|
||||||
|
| `-- OTA progress, failure blocks, pending reports
|
||||||
|
|
|
||||||
|
`-- Download and work area
|
||||||
|
`-- Temporary files used to prepare an update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Persistent** means that information remains after a restart. Here, persistent device data also remains when either system slot is replaced.
|
||||||
|
|
||||||
|
Suppose A is running and B will receive the next version. The installer may replace B. It must preserve A and the data needed to start and use A again.
|
||||||
|
|
||||||
|
The running system in this example keeps its system files read-only. Files that change during normal operation go into the persistent data area. A read-only filesystem prevents ordinary writes to those system files. It does not protect them from every failure or from an installer that writes directly to the storage device.
|
||||||
|
|
||||||
|
The layout still has shared parts. If an update damages the shared boot components, neither slot may start. Updating those components needs its own safe installation and recovery method.
|
||||||
|
|
||||||
|
Two slots on one storage device also do not protect against failure of that whole device.
|
||||||
|
|
||||||
|
The following diagram adds access responsibilities to the logical layout. A is running while the installer prepares B. Trial requests and confirmation use the platform's boot control interface.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[PlantUML source](../diagrams/plantuml/ab-storage-layout.puml) · [Simulation of saved boot state across resets](../examples/simulations/ab-trial-boot/readme.md)
|
||||||
|
|
||||||
|
## 3. What Lives Outside the System Slots?
|
||||||
|
|
||||||
|
A system release provides common software. Device identity and calibration belong to an individual device. Installing a system image must not replace them with values from the release build.
|
||||||
|
|
||||||
|
Configuration has two possible sources. The system image can provide default settings. Settings chosen for this device live outside the image. The design must explain how the application uses both.
|
||||||
|
|
||||||
|
Application data also lives outside the replaceable system slots in this example. Updating the system should not erase stored measurements or a local database.
|
||||||
|
|
||||||
|
Separation does not mean that every program may write every file. The platform must define access rules. For example, an ordinary application should not be able to replace the device's identity keys.
|
||||||
|
|
||||||
|
[ADR-016: Separate System Software and Persistent Device Data](../adr/ADR-016-separate-system-software-and-persistent-device-data.md) explains the conditions for this choice.
|
||||||
|
|
||||||
|
## 4. Shared Data Must Work With Both Versions
|
||||||
|
|
||||||
|
Both A and B can use the same persistent data. This saves space and keeps new measurements available after a return to A. It also creates a compatibility requirement.
|
||||||
|
|
||||||
|
Imagine this sequence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
A uses database format 1
|
||||||
|
|
|
||||||
|
B starts and changes the database to format 2
|
||||||
|
|
|
||||||
|
B fails its health checks
|
||||||
|
|
|
||||||
|
The device returns to A
|
||||||
|
|
|
||||||
|
Can A still read the database?
|
||||||
|
```
|
||||||
|
|
||||||
|
If the answer is no, keeping A was not enough.
|
||||||
|
|
||||||
|
A **data migration** changes stored data into a new format. Before allowing one, define how recovery will work. Possible approaches include:
|
||||||
|
|
||||||
|
- Keep a format that both versions can use during the trial.
|
||||||
|
- Prepare a separate copy for the new version and keep the old copy for recovery.
|
||||||
|
- Save a backup and use a tested restore procedure.
|
||||||
|
- Delay a change that the old version cannot read until the device no longer promises a return to it.
|
||||||
|
|
||||||
|
Each approach has a cost. Copies and backups need space. Restoring old data may lose measurements or settings written after the copy was made. Delaying a migration only helps if the new version can work before the change.
|
||||||
|
|
||||||
|
Configuration needs the same care. A new setting may be harmless to an old application, or it may stop that application from starting.
|
||||||
|
|
||||||
|
The product must define how long the previous version remains a supported recovery choice. Passing health checks does not make every later data change safe for that version.
|
||||||
|
|
||||||
|
## 5. Update State Must Be Available to Its Readers
|
||||||
|
|
||||||
|
Chapter 04 introduced saved trial attempts, confirmation, recovery progress, and failure blocks. These records have different readers.
|
||||||
|
|
||||||
|
| State | Who needs it? | When must it be available? |
|
||||||
|
|---|---|---|
|
||||||
|
| Selected system and trial attempts | Boot control | Before the selected Linux system starts |
|
||||||
|
| System confirmation | Boot control and the program that confirms the system | During confirmation and on later boots |
|
||||||
|
| Installation and recovery progress | OTA manager or service supervisor | After a restart or interrupted operation |
|
||||||
|
| Failed package version block | Update policy owner | Before another automatic installation |
|
||||||
|
| Pending result report | OTA manager | When reporting can resume |
|
||||||
|
|
||||||
|
Putting all these records in one file is not always possible. Boot control may not be able to read the filesystem used by the OTA manager. The platform must choose a location and format each reader supports.
|
||||||
|
|
||||||
|
State needed to recover from replacing B must not exist only inside B. State needed by both systems must remain available when either slot is replaced.
|
||||||
|
|
||||||
|
Saving state also needs a method that handles power loss. After an interrupted write, the reader must recognize a valid record or follow a defined recovery path. The platform must define what happens when no valid record remains. It must not treat unreadable state as proof that the new version was confirmed.
|
||||||
|
|
||||||
|
Boot control and Linux must also agree on what the records mean. Both retained system versions need to understand the state they may read during recovery.
|
||||||
|
|
||||||
|
These requirements are described in [ADR-017: Keep Recovery State Outside Replaceable Software](../adr/ADR-017-keep-recovery-state-outside-replaceable-software.md). The next chapter will look more closely at safe writes and activation.
|
||||||
|
|
||||||
|
## 6. Storage for an Independent Service Update
|
||||||
|
|
||||||
|
The package example uses a stable system platform. The service files may be replaced in the running filesystem. It does not gain A/B protection just because its data is stored separately.
|
||||||
|
|
||||||
|
A possible layout is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Stable system platform
|
||||||
|
|
|
||||||
|
+-- Installed service files
|
||||||
|
|
|
||||||
|
+-- Service configuration and data
|
||||||
|
|
|
||||||
|
+-- Local recovery files
|
||||||
|
| `-- Exact previous package + verification information
|
||||||
|
|
|
||||||
|
+-- Supervisor state
|
||||||
|
| `-- Update progress, failed version block, pending report
|
||||||
|
|
|
||||||
|
`-- Download and work area
|
||||||
|
`-- New package and temporary files
|
||||||
|
```
|
||||||
|
|
||||||
|
The supervisor must remain able to run when the service fails. Its recovery files and state must survive the package operations it controls. They must not be deleted by the service's removal scripts or by ordinary cache cleanup.
|
||||||
|
|
||||||
|
Keeping the previous package locally allows recovery while offline. The device must also retain any dependencies and installation inputs needed for the supported downgrade. A single package file may not be enough.
|
||||||
|
|
||||||
|
Before installing version 2, check that version 1 can still use the resulting configuration and data. Package installation scripts may change these files even before the service starts.
|
||||||
|
|
||||||
|
[ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) defines the recovery policy for this example.
|
||||||
|
|
||||||
|
## 7. Plan for Space and Cleanup
|
||||||
|
|
||||||
|
The update needs more space than the final installed software alone. Depending on the method, it may need:
|
||||||
|
|
||||||
|
- The running version and the prepared new version.
|
||||||
|
- A downloaded artifact and files used during installation.
|
||||||
|
- Previous packages and their required dependencies.
|
||||||
|
- A data copy or backup used for recovery.
|
||||||
|
- Saved state, reports, and normal application data.
|
||||||
|
|
||||||
|
Some installers write verified download data directly into an inactive slot. Others keep a complete download first. The storage plan must match the chosen method.
|
||||||
|
|
||||||
|
Check the available space before starting. Normal services may keep writing data during an update, so the design also needs limits or reserved space for essential state. Logs and downloads must not consume all space needed for recovery records.
|
||||||
|
|
||||||
|
Cleanup needs clear rules. A partial download can usually be removed or downloaded again. A previous package needed for recovery cannot be treated as an ordinary disposable cache file.
|
||||||
|
|
||||||
|
For A/B, the inactive slot may become the target of the next update after the current system is confirmed. Replacing it removes that older recovery version. The update policy must make this change deliberately.
|
||||||
|
|
||||||
|
Never delete the only usable recovery files to make room for an unconfirmed update.
|
||||||
|
|
||||||
|
## 8. Check the Layout Against Failures
|
||||||
|
|
||||||
|
For each operation, ask which areas it can change and what remains usable if it stops halfway.
|
||||||
|
|
||||||
|
| Failure | What this design needs |
|
||||||
|
|---|---|
|
||||||
|
| Power is lost while writing B | A and the information needed to select it remain usable. B is not selected as a ready system. |
|
||||||
|
| Power is lost while saving trial state | Boot control detects a valid record or follows its defined recovery path. |
|
||||||
|
| B changes shared data and then fails | A can use the data, or a tested data recovery path is available. |
|
||||||
|
| The download area fills up | The update stops safely; essential state can still be saved. |
|
||||||
|
| Service installation is interrupted | The supervisor can read its progress and follow the package manager's recovery rules. |
|
||||||
|
| Cleanup runs during service recovery | Required packages, verification information, and recovery state are retained. |
|
||||||
|
| Shared boot code or the storage device fails | A separate recovery method is needed; two system slots alone are insufficient. |
|
||||||
|
|
||||||
|
This table describes requirements. A storage diagram does not prove they are met. Practical examples will need to test interrupted writes, installation, startup, and recovery.
|
||||||
|
|
||||||
|
## 9. Related Architecture Decisions
|
||||||
|
|
||||||
|
| Question | Further reading |
|
||||||
|
|---|---|
|
||||||
|
| When should a system get a trial boot? | [ADR-014: Trial Boot and Confirmation for System Updates](../adr/ADR-014-trial-boot-and-confirmation.md) |
|
||||||
|
| How is a failed service package restored? | [ADR-015: Service Checks and Package Downgrade](../adr/ADR-015-service-checks-and-package-downgrade.md) |
|
||||||
|
| When should device data be separate from system software? | [ADR-016: Separate System Software and Persistent Device Data](../adr/ADR-016-separate-system-software-and-persistent-device-data.md) |
|
||||||
|
| Where should recovery state live? | [ADR-017: Keep Recovery State Outside Replaceable Software](../adr/ADR-017-keep-recovery-state-outside-replaceable-software.md) |
|
||||||
|
|
||||||
|
## 10. What Comes Next?
|
||||||
|
|
||||||
|
A useful storage layout makes the boundaries clear: what the update replaces, what it preserves, and what recovery can still read.
|
||||||
|
|
||||||
|
Next, [chapter 06: Atomic Updates](06-atomic-updates.md) explains how to prepare and activate a new version without exposing an incomplete one, and how the device makes a safe decision after an interrupted operation.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
# 06. Atomic Updates: Preparation, Activation, and Interrupted Operations
|
||||||
|
|
||||||
|
[Chapter 05](05-storage-layout.md) described where software, device data, and recovery state live. Now we need to decide in which order to change them.
|
||||||
|
|
||||||
|
The main question is simple: **if power stops between two operations, what will the device do when it starts again?**
|
||||||
|
|
||||||
|
We will use the same A/B system and independent service examples. They show possible designs, rather than rules for every platform.
|
||||||
|
|
||||||
|
## 1. What Does Atomic Mean?
|
||||||
|
|
||||||
|
An **atomic operation** has one visible result: the old state or the new state. A reader must not see a partly applied result.
|
||||||
|
|
||||||
|
An OTA update includes many operations. Downloading, writing software, changing boot state, and checking the new system do not become one atomic operation just because the device has two slots.
|
||||||
|
|
||||||
|
It helps to separate three questions:
|
||||||
|
|
||||||
|
| Question | Mechanism in this A/B example |
|
||||||
|
|---|---|
|
||||||
|
| Can the installer prepare B without changing running system A? | Write only to the inactive slot. |
|
||||||
|
| Can boot control select a complete system after an interrupted state change? | Use a defined state storage and recovery method. |
|
||||||
|
| Does B work on this device? | Trial boot, local checks, and confirmation. |
|
||||||
|
|
||||||
|
**Activation** means allowing the new version to start. **Confirmation** means accepting it after checks. They are different events. B can be complete and safe to try, yet still fail at startup.
|
||||||
|
|
||||||
|
The aim is to make each visible switch safe and to define recovery between switches.
|
||||||
|
|
||||||
|
## 2. Prepare B While A Remains Available
|
||||||
|
|
||||||
|
Assume A is confirmed, B is inactive, and no earlier trial is pending. The installer has exclusive control of B. Shared boot code and persistent application data are outside its write target.
|
||||||
|
|
||||||
|
Before writing B, check the update policy, target hardware, available space, and trusted update information. Then:
|
||||||
|
|
||||||
|
1. Save the installation intent, including the release and target slot.
|
||||||
|
2. Write the new software to B. Keep A as the normal boot choice.
|
||||||
|
3. Complete the platform's required storage writes and check their results.
|
||||||
|
4. Verify the installed contents against the expected release.
|
||||||
|
5. Save that this release is prepared in B.
|
||||||
|
6. Ask boot control to save a trial request for B.
|
||||||
|
7. Save the local result of that request for later reporting.
|
||||||
|
|
||||||
|
The expected release information must come from a trusted source. Comparing two untrusted hashes does not establish trust. Signature policy will be covered with security.
|
||||||
|
|
||||||
|
A check of the downloaded file alone does not prove that writing B succeeded. Define how the installed contents are checked. If the installer decompresses or changes the artifact, the check must match the resulting format.
|
||||||
|
|
||||||
|
The platform must also define when writes have reached persistent storage. A read from a cache can match the expected bytes while required storage work is still pending.
|
||||||
|
|
||||||
|
If preparation fails, keep B unavailable for trial. Preserve A and enough state to retry or stop safely. Do not replace a slot that is running, selected for a pending trial, or still required for recovery.
|
||||||
|
|
||||||
|
This ordering is described in [ADR-018: Prepare and Verify Before Activation](../adr/ADR-018-prepare-and-verify-before-activation.md).
|
||||||
|
|
||||||
|
## 3. The Activation Boundary
|
||||||
|
|
||||||
|
In this example, boot control owns the trial target, attempt count, and confirmation. The OTA manager uses its interface; it does not maintain a second boot selector in a progress file.
|
||||||
|
|
||||||
|
Before boot control accepts B, the installed release and the state needed to try it must already be saved. A successful request means that boot control has saved the request according to its storage contract. It does not mean that B is confirmed.
|
||||||
|
|
||||||
|
If the request returns an error or its result is lost during a restart, the OTA manager must read boot state before retrying. An error does not always prove that nothing changed.
|
||||||
|
|
||||||
|
[Preparation and activation sequence — PlantUML source](../diagrams/plantuml/ab-installation-sequence.puml)
|
||||||
|
|
||||||
|
There is a gap between steps 6 and 7: boot control may have saved the request while the OTA manager still says “prepared.” After a restart, boot control can start B. The OTA manager must accept that saved boot decision and repair its progress record. It must not overwrite B or reset the trial attempt count.
|
||||||
|
|
||||||
|
Two separate records do not form one transaction. Safe ordering and recovery rules handle the states left between them.
|
||||||
|
|
||||||
|
## 4. Save State With an Explicit Storage Contract
|
||||||
|
|
||||||
|
**Durable state** is state that remains available after the failures covered by the design. A successful ordinary file write is not enough to establish this property.
|
||||||
|
|
||||||
|
For a Linux file, one possible replacement procedure is:
|
||||||
|
|
||||||
|
1. Create a temporary file in the same directory as the destination.
|
||||||
|
2. Write the complete new record and check for errors.
|
||||||
|
3. Use `fsync()` on the temporary file and check the result.
|
||||||
|
4. Rename it over the destination.
|
||||||
|
5. Use `fsync()` on the containing directory and check the result.
|
||||||
|
|
||||||
|
The Linux [fsync manual](https://man7.org/linux/man-pages/man2/fsync.2.html) explains that synchronizing a file does not by itself make its directory entry durable. This procedure needs a filesystem and storage stack that support the required operations. It is not a general recipe for raw flash or bootloader state.
|
||||||
|
|
||||||
|
After an interrupted operation, a reader needs a valid old record, a valid new record, or a defined recovery action. Records can include a format version, release identity, generation number, and a checksum. A checksum can help detect damage; it does not prove authenticity or make writes atomic.
|
||||||
|
|
||||||
|
Another option is redundant records. Keep the previous valid record while saving a new one, validate both after restart, and apply a defined selection rule. The storage design must explain how interrupted writes affect each copy. Two copies are not sufficient if one erase operation can destroy both.
|
||||||
|
|
||||||
|
When no valid boot record remains, use the platform's separate recovery policy. Do not interpret missing or unreadable state as confirmation. Do not invent a normal slot from an OTA progress file that boot control cannot trust.
|
||||||
|
|
||||||
|
## 5. Continue After a Restart
|
||||||
|
|
||||||
|
Before any new write, read the records owned by boot control and inspect the target. Check the release identity, rather than only a flag such as `ready = true`.
|
||||||
|
|
||||||
|
| State found after restart | Action in this example |
|
||||||
|
|---|---|
|
||||||
|
| A selected; B partly written | Keep A; restart preparation of B from the beginning. |
|
||||||
|
| A selected; B contains the expected complete release; progress is old | Verify B again, then save prepared state. |
|
||||||
|
| A selected; B prepared; no trial requested | Keep A until policy allows activation. |
|
||||||
|
| B trial requested; progress still says prepared | Keep the boot request; repair progress without writing B. |
|
||||||
|
| Trial ended or B was confirmed while reporting was interrupted | Read the saved boot outcome; do not create a fresh trial. |
|
||||||
|
| Boot state is unreadable | Stop ordinary installation and use the platform recovery policy. |
|
||||||
|
|
||||||
|
An operation is **idempotent** if repeating it has the same intended effect as doing it once. Reading state and verifying the same release can usually be repeated. Rewriting all of B can be repeated here only while B is inactive and has no pending trial.
|
||||||
|
|
||||||
|
Repeating a trial request is different. If it restores the attempt count, a failed system could receive endless attempts. Read the existing trial and its result before deciding what to do.
|
||||||
|
|
||||||
|
Package scripts, database migrations, and external actions also need their own repeat rules. An “installation in progress” flag cannot tell whether such an action completed.
|
||||||
|
|
||||||
|
[Installation states and restart decisions — PlantUML source](../diagrams/plantuml/ab-installation-recovery.puml)
|
||||||
|
|
||||||
|
[ADR-019: Recover Interrupted Updates From Durable State](../adr/ADR-019-recover-interrupted-updates-from-durable-state.md) defines the ownership and recovery rules used here. [ADR-017](../adr/ADR-017-keep-recovery-state-outside-replaceable-software.md) defines where the records must live.
|
||||||
|
|
||||||
|
## 6. An Independent Service Update
|
||||||
|
|
||||||
|
A service package may replace files in the running filesystem. Stopping the service does not make those replacements one atomic operation.
|
||||||
|
|
||||||
|
If installation stops halfway, some files or scripts may have changed while others have not. Use the package manager's documented repair procedure before assuming that the previous package can be installed safely. Retain its dependencies, required inputs, and compatible data as described in [ADR-015](../adr/ADR-015-service-checks-and-package-downgrade.md).
|
||||||
|
|
||||||
|
For an application that supports it, another design is to prepare a complete version in a separate directory and switch a reference to it. This gives a smaller activation boundary. It still needs rules for saving that reference, stopping and starting the process, and keeping data compatible. A running process may still hold old files open after the reference changes.
|
||||||
|
|
||||||
|
Neither design makes database changes or external actions part of the software switch. Define their recovery separately.
|
||||||
|
|
||||||
|
## 7. A C++ Model of Interrupted Installation
|
||||||
|
|
||||||
|
The [C++17 example](../examples/simulations/interrupted-installation/readme.md) models one A-to-B installation. It keeps simulated persistent storage separate from an installer object. Recreating the installer removes its temporary verification result while keeping saved bytes and records.
|
||||||
|
|
||||||
|
It covers interruptions after intent, partial writing, complete writing, verification, prepared state, and trial request. It also models three possible results of an interrupted boot-state write: the old record, the new record, or no valid record.
|
||||||
|
|
||||||
|
Important checks include:
|
||||||
|
|
||||||
|
- B cannot be activated before verification and saved prepared state.
|
||||||
|
- A remains unchanged during every installation scenario.
|
||||||
|
- A saved trial request is preserved when progress is old.
|
||||||
|
- A restart requires fresh verification before a new activation.
|
||||||
|
- Invalid boot state requires separate recovery.
|
||||||
|
|
||||||
|
The model uses memory, expected byte strings, and explicit reset points. It does not implement a durable file store, cryptographic verification, or a real bootloader. It does not simulate interruption inside a storage write. The three boot-record outcomes are supplied assumptions, not evidence that a storage method produces them.
|
||||||
|
|
||||||
|
The existing [trial boot example](../examples/simulations/ab-trial-boot/readme.md) continues with attempts, health checks, confirmation, and return to A. The examples are separate programs.
|
||||||
|
|
||||||
|
## 8. Check the Design Against Failures
|
||||||
|
|
||||||
|
| Failure | Required result |
|
||||||
|
|---|---|
|
||||||
|
| Power stops during writing B | A remains available; B is not selected for trial. |
|
||||||
|
| Installed contents fail verification | Stop activation; retain A. |
|
||||||
|
| Saving prepared state fails | Do not request activation until saved state is checked. |
|
||||||
|
| Saving the trial request is interrupted | Boot control reads a valid record or enters defined recovery. |
|
||||||
|
| Trial request is saved but progress is old | Read boot state; do not rewrite B or refresh attempts. |
|
||||||
|
| State storage fills up or returns a write error | Stop forward progress and inspect state before retrying. |
|
||||||
|
| A package script stops after changing shared data | Follow its defined recovery procedure; software downgrade alone may be insufficient. |
|
||||||
|
|
||||||
|
Model tests check ordering. Platform tests must also interrupt real writes, resets, and recovery on the chosen storage. Test failures during recovery too. A completed write in a C++ model does not prove that hardware will retain it.
|
||||||
|
|
||||||
|
## 9. What Comes Next?
|
||||||
|
|
||||||
|
Preparation keeps incomplete software out of the boot path. Activation gives a complete version a chance to run. Confirmation still depends on checks after startup.
|
||||||
|
|
||||||
|
Next, we will look more closely at rollback: when to return to a previous version, what recovery needs, and when that previous version is no longer a usable choice.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(ab_trial_boot LANGUAGES CXX)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
add_library(ab-simulation simulation.cpp)
|
||||||
|
target_compile_features(ab-simulation PUBLIC cxx_std_17)
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(ab-simulation PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(ab-simulate main.cpp)
|
||||||
|
target_link_libraries(ab-simulate PRIVATE ab-simulation)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(ab-simulation-tests tests.cpp)
|
||||||
|
target_link_libraries(ab-simulation-tests PRIVATE ab-simulation)
|
||||||
|
add_test(NAME recovery-invariants COMMAND ab-simulation-tests)
|
||||||
|
add_test(NAME all-scenarios COMMAND ab-simulate all)
|
||||||
|
add_test(NAME cli-help COMMAND ab-simulate --help)
|
||||||
|
add_test(NAME cli-invalid-scenario COMMAND ab-simulate invalid)
|
||||||
|
set_tests_properties(cli-invalid-scenario PROPERTIES WILL_FAIL TRUE)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
const std::string selected = argc > 1 ? argv[1] : "all";
|
||||||
|
if (argc > 2 || selected == "--help" || selected == "-h" ||
|
||||||
|
(selected != "all" && std::find(ota::scenarios().begin(), ota::scenarios().end(),
|
||||||
|
selected) == ota::scenarios().end())) {
|
||||||
|
std::ostream &output = (argc == 2 && (selected == "--help" || selected == "-h"))
|
||||||
|
? std::cout
|
||||||
|
: std::cerr;
|
||||||
|
output << "Usage: ab-simulate [all";
|
||||||
|
for (const auto &scenario: ota::scenarios()) output << '|' << scenario;
|
||||||
|
output << "]\n";
|
||||||
|
return &output == &std::cout ? 0 : 2;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const auto &scenario: ota::scenarios()) {
|
||||||
|
if (selected != "all" && selected != scenario) continue;
|
||||||
|
std::cout << "\n=== " << scenario << " ===\n";
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
ota::run_scenario(scenario, directory.path(), &std::cout);
|
||||||
|
}
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
std::cerr << "Simulation failed: " << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# A/B trial boot simulation
|
||||||
|
|
||||||
|
This C++17 standard-library example follows the [trial boot diagram](../../../diagrams/plantuml/ab-trial-boot.puml), [chapter 04](../../../docs/04-starting-an-updated-system-or-service.md), and [ADR-014](../../../adr/ADR-014-trial-boot-and-confirmation.md). It illustrates boot decisions; it does not install software or access a device. No third-party libraries are required.
|
||||||
|
|
||||||
|
Build and run from the repository root with CMake 3.20 or later and a C++17 compiler. These commands use a Linux temporary build directory to keep generated files outside the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S examples/simulations/ab-trial-boot -B /tmp/ota-ab-build -DCMAKE_BUILD_TYPE=Debug
|
||||||
|
cmake --build /tmp/ota-ab-build
|
||||||
|
/tmp/ota-ab-build/ab-simulate all
|
||||||
|
/tmp/ota-ab-build/ab-simulate attempts-exhausted
|
||||||
|
ctest --test-dir /tmp/ota-ab-build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
| Scenario | Expected result |
|
||||||
|
|---|---|
|
||||||
|
| `success` | Checks pass; B is confirmed and selected on the next boot. |
|
||||||
|
| `health-failure` | B fails checks; reset selects A; recovery checks pass. |
|
||||||
|
| `attempts-exhausted` | B never reaches checks; two unconfirmed attempts lead to A. |
|
||||||
|
| `restart-during-trial` | A reset preserves the consumed attempt; the second trial passes. |
|
||||||
|
| `interrupted-preparation` | Incomplete B remains ineligible; reset selects A. |
|
||||||
|
| `recovery-failure` | A also fails; stop at the boundary of a separate recovery policy. |
|
||||||
|
|
||||||
|
With no argument, `ab-simulate` runs all scenarios. Use `--help` to list the accepted arguments. Invalid arguments return a nonzero exit status.
|
||||||
|
|
||||||
|
The attempt limit of two is an illustrative choice. Each reset reconstructs the device from a versioned text record (`boot-state.txt`) in a temporary directory. The running slot is volatile. The saved normal slot stays A until B is confirmed. Boot control saves each consumed attempt before returning B as the selected system. A failed health check requests recovery immediately rather than spending the remaining trial attempts.
|
||||||
|
|
||||||
|
The example keeps boot state outside the simulated slots, as shown in the [storage diagram](../../../diagrams/plantuml/ab-storage-layout.puml). Each scenario uses a separate temporary directory, removed when it finishes. The model covers one A-to-B update; it does not model subsequent releases.
|
||||||
|
|
||||||
|
## Assumptions and limits
|
||||||
|
|
||||||
|
Installation and artifact verification are represented by one preparation operation. Health checks are supplied boolean outcomes, including a timeout represented as failure. Resets are explicit calls; watchdog coverage and real startup timing are not simulated. Recovery assumes that A and shared data remain usable, unless the recovery-failure scenario supplies a failed check.
|
||||||
|
|
||||||
|
The text file is a teaching stand-in for boot state, using standard C++ streams and `std::filesystem`. It records the phase, normal slot, optional trial slot, attempts remaining, slot readiness, and outcome. Replacement uses a temporary file and rename, but does not flush storage, implement redundant records, or recover invalid records. Unreadable records stop the simulation with an error; they are never treated as confirmation. This is not a power-loss-safe boot-state implementation. The simulation assumes completed valid writes; it does not test interruption within a write, coordination with OTA progress, data migration, signatures, bootloader integration, or hardware failure. These need platform-specific implementation and failure-injection tests.
|
||||||
|
|
||||||
|
Outcomes remain local in the model. There is no server dependency or reporting transport. A real implementation must retain pending reports separately and send them when connectivity returns.
|
||||||
|
|
||||||
|
## Source and checks
|
||||||
|
|
||||||
|
- [simulation.hpp](simulation.hpp) and [simulation.cpp](simulation.cpp): typed state model, saved records, reset behavior, and scenarios.
|
||||||
|
- [main.cpp](main.cpp): command-line runner.
|
||||||
|
- [tests.cpp](tests.cpp): scenario outcomes and invariants for saved attempts, incomplete slots, confirmation, and unreadable state. Checks remain enabled in Release builds.
|
||||||
|
- [CMakeLists.txt](CMakeLists.txt): builds the runner and registers the invariant and CLI checks with CTest.
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <random>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace ota {
|
||||||
|
namespace {
|
||||||
|
constexpr const char *phase_names[] = {
|
||||||
|
"A confirmed", "Prepare B", "B prepared and verified", "B trial pending",
|
||||||
|
"B trial running", "B confirmed", "Recovery to A pending",
|
||||||
|
"A recovery running", "A recovery checked", "Separate recovery required"};
|
||||||
|
|
||||||
|
Phase read_phase(const std::string &value) {
|
||||||
|
for (int i = 0; i < 10; ++i) {
|
||||||
|
if (value == phase_names[i]) return static_cast<Phase>(i);
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Invalid saved phase");
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot read_slot(const std::string &value) {
|
||||||
|
if (value == "A") return Slot::A;
|
||||||
|
if (value == "B") return Slot::B;
|
||||||
|
throw std::runtime_error("Invalid saved slot");
|
||||||
|
}
|
||||||
|
|
||||||
|
void require(bool condition, const char *message) {
|
||||||
|
if (!condition) throw std::logic_error(message);
|
||||||
|
}
|
||||||
|
}// namespace
|
||||||
|
|
||||||
|
const char *name(Phase phase) { return phase_names[static_cast<int>(phase)]; }
|
||||||
|
const char *name(Slot slot) { return slot == Slot::A ? "A" : "B"; }
|
||||||
|
|
||||||
|
Device::Device(const std::filesystem::path &directory)
|
||||||
|
: path_(directory / "boot-state.txt") {
|
||||||
|
if (!std::filesystem::exists(path_)) {
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::ifstream input(path_);
|
||||||
|
std::string header, phase, normal, trial;
|
||||||
|
int a_ready = 0, b_ready = 0;
|
||||||
|
if (!(input >> header >> std::quoted(phase) >> normal >> trial >> state_.attempts_left >> a_ready >> b_ready >> std::quoted(state_.outcome)) || header != "OTA_AB_STATE_V1") {
|
||||||
|
throw std::runtime_error("Unreadable saved boot state");
|
||||||
|
}
|
||||||
|
input >> std::ws;
|
||||||
|
if (!input.eof() || state_.attempts_left < 0 ||
|
||||||
|
(a_ready != 0 && a_ready != 1) || (b_ready != 0 && b_ready != 1)) {
|
||||||
|
throw std::runtime_error("Invalid saved boot state");
|
||||||
|
}
|
||||||
|
state_.phase = read_phase(phase);
|
||||||
|
state_.normal = read_slot(normal);
|
||||||
|
if (trial != "none") state_.trial = read_slot(trial);
|
||||||
|
state_.a_ready = a_ready != 0;
|
||||||
|
state_.b_ready = b_ready != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Device::save() const {
|
||||||
|
auto temporary = path_;
|
||||||
|
temporary += ".tmp";
|
||||||
|
std::ofstream output(temporary, std::ios::trunc);
|
||||||
|
output << "OTA_AB_STATE_V1\n"
|
||||||
|
<< std::quoted(name(state_.phase)) << '\n'
|
||||||
|
<< name(state_.normal) << '\n'
|
||||||
|
<< (state_.trial ? name(*state_.trial) : "none") << '\n'
|
||||||
|
<< state_.attempts_left << '\n'
|
||||||
|
<< state_.a_ready << ' ' << state_.b_ready << '\n'
|
||||||
|
<< std::quoted(state_.outcome) << '\n';
|
||||||
|
output.close();
|
||||||
|
if (!output) throw std::runtime_error("Cannot save boot state");
|
||||||
|
// Teaching stand-in: rename alone does not guarantee power-loss durability.
|
||||||
|
std::filesystem::rename(temporary, path_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Device::prepare(bool complete) {
|
||||||
|
require(state_.phase == Phase::AConfirmed || state_.phase == Phase::PreparingB,
|
||||||
|
"Cannot prepare B in this phase");
|
||||||
|
state_.b_ready = false;
|
||||||
|
state_.phase = Phase::PreparingB;
|
||||||
|
save();
|
||||||
|
if (complete) {
|
||||||
|
// Represents complete installation AND artifact verification.
|
||||||
|
state_.b_ready = true;
|
||||||
|
state_.phase = Phase::BPrepared;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Device::request_trial(int attempts) {
|
||||||
|
require(state_.phase == Phase::BPrepared && state_.b_ready,
|
||||||
|
"B must be prepared and verified before trial");
|
||||||
|
require(attempts > 0, "The trial needs at least one attempt");
|
||||||
|
state_.phase = Phase::TrialPending;
|
||||||
|
state_.trial = Slot::B;
|
||||||
|
state_.attempts_left = attempts;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot Device::boot() {
|
||||||
|
require(state_.phase != Phase::SeparateRecoveryRequired,
|
||||||
|
"Separate recovery is required; do not alternate failed systems");
|
||||||
|
if (state_.trial) {
|
||||||
|
if (state_.attempts_left > 0) {
|
||||||
|
--state_.attempts_left;
|
||||||
|
state_.phase = Phase::TrialRunning;
|
||||||
|
// Persist the consumed attempt BEFORE handing control to B.
|
||||||
|
save();
|
||||||
|
running_ = *state_.trial;
|
||||||
|
return *running_;
|
||||||
|
}
|
||||||
|
state_.phase = Phase::RecoveryPending;
|
||||||
|
state_.trial.reset();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
if (state_.phase == Phase::RecoveryPending) {
|
||||||
|
state_.phase = Phase::RecoveryRunning;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
running_ = state_.normal;
|
||||||
|
return *running_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Device::check(bool healthy) {
|
||||||
|
require(running_.has_value(), "Boot before checking the running system");
|
||||||
|
require(state_.phase == Phase::TrialRunning || state_.phase == Phase::RecoveryRunning,
|
||||||
|
"Checks require a running trial or recovery");
|
||||||
|
if (state_.phase == Phase::TrialRunning) {
|
||||||
|
state_.trial.reset();
|
||||||
|
state_.attempts_left = 0;
|
||||||
|
if (healthy) {
|
||||||
|
state_.phase = Phase::BConfirmed;
|
||||||
|
state_.normal = Slot::B;
|
||||||
|
state_.outcome = "B confirmed";
|
||||||
|
} else {
|
||||||
|
state_.phase = Phase::RecoveryPending;
|
||||||
|
}
|
||||||
|
} else if (healthy) {
|
||||||
|
state_.phase = Phase::RecoveryChecked;
|
||||||
|
state_.outcome = "Recovered to A";
|
||||||
|
} else {
|
||||||
|
state_.phase = Phase::SeparateRecoveryRequired;
|
||||||
|
state_.outcome = "Recovery A failed";
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryDirectory::TemporaryDirectory() {
|
||||||
|
std::random_device random;
|
||||||
|
for (int attempt = 0; attempt < 100; ++attempt) {
|
||||||
|
auto candidate = std::filesystem::temp_directory_path() /
|
||||||
|
("ota-ab-simulation-" + std::to_string(random()) + "-" + std::to_string(random()));
|
||||||
|
if (std::filesystem::create_directory(candidate)) {
|
||||||
|
path_ = std::move(candidate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Cannot create a unique temporary directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryDirectory::~TemporaryDirectory() {
|
||||||
|
std::error_code ignored;
|
||||||
|
std::filesystem::remove_all(path_, ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> &scenarios() {
|
||||||
|
static const std::vector<std::string> names = {
|
||||||
|
"success", "health-failure", "attempts-exhausted", "restart-during-trial",
|
||||||
|
"interrupted-preparation", "recovery-failure"};
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
Device run_scenario(const std::string &scenario, const std::filesystem::path &directory,
|
||||||
|
std::ostream *output) {
|
||||||
|
if (std::find(scenarios().begin(), scenarios().end(), scenario) == scenarios().end()) {
|
||||||
|
throw std::invalid_argument("Unknown scenario: " + scenario);
|
||||||
|
}
|
||||||
|
Device device(directory);
|
||||||
|
auto show = [&](const std::string &event) {
|
||||||
|
if (!output) return;
|
||||||
|
const auto &state = device.state();
|
||||||
|
*output << event << ": " << name(state.phase) << "; normal=" << name(state.normal)
|
||||||
|
<< "; attempts_left=" << state.attempts_left << "; running="
|
||||||
|
<< (device.running() ? name(*device.running()) : "none") << '\n';
|
||||||
|
};
|
||||||
|
auto reset = [&] {
|
||||||
|
device = Device(directory);
|
||||||
|
const auto selected = device.boot();
|
||||||
|
show(std::string("Reset -> ") + name(selected));
|
||||||
|
};
|
||||||
|
auto message = [&](const char *text) { if (output) *output << text << '\n'; };
|
||||||
|
|
||||||
|
show("Initial state");
|
||||||
|
if (scenario == "interrupted-preparation") {
|
||||||
|
device.prepare(false);
|
||||||
|
show("Interrupted installation: B remains ineligible");
|
||||||
|
reset();
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
device.prepare();
|
||||||
|
show("Installation and verification complete");
|
||||||
|
device.request_trial(2);
|
||||||
|
show("Trial requested");
|
||||||
|
reset();
|
||||||
|
if (scenario == "attempts-exhausted" || scenario == "restart-during-trial") {
|
||||||
|
message("No confirmation: simulate a reset caused by power loss or watchdog");
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
if (scenario == "attempts-exhausted") {
|
||||||
|
message("B still cannot reach health checks; simulate another reset");
|
||||||
|
reset();
|
||||||
|
device.check(true);
|
||||||
|
show("Recovery checks pass");
|
||||||
|
} else if (scenario == "health-failure" || scenario == "recovery-failure") {
|
||||||
|
device.check(false);
|
||||||
|
show("B health checks fail");
|
||||||
|
reset();
|
||||||
|
device.check(scenario != "recovery-failure");
|
||||||
|
show("Recovery checks complete");
|
||||||
|
} else {
|
||||||
|
device.check(true);
|
||||||
|
show("B health checks pass; confirmation saved");
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}// namespace ota
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <ostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ota {
|
||||||
|
|
||||||
|
enum class Phase {
|
||||||
|
AConfirmed,
|
||||||
|
PreparingB,
|
||||||
|
BPrepared,
|
||||||
|
TrialPending,
|
||||||
|
TrialRunning,
|
||||||
|
BConfirmed,
|
||||||
|
RecoveryPending,
|
||||||
|
RecoveryRunning,
|
||||||
|
RecoveryChecked,
|
||||||
|
SeparateRecoveryRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Slot { A,
|
||||||
|
B };
|
||||||
|
|
||||||
|
const char *name(Phase phase);
|
||||||
|
const char *name(Slot slot);
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
Phase phase = Phase::AConfirmed;
|
||||||
|
Slot normal = Slot::A;
|
||||||
|
std::optional<Slot> trial;
|
||||||
|
int attempts_left = 0;
|
||||||
|
bool a_ready = true;
|
||||||
|
bool b_ready = false;
|
||||||
|
std::string outcome;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Device {
|
||||||
|
public:
|
||||||
|
explicit Device(const std::filesystem::path &directory);
|
||||||
|
const State &state() const { return state_; }
|
||||||
|
std::optional<Slot> running() const { return running_; }
|
||||||
|
void prepare(bool complete = true);
|
||||||
|
void request_trial(int attempts = 2);
|
||||||
|
Slot boot();
|
||||||
|
void check(bool healthy);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void save() const;
|
||||||
|
State state_;
|
||||||
|
std::filesystem::path path_;
|
||||||
|
// Volatile: reconstructing a Device requires another boot before checks.
|
||||||
|
std::optional<Slot> running_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Own only a freshly created directory and clean it up at scope exit.
|
||||||
|
class TemporaryDirectory {
|
||||||
|
public:
|
||||||
|
TemporaryDirectory();
|
||||||
|
~TemporaryDirectory();
|
||||||
|
TemporaryDirectory(const TemporaryDirectory &) = delete;
|
||||||
|
TemporaryDirectory &operator=(const TemporaryDirectory &) = delete;
|
||||||
|
const std::filesystem::path &path() const { return path_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<std::string> &scenarios();
|
||||||
|
Device run_scenario(const std::string &scenario,
|
||||||
|
const std::filesystem::path &directory,
|
||||||
|
std::ostream *output = nullptr);
|
||||||
|
|
||||||
|
}// namespace ota
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void expect(bool condition, const char *message) {
|
||||||
|
if (!condition) throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Operation>
|
||||||
|
void expect_rejected(Operation operation) {
|
||||||
|
try {
|
||||||
|
operation();
|
||||||
|
} catch (const std::logic_error &) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Invalid operation was accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
void scenario_outcomes() {
|
||||||
|
struct Expected {
|
||||||
|
const char *scenario;
|
||||||
|
ota::Phase phase;
|
||||||
|
ota::Slot normal;
|
||||||
|
const char *outcome;
|
||||||
|
};
|
||||||
|
const Expected expected[] = {
|
||||||
|
{"success", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
|
||||||
|
{"health-failure", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
|
||||||
|
{"attempts-exhausted", ota::Phase::RecoveryChecked, ota::Slot::A, "Recovered to A"},
|
||||||
|
{"restart-during-trial", ota::Phase::BConfirmed, ota::Slot::B, "B confirmed"},
|
||||||
|
{"interrupted-preparation", ota::Phase::PreparingB, ota::Slot::A, ""},
|
||||||
|
{"recovery-failure", ota::Phase::SeparateRecoveryRequired, ota::Slot::A,
|
||||||
|
"Recovery A failed"}};
|
||||||
|
for (const auto &result: expected) {
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
auto device = ota::run_scenario(result.scenario, directory.path());
|
||||||
|
const ota::Device restored(directory.path());
|
||||||
|
const auto &state = restored.state();
|
||||||
|
expect(state.phase == result.phase, "Unexpected saved phase");
|
||||||
|
expect(state.normal == result.normal, "Unexpected normal slot");
|
||||||
|
expect(state.outcome == result.outcome, "Unexpected saved outcome");
|
||||||
|
expect(state.a_ready, "The retained slot was modified");
|
||||||
|
if (state.phase == ota::Phase::SeparateRecoveryRequired) {
|
||||||
|
expect_rejected([&] { device.boot(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void saved_attempts() {
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
ota::Device device(directory.path());
|
||||||
|
device.prepare();
|
||||||
|
device.request_trial(2);
|
||||||
|
for (int left: {1, 0}) {
|
||||||
|
device = ota::Device(directory.path());
|
||||||
|
expect(device.boot() == ota::Slot::B, "Expected trial boot of B");
|
||||||
|
const ota::Device restored(directory.path());
|
||||||
|
expect(restored.state().attempts_left == left, "Attempt was not saved before boot");
|
||||||
|
expect(restored.state().normal == ota::Slot::A, "B was confirmed prematurely");
|
||||||
|
}
|
||||||
|
device = ota::Device(directory.path());
|
||||||
|
expect(device.boot() == ota::Slot::A, "Exhausted trial did not return to A");
|
||||||
|
expect(device.state().phase == ota::Phase::RecoveryRunning, "Recovery checks skipped");
|
||||||
|
}
|
||||||
|
|
||||||
|
void incomplete_slot() {
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
ota::Device device(directory.path());
|
||||||
|
device.prepare(false);
|
||||||
|
device = ota::Device(directory.path());
|
||||||
|
expect(!device.state().b_ready, "Incomplete B was marked ready");
|
||||||
|
expect_rejected([&] { device.request_trial(); });
|
||||||
|
expect(device.boot() == ota::Slot::A, "Incomplete B was selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
void confirmation_requires_boot() {
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
ota::Device device(directory.path());
|
||||||
|
device.prepare();
|
||||||
|
expect_rejected([&] { device.request_trial(0); });
|
||||||
|
expect_rejected([&] { device.request_trial(-1); });
|
||||||
|
device.request_trial();
|
||||||
|
expect_rejected([&] { device.check(true); });
|
||||||
|
device.boot();
|
||||||
|
ota::Device restored(directory.path());
|
||||||
|
expect(!restored.running(), "Running slot survived reconstruction");
|
||||||
|
expect_rejected([&] { restored.check(true); });
|
||||||
|
device.check(true);
|
||||||
|
expect_rejected([&] { device.check(false); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void unreadable_state() {
|
||||||
|
ota::TemporaryDirectory directory;
|
||||||
|
{
|
||||||
|
std::ofstream output(directory.path() / "boot-state.txt");
|
||||||
|
output << "incomplete record\n";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const ota::Device device(directory.path());
|
||||||
|
} catch (const std::runtime_error &) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Unreadable state was accepted");
|
||||||
|
}
|
||||||
|
}// namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
scenario_outcomes();
|
||||||
|
saved_attempts();
|
||||||
|
incomplete_slot();
|
||||||
|
confirmation_requires_boot();
|
||||||
|
unreadable_state();
|
||||||
|
std::cout << "All recovery invariant checks passed\n";
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
std::cerr << "Test failed: " << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(interrupted_installation LANGUAGES CXX)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
add_library(installation-model simulation.cpp)
|
||||||
|
target_compile_features(installation-model PUBLIC cxx_std_17)
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(installation-model PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(installation-simulate main.cpp)
|
||||||
|
target_link_libraries(installation-simulate PRIVATE installation-model)
|
||||||
|
|
||||||
|
include(CTest)
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
add_executable(installation-tests tests.cpp)
|
||||||
|
target_link_libraries(installation-tests PRIVATE installation-model)
|
||||||
|
add_test(NAME installation-invariants COMMAND installation-tests)
|
||||||
|
add_test(NAME all-scenarios COMMAND installation-simulate all)
|
||||||
|
add_test(NAME cli-help COMMAND installation-simulate --help)
|
||||||
|
add_test(NAME cli-invalid COMMAND installation-simulate invalid)
|
||||||
|
set_tests_properties(cli-invalid PROPERTIES WILL_FAIL TRUE)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace model = ota::installation;
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc == 2 && std::string(argv[1]) == "--help") {
|
||||||
|
std::cout << "Usage: installation-simulate [all|--help]\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (argc > 2 || (argc == 2 && std::string(argv[1]) != "all")) {
|
||||||
|
std::cerr << "Usage: installation-simulate [all|--help]\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (int cut = 0; cut <= model::installation_steps; ++cut) {
|
||||||
|
model::Storage storage;
|
||||||
|
{
|
||||||
|
model::Installer installer(storage);
|
||||||
|
for (int step = 0; step < cut; ++step) model::perform_step(installer, step);
|
||||||
|
}
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
const auto action = restarted.recover();
|
||||||
|
std::cout << "Reset after " << cut << " steps: boot=" << model::boot_choice(storage.boot)
|
||||||
|
<< "; " << model::name(action) << '\n';
|
||||||
|
}
|
||||||
|
for (const auto result: {model::WriteResult::OldRecord, model::WriteResult::NewRecord, model::WriteResult::InvalidRecord}) {
|
||||||
|
model::Storage storage;
|
||||||
|
{
|
||||||
|
model::Installer installer(storage);
|
||||||
|
for (int step = 0; step < 5; ++step) model::perform_step(installer, step);
|
||||||
|
try {
|
||||||
|
installer.request_trial(result);
|
||||||
|
} catch (const std::runtime_error &error) {
|
||||||
|
std::cout << error.what() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
std::cout << "Boot-state write outcome: " << model::name(restarted.recover()) << '\n';
|
||||||
|
}
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
std::cerr << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Interrupted A/B installation model
|
||||||
|
|
||||||
|
This C++17 example follows [chapter 06](../../../docs/06-atomic-updates.md), [ADR-018](../../../adr/ADR-018-prepare-and-verify-before-activation.md), and [ADR-019](../../../adr/ADR-019-recover-interrupted-updates-from-durable-state.md). It models preparation and activation of B while confirmed system A remains available. It does not access devices or install files.
|
||||||
|
|
||||||
|
Build and run from the repository root with CMake 3.20 or later and a C++17 compiler. No third-party libraries are required.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S examples/simulations/interrupted-installation -B /tmp/ota-installation-build -DCMAKE_BUILD_TYPE=Debug
|
||||||
|
cmake --build /tmp/ota-installation-build
|
||||||
|
/tmp/ota-installation-build/installation-simulate all
|
||||||
|
ctest --test-dir /tmp/ota-installation-build --output-on-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
With no argument, the runner also runs all scenarios. `--help` prints usage. Unknown arguments return a nonzero exit status.
|
||||||
|
|
||||||
|
## Reset Points
|
||||||
|
|
||||||
|
Each run starts from fresh storage. It performs the first N steps, destroys the installer, and creates another installer using the same simulated storage.
|
||||||
|
|
||||||
|
| Steps completed before reset | Last operation | Recovery action |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | None | Keep A; start preparation. |
|
||||||
|
| 1 | Save release and target intent | Keep A; restart preparation. |
|
||||||
|
| 2 | Write part of B | Keep A; restart preparation. |
|
||||||
|
| 3 | Complete writing B | Verify again; save prepared state; wait for activation policy. |
|
||||||
|
| 4 | Verify B | Repeat verification; save prepared state; wait for activation policy. |
|
||||||
|
| 5 | Save prepared state | Verify again; wait for activation policy. |
|
||||||
|
| 6 | Save trial request | Preserve trial and attempts; repair old progress. |
|
||||||
|
| 7 | Save trial-requested progress | Preserve trial and attempts. |
|
||||||
|
|
||||||
|
The runner also supplies three possible results of an interrupted boot-record write: retain the old record, retain the new record, or find no valid record. The last case requires separate recovery. These are assumed outcomes, not a redundant-record algorithm.
|
||||||
|
|
||||||
|
## Storage and Ownership
|
||||||
|
|
||||||
|
`Storage` represents persistent bytes and records in memory. Each modeled storage operation is already complete and durable when it returns. `Installer` holds a temporary verification result. Recreating it clears that result, so a new activation requires fresh verification.
|
||||||
|
|
||||||
|
Boot state owns selection and the trial limit. OTA progress can be old. When a trial request already exists, recovery repairs progress without rewriting B or renewing attempts. Recovery with A selected checks both the release identity and installed contents before accepting B as prepared.
|
||||||
|
|
||||||
|
Only one installer is active. A is initially confirmed; there is no earlier pending trial. B is writable only while boot state selects A. A remains unchanged. `recover()` does not activate B automatically; the caller still owns activation policy.
|
||||||
|
|
||||||
|
## Checks and Limits
|
||||||
|
|
||||||
|
[tests.cpp](tests.cpp) checks all reset points and their continuation, activation guards, loss of temporary verification, damaged contents, wrong release identity, all supplied boot-write outcomes, and repeated recovery without renewing attempts. Checks remain active in Release builds.
|
||||||
|
|
||||||
|
The model compares strings with fixed expected bytes. It has no signatures, real files, flushing, redundant records, or interruption inside a write. It does not model storage wear, concurrent installers, data migration, running processes, or later releases. Direct changes to `Storage` in tests represent faults between installer instances.
|
||||||
|
|
||||||
|
The boot record is simplified to A selected, B trial, or invalid. Attempts are recorded but not consumed here. The printed boot choice is the next lifecycle destination, not a real bootloader decision. Confirmation, attempt exhaustion, and recovery health checks remain in the separate [A/B trial boot example](../ab-trial-boot/readme.md); the programs are not integrated.
|
||||||
|
|
||||||
|
Real storage needs a defined durability contract and failure-injection tests on the target platform. This model checks ordering and recovery decisions only.
|
||||||
|
|
||||||
|
## Sources and Diagrams
|
||||||
|
|
||||||
|
- [simulation.hpp](simulation.hpp) and [simulation.cpp](simulation.cpp): storage, installer, and recovery rules.
|
||||||
|
- [main.cpp](main.cpp): scenario output.
|
||||||
|
- [CMakeLists.txt](CMakeLists.txt): build and CTest integration.
|
||||||
|
- [Preparation and activation sequence](../../../diagrams/plantuml/ab-installation-sequence.puml).
|
||||||
|
- [Installation recovery states](../../../diagrams/plantuml/ab-installation-recovery.puml).
|
||||||
|
|
||||||
|
Format C++ sources with the repository's root `.clang-format`.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace ota::installation {
|
||||||
|
|
||||||
|
const std::string &expected_contents() {
|
||||||
|
static const std::string contents = "release-2:kernel+device-tree+rootfs";
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *name(RecoveryAction action) {
|
||||||
|
switch (action) {
|
||||||
|
case RecoveryAction::RestartPreparation:
|
||||||
|
return "restart preparation; keep A";
|
||||||
|
case RecoveryAction::ReadyToActivate:
|
||||||
|
return "B verified again; wait for activation policy";
|
||||||
|
case RecoveryAction::PreserveTrial:
|
||||||
|
return "preserve saved B trial and attempts";
|
||||||
|
case RecoveryAction::SeparateRecovery:
|
||||||
|
return "stop; separate recovery required";
|
||||||
|
}
|
||||||
|
throw std::logic_error("Unknown recovery action");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *boot_choice(BootRecord record) {
|
||||||
|
switch (record) {
|
||||||
|
case BootRecord::ASelected:
|
||||||
|
return "A";
|
||||||
|
case BootRecord::BTrial:
|
||||||
|
return "B trial (attempt handling is outside this model)";
|
||||||
|
case BootRecord::Invalid:
|
||||||
|
return "separate recovery";
|
||||||
|
}
|
||||||
|
throw std::logic_error("Unknown boot record");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::require_writable() const {
|
||||||
|
if (storage_.boot != BootRecord::ASelected) {
|
||||||
|
throw std::logic_error("B is selected or boot state is invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::begin() {
|
||||||
|
require_writable();
|
||||||
|
verified_ = false;
|
||||||
|
storage_.release = "release-2";
|
||||||
|
storage_.progress = Progress::Writing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::write_partial() {
|
||||||
|
require_writable();
|
||||||
|
if (storage_.progress != Progress::Writing || storage_.release != "release-2") {
|
||||||
|
throw std::logic_error("Save matching intent before writing");
|
||||||
|
}
|
||||||
|
verified_ = false;
|
||||||
|
storage_.b_contents = expected_contents().substr(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::finish_write() {
|
||||||
|
require_writable();
|
||||||
|
if (storage_.progress != Progress::Writing || storage_.release != "release-2") {
|
||||||
|
throw std::logic_error("Save matching intent before writing");
|
||||||
|
}
|
||||||
|
verified_ = false;
|
||||||
|
storage_.b_contents = expected_contents();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::verify() {
|
||||||
|
require_writable();
|
||||||
|
verified_ = false;
|
||||||
|
if (storage_.release != "release-2" || storage_.b_contents != expected_contents()) {
|
||||||
|
throw std::runtime_error("Installed release does not match expected contents");
|
||||||
|
}
|
||||||
|
verified_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::save_prepared() {
|
||||||
|
require_writable();
|
||||||
|
if (!verified_) throw std::logic_error("Verify B before saving prepared state");
|
||||||
|
storage_.progress = Progress::Prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::request_trial(WriteResult result) {
|
||||||
|
require_writable();
|
||||||
|
if (!verified_ || storage_.progress != Progress::Prepared) {
|
||||||
|
throw std::logic_error("Verify B and save prepared state before activation");
|
||||||
|
}
|
||||||
|
// Supplied outcomes stand for the storage contract of boot control.
|
||||||
|
// OldRecord leaves all fields intact; NewRecord saves one complete record.
|
||||||
|
if (result == WriteResult::OldRecord) {
|
||||||
|
throw std::runtime_error("Interrupted request; old record remains");
|
||||||
|
}
|
||||||
|
if (result == WriteResult::InvalidRecord) {
|
||||||
|
storage_.boot = BootRecord::Invalid;
|
||||||
|
throw std::runtime_error("Interrupted request; no valid record remains");
|
||||||
|
}
|
||||||
|
storage_.boot = BootRecord::BTrial;
|
||||||
|
storage_.attempts_left = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Installer::save_requested() {
|
||||||
|
if (storage_.boot != BootRecord::BTrial) {
|
||||||
|
throw std::logic_error("No saved B trial request");
|
||||||
|
}
|
||||||
|
storage_.progress = Progress::TrialRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecoveryAction Installer::recover() {
|
||||||
|
verified_ = false;
|
||||||
|
// Boot state takes priority over progress. Never write B or renew attempts
|
||||||
|
// when a trial is already selected. Invalid boot state is not confirmation.
|
||||||
|
if (storage_.boot == BootRecord::Invalid) return RecoveryAction::SeparateRecovery;
|
||||||
|
if (storage_.boot == BootRecord::BTrial) {
|
||||||
|
save_requested();
|
||||||
|
return RecoveryAction::PreserveTrial;
|
||||||
|
}
|
||||||
|
if (storage_.release != "release-2" || storage_.b_contents != expected_contents()) {
|
||||||
|
return RecoveryAction::RestartPreparation;
|
||||||
|
}
|
||||||
|
verify();
|
||||||
|
save_prepared();
|
||||||
|
return RecoveryAction::ReadyToActivate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void perform_step(Installer &installer, int step) {
|
||||||
|
switch (step) {
|
||||||
|
case 0:
|
||||||
|
installer.begin();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
installer.write_partial();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
installer.finish_write();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
installer.verify();
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
installer.save_prepared();
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
installer.request_trial();
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
installer.save_requested();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw std::logic_error("Unknown installation step");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}// namespace ota::installation
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace ota::installation {
|
||||||
|
|
||||||
|
enum class Progress { Idle,
|
||||||
|
Writing,
|
||||||
|
Prepared,
|
||||||
|
TrialRequested };
|
||||||
|
enum class BootRecord { ASelected,
|
||||||
|
BTrial,
|
||||||
|
Invalid };
|
||||||
|
enum class WriteResult { OldRecord,
|
||||||
|
NewRecord,
|
||||||
|
InvalidRecord };
|
||||||
|
enum class RecoveryAction { RestartPreparation,
|
||||||
|
ReadyToActivate,
|
||||||
|
PreserveTrial,
|
||||||
|
SeparateRecovery };
|
||||||
|
|
||||||
|
// This object stands for persistent storage. Each assignment is a completed
|
||||||
|
// durable operation in the model, not a real storage implementation.
|
||||||
|
struct Storage {
|
||||||
|
std::string a_contents = "release-1";
|
||||||
|
std::string b_contents;
|
||||||
|
std::string release;
|
||||||
|
Progress progress = Progress::Idle;
|
||||||
|
BootRecord boot = BootRecord::ASelected;
|
||||||
|
int attempts_left = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::string &expected_contents();
|
||||||
|
const char *name(RecoveryAction action);
|
||||||
|
const char *boot_choice(BootRecord record);
|
||||||
|
|
||||||
|
class Installer {
|
||||||
|
public:
|
||||||
|
explicit Installer(Storage &storage) : storage_(storage) {}
|
||||||
|
void begin();
|
||||||
|
void write_partial();
|
||||||
|
void finish_write();
|
||||||
|
void verify();
|
||||||
|
void save_prepared();
|
||||||
|
void request_trial(WriteResult result = WriteResult::NewRecord);
|
||||||
|
void save_requested();
|
||||||
|
RecoveryAction recover();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void require_writable() const;
|
||||||
|
Storage &storage_;
|
||||||
|
// Lost when the installer is reconstructed after a simulated reset.
|
||||||
|
bool verified_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ordered operations; cut points are before step 0 and after each step.
|
||||||
|
constexpr int installation_steps = 7;
|
||||||
|
void perform_step(Installer &installer, int step);
|
||||||
|
|
||||||
|
}// namespace ota::installation
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#include "simulation.hpp"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace model = ota::installation;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void expect(bool condition, const char *message) {
|
||||||
|
if (!condition) throw std::runtime_error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<class Operation>
|
||||||
|
void expect_rejected(Operation operation) {
|
||||||
|
try {
|
||||||
|
operation();
|
||||||
|
} catch (const std::logic_error &) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw std::runtime_error("Unsafe operation accepted");
|
||||||
|
}
|
||||||
|
|
||||||
|
void cut_points() {
|
||||||
|
for (int cut = 0; cut <= model::installation_steps; ++cut) {
|
||||||
|
model::Storage storage;
|
||||||
|
{
|
||||||
|
model::Installer installer(storage);
|
||||||
|
for (int step = 0; step < cut; ++step) model::perform_step(installer, step);
|
||||||
|
}
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
const auto action = restarted.recover();
|
||||||
|
expect(storage.a_contents == "release-1", "A changed during installation");
|
||||||
|
if (cut < 3) {
|
||||||
|
expect(action == model::RecoveryAction::RestartPreparation, "Incomplete B accepted");
|
||||||
|
expect(storage.boot == model::BootRecord::ASelected, "Incomplete B selected");
|
||||||
|
expect_rejected([&] { restarted.request_trial(); });
|
||||||
|
// Exercise the documented restart path, not only its decision.
|
||||||
|
for (int step = 0; step < model::installation_steps; ++step) model::perform_step(restarted, step);
|
||||||
|
} else if (cut < 6) {
|
||||||
|
expect(action == model::RecoveryAction::ReadyToActivate, "Complete B not reverified");
|
||||||
|
expect(storage.boot == model::BootRecord::ASelected, "Recovery activated B without policy");
|
||||||
|
restarted.request_trial();
|
||||||
|
restarted.save_requested();
|
||||||
|
} else {
|
||||||
|
expect(action == model::RecoveryAction::PreserveTrial, "Saved trial lost");
|
||||||
|
expect(storage.progress == model::Progress::TrialRequested, "Old progress not repaired");
|
||||||
|
}
|
||||||
|
expect(storage.boot == model::BootRecord::BTrial, "Continuation did not reach trial");
|
||||||
|
expect(storage.b_contents == model::expected_contents(), "Selected B is incomplete");
|
||||||
|
expect(storage.a_contents == "release-1", "Continuation changed A");
|
||||||
|
storage.attempts_left = 1;// Boot control has already consumed an attempt.
|
||||||
|
model::Installer second_restart(storage);
|
||||||
|
expect(second_restart.recover() == model::RecoveryAction::PreserveTrial, "Repeated recovery lost trial");
|
||||||
|
expect(storage.attempts_left == 1, "Recovery renewed trial attempts");
|
||||||
|
expect_rejected([&] { second_restart.begin(); });
|
||||||
|
expect_rejected([&] { second_restart.write_partial(); });
|
||||||
|
expect_rejected([&] { second_restart.finish_write(); });
|
||||||
|
expect_rejected([&] { second_restart.request_trial(); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void activation_guards() {
|
||||||
|
model::Storage storage;
|
||||||
|
model::Installer installer(storage);
|
||||||
|
expect_rejected([&] { installer.finish_write(); });
|
||||||
|
expect_rejected([&] { installer.save_requested(); });
|
||||||
|
installer.begin();
|
||||||
|
installer.finish_write();
|
||||||
|
expect_rejected([&] { installer.save_prepared(); });
|
||||||
|
installer.verify();
|
||||||
|
expect_rejected([&] { installer.request_trial(); });
|
||||||
|
installer.save_prepared();
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
expect_rejected([&] { restarted.request_trial(); });
|
||||||
|
expect(restarted.recover() == model::RecoveryAction::ReadyToActivate, "Fresh verification failed");
|
||||||
|
restarted.request_trial();
|
||||||
|
}
|
||||||
|
|
||||||
|
void bad_contents_and_identity() {
|
||||||
|
for (bool wrong_release: {false, true}) {
|
||||||
|
model::Storage storage;
|
||||||
|
{
|
||||||
|
model::Installer installer(storage);
|
||||||
|
for (int step = 0; step < 5; ++step) model::perform_step(installer, step);
|
||||||
|
}
|
||||||
|
if (wrong_release) storage.release = "another-release";
|
||||||
|
else
|
||||||
|
storage.b_contents = "damaged";
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
expect(restarted.recover() == model::RecoveryAction::RestartPreparation, "Stale prepared record trusted");
|
||||||
|
try {
|
||||||
|
restarted.verify();
|
||||||
|
throw std::logic_error("Bad release passed verification");
|
||||||
|
} catch (const std::runtime_error &) {
|
||||||
|
// Expected content or identity failure; logic_error escapes.
|
||||||
|
}
|
||||||
|
expect_rejected([&] { restarted.request_trial(); });
|
||||||
|
expect(storage.boot == model::BootRecord::ASelected, "Bad release selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void interrupted_boot_write() {
|
||||||
|
for (const auto result: {model::WriteResult::OldRecord, model::WriteResult::NewRecord, model::WriteResult::InvalidRecord}) {
|
||||||
|
model::Storage storage;
|
||||||
|
{
|
||||||
|
model::Installer installer(storage);
|
||||||
|
for (int step = 0; step < 5; ++step) model::perform_step(installer, step);
|
||||||
|
bool failed = false;
|
||||||
|
try {
|
||||||
|
installer.request_trial(result);
|
||||||
|
} catch (const std::runtime_error &) {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
expect(failed == (result != model::WriteResult::NewRecord), "Wrong request result");
|
||||||
|
}
|
||||||
|
model::Installer restarted(storage);
|
||||||
|
const auto action = restarted.recover();
|
||||||
|
if (result == model::WriteResult::OldRecord) {
|
||||||
|
expect(action == model::RecoveryAction::ReadyToActivate, "Old boot record not retained");
|
||||||
|
expect(storage.attempts_left == 0, "Old record partly changed");
|
||||||
|
} else if (result == model::WriteResult::NewRecord) {
|
||||||
|
expect(action == model::RecoveryAction::PreserveTrial, "New boot record not retained");
|
||||||
|
expect(storage.attempts_left == 2, "New trial limit lost");
|
||||||
|
} else {
|
||||||
|
expect(action == model::RecoveryAction::SeparateRecovery, "Invalid boot state accepted");
|
||||||
|
expect(storage.progress == model::Progress::Prepared, "Invalid boot state changed progress");
|
||||||
|
expect_rejected([&] { restarted.begin(); });
|
||||||
|
expect_rejected([&] { restarted.request_trial(); });
|
||||||
|
}
|
||||||
|
expect(storage.a_contents == "release-1", "Boot record failure changed A");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}// namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
cut_points();
|
||||||
|
activation_guards();
|
||||||
|
bad_contents_and_identity();
|
||||||
|
interrupted_boot_write();
|
||||||
|
std::cout << "All installation recovery checks passed\n";
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
std::cerr << "Test failed: " << error.what() << '\n';
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A practical reference design for reliable and secure over-the-air updates on embedded Linux.
|
A practical reference design for reliable and secure over-the-air updates on embedded Linux.
|
||||||
|
|
||||||
This repository is intended as an educational reference design. It demonstrates one possible approach to designing and implementing a reliable OTA update system for embedded Linux.
|
This repository demonstrates one possible approach to designing and implementing a reliable OTA update system for embedded Linux.
|
||||||
|
|
||||||
The project focuses on the engineering decisions behind OTA systems, including:
|
The project focuses on the engineering decisions behind OTA systems, including:
|
||||||
|
|
||||||
@@ -22,6 +22,10 @@ This repository is not intended to be a production-ready framework. Instead, it
|
|||||||
|
|
||||||
The repository is being developed incrementally as a structured series of articles, diagrams, architecture decisions, and practical examples.
|
The repository is being developed incrementally as a structured series of articles, diagrams, architecture decisions, and practical examples.
|
||||||
|
|
||||||
|
Start with the [series overview](docs/00-series-overview.md). The current practical material includes [PlantUML diagrams](diagrams/readme.md), an [A/B trial boot simulation](examples/simulations/ab-trial-boot/readme.md), and an [interrupted installation model](examples/simulations/interrupted-installation/readme.md). [Chapter 06: Atomic Updates](docs/06-atomic-updates.md) connects preparation, activation, and recovery after interruption.
|
||||||
|
|
||||||
|
See the [changelog](CHANGELOG.md) for dated updates to the chapters, architecture decisions, diagrams, and simulations, with links to the material and related history.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user