Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for generating a configuration file from machine configuration #2466

Merged
merged 21 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 16 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
59 changes: 47 additions & 12 deletions common/Extensions/WindowExExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,42 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
/// Open file picker
/// </summary>
/// <param name="window">Target window</param>
/// <param name="filters">List of type filters (e.g. *.yaml, *.txt), or empty/<c>null</c> to allow all file types</param>
/// <param name="logger">Logger instance</param>
/// <param name="filters">List of type filters (e.g. *.yaml, *.txt)</param>
/// <returns>Storage file or <c>null</c> if no file was selected</returns>
public static async Task<StorageFile?> OpenFilePickerAsync(this WindowEx window, ILogger? logger, params (string Type, string Name)[] filters)
{
var filePicker = window.FileDialogInternal<FileOpenDialog>(logger, filters);
if (filePicker.HasValue)
{
var fileName = filePicker.Value.Item1;
return await StorageFile.GetFileFromPathAsync(fileName);
}

return null;
}

/// <summary>
/// Save file dialog
/// </summary>
/// <param name="window">Target window</param>
/// <param name="logger">Logger instance</param>
/// <param name="filters">List of type filters (e.g. *.yaml, *.txt)</param>
/// <returns>Tuple with the file name and file type index or <c>null</c> if no file was selected</returns>
public static (string, int)? SaveFileDialog(this WindowEx window, ILogger? logger, params (string Type, string Name)[] filters)
{
return window.FileDialogInternal<FileSaveDialog>(logger, filters);
}

/// <summary>
/// Core implementation for file dialog
/// </summary>
/// <typeparam name="T">File dialog types</typeparam>
/// <param name="window">Target window</param>
/// <param name="logger">Logger instance</param>
/// <param name="filters">List of type filters (e.g. *.yaml, *.txt)</param>
/// <returns>Tuple with the file name and file type index or <c>null</c> if no file was selected</returns>
private static (string, int)? FileDialogInternal<T>(this WindowEx window, ILogger? logger, params (string Type, string Name)[] filters)
{
try
{
Expand All @@ -95,6 +128,7 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
}

string fileName;
int fileTypeIndex;

// File picker fails when running the application as admin.
// To workaround this issue, we instead use the Win32 picking APIs
Expand All @@ -112,11 +146,7 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
{
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);

var hr = PInvoke.CoCreateInstance<IFileOpenDialog>(
typeof(FileOpenDialog).GUID,
null,
CLSCTX.CLSCTX_INPROC_SERVER,
out var fsd);
var hr = PInvoke.CoCreateInstance<IFileDialog>(typeof(T).GUID, null, CLSCTX.CLSCTX_INPROC_SERVER, out var fileDialog);
Marshal.ThrowExceptionForHR(hr);

IShellItem ppsi;
Expand All @@ -133,10 +163,10 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
extensions.Add(extension);
}

fsd.SetFileTypes(CollectionsMarshal.AsSpan(extensions));
fileDialog.SetFileTypes(CollectionsMarshal.AsSpan(extensions));

fsd.Show(new HWND(hWnd));
fsd.GetResult(out ppsi);
fileDialog.Show(new HWND(hWnd));
fileDialog.GetResult(out ppsi);
}
finally
{
Expand All @@ -156,19 +186,24 @@ public static void SetRequestedTheme(this WindowEx window, ElementTheme theme)
ppsi.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out pFileName);
fileName = new string(pFileName);
Marshal.FreeCoTaskMem((IntPtr)pFileName.Value);

// NOTE: IFileDialog::GetFileTypeIndex method is a one-based
// index rather than zero-based.
fileDialog.GetFileTypeIndex(out var uFileTypeIndex);
fileTypeIndex = (int)uFileTypeIndex - 1;
}

return await StorageFile.GetFileFromPathAsync(fileName);
return (fileName, fileTypeIndex);
}
catch (COMException e) when (e.ErrorCode == FilePickerCanceledErrorCode)
{
// No-op: Operation was canceled by the user
return null;
}
catch (Exception e)
{
logger?.Error("File picker failed. Returning null.", e);
return null;
}

return null;
}
}
1 change: 1 addition & 0 deletions common/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
EnableWindow
CoCreateInstance
FileOpenDialog
FileSaveDialog
IFileOpenDialog
SHCreateItemFromParsingName
GetCurrentPackageFullName
Expand Down
10 changes: 9 additions & 1 deletion tools/SetupFlow/DevHome.SetupFlow/Models/ISetupTaskGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,18 @@ public interface ISetupTaskGroup
public ReviewTabViewModelBase GetReviewTabViewModel();

/// <summary>
/// Gets all the individual setup tasks that make up this group
/// Gets all the setup tasks that make up this group
/// </summary>
public IEnumerable<ISetupTask> SetupTasks
{
get;
}

