Deferred mounting volumes and mapping ports to Start#14320
Deferred mounting volumes and mapping ports to Start#14320kvega005 wants to merge 15 commits intomicrosoft:feature/wsl-for-appsfrom
Conversation
…ttps://github.com/kvega005/WSL into user/kevinve/map_ports_and_mount_volumes_at_start
There was a problem hiding this comment.
Pull request overview
This PR refactors WSLA container lifecycle so port reservation/relay activation and Windows-folder volume mounting occur at Start() time (and are released on stop), enabling dynamic VM-port assignment in bridge networking mode.
Changes:
- Deferred WSLA container volume mounting + host port reservation from
Create()toStart(), with cleanup on stop/error. - Added two-phase port binding: reserve host port first, then activate relay once the VM-side port is known (incl. Docker inspect for bridge mode).
- Updated schemas/tests to support the new flow (Docker
NetworkSettings.Ports,HostConfig.Binds, revised Unmap signature, new regression test).
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/windows/WSLATests.cpp | Updates Unmap calls and adds a regression test covering deferred port/volume setup at Start/Stop. |
| src/windows/wslrelay/localhost.cpp | Adds “inactive reservation” behavior (LinuxPort==0) and allows later activation by updating LinuxPort. |
| src/windows/wslasession/WSLAVirtualMachine.h | Introduces ReserveHostPort() and simplifies UnmapPort() signature. |
| src/windows/wslasession/WSLAVirtualMachine.cpp | Implements reserve/map/unmap via the relay channel with LinuxPort=0 for reservation/unmap. |
| src/windows/wslasession/WSLASession.h | Updates COM method signature for UnmapVmPort. |
| src/windows/wslasession/WSLASession.cpp | Routes UnmapVmPort to the simplified VM UnmapPort. |
| src/windows/wslasession/WSLAContainerMetadata.h | Adds runtime-only Mounted state tracking for volume mounts. |
| src/windows/wslasession/WSLAContainer.h | Stores network mode and splits COM wrapper disconnection from resource release. |
| src/windows/wslasession/WSLAContainer.cpp | Defers mounts/port reservation to Start, resolves bridge ports post-start via inspect, adds OnStopped cleanup. |
| src/windows/wslaservice/inc/wslaservice.idl | Changes IWSLASession::UnmapVmPort signature. |
| src/windows/inc/docker_schema.h | Adds HostConfig.Binds, InspectContainer.NetworkSettings.Ports, and ContainerInfo.HostConfig. |
| src/windows/common/WSLAContainerLauncher.h | Adds SetName() to enable tests to rename launchers. |
| src/windows/common/WSLAContainerLauncher.cpp | Implements SetName(). |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ttps://github.com/kvega005/WSL into user/kevinve/map_ports_and_mount_volumes_at_start
| HRESULT WSLASession::UnmapVmPort(int Family, short WindowsPort, short LinuxPort) | ||
| try | ||
| { | ||
| COMServiceExecutionContext context; | ||
|
|
||
| auto lock = m_lock.lock_shared(); | ||
| THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_virtualMachine); | ||
|
|
||
| m_virtualMachine->UnmapPort(Family, WindowsPort, LinuxPort); | ||
| m_virtualMachine->UnmapPort(Family, WindowsPort); |
There was a problem hiding this comment.
LinuxPort is no longer used in WSLASession::UnmapVmPort, which can trigger an unreferenced-parameter warning under MSVC (and the repo uses warnings-as-errors in some configs). To keep the COM signature intact while avoiding /WX issues, consider adding UNREFERENCED_PARAMETER(LinuxPort); (or omitting the parameter name in the definition).
src/windows/wslrelay/localhost.cpp
Outdated
| if (it->second->LinuxPort == 0 && message->LinuxPort != 0) | ||
| { | ||
| it->second->LinuxPort = message->LinuxPort; | ||
| update = true; | ||
| } |
There was a problem hiding this comment.
PortRelay::LinuxPort is now mutated at runtime (to activate a previously reserved port) while the accept thread concurrently reads it via isActive()/LaunchRelay(). Because LinuxPort is a plain integer with no synchronization, this introduces a data race (undefined behavior). Consider either (1) making LinuxPort an std::atomic<uint32_t> (or uint16_t to match the message) and using atomic loads/stores, or (2) stopping/joining the accept thread before updating LinuxPort, then restarting it after the update, so reads/writes are synchronized.
| try | ||
| { | ||
| m_dockerClient.StartContainer(m_id); | ||
| } | ||
| CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to start container '%hs'", m_id.c_str()); | ||
|
|
||
| // For bridge mode, inspect the container to discover the VM ports Docker assigned. | ||
| if (m_networkMode == "bridge") | ||
| { | ||
| auto dockerInspect = m_dockerClient.InspectContainer(m_id); | ||
| ResolveVmPorts(m_mappedPorts, dockerInspect); | ||
| } | ||
|
|
||
| // Activate port relays now that VM ports are known. | ||
| ActivatePortRelays(m_mappedPorts, m_virtualMachine); | ||
|
|
There was a problem hiding this comment.
If any step after StartContainer() succeeds (InspectContainer/ResolveVmPorts/ActivatePortRelays) throws, the container will remain running but the scope_exit cleanups will unmount volumes and unreserve host ports, and the object will remain in the Created state. This can leave an orphaned running container with broken volumes/port exposure. Consider adding a scope_exit that stops/kills the container (best-effort) once StartContainer has succeeded, and only releases it after Transition(Running) + successful relay activation.
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1); | ||
|
|
||
| // Start the container — volume should now be mounted. | ||
| VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone)); | ||
| VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning); | ||
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 0); | ||
|
|
||
| // Verify the volume is unmounted after container is stopped. | ||
| VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0)); | ||
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1); |
There was a problem hiding this comment.
This test validates deferred volume mounts by grepping for any mount under '/mnt/', but the session can legitimately have other mounts (or leftover mounts from earlier tests), making this check flaky and not specific to the container volume under test. Prefer asserting on the exact expected mountpoint (e.g., /mnt/wsla//volumes/0) using the existing ExpectMount helper, and/or isolate by resetting/creating a session like other volume tests do.
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1); | |
| // Start the container — volume should now be mounted. | |
| VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone)); | |
| VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning); | |
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 0); | |
| // Verify the volume is unmounted after container is stopped. | |
| VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0)); | |
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/'"}, 1); | |
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 1); | |
| // Start the container — volume should now be mounted. | |
| VERIFY_SUCCEEDED(container->Get().Start(WSLAContainerStartFlagsNone)); | |
| VERIFY_ARE_EQUAL(container->State(), WslaContainerStateRunning); | |
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 0); | |
| // Verify the volume is unmounted after container is stopped. | |
| VERIFY_SUCCEEDED(container->Get().Stop(WSLASignalSIGKILL, 0)); | |
| ExpectCommandResult(m_defaultSession.get(), {"/bin/sh", "-c", "findmnt -o TARGET -l | grep '^/mnt/wsla/deferred-volume/volumes/0$'"}, 1); |
| { | ||
| if (e.VmPort != 0) | ||
| { | ||
| continue; // Host mode port — VM port is already known. |
There was a problem hiding this comment.
What does it mean by "host mode port" here?
| { | ||
| auto dockerInspect = m_dockerClient.InspectContainer(m_id); | ||
| ResolveVmPorts(m_mappedPorts, dockerInspect); | ||
| } |
There was a problem hiding this comment.
If the networking mode is not "bridge", then we should not call ActivatePortRelays. Is the idea that the CLI/API will sanity check that if the port mappings are provided, then the networking mode would have to be 'bridge'? In the standard case, for host network containers, the port mapping is not passed by the user.
There was a problem hiding this comment.
The port relays need to activated in host mode as well
| { | ||
| auto dockerInspect = m_dockerClient.InspectContainer(m_id); | ||
| ResolveVmPorts(m_mappedPorts, dockerInspect); | ||
| } |
There was a problem hiding this comment.
If any of these above calls fail, do we want to stop/delete the started container? Otherwise we will have a started container without the VM ports resolved
There was a problem hiding this comment.
That's actually a good point. I need to rethink this a bit. I think if we wait to start the container then fail to activate the port relays, we would inadvertently have allowed the init process to run for a brief period of time. This means there may be a side effect. We probably do not want to fail the start container call after the init process has ran for some time.
|
|
||
| void WSLAVirtualMachine::ReserveHostPort(_In_ int Family, _In_ short WindowsPort) | ||
| { | ||
| MapPortImpl(Family, WindowsPort, 0, false); |
There was a problem hiding this comment.
nit: this last bool parameter is very confusing. Ideally we should use a meaningful enum here to indicate that false means "we are NOT unmapping ports" and true means "we are unmapping ports"
At the very least, add comments next to these.
| void WSLAVirtualMachine::UnmapPort(_In_ int Family, _In_ short WindowsPort) | ||
| { | ||
| MapPortImpl(Family, WindowsPort, LinuxPort, true); | ||
| MapPortImpl(Family, WindowsPort, 0, true); |
There was a problem hiding this comment.
nit: again, a comment saying unmap does not require LinuxPort to be provided could be very helpful for readability
| for (auto& e : ports) | ||
| { | ||
| if (!e->Pending) | ||
| if (!e->Pending && e->IsActive()) |
There was a problem hiding this comment.
The pending and active checks are not happening atomically, could this cause a race? Similar questions for the port mapping related functionality happening from within this thread
| VERIFY_ARE_EQUAL(container.State(), WslaContainerStateRunning); | ||
|
|
||
| // Start container 2 — should fail because the host port is already reserved by container 1. | ||
| VERIFY_ARE_EQUAL(container2.Get().Start(WSLAContainerStartFlagsNone), HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)); |
There was a problem hiding this comment.
A more meaningful error message around port mapping failure due to conflict would be useful
| WSLA_CONTAINER_STATE m_state = WslaContainerStateInvalid; | ||
| WSLASession& m_wslaSession; | ||
| WSLAVirtualMachine& m_virtualMachine; | ||
| std::string m_networkMode; |
There was a problem hiding this comment.
I recommend storing the networking mode as WSLA_CONTAINER_NETWORK_TYPE instead of a string here
|
|
||
| void WSLAVirtualMachine::ReserveHostPort(_In_ int Family, _In_ short WindowsPort) | ||
| { | ||
| MapPortImpl(Family, WindowsPort, 0, false); |
There was a problem hiding this comment.
If we want to be able to map host ports without actually starting to relay, we should flags to signal the difference between:
- I have previously reserved this port, and now I want to start relaying to guest port X
- I want to map this host port for the first time
Otherwise this can create a race where if let's say container A is starting and does 1), container B could come and steal the port by doing 2) before container A is done allocating the port fully
| { | ||
| for (auto& e : ports) | ||
| { | ||
| if (e.VmPort != 0) |
There was a problem hiding this comment.
This shouldn't be possible right ? ResolveVmPorts() is only called in bridge mode, and in this mode we always set VmPort to 0
There was a problem hiding this comment.
If that's the case, I'd recommend switching to a WI_ASSERT here
|
|
||
| bool IsActive() const | ||
| { | ||
| return LinuxPort != 0; |
There was a problem hiding this comment.
LinuxPort should be changed to an std::atomic here since we're now reading & writing it from multiple threads.
In truth we should probably rewrite this relay logic, but for now let's just make it an std::atomic
| { | ||
| auto dockerInspect = m_dockerClient.InspectContainer(m_id); | ||
| ResolveVmPorts(m_mappedPorts, dockerInspect); | ||
| } |
There was a problem hiding this comment.
The port relays need to activated in host mode as well
Summary of the Pull Request
This PR defers port reservation and volume mounting to after the Docker container is started. Previously these resources were set up during
Create(), but they only need to be active while the container is running. This also enables bridge mode support, where Docker assigns the VM port dynamically during container start.Changes
Deferred resource setup (
WSLAContainer)Create()toStart()OnStopped()to release ports and unmount volumes when the container stopsTwo-phase port binding (
WSLAVirtualMachine/localhost.cpp)ReserveHostPort()— binds a host port without a relay targetMapPort()activates relaying on an already-reserved portUnmapPort()— removed unusedLinuxPortparameterBridge mode VM port resolution
StartContainer(), for bridge mode only: inspects the container to discover Docker-assigned VM ports, then activates relaysPort relay:
Start()time before Docker assigns the VM-side port.StartContainer(), the container is inspected to discover the VM ports Docker assigned (for bridge mode), and then the relay is activated with the now-known VM ports.PR Checklist
Detailed Description of the Pull Request / Additional comments
Validation Steps Performed