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