Container lifecycle transitions (#41140)
Kevin Vega committed
Aug 24, 2026 at 18:25 UTC
3f01c37587136aad1c8e5c4212ab29fd08a34cec
6 files changed
+820
-192
src/windows/wslcsession/DockerEventTracker.cpp
+64
-31
@@ -157,7 +157,11 @@ void DockerEventTracker::OnEvent(const std::string_view& event)
157
void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime)
158
{
159
static std::map<std::string, ContainerEvent> events{
160
- {"start", ContainerEvent::Start}, {"die", ContainerEvent::Stop}, {"destroy", ContainerEvent::Destroy}, {"exec_die", ContainerEvent::ExecDied}};
160
+ {"start", ContainerEvent::Start},
161
+ {"die", ContainerEvent::Stop},
162
+ {"destroy", ContainerEvent::Destroy},
163
+ {"exec_die", ContainerEvent::ExecDied},
164
+ {"restart", ContainerEvent::Restart}};
165
166
auto actor = parsed.find("Actor");
167
THROW_HR_IF_MSG(E_INVALIDARG, actor == parsed.end(), "Missing Actor in container event");
@@ -191,15 +195,23 @@ void DockerEventTracker::OnContainerEvent(const nlohmann::json& parsed, const st
195
}
196
}
197
194
- std::lock_guard lock{m_lock};
195
-
196
- for (const auto& e : m_containerCallbacks)
198
+ // Snapshot the matching callbacks so that they can be invoked without holding m_lock. Callbacks can register and
199
+ // unregister callbacks (a container that stops releases its exec processes), which would otherwise mutate the
200
+ // vector being iterated.
201
+ std::vector<std::shared_ptr<ContainerCallback>> callbacks;
202
{
198
- if (e.ContainerId == containerId && (!e.ExecId.has_value() || e.ExecId == execId))
203
+ std::lock_guard lock{m_lock};
204
+
205
+ for (const auto& e : m_containerCallbacks)
206
{
200
- e.Callback(it->second, exitCode, eventTime);
207
+ if (e->ContainerId == containerId && (!e->ExecId.has_value() || e->ExecId == execId))
208
+ {
209
+ callbacks.emplace_back(e);
210
+ }
211
}
212
}
213
+
214
+ InvokeCallbacks(callbacks, [&](const ContainerCallback& e) { e.Callback(it->second, exitCode, eventTime); });
215
}
216
217
void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime)
@@ -220,12 +232,13 @@ void DockerEventTracker::OnVolumeEvent(const nlohmann::json& parsed, const std::
232
233
auto volumeName = id->get<std::string>();
234
223
- std::lock_guard lock{m_lock};
224
-
225
- for (const auto& e : m_volumeCallbacks)
235
+ std::vector<std::shared_ptr<VolumeCallback>> callbacks;
236
{
227
- e.Callback(volumeName, it->second, eventTime);
237
+ std::lock_guard lock{m_lock};
238
+ callbacks = m_volumeCallbacks;
239
}
240
+
241
+ InvokeCallbacks(callbacks, [&](const VolumeCallback& e) { e.Callback(volumeName, it->second, eventTime); });
242
}
243
244
void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId)
@@ -257,10 +270,11 @@ void DockerEventTracker::WaitForObjectCreated(const std::string& ObjectId)
270
DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainerStateUpdates(
271
const std::string& ContainerId, ContainerStateChangeCallback&& Callback) noexcept
272
{
260
- std::lock_guard lock{m_lock};
261
-
273
auto id = m_callbackId++;
263
- m_containerCallbacks.emplace_back(id, ContainerId, std::optional<std::string>{}, std::move(Callback));
274
+ auto entry = std::make_shared<ContainerCallback>(id, std::string{ContainerId}, std::optional<std::string>{}, std::move(Callback));
275
+
276
+ std::lock_guard lock{m_lock};
277
+ m_containerCallbacks.emplace_back(std::move(entry));
278
279
return EventTrackingReference{this, id};
280
}
@@ -268,39 +282,58 @@ DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterContainer
282
DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterExecStateUpdates(
283
const std::string& ContainerId, const std::string& ExecId, ContainerStateChangeCallback&& Callback) noexcept
284
{
271
- std::lock_guard lock{m_lock};
272
-
285
auto id = m_callbackId++;
274
- m_containerCallbacks.emplace_back(id, ContainerId, ExecId, std::move(Callback));
286
+ auto entry = std::make_shared<ContainerCallback>(id, std::string{ContainerId}, std::optional<std::string>{ExecId}, std::move(Callback));
287
+
288
+ std::lock_guard lock{m_lock};
289
+ m_containerCallbacks.emplace_back(std::move(entry));
290
291
return EventTrackingReference{this, id};
292
}
293
294
DockerEventTracker::EventTrackingReference DockerEventTracker::RegisterVolumeUpdates(VolumeEventCallback&& Callback) noexcept
295
{
281
- std::lock_guard lock{m_lock};
282
-
296
auto id = m_callbackId++;
284
- m_volumeCallbacks.emplace_back(id, std::move(Callback));
297
+ auto entry = std::make_shared<VolumeCallback>(id, std::move(Callback));
298
+
299
+ std::lock_guard lock{m_lock};
300
+ m_volumeCallbacks.emplace_back(std::move(entry));
301
302
return EventTrackingReference{this, id};
303
}
304
305
void DockerEventTracker::UnregisterCallback(size_t Id) noexcept
306
{
291
- std::lock_guard lock{m_lock};
307
+ std::shared_ptr<CallbackRegistration> registration;
308
293
- // Try container callbacks first.
294
- auto containerRemove = std::ranges::remove_if(m_containerCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
295
- if (!containerRemove.empty())
309
{
297
- WI_ASSERT(containerRemove.size() == 1);
298
- m_containerCallbacks.erase(containerRemove.begin(), containerRemove.end());
299
- return;
310
+ std::lock_guard lock{m_lock};
311
+
312
+ auto matches = [Id](const auto& e) { return e->CallbackId == Id; };
313
+
314
+ // Try container callbacks first, then volume callbacks.
315
+ if (auto container = std::ranges::find_if(m_containerCallbacks, matches); container != m_containerCallbacks.end())
316
+ {
317
+ registration = std::move(*container);
318
+ m_containerCallbacks.erase(container);
319
+ }
320
+ else
321
+ {
322
+ auto volume = std::ranges::find_if(m_volumeCallbacks, matches);
323
+ WI_ASSERT(volume != m_volumeCallbacks.end());
324
+
325
+ if (volume != m_volumeCallbacks.end())
326
+ {
327
+ registration = std::move(*volume);
328
+ m_volumeCallbacks.erase(volume);
329
+ }
330
+ }
331
}
332
302
- // Then volume callbacks.
303
- auto volumeRemove = std::ranges::remove_if(m_volumeCallbacks, [Id](auto& entry) { return entry.CallbackId == Id; });
304
- WI_ASSERT(volumeRemove.size() == 1);
305
- m_volumeCallbacks.erase(volumeRemove.begin(), volumeRemove.end());
306
-}
\ No newline at end of file
333
+ if (registration)
334
+ {
335
+ // Wait for any in-flight invocation to complete so the callback can't run once this returns.
336
+ std::lock_guard invokeLock{registration->InvokeLock};
337
+ registration->Unregistered = true;
338
+ }
339
+}
src/windows/wslcsession/DockerEventTracker.h
+52
-11
@@ -26,6 +26,7 @@ enum class ContainerEvent
26
{
27
Create,
28
Start,
29
+ Restart,
30
Stop,
31
Exit,
32
Destroy,
@@ -84,28 +85,68 @@ private:
85
void OnContainerEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
86
void OnVolumeEvent(const nlohmann::json& parsed, const std::string& action, std::int64_t eventTime);
87
87
- struct ContainerCallback
88
+ // Callbacks are invoked without holding m_lock so that a callback can register or unregister callbacks, and so
89
+ // that a callback taking its own lock can't invert with a thread that registers a callback under that same lock.
90
+ struct CallbackRegistration
91
{
89
- size_t CallbackId;
90
- std::string ContainerId;
91
- std::optional<std::string> ExecId;
92
- ContainerStateChangeCallback Callback;
92
+ NON_COPYABLE(CallbackRegistration);
93
+ NON_MOVABLE(CallbackRegistration);
94
+
95
+ CallbackRegistration(size_t Id) noexcept : CallbackId(Id)
96
+ {
97
+ }
98
+
99
+ const size_t CallbackId;
100
+
101
+ // Held while the callback runs so it can't be invoked once UnregisterCallback() returned for it.
102
+ // N.B. Recursive so a running callback can unregister itself.
103
+ std::recursive_mutex InvokeLock;
104
+ _Guarded_by_(InvokeLock) bool Unregistered = false;
105
};
106
95
- struct VolumeCallback
107
+ struct ContainerCallback : CallbackRegistration
108
{
97
- size_t CallbackId;
98
- VolumeEventCallback Callback;
109
+ ContainerCallback(size_t Id, std::string&& ContainerId, std::optional<std::string>&& ExecId, ContainerStateChangeCallback&& Callback) :
110
+ CallbackRegistration(Id), ContainerId(std::move(ContainerId)), ExecId(std::move(ExecId)), Callback(std::move(Callback))
111
+ {
112
+ }
113
+
114
+ const std::string ContainerId;
115
+ const std::optional<std::string> ExecId;
116
+ const ContainerStateChangeCallback Callback;
117
};
118
101
- std::vector<ContainerCallback> m_containerCallbacks;
102
- std::vector<VolumeCallback> m_volumeCallbacks;
119
+ struct VolumeCallback : CallbackRegistration
120
+ {
121
+ VolumeCallback(size_t Id, VolumeEventCallback&& Callback) : CallbackRegistration(Id), Callback(std::move(Callback))
122
+ {
123
+ }
124
+
125
+ const VolumeEventCallback Callback;
126
+ };
127
+
128
+ _Guarded_by_(m_lock) std::vector<std::shared_ptr<ContainerCallback>> m_containerCallbacks;
129
+ _Guarded_by_(m_lock) std::vector<std::shared_ptr<VolumeCallback>> m_volumeCallbacks;
130
+
131
+ // Invokes a snapshot of callbacks taken under m_lock, skipping registrations that have since been unregistered.
132
+ template <typename TCallback, typename TInvoke>
133
+ static void InvokeCallbacks(const std::vector<std::shared_ptr<TCallback>>& Callbacks, const TInvoke& Invoke)
134
+ {
135
+ for (const auto& e : Callbacks)
136
+ {
137
+ std::lock_guard invokeLock{e->InvokeLock};
138
+ if (!e->Unregistered)
139
+ {
140
+ Invoke(*e);
141
+ }
142
+ }
143
+ }
144
145
_Guarded_by_(m_lock) std::unordered_set<std::string> m_createdObjects;
146
_Guarded_by_(m_lock) wil::unique_event m_objectCreated { wil::EventOptions::ManualReset };
147
148
WSLCSession& m_session;
108
- std::recursive_mutex m_lock;
149
+ std::mutex m_lock;
150
std::atomic<size_t> m_callbackId{0};
151
};
152
} // namespace wsl::windows::service::wslc
\ No newline at end of file
src/windows/wslcsession/WSLCContainer.cpp
+266
-130
@@ -1049,8 +1049,10 @@ void WSLCContainerImpl::Attach(LPCSTR DetachKeys, WSLCHandle* Stdin, WSLCHandle*
1049
1050
void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessStartOptions* StartOptions)
1051
{
1052
- // Acquire an exclusive lock since this method modifies m_initProcessControl, m_initProcess and m_state.
1052
+ std::shared_ptr<StateTransition> transition;
1053
+ auto lifecycleLock = m_lifecycleLock.lock_shared();
1054
auto lock = m_lock.lock_exclusive();
1055
+ WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1056
1057
THROW_HR_WITH_USER_ERROR_IF(WSLC_E_CONTAINER_IS_RUNNING, Localization::MessageWslcContainerIsRunning(m_id), m_state == WslcContainerStateRunning);
1058
@@ -1102,12 +1104,14 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1104
1105
auto control = std::make_unique<DockerContainerProcessControl>(*this, m_runtime.Docker());
1106
1105
- std::lock_guard processesLock{m_processesLock};
1106
- m_initProcessControl = control.get();
1107
-
1108
- m_initProcess = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), m_initProcessFlags);
1107
+ {
1108
+ std::lock_guard processesLock{m_processesLock};
1109
+ m_initProcessControl = control.get();
1110
+ m_initProcess = wil::MakeOrThrow<WSLCProcess>(std::move(control), std::move(io), m_initProcessFlags);
1111
+ }
1112
1113
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() mutable {
1114
+ std::lock_guard processesLock{m_processesLock};
1115
m_initProcess.Reset();
1116
m_initProcessControl = nullptr;
1117
});
@@ -1134,9 +1138,6 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1138
auto portCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [this]() { UnmapPorts(); });
1139
MapPorts();
1140
1137
- m_stopNotification.Event.ResetEvent();
1138
- m_stopNotification.EventTime.store(0, std::memory_order_relaxed);
1139
-
1141
try
1142
{
1143
m_runtime.Docker().StartContainer(m_id, detachKeys);
@@ -1181,150 +1182,264 @@ void WSLCContainerImpl::Start(WSLCContainerStartFlags Flags, const WSLCProcessSt
1182
}
1183
}
1184
1185
+ transition = StartTransition(TransitionKind::Start, ContainerEvent::Start);
1186
+
1187
portCleanup.release();
1188
volumeCleanup.release();
1186
-
1187
- Transition(WslcContainerStateRunning);
1189
cleanup.release();
1190
+
1191
+ lock.reset();
1192
+ lifecycleLock.reset();
1193
+ AttachToTransition(transition);
1194
}
1195
1191
-void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime)
1196
+void WSLCContainerImpl::WaitForConflictingTransitionToComplete(
1197
+ wil::rwlock_release_exclusive_scope_exit& lock, wil::rwlock_release_shared_scope_exit& lifecycleLock, std::optional<TransitionKind> kind)
1198
{
1193
- // We must release m_lock and m_stopLock before the wrapper's destructor calls
1194
- // Disconnect(), so in-flight COM callers can drain from COMImplClass::m_callers.
1195
- unique_com_disconnect comWrapper;
1196
-
1197
- if (event == ContainerEvent::Stop)
1199
+ while (m_transition && (!kind.has_value() || m_transition->Kind != kind.value()))
1200
{
1199
- THROW_HR_IF(E_UNEXPECTED, !exitCode.has_value());
1200
- SetExitCode(exitCode.value());
1201
-
1202
- std::unique_lock stopGuard{m_stopLock, std::try_to_lock};
1203
-
1204
- m_stopNotification.EventTime.store(eventTime, std::memory_order_release);
1205
- m_stopNotification.Event.SetEvent();
1206
-
1207
- // If Stop() is already in flight, it will wake when the stop event is signaled and take care of cleanup.
1208
- if (!stopGuard.owns_lock())
1201
{
1210
- return;
1202
+ auto transition = m_transition;
1203
+ lock.reset();
1204
+ lifecycleLock.reset();
1205
+ WaitForTransitionCompletion(transition);
1206
}
1207
1213
- auto lock = m_lock.lock_exclusive();
1214
- comWrapper = OnStopped(eventTime);
1208
+ lifecycleLock = m_lifecycleLock.lock_shared();
1209
+ lock = m_lock.lock_exclusive();
1210
}
1216
- else if (event == ContainerEvent::Destroy)
1217
- {
1218
- WI_ASSERT(!m_destroyEvent.is_signaled());
1219
- m_destroyEvent.SetEvent();
1220
-
1221
- auto lock = m_lock.lock_exclusive();
1211
+}
1212
1223
- if (m_state != WslcContainerStateDeleted)
1224
- {
1225
- Transition(WslcContainerStateDeleted, eventTime);
1226
- comWrapper = ReleaseResources();
1227
- }
1213
+__requires_exclusive_lock_held(m_lock) std::shared_ptr<WSLCContainerImpl::StateTransition> WSLCContainerImpl::StartTransition(
1214
+ TransitionKind kind, ContainerEvent expectedEvent)
1215
+{
1216
+ auto transition = std::make_shared<StateTransition>(kind, expectedEvent);
1217
+ WI_ASSERT(!m_transition);
1218
+ m_transition = transition;
1219
+ return transition;
1220
+}
1221
1229
- // Signal init exit after the state transition so awaiters observe state=Deleted
1230
- // (and any post-delete cleanup) rather than the prior Running/Exited state.
1231
- SignalInitProcessExit();
1232
- }
1222
+void WSLCContainerImpl::WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const
1223
+{
1224
+ auto io = m_wslcSession.CreateIOContext();
1225
+ io.AddHandle(std::make_unique<EventHandle>(transition->Completed.get()));
1226
+ io.Run({});
1227
1234
- WSL_LOG(
1235
- "ContainerEvent",
1236
- TraceLoggingValue(m_name.c_str(), "Name"),
1237
- TraceLoggingValue(m_id.c_str(), "Id"),
1238
- TraceLoggingValue((int)event, "Event"));
1228
+ WI_ASSERT(transition->Completed.is_signaled());
1229
}
1230
1241
-void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1231
+void WSLCContainerImpl::AttachToTransition(const std::shared_ptr<StateTransition>& transition) const
1232
{
1243
- // N.B. comWrapper must be destructed after m_lock and m_stopLock are released.
1244
- unique_com_disconnect comWrapper;
1233
+ WaitForTransitionCompletion(transition);
1234
1246
- std::unique_lock stopGuard{m_stopLock};
1247
- auto lock = m_lock.lock_exclusive();
1235
+ unique_com_disconnect wrapper;
1236
1249
- if (m_state == WslcContainerStateExited && !Kill)
1250
- {
1251
- return;
1252
- }
1253
- else if (m_state != WslcContainerStateRunning)
1237
+ // Take ownership of the deferred COM disconnect after OnEvent leaves its critical section.
1238
{
1255
- THROW_HR_WITH_USER_ERROR_MSG(
1256
- WSLC_E_CONTAINER_NOT_RUNNING,
1257
- Localization::MessageWslcContainerNotRunning(m_id),
1258
- "Cannot stop container '%hs', state: %i",
1259
- m_id.c_str(),
1260
- m_state);
1239
+ auto lock = m_lock.lock_exclusive();
1240
+ wrapper = std::move(transition->Wrapper);
1241
}
1242
1263
- std::optional<WSLCSignal> SignalArg;
1264
- if (Signal != WSLCSignalNone)
1243
+ if (transition->Exception)
1244
{
1266
- SignalArg = Signal;
1245
+ std::rethrow_exception(transition->Exception);
1246
}
1247
+}
1248
1269
- ValidateStopTimeout(TimeoutSeconds, true);
1249
+__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::CompleteTransition(const std::shared_ptr<StateTransition>& transition, std::exception_ptr exception) noexcept
1250
+{
1251
+ WI_ASSERT(m_transition == transition);
1252
+ transition->Exception = std::move(exception);
1253
+ m_transition.reset();
1254
+ transition->Completed.SetEvent();
1255
+}
1256
1271
- // Don't wait for the container to stop if we're not sending SIGKILL, since it may not stop the container.
1272
- // N.B. If the signal was SIGTERM for instance, we'll receive the stop notification via OnEvent().
1273
- bool waitForStop = !Kill || (SignalArg.value_or(WSLCSignalSIGKILL) == WSLCSignalSIGKILL);
1257
+void WSLCContainerImpl::OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime) noexcept
1258
+{
1259
+ // Either owner may disconnect the COM wrapper, so both must outlive m_lock.
1260
+ unique_com_disconnect comWrapper;
1261
+ std::shared_ptr<StateTransition> transition;
1262
1275
- try
1263
{
1277
- if (Kill)
1278
- {
1279
- m_runtime.Docker().SignalContainer(m_id, SignalArg);
1264
+ auto lifecycleLock = m_lifecycleLock.lock_exclusive();
1265
+ auto lock = m_lock.lock_exclusive();
1266
+ transition = m_transition;
1267
1281
- if (!waitForStop)
1268
+ if (event == ContainerEvent::Start)
1269
+ {
1270
+ // Only WSLC should start the container, so if we receive a start event, it must be expected by a transition.
1271
+ // Otherwise the container was started externally. Log if the container was started externally.
1272
+ if (transition && transition->ExpectedEvent == ContainerEvent::Start)
1273
{
1283
- return;
1274
+ WI_ASSERT(m_state == WslcContainerStateCreated || m_state == WslcContainerStateExited);
1275
+ CommitState(WslcContainerStateRunning, eventTime);
1276
+ CompleteTransition(transition);
1277
+ }
1278
+ else
1279
+ {
1280
+ WSL_LOG("UnexpectedContainerStart", TraceLoggingValue(m_id.c_str(), "Id"));
1281
}
1282
}
1286
- else
1283
+ else if (event == ContainerEvent::Stop)
1284
{
1288
- std::optional<LONG> TimeoutArg;
1289
- if (TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT)
1285
+ WI_ASSERT(exitCode.has_value());
1286
+ OnStopped(exitCode.value(), eventTime);
1287
+ }
1288
+ else if (event == ContainerEvent::Destroy)
1289
+ {
1290
+ if (m_state != WslcContainerStateDeleted)
1291
{
1291
- TimeoutArg = TimeoutSeconds;
1292
+ CommitState(WslcContainerStateDeleted, eventTime);
1293
+ comWrapper = ReleaseResources();
1294
}
1295
1294
- m_runtime.Docker().StopContainer(m_id, SignalArg, TimeoutArg);
1296
+ // Signal init exit after the state transition and resource cleanup so awaiters observe Deleted.
1297
+ SignalInitProcessExit();
1298
+
1299
+ if (transition)
1300
+ {
1301
+ WI_ASSERT(transition->ExpectedEvent == ContainerEvent::Destroy);
1302
+
1303
+ // Let a COM caller waiting on this transition perform the disconnect, avoiding a deadlock with OnEvent.
1304
+ transition->Wrapper = std::move(comWrapper);
1305
+
1306
+ CompleteTransition(transition);
1307
+ }
1308
}
1309
+
1310
+ WSL_LOG(
1311
+ "ContainerEvent",
1312
+ TraceLoggingValue(m_name.c_str(), "Name"),
1313
+ TraceLoggingValue(m_id.c_str(), "Id"),
1314
+ TraceLoggingValue((int)event, "Event"));
1315
}
1297
- catch (const DockerHTTPException& e)
1316
+}
1317
+
1318
+void WSLCContainerImpl::Stop(WSLCSignal Signal, LONG TimeoutSeconds, bool Kill)
1319
+{
1320
+ std::shared_ptr<StateTransition> transition;
1321
+
1322
{
1299
- // HTTP 304 is returned when the container is already stopped.
1300
- if (Kill || e.StatusCode() != 304)
1323
+ auto lifecycleLock = m_lifecycleLock.lock_shared();
1324
+ auto lock = m_lock.lock_exclusive();
1325
+ WaitForConflictingTransitionToComplete(lock, lifecycleLock, TransitionKind::Stop);
1326
+
1327
+ transition = m_transition;
1328
+ WI_ASSERT(!transition || transition->Kind == TransitionKind::Stop);
1329
+
1330
+ // There can be an active stop transition post observing the exited state for cases where additional work needs to be done
1331
+ // after the container stopped: e.g. auto remove, restart, etc. Therefore, if there is an active stop transition, we still
1332
+ // need to attach to it below. This check simply skips creating a new transition once the state is already exited.
1333
+ if (!transition && m_state != WslcContainerStateRunning)
1334
+ {
1335
+ if (m_state == WslcContainerStateExited && !Kill)
1336
+ {
1337
+ return;
1338
+ }
1339
+
1340
+ THROW_HR_WITH_USER_ERROR_MSG(
1341
+ WSLC_E_CONTAINER_NOT_RUNNING,
1342
+ Localization::MessageWslcContainerNotRunning(m_id),
1343
+ "Cannot stop container '%hs', state: %i",
1344
+ m_id.c_str(),
1345
+ m_state);
1346
+ }
1347
+ // This check ensures WSLC does not call into docker if it has already observed the exited state. This prevents
1348
+ // conflicting with scenarios where work needs to be done after the container exits.
1349
+ else if (m_state == WslcContainerStateRunning)
1350
{
1302
- THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
1351
+ std::optional<WSLCSignal> SignalArg;
1352
+
1353
+ if (Signal != WSLCSignalNone)
1354
+ {
1355
+ SignalArg = Signal;
1356
+ }
1357
+
1358
+ ValidateStopTimeout(TimeoutSeconds, true);
1359
+
1360
+ // Don't wait for the container to stop if we're not sending SIGKILL, since it may not stop the container.
1361
+ // N.B. If the signal was SIGTERM for instance, we'll receive the stop notification via OnEvent().
1362
+ bool waitForStop = !Kill || (SignalArg.value_or(WSLCSignalSIGKILL) == WSLCSignalSIGKILL);
1363
+ const auto generation = m_stateGeneration;
1364
+
1365
+ lock.reset();
1366
+ lifecycleLock.reset();
1367
+
1368
+ try
1369
+ {
1370
+ if (Kill)
1371
+ {
1372
+ m_runtime.Docker().SignalContainer(m_id, SignalArg);
1373
+ }
1374
+ else
1375
+ {
1376
+ std::optional<LONG> TimeoutArg;
1377
+
1378
+ if (TimeoutSeconds != WSLC_STOP_TIMEOUT_DEFAULT)
1379
+ {
1380
+ TimeoutArg = TimeoutSeconds;
1381
+ }
1382
+
1383
+ m_runtime.Docker().StopContainer(m_id, SignalArg, TimeoutArg);
1384
+ }
1385
+ }
1386
+ catch (const DockerHTTPException& e)
1387
+ {
1388
+ // HTTP 304 is returned when the container is already stopped.
1389
+ if (Kill || e.StatusCode() != 304)
1390
+ {
1391
+ THROW_DOCKER_USER_ERROR_MSG(e, "Failed to %hs container '%hs'", Kill ? "kill" : "stop", m_id.c_str());
1392
+ }
1393
+ }
1394
+
1395
+ if (waitForStop)
1396
+ {
1397
+ lock = m_lock.lock_exclusive();
1398
+ transition = m_transition;
1399
+
1400
+ // The container can exit and start again while the locks are released, so an unchanged generation is
1401
+ // the only proof that the stop event this call is waiting for is still to come.
1402
+ if (m_stateGeneration == generation)
1403
+ {
1404
+ if (!transition)
1405
+ {
1406
+ transition = StartTransition(TransitionKind::Stop, ContainerEvent::Stop);
1407
+ }
1408
+ }
1409
+ // The run already ended: keep waiting on the work it triggered (e.g. auto-remove), never on a start
1410
+ // that raced in behind it.
1411
+ else if (transition && transition->Kind == TransitionKind::Start)
1412
+ {
1413
+ transition.reset();
1414
+ }
1415
+ }
1416
+ else
1417
+ {
1418
+ transition.reset();
1419
+ }
1420
}
1421
}
1422
1306
- // Wait for the stop event to get the Docker timestamp.
1307
- std::optional<std::int64_t> stopTimestamp;
1308
- if (m_wslcSession.WaitForEventOrSessionTerminating(m_stopNotification.Event.get(), 60s))
1423
+ if (transition)
1424
{
1310
- stopTimestamp = m_stopNotification.EventTime.load(std::memory_order_acquire);
1425
+ AttachToTransition(transition);
1426
}
1427
+}
1428
1313
- comWrapper = OnStopped(stopTimestamp);
1429
+__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp)
1430
+{
1431
+ auto transition = m_transition;
1432
1315
- if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
1433
+ // A Stop while expecting Start should not occur normally: Docker emits start before die, and the event stream processes
1434
+ // them serially. It would indicate external manipulation. Ignoring it avoids applying an old exit code to the newly
1435
+ // staged init process.
1436
+ if (transition && (transition->ExpectedEvent == ContainerEvent::Start))
1437
{
1317
- // Release locks before waiting on the docker destroy event: OnEvent(Destroy) takes m_lock,
1318
- // and the wrapper's destructor (Disconnect) must run after locks are released.
1319
- lock.reset();
1320
- stopGuard.unlock();
1321
- m_wslcSession.WaitForEventOrSessionTerminating(m_destroyEvent.get(), 60s);
1438
+ WSL_LOG("UnexpectedContainerExit", TraceLoggingValue(m_id.c_str(), "Id"), TraceLoggingValue(exitCode, "ExitCode"));
1439
+ return;
1440
}
1323
-}
1441
1325
-__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::OnStopped(std::optional<std::int64_t> stopTimestamp)
1326
-{
1327
- unique_com_disconnect comWrapper;
1442
+ SetExitCode(exitCode);
1443
1444
// Notify plugin manager that the container is stopping. Errors are ignored.
1445
if (m_state == WslcContainerStateRunning)
@@ -1339,26 +1454,51 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
1454
ReleaseProcesses();
1455
ReleaseRuntimeResources();
1456
1342
- // Only drive state transition + auto-delete if we're still Running. A concurrent
1343
- // Delete() may have already moved us to Deleted.
1457
+ // Ignore duplicate or late Stop events so they do not overwrite an already committed state.
1458
if (m_state == WslcContainerStateRunning)
1459
{
1346
- Transition(WslcContainerStateExited, stopTimestamp);
1460
+ CommitState(WslcContainerStateExited, stopTimestamp);
1461
+ }
1462
+
1463
+ std::exception_ptr transitionException;
1464
+
1465
+ // Docker delete request is already sent.
1466
+ if (transition && transition->ExpectedEvent == ContainerEvent::Destroy)
1467
+ {
1468
+ return;
1469
+ }
1470
+
1471
+ // Stop with Rm must initiate Delete.
1472
+ if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
1473
+ {
1474
+ try
1475
+ {
1476
+ m_runtime.Docker().DeleteContainer(m_id, true, true);
1477
+
1478
+ if (transition)
1479
+ {
1480
+ transition->ExpectedEvent = ContainerEvent::Destroy;
1481
+ }
1482
+ else
1483
+ {
1484
+ transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1485
+ }
1486
1348
- if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsRm))
1487
+ return;
1488
+ }
1489
+ catch (...)
1490
{
1350
- comWrapper = DeleteExclusiveLockHeld(WSLCDeleteFlagsForce | WSLCDeleteFlagsDeleteVolumes);
1491
+ transitionException = std::current_exception();
1492
+ LOG_CAUGHT_EXCEPTION_MSG("Failed to remove container '%hs'", m_id.c_str());
1493
}
1494
}
1495
1354
- // For the Rm path, defer init-exit signaling to OnEvent(Destroy) so callers waiting
1355
- // on init exit observe destroy-side cleanup first.
1356
- if (WI_IsFlagClear(m_containerFlags, WSLCContainerFlagsRm))
1496
+ SignalInitProcessExit();
1497
+
1498
+ if (transition)
1499
{
1358
- SignalInitProcessExit();
1500
+ CompleteTransition(transition, std::move(transitionException));
1501
}
1360
-
1361
- return comWrapper;
1502
}
1503
1504
void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer)
@@ -1400,22 +1540,20 @@ void WSLCContainerImpl::RecoverPorts(const common::docker_schema::ContainerInfo&
1540
1541
void WSLCContainerImpl::Delete(WSLCDeleteFlags Flags)
1542
{
1403
- // N.B. wrapper must be destroyed after m_lock is released, since its destructor calls Disconnect().
1404
- unique_com_disconnect wrapper;
1405
- {
1406
- auto lock = m_lock.lock_exclusive();
1407
- wrapper = DeleteExclusiveLockHeld(Flags);
1408
- }
1543
+ std::shared_ptr<StateTransition> transition;
1544
+ auto lifecycleLock = m_lifecycleLock.lock_shared();
1545
+ auto lock = m_lock.lock_exclusive();
1546
+ WaitForConflictingTransitionToComplete(lock, lifecycleLock);
1547
1410
- // Wait for the docker destroy event so anonymous volume cleanup is reflected in tracking by
1411
- // the time we return.
1412
- if (WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes))
1413
- {
1414
- m_wslcSession.WaitForEventOrSessionTerminating(m_destroyEvent.get(), 60s);
1415
- }
1548
+ RequestDeleteExclusiveLockHeld(Flags);
1549
+ transition = StartTransition(TransitionKind::Delete, ContainerEvent::Destroy);
1550
+
1551
+ lock.reset();
1552
+ lifecycleLock.reset();
1553
+ AttachToTransition(transition);
1554
}
1555
1418
-__requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::DeleteExclusiveLockHeld(WSLCDeleteFlags Flags)
1556
+__requires_exclusive_lock_held(m_lock) void WSLCContainerImpl::RequestDeleteExclusiveLockHeld(WSLCDeleteFlags Flags)
1557
{
1558
// Validate that the container is not running or already deleted.
1559
THROW_HR_WITH_USER_ERROR_IF(
@@ -1433,9 +1571,6 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
1571
m_runtime.Docker().DeleteContainer(m_id, WI_IsFlagSet(Flags, WSLCDeleteFlagsForce), WI_IsFlagSet(Flags, WSLCDeleteFlagsDeleteVolumes));
1572
}
1573
CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to delete container '%hs'", m_id.c_str());
1436
-
1437
- Transition(WslcContainerStateDeleted);
1438
- return ReleaseResources();
1574
}
1575
1576
void WSLCContainerImpl::Export(WSLCHandle OutHandle) const
@@ -2816,7 +2951,7 @@ __requires_exclusive_lock_held(m_lock) unique_com_disconnect WSLCContainerImpl::
2951
return unique_com_disconnect{std::exchange(m_comWrapper, nullptr)};
2952
}
2953
2819
-__requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt) noexcept
2954
+__requires_lock_held(m_lock) void WSLCContainerImpl::CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt) noexcept
2955
{
2956
// N.B. A deleted container cannot transition back to any other state.
2957
WI_ASSERT(m_state != WslcContainerStateDeleted);
@@ -2828,6 +2963,7 @@ __requires_lock_held(m_lock) void WSLCContainerImpl::Transition(WSLCContainerSta
2963
TraceLoggingValue(m_id.c_str(), "ID"));
2964
2965
m_state = State;
2966
+ m_stateGeneration++;
2967
m_stateChangedAt = stateChangedAt.value_or(static_cast<std::int64_t>(std::time(nullptr)));
2968
2969
// Keep the VM alive while this container is Running and release the hold once it leaves that
src/windows/wslcsession/WSLCContainer.h
+47
-15
@@ -123,7 +123,7 @@ public:
123
// Re-registers a stopped container's VM-scoped port allocations against the restarted VM.
124
void RecoverPorts(const common::docker_schema::ContainerInfo& dockerContainer);
125
126
- __requires_lock_held(m_lock) void Transition(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt = std::nullopt) noexcept;
126
+ __requires_lock_held(m_lock) void CommitState(WSLCContainerState State, std::optional<std::int64_t> stateChangedAt = std::nullopt) noexcept;
127
128
const std::string& ID() const noexcept;
129
@@ -151,17 +151,53 @@ public:
151
std::function<void(const WSLCContainerImpl*)>&& OnDeleted);
152
153
private:
154
- __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect DeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
154
+ enum class TransitionKind
155
+ {
156
+ Start,
157
+ Stop,
158
+ Delete
159
+ };
160
+
161
+ struct StateTransition
162
+ {
163
+ StateTransition(TransitionKind kind, ContainerEvent expectedEvent) : Kind(kind), ExpectedEvent(expectedEvent)
164
+ {
165
+ }
166
+
167
+ const TransitionKind Kind;
168
+ wil::unique_event Completed{wil::EventOptions::ManualReset};
169
+ std::exception_ptr Exception;
170
+
171
+ // Access under WSLCContainerImpl::m_lock.
172
+ ContainerEvent ExpectedEvent;
173
+ unique_com_disconnect Wrapper;
174
+ };
175
+
176
+ __requires_exclusive_lock_held(m_lock) void RequestDeleteExclusiveLockHeld(WSLCDeleteFlags Flags);
177
178
void AllocateBridgedModePorts();
157
- void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime);
179
+ void OnEvent(ContainerEvent event, std::optional<int> exitCode, std::int64_t eventTime) noexcept;
180
+
181
+ __requires_exclusive_lock_held(m_lock) std::shared_ptr<StateTransition> StartTransition(TransitionKind kind, ContainerEvent expectedEvent);
182
+
183
+ // Returns with both locks held when no transition is active or the active transition matches kind.
184
+ void WaitForConflictingTransitionToComplete(
185
+ wil::rwlock_release_exclusive_scope_exit& lock,
186
+ wil::rwlock_release_shared_scope_exit& lifecycleLock,
187
+ std::optional<TransitionKind> kind = std::nullopt);
188
+
189
+ void WaitForTransitionCompletion(const std::shared_ptr<StateTransition>& transition) const;
190
+ void AttachToTransition(const std::shared_ptr<StateTransition>& transition) const;
191
+
192
+ __requires_exclusive_lock_held(m_lock) void CompleteTransition(
193
+ const std::shared_ptr<StateTransition>& transition, std::exception_ptr exception = {}) noexcept;
194
195
__requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect ReleaseResources();
196
__requires_exclusive_lock_held(m_lock) void ReleaseRuntimeResources();
197
__requires_exclusive_lock_held(m_lock) void ReleaseProcesses();
198
__requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect PrepareDisconnectComWrapper();
199
164
- __requires_exclusive_lock_held(m_lock) [[nodiscard]] unique_com_disconnect OnStopped(std::optional<std::int64_t> stopTimestamp);
200
+ __requires_exclusive_lock_held(m_lock) void OnStopped(int exitCode, std::optional<std::int64_t> stopTimestamp);
201
202
void SetExitCode(int ExitCode) noexcept;
203
void SignalInitProcessExit() noexcept;
@@ -179,6 +215,9 @@ private:
215
216
__requires_shared_lock_held(m_lock) std::string InspectLockHeld() const;
217
218
+ // Lifecycle requests hold this shared until their transitions are published; event delivery holds it exclusively.
219
+ // N.B. Stop releases it across the docker request, which can block indefinitely, and re-checks m_stateGeneration instead.
220
+ wil::srwlock m_lifecycleLock;
221
mutable wil::srwlock m_lock;
222
std::string m_name;
223
std::string m_image;
@@ -190,17 +229,7 @@ private:
229
__guarded_by(m_processesLock) Microsoft::WRL::ComPtr<IWSLCProcess> m_initProcess;
230
__guarded_by(m_processesLock) DockerContainerProcessControl* m_initProcessControl = nullptr;
231
193
- struct StopNotification
194
- {
195
- std::atomic<std::int64_t> EventTime{0};
196
- wil::unique_event Event{wil::EventOptions::None};
197
- } m_stopNotification;
198
-
199
- wil::unique_event m_destroyEvent{wil::EventOptions::ManualReset};
200
-
201
- // Serializes Stop() callers and signals OnEvent that a Stop is in flight.
202
- // Must be acquired before m_lock when both are needed.
203
- std::mutex m_stopLock;
232
+ _Guarded_by_(m_lock) std::shared_ptr<StateTransition> m_transition;
233
234
// The container outlives any single VM: it survives idle-termination and is reused when the VM
235
// restarts. VM-scoped resources (Vm(), Docker(), Volumes(), Events(), Relay()) are therefore
@@ -210,6 +239,9 @@ private:
239
std::int64_t m_stateChangedAt{static_cast<std::int64_t>(std::time(nullptr))};
240
std::int64_t m_createdAt{};
241
WSLCContainerState m_state = WslcContainerStateInvalid;
242
+
243
+ // Bumped on every state change so a thread that released m_lock can detect a state cycle, not just a difference.
244
+ std::uint64_t m_stateGeneration{};
245
WSLCSession& m_wslcSession;
246
IWSLCPluginNotifier* m_pluginNotifier;
247
std::vector<ContainerPortMapping> m_mappedPorts;
test/windows/PluginTests.cpp
+1
@@ -780,6 +780,7 @@ class PluginTests
780
WSLC Image created, session=*, id=sha256:*, name=wslc-registry:latest
781
WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running
782
WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest
783
+ WSLC Container stopping, session=*, id=*
784
WSLC Session stopping, name=plugin-wslc-pull-test, id=*)";
785
786
ValidateLogFile(ExpectedOutput);
test/windows/WSLCTests.cpp
+390
-5
@@ -28,6 +28,7 @@ Abstract:
28
29
using namespace std::literals::chrono_literals;
30
using namespace wsl::windows::common::registry;
31
+using wsl::windows::common::ClientRunningWSLCProcess;
32
using wsl::windows::common::RunningWSLCContainer;
33
using wsl::windows::common::RunningWSLCProcess;
34
using wsl::windows::common::WSLCContainerLauncher;
@@ -206,6 +207,19 @@ class WSLCTests
207
return RunningWSLCContainer(std::move(rawContainer), {});
208
}
209
210
+ RunningWSLCContainer LaunchContainerWithBlockingStopHandler(const std::string& name)
211
+ {
212
+ WSLCContainerLauncher launcher(
213
+ "debian:latest",
214
+ name,
215
+ {"/bin/sh", "-c", "trap 'echo stopping; read value; exit 0' TERM; echo ready; while true; do sleep 1; done"},
216
+ {},
217
+ "host",
218
+ WSLCProcessFlagsStdin);
219
+
220
+ return launcher.Launch(*m_defaultSession);
221
+ }
222
+
223
struct ListContainersResult
224
{
225
wil::unique_cotaskmem_array_ptr<WSLCContainerEntry> Containers;
@@ -7104,11 +7118,7 @@ class WSLCTests
7118
std::thread stopThread([&]() { VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalNone, -1)); });
7119
7120
auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7107
- // TODO: calling Kill() here hangs since Stop() holds the container lock.
7108
- // Update this once fixed to:
7109
- // LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGKILL));
7110
-
7111
- LOG_IF_FAILED(initProcess.Get().Signal(WSLCSignalSIGKILL));
7121
+ LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGKILL));
7122
7123
if (stopThread.joinable())
7124
{
@@ -7344,6 +7354,301 @@ class WSLCTests
7354
}
7355
}
7356
7357
+ WSLC_TEST_METHOD(ConcurrentContainerStopAndKill)
7358
+ {
7359
+ auto container = LaunchContainerWithBlockingStopHandler("test-concurrent-container-stops");
7360
+ auto initProcess = container.GetInitProcess();
7361
+ auto input = initProcess.GetStdHandle(0);
7362
+ auto outputHandle = initProcess.GetStdHandle(1);
7363
+ PartialHandleRead output{outputHandle.get()};
7364
+ output.ExpectConsume("ready\n");
7365
+
7366
+ HRESULT stopResult{};
7367
+ HRESULT killResult{};
7368
+ std::thread stopThread;
7369
+ std::thread killThread;
7370
+
7371
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7372
+ input.reset();
7373
+
7374
+ if (stopThread.joinable())
7375
+ {
7376
+ stopThread.join();
7377
+ }
7378
+
7379
+ if (killThread.joinable())
7380
+ {
7381
+ killThread.join();
7382
+ }
7383
+ });
7384
+
7385
+ stopThread = std::thread([&]() { stopResult = container.Get().Stop(WSLCSignalSIGTERM, WSLC_STOP_TIMEOUT_NONE); });
7386
+
7387
+ output.ExpectConsume("stopping\n");
7388
+
7389
+ // A second lifecycle request must reach Docker while the indefinite Stop request is blocked.
7390
+ wil::unique_event killStarted{wil::EventOptions::ManualReset};
7391
+ killThread = std::thread([&]() {
7392
+ killStarted.SetEvent();
7393
+ killResult = container.Get().Kill(WSLCSignalSIGTERM);
7394
+ });
7395
+
7396
+ VERIFY_IS_TRUE(killStarted.wait(30 * 1000));
7397
+ VERIFY_ARE_EQUAL(WaitForSingleObject(killThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7398
+ killThread.join();
7399
+ VERIFY_SUCCEEDED(killResult);
7400
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 100), WAIT_TIMEOUT);
7401
+
7402
+ const char stopInput = '\n';
7403
+ DWORD bytesWritten{};
7404
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(input.get(), &stopInput, sizeof(stopInput), &bytesWritten, nullptr));
7405
+ VERIFY_ARE_EQUAL(bytesWritten, static_cast<DWORD>(sizeof(stopInput)));
7406
+ input.reset();
7407
+
7408
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7409
+
7410
+ stopThread.join();
7411
+ cleanup.release();
7412
+
7413
+ VERIFY_SUCCEEDED(stopResult);
7414
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7415
+ }
7416
+
7417
+ WSLC_TEST_METHOD(ConcurrentContainerStopAndIgnoredSignal)
7418
+ {
7419
+ // The init process blocks in its SIGTERM handler and only logs SIGUSR1, so the Stop stays pending
7420
+ // across the Kill instead of being completed by it.
7421
+ WSLCContainerLauncher launcher(
7422
+ "debian:latest",
7423
+ "test-concurrent-stop-ignored-signal",
7424
+ {"/bin/sh",
7425
+ "-c",
7426
+ "trap 'echo stopping; while true; do sleep 1; done' TERM; trap 'echo signaled' USR1; echo ready; while true; do "
7427
+ "sleep 1; done"},
7428
+ {},
7429
+ "host");
7430
+
7431
+ auto container = launcher.Launch(*m_defaultSession);
7432
+ auto initProcess = container.GetInitProcess();
7433
+ auto outputHandle = initProcess.GetStdHandle(1);
7434
+ PartialHandleRead output{outputHandle.get()};
7435
+ output.ExpectConsume("ready\n");
7436
+
7437
+ HRESULT stopResult{};
7438
+ HRESULT killResult{};
7439
+ std::thread stopThread;
7440
+ std::thread killThread;
7441
+
7442
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7443
+ LOG_IF_FAILED(container.Get().Kill(WSLCSignalSIGKILL));
7444
+
7445
+ if (stopThread.joinable())
7446
+ {
7447
+ stopThread.join();
7448
+ }
7449
+
7450
+ if (killThread.joinable())
7451
+ {
7452
+ killThread.join();
7453
+ }
7454
+ });
7455
+
7456
+ stopThread = std::thread([&]() { stopResult = container.Get().Stop(WSLCSignalSIGTERM, WSLC_STOP_TIMEOUT_NONE); });
7457
+
7458
+ output.ExpectConsume("stopping\n");
7459
+
7460
+ // A signal that doesn't stop the container must not wait on the Stop it raced.
7461
+ killThread = std::thread([&]() { killResult = container.Get().Kill(WSLCSignalSIGUSR1); });
7462
+
7463
+ VERIFY_ARE_EQUAL(WaitForSingleObject(killThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7464
+ killThread.join();
7465
+ VERIFY_SUCCEEDED(killResult);
7466
+
7467
+ // The signal reached the container, and the Stop is still pending.
7468
+ output.ExpectConsume("signaled\n");
7469
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 100), WAIT_TIMEOUT);
7470
+
7471
+ VERIFY_SUCCEEDED(container.Get().Kill(WSLCSignalSIGKILL));
7472
+
7473
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7474
+ stopThread.join();
7475
+ cleanup.release();
7476
+
7477
+ VERIFY_SUCCEEDED(stopResult);
7478
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7479
+ }
7480
+
7481
+ WSLC_TEST_METHOD(ConcurrentContainerStopTimeoutOverride)
7482
+ {
7483
+ auto container = LaunchContainerWithBlockingStopHandler("test-concurrent-stop-timeout");
7484
+ auto initProcess = container.GetInitProcess();
7485
+ auto input = initProcess.GetStdHandle(0);
7486
+ auto outputHandle = initProcess.GetStdHandle(1);
7487
+ PartialHandleRead output{outputHandle.get()};
7488
+ output.ExpectConsume("ready\n");
7489
+
7490
+ HRESULT indefiniteStopResult{};
7491
+ HRESULT immediateStopResult{};
7492
+ std::thread indefiniteStopThread;
7493
+ std::thread immediateStopThread;
7494
+
7495
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7496
+ input.reset();
7497
+
7498
+ if (indefiniteStopThread.joinable())
7499
+ {
7500
+ indefiniteStopThread.join();
7501
+ }
7502
+
7503
+ if (immediateStopThread.joinable())
7504
+ {
7505
+ immediateStopThread.join();
7506
+ }
7507
+ });
7508
+
7509
+ indefiniteStopThread =
7510
+ std::thread([&]() { indefiniteStopResult = container.Get().Stop(WSLCSignalNone, WSLC_STOP_TIMEOUT_NONE); });
7511
+
7512
+ output.ExpectConsume("stopping\n");
7513
+ VERIFY_ARE_EQUAL(WaitForSingleObject(indefiniteStopThread.native_handle(), 100), WAIT_TIMEOUT);
7514
+
7515
+ immediateStopThread = std::thread([&]() { immediateStopResult = container.Get().Stop(WSLCSignalNone, 0); });
7516
+
7517
+ VERIFY_ARE_EQUAL(WaitForSingleObject(immediateStopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7518
+ VERIFY_ARE_EQUAL(WaitForSingleObject(indefiniteStopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7519
+
7520
+ indefiniteStopThread.join();
7521
+ immediateStopThread.join();
7522
+ cleanup.release();
7523
+
7524
+ VERIFY_SUCCEEDED(indefiniteStopResult);
7525
+ VERIFY_SUCCEEDED(immediateStopResult);
7526
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7527
+ }
7528
+
7529
+ WSLC_TEST_METHOD(ConcurrentContainerStopAndStart)
7530
+ {
7531
+ auto container = LaunchContainerWithBlockingStopHandler("test-concurrent-stop-start");
7532
+ auto initProcess = container.GetInitProcess();
7533
+ auto input = initProcess.GetStdHandle(0);
7534
+ auto outputHandle = initProcess.GetStdHandle(1);
7535
+ PartialHandleRead output{outputHandle.get()};
7536
+ output.ExpectConsume("ready\n");
7537
+
7538
+ HRESULT stopResult{};
7539
+ HRESULT startResult{};
7540
+ std::thread stopThread;
7541
+ std::thread startThread;
7542
+
7543
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7544
+ input.reset();
7545
+
7546
+ if (stopThread.joinable())
7547
+ {
7548
+ stopThread.join();
7549
+ }
7550
+
7551
+ if (startThread.joinable())
7552
+ {
7553
+ startThread.join();
7554
+ }
7555
+ });
7556
+
7557
+ stopThread = std::thread([&]() { stopResult = container.Get().Stop(WSLCSignalNone, WSLC_STOP_TIMEOUT_NONE); });
7558
+
7559
+ output.ExpectConsume("stopping\n");
7560
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 100), WAIT_TIMEOUT);
7561
+
7562
+ startThread = std::thread([&]() { startResult = container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr); });
7563
+
7564
+ VERIFY_ARE_EQUAL(WaitForSingleObject(startThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7565
+ startThread.join();
7566
+ VERIFY_ARE_EQUAL(startResult, WSLC_E_CONTAINER_IS_RUNNING);
7567
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 100), WAIT_TIMEOUT);
7568
+
7569
+ const char stopInput = '\n';
7570
+ DWORD bytesWritten{};
7571
+ VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(input.get(), &stopInput, sizeof(stopInput), &bytesWritten, nullptr));
7572
+ VERIFY_ARE_EQUAL(bytesWritten, static_cast<DWORD>(sizeof(stopInput)));
7573
+ input.reset();
7574
+
7575
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7576
+ stopThread.join();
7577
+ cleanup.release();
7578
+
7579
+ VERIFY_SUCCEEDED(stopResult);
7580
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
7581
+ }
7582
+
7583
+ WSLC_TEST_METHOD(ConcurrentContainerStopAndForceDelete)
7584
+ {
7585
+ auto container = LaunchContainerWithBlockingStopHandler("test-concurrent-stop-delete");
7586
+ auto initProcess = container.GetInitProcess();
7587
+ auto input = initProcess.GetStdHandle(0);
7588
+ auto outputHandle = initProcess.GetStdHandle(1);
7589
+ PartialHandleRead output{outputHandle.get()};
7590
+ output.ExpectConsume("ready\n");
7591
+
7592
+ HRESULT stopResult{};
7593
+ HRESULT deleteResult{};
7594
+ std::thread stopThread;
7595
+ std::thread deleteThread;
7596
+
7597
+ auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() {
7598
+ input.reset();
7599
+
7600
+ if (stopThread.joinable())
7601
+ {
7602
+ stopThread.join();
7603
+ }
7604
+
7605
+ if (deleteThread.joinable())
7606
+ {
7607
+ deleteThread.join();
7608
+ }
7609
+ });
7610
+
7611
+ stopThread = std::thread([&]() { stopResult = container.Get().Stop(WSLCSignalNone, WSLC_STOP_TIMEOUT_NONE); });
7612
+
7613
+ output.ExpectConsume("stopping\n");
7614
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 100), WAIT_TIMEOUT);
7615
+
7616
+ deleteThread = std::thread([&]() { deleteResult = container.Get().Delete(WSLCDeleteFlagsForce); });
7617
+
7618
+ VERIFY_ARE_EQUAL(WaitForSingleObject(deleteThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7619
+ VERIFY_ARE_EQUAL(WaitForSingleObject(stopThread.native_handle(), 30 * 1000), WAIT_OBJECT_0);
7620
+
7621
+ deleteThread.join();
7622
+ stopThread.join();
7623
+ input.reset();
7624
+ cleanup.release();
7625
+
7626
+ VERIFY_SUCCEEDED(stopResult);
7627
+ VERIFY_SUCCEEDED(deleteResult);
7628
+ container.SetDeleteOnClose(false);
7629
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateDeleted);
7630
+ }
7631
+
7632
+ WSLC_TEST_METHOD(ForceDeleteAutoRemoveContainer)
7633
+ {
7634
+ WSLCContainerLauncher launcher("debian:latest", "test-force-delete-auto-remove", {"sleep", "99999"});
7635
+ launcher.SetContainerFlags(WSLCContainerFlagsRm);
7636
+
7637
+ auto container = launcher.Launch(*m_defaultSession);
7638
+ auto id = container.Id();
7639
+ auto name = container.Name();
7640
+
7641
+ VERIFY_SUCCEEDED(container.Get().Delete(WSLCDeleteFlagsForce));
7642
+ container.SetDeleteOnClose(false);
7643
+
7644
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateDeleted);
7645
+ VERIFY_ARE_EQUAL(container.Get().Delete(WSLCDeleteFlagsForce), RPC_E_DISCONNECTED);
7646
+
7647
+ wil::com_ptr<IWSLCContainer> openedContainer;
7648
+ VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(id.c_str(), &openedContainer), WSLC_E_CONTAINER_NOT_FOUND);
7649
+ VERIFY_ARE_EQUAL(m_defaultSession->OpenContainer(name.c_str(), &openedContainer), WSLC_E_CONTAINER_NOT_FOUND);
7650
+ }
7651
+
7652
WSLC_TEST_METHOD(ContainerListFilter)
7653
{
7654
// Lists containers with the given filter options and returns the names as a set.
@@ -9141,6 +9446,86 @@ class WSLCTests
9446
VERIFY_ARE_EQUAL(process.GetExitCode(), 128 + WSLCSignalSIGKILL);
9447
}
9448
9449
+ // Stopping a container releases every in-flight exec from inside the Docker 'die' event callback. Several execs are
9450
+ // required: releasing them unregisters their event callbacks while the tracker is dispatching that same event.
9451
+ WSLC_TEST_METHOD(ExecContainerStopManyExecs)
9452
+ {
9453
+ constexpr unsigned int c_execCount = 8;
9454
+
9455
+ WSLCContainerLauncher launcher("debian:latest", "test-exec-stop-many", {"sleep", "99999"}, {}, "none");
9456
+ auto container = launcher.Launch(*m_defaultSession);
9457
+
9458
+ std::vector<ClientRunningWSLCProcess> processes;
9459
+ std::vector<wil::unique_event> exitEvents;
9460
+ processes.reserve(c_execCount);
9461
+ exitEvents.reserve(c_execCount);
9462
+
9463
+ for (unsigned int i = 0; i < c_execCount; ++i)
9464
+ {
9465
+ processes.emplace_back(WSLCProcessLauncher({}, {"sleep", "99999"}).Launch(container.Get()));
9466
+ exitEvents.emplace_back(processes.back().GetExitEvent());
9467
+ }
9468
+
9469
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
9470
+
9471
+ // No exec may be skipped when the container releases them.
9472
+ for (unsigned int i = 0; i < c_execCount; ++i)
9473
+ {
9474
+ VERIFY_IS_TRUE(exitEvents[i].wait(30 * 1000));
9475
+ VERIFY_ARE_EQUAL(processes[i].GetExitCode(), 128 + WSLCSignalSIGKILL);
9476
+ }
9477
+
9478
+ // Lifecycle events must still be delivered once the stop has been processed.
9479
+ VERIFY_SUCCEEDED(container.Get().Start(WSLCContainerStartFlagsNone, nullptr, nullptr));
9480
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateRunning);
9481
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
9482
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
9483
+ }
9484
+
9485
+ // Exec() registers its event callback while holding the container lock, concurrently with the Docker event thread
9486
+ // delivering exec_die for other execs on the same container.
9487
+ WSLC_TEST_METHOD(ExecContainerEventStress)
9488
+ {
9489
+ constexpr unsigned int c_threadCount = 4;
9490
+ constexpr unsigned int c_iterationsPerThread = 25;
9491
+
9492
+ WSLCContainerLauncher launcher("debian:latest", "test-exec-event-stress", {"sleep", "99999"}, {}, "none");
9493
+ auto container = launcher.Launch(*m_defaultSession);
9494
+
9495
+ std::atomic<unsigned int> failures = 0;
9496
+ std::vector<std::thread> threads;
9497
+ threads.reserve(c_threadCount);
9498
+
9499
+ for (unsigned int t = 0; t < c_threadCount; ++t)
9500
+ {
9501
+ threads.emplace_back([&]() {
9502
+ for (unsigned int i = 0; i < c_iterationsPerThread; ++i)
9503
+ {
9504
+ // N.B. Each process is released without waiting, so its callback is unregistered while exec_die
9505
+ // events are still being dispatched.
9506
+ auto [result, process] = WSLCProcessLauncher({}, {"/bin/true"}).LaunchNoThrow(container.Get());
9507
+ if (FAILED(result))
9508
+ {
9509
+ LogError("Exec unexpected HR: 0x%08x", result);
9510
+ ++failures;
9511
+ return;
9512
+ }
9513
+ }
9514
+ });
9515
+ }
9516
+
9517
+ for (auto& thread : threads)
9518
+ {
9519
+ thread.join();
9520
+ }
9521
+
9522
+ VERIFY_ARE_EQUAL(failures.load(), 0u);
9523
+
9524
+ // The event stream must still be live after the exec_die storm.
9525
+ VERIFY_SUCCEEDED(container.Get().Stop(WSLCSignalSIGKILL, 0));
9526
+ VERIFY_ARE_EQUAL(container.State(), WslcContainerStateExited);
9527
+ }
9528
+
9529
void RunPortMappingsTest(IWSLCSession& session, const std::string& containerNetworkType, bool virtionet)
9530
{
9531
WEX::Logging::Log::Comment(