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";