Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Adding QuickStart Playground experimental feature #2846

Merged
merged 3 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,9 @@ dotnet_style_prefer_simplified_interpolation = true:suggestion
# Spelling

spelling_exclusion_path = .\exclusion.dic

# Diagnostic configuration

# CS8305: Type is for evaluation purposes only and is subject to change or removal in future updates.
dotnet_diagnostic.CS8305.severity = suggestion

13 changes: 13 additions & 0 deletions common/Contracts/IQuickstartSetupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Threading.Tasks;

namespace DevHome.Common.Contracts;

public interface IQuickstartSetupService
{
public bool IsDevHomeAzureExtensionInstalled();

public Task InstallDevHomeAzureExtensionAsync();
}
2 changes: 1 addition & 1 deletion common/DevHome.Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.0.4" />
<PackageReference Include="Microsoft.Windows.DevHome.SDK" Version="0.400.460" />
<PackageReference Include="Microsoft.Windows.DevHome.SDK" Version="0.600.494" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
Expand Down
159 changes: 86 additions & 73 deletions common/Models/ExperimentalFeature.cs
Original file line number Diff line number Diff line change
@@ -1,75 +1,88 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Contracts;
using DevHome.Common.Services;
using DevHome.Common.TelemetryEvents;
using DevHome.Telemetry;

namespace DevHome.Common.Models;

public partial class ExperimentalFeature : ObservableObject
{
private readonly bool _isEnabledByDefault;

[ObservableProperty]
private bool _isEnabled;

public string Id { get; init; }

public bool IsVisible { get; init; }

public static ILocalSettingsService? LocalSettingsService { get; set; }

public ExperimentalFeature(string id, bool enabledByDefault, bool visible = true)
{
Id = id;
_isEnabledByDefault = enabledByDefault;
IsVisible = visible;

IsEnabled = CalculateEnabled();
}

public bool CalculateEnabled()
{
if (LocalSettingsService!.HasSettingAsync($"ExperimentalFeature_{Id}").Result)
{
return LocalSettingsService.ReadSettingAsync<bool>($"ExperimentalFeature_{Id}").Result;
}

return _isEnabledByDefault;
}

public string Name
{
get
{
var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources");
return stringResource.GetLocalized(Id + "_Name");
}
}

public string Description
{
get
{
var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources");
return stringResource.GetLocalized(Id + "_Description");
}
}

[RelayCommand]
public async Task OnToggledAsync()
{
IsEnabled = !IsEnabled;

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Contracts;
using DevHome.Common.Services;
using DevHome.Common.TelemetryEvents;
using DevHome.Telemetry;

namespace DevHome.Common.Models;

public partial class ExperimentalFeature : ObservableObject
{
private readonly bool _isEnabledByDefault;

[ObservableProperty]
private bool _isEnabled;

public string Id { get; init; }

public bool IsVisible { get; init; }

public static ILocalSettingsService? LocalSettingsService { get; set; }

public static IQuickstartSetupService? QuickstartSetupService { get; set; }

public ExperimentalFeature(string id, bool enabledByDefault, bool visible = true)
{
Id = id;
_isEnabledByDefault = enabledByDefault;
IsVisible = visible;

IsEnabled = CalculateEnabled();
}

public bool CalculateEnabled()
{
if (LocalSettingsService!.HasSettingAsync($"ExperimentalFeature_{Id}").Result)
{
return LocalSettingsService.ReadSettingAsync<bool>($"ExperimentalFeature_{Id}").Result;
}

return _isEnabledByDefault;
}

public string Name
{
get
{
var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources");
return stringResource.GetLocalized(Id + "_Name");
}
}

public string Description
{
get
{
var stringResource = new StringResource("DevHome.Settings.pri", "DevHome.Settings/Resources");
return stringResource.GetLocalized(Id + "_Description");
}
}

[RelayCommand]
public async Task OnToggledAsync()
{
IsEnabled = !IsEnabled;

await LocalSettingsService!.SaveSettingAsync($"ExperimentalFeature_{Id}", IsEnabled);

await LocalSettingsService!.SaveSettingAsync($"IsSeeker", true);

TelemetryFactory.Get<ITelemetry>().Log("ExperimentalFeature_Toggled_Event", LogLevel.Critical, new ExperimentalFeatureEvent(Id, IsEnabled));
}
}
await LocalSettingsService!.SaveSettingAsync($"IsSeeker", true);

TelemetryFactory.Get<ITelemetry>().Log("ExperimentalFeature_Toggled_Event", LogLevel.Critical, new ExperimentalFeatureEvent(Id, IsEnabled));

// To simplify setup for the Quickstart experimental feature, install the associated Dev Home Azure Extension if it's not already present
// when that feature is enabled. Those operations will only occur on Canary and Stable builds of Dev Home.
if (string.Equals(Id, "QuickstartPlayground", StringComparison.Ordinal) && IsEnabled)
{
if (!QuickstartSetupService!.IsDevHomeAzureExtensionInstalled())
{
await QuickstartSetupService!.InstallDevHomeAzureExtensionAsync();
}
}
}
}
18 changes: 16 additions & 2 deletions common/Services/AppInstallManagerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ await Task.Run(() =>

installItem.Completed += (sender, args) =>
{
tcs.SetResult(true);
if (!tcs.TrySetResult(true))
{
_log.Information("WidgetHostingService", $"{packageId} In Completed handler, RanToCompleted already set.");
}
else
{
_log.Information("WidgetHostingService", $"{packageId} In Completed handler, RanToCompleted set.");
}
};

installItem.StatusChanged += (sender, args) =>
Expand All @@ -135,7 +142,14 @@ await Task.Run(() =>
}
else if (installItem.GetCurrentStatus().InstallState == AppInstallState.Completed)
{
tcs.SetResult(true);
if (!tcs.TrySetResult(true))
{
_log.Information("WidgetHostingService", $"{packageId} In StatusChanged handler, RanToCompleted already set.");
}
else
{
_log.Information("WidgetHostingService", $"{packageId} In StatusChanged handler, RanToCompleted set.");
}
}
};
return tcs.Task;
Expand Down
2 changes: 2 additions & 0 deletions common/Services/IStringResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ namespace DevHome.Common.Services;
public interface IStringResource
{
public string GetLocalized(string key, params object[] args);

public string GetResourceFromPackage(string resource, string packageFullName);
}
51 changes: 51 additions & 0 deletions common/Services/QuickstartSetupService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Threading.Tasks;
using DevHome.Common.Contracts;
using DevHome.Common.Services;
using Serilog;

