Skip to content

Commit

Permalink
Hide WinRT - Part I (#277)
Browse files Browse the repository at this point in the history
I'm sorry for this PR being so big.
I took a top-down approach in attempt to let the commits tell a story
about how we are approaching this refactoring. Or should I say rewrite
instead? Anyways, there are more commits than files touched. It's better
reviewed in a commit-by-commit fashion, in my opinion. I had to do it
three times to get the storytelling right. I hope it helps.

This focus on the Flutter side. It is more complicated than the agent
side, IMHO. [The second
commit](d2fe835)
is the key. All the rest is either making it possible or adapting to the
changes introduced to make it possible. (Well, except for the first,
which is just a QoL thing - I hate when GitHub flags me because a
missing newline at EOF).

The essence is: we are hiding WinRT types (including the coroutines)
from the MS Store API wrappers, except in the very bottom and very top
layers, which can afford being Windows-specific.

Since the purchase request must be initiated in the main UI thread and
yet must be non-blocking, we leverage the `IAsyncOperation::Completed()`
method to supply a completion handler callback to deal with the result
and report back to the Flutter land whether the operation succeeded or
not. That's the only operation that will remain asynchronous. The rest
is progressively being turned into synchronous, blocking API, such as
fetching a product. That required splitting the act of fetching the
product (which, due being a blocking call must be called in a background
thread) from the acting of purchasing. That split motivated me to hide
the method `Product::PromptUserForPurchase` and re-expose it in a child
class only acessible from the `ClientStoreService` class, which is
intended to be used in GUI only. This way it's harder to attempt to call
such method without having a parent window to render the native dialogs
on top of.

The next PR will build on top of this to complete removing the
coroutines and hiding the WinRT on the part of the code that is relevant
to the agent.

NOTE: Git didn't handle well the file renaming on the pair
`base/Context.[ch]pp` into `base/impl/StoreContext.[ch]pp`. That
renaming is to reinforce that it's one implementation, others will come
(talking to the store mock on Windows and on Linux).

Talking about mock implementations, I left an easter egg: This is how
we'll select production vs mock API (when we implement it):

![image](https://github.com/canonical/ubuntu-pro-for-windows/assets/11138291/415446f0-d257-4806-857a-a02f73f1ae1b)


CMake detects that environment variable when building the Flutter app
and selects the appropriate sources and also set the preprocessor macro
with the same name. [See the third
commit](d2fe835)
for the details. That trick is even more transparent on MSBuild (I
believe that will be needed to build the `storeapi.dll`). Essentially,
environment variables are valid MSBuild properties. [See the MSBuild
docs](https://learn.microsoft.com/en-us/visualstudio/msbuild/how-to-use-environment-variables-in-a-build?view=vs-2022#reference-environment-variables)
for the details. But let's leave that for later. We are still two steps
behind building with mocks.
  • Loading branch information
CarlosNihelton authored Sep 18, 2023
2 parents e350a52 + f5e972a commit 0b627e7
Show file tree
Hide file tree
Showing 20 changed files with 462 additions and 286 deletions.
2 changes: 2 additions & 0 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
BasedOnStyle: Google
---
Language: Cpp
AllowShortFunctionsOnASingleLine: All
DerivePointerAlignment: false
InsertNewlineAtEOF: true
PointerAlignment: Left
18 changes: 16 additions & 2 deletions gui/packages/p4w_ms_store/windows/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,26 @@ include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include)

# Relative to the top level cmake project which will be either this plugin or the app
# consuming it. Either way, same relative distance to the directory we are looking for.
set(STORE_API_DEFINES "")
set(STORE_API_DIR "${CMAKE_SOURCE_DIR}/../../../../storeapi/")
set(STORE_API_SRC
"${STORE_API_DIR}/gui/ClientStoreService.hpp"
"${STORE_API_DIR}/base/StoreService.hpp"
"${STORE_API_DIR}/base/Purchase.hpp"
"${STORE_API_DIR}/base/Exception.hpp"
"${STORE_API_DIR}/base/Context.cpp"
"${STORE_API_DIR}/base/Context.hpp"
"${STORE_API_DIR}/base/DefaultContext.hpp"

# API wrapper implementations
"${STORE_API_DIR}/base/impl/StoreContext.cpp"
"${STORE_API_DIR}/base/impl/StoreContext.hpp"
)
if(DEFINED ENV{UP4W_TEST_WITH_MS_STORE_MOCK})
# TODO: Change to informative warning and update the text once mock client API wrappers are avaiable.
message(FATAL_ERROR "Unsupported build with the MS Store Mock client API due environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' set to '$ENV{UP4W_TEST_WITH_MS_STORE_MOCK}'.")
list(APPEND STORE_API_DEFINES "UP4W_TEST_WITH_MS_STORE_MOCK")
else()
message(STATUS "Building with the production version of MS Store client API. Set the environment variable 'UP4W_TEST_WITH_MS_STORE_MOCK' if you want to build with the mock store API.")
endif()

# Any new source files that you add to the plugin should be added here.
list(APPEND PLUGIN_SOURCES
Expand Down Expand Up @@ -97,6 +109,8 @@ apply_standard_settings(${PLUGIN_NAME})
set_target_properties(${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden)
target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
# Further #defines we may want to pass.
target_compile_definitions(${PLUGIN_NAME} PRIVATE ${STORE_API_DEFINES})

# Source include directories and library dependencies. Add any plugin-specific
# dependencies here.
Expand Down
48 changes: 33 additions & 15 deletions gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.cpp
Original file line number Diff line number Diff line change
@@ -1,26 +1,44 @@
#include "p4w_ms_store_plugin_impl.h"

#include <flutter/encodable_value.h>

#include <base/DefaultContext.hpp>
#include <base/Exception.hpp>
#include <base/Purchase.hpp>
#include <gui/ClientStoreService.hpp>

#include <cstdint>
#include <exception>

namespace p4w_ms_store {

winrt::fire_and_forget PurchaseSubscription(
HWND topLevelWindow, std::string productId,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
try {
StoreApi::ClientStoreService service{topLevelWindow};
const auto res =
co_await service.PromptUserToSubscribe(std::move(productId));
result->Success(static_cast<int>(res));
} catch (StoreApi::Exception& err) {
result->Error(channelName, err.what());
} catch (winrt::hresult_error& err) {
result->Error(channelName, winrt::to_string(err.message()));
} catch (std::exception& err) {
result->Error(channelName, err.what());
}
std::shared_ptr<flutter::MethodResult<flutter::EncodableValue>>
result) try {
winrt::apartment_context windowContext{};
StoreApi::ClientStoreService service{topLevelWindow};
// Blocks a background thread while retrieving the product.
co_await winrt::resume_background();
const auto product = service.FetchAvailableProduct(productId);
// Resumes in the UI thread to display the native dialog.
co_await windowContext;
product.PromptUserForPurchase(
[result](StoreApi::PurchaseStatus status, int32_t error) {
if (error < 0) {
winrt::hresult_error err{error};
result->Error(channelName, winrt::to_string(err.message()));
return;
}

result->Success(static_cast<int>(status));
});
} catch (StoreApi::Exception& err) {
result->Error(channelName, err.what());
} catch (winrt::hresult_error& err) {
result->Error(channelName, winrt::to_string(err.message()));
} catch (std::exception& err) {
result->Error(channelName, err.what());
} catch (...) {
result->Error(channelName, "Unknown exception thrown in the native layer.");
}

} // namespace p4w_ms_store
11 changes: 9 additions & 2 deletions gui/packages/p4w_ms_store/windows/p4w_ms_store_plugin_impl.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
#ifndef FLUTTER_PLUGIN_P4W_MS_STORE_PLUGIN_IMPL_H_
#define FLUTTER_PLUGIN_P4W_MS_STORE_PLUGIN_IMPL_H_

#include <windows.h>
#include <winrt/base.h>

#include <flutter/encodable_value.h>
#include <flutter/flutter_view.h>
#include <flutter/method_result.h>
#include <winrt/windows.foundation.h>

#include <memory>
#include <string>


namespace p4w_ms_store {

Expand All @@ -19,7 +26,7 @@ inline HWND GetRootWindow(flutter::FlutterView* view) {

winrt::fire_and_forget PurchaseSubscription(
HWND topLevelWindow, std::string productId,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);
std::shared_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

} // namespace p4w_ms_store

Expand Down
2 changes: 1 addition & 1 deletion msix/storeapi/StoreApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Int GetSubscriptionExpirationDate(const char* productID,
try {
StoreApi::ServerStoreService service{};

*expirationUnix = service.CurrentExpirationDate(productID).get();
*expirationUnix = service.CurrentExpirationDate(productID);
return 0;

} catch (const StoreApi::Exception& err) {
Expand Down
7 changes: 5 additions & 2 deletions msix/storeapi/storeapi.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Label="Mock API" Condition="'$(UP4W_TEST_WITH_MS_STORE_MOCK)' == ''">
<DefineConstants>UP4W_TEST_WITH_MS_STORE_MOCK</DefineConstants>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
Expand Down Expand Up @@ -87,15 +90,15 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="..\..\storeapi\agent\ServerStoreService.hpp" />
<ClInclude Include="..\..\storeapi\base\Context.hpp" />
<ClInclude Include="..\..\storeapi\base\Exception.hpp" />
<ClInclude Include="..\..\storeapi\base\StoreService.hpp" />
<ClCompile Include="..\..\storeapi\base\impl\StoreContext.cpp" />
<ClInclude Include="framework.hpp" />
<ClInclude Include="StoreApi.hpp" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\..\storeapi\agent\ServerStoreService.cpp" />
<ClCompile Include="..\..\storeapi\base\Context.cpp" />
<ClCompile Include="..\..\storeapi\base\impl\StoreContext.hpp" />
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="StoreApi.cpp" />
</ItemGroup>
Expand Down
21 changes: 12 additions & 9 deletions storeapi/agent/ServerStoreService.hpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#pragma once
#include <ctime>
#include <pplawait.h>

#include "base/Context.hpp"
#include "base/DefaultContext.hpp"
#include "base/Exception.hpp"
#include "base/StoreService.hpp"

#include <chrono>
#include <cstdint>
#include <limits>

namespace StoreApi {

// Models the interesting user information the application can correlate
Expand All @@ -19,7 +23,7 @@ struct UserInfo {

// Adds functionality on top of the [StoreService] interesting to background
// server applications.
template <typename ContextType = Context>
template <typename ContextType = DefaultContext>
class ServerStoreService : public StoreService<ContextType> {
public:
// Generates the user ID key (a.k.a the JWT) provided the server AAD [token]
Expand All @@ -45,17 +49,16 @@ class ServerStoreService : public StoreService<ContextType> {
// product or the lowest int64_t otherwise (a date too far in the past). This
// raw return value suits well for crossing ABI boundaries, such as returning
// to a caller in Go.
concurrency::task<std::int64_t> CurrentExpirationDate(std::string productId) {
auto product = co_await this->GetSubscriptionProduct(productId);
std::int64_t CurrentExpirationDate(std::string productId) {
auto product = this->GetSubscriptionProduct(productId);
if (!product.IsInUserCollection()) {
co_return std::numeric_limits<std::int64_t>::lowest();
return std::numeric_limits<std::int64_t>::lowest();
}
// C++20 guarantees that std::chrono::system_clock measures UNIX time.
const auto t = winrt::clock::to_sys(product.CurrentExpirationDate());
const auto dur = t.time_since_epoch();
const auto dur = product.CurrentExpirationDate().time_since_epoch();

// just need to convert the duration to seconds.
co_return duration_cast<std::chrono::seconds>(dur).count();
return duration_cast<std::chrono::seconds>(dur).count();
}
};

Expand Down
67 changes: 0 additions & 67 deletions storeapi/base/Context.cpp

This file was deleted.

3 changes: 3 additions & 0 deletions storeapi/base/DefaultContext.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#pragma once

#include "impl/StoreContext.hpp"
6 changes: 6 additions & 0 deletions storeapi/base/Exception.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#pragma once
#include <exception>
#include <format>
#include <source_location>
#include <string>
#include <type_traits>

namespace StoreApi {

Expand All @@ -12,6 +16,7 @@ enum class ErrorCode {
NoLocalUser,
TooManyLocalUsers,
EmptyJwt,
InvalidProductId,
// ABI Boundary errors:
AllocationFailure = -10,
// - input string argument errors
Expand Down Expand Up @@ -71,4 +76,5 @@ class Exception {
m_loc.file_name(), m_loc.line(), m_loc.function_name());
}
};

} // namespace StoreApi
30 changes: 30 additions & 0 deletions storeapi/base/Purchase.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

/// Types and aliases that must be present in all Context implementations.

#include <cstdint>
#include <functional>

namespace StoreApi {
// We'll certainly want to show in the UI the result of the purchase operation
// in a localizable way. Thus we must agree on the values returned across the
// different languages involved. Since we don't control the Windows Runtime
// APIs, it wouldn't be future-proof to return the raw value of
// StorePurchaseStatus enum right away.
// This must strictly in sync the Dart PurchaseStatus enum in
// https://github.com/canonical/ubuntu-pro-for-windows/blob/main/gui/packages/p4w_ms_store/lib/p4w_ms_store_platform_interface.dart#L8-L16
// so we don't misinterpret the native call return values.
enum class PurchaseStatus : std::int8_t {
Succeeded = 0,
AlreadyPurchased = 1,
UserGaveUp = 2,
NetworkError = 3,
ServerError = 4,
Unknown = 5,
};

/// A callable the client application must provide to receive the result of the
/// asynchronous purchase operation.
using PurchaseCallback = std::function<void(PurchaseStatus, std::int32_t)>;

} // namespace StoreApi
30 changes: 13 additions & 17 deletions storeapi/base/StoreService.hpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
#pragma once
// For the underlying Store API
#include <winrt/windows.services.store.h>

// To provide coroutines capable of returning non-WinRT types.
#include <pplawait.h>

#include <array>
#include <format>
#include <string>

#include "Exception.hpp"

Expand All @@ -20,27 +17,26 @@ class StoreService {
// The underlying store context.
ContextType context{};
// We only care about subscription add-ons.
static constexpr wchar_t _productKind[] = L"Durable";
static constexpr char _productKind[] = "Durable";

// An asynchronous operation that eventually returns an instance of
// [ContextType::Product] subscription add-on matching the provided product
// [id]. Callers must ensure the underlying string pointed by the [id]
// remains valid until this function completes.
concurrency::task<typename ContextType::Product> GetSubscriptionProduct(
std::string_view id) {
auto products =
co_await context.GetProducts({{_productKind}}, {winrt::to_hstring(id)});
// A blocking operation that returns an instance of [ContextType::Product]
// subscription add-on matching the provided product [id].
typename ContextType::Product GetSubscriptionProduct(std::string id) {
std::array<const std::string, 1> ids{std::move(id)};
std::array<const std::string, 1> kinds{_productKind};
auto products = context.GetProducts(kinds, ids);
auto size = products.size();
switch (size) {
case 0:
throw Exception(ErrorCode::NoProductsFound, std::format("id={}", id));
throw Exception(ErrorCode::NoProductsFound,
std::format("id={}", ids[0]));
case 1:
co_return products[0];
return products[0];
default:
throw Exception(
ErrorCode::TooManyProductsFound,
std::format("Expected one but found {} products for id {}", size,
id));
ids[0]));
}
}
};
Expand Down
Loading

0 comments on commit 0b627e7

Please sign in to comment.