/// <summary>
/// Gets all the DSC tasks that make up this group
/// </summary>
public IEnumerable<ISetupTask> DSCTasks
{
get;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using YamlDotNet.Core;
using YamlDotNet.Serialization;

namespace DevHome.SetupFlow.Models.WingetConfigure;

/// <summary>
Expand All @@ -9,6 +12,7 @@ namespace DevHome.SetupFlow.Models.WingetConfigure;
/// </summary>
public class WinGetDscSettings : WinGetConfigSettingsBase
{
[YamlMember(ScalarStyle = ScalarStyle.DoubleQuoted)]
public string Id { get; set; }

public string Source { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using DevHome.SetupFlow.Models;
using DevHome.SetupFlow.Models.WingetConfigure;
using DevHome.SetupFlow.TaskGroups;
using Serilog;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;

Expand All @@ -21,13 +22,8 @@ public enum ConfigurationFileKind

public class ConfigurationFileBuilder
{
private readonly SetupFlowOrchestrator _orchestrator;

public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator)
{
_orchestrator = orchestrator;
}

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

/// <summary>
/// Builds an object that represents a config file that can be used by WinGet Configure to install
/// apps and clone repositories.This is already formatted as valid yaml and can be written
Expand All @@ -36,21 +32,33 @@ public ConfigurationFileBuilder(SetupFlowOrchestrator orchestrator)
/// <returns>The config file object representing the yaml file.</returns>
public WinGetConfigFile BuildConfigFileObjectFromTaskGroups(IList<ISetupTaskGroup> taskGroups, ConfigurationFileKind configurationFileKind)
{
var listOfResources = new List<WinGetConfigResource>();

List<WinGetConfigResource> repoResources = [];
AmelBawa-msft marked this conversation as resolved.
Show resolved Hide resolved
List<WinGetConfigResource> appResources = [];
foreach (var taskGroup in taskGroups)
{
if (taskGroup is RepoConfigTaskGroup repoConfigGroup)
{
// Add the GitDSC resource blocks to yaml
listOfResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind));
repoResources.AddRange(GetResourcesForCloneTaskGroup(repoConfigGroup, configurationFileKind));
}
else if (taskGroup is AppManagementTaskGroup appManagementGroup)
{
// Add the WinGetDsc resource blocks to yaml
listOfResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind));
appResources.AddRange(GetResourcesForAppManagementTaskGroup(appManagementGroup, configurationFileKind));
}
}

// If Git is not added to the apps to install and there are
// repositories to clone, add Git as a pre-requisite
var isGitAdded = appResources
.Select(r => r.Settings as WinGetDscSettings)
.Any(s => s.Id == DscHelpers.GitWinGetPackageId);
if (!isGitAdded && repoResources.Count > 0)
{
appResources.Add(CreateWinGetInstallForGitPreReq());
}

List<WinGetConfigResource> listOfResources = [..appResources, ..repoResources];

if (listOfResources.Count == 0)
{
Expand Down Expand Up @@ -114,22 +122,24 @@ public string SerializeWingetFileObjectToString(WinGetConfigFile configFile)
private List<WinGetConfigResource> GetResourcesForCloneTaskGroup(RepoConfigTaskGroup repoConfigGroup, ConfigurationFileKind configurationFileKind)
{
var listOfResources = new List<WinGetConfigResource>();
var repoConfigTasks = repoConfigGroup.SetupTasks
var repoConfigTasks = repoConfigGroup.DSCTasks
.Where(task => task is CloneRepoTask)
.Select(task => task as CloneRepoTask)
.ToList();

if (repoConfigTasks.Count != 0)
{
listOfResources.Add(CreateWinGetInstallForGitPreReq());
}

foreach (var repoConfigTask in repoConfigTasks)
{
if (repoConfigTask.RepositoryToClone is GenericRepository genericRepository)
{
listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, genericRepository.RepoUri, configurationFileKind));
}
try
{
if (!repoConfigTask.RepositoryToClone.IsPrivate)
{
listOfResources.Add(CreateResourceFromTaskForGitDsc(repoConfigTask, repoConfigTask.RepositoryToClone.RepoUri, configurationFileKind));
}
}
catch (Exception e)
{
_log.Error($"Error creating a repository resource entry", e);
}
}

