diff --git a/SampleApps/WebView2APISample/App.cpp b/SampleApps/WebView2APISample/App.cpp index 70f549f1..f9a6a676 100644 --- a/SampleApps/WebView2APISample/App.cpp +++ b/SampleApps/WebView2APISample/App.cpp @@ -14,7 +14,9 @@ #include #include "AppWindow.h" +#include "CheckFailure.h" #include "DpiUtil.h" +#include "ScenarioClusterEnvironment.h" HINSTANCE g_hInstance; int g_nCmdShow; @@ -44,6 +46,12 @@ wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmd DWORD creationModeId = IDM_CREATION_MODE_WINDOWED; WebViewCreateOption opt; + // When launched with --clustername=..., this instance is a secondary host + // process that should join an existing shared cluster environment instead + // of creating a normal (private) environment. + bool joinCluster = false; + ClusterEnvironmentSpec clusterSpec; + if (lpCmdLine && lpCmdLine[0]) { int paramCount = 0; @@ -94,6 +102,64 @@ wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmd { userDataFolder = nextParam.substr(nextParam.find(L'=') + 1); } + else if (NEXT_PARAM_CONTAINS(L"clustername=")) + { + joinCluster = true; + clusterSpec.clusterName = nextParam.substr(nextParam.find(L'=') + 1); + } + else if (NEXT_PARAM_CONTAINS(L"clusterlang=")) + { + clusterSpec.language = nextParam.substr(nextParam.find(L'=') + 1); + } + else if (NEXT_PARAM_CONTAINS(L"clusterargs=")) + { + clusterSpec.additionalBrowserArguments = + nextParam.substr(nextParam.find(L'=') + 1); + } + else if (NEXT_PARAM_CONTAINS(L"clustersso=")) + { + clusterSpec.allowSingleSignOn = + nextParam.substr(nextParam.find(L'=') + 1) == L"1"; + } + else if (NEXT_PARAM_CONTAINS(L"clustertracking=")) + { + clusterSpec.enableTrackingPrevention = + nextParam.substr(nextParam.find(L'=') + 1) == L"1"; + } + else if (NEXT_PARAM_CONTAINS(L"clusterextensions=")) + { + clusterSpec.areBrowserExtensionsEnabled = + nextParam.substr(nextParam.find(L'=') + 1) == L"1"; + } + else if (NEXT_PARAM_CONTAINS(L"clusterisolation=")) + { + clusterSpec.perHostProfileIsolation = + nextParam.substr(nextParam.find(L'=') + 1) == L"1"; + } + else if (NEXT_PARAM_CONTAINS(L"clusterchannels=")) + { + // wcstol rather than stoi: a malformed value should fall back to + // 0 (NONE) rather than throw out of command-line parsing. Masked + // because the value is cast to a flags enum, and a negative or + // oversized number would otherwise set every reserved bit. + const long channels = + wcstol(nextParam.substr(nextParam.find(L'=') + 1).c_str(), nullptr, 10); + clusterSpec.releaseChannels = + static_cast(channels & kAllReleaseChannels); + } + else if (NEXT_PARAM_CONTAINS(L"clustersearchkind=")) + { + // Parsed the same way the parent writes it, rather than by + // testing for L"1", so a value added to the enum later does not + // silently arrive as MOST_STABLE. Clamped for the same reason + // the channels are masked: this is cast to an enum. + const long searchKind = + wcstol(nextParam.substr(nextParam.find(L'=') + 1).c_str(), nullptr, 10); + clusterSpec.channelSearchKind = + searchKind == COREWEBVIEW2_CHANNEL_SEARCH_KIND_LEAST_STABLE + ? COREWEBVIEW2_CHANNEL_SEARCH_KIND_LEAST_STABLE + : COREWEBVIEW2_CHANNEL_SEARCH_KIND_MOST_STABLE; + } else if (NEXT_PARAM_CONTAINS(L"creationmode=")) { nextParam = nextParam.substr(nextParam.find(L'=') + 1); @@ -125,7 +191,18 @@ wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmd DpiUtil::SetProcessDpiAwarenessContext(dpiAwarenessContext); - new AppWindow(creationModeId, opt, initialUri, userDataFolder, true); + if (joinCluster) + { + // COM is required before calling into the loader; AppWindow (created in + // the completion handler) will also initialize COM on this thread. + CHECK_FAILURE(OleInitialize(nullptr)); + CreateOrJoinClusterAndOpenWindow( + clusterSpec, /*isMainWindow=*/true, /*parent=*/nullptr); + } + else + { + new AppWindow(creationModeId, opt, initialUri, userDataFolder, true); + } int retVal = RunMessagePump(); diff --git a/SampleApps/WebView2APISample/AppStartPage.cpp b/SampleApps/WebView2APISample/AppStartPage.cpp index ae180d65..824e5117 100644 --- a/SampleApps/WebView2APISample/AppStartPage.cpp +++ b/SampleApps/WebView2APISample/AppStartPage.cpp @@ -86,6 +86,62 @@ std::wstring GetRuntimePath(AppWindow* appWindow) return ResolvePathAndTrimFile(runtimePath); } +// Returns the actual user data folder in use by the current WebView2 +// environment. Empty if the environment doesn't support querying it +// (ICoreWebView2Environment7 unavailable). +std::wstring GetUserDataFolder(AppWindow* appWindow) +{ + wil::com_ptr environment = appWindow->GetWebViewEnvironment(); + auto environment7 = environment.try_query(); + if (!environment7) + return L""; + + wil::unique_cotaskmem_string userDataFolder; + if (FAILED(environment7->get_UserDataFolder(&userDataFolder)) || !userDataFolder) + return L""; + return userDataFolder.get(); +} + +// Classifies a user data folder as shared when it lives under the shared +// cluster root, mirroring the detection the browser itself uses. +std::wstring GetUserDataFolderKind(const std::wstring& userDataFolder) +{ + if (userDataFolder.empty()) + return L"Unknown"; + + std::wstring lowerUserDataFolder = userDataFolder; + std::transform( + lowerUserDataFolder.begin(), lowerUserDataFolder.end(), lowerUserDataFolder.begin(), + ::towlower); + return lowerUserDataFolder.find(L"\\microsoft\\webview2clusters\\") != std::wstring::npos + ? L"Shared (cluster)" + : L"Non-shared"; +} + +// Percent-encodes the characters that would otherwise be read as query string +// syntax. The folder carries a user-chosen name that may contain '&' or '=', +// either of which would split the value and blank the field. +std::wstring EscapeForQuery(const std::wstring& value) +{ + static constexpr wchar_t kHexDigits[] = L"0123456789ABCDEF"; + std::wstring escaped; + escaped.reserve(value.size()); + for (const wchar_t c : value) + { + if (c == L'&' || c == L'=' || c == L'%' || c == L'#' || c == L'?' || c == L'+') + { + escaped.push_back(L'%'); + escaped.push_back(kHexDigits[(c >> 4) & 0xF]); + escaped.push_back(kHexDigits[c & 0xF]); + } + else + { + escaped.push_back(c); + } + } + return escaped; +} + std::wstring GetUri(AppWindow* appWindow) { std::wstring uri = appWindow->GetLocalUri(L"AppStartPage.html", true); @@ -102,6 +158,13 @@ std::wstring GetUri(AppWindow* appWindow) uri += L"&runtimePath="; uri += GetRuntimePath(appWindow); + std::wstring userDataFolder = GetUserDataFolder(appWindow); + uri += L"&userDataFolder="; + uri += EscapeForQuery(userDataFolder); + + uri += L"&userDataFolderKind="; + uri += GetUserDataFolderKind(userDataFolder); + return uri; } diff --git a/SampleApps/WebView2APISample/AppWindow.cpp b/SampleApps/WebView2APISample/AppWindow.cpp index 1c33cbb2..a7abeef9 100644 --- a/SampleApps/WebView2APISample/AppWindow.cpp +++ b/SampleApps/WebView2APISample/AppWindow.cpp @@ -31,6 +31,7 @@ #include "ScenarioAddHostObject.h" #include "ScenarioAuthentication.h" #include "ScenarioClientCertificateRequested.h" +#include "ScenarioClusterEnvironment.h" #include "ScenarioCookieManagement.h" #include "ScenarioCustomDownloadExperience.h" #include "ScenarioCustomScheme.h" @@ -203,9 +204,10 @@ AppWindow::AppWindow( UINT creationModeId, const WebViewCreateOption& opt, const std::wstring& initialUri, const std::wstring& userDataFolderParam, bool isMainWindow, std::function webviewCreatedCallback, bool customWindowRect, RECT windowRect, - bool shouldHaveToolbar, bool isPopup) + bool shouldHaveToolbar, bool isPopup, ICoreWebView2Environment* providedEnvironment) : m_creationModeId(creationModeId), m_webviewOption(opt), m_initialUri(initialUri), - m_onWebViewFirstInitialized(webviewCreatedCallback), m_isPopupWindow(isPopup) + m_onWebViewFirstInitialized(webviewCreatedCallback), m_isPopupWindow(isPopup), + m_providedEnvironment(providedEnvironment) { // Initialize COM as STA. CHECK_FAILURE(OleInitialize(NULL)); @@ -648,6 +650,18 @@ bool AppWindow::ExecuteWebViewCommands(WPARAM wParam, LPARAM lParam) NewComponent(this); return true; } + case IDM_SCENARIO_CLUSTER_ENVIRONMENT: + { + NewComponent( + this, ScenarioClusterEnvironment::Mode::CreateOrJoin); + return true; + } + case IDM_SCENARIO_CLUSTER_ENVIRONMENT_GET_OPTIONS: + { + NewComponent( + this, ScenarioClusterEnvironment::Mode::GetOptions); + return true; + } case IDM_SCENARIO_CUSTOM_SCHEME_NAVIGATE: { NewComponent(this); @@ -1841,6 +1855,14 @@ void AppWindow::InitializeWebView() } //! [CreateCoreWebView2EnvironmentWithOptions] + // When a pre-created environment was provided, host the WebView in it + // directly instead of creating a new environment. + if (m_providedEnvironment) + { + OnCreateEnvironmentCompleted(S_OK, m_providedEnvironment.get()); + return; + } + std::wstring args; // Page Interaction Restriction Manager requires msPageInteractionManagerWebview2 to be // enabled from the args, as by default it's disabled in the browser. If you want to diff --git a/SampleApps/WebView2APISample/AppWindow.h b/SampleApps/WebView2APISample/AppWindow.h index 5b1cf2ea..81c73c12 100644 --- a/SampleApps/WebView2APISample/AppWindow.h +++ b/SampleApps/WebView2APISample/AppWindow.h @@ -104,7 +104,7 @@ class AppWindow const std::wstring& initialUri = L"", const std::wstring& userDataFolderParam = L"", bool isMainWindow = false, std::function webviewCreatedCallback = nullptr, bool customWindowRect = false, RECT windowRect = {0}, bool shouldHaveToolbar = true, - bool isPopup = false); + bool isPopup = false, ICoreWebView2Environment* providedEnvironment = nullptr); ~AppWindow(); @@ -269,6 +269,10 @@ class AppWindow int m_refCount = 1; bool m_isClosed = false; + // When non-null, InitializeWebView uses this pre-created environment + // instead of creating its own via CreateCoreWebView2EnvironmentWithOptions. + wil::com_ptr m_providedEnvironment; + // The following is state that belongs with the webview, and should // be reinitialized along with it. Everything here is undefined when // m_webView is null. diff --git a/SampleApps/WebView2APISample/ScenarioClusterEnvironment.cpp b/SampleApps/WebView2APISample/ScenarioClusterEnvironment.cpp new file mode 100644 index 00000000..6933692e --- /dev/null +++ b/SampleApps/WebView2APISample/ScenarioClusterEnvironment.cpp @@ -0,0 +1,587 @@ +// Copyright (C) Microsoft Corporation. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "stdafx.h" + +#include "ScenarioClusterEnvironment.h" + +#include +#include +#include + +#include "CheckFailure.h" +#include "TextInputDialog.h" +#include "resource.h" + +// Generated SDK header for the cluster options object, included by bare name +// like the other generated WebView2 SDK headers. +#include "ClusterEnvironmentOptions.h" + +using namespace Microsoft::WRL; + +namespace +{ +// Checkbox values identifying each boolean cluster option. +enum ClusterOption +{ + kAllowSingleSignOn = 1, + kEnableTrackingPrevention, + kAreBrowserExtensionsEnabled, + kPerHostProfileIsolation, +}; + +constexpr wchar_t kClusterNameLabel[] = L"Cluster Name"; +constexpr wchar_t kLanguageLabel[] = L"Language (blank for default)"; +constexpr wchar_t kBrowserArgsLabel[] = L"Additional Browser Arguments"; +constexpr wchar_t kOptionsGroupLabel[] = L"Environment options:"; +constexpr wchar_t kChannelsGroupLabel[] = L"Release channels to search:"; +constexpr wchar_t kSearchKindLabel[] = L"Channel search order"; +constexpr wchar_t kDefaultClusterName[] = L"SampleCluster"; + +// Dropdown entries for kSearchKindLabel, ordered to match the values of +// COREWEBVIEW2_CHANNEL_SEARCH_KIND (MostStable is 0, LeastStable is 1). +constexpr wchar_t kMostStableLabel[] = L"MostStable"; +constexpr wchar_t kLeastStableLabel[] = L"LeastStable"; + +// Reads a string property and returns it, freeing the COM allocation. +std::wstring GetOptionString( + ICoreWebView2ExperimentalClusterEnvironmentOptions* options, + HRESULT (STDMETHODCALLTYPE ICoreWebView2ExperimentalClusterEnvironmentOptions::*getter)( + LPWSTR*)) +{ + wil::unique_cotaskmem_string value; + if (SUCCEEDED((options->*getter)(&value)) && value) + return value.get(); + return std::wstring(); +} + +// Reads a bool property, returning false on failure. +bool GetOptionBool( + ICoreWebView2ExperimentalClusterEnvironmentOptions* options, + HRESULT (STDMETHODCALLTYPE ICoreWebView2ExperimentalClusterEnvironmentOptions::*getter)( + BOOL*)) +{ + BOOL value = FALSE; + if (SUCCEEDED((options->*getter)(&value))) + return value != FALSE; + return false; +} + +// Reads the release-channels mask, falling back to the documented default. +COREWEBVIEW2_RELEASE_CHANNELS GetOptionReleaseChannels( + ICoreWebView2ExperimentalClusterEnvironmentOptions* options) +{ + COREWEBVIEW2_RELEASE_CHANNELS value = kAllReleaseChannels; + if (SUCCEEDED(options->get_ReleaseChannels(&value))) + return value; + return kAllReleaseChannels; +} + +// Reads the channel search order, falling back to the documented default. +COREWEBVIEW2_CHANNEL_SEARCH_KIND GetOptionChannelSearchKind( + ICoreWebView2ExperimentalClusterEnvironmentOptions* options) +{ + COREWEBVIEW2_CHANNEL_SEARCH_KIND value = COREWEBVIEW2_CHANNEL_SEARCH_KIND_MOST_STABLE; + if (SUCCEEDED(options->get_ChannelSearchKind(&value))) + return value; + return COREWEBVIEW2_CHANNEL_SEARCH_KIND_MOST_STABLE; +} + +// Renders a release-channels mask as "Stable | Beta", or "None" when empty. +std::wstring FormatReleaseChannels(COREWEBVIEW2_RELEASE_CHANNELS channels) +{ + const std::pair kNames[] = { + {COREWEBVIEW2_RELEASE_CHANNELS_STABLE, L"Stable"}, + {COREWEBVIEW2_RELEASE_CHANNELS_BETA, L"Beta"}, + {COREWEBVIEW2_RELEASE_CHANNELS_DEV, L"Dev"}, + {COREWEBVIEW2_RELEASE_CHANNELS_CANARY, L"Canary"}, + }; + + std::wstring result; + for (const auto& entry : kNames) + { + if ((channels & entry.first) == 0) + continue; + if (!result.empty()) + result += L" | "; + result += entry.second; + } + return result.empty() ? L"None" : result; +} + +// Renders a channel search order as its enum name. +PCWSTR FormatChannelSearchKind(COREWEBVIEW2_CHANNEL_SEARCH_KIND kind) +{ + return kind == COREWEBVIEW2_CHANNEL_SEARCH_KIND_LEAST_STABLE ? kLeastStableLabel + : kMostStableLabel; +} + +// Builds a cluster options object from |spec|. +ComPtr BuildClusterOptions( + const ClusterEnvironmentSpec& spec) +{ + auto options = Make(); + CHECK_FAILURE(options->put_ClusterName(spec.clusterName.c_str())); + if (!spec.language.empty()) + CHECK_FAILURE(options->put_Language(spec.language.c_str())); + if (!spec.additionalBrowserArguments.empty()) + CHECK_FAILURE( + options->put_AdditionalBrowserArguments(spec.additionalBrowserArguments.c_str())); + CHECK_FAILURE(options->put_AllowSingleSignOnUsingOSPrimaryAccount( + spec.allowSingleSignOn ? TRUE : FALSE)); + CHECK_FAILURE( + options->put_EnableTrackingPrevention(spec.enableTrackingPrevention ? TRUE : FALSE)); + CHECK_FAILURE(options->put_AreBrowserExtensionsEnabled( + spec.areBrowserExtensionsEnabled ? TRUE : FALSE)); + CHECK_FAILURE( + options->put_PerHostProfileIsolation(spec.perHostProfileIsolation ? TRUE : FALSE)); + CHECK_FAILURE(options->put_ReleaseChannels(spec.releaseChannels)); + CHECK_FAILURE(options->put_ChannelSearchKind(spec.channelSearchKind)); + return options; +} + +// Opens a new sample-app window whose WebView is hosted in the shared cluster +// environment. AppWindow deletes itself when its window is closed. +void OpenWindowInSharedEnvironment(ICoreWebView2Environment* environment, bool isMainWindow) +{ + WebViewCreateOption opt; + new AppWindow( + IDM_CREATION_MODE_WINDOWED, opt, /*initialUri=*/L"", /*userDataFolderParam=*/L"", + isMainWindow, /*webviewCreatedCallback=*/nullptr, /*customWindowRect=*/false, + /*windowRect=*/{0}, /*shouldHaveToolbar=*/true, /*isPopup=*/false, + /*providedEnvironment=*/environment); +} + +// Builds a human-readable, multi-line summary of a cluster options object. +std::wstring FormatClusterOptions(ICoreWebView2ExperimentalClusterEnvironmentOptions* options) +{ + std::wostringstream summary; + summary << L"ClusterName: " + << GetOptionString( + options, + &ICoreWebView2ExperimentalClusterEnvironmentOptions::get_ClusterName) + << L"\r\nLanguage: " + << GetOptionString( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions::get_Language) + << L"\r\nAdditionalBrowserArguments: " + << GetOptionString( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AdditionalBrowserArguments) + << L"\r\nAllowSingleSignOnUsingOSPrimaryAccount: " + << (GetOptionBool( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AllowSingleSignOnUsingOSPrimaryAccount) + ? L"true" + : L"false") + << L"\r\nEnableTrackingPrevention: " + << (GetOptionBool( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_EnableTrackingPrevention) + ? L"true" + : L"false") + << L"\r\nAreBrowserExtensionsEnabled: " + << (GetOptionBool( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AreBrowserExtensionsEnabled) + ? L"true" + : L"false") + << L"\r\nPerHostProfileIsolation: " + << (GetOptionBool( + options, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_PerHostProfileIsolation) + ? L"true" + : L"false") + << L"\r\nReleaseChannels: " + << FormatReleaseChannels(GetOptionReleaseChannels(options)) + << L"\r\nChannelSearchKind: " + << FormatChannelSearchKind(GetOptionChannelSearchKind(options)); + return summary.str(); +} + +// Returns true if the requested |spec| matches the |pinned| option set on the +// scalar fields this sample exposes (the cluster name is the rendezvous key +// and is excluded). Mirrors the loader's strict, full-set equality closely +// enough for a pre-flight check before launching a second host process. +bool SpecMatchesPinned( + const ClusterEnvironmentSpec& spec, + ICoreWebView2ExperimentalClusterEnvironmentOptions* pinned) +{ + return spec.language == + GetOptionString( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions::get_Language) && + spec.additionalBrowserArguments == + GetOptionString( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AdditionalBrowserArguments) && + spec.allowSingleSignOn == + GetOptionBool( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AllowSingleSignOnUsingOSPrimaryAccount) && + spec.enableTrackingPrevention == + GetOptionBool( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_EnableTrackingPrevention) && + spec.areBrowserExtensionsEnabled == + GetOptionBool( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_AreBrowserExtensionsEnabled) && + spec.perHostProfileIsolation == + GetOptionBool( + pinned, &ICoreWebView2ExperimentalClusterEnvironmentOptions:: + get_PerHostProfileIsolation) && + spec.releaseChannels == GetOptionReleaseChannels(pinned) && + spec.channelSearchKind == GetOptionChannelSearchKind(pinned); +} + +// Shows a read-only, multi-line summary in a single scrollable text box, since +// the dialog's description field is fixed size and truncates. +void ShowClusterSummaryDialog( + HWND parent, PCWSTR title, PCWSTR prompt, const std::wstring& heading, + const std::wstring& summary) +{ + TextInputDialog::Builder(parent, title, prompt) + .AddTextArea( + heading, summary, /*readOnly=*/true, /*labelHeight=*/18, + /*inputHeight=*/160) + .Build(); +} +// The loader rejects a cluster name with a trailing space or dot, and a +// leading space names a different cluster. +std::wstring Trimmed(const std::wstring& value) +{ + const size_t first = value.find_first_not_of(L" \t\r\n"); + if (first == std::wstring::npos) + return std::wstring(); + return value.substr(first, value.find_last_not_of(L" \t\r\n") - first + 1); +} +} // namespace + +void CreateOrJoinClusterAndOpenWindow( + const ClusterEnvironmentSpec& spec, bool isMainWindow, HWND parent) +{ + auto options = BuildClusterOptions(spec); + std::wstring clusterName = spec.clusterName; + + HRESULT hr = CreateOrJoinCoreWebView2ClusterEnvironment( + options.Get(), + Callback( + [isMainWindow, parent, clusterName]( + HRESULT errorCode, + ICoreWebView2ExperimentalClusterEnvironmentCreateResult* result) -> HRESULT + { + if (FAILED(errorCode) || !result) + { + ShowFailure( + errorCode, L"CreateOrJoinCoreWebView2ClusterEnvironment failed"); + if (isMainWindow) + PostQuitMessage(1); + return S_OK; + } + + COREWEBVIEW2_CLUSTER_ENVIRONMENT_STATUS status = + COREWEBVIEW2_CLUSTER_ENVIRONMENT_STATUS_SUCCEEDED; + CHECK_FAILURE(result->get_Status(&status)); + if (status == COREWEBVIEW2_CLUSTER_ENVIRONMENT_STATUS_OPTIONS_MISMATCH) + { + // A cluster already exists for this ClusterName with a + // different pinned option set, so this host cannot join it. + ShowPinnedClusterOptions(clusterName, parent); + if (isMainWindow) + PostQuitMessage(1); + return S_OK; + } + if (status == COREWEBVIEW2_CLUSTER_ENVIRONMENT_STATUS_NOT_SUPPORTED) + { + // Reported as a status rather than a failure, so a real + // application would fall back to a private environment. + MessageBox( + parent, + L"Cluster environments are not supported in this host " + L"process. Use a private environment instead.", + L"Shared Cluster Environment", MB_OK); + if (isMainWindow) + PostQuitMessage(1); + return S_OK; + } + + // Only SUCCEEDED carries an environment. Anything else, now or + // once the enum grows, has none to hand out. + if (status != COREWEBVIEW2_CLUSTER_ENVIRONMENT_STATUS_SUCCEEDED) + { + ShowFailure(E_UNEXPECTED, L"CreateOrJoin reported an unrecognized status."); + if (isMainWindow) + PostQuitMessage(1); + return S_OK; + } + + wil::com_ptr environment; + CHECK_FAILURE(result->get_Environment(&environment)); + OpenWindowInSharedEnvironment(environment.get(), isMainWindow); + return S_OK; + }) + .Get()); + if (FAILED(hr)) + { + ShowFailure(hr, L"CreateOrJoinCoreWebView2ClusterEnvironment call failed"); + if (isMainWindow) + PostQuitMessage(1); + } +} + +void ShowPinnedClusterOptions(const std::wstring& clusterName, HWND parent) +{ + wil::com_ptr pinned; + HRESULT hr = GetCoreWebView2ClusterEnvironmentOptions(clusterName.c_str(), &pinned); + if (SUCCEEDED(hr) && !pinned) + { + std::wstring message = + L"No cluster is currently pinned for ClusterName \"" + clusterName + L"\"."; + MessageBox(parent, message.c_str(), L"Get Cluster Options", MB_OK); + return; + } + if (FAILED(hr) || !pinned) + { + ShowFailure(hr, L"Failed to read the pinned cluster options."); + return; + } + + ShowClusterSummaryDialog( + parent, L"Pinned Cluster Options", L"Options pinned for this cluster", + L"Pinned options:", FormatClusterOptions(pinned.get())); +} + +// Quotes `value` so CommandLineToArgvW in the child rebuilds it exactly. None +// of these fields forbid a space, and an unquoted one would reach the child as +// two arguments, leaving the two hosts asking for different clusters. +std::wstring QuoteForChildCommandLine(const std::wstring& value) +{ + std::wstring quoted = L"\""; + for (auto it = value.begin();; ++it) + { + size_t backslashes = 0; + while (it != value.end() && *it == L'\\') + { + ++it; + ++backslashes; + } + if (it == value.end()) + { + // These precede the closing quote, so they must not escape it. + quoted.append(backslashes * 2, L'\\'); + break; + } + if (*it == L'"') + { + // Escape the run, then the quote itself. + quoted.append(backslashes * 2 + 1, L'\\'); + } + else + { + quoted.append(backslashes, L'\\'); + } + quoted.push_back(*it); + } + quoted.push_back(L'"'); + return quoted; +} + +void LaunchClusterHostProcess(const ClusterEnvironmentSpec& spec) +{ + wchar_t exePath[MAX_PATH] = {}; + const DWORD pathLength = GetModuleFileNameW(nullptr, exePath, ARRAYSIZE(exePath)); + // A path that did not fit returns the buffer size rather than zero, so the + // truncated value has to be rejected too: launching it would either fail or + // start something other than this sample. + if (pathLength == 0 || pathLength == ARRAYSIZE(exePath)) + { + ShowFailure(HRESULT_FROM_WIN32(GetLastError()), L"Failed to locate the sample app."); + return; + } + + // Pass every option explicitly so the second host reconstructs an identical + // set and joins rather than mismatching. + std::wostringstream cmd; + cmd << L"\"" << exePath << L"\"" << L" --clustername=" + << QuoteForChildCommandLine(spec.clusterName) << L" --clusterlang=" + << QuoteForChildCommandLine(spec.language) << L" --clustersso=" + << (spec.allowSingleSignOn ? 1 : 0) << L" --clustertracking=" + << (spec.enableTrackingPrevention ? 1 : 0) << L" --clusterextensions=" + << (spec.areBrowserExtensionsEnabled ? 1 : 0) << L" --clusterisolation=" + << (spec.perHostProfileIsolation ? 1 : 0) << L" --clusterchannels=" + << static_cast(spec.releaseChannels) << L" --clustersearchkind=" + << static_cast(spec.channelSearchKind); + if (!spec.additionalBrowserArguments.empty()) + cmd << L" --clusterargs=" << QuoteForChildCommandLine(spec.additionalBrowserArguments); + std::wstring commandLine = cmd.str(); + + STARTUPINFOW startupInfo = {sizeof(startupInfo)}; + PROCESS_INFORMATION processInfo = {}; + if (!CreateProcessW( + exePath, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, + &startupInfo, &processInfo)) + { + ShowFailure( + HRESULT_FROM_WIN32(GetLastError()), + L"Failed to launch a second host process for the cluster."); + return; + } + CloseHandle(processInfo.hThread); + CloseHandle(processInfo.hProcess); +} + +ScenarioClusterEnvironment::ScenarioClusterEnvironment(AppWindow* appWindow, Mode mode) + : m_appWindow(appWindow) +{ + switch (mode) + { + case Mode::CreateOrJoin: + PromptAndCreateOrJoin(); + break; + case Mode::GetOptions: + PromptAndGetOptions(); + break; + } +} + +ScenarioClusterEnvironment::~ScenarioClusterEnvironment() +{ +} + +bool ScenarioClusterEnvironment::PromptAndCreateOrJoin() +{ + TextInputDialog dialog = + TextInputDialog::Builder( + m_appWindow->GetMainWindow(), L"Shared Cluster Environment", + L"Launch a new host process that joins this shared cluster") + .AddTextArea(kClusterNameLabel, kDefaultClusterName, false, 20, 24) + .AddTextArea(kLanguageLabel, L"", false, 20, 24) + .AddTextArea(kBrowserArgsLabel, L"", false, 20, 24) + .AddCheckBoxGroup( + kOptionsGroupLabel, + {{L"AllowSingleSignOnUsingOSPrimaryAccount", kAllowSingleSignOn, false}, + {L"EnableTrackingPrevention", kEnableTrackingPrevention, true}, + {L"AreBrowserExtensionsEnabled", kAreBrowserExtensionsEnabled, false}, + {L"PerHostProfileIsolation", kPerHostProfileIsolation, true}}) + .AddCheckBoxGroup( + kChannelsGroupLabel, {{L"Stable", COREWEBVIEW2_RELEASE_CHANNELS_STABLE, true}, + {L"Beta", COREWEBVIEW2_RELEASE_CHANNELS_BETA, true}, + {L"Dev", COREWEBVIEW2_RELEASE_CHANNELS_DEV, true}, + {L"Canary", COREWEBVIEW2_RELEASE_CHANNELS_CANARY, true}}) + .AddDropDown( + kSearchKindLabel, {kMostStableLabel, kLeastStableLabel}, + COREWEBVIEW2_CHANNEL_SEARCH_KIND_LEAST_STABLE) + .Build(); + + if (!dialog.confirmed) + return false; + + ClusterEnvironmentSpec spec; + + // Cluster name: fall back to the default when the user left it blank. + spec.clusterName = kDefaultClusterName; + auto nameIt = dialog.results.find(kClusterNameLabel); + if (nameIt != dialog.results.end()) + { + std::wstring input = Trimmed(std::get