diff --git a/README.md b/README.md index afbde4a..8e46657 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,8 @@ UpdateManager.ConfirmDeploymentImage(); - [Update sessions](docs/update-sessions.md) - staging an image, resuming an interrupted download, session rules and result codes. - [Confirm and revert](docs/confirm-and-revert.md) - `ConfirmDeploymentImage`, - `RequestDeploymentRevert`, `RequestClrRevert`, `RequestReboot` and the test cycle. + `RequestDeploymentRevert`, `RequestClrRevert`, `RequestReboot`, the test cycle and the + `[DeploymentConfirmationTimeout]` failsafe. - [Writing an update provider](docs/writing-an-update-provider.md) - how to build an update library that fetches images from a server or other provider, end to end, on the `IUpdateProvider` contract from the `nanoFramework.Runtime.InFieldUpdate.Provider` package. diff --git a/Samples/IFU_blink-app/Program.cs b/Samples/IFU_blink-app/Program.cs index 4ac329b..5b3de85 100644 --- a/Samples/IFU_blink-app/Program.cs +++ b/Samples/IFU_blink-app/Program.cs @@ -10,6 +10,8 @@ public class Program { private static GpioController s_GpioController; + // revert this deployment if it isn't confirmed within 60 seconds + [DeploymentConfirmationTimeout(60)] public static void Main() { Debug.WriteLine("Hello from nanoFramework!"); diff --git a/Samples/IFU_blink-app/README.md b/Samples/IFU_blink-app/README.md index 3dc0d05..13b9b3c 100644 --- a/Samples/IFU_blink-app/README.md +++ b/Samples/IFU_blink-app/README.md @@ -5,3 +5,8 @@ This app can be used to visually test the update workflow. Adjust the `blinkCount` var around line 26 to the number of blinks matching the version build number. For example, when setting `AssemblyVersion` to 1.0.3, change it to 3 so it blinks 3 times. For version 1.0.2 make 2 and so on. Note: if needed, change/add a new GPIO configuration to open the LED pin. Around line 19. + +`Main` carries `[DeploymentConfirmationTimeout(60)]`. To see the failsafe at work, comment out the +`ConfirmDeploymentImage()` call: about 60 seconds after booting the new version the device resets, and the +previous version (with its blink count) comes back. See +[Confirmation timeout](../../docs/confirm-and-revert.md#confirmation-timeout). diff --git a/docs/confirm-and-revert.md b/docs/confirm-and-revert.md index fd5ee8d..4cde894 100644 --- a/docs/confirm-and-revert.md +++ b/docs/confirm-and-revert.md @@ -30,7 +30,8 @@ A completed update session marks the new deployment image for a **test** swap: - on the next reboot the previous image is restored. This makes a bad update self-healing: an image that cannot get far enough to confirm itself is -rolled back automatically. +rolled back automatically. To make sure that reboot actually happens when the new application +hangs, add a [confirmation timeout](#confirmation-timeout). ### `ConfirmDeploymentImage` @@ -93,6 +94,72 @@ Things worth checking in `SelfTest`: Keep it bounded in time: until the image is confirmed, any reset brings the previous image back, so a self-test that never completes means the update does not stick. +## Confirmation timeout + +"On the next reboot" needs something to cause that reboot. A new application that hangs, +deadlocks or waits forever never confirms itself, and it never reboots either. The device stays +stuck on the broken image until someone power-cycles it. On a device in the field that can be a +long wait. + +Put `DeploymentConfirmationTimeout` on `Main` to close that gap: + +```csharp +using nanoFramework.Runtime.InFieldUpdate; + +public class Program +{ + // this deployment has 2 minutes to confirm itself, otherwise it is reverted + [DeploymentConfirmationTimeout(120)] + public static void Main() + { + if (UpdateManager.GetStatus(ImageType.Deployment) == UpdateStatus.Testing && SelfTest()) + { + UpdateManager.ConfirmDeploymentImage(); + } + + // normal application start + } +} +``` + +When the deployment is running as an unconfirmed test image, the nanoCLR starts a countdown just +before `Main` runs (static constructors are included). If `ConfirmDeploymentImage()` isn't called +in time, the device is reset and MCUboot restores the previous deployment. + +- **Units**: seconds. `0`, or no attribute at all, leaves the countdown off. +- **Only when it matters**: the countdown is only armed when the deployment is unconfirmed. Once + the image is confirmed, the attribute has no effect, so it is safe to leave in place. +- **Per deployment**: the value travels with the application. Each deployment can choose its + own, without a firmware change. +- **Once per power cycle**: the countdown starts once. A CLR-only reboot neither restarts nor + extends it. +- **What stops it**: + - a successful `ConfirmDeploymentImage()`; + - a confirm sent by a tool over the Wire Protocol; + - the start of a debugger session (source-level debugging), so a developer stepping through + code isn't reset in the middle of it. + + A failed confirm keeps the countdown running. When it is cancelled by a debugger, the image is + still unconfirmed, so it is reverted on the next reboot unless it gets confirmed. +- **A deployment that can't start**: this case doesn't need the attribute. If an unconfirmed + deployment fails to load (corrupted assembly, mismatch with the firmware's native code) or has + no entry point, the nanoCLR reboots straight away so it is reverted. +- **Platform support**: implemented for ChibiOS targets with MCUboot. On other platforms the + attribute is ignored. + +### Choosing a value + +The timeout has to cover the worst-case time from power-on to the `ConfirmDeploymentImage()` call: + +- static constructors; +- the self-test, including waiting for the network to come up and the update server to answer; +- any retries the self-test makes. + +Leave a generous margin. A timeout that is too short reverts a perfectly good update on a slow day. +That is recoverable, because the update can be retried, but it is still wasted effort. Values in +minutes are typical for applications that check connectivity. A few tens of seconds is enough for +one that only checks local peripherals. + ## nanoCLR image The nanoCLR image behaves differently: **the running nanoCLR is already confirmed at startup**, so diff --git a/docs/overview.md b/docs/overview.md index 93d5bbd..f74b92a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -52,7 +52,8 @@ flowchart LR [confirms itself](confirm-and-revert.md). 4. **Confirm or revert.** A confirmed image becomes permanent. An image that is not confirmed - because it crashed, hung, or decided it is broken - is replaced by the previous one on the next - reboot. + reboot. A [confirmation timeout](confirm-and-revert.md#confirmation-timeout) on `Main` forces + that reboot when the new application hangs. ## Update states @@ -88,7 +89,7 @@ interleave. See [Update sessions](update-sessions.md). | Query | `GetStatus`, `GetPrimaryImageInfo`, `GetSecondaryImageInfo`, `GetImageList`, `ImageInfo`, `ImageInfoExtensions.ToTable` | [Querying images](querying-images.md) | | Stage | `StartUpdateSession`, `ResumeUpdateSession`, `StoreImageChunk`, `UpdateSession.Write`, `CompleteUpdateSession`, `AbortUpdateSession`, `EraseSecondaryImage` | [Update sessions](update-sessions.md) | | Diagnostics | `GetUpdateSessionOwner`, `UpdateSessionResult` (returned by every session operation) | [Update sessions](update-sessions.md) | -| Confirm / revert | `ConfirmDeploymentImage`, `RequestDeploymentRevert`, `RequestClrRevert` | [Confirm and revert](confirm-and-revert.md) | +| Confirm / revert | `ConfirmDeploymentImage`, `RequestDeploymentRevert`, `RequestClrRevert`, `DeploymentConfirmationTimeoutAttribute` | [Confirm and revert](confirm-and-revert.md) | | Reboot | `RequestReboot` | [Confirm and revert](confirm-and-revert.md) | | Provider contract (`nanoFramework.Runtime.InFieldUpdate.Provider` package) | `IUpdateProvider`, `UpdateAgentOptions`, `HealthCheck` | [Writing an update provider](writing-an-update-provider.md) | diff --git a/nanoFramework.Runtime.InFieldUpdate/DeploymentConfirmationTimeoutAttribute.cs b/nanoFramework.Runtime.InFieldUpdate/DeploymentConfirmationTimeoutAttribute.cs new file mode 100644 index 0000000..d60c1f5 --- /dev/null +++ b/nanoFramework.Runtime.InFieldUpdate/DeploymentConfirmationTimeoutAttribute.cs @@ -0,0 +1,38 @@ +// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +using System; + +namespace nanoFramework.Runtime.InFieldUpdate +{ + /// + /// Sets how long a new deployment has to confirm itself before the device reboots and reverts it. + /// + /// + /// + /// Apply it to the application entry point (Main). When the deployment is running as an unconfirmed + /// test image, the CLR starts a countdown before Main runs. If + /// isn't called before it expires, the device is reset and the bootloader reverts to the previous deployment. + /// + /// + /// The countdown is armed at most once per power cycle and only for an unconfirmed deployment. It is cancelled by a + /// successful confirmation or when a debugger session starts. A value of 0 disables it. + /// + /// + /// The value is read by the CLR from the attribute metadata. Platforms without support for it ignore the attribute. + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class DeploymentConfirmationTimeoutAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Time, in seconds, the deployment has to confirm itself. 0 disables the countdown. + public DeploymentConfirmationTimeoutAttribute(uint seconds) + { + } + } +} diff --git a/nanoFramework.Runtime.InFieldUpdate/nanoFramework.Runtime.InFieldUpdate.nfproj b/nanoFramework.Runtime.InFieldUpdate/nanoFramework.Runtime.InFieldUpdate.nfproj index 64847d6..7190e97 100644 --- a/nanoFramework.Runtime.InFieldUpdate/nanoFramework.Runtime.InFieldUpdate.nfproj +++ b/nanoFramework.Runtime.InFieldUpdate/nanoFramework.Runtime.InFieldUpdate.nfproj @@ -40,6 +40,7 @@ +