return listOfResources;
Expand All @@ -143,7 +153,7 @@ private List<WinGetConfigResource> GetResourcesForCloneTaskGroup(RepoConfigTaskG
private List<WinGetConfigResource> GetResourcesForAppManagementTaskGroup(AppManagementTaskGroup appManagementGroup, ConfigurationFileKind configurationFileKind)
{
var listOfResources = new List<WinGetConfigResource>();
var installList = appManagementGroup.SetupTasks
var installList = appManagementGroup.DSCTasks
.Where(task => task is InstallPackageTask)
.Select(task => task as InstallPackageTask)
.ToList();
Expand Down Expand Up @@ -177,8 +187,16 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa
{
Resource = DscHelpers.WinGetDscResource,
Id = id,
Directives = new() { AllowPrerelease = true, Description = $"Installing {arguments.PackageId}" },
Settings = new WinGetDscSettings() { Id = arguments.PackageId, Source = DscHelpers.DscSourceNameForWinGet },
Directives = new()
{
AllowPrerelease = true,
Description = $"Installing {arguments.PackageId}",
},
Settings = new WinGetDscSettings()
{
Id = arguments.PackageId,
Source = arguments.CatalogName,
},
};
}

Expand All @@ -190,16 +208,13 @@ private WinGetConfigResource CreateResourceFromTaskForWinGetDsc(InstallPackageTa
/// <returns>The WinGetConfigResource object that represents the block of yaml needed by GitDsc to clone the repository. </returns>
private WinGetConfigResource CreateResourceFromTaskForGitDsc(CloneRepoTask task, Uri webAddress, ConfigurationFileKind configurationFileKind)
{
// For normal cases, the Id will be null. This can be changed in the future when a use case for this Dsc File builder is needed outside the setup
// setup target flow. We can likely drop the if statement and just use whats in its body.
string id = null;
// WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI.
// So we add a description to the Id to make it more readable in the UI. These do not need to be localized.
var id = $"Clone {task.RepositoryName}: {task.CloneLocation.FullName}";
var gitDependsOnId = DscHelpers.GitWinGetPackageId;

if (configurationFileKind == ConfigurationFileKind.SetupTarget)
{
// WinGet configure uses the Id property to uniquely identify a resource and also to display the resource status in the UI.
// So we add a description to the Id to make it more readable in the UI. These do not need to be localized.
id = $"Clone {task.RepositoryName}" + ": " + task.CloneLocation.FullName;
gitDependsOnId = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}";
}

Expand All @@ -223,7 +238,7 @@ private WinGetConfigResource CreateWinGetInstallForGitPreReq()
return new WinGetConfigResource()
{
Resource = DscHelpers.WinGetDscResource,
Id = $"{DscHelpers.GitWinGetPackageId} | Install: {DscHelpers.GitName}",
Id = DscHelpers.GitWinGetPackageId,
Directives = new() { AllowPrerelease = true, Description = $"Installing {DscHelpers.GitName}" },
Settings = new WinGetDscSettings() { Id = DscHelpers.GitWinGetPackageId, Source = DscHelpers.DscSourceNameForWinGet },
};
Expand Down
28 changes: 24 additions & 4 deletions tools/SetupFlow/DevHome.SetupFlow/Services/PackageProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private sealed class PackageCache
/// <summary>
/// Occurs when a package selection has changed
/// </summary>
public event EventHandler<PackageViewModel> PackageSelectionChanged;
public event EventHandler SelectedPackagesItemChanged;

public PackageProvider(PackageViewModelFactory packageViewModelFactory)
{
Expand Down Expand Up @@ -107,7 +107,7 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl
_log.Debug($"Creating view model for package [{package.Id}]");
var viewModel = _packageViewModelFactory(package);
viewModel.SelectionChanged += OnPackageSelectionChanged;
viewModel.SelectionChanged += (sender, package) => PackageSelectionChanged?.Invoke(sender, package);
viewModel.VersionChanged += OnSelectedPackageVersionChanged;

// Cache if requested
if (cachePermanently)
Expand All @@ -122,10 +122,25 @@ public PackageViewModel CreateOrGet(IWinGetPackage package, bool cachePermanentl

return viewModel;
}
}

private void OnSelectedPackageVersionChanged(object sender, string version)
{
var packageViewModel = sender as PackageViewModel;
if (packageViewModel?.IsSelected == true)
{
// Notify subscribers that an item in the list of selected packages has changed
SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty);
}
}

public void OnPackageSelectionChanged(object sender, PackageViewModel packageViewModel)
{
private void OnPackageSelectionChanged(object sender, bool isSelected)
{
if (sender is not PackageViewModel packageViewModel)
{
return;
}

lock (_lock)
{
if (packageViewModel.IsSelected)
Expand Down Expand Up @@ -154,12 +169,17 @@ public void OnPackageSelectionChanged(object sender, PackageViewModel packageVie
{
_log.Debug($"Removing package [{packageViewModel.Package.Id}] from cache");
_packageViewModelCache.Remove(packageViewModel.UniqueKey);
packageViewModel.SelectionChanged -= OnPackageSelectionChanged;
packageViewModel.VersionChanged -= OnSelectedPackageVersionChanged;
}

// Remove from the selected package collection
_selectedPackages.Remove(packageViewModel);
}
}

// Notify subscribers that an item in the list of selected packages has changed
SelectedPackagesItemChanged?.Invoke(packageViewModel, EventArgs.Empty);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public static class StringResourceKey
public static readonly string EditClonePathDialog = nameof(EditClonePathDialog);
public static readonly string EditClonePathDialogUncheckCheckMark = nameof(EditClonePathDialogUncheckCheckMark);
public static readonly string FilePickerFileTypeOption = nameof(FilePickerFileTypeOption);
public static readonly string FilePickerSingleFileTypeOption = nameof(FilePickerSingleFileTypeOption);
public static readonly string FileTypeNotSupported = nameof(FileTypeNotSupported);
public static readonly string InstalledPackage = nameof(InstalledPackage);
public static readonly string InstalledPackageReboot = nameof(InstalledPackageReboot);
Expand Down
Loading
Loading