namespace DevHome.Services;

public class QuickstartSetupService(IAppInstallManagerService appInstallManagerService, IPackageDeploymentService packageDeploymentService) : IQuickstartSetupService
{
#if CANARY_BUILD
private const string AzureExtensionStorePackageId = "9NBVFRMSFXHW";
private const string AzureExtensionPackageFamilyName = "Microsoft.Windows.DevHomeAzureExtension.Canary_8wekyb3d8bbwe";
#elif STABLE_BUILD
private const string AzureExtensionStorePackageId = "9MV8F79FGXTR";
private const string AzureExtensionPackageFamilyName = "Microsoft.Windows.DevHomeAzureExtension_8wekyb3d8bbwe";
#else
private const string AzureExtensionStorePackageId = "";
private const string AzureExtensionPackageFamilyName = "";
#endif

private readonly ILogger _log = Log.ForContext("SourceContext", nameof(QuickstartSetupService));

private readonly IAppInstallManagerService _appInstallManagerService = appInstallManagerService;
private readonly IPackageDeploymentService _packageDeploymentService = packageDeploymentService;

public bool IsDevHomeAzureExtensionInstalled()
{
#if CANARY_BUILD || STABLE_BUILD
var packages = _packageDeploymentService.FindPackagesForCurrentUser(AzureExtensionPackageFamilyName);
return packages.Any();
#endif
return true;
}

public async Task InstallDevHomeAzureExtensionAsync()
{
try
{
_log.Information("Installing DevHomeAzureExtension");
await _appInstallManagerService.TryInstallPackageAsync(AzureExtensionStorePackageId);
}
catch (Exception ex)
{
_log.Error(ex, "Installing DevHomeAzureExtension failed");
}
}
}
27 changes: 27 additions & 0 deletions common/Services/StringResource.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Globalization;
using Microsoft.Windows.ApplicationModel.Resources;
using Windows.Win32;
using Windows.Win32.Foundation;

namespace DevHome.Common.Services;

public class StringResource : IStringResource
{
private const int MaxBufferLength = 1024;

private readonly ResourceLoader _resourceLoader;

/// <summary>
Expand Down Expand Up @@ -52,4 +57,26 @@ public string GetLocalized(string key, params object[] args)

return string.IsNullOrEmpty(value) ? key : value;
}

/// <summary>
/// Gets the string of a ms-resource for a given package.
/// </summary>
/// <param name="resource">the ms-resource:// path to a resource in an app package's pri file.</param>
/// <param name="packageFullName">the package containing the resource.</param>
/// <returns>The retrieved string represented by the resource key.</returns>
public unsafe string GetResourceFromPackage(string resource, string packageFullName)
{
var indirectPathToResource = "@{" + packageFullName + "?" + resource + "}";
Span<char> outputBuffer = new char[MaxBufferLength];

fixed (char* outBufferPointer = outputBuffer)
{
fixed (char* resourcePathPointer = indirectPathToResource)
{
var res = PInvoke.SHLoadIndirectString(resourcePathPointer, new PWSTR(outBufferPointer), (uint)outputBuffer.Length);
res.ThrowOnFailure();
return new string(outputBuffer.TrimEnd('\0'));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics.Tracing;
using DevHome.Telemetry;
using Microsoft.Diagnostics.Telemetry.Internal;

namespace DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground;

[EventData]
public class FeedbackSubmitted(bool isPositive, string message) : EventBase
{
public bool IsPositive { get; } = isPositive;

public string Message { get; } = message;

public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;

public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
{
// No sensitive strings to replace.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics.Tracing;
using DevHome.Telemetry;
using Microsoft.Diagnostics.Telemetry.Internal;

namespace DevHome.Common.TelemetryEvents.SetupFlow.QuickstartPlayground;

[EventData]
public class GenerateButtonClicked(string prompt) : EventBase
{
public string Prompt { get; } = prompt;

public override PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;

public override void ReplaceSensitiveStrings(Func<string, string> replaceSensitiveStrings)
{
// No sensitive strings to replace.
}
}
Loading