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(