Add container restart runtime support (#41454)

beena352 committed Sep 4, 2026 at 08:16 UTC 556440f392aef150dc2e8d39152b20a4d7d5b83c
9 files changed +480 -16
doc/docs/api-reference/c/error-codes.md
+2
@@ -20,6 +20,7 @@
20 #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
21 #define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
22 #define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
23 +#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 19) /* 0x80040613 */
24 ```
25
26 | Symbol | Hex Value |
@@ -43,5 +44,6 @@
44 | `WSLC_E_VM_NOT_RUNNING` | `0x80040610` |
45 | `WSLC_E_EVENTS_LOST` | `0x80040611` |
46 | `WSLC_E_EVENT_STREAM_FINISHED` | `0x80040612` |
47 +| `WSLC_E_CONTAINER_DELETED` | `0x80040613` |
48
49 ---
localization/strings/en-US/Resources.resw
+4
@@ -2195,6 +2195,10 @@ Usage:
2195 <value>Container '{}' not found.</value>
2196 <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2197 </data>
2198 + <data name="MessageWslcContainerDeleted" xml:space="preserve">
2199 + <value>Container '{}' has been deleted.</value>
2200 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2201 + </data>
2202 <data name="MessageWslcNetworkNameRequired" xml:space="preserve">
2203 <value>Network name cannot be empty.</value>
2204 </data>
src/windows/WslcSDK/winrt/wslcsdk.idl
+1
@@ -381,5 +381,6 @@ namespace Microsoft.WSL.Containers
381 ContainerDisabled = 0x8004060C,
382 RegistryBlockedByPolicy = 0x8004060D,
383 VolumeNotAvailable = 0x8004060E,
384 + ContainerDeleted = 0x80040613,
385 };
386 }
src/windows/WslcSDK/wslcsdk.h
+1
@@ -46,6 +46,7 @@ EXTERN_C_START
46 #define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */
47 #define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */
48 #define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */
49 +#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 19) /* 0x80040613 */
50
51 // Session values
52 #define WSLC_SESSION_OPTIONS_SIZE 72
src/windows/common/wslutil.cpp
+1
@@ -180,6 +180,7 @@ static const std::map<HRESULT, LPCWSTR> g_commonErrors{
180 X(WSLC_E_NETWORK_NOT_FOUND),
181 X(WSLC_E_SESSION_NOT_FOUND),
182 X(WSLC_E_VM_NOT_RUNNING),
183 + X(WSLC_E_CONTAINER_DELETED),
184 X(WSLC_E_EVENTS_LOST),
185 X(WSLC_E_EVENT_STREAM_FINISHED),
186 X(WSLC_E_WU_SEARCH_FAILED),
src/windows/service/inc/wslc.idl
+2
@@ -582,6 +582,7 @@ interface IWSLCContainer : IUnknown
582 HRESULT DisconnectFromNetwork([in] LPCSTR NetworkName);
583 HRESULT UploadArchive([in] WSLCHandle TarHandle, [in, string] LPCSTR DestPath, [in] ULONGLONG ContentSize);
584 HRESULT DownloadArchive([in, string] LPCSTR SrcPath, [in] WSLCHandle OutHandle);
585 + HRESULT Restart([in] WSLCSignal Signal, [in] LONG TimeoutSeconds, [in, unique] IWarningCallback* WarningCallback);
586 }
587
588 typedef struct _WSLCDeletedImageInformation
@@ -911,3 +912,4 @@ cpp_quote("#define WSLC_E_SESSION_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILIT
912 cpp_quote("#define WSLC_E_VM_NOT_RUNNING MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 16) /* 0x80040610 */")
913 cpp_quote("#define WSLC_E_EVENTS_LOST MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 17) /* 0x80040611 */")
914 cpp_quote("#define WSLC_E_EVENT_STREAM_FINISHED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 18) /* 0x80040612 */")
915 +cpp_quote("#define WSLC_E_CONTAINER_DELETED MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, WSLC_E_BASE + 19) /* 0x80040613 */")
src/windows/wslcsession/WSLCContainer.cpp
+182 -14
@@ -1037,11 +1037,20 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
1037 }
1038
1039 void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions)
1040 +{
1041 + StartPhase(Flags, StartOptions, false);
1042 +}
1043 +
1044 +void WSLCContainerImpl::StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase)
1045 {
1046 std::shared_ptr<StateTransition> transition;
1047 auto lifecycleLock = m_lifecycleLock.lock_shared();
1048 auto lock = m_lock.lock_exclusive();
1044 - WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1049 +
1050 + WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, !RestartPhase);
1051 +
1052 + // A Delete() that raced a restart may have already moved the container to the Deleted state.
1053 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);
1054
1055 THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
1056
@@ -1120,10 +1129,17 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1129 Localization::MessageWslcVolumeNotAvailable(wsl::shared::string::Join(unavailableVolumes, ',')),
1130 !unavailableVolumes.empty());
1131
1123 - auto volumeCleanup = MountVolumes(m_mountedVolumes, m_runtime.Vm());
1132 + // A restart keeps its ports and mounts across both phases, so re-acquiring them here would collide
1133 + // with the container's own reservations. Release them if the start does not land, since an exited
1134 + // container must not keep holding them.
1135 + auto resourceCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { ReleaseRuntimeResources(); });
1136
1125 - auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
1126 - MapPorts();
1137 + if (!m_runtimeResourcesHeld)
1138 + {
1139 + MountVolumes(m_mountedVolumes, m_runtime.Vm()).release();
1140 + MapPorts();
1141 + m_runtimeResourcesHeld = true;
1142 + }
1143
1144 try
1145 {
@@ -1171,8 +1187,7 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1187
1188 transition = StartTransition(TransitionKind::Start, ContainerEvent::Start);
1189
1174 - portCleanup.release();
1175 - volumeCleanup.release();
1190 + resourceCleanup.release();
1191 cleanup.release();
1192
1193 lock.reset();
@@ -1181,16 +1196,29 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1196 }
1197
1198 void WSLCContainerImpl::WaitForConflictingTransitionToComplete(
1184 - wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind)
1199 + wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind, bool waitForRestart)
1200 {
1186 - while (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
1201 + while (true)
1202 {
1203 + // A restart spans two transitions, so waiting on the one in flight is not enough.
1204 + if (waitForRestart && m_restart)
1205 + {
1206 + auto restart = m_restart;
1207 + lock.reset();
1208 + lifecycleLock.reset();
1209 + WaitForCompletionEvent(restart->Completed.get());
1210 + }
1211 + else if (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
1212 {
1213 auto transition = m_transition;
1214 lock.reset();
1215 lifecycleLock.reset();
1216 WaitForTransitionCompletion(transition);
1217 }
1218 + else
1219 + {
1220 + return;
1221 + }
1222
1223 lifecycleLock = m_lifecycleLock.lock_shared();
1224 lock = m_lock.lock_exclusive();
@@ -1206,11 +1234,16 @@ __requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateT
1234 return transition;
1235 }
1236
1209 -void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
1237 +void WSLCContainerImpl::WaitForCompletionEvent(HANDLE Event) const
1238 {
1239 auto io = m_wslcSession.CreateIOContext();
1212 - io.AddHandle(std::make_unique<EventHandle>(transition->Completed.get()));
1240 + io.AddHandle(std::make_unique<EventHandle>(Event));
1241 io.Run({});
1242 +}
1243 +
1244 +void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
1245 +{
1246 + WaitForCompletionEvent(transition->Completed.get());
1247
1248 WI_ASSERT(transition->Completed.is_signaled());
1249 }
@@ -1325,13 +1358,25 @@ void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCod
1358 }
1359
1360 void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1361 +{
1362 + StopPhase(Signal, TimeoutSeconds, Kill, false);
1363 +}
1364 +
1365 +void WSLCContainerImpl::StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase)
1366 {
1367 std::shared_ptr<StateTransition> transition;
1368
1369 {
1370 auto lifecycleLock = m_lifecycleLock.lock_shared();
1371 auto lock = m_lock.lock_exclusive();
1334 - WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop);
1372 +
1373 + // Kill is the escape hatch when a restart's stop phase is stuck, so it must not wait on the very
1374 + // restart it is meant to unblock. Landing between the phases finds the container exited, which is
1375 + // turned away below like any other kill of a stopped container.
1376 + WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop, !RestartPhase && !Kill);
1377 +
1378 + // A Delete() that raced a restart may have already moved the container to the Deleted state.
1379 + THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_DELETED, Localization::MessageWslcContainerDeleted(m_id), m_state == WslcContainerStateDeleted);
1380
1381 transition = m_transition;
1382 WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop);
@@ -1397,6 +1442,14 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1442 // HTTP 304 is returned when the container is already stopped.
1443 if (Kill || e.StatusCode() != 304)
1444 {
1445 + lock = m_lock.lock_exclusive();
1446 +
1447 + // A force delete can win the locks released above, so the container may be gone rather than stuck.
1448 + THROW_HR_WITH_USER_ERROR_IF(
1449 + WSLC_E_CONTAINER_DELETED,
1450 + Localization::MessageWslcContainerDeleted(m_id),
1451 + m_state == WslcContainerStateDeleted || (m_transition && m_transition->ExpectedEvent == ContainerEvent::Destroy));
1452 +
1453 THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
1454 }
1455 }
@@ -1435,6 +1488,93 @@ void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1488 }
1489 }
1490
1491 +void WSLCContainerImpl::Restart(WSLCSignal Signal, LONG TimeoutSeconds)
1492 +{
1493 + // The stop phase is skipped when the container is not running, so it cannot be the only validation.
1494 + ValidateStopTimeout(TimeoutSeconds, true);
1495 +
1496 + bool wasRunning{};
1497 + auto restart = std::make_shared<RestartTransaction>();
1498 +
1499 + {
1500 + auto lifecycleLock = m_lifecycleLock.lock_shared();
1501 + auto lock = m_lock.lock_exclusive();
1502 + WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1503 +
1504 + wasRunning = m_state == WslcContainerStateRunning;
1505 +
1506 + // N.B. Stop() and Start() each take m_lock, so it cannot be held across both phases. m_restart
1507 + // stands them down until the start phase commits Running instead.
1508 + m_restart = restart;
1509 + }
1510 +
1511 + // N.B. Nothing between here and the cleanup below may throw — nothing clears m_restart until it is armed.
1512 + bool succeeded = false;
1513 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this, restart, &succeeded]() {
1514 + // N.B. Signalled last so a waiter cannot observe the restart as complete before the failure
1515 + // cleanup below has published its delete.
1516 + auto release = wil::scope_exit([&restart]() { restart->Completed.SetEvent(); });
1517 +
1518 + std::shared_ptr<StateTransition> transition;
1519 +
1520 + {
1521 + auto lifecycleLock = m_lifecycleLock.lock_shared();
1522 + auto lock = m_lock.lock_exclusive();
1523 +
1524 + // CommitState() clears this once the start phase lands, so a later restart may already own it.
1525 + if (m_restart == restart)
1526 + {
1527 + m_restart.reset();
1528 + }
1529 +
1530 + if (!succeeded)
1531 + {
1532 + transition = OnFailedRestartExclusiveLockHeld();
1533 + }
1534 + }
1535 +
1536 + if (transition)
1537 + {
1538 + AttachToTransition(transition);
1539 + }
1540 + });
1541 +
1542 + if (wasRunning)
1543 + {
1544 + StopPhase(Signal, TimeoutSeconds, false, true);
1545 + }
1546 +
1547 + StartPhase(WSLCContainerStartFlagsNone, nullptr, true);
1548 + succeeded = true;
1549 +}
1550 +
1551 +// N.B. Runs with m_restart already cleared, so the delete below is no longer suppressed by OnStopped().
1552 +__requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateTransition> WSLCContainerImpl::OnFailedRestartExclusiveLockHeld()
1553 +{
1554 + // The start phase waits for the start event after Docker has accepted the start, so it can throw
1555 + // on a container that is coming up. Leave that container alone; it still owns its resources.
1556 + if (m_transition || m_state == WslcContainerStateRunning)
1557 + {
1558 + return nullptr;
1559 + }
1560 +
1561 + // The stop phase held these back for a start phase that never landed.
1562 + if (m_runtimeResourcesHeld)
1563 + {
1564 + ReleaseRuntimeResources();
1565 + }
1566 +
1567 + if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm) || m_state != WslcContainerStateExited)
1568 + {
1569 + return nullptr;
1570 + }
1571 +
1572 + // N.B. Requested here rather than through Delete() so the removal shares the scope that clears
1573 + // m_restart, which is what stops a released Start() from bringing the container back up first.
1574 + RequestDeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
1575 + return StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1576 +}
1577 +
1578 __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::int64_t stopTime)
1579 {
1580 auto transition = m_transition;
@@ -1461,7 +1601,12 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
1601 }
1602
1603 ReleaseProcesses();
1464 - ReleaseRuntimeResources();
1604 +
1605 + // A restart's start phase relies on the container's ports and mounts still being held.
1606 + if (!m_restart)
1607 + {
1608 + ReleaseRuntimeResources();
1609 + }
1610
1611 // Ignore duplicate or late Stop events so they do not overwrite an already committed state.
1612 if (m_state == WslcContainerStateRunning)
@@ -1478,7 +1623,7 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exi
1623 }
1624
1625 // Stop with Rm must initiate Delete.
1481 - if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
1626 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm) && !m_restart)
1627 {
1628 try
1629 {
@@ -1552,7 +1697,10 @@ void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
1697 std::shared_ptr<StateTransition> transition;
1698 auto lifecycleLock = m_lifecycleLock.lock_shared();
1699 auto lock = m_lock.lock_exclusive();
1555 - WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1700 +
1701 + // N.B. Unlike Start() and Stop(), this deliberately does not stand down for an in-flight restart.
1702 + // A remove that lands between the two phases takes effect, and the restart's start phase fails.
1703 + WaitForConflictingTransitionToComplete(lock, lifecycleLock, std::nullopt, false);
1704
1705 RequestDeleteExclusiveLockHeld(Flags);
1706 transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
@@ -2902,6 +3050,8 @@ __requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::ReleaseRuntimeRes
3050 {
3051 WSL_LOG("ReleaseRuntimeResources", TraceLoggingValue(m_id.c_str(), "ID"));
3052
3053 + m_runtimeResourcesHeld = false;
3054 +
3055 // Release runtime resources (port relays, volume mounts) that were set up at Start().
3056 UnmapPorts();
3057
@@ -2972,6 +3122,12 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerSt
3122
3123 RecordEvent(WSLCStateToEventAction(State), Time, ExitCode);
3124
3125 + if (State == WslcContainerStateRunning)
3126 + {
3127 + // The restart's start phase landed, so a later exit must auto-delete an --rm container again.
3128 + m_restart.reset();
3129 + }
3130 +
3131 // Keep the VM alive while this container is Running and release the hold once it leaves that
3132 // state, even when no client holds the wrapper (e.g. a detached `run -d` container). Dropping
3133 // the hold on the transition out of Running is what lets an otherwise-idle VM be torn down; a
@@ -3110,6 +3266,18 @@ try
3266 }
3267 CATCH_RETURN();
3268
3269 +HRESULT WSLCContainer::Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, IWarningCallback* WarningCallback)
3270 +try
3271 +{
3272 + WSLCExecutionContext context(&m_session, WarningCallback);
3273 +
3274 + // Hold a VM lease across both phases: the container is not Running in between, so nothing else
3275 + // keeps the VM alive.
3276 + auto vmLease = m_session.Runtime().AcquireVmLease();
3277 + return CallImpl(&WSLCContainerImpl::Restart, Signal, TimeoutSeconds);
3278 +}
3279 +CATCH_RETURN();
3280 +
3281 HRESULT WSLCContainer::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, IWarningCallback* WarningCallback)
3282 try
3283 {
src/windows/wslcsession/WSLCContainer.h
+34 -2
@@ -99,6 +99,7 @@ public:
99 void Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions);
100 void Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle* Stdout, WSLCHandle* Stderr) const;
101 void Stop(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, bool Kill);
102 + void Restart(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds);
103 void Delete(WSLCDeleteFlags Flags);
104 void Export(WSLCHandle TarHandle) const;
105 void UploadArchive(WSLCHandle TarHandle, LPCSTR DestPath, ULONGLONG ContentSize) const;
@@ -181,6 +182,13 @@ private:
182 unique_com_disconnect Wrapper;
183 };
184
185 + // Restart() runs a stop phase followed by a start phase. This marks the pair as one transaction so
186 + // that Start() and Stop() cannot land in between.
187 + struct RestartTransaction
188 + {
189 + wil::unique_event Completed{wil::EventOptions::ManualReset};
190 + };
191 +
192 __requires_exclusive_lock_held(m_lock) void RequestDeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
193
194 void AllocateBridgedModePorts();
@@ -188,12 +196,24 @@ private:
196
197 __requires_exclusive_lock_held(m_lock) std::shared_ptr<StateTransition> StartTransition(TransitionKind kind, ContainerEvent expectedEvent);
198
191 - // Returns with both locks held when no transition is active or the active transition matches kind.
199 + // Returns with both locks held when no transition is active (or it matches kind) and, if waitForRestart,
200 + // no restart is in flight either. Both conditions are re-checked every time the locks come back.
201 void WaitForConflictingTransitionToComplete(
202 wil::rwlock_release_exclusive_scope_exit& lock,
203 wil::rwlock_release_shared_scope_exit& lifecycleLock,
195 - std::optional<TransitionKind> kind = std::nullopt);
204 + std::optional<TransitionKind> kind = std::nullopt,
205 + bool waitForRestart = true);
206
207 + // Phases of Restart(). Identical to Start() and Stop() except that they do not stand down for the
208 + // restart they are part of.
209 + void StartPhase(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions, bool RestartPhase);
210 + void StopPhase(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill, bool RestartPhase);
211 +
212 + // Undoes what the phases left half-done: releases the resources the stop phase held back and
213 + // requests the auto-delete OnStopped() deferred, returning that delete's transition.
214 + __requires_exclusive_lock_held(m_lock) std::shared_ptr<StateTransition> OnFailedRestartExclusiveLockHeld();
215 +
216 + void WaitForCompletionEvent(HANDLE Event) const;
217 void WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const;
218 void AttachToTransition(const std::shared_ptr<StateTransition>& transition) const;
219
@@ -239,6 +259,17 @@ private:
259
260 _Guarded_by_(m_lock) std::shared_ptr<StateTransition> m_transition;
261
262 + // Non-null from before Restart()'s stop phase until its start phase commits Running. Start() and
263 + // Stop() stand down for that window, and OnStopped() keeps the container's runtime resources mapped
264 + // and skips the auto-delete of an --rm container. Delete() does not stand down: a remove that lands
265 + // between the two phases takes effect, and the restart's start phase fails.
266 + _Guarded_by_(m_lock) std::shared_ptr<RestartTransaction> m_restart;
267 +
268 + // True between a successful StartPhase() and the release of the container's ports and mounts. A
269 + // restart leaves this set across the two phases, which is what tells the start phase they are still
270 + // held and must not be re-acquired.
271 + _Guarded_by_(m_lock) bool m_runtimeResourcesHeld = false;
272 +
273 // The container outlives any single VM: it survives idle-termination and is reused when the VM
274 // restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore
275 // fetched from the (stable) runtime at each use rather than cached, since a cached reference
@@ -279,6 +310,7 @@ public:
310
311 IFACEMETHOD(Attach)(_In_opt_ LPCSTR DetachKeys, _Out_ WSLCHandle* Stdin, _Out_ WSLCHandle* Stdout, _Out_ WSLCHandle* Stderr) override;
312 IFACEMETHOD(Stop)(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds) override;
313 + IFACEMETHOD(Restart)(_In_ WSLCSignal Signal, _In_ LONG TimeoutSeconds, _In_opt_ IWarningCallback* WarningCallback) override;
314 IFACEMETHOD(Kill)(_In_ WSLCSignal Signal) override;
315 IFACEMETHOD(Delete)(WSLCDeleteFlags Flags) override;
316 IFACEMETHOD(Export)(_In_ WSLCHandle TarHandle) override;
test/windows/WSLCTests.cpp
+253
@@ -4983,6 +4983,10 @@ class WSLCTests
4983 VERIFY_ARE_EQUAL(recoveredContainer->Start(WSLCContainerStartFlagsNone, nullptr, nullptr), WSLC_E_VOLUME_NOT_AVAILABLE);
4984 ValidateCOMErrorMessageContains(wsl::shared::string::MultiByteToWide(volumeName));
4985
4986 + // The container is not running, so the restart is only its start phase and is refused the same way.
4987 + VERIFY_ARE_EQUAL(recoveredContainer->Restart(WSLCSignalSIGTERM, 0, nullptr), WSLC_E_VOLUME_NOT_AVAILABLE);
4988 + ValidateCOMErrorMessageContains(wsl::shared::string::MultiByteToWide(volumeName));
4989 +
4990 // Inspecting the volume reports the failure via an "Error" entry in its status.
4991 {
4992 wil::unique_cotaskmem_ansistring inspectOutput;
@@ -6858,6 +6862,255 @@ class WSLCTests
6862 }
6863 }
6864
6865 + WSLC_TEST_METHOD(ContainerRestart)
6866 + {
6867 + // A running container is stopped and started again, replacing its init process.
6868 + {
6869 + WSLCContainerLauncher launcher("debian:latest", "test-restart-running", {"sleep", "99999"});
6870 + auto container = launcher.Launch(*m_defaultSession);
6871 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6872 +
6873 + auto initProcess = container.GetInitProcess();
6874 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr));
6875 +
6876 + VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128);
6877 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6878 + }
6879 +
6880 + // A created container has no stop phase.
6881 + {
6882 + WSLCContainerLauncher launcher("debian:latest", "test-restart-created", {"sleep", "99999"});
6883 + auto container = launcher.Create(*m_defaultSession);
6884 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateCreated);
6885 +
6886 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr));
6887 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6888 + }
6889 +
6890 + // An exited container is started again.
6891 + {
6892 + WSLCContainerLauncher launcher("debian:latest", "test-restart-exited", {"echo", "OK"});
6893 + auto container = launcher.Launch(*m_defaultSession);
6894 +
6895 + auto initProcess = container.GetInitProcess();
6896 + ValidateProcessOutput(initProcess, {{1, "OK\n"}});
6897 +
6898 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr));
6899 +
6900 + auto restartedProcess = container.GetInitProcess();
6901 + VERIFY_ARE_EQUAL(restartedProcess.Wait(), 0);
6902 +
6903 + COMOutputHandle stdoutLogs{};
6904 + COMOutputHandle stderrLogs{};
6905 + VERIFY_SUCCEEDED(container.Get().Logs(WSLCLogsFlagsNone, &stdoutLogs, &stderrLogs, 0, 0, 0));
6906 + ValidateHandleOutput(stdoutLogs.Get(), "OK\nOK\n");
6907 + }
6908 +
6909 + // Restarting a container with the autorm flag set must not auto-delete it, but a later stop must.
6910 + {
6911 + WSLCContainerLauncher launcher("debian:latest", "test-restart-autorm", {"sleep", "99999"});
6912 + launcher.SetContainerFlags(WSLCContainerFlagsRm | WSLCContainerFlagsInit);
6913 + auto container = launcher.Launch(*m_defaultSession);
6914 +
6915 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGTERM, WSLC_STOP_TIMEOUT_DEFAULT, nullptr));
6916 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6917 +
6918 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
6919 + VERIFY_ARE_EQUAL(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr), RPC_E_DISCONNECTED);
6920 + }
6921 +
6922 + // Validate that deleted containers can't be restarted.
6923 + {
6924 + WSLCContainerLauncher launcher("debian:latest", "test-restart-deleted", {"sleep", "99999"});
6925 + auto container = launcher.Launch(*m_defaultSession);
6926 +
6927 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
6928 + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsNone));
6929 +
6930 + VERIFY_ARE_EQUAL(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr), RPC_E_DISCONNECTED);
6931 + }
6932 +
6933 + // Ports and mounts survive a restart: they are held across both phases rather than released and re-acquired.
6934 + {
6935 + const auto hostFolder = std::filesystem::current_path() / "test-restart-volume";
6936 + std::filesystem::create_directories(hostFolder);
6937 + VERIFY_IS_TRUE((std::ofstream(hostFolder / "marker.txt") << "restart-marker").good());
6938 + auto folderCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
6939 + std::error_code ec;
6940 + std::filesystem::remove_all(hostFolder, ec);
6941 + });
6942 +
6943 + constexpr uint16_t hostPort = 1252;
6944 + const std::string containerPort = "8000/tcp";
6945 + const std::string volumePath = "/data";
6946 + const auto markerUrl = std::format(L"http://127.0.0.1:{}/marker.txt", hostPort);
6947 +
6948 + WSLCContainerLauncher launcher(
6949 + "python:3.12-alpine",
6950 + "test-restart-ports-volumes",
6951 + {"python3", "-m", "http.server", "8000", "--bind", "0.0.0.0", "--directory", volumePath},
6952 + {"PYTHONUNBUFFERED=1"},
6953 + "bridge");
6954 + launcher.AddPort(hostPort, 8000, AF_INET);
6955 + launcher.AddVolume(hostFolder.wstring(), volumePath, true);
6956 +
6957 + auto container = launcher.Launch(*m_defaultSession);
6958 + auto initProcess = container.GetInitProcess();
6959 + WaitForOutput(initProcess.GetStdHandle(1), "Serving HTTP on");
6960 + ExpectHttpResponse(markerUrl.c_str(), 200);
6961 +
6962 + // A start phase that re-reserved the host port would collide with the container's own reservation.
6963 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr));
6964 + VERIFY_ARE_EQUAL(initProcess.Wait(), WSLCSignalSIGKILL + 128);
6965 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
6966 +
6967 + const auto inspect = container.Inspect();
6968 + VERIFY_IS_TRUE(inspect.Ports.contains(containerPort));
6969 + VERIFY_ARE_EQUAL(inspect.Ports.at(containerPort).size(), 1u);
6970 + VERIFY_ARE_EQUAL(std::to_string(hostPort), inspect.Ports.at(containerPort)[0].HostPort);
6971 +
6972 + VERIFY_ARE_EQUAL(inspect.Mounts.size(), 1u);
6973 + VERIFY_ARE_EQUAL(inspect.Mounts[0].Destination, volumePath);
6974 + VERIFY_IS_FALSE(inspect.Mounts[0].ReadWrite);
6975 + VERIFY_ARE_EQUAL(inspect.Mounts[0].Type, "bind");
6976 +
6977 + // The restarted init has to bind again before the held relay has anything to forward to.
6978 + wsl::shared::retry::RetryWithTimeout<void>(
6979 + [&]() { ExpectHttpResponse(markerUrl.c_str(), 200); }, std::chrono::milliseconds(500), std::chrono::seconds(30));
6980 + }
6981 +
6982 + // An init that ignores SIGTERM keeps the restart's stop phase in flight until the timeout expires,
6983 + // which is what gives the requests below a window to land in the middle of a restart.
6984 + const std::vector<std::string> ignoreStopSignal = {
6985 + "/bin/sh", "-c", "trap 'echo stopping' TERM; while true; do sleep 1; done"};
6986 + const std::string stopSignalMarker = "stopping";
6987 + constexpr LONG stopTimeoutSeconds = 10;
6988 +
6989 + // A stop issued during a restart waits for both phases, so it can't be lost in between them.
6990 + {
6991 + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-stop", ignoreStopSignal);
6992 + auto container = launcher.Launch(*m_defaultSession);
6993 + auto initProcess = container.GetInitProcess();
6994 +
6995 + std::promise<HRESULT> restartResult;
6996 + std::thread restartThread(
6997 + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); });
6998 +
6999 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); });
7000 +
7001 + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker);
7002 +
7003 + VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
7004 + VERIFY_SUCCEEDED(restartResult.get_future().get());
7005 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7006 + }
7007 +
7008 + // A kill issued during a restart deliberately does not wait for it: it is what unblocks a stop phase
7009 + // that an init like this one would otherwise keep in flight for the whole timeout.
7010 + {
7011 + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-kill", ignoreStopSignal);
7012 + auto container = launcher.Launch(*m_defaultSession);
7013 + auto initProcess = container.GetInitProcess();
7014 +
7015 + std::promise<HRESULT> restartResult;
7016 + std::thread restartThread(
7017 + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); });
7018 +
7019 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); });
7020 +
7021 + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker);
7022 +
7023 + VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
7024 + VERIFY_SUCCEEDED(restartResult.get_future().get());
7025 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
7026 + }
7027 +
7028 + // A delete issued during a restart deliberately does not wait for it, matching docker: whichever of
7029 + // the delete and the restart's start phase lands first wins, and the other one fails.
7030 + {
7031 + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-delete", ignoreStopSignal);
7032 + auto container = launcher.Launch(*m_defaultSession);
7033 + auto initProcess = container.GetInitProcess();
7034 +
7035 + std::promise<HRESULT> restartResult;
7036 + std::thread restartThread(
7037 + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); });
7038 +
7039 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); });
7040 + auto restartFuture = restartResult.get_future();
7041 +
7042 + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker);
7043 +
7044 + // The gap between the two phases is short, so poll for it: until the container has exited, every
7045 + // delete is turned away by the ordinary running-container guard rather than by the restart.
7046 + const auto deleteResult = wsl::shared::retry::RetryWithTimeout<HRESULT>(
7047 + [&]() {
7048 + const auto result = container.Get().Delete(WSLCDeleteFlagsNone);
7049 + THROW_HR_IF(
7050 + WSLC_E_CONTAINER_IS_RUNNING,
7051 + result == WSLC_E_CONTAINER_IS_RUNNING &&
7052 + restartFuture.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready);
7053 + return result;
7054 + },
7055 + std::chrono::milliseconds(100),
7056 + std::chrono::seconds(30));
7057 +
7058 + const auto restartHr = restartFuture.get();
7059 +
7060 + if (SUCCEEDED(deleteResult))
7061 + {
7062 + VERIFY_ARE_EQUAL(restartHr, WSLC_E_CONTAINER_DELETED);
7063 + }
7064 + else
7065 + {
7066 + // The start phase closed the gap first, so the container was running again by the last attempt.
7067 + VERIFY_ARE_EQUAL(deleteResult, WSLC_E_CONTAINER_IS_RUNNING);
7068 + VERIFY_SUCCEEDED(restartHr);
7069 + }
7070 + }
7071 +
7072 + // A force delete is not turned away by the running-container guard, so unlike the delete above it does
7073 + // not have to wait for the gap between the phases: it lands while the stop phase is still in flight.
7074 + {
7075 + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-force-delete", ignoreStopSignal);
7076 + auto container = launcher.Launch(*m_defaultSession);
7077 + auto initProcess = container.GetInitProcess();
7078 +
7079 + std::promise<HRESULT> restartResult;
7080 + std::thread restartThread(
7081 + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); });
7082 +
7083 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); });
7084 +
7085 + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker);
7086 +
7087 + VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsForce));
7088 + container.SetDeleteOnClose(false);
7089 + VERIFY_ARE_EQUAL(restartResult.get_future().get(), WSLC_E_CONTAINER_DELETED);
7090 + }
7091 +
7092 + // A restart issued during a restart waits for both of the first one's phases, so the two pairs
7093 + // cannot interleave and the container is left running.
7094 + {
7095 + WSLCContainerLauncher launcher("debian:latest", "test-restart-race-restart", ignoreStopSignal);
7096 + auto container = launcher.Launch(*m_defaultSession);
7097 + auto initProcess = container.GetInitProcess();
7098 +
7099 + std::promise<HRESULT> restartResult;
7100 + std::thread restartThread(
7101 + [&]() { restartResult.set_value(container.Get().Restart(WSLCSignalSIGTERM, stopTimeoutSeconds, nullptr)); });
7102 +
7103 + auto joinThread = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { restartThread.join(); });
7104 +
7105 + WaitForOutput(initProcess.GetStdHandle(1), stopSignalMarker);
7106 +
7107 + // The first restart is still in its stop phase, so this one only returns once that pair is done.
7108 + VERIFY_SUCCEEDED(container.Get().Restart(WSLCSignalSIGKILL, 0, nullptr));
7109 + VERIFY_SUCCEEDED(restartResult.get_future().get());
7110 + VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
7111 + }
7112 + }
7113 +
7114 WSLC_TEST_METHOD(EventStream)
7115 {
7116 constexpr auto c_containerName = "wslc-test-events";