// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Globalization; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Management.Deployment; using Windows.Foundation; namespace Microsoft.CmdPal.Ext.WinGet.Pages; public partial class InstallPackageCommand : InvokableCommand { private readonly CatalogPackage _package; private readonly StatusMessage _installBanner = new(); private IAsyncOperationWithProgress? _installAction; private IAsyncOperationWithProgress? _unInstallAction; private Task? _installTask; public PackageInstallCommandState InstallCommandState { get; private set; } public event EventHandler? InstallStateChanged; private static readonly CompositeFormat UninstallingPackage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstalling_package); private static readonly CompositeFormat InstallingPackage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_installing_package); private static readonly CompositeFormat InstallPackageFinished = System.Text.CompositeFormat.Parse(Properties.Resources.winget_install_package_finished); private static readonly CompositeFormat UninstallPackageFinished = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstall_package_finished); private static readonly CompositeFormat QueuedPackageDownload = System.Text.CompositeFormat.Parse(Properties.Resources.winget_queued_package_download); private static readonly CompositeFormat InstallPackageFinishing = System.Text.CompositeFormat.Parse(Properties.Resources.winget_install_package_finishing); private static readonly CompositeFormat QueuedPackageUninstall = System.Text.CompositeFormat.Parse(Properties.Resources.winget_queued_package_uninstall); private static readonly CompositeFormat UninstallPackageFinishing = System.Text.CompositeFormat.Parse(Properties.Resources.winget_uninstall_package_finishing); private static readonly CompositeFormat DownloadProgress = System.Text.CompositeFormat.Parse(Properties.Resources.winget_download_progress); internal bool SkipDependencies { get; set; } public InstallPackageCommand(CatalogPackage package, PackageInstallCommandState isInstalled) { _package = package; InstallCommandState = isInstalled; UpdateAppearance(); } internal void FakeChangeStatus() { InstallCommandState = InstallCommandState switch { PackageInstallCommandState.Install => PackageInstallCommandState.Uninstall, PackageInstallCommandState.Update => PackageInstallCommandState.Uninstall, PackageInstallCommandState.Uninstall => PackageInstallCommandState.Install, _ => throw new NotImplementedException(), }; UpdateAppearance(); } private void UpdateAppearance() { Icon = InstallCommandState switch { PackageInstallCommandState.Install => Icons.DownloadIcon, PackageInstallCommandState.Update => Icons.UpdateIcon, PackageInstallCommandState.Uninstall => Icons.CompletedIcon, _ => throw new NotImplementedException(), }; Name = InstallCommandState switch { PackageInstallCommandState.Install => Properties.Resources.winget_install_name, PackageInstallCommandState.Update => Properties.Resources.winget_update_name, PackageInstallCommandState.Uninstall => Properties.Resources.winget_uninstall_name, _ => throw new NotImplementedException(), }; } public override ICommandResult Invoke() { // TODO: LOCK in here, so this can only be invoked once until the // install / uninstall is done. Just use like, an atomic if (_installTask != null) { return CommandResult.KeepOpen(); } if (InstallCommandState == PackageInstallCommandState.Uninstall) { // Uninstall _installBanner.State = MessageState.Info; _installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallingPackage, _package.Name); WinGetExtensionHost.Instance.ShowStatus(_installBanner, StatusContext.Extension); var installOptions = WinGetStatics.WinGetFactory.CreateUninstallOptions(); installOptions.PackageUninstallScope = PackageUninstallScope.Any; _unInstallAction = WinGetStatics.Manager.UninstallPackageAsync(_package, installOptions); var handler = new AsyncOperationProgressHandler(OnUninstallProgress); _unInstallAction.Progress = handler; _installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction)); } else if (InstallCommandState is PackageInstallCommandState.Install or PackageInstallCommandState.Update) { // Install _installBanner.State = MessageState.Info; _installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallingPackage, _package.Name); WinGetExtensionHost.Instance.ShowStatus(_installBanner, StatusContext.Extension); var installOptions = WinGetStatics.WinGetFactory.CreateInstallOptions(); installOptions.PackageInstallScope = PackageInstallScope.Any; installOptions.SkipDependencies = SkipDependencies; _installAction = WinGetStatics.Manager.InstallPackageAsync(_package, installOptions); var handler = new AsyncOperationProgressHandler(OnInstallProgress); _installAction.Progress = handler; _installTask = Task.Run(() => TryDoInstallOperation(_installAction)); } return CommandResult.KeepOpen(); } private async void TryDoInstallOperation( IAsyncOperationWithProgress action) { try { await action.AsTask(); _installBanner.Message = InstallCommandState == PackageInstallCommandState.Uninstall ? string.Format(CultureInfo.CurrentCulture, UninstallPackageFinished, _package.Name) : string.Format(CultureInfo.CurrentCulture, InstallPackageFinished, _package.Name); _installBanner.Progress = null; _installBanner.State = MessageState.Success; _installTask = null; _ = Task.Run(async () => { await Task.Delay(2500).ConfigureAwait(false); if (_installTask == null) { WinGetExtensionHost.Instance.HideStatus(_installBanner); } }); InstallStateChanged?.Invoke(this, this); } catch (Exception ex) { _installBanner.State = MessageState.Error; _installBanner.Progress = null; _installBanner.Message = ex.Message; _installTask = null; } } private static string FormatBytes(ulong bytes) { const long KB = 1024; const long MB = KB * 1024; const long GB = MB * 1024; return bytes >= GB ? $"{bytes / (double)GB:F2} GB" : bytes >= MB ? $"{bytes / (double)MB:F2} MB" : bytes >= KB ? $"{bytes / (double)KB:F2} KB" : $"{bytes} bytes"; } private void OnInstallProgress( IAsyncOperationWithProgress operation, InstallProgress progress) { switch (progress.State) { case PackageInstallProgressState.Queued: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, QueuedPackageDownload, _package.Name); break; case PackageInstallProgressState.Downloading: if (progress.BytesRequired > 0) { var downloadText = string.Format(CultureInfo.CurrentCulture, DownloadProgress, FormatBytes(progress.BytesDownloaded), FormatBytes(progress.BytesRequired)); _installBanner.Progress ??= new ProgressState() { IsIndeterminate = false }; var downloaded = progress.BytesDownloaded / (float)progress.BytesRequired; var percent = downloaded * 100.0f; ((ProgressState)_installBanner.Progress).ProgressPercent = (uint)percent; _installBanner.Message = downloadText; } break; case PackageInstallProgressState.Installing: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallingPackage, _package.Name); _installBanner.Progress = new ProgressState() { IsIndeterminate = true }; break; case PackageInstallProgressState.PostInstall: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, InstallPackageFinishing, _package.Name); break; case PackageInstallProgressState.Finished: _installBanner.Message = Properties.Resources.winget_install_finished; // progressBar.IsIndeterminate(false); _installBanner.Progress = null; _installBanner.State = MessageState.Success; break; default: _installBanner.Message = string.Empty; break; } } private void OnUninstallProgress( IAsyncOperationWithProgress operation, UninstallProgress progress) { switch (progress.State) { case PackageUninstallProgressState.Queued: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, QueuedPackageUninstall, _package.Name); break; case PackageUninstallProgressState.Uninstalling: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallingPackage, _package.Name); _installBanner.Progress = new ProgressState() { IsIndeterminate = true }; break; case PackageUninstallProgressState.PostUninstall: _installBanner.Message = string.Format(CultureInfo.CurrentCulture, UninstallPackageFinishing, _package.Name); break; case PackageUninstallProgressState.Finished: _installBanner.Message = Properties.Resources.winget_uninstall_finished; _installBanner.Progress = null; _installBanner.State = MessageState.Success; break; default: _installBanner.Message = string.Empty; break; } } } public enum PackageInstallCommandState { Uninstall = 0, Update = 1, Install = 2, }