| 1 | /*++ |
| 2 | |
| 3 | Copyright (c) Microsoft. All rights reserved. |
| 4 | |
| 5 | Module Name: |
| 6 | |
| 7 | WSLCVolumes.cpp |
| 8 | |
| 9 | Abstract: |
| 10 | |
| 11 | Contains the implementation of WSLCVolumes. |
| 12 | |
| 13 | --*/ |
| 14 | |
| 15 | #include "precomp.h" |
| 16 | #include "WSLCVolumes.h" |
| 17 | #include "WSLCVhdVolume.h" |
| 18 | #include "WSLCGuestVolume.h" |
| 19 | #include "WSLCVirtualMachine.h" |
| 20 | #include "docker_schema.h" |
| 21 | |
| 22 | using wsl::shared::Localization; |
| 23 | |
| 24 | namespace wsl::windows::service::wslc { |
| 25 | |
| 26 | WSLCVolumes::WSLCVolumes( |
| 27 | DockerHTTPClient& dockerClient, WSLCVirtualMachine& virtualMachine, DockerEventTracker& eventTracker, const std::filesystem::path& storagePath) : |
| 28 | m_dockerClient(dockerClient), m_virtualMachine(virtualMachine), m_storagePath(storagePath) |
| 29 | { |
| 30 | // Hold m_lock exclusively across both callback registration and the recovery loop. |
| 31 | // This ensures any volume events that arrive while recovering are queued behind us in OnVolumeEvent, |
| 32 | // and dedup naturally against entries inserted by recovery (insert is a no-op for existing keys). |
| 33 | auto lock = m_lock.lock_exclusive(); |
| 34 | |
| 35 | m_volumeEventTracking = eventTracker.RegisterVolumeUpdates( |
| 36 | std::bind(&WSLCVolumes::OnVolumeEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); |
| 37 | |
| 38 | for (const auto& volume : dockerClient.ListVolumes()) |
| 39 | { |
| 40 | try |
| 41 | { |
| 42 | OpenVolumeExclusiveLockHeld(volume); |
| 43 | } |
| 44 | catch (...) |
| 45 | { |
| 46 | LOG_CAUGHT_EXCEPTION_MSG("Failed to recover volume: %hs", volume.Name.c_str()); |
| 47 | EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcFailedToRecoverVolume(volume.Name)); |
| 48 | } |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | __requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const wsl::windows::common::docker_schema::Volume& vol) |
| 53 | { |
| 54 | THROW_HR_IF_MSG(E_UNEXPECTED, vol.Driver != "local", "Unrecognized volume driver: %hs", vol.Driver.c_str()); |
| 55 | |
| 56 | if (vol.Labels.has_value() && vol.Labels->contains(WSLCVolumeMetadataLabel)) |
| 57 | { |
| 58 | auto metadata = wsl::shared::FromJson<WSLCVolumeMetadata>(vol.Labels->at(WSLCVolumeMetadataLabel).c_str()); |
| 59 | |
| 60 | if (metadata.Driver == WSLCVhdVolumeDriver) |
| 61 | { |
| 62 | m_volumes.insert({vol.Name, WSLCVhdVolumeImpl::Open(vol, m_virtualMachine, m_dockerClient)}); |
| 63 | return; |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | m_volumes.insert({vol.Name, WSLCGuestVolumeImpl::Open(vol, m_dockerClient)}); |
| 68 | } |
| 69 | |
| 70 | void WSLCVolumes::OnVolumeEvent(const std::string& volumeName, VolumeEvent event, std::int64_t) |
| 71 | { |
| 72 | auto lock = m_lock.lock_exclusive(); |
| 73 | |
| 74 | // If this event matches the next self-initiated operation we are waiting to observe, the |
| 75 | // map mutation has already been applied by CreateVolume / DeleteVolume. Just pop the event and |
| 76 | // skip updating m_volumes. |
| 77 | if (!m_expectedEvents.empty() && m_expectedEvents.front().first == volumeName && m_expectedEvents.front().second == event) |
| 78 | { |
| 79 | m_expectedEvents.pop_front(); |
| 80 | return; |
| 81 | } |
| 82 | |
| 83 | if (event == VolumeEvent::Create) |
| 84 | { |
| 85 | OpenVolumeExclusiveLockHeld(volumeName); |
| 86 | } |
| 87 | else if (event == VolumeEvent::Destroy) |
| 88 | { |
| 89 | OnVolumeDeletedExclusiveLockHeld(volumeName); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | WSLCVolumeInformation WSLCVolumes::CreateVolume( |
| 94 | LPCSTR Name, LPCSTR Driver, std::map<std::string, std::string>&& DriverOpts, std::map<std::string, std::string>&& Labels) |
| 95 | { |
| 96 | auto lock = m_lock.lock_exclusive(); |
| 97 | |
| 98 | if (Name != nullptr && Name[0] != '\0') |
| 99 | { |
| 100 | THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS), m_volumes.contains(Name)); |
| 101 | } |
| 102 | |
| 103 | std::string driver = (Driver != nullptr && Driver[0] != '\0') ? Driver : WSLCGuestVolumeDriver; |
| 104 | std::unique_ptr<IWSLCVolume> volume; |
| 105 | |
| 106 | if (driver == WSLCVhdVolumeDriver) |
| 107 | { |
| 108 | volume = WSLCVhdVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_storagePath, m_virtualMachine, m_dockerClient); |
| 109 | } |
| 110 | else if (driver == WSLCGuestVolumeDriver) |
| 111 | { |
| 112 | volume = WSLCGuestVolumeImpl::Create(Name, std::move(DriverOpts), std::move(Labels), m_dockerClient); |
| 113 | } |
| 114 | else |
| 115 | { |
| 116 | THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcInvalidVolumeType(driver)); |
| 117 | } |
| 118 | |
| 119 | const auto& name = volume->Name(); |
| 120 | auto info = volume->GetVolumeInformation(); |
| 121 | |
| 122 | auto [it, inserted] = m_volumes.insert({name, std::move(volume)}); |
| 123 | WI_VERIFY(inserted); |
| 124 | |
| 125 | return info; |
| 126 | } |
| 127 | |
| 128 | void WSLCVolumes::DeleteVolume(LPCSTR Name) |
| 129 | { |
| 130 | THROW_HR_IF(E_POINTER, Name == nullptr); |
| 131 | |
| 132 | auto lock = m_lock.lock_exclusive(); |
| 133 | |
| 134 | auto it = m_volumes.find(Name); |
| 135 | THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end()); |
| 136 | |
| 137 | it->second->Delete(); |
| 138 | m_volumes.erase(it); |
| 139 | |
| 140 | // Record that we initiated this destroy so OnVolumeEvent ignores the matching docker event. |
| 141 | m_expectedEvents.emplace_back(Name, VolumeEvent::Destroy); |
| 142 | } |
| 143 | |
| 144 | std::vector<wsl::windows::common::wslc_schema::VolumeListEntry> WSLCVolumes::ListVolumes(std::map<std::string, std::vector<std::string>>&& Filters) const |
| 145 | { |
| 146 | // Pull the driver filter out and forward everything else to docker for filtering. |
| 147 | // Driver filter is special-cased because our driver concept doesn't map 1:1 to docker's. |
| 148 | std::vector<std::string> drivers; |
| 149 | auto it = Filters.find("driver"); |
| 150 | if (it != Filters.end()) |
| 151 | { |
| 152 | drivers = std::move(it->second); |
| 153 | Filters.erase(it); |
| 154 | } |
| 155 | |
| 156 | std::vector<wsl::windows::common::docker_schema::Volume> dockerVolumes; |
| 157 | try |
| 158 | { |
| 159 | dockerVolumes = m_dockerClient.ListVolumes(Filters); |
| 160 | } |
| 161 | CATCH_AND_THROW_DOCKER_USER_ERROR("Failed to list volumes"); |
| 162 | |
| 163 | std::unordered_set<std::string> dockerVolumeNames; |
| 164 | dockerVolumeNames.reserve(dockerVolumes.size()); |
| 165 | for (const auto& vol : dockerVolumes) |
| 166 | { |
| 167 | dockerVolumeNames.insert(vol.Name); |
| 168 | } |
| 169 | |
| 170 | auto lock = m_lock.lock_shared(); |
| 171 | |
| 172 | std::vector<wsl::windows::common::wslc_schema::VolumeListEntry> result; |
| 173 | result.reserve(dockerVolumeNames.size()); |
| 174 | |
| 175 | for (const auto& [name, vol] : m_volumes) |
| 176 | { |
| 177 | // Must be in docker's filtered list. |
| 178 | if (!dockerVolumeNames.contains(name)) |
| 179 | { |
| 180 | continue; |
| 181 | } |
| 182 | |
| 183 | // Apply driver filter using the WSLC driver names. |
| 184 | if (!drivers.empty() && std::ranges::find(drivers, vol->Driver()) == drivers.end()) |
| 185 | { |
| 186 | continue; |
| 187 | } |
| 188 | |
| 189 | wsl::windows::common::wslc_schema::VolumeListEntry entry; |
| 190 | entry.Name = vol->Name(); |
| 191 | entry.Driver = vol->Driver(); |
| 192 | entry.Mountpoint = vol->Mountpoint(); |
| 193 | entry.Scope = WSLCVolumeScope; |
| 194 | entry.Labels = vol->Labels(); |
| 195 | |
| 196 | result.push_back(std::move(entry)); |
| 197 | } |
| 198 | |
| 199 | return result; |
| 200 | } |
| 201 | |
| 202 | std::string WSLCVolumes::InspectVolume(const std::string& Name) const |
| 203 | { |
| 204 | auto lock = m_lock.lock_shared(); |
| 205 | |
| 206 | auto it = m_volumes.find(Name); |
| 207 | THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end()); |
| 208 | |
| 209 | return it->second->Inspect(); |
| 210 | } |
| 211 | |
| 212 | std::pair<HRESULT, std::string> WSLCVolumes::GetVolumeStatus(const std::string& Name) const |
| 213 | { |
| 214 | auto lock = m_lock.lock_shared(); |
| 215 | |
| 216 | auto it = m_volumes.find(Name); |
| 217 | THROW_HR_WITH_USER_ERROR_IF(WSLC_E_VOLUME_NOT_FOUND, Localization::MessageWslcVolumeNotFound(Name), it == m_volumes.end()); |
| 218 | |
| 219 | return it->second->Status(); |
| 220 | } |
| 221 | |
| 222 | WSLCVolumes::PruneVolumesResult WSLCVolumes::PruneVolumes(const std::map<std::string, std::vector<std::string>>& Filters) |
| 223 | { |
| 224 | auto lock = m_lock.lock_exclusive(); |
| 225 | |
| 226 | auto dockerResult = m_dockerClient.PruneVolumes(Filters); |
| 227 | |
| 228 | PruneVolumesResult result{}; |
| 229 | result.SpaceReclaimed = dockerResult.SpaceReclaimed; |
| 230 | |
| 231 | if (!dockerResult.VolumesDeleted.has_value() || dockerResult.VolumesDeleted->empty()) |
| 232 | { |
| 233 | return result; |
| 234 | } |
| 235 | |
| 236 | result.Volumes.reserve(dockerResult.VolumesDeleted->size()); |
| 237 | |
| 238 | // TODO: VHD volumes are exposed to docker as bind mounts, which docker's volume |
| 239 | // prune skips. So this only ever prunes guest volumes today. VHD volume pruning |
| 240 | // requires custom handling that's not implemented yet. |
| 241 | for (const auto& name : dockerResult.VolumesDeleted.value()) |
| 242 | { |
| 243 | // Only report volumes that we manage. |
| 244 | auto it = m_volumes.find(name); |
| 245 | if (it == m_volumes.end()) |
| 246 | { |
| 247 | WSL_LOG("PrunedUnknownVolume", TraceLoggingValue(name.c_str(), "name")); |
| 248 | continue; |
| 249 | } |
| 250 | |
| 251 | try |
| 252 | { |
| 253 | it->second->OnDeleted(); |
| 254 | } |
| 255 | catch (...) |
| 256 | { |
| 257 | LOG_CAUGHT_EXCEPTION_MSG("Failed to release host resources for pruned volume: %hs", name.c_str()); |
| 258 | EMIT_USER_WARNING(wsl::shared::Localization::MessageWslcVolumeReleaseFailed(name)); |
| 259 | } |
| 260 | |
| 261 | m_volumes.erase(it); |
| 262 | m_expectedEvents.emplace_back(name, VolumeEvent::Destroy); |
| 263 | result.Volumes.push_back(name); |
| 264 | } |
| 265 | |
| 266 | return result; |
| 267 | } |
| 268 | |
| 269 | __requires_lock_held(m_lock) void WSLCVolumes::OpenVolumeExclusiveLockHeld(const std::string& volumeName) |
| 270 | { |
| 271 | if (volumeName.empty() || m_volumes.contains(volumeName)) |
| 272 | { |
| 273 | return; |
| 274 | } |
| 275 | |
| 276 | try |
| 277 | { |
| 278 | OpenVolumeExclusiveLockHeld(m_dockerClient.InspectVolume(volumeName)); |
| 279 | } |
| 280 | catch (const DockerHTTPException& e) |
| 281 | { |
| 282 | // A 404 here is expected when a late `create` event arrives after the volume has already |
| 283 | // been deleted (e.g. user calls CreateVolume then DeleteVolume; the create event from |
| 284 | // docker can race in after the delete has been processed). |
| 285 | if (e.StatusCode() != 404) |
| 286 | { |
| 287 | LOG_CAUGHT_EXCEPTION_MSG("Failed to open volume: %hs", volumeName.c_str()); |
| 288 | } |
| 289 | } |
| 290 | CATCH_LOG_MSG("Failed to open volume: %hs", volumeName.c_str()); |
| 291 | } |
| 292 | |
| 293 | __requires_lock_held(m_lock) void WSLCVolumes::OnVolumeDeletedExclusiveLockHeld(const std::string& volumeName) |
| 294 | { |
| 295 | auto it = m_volumes.find(volumeName); |
| 296 | if (it != m_volumes.end()) |
| 297 | { |
| 298 | it->second->OnDeleted(); |
| 299 | m_volumes.erase(it); |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | } // namespace wsl::windows::service::wslc |