master
cpp 303 lines 9.73 KB
Raw
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