@samitouri / QOSAMI-WSL / commits / f2adc87b

Add support for callback when loading images and display loaded images in wslc image load (#40960)

* Save state' * Cleanup diff * Cleanup diff * Cleanup diff * Integrate with plugin API * Format * Cleanup diff * Fix logic error

Blue committed Jul 2, 2026 at 15:01 UTC f2adc87b1b5ad8a180f409178349c473f1442604
14 files changed +281 -62
localization/strings/en-US/Resources.resw
+8
@@ -3095,6 +3095,14 @@ On first run, creates the file with all settings commented out at their defaults
3095 <data name="WSLCCLI_ImageLoadNoInputError" xml:space="preserve">
3096 <value>Requested load but no input provided.</value>
3097 </data>
3098 + <data name="WSLCCLI_ImageLoaded" xml:space="preserve">
3099 + <value>Loaded image: {}</value>
3100 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3101 + </data>
3102 + <data name="WSLCCLI_ImageLoadedId" xml:space="preserve">
3103 + <value>Loaded image ID: {}</value>
3104 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
3105 + </data>
3106 <data name="WSLCCLI_NetworkCommandDesc" xml:space="preserve">
3107 <value>Manage networks.</value>
3108 </data>
msipackage/package.wix.in
+8
@@ -332,6 +332,14 @@
332 </RegistryKey>
333 </RegistryKey>
334
335 + <!-- IImageLoadCallback-->
336 + <RegistryKey Root="HKCR" Key="Interface\{7F3B62FD-E1C1-4F69-A8E2-853CAD9051BD}">
337 + <RegistryValue Value="IImageLoadCallback" Type="string" />
338 + <RegistryKey Key="ProxyStubClsid32">
339 + <RegistryValue Value="{4EA0C6DD-E9FF-48E7-994E-13A31D10DC60}" Type="string" />
340 + </RegistryKey>
341 + </RegistryKey>
342 +
343 <!-- IWSLCSession-->
344 <RegistryKey Root="HKCR" Key="Interface\{EF0661E4-6364-40EA-B433-E2FDF11F3519}">
345 <RegistryValue Value="IWSLCSession" Type="string" />
src/windows/common/wslutil.cpp
+3 -3
@@ -1320,17 +1320,17 @@ std::pair<std::string, std::optional<std::string>> wsl::windows::common::wslutil
1320
1321 THROW_HR_IF_MSG(E_UNEXPECTED, !repo.matched, "Unexpected regex match. Input: %hs", Input.c_str());
1322
1323 - EnumReferenceFormat referenceFormat = EnumReferenceFormat::None;
1323 + EnumReferenceFormat referenceFormat = EnumReferenceFormatNone;
1324 std::optional<std::string> tagOrDigest;
1325 if (digest.matched) // <repo>:[tag]@<digest> (If both digest and tag are specified, digest takes precedence).
1326 {
1327 tagOrDigest = digest.str();
1328 - referenceFormat = EnumReferenceFormat::Digest;
1328 + referenceFormat = EnumReferenceFormatDigest;
1329 }
1330 else if (tag.matched) // <repo>:<tag>
1331 {
1332 tagOrDigest = tag.str();
1333 - referenceFormat = EnumReferenceFormat::Tag;
1333 + referenceFormat = EnumReferenceFormatTag;
1334 }
1335
1336 if (Format)
src/windows/common/wslutil.h
-7
@@ -48,13 +48,6 @@ inline auto c_vhdFileExtension = L".vhd";
48 inline auto c_vhdxFileExtension = L".vhdx";
49 inline constexpr auto c_vmOwner = L"WSL"; // TODO-WSLC: Does this apply to WSLC ?
50
51 -enum class EnumReferenceFormat
52 -{
53 - None,
54 - Tag,
55 - Digest
56 -};
57 -
51 struct GitHubReleaseAsset
52 {
53 std::wstring url;
src/windows/service/inc/wslc.idl
+19 -2
@@ -89,6 +89,23 @@ interface IWarningCallback : IUnknown
89 HRESULT OnWarning([in, string] LPCWSTR Message);
90 };
91
92 +typedef enum _EnumReferenceFormat
93 +{
94 + EnumReferenceFormatNone = 0,
95 + EnumReferenceFormatTag = 1,
96 + EnumReferenceFormatDigest = 2,
97 +} EnumReferenceFormat;
98 +
99 +[
100 + uuid(7F3B62FD-E1C1-4F69-A8E2-853CAD9051BD),
101 + pointer_default(unique),
102 + object
103 +]
104 +interface IImageLoadCallback : IUnknown
105 +{
106 + HRESULT OnImageLoaded([in, string] LPCSTR Image, [in] EnumReferenceFormat Format);
107 +};
108 +
109 [
110 uuid(F3E6D5B2-1D40-4E8B-9C39-7A45D1C0F8A2),
111 pointer_default(unique),
@@ -603,8 +620,8 @@ interface IWSLCSession : IUnknown
620 // Image management.
621 HRESULT PullImage([in] LPCSTR Image, [in, unique] LPCSTR RegistryAuthenticationInformation, [in, unique] IProgressCallback* ProgressCallback, [in, unique] IWarningCallback* WarningCallback);
622 HRESULT BuildImage([in] const WSLCBuildImageOptions* Options, [in, unique] IProgressCallback* ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
606 - HRESULT LoadImage([in] WSLCHandle ImageHandle, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback);
607 - HRESULT ImportImage([in] WSLCHandle ImageHandle, [in, unique] LPCSTR ImageName, [in, unique] IProgressCallback* ProgressCallback, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback, [out] LPSTR* ImageId);
623 + HRESULT LoadImage([in] WSLCHandle ImageHandle, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback, [in, unique] IImageLoadCallback* LoadCallback);
624 + HRESULT ImportImage([in] WSLCHandle ImageHandle, [in, unique] LPCSTR ImageName, [in] ULONGLONG ContentLength, [in, unique] IWarningCallback* WarningCallback, [out] LPSTR* ImageId);
625 HRESULT SaveImage([in] WSLCHandle OutputHandle, [in] LPCSTR ImageNameOrID, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
626 HRESULT SaveImages([in] WSLCHandle OutputHandle, [in] const WSLCStringArray* ImageNames, [in, unique] IProgressCallback * ProgressCallback, [in, unique, system_handle(sh_event)] HANDLE CancelEvent);
627 HRESULT ListImages([in, unique] const WSLCListImagesOptions* Options, [out, size_is(, *Count)] WSLCImageInformation** Images, [out] ULONG* Count);
src/windows/wslc/services/ImageService.cpp
+3 -4
@@ -231,11 +231,11 @@ std::vector<ImageInformation> ImageService::List(
231 return result;
232 }
233
234 -void ImageService::Load(wsl::windows::wslc::models::Session& session, const std::wstring& input)
234 +void ImageService::Load(wsl::windows::wslc::models::Session& session, const std::wstring& input, IImageLoadCallback* callback)
235 {
236 auto source = OpenImageInput(input);
237 auto warningCallback = Microsoft::WRL::Make<WarningCallback>();
238 - THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(source.Handle.Get()), nullptr, source.ContentLength, warningCallback.Get()));
238 + THROW_IF_FAILED(session.Get()->LoadImage(ToCOMInputHandle(source.Handle.Get()), source.ContentLength, warningCallback.Get(), callback));
239 }
240
241 std::string ImageService::Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName)
@@ -246,7 +246,6 @@ std::string ImageService::Import(wsl::windows::wslc::models::Session& session, c
246 THROW_IF_FAILED(session.Get()->ImportImage(
247 ToCOMInputHandle(source.Handle.Get()),
248 imageName.empty() ? nullptr : imageName.c_str(),
249 - nullptr,
249 source.ContentLength,
250 warningCallback.Get(),
251 &imageId));
@@ -284,7 +283,7 @@ void ImageService::Tag(wsl::windows::wslc::models::Session& session, const std::
283 {
284 EnumReferenceFormat format;
285 auto [repo, tag] = ParseImage(targetImage, &format);
287 - if (format == EnumReferenceFormat::Digest)
286 + if (format == EnumReferenceFormatDigest)
287 {
288 THROW_HR_WITH_USER_ERROR(E_INVALIDARG, Localization::MessageWslcTagImageInvalidFormat(targetImage.c_str()));
289 }
src/windows/wslc/services/ImageService.h
+1 -1
@@ -35,7 +35,7 @@ public:
35
36 static std::vector<wsl::windows::wslc::models::ImageInformation> List(
37 wsl::windows::wslc::models::Session& session, const std::vector<std::pair<std::string, std::string>>& filters = {});
38 - static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input);
38 + static void Load(wsl::windows::wslc::models::Session& session, const std::wstring& input, IImageLoadCallback* callback = nullptr);
39 static std::string Import(wsl::windows::wslc::models::Session& session, const std::wstring& input, const std::string& imageName);
40 static void Delete(wsl::windows::wslc::models::Session& session, const std::string& image, bool force, bool noPrune);
41 static wsl::windows::common::wslc_schema::InspectImage Inspect(wsl::windows::wslc::models::Session& session, const std::string& image);
src/windows/wslc/tasks/ImageTasks.cpp
+38 -1
@@ -35,6 +35,42 @@ using namespace wsl::windows::wslc::services;
35
36 namespace wsl::windows::wslc::task {
37
38 +namespace {
39 +
40 + class DECLSPEC_UUID("91EF98A7-99A8-41C2-893C-43CDFB7DB69F") WSLCImageLoadCallback
41 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IImageLoadCallback, IFastRundown>
42 + {
43 + public:
44 + explicit WSLCImageLoadCallback(Reporter& reporter) : m_reporter(reporter)
45 + {
46 + }
47 +
48 + HRESULT OnImageLoaded(LPCSTR Reference, EnumReferenceFormat Format) override
49 + try
50 + {
51 + if (Format == EnumReferenceFormatDigest)
52 + {
53 + m_reporter.Output(L"{}\n", Localization::WSLCCLI_ImageLoadedId(Reference));
54 + }
55 + else if (Format == EnumReferenceFormatTag)
56 + {
57 + m_reporter.Output(L"{}\n", Localization::WSLCCLI_ImageLoaded(Reference));
58 + }
59 + else
60 + {
61 + THROW_HR_MSG(E_UNEXPECTED, "Unexpected reference type: %d, '%hs'", Format, Reference);
62 + }
63 +
64 + return S_OK;
65 + }
66 + CATCH_RETURN();
67 +
68 + private:
69 + Reporter& m_reporter;
70 + };
71 +
72 +} // namespace
73 +
74 static bool TryInspectImage(Session& session, const std::string& imageId, std::optional<wslc_schema::InspectImage>& inspectData)
75 {
76 try
@@ -223,7 +259,8 @@ void LoadImage(CLIExecutionContext& context)
259 if (context.Args.Contains(ArgType::Input))
260 {
261 auto& input = context.Args.Get<ArgType::Input>();
226 - services::ImageService::Load(session, input);
262 + auto callback = wil::MakeOrThrow<WSLCImageLoadCallback>(context.Reporter);
263 + services::ImageService::Load(session, input, callback.Get());
264 return;
265 }
266
src/windows/wslcsession/WSLCSession.cpp
+45 -20
@@ -1221,11 +1221,9 @@ try
1221 }
1222 CATCH_RETURN();
1223
1224 -HRESULT WSLCSession::LoadImage(const WSLCHandle ImageHandle, IProgressCallback* ProgressCallback, ULONGLONG ContentSize, IWarningCallback* WarningCallback)
1224 +HRESULT WSLCSession::LoadImage(const WSLCHandle ImageHandle, ULONGLONG ContentSize, IWarningCallback* WarningCallback, IImageLoadCallback* LoadCallback)
1225 try
1226 {
1227 - UNREFERENCED_PARAMETER(ProgressCallback);
1228 -
1227 WSLCExecutionContext context(this, WarningCallback);
1228
1229 auto lock = m_lock.lock_shared();
@@ -1234,18 +1232,15 @@ try
1232
1233 auto requestContext = m_dockerClient->LoadImage(ContentSize);
1234
1237 - std::ignore = ImportImageImpl(*requestContext, ImageHandle);
1235 + std::ignore = ImportImageImpl(*requestContext, ImageHandle, LoadCallback);
1236
1237 return S_OK;
1238 }
1239 CATCH_RETURN();
1240
1243 -HRESULT WSLCSession::ImportImage(
1244 - const WSLCHandle ImageHandle, LPCSTR ImageName, IProgressCallback* ProgressCallback, ULONGLONG ContentSize, IWarningCallback* WarningCallback, LPSTR* ImageId)
1241 +HRESULT WSLCSession::ImportImage(const WSLCHandle ImageHandle, LPCSTR ImageName, ULONGLONG ContentSize, IWarningCallback* WarningCallback, LPSTR* ImageId)
1242 try
1243 {
1247 - UNREFERENCED_PARAMETER(ProgressCallback);
1248 -
1244 WSLCExecutionContext context(this, WarningCallback);
1245
1246 RETURN_HR_IF_NULL(E_POINTER, ImageId);
@@ -1288,10 +1283,16 @@ try
1283 }
1284 CATCH_RETURN();
1285
1291 -std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle)
1286 +std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle, IImageLoadCallback* LoadCallback)
1287 {
1288 auto userHandle = OpenUserHandle(ImageHandle);
1289
1290 + std::optional<UserCOMCallback> comCall;
1291 + if (LoadCallback != nullptr)
1292 + {
1293 + comCall = RegisterUserCOMCallback();
1294 + }
1295 +
1296 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !m_dockerClient.has_value());
1297
1298 auto io = CreateIOContext();
@@ -1343,6 +1344,37 @@ std::optional<std::string> WSLCSession::ImportImageImpl(DockerHTTPClient::HTTPRe
1344 else if (parsed.stream.has_value())
1345 {
1346 WSL_LOG("ImageImportProgress", TraceLoggingValue(parsed.stream->c_str(), "Content"));
1347 +
1348 + {
1349 + static constexpr std::string_view c_loadedImagePrefix = "Loaded image: ";
1350 + static constexpr std::string_view c_loadedImageIdPrefix = "Loaded image ID: ";
1351 +
1352 + for (const auto& entry : shared::string::Split(*parsed.stream, '\n'))
1353 + {
1354 + std::string name;
1355 + EnumReferenceFormat format = EnumReferenceFormatNone;
1356 + if (entry.starts_with(c_loadedImagePrefix))
1357 + {
1358 + name = entry.substr(c_loadedImagePrefix.size());
1359 + format = EnumReferenceFormatTag;
1360 + }
1361 + else if (entry.starts_with(c_loadedImageIdPrefix))
1362 + {
1363 + name = entry.substr(c_loadedImageIdPrefix.size());
1364 + format = EnumReferenceFormatDigest;
1365 + }
1366 +
1367 + if (!name.empty())
1368 + {
1369 + OnImageCreated(name);
1370 +
1371 + if (LoadCallback != nullptr)
1372 + {
1373 + THROW_IF_FAILED(LoadCallback->OnImageLoaded(name.c_str(), format));
1374 + }
1375 + }
1376 + }
1377 + }
1378 }
1379 else if (parsed.status.has_value())
1380 {
@@ -3097,28 +3129,21 @@ HRESULT WSLCSession::PullImage(LPCSTR Image, LPCSTR RegistryAuthenticationInform
3129 return PullImage(Image, RegistryAuthenticationInformation, progress.Get(), warning.Get());
3130 }
3131
3100 -HRESULT WSLCSession::LoadImage(WSLCCompatHandle ImageHandle, IWSLCCompatProgressCallback* ProgressCallback, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback)
3132 +HRESULT WSLCSession::LoadImage(WSLCCompatHandle ImageHandle, IWSLCCompatProgressCallback*, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback)
3133 {
3134 const auto handle = apicompat::Convert(ImageHandle);
3103 - const auto progress = apicompat::Convert(ProgressCallback);
3135 const auto warning = apicompat::Convert(WarningCallback);
3136
3106 - return LoadImage(handle, progress.Get(), ContentLength, warning.Get());
3137 + return LoadImage(handle, ContentLength, warning.Get(), nullptr);
3138 }
3139
3140 HRESULT WSLCSession::ImportImage(
3110 - WSLCCompatHandle ImageHandle,
3111 - LPCSTR ImageName,
3112 - IWSLCCompatProgressCallback* ProgressCallback,
3113 - ULONGLONG ContentLength,
3114 - IWSLCCompatWarningCallback* WarningCallback,
3115 - LPSTR* ImageId)
3141 + WSLCCompatHandle ImageHandle, LPCSTR ImageName, IWSLCCompatProgressCallback*, ULONGLONG ContentLength, IWSLCCompatWarningCallback* WarningCallback, LPSTR* ImageId)
3142 {
3143 const auto handle = apicompat::Convert(ImageHandle);
3118 - const auto progress = apicompat::Convert(ProgressCallback);
3144 const auto warning = apicompat::Convert(WarningCallback);
3145
3121 - return ImportImage(handle, ImageName, progress.Get(), ContentLength, warning.Get(), ImageId);
3146 + return ImportImage(handle, ImageName, ContentLength, warning.Get(), ImageId);
3147 }
3148
3149 HRESULT WSLCSession::ListImages(const WSLCCompatListImagesOptions* Options, WSLCCompatImageInformation** Images, ULONG* Count)
src/windows/wslcsession/WSLCSession.h
+7 -3
@@ -110,11 +110,14 @@ public:
110 _In_opt_ IProgressCallback* ProgressCallback,
111 _In_opt_ IWarningCallback* WarningCallback) override;
112 IFACEMETHOD(BuildImage)(_In_ const WSLCBuildImageOptions* Options, _In_opt_ IProgressCallback* ProgressCallback, _In_opt_ HANDLE CancelEvent) override;
113 - IFACEMETHOD(LoadImage)(_In_ const WSLCHandle ImageHandle, _In_ IProgressCallback* ProgressCallback, _In_ ULONGLONG ContentLength, _In_opt_ IWarningCallback* WarningCallback) override;
113 + IFACEMETHOD(LoadImage)(
114 + _In_ const WSLCHandle ImageHandle,
115 + _In_ ULONGLONG ContentLength,
116 + _In_opt_ IWarningCallback* WarningCallback,
117 + _In_opt_ IImageLoadCallback* LoadCallback) override;
118 IFACEMETHOD(ImportImage)(
119 _In_ const WSLCHandle ImageHandle,
120 _In_opt_ LPCSTR ImageName,
117 - _In_ IProgressCallback* ProgressCallback,
121 _In_ ULONGLONG ContentLength,
122 _In_opt_ IWarningCallback* WarningCallback,
123 _Out_ LPSTR* ImageId) override;
@@ -289,7 +292,8 @@ private:
292 void StartContainerd();
293 void StartDockerd();
294 int StopProcess(ServiceRunningProcess& Process, DWORD TerminateTimeoutMs, DWORD KillTimeoutMs);
292 - std::optional<std::string> ImportImageImpl(DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle);
295 + std::optional<std::string> ImportImageImpl(
296 + DockerHTTPClient::HTTPRequestContext& Request, const WSLCHandle ImageHandle, IImageLoadCallback* LoadCallback = nullptr);
297 void RecoverExistingContainers();
298 void RecoverExistingNetworks();
299
test/windows/Common.cpp
+1 -1
@@ -2986,7 +2986,7 @@ void LoadTestImage(IWSLCSession& session, std::string_view imageName)
2986 LARGE_INTEGER fileSize{};
2987 THROW_LAST_ERROR_IF(!GetFileSizeEx(imageFile.get(), &fileSize));
2988
2989 - THROW_IF_FAILED(session.LoadImage(wsl::windows::common::wslutil::ToCOMInputHandle(imageFile.get()), nullptr, fileSize.QuadPart, nullptr));
2989 + THROW_IF_FAILED(session.LoadImage(wsl::windows::common::wslutil::ToCOMInputHandle(imageFile.get()), fileSize.QuadPart, nullptr, nullptr));
2990 }
2991
2992 void ExpectHttpResponse(LPCWSTR Url, std::optional<int> expectedCode, bool retry)
test/windows/PluginTests.cpp
+4
@@ -666,6 +666,7 @@ class PluginTests
666 WSLCMountFolder(nonexistent): {}
667 WSLCMountFolder(relative): {}
668 Test completed
669 + WSLC Image created, session=*, id=sha256:*, name=debian:latest
670 WSLC Container started, session=*, id=*, name=wslc-plugin-container, image=debian:latest, state=*
671 WSLC Container stopping, session=*, id=*
672 WSLC Image deleted, session=*, id=*
@@ -716,6 +717,8 @@ class PluginTests
717 constexpr auto ExpectedOutput =
718 LR"(Plugin loaded. TestMode=21
719 WSLC Session created, name=plugin-wslc-pull-test, id=*, pid=*, token=set, sid=set
720 + WSLC Image created, session=*, id=sha256:*, name=debian:latest
721 + WSLC Image created, session=*, id=sha256:*, name=wslc-registry:latest
722 WSLC Container started, session=*, id=*, name=*, image=wslc-registry:latest, state=running
723 WSLC Image created, session=*, id=sha256:*, name=127.0.0.1:5000/debian:latest
724 WSLC Session stopping, name=plugin-wslc-pull-test, id=*)";
@@ -769,6 +772,7 @@ class PluginTests
772 constexpr auto ExpectedOutput =
773 LR"(Plugin loaded. TestMode=20
774 WSLC Session created, name=plugin-wslc-container-rejected, id=*, pid=*, token=set, sid=set
775 + WSLC Image created, session=*, id=sha256:*, name=debian:latest
776 WSLC Container started, session=*, id=*, name=*, image=debian:latest, state=*
777 OnWslcContainerStarted: ERROR_ACCESS_DENIED
778 WSLC Session stopping, name=plugin-wslc-container-rejected, id=*)";
test/windows/WSLCTests.cpp
+137 -20
@@ -1194,7 +1194,7 @@ class WSLCTests
1194 LARGE_INTEGER fileSize{};
1195 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
1196
1197 - VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
1197 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), fileSize.QuadPart, nullptr, nullptr));
1198
1199 // Verify that the image is in the list of images.
1200 ExpectImagePresent(*m_defaultSession, "hello-world:latest");
@@ -1216,7 +1216,7 @@ class WSLCTests
1216 VERIFY_IS_TRUE(GetFileSizeEx(currentExecutableHandle.get(), &fileSize));
1217
1218 VERIFY_ARE_EQUAL(
1219 - m_defaultSession->LoadImage(ToCOMInputHandle(currentExecutableHandle.get()), nullptr, fileSize.QuadPart, nullptr), E_FAIL);
1219 + m_defaultSession->LoadImage(ToCOMInputHandle(currentExecutableHandle.get()), fileSize.QuadPart, nullptr, nullptr), E_FAIL);
1220
1221 ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
1222 }
@@ -1229,7 +1229,7 @@ class WSLCTests
1229
1230 std::promise<HRESULT> loadResult;
1231 std::thread operationThread([&]() {
1232 - loadResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024, nullptr));
1232 + loadResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), 1024 * 1024, nullptr, nullptr));
1233 });
1234
1235 auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
@@ -1253,7 +1253,7 @@ class WSLCTests
1253 std::promise<HRESULT> terminateResult;
1254 wil::unique_event testCompleted{wil::EventOptions::ManualReset};
1255 std::thread operationThread([&]() {
1256 - terminateResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), nullptr, 1024 * 1024, nullptr));
1256 + terminateResult.set_value(m_defaultSession->LoadImage(ToCOMInputHandle(pipeRead.get()), 1024 * 1024, nullptr, nullptr));
1257 WI_ASSERT(testCompleted.is_signaled());
1258 });
1259
@@ -1274,6 +1274,126 @@ class WSLCTests
1274 }
1275 }
1276
1277 + class CapturingImageLoadCallback
1278 + : public Microsoft::WRL::RuntimeClass<Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, IImageLoadCallback, IFastRundown>
1279 + {
1280 + public:
1281 + HRESULT OnImageLoaded(LPCSTR ImageName, EnumReferenceFormat Format) override
1282 + {
1283 + m_images.emplace_back(ImageName, Format);
1284 + return S_OK;
1285 + }
1286 +
1287 + const std::vector<std::pair<std::string, EnumReferenceFormat>>& GetImages() const
1288 + {
1289 + return m_images;
1290 + }
1291 +
1292 + private:
1293 + std::vector<std::pair<std::string, EnumReferenceFormat>> m_images;
1294 + };
1295 +
1296 + WSLC_TEST_METHOD(LoadImageCallback)
1297 + {
1298 + SKIP_TEST_SERVER();
1299 +
1300 + const std::filesystem::path imageTar = L"LoadImageCallbackExport.tar";
1301 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
1302 +
1303 + // Save both images into a single archive.
1304 + {
1305 + wil::unique_handle tarFile{
1306 + CreateFileW(imageTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
1307 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == tarFile.get());
1308 +
1309 + std::vector<LPCSTR> names = {"debian:latest", "hello-world:latest"};
1310 + WSLCStringArray array{.Values = names.data(), .Count = static_cast<ULONG>(names.size())};
1311 + VERIFY_SUCCEEDED(m_defaultSession->SaveImages(ToCOMInputHandle(tarFile.get()), &array, nullptr, nullptr));
1312 + }
1313 +
1314 + // Delete both images so that loading actually recreates them.
1315 + DeleteImage("hello-world:latest", WSLCDeleteImageFlagsForce);
1316 + DeleteImage("debian:latest", WSLCDeleteImageFlagsForce);
1317 + ExpectImagePresent(*m_defaultSession, "hello-world:latest", false);
1318 + ExpectImagePresent(*m_defaultSession, "debian:latest", false);
1319 +
1320 + auto callback = Microsoft::WRL::Make<CapturingImageLoadCallback>();
1321 + {
1322 + wil::unique_handle tarFile{
1323 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1324 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == tarFile.get());
1325 +
1326 + LARGE_INTEGER fileSize{};
1327 + VERIFY_IS_TRUE(GetFileSizeEx(tarFile.get(), &fileSize));
1328 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(tarFile.get()), fileSize.QuadPart, nullptr, callback.Get()));
1329 + }
1330 +
1331 + ExpectImagePresent(*m_defaultSession, "debian:latest");
1332 + ExpectImagePresent(*m_defaultSession, "hello-world:latest");
1333 +
1334 + // Validate that both images have been reported.
1335 + const auto loaded = callback->GetImages();
1336 + VERIFY_ARE_EQUAL(static_cast<size_t>(2), loaded.size());
1337 + VERIFY_IS_TRUE(std::ranges::find(loaded, std::make_pair(std::string("debian:latest"), EnumReferenceFormatTag)) != loaded.end());
1338 + VERIFY_IS_TRUE(
1339 + std::ranges::find(loaded, std::make_pair(std::string("hello-world:latest"), EnumReferenceFormatTag)) != loaded.end());
1340 + }
1341 +
1342 + WSLC_TEST_METHOD(LoadImageCallbackById)
1343 + {
1344 + SKIP_TEST_SERVER();
1345 +
1346 + const std::filesystem::path imageTar = L"LoadImageCallbackByIdExport.tar";
1347 + auto cleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LOG_IF_WIN32_BOOL_FALSE(DeleteFileW(imageTar.c_str())); });
1348 +
1349 + auto restore = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { LoadTestImage(*m_defaultSession, "hello-world:latest"); });
1350 +
1351 + std::string imageId;
1352 + {
1353 + wil::unique_cotaskmem_array_ptr<WSLCImageInformation> images;
1354 + VERIFY_SUCCEEDED(m_defaultSession->ListImages(nullptr, images.addressof(), images.size_address<ULONG>()));
1355 + for (const auto& image : images)
1356 + {
1357 + if (std::strcmp(image.Image, "hello-world:latest") == 0)
1358 + {
1359 + imageId = image.Hash;
1360 + break;
1361 + }
1362 + }
1363 + }
1364 +
1365 + VERIFY_IS_FALSE(imageId.empty());
1366 + VERIFY_IS_TRUE(imageId.starts_with("sha256:"));
1367 +
1368 + // Save the image by ID.
1369 + {
1370 + wil::unique_handle tarFile{
1371 + CreateFileW(imageTar.c_str(), GENERIC_WRITE, FILE_SHARE_READ, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr)};
1372 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == tarFile.get());
1373 +
1374 + std::vector<LPCSTR> names = {imageId.c_str()};
1375 + WSLCStringArray array{.Values = names.data(), .Count = static_cast<ULONG>(names.size())};
1376 + VERIFY_SUCCEEDED(m_defaultSession->SaveImages(ToCOMInputHandle(tarFile.get()), &array, nullptr, nullptr));
1377 + }
1378 +
1379 + auto callback = Microsoft::WRL::Make<CapturingImageLoadCallback>();
1380 + {
1381 + wil::unique_handle tarFile{
1382 + CreateFileW(imageTar.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
1383 + VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == tarFile.get());
1384 +
1385 + LARGE_INTEGER fileSize{};
1386 + VERIFY_IS_TRUE(GetFileSizeEx(tarFile.get(), &fileSize));
1387 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(tarFile.get()), fileSize.QuadPart, nullptr, callback.Get()));
1388 + }
1389 +
1390 + // Validate that the expected image ID was reported.
1391 + const auto& loaded = callback->GetImages();
1392 + VERIFY_ARE_EQUAL(static_cast<size_t>(1), loaded.size());
1393 + VERIFY_ARE_EQUAL(imageId, loaded[0].first);
1394 + VERIFY_ARE_EQUAL(EnumReferenceFormatDigest, loaded[0].second);
1395 + }
1396 +
1397 WSLC_TEST_METHOD(ImportImage)
1398 {
1399 SKIP_TEST_SERVER();
@@ -1291,7 +1411,7 @@ class WSLCTests
1411
1412 wil::unique_cotaskmem_ansistring imageId;
1413 VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
1294 - ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", nullptr, fileSize.QuadPart, nullptr, &imageId));
1414 + ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world:test", fileSize.QuadPart, nullptr, &imageId));
1415
1416 ExpectImagePresent(*m_defaultSession, "my-hello-world:test");
1417
@@ -1309,8 +1429,7 @@ class WSLCTests
1429 // Validate that ImportImage fails if no tag is passed
1430 {
1431 VERIFY_ARE_EQUAL(
1312 - m_defaultSession->ImportImage(
1313 - ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", nullptr, fileSize.QuadPart, nullptr, &imageId),
1432 + m_defaultSession->ImportImage(ToCOMInputHandle(imageTarFileHandle.get()), "my-hello-world", fileSize.QuadPart, nullptr, &imageId),
1433 E_INVALIDARG);
1434 }
1435
@@ -1322,7 +1441,7 @@ class WSLCTests
1441
1442 VERIFY_ARE_EQUAL(
1443 m_defaultSession->ImportImage(
1325 - ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", nullptr, fileSize.QuadPart, nullptr, &imageId),
1444 + ToCOMInputHandle(currentExecutableHandle.get()), "invalid-image:test", fileSize.QuadPart, nullptr, &imageId),
1445 E_FAIL);
1446
1447 ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
@@ -1345,8 +1464,7 @@ class WSLCTests
1464 VERIFY_ARE_EQUAL(fileSize.QuadPart, 300 * _1MB);
1465
1466 VERIFY_ARE_EQUAL(
1348 - m_defaultSession->ImportImage(
1349 - ToCOMInputHandle(largeFile.get()), "invalid-large-image:test", nullptr, fileSize.QuadPart, nullptr, &imageId),
1467 + m_defaultSession->ImportImage(ToCOMInputHandle(largeFile.get()), "invalid-large-image:test", fileSize.QuadPart, nullptr, &imageId),
1468 E_FAIL);
1469
1470 ValidateCOMErrorMessage(L"archive/tar: invalid tar header");
@@ -1361,8 +1479,8 @@ class WSLCTests
1479 std::promise<HRESULT> importResult;
1480 std::thread operationThread([&]() {
1481 wil::unique_cotaskmem_ansistring id;
1364 - importResult.set_value(m_defaultSession->ImportImage(
1365 - ToCOMInputHandle(pipeRead.get()), "broken-read:eof", nullptr, 1024 * 1024, nullptr, &id));
1482 + importResult.set_value(
1483 + m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "broken-read:eof", 1024 * 1024, nullptr, &id));
1484 });
1485
1486 auto threadCleanup = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&]() { operationThread.join(); });
@@ -1388,7 +1506,7 @@ class WSLCTests
1506 std::thread operationThread([&]() {
1507 wil::unique_cotaskmem_ansistring id;
1508 terminateResult.set_value(m_defaultSession->ImportImage(
1391 - ToCOMInputHandle(pipeRead.get()), "session-terminate:test", nullptr, 1024 * 1024, nullptr, &id));
1509 + ToCOMInputHandle(pipeRead.get()), "session-terminate:test", 1024 * 1024, nullptr, &id));
1510 WI_ASSERT(testCompleted.is_signaled());
1511 });
1512
@@ -2518,7 +2636,7 @@ class WSLCTests
2636 LARGE_INTEGER fileSize{};
2637 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2638 // Load the image from a saved tar
2521 - VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
2639 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), fileSize.QuadPart, nullptr, nullptr));
2640 // Verify that the image is in the list of images.
2641 ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2642 WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
@@ -2553,7 +2671,7 @@ class WSLCTests
2671 LARGE_INTEGER fileSize{};
2672 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2673 // Load the image from a saved tar
2556 - VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
2674 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), fileSize.QuadPart, nullptr, nullptr));
2675 // Verify that the image is in the list of images.
2676 ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2677 WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
@@ -2645,7 +2763,7 @@ class WSLCTests
2763 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2764 LARGE_INTEGER fileSize{};
2765 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2648 - VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
2766 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), fileSize.QuadPart, nullptr, nullptr));
2767 }
2768
2769 ExpectImagePresent(*m_defaultSession, "hello-world:latest");
@@ -2748,8 +2866,7 @@ class WSLCTests
2866 wil::unique_event testCompleted{wil::EventOptions::ManualReset};
2867 std::thread operationThread([&]() {
2868 wil::unique_cotaskmem_ansistring id;
2751 - result.set_value(
2752 - m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", nullptr, 1024 * 1024, nullptr, &id));
2869 + result.set_value(m_defaultSession->ImportImage(ToCOMInputHandle(pipeRead.get()), "dummy:latest", 1024 * 1024, nullptr, &id));
2870
2871 WI_ASSERT(testCompleted.is_signaled()); // Sanity check.
2872 });
@@ -2796,7 +2913,7 @@ class WSLCTests
2913 VERIFY_IS_FALSE(INVALID_HANDLE_VALUE == imageTarFileHandle.get());
2914 LARGE_INTEGER fileSize{};
2915 VERIFY_IS_TRUE(GetFileSizeEx(imageTarFileHandle.get(), &fileSize));
2799 - VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), nullptr, fileSize.QuadPart, nullptr));
2916 + VERIFY_SUCCEEDED(m_defaultSession->LoadImage(ToCOMInputHandle(imageTarFileHandle.get()), fileSize.QuadPart, nullptr, nullptr));
2917 // Verify that the image is in the list of images.
2918 ExpectImagePresent(*m_defaultSession, "hello-world:latest");
2919 WSLCContainerLauncher launcher("hello-world:latest", "wslc-hello-world-container");
@@ -2830,7 +2947,7 @@ class WSLCTests
2947
2948 wil::unique_cotaskmem_ansistring importedImageId;
2949 VERIFY_SUCCEEDED(m_defaultSession->ImportImage(
2833 - ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", nullptr, fileSize.QuadPart, nullptr, &importedImageId));
2950 + ToCOMInputHandle(containerTarFileHandle.get()), "test-imported-container:latest", fileSize.QuadPart, nullptr, &importedImageId));
2951
2952 // Verify that the image is in the list of images.
2953 ExpectImagePresent(*m_defaultSession, "test-imported-container:latest");
test/windows/wslc/e2e/WSLCE2EImageSaveTests.cpp
+7
@@ -152,6 +152,13 @@ class WSLCE2EImageSaveTests
152 const auto loadResult = RunWslc(std::format(L"image load --input \"{}\"", SavedArchivePath.wstring()));
153 loadResult.Verify({.Stderr = L"", .ExitCode = 0});
154
155 + auto loadedImages = wsl::shared::string::Split(loadResult.Stdout.value(), L'\n');
156 +
157 + VERIFY_IS_TRUE(
158 + std::ranges::find(loadedImages, std::format(L"Loaded image: {}\r", DebianImage.NameAndTag())) != loadedImages.end());
159 + VERIFY_IS_TRUE(
160 + std::ranges::find(loadedImages, std::format(L"Loaded image: {}\r", AlpineImage.NameAndTag())) != loadedImages.end());
161 +
162 // Run a container from each loaded image to confirm both are restored and runnable.
163 const auto runDebian = RunWslc(std::format(L"container run --rm {} echo ok!", DebianImage.NameAndTag()));
164 runDebian.Verify({.Stdout = std::format(L"ok!\n"), .Stderr = L"", .ExitCode = 0});