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});