Skip to content

First-run workload experience #34322

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

Merged
merged 16 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
ca96fae
Added WorkloadIntegrityChecker to use as a sidecar for running the In…
MiYanni Jul 14, 2023
4396ec8
Merge branch 'main' into FirstRunWorkloadExperience
MiYanni Jul 14, 2023
fea7f40
Removed using cleanup only changes. Just going to put this in a big c…
MiYanni Jul 24, 2023
a46baba
Testing using minimal for workload install output.
MiYanni Jul 24, 2023
8e67377
Merge branch 'main' into FirstRunWorkloadExperience
MiYanni Jul 24, 2023
dd3eeff
Added SkipWorkloadIntegrityCheck via an environment variable so that …
MiYanni Jul 25, 2023
2c23616
Fix broken tests from configuration change.
MiYanni Jul 27, 2023
ae65a33
Added 2 failsafe states where the integrity check will be skipped if …
MiYanni Jul 29, 2023
cec578a
Merge branch 'main' into FirstRunWorkloadExperience
MiYanni Jul 29, 2023
4a3aadd
Added try/catch so the integrity check doesn't hinder executing the c…
MiYanni Jul 29, 2023
de216a6
Added a message that tells the user when the workload integrity check…
MiYanni Aug 1, 2023
8884a8d
Added milliseconds to the workload log name for MSI-based installs. W…
MiYanni Aug 2, 2023
bd0ea1a
Forgot to add the DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK to the environ…
MiYanni Aug 2, 2023
8208350
Set the default value for the workload integrity check skip based on …
MiYanni Aug 10, 2023
38ec517
Updated WorkloadIntegrityCheckError to have clearer wording and prope…
MiYanni Aug 16, 2023
6fc3eaa
Added environment variables used via EnvironmentProvider to Environme…
MiYanni Aug 16, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,20 @@ public class DotnetFirstRunConfiguration

public bool NoLogo { get; }

public bool SkipWorkloadIntegrityCheck { get; }

public DotnetFirstRunConfiguration(
bool generateAspNetCertificate,
bool telemetryOptout,
bool addGlobalToolsToPath,
bool nologo)
bool nologo,
bool skipWorkloadIntegrityCheck)
{
GenerateAspNetCertificate = generateAspNetCertificate;
TelemetryOptout = telemetryOptout;
AddGlobalToolsToPath = addGlobalToolsToPath;
NoLogo = nologo;
SkipWorkloadIntegrityCheck = skipWorkloadIntegrityCheck;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

namespace Microsoft.DotNet.Cli.NuGetPackageDownloader
{
// TODO: Never name a class the same name as the namespace. Update either for easier type resolution.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this TODO for (foreshadowing) future cleanup purposes.

internal class NuGetPackageDownloader : INuGetPackageDownloader
{
private readonly SourceCacheContext _cacheSettings;
Expand Down
39 changes: 27 additions & 12 deletions src/Cli/dotnet/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
using Microsoft.DotNet.Configurer;
using Microsoft.DotNet.ShellShim;
using Microsoft.Extensions.EnvironmentAbstractions;
using LocalizableStrings = Microsoft.DotNet.Cli.Utils.LocalizableStrings;
using Microsoft.DotNet.CommandFactory;
using NuGet.Frameworks;
using CommandResult = System.CommandLine.Parsing.CommandResult;
using System.CommandLine;
using Microsoft.DotNet.Workloads.Workload;

namespace Microsoft.DotNet.Cli
{
Expand Down Expand Up @@ -162,14 +162,13 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry

var environmentProvider = new EnvironmentProvider();

bool generateAspNetCertificate =
environmentProvider.GetEnvironmentVariableAsBool("DOTNET_GENERATE_ASPNET_CERTIFICATE", defaultValue: true);
bool telemetryOptout =
environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
bool addGlobalToolsToPath =
environmentProvider.GetEnvironmentVariableAsBool("DOTNET_ADD_GLOBAL_TOOLS_TO_PATH", defaultValue: true);
bool nologo =
environmentProvider.GetEnvironmentVariableAsBool("DOTNET_NOLOGO", defaultValue: false);
bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool("DOTNET_GENERATE_ASPNET_CERTIFICATE", defaultValue: true);
bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool("DOTNET_ADD_GLOBAL_TOOLS_TO_PATH", defaultValue: true);
bool nologo = environmentProvider.GetEnvironmentVariableAsBool("DOTNET_NOLOGO", defaultValue: false);
bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool("DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK",
// Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false.
defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment());

ReportDotnetHomeUsage(environmentProvider);

Expand All @@ -186,7 +185,8 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry
generateAspNetCertificate: generateAspNetCertificate,
telemetryOptout: telemetryOptout,
addGlobalToolsToPath: addGlobalToolsToPath,
nologo: nologo);
nologo: nologo,
skipWorkloadIntegrityCheck: skipWorkloadIntegrityCheck);

ConfigureDotNetForFirstTimeUse(
firstTimeUseNoticeSentinel,
Expand Down Expand Up @@ -291,7 +291,7 @@ private static void ReportDotnetHomeUsage(IEnvironmentProvider provider)

Reporter.Verbose.WriteLine(
string.Format(
LocalizableStrings.DotnetCliHomeUsed,
Utils.LocalizableStrings.DotnetCliHomeUsed,
home,
CliFolderPathCalculator.DotnetHomeVariableName));
}
Expand All @@ -305,16 +305,18 @@ private static void ConfigureDotNetForFirstTimeUse(
IEnvironmentProvider environmentProvider,
Dictionary<string, double> performanceMeasurements)
{
var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists();
var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider);
var commandFactory = new DotNetCommandFactory(alwaysRunOutOfProc: true);
var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator();
var reporter = Reporter.Output;
var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
firstTimeUseNoticeSentinel,
aspNetCertificateSentinel,
aspnetCertificateGenerator,
toolPathSentinel,
dotnetFirstRunConfiguration,
Reporter.Output,
reporter,
environmentPath,
performanceMeasurements);

Expand All @@ -324,6 +326,19 @@ private static void ConfigureDotNetForFirstTimeUse(
{
DotDefaultPathCorrector.Correct();
}

if (isFirstTimeUse && !dotnetFirstRunConfiguration.SkipWorkloadIntegrityCheck)
{
try
{
WorkloadIntegrityChecker.RunFirstUseCheck(reporter);
}
catch (Exception)
{
// If the workload check fails for any reason, we want to eat the failure and continue running the command.
reporter.WriteLine(Workloads.Workload.LocalizableStrings.WorkloadIntegrityCheckError.Yellow());
}
}
}

private static void InitializeProcess()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,11 @@
<data name="WorkloadInfoDescription" xml:space="preserve">
<value>Display information about installed workloads.</value>
</data>
</root>
<data name="WorkloadIntegrityCheck" xml:space="preserve">
<value>Checking the state of installed workloads...</value>
</data>
<data name="WorkloadIntegrityCheckError" xml:space="preserve">
<value>An issue was encountered when verifying workloads. For more information, run "dotnet workload update".</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure dotnet workload update is the right guidance here to get the error message. For example, if a download failed during the workload integrity check due to network issues, then when you ran dotnet workload update you might not get the same error.

I'm not sure exactly what we should say here though. Any ideas @baronfel?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly there will be scenarios in which dotnet workload update won't show the exact same issue encountered in first-run. However, the point of the first-run check is to validate that the workloads are in a good state. If it fails, it shouldn't impede a user's command. If they see this message in the first command they run and the first command is, say, dotnet build and their build fails, it gives them another avenue to investigate. dotnet workload update could fix their scenario, potentially. This first-run check is just a band-aid knee-pad to help avoid potential pain for the user.

One idea is to have error codes for the CLI that go to articles about potential reasons for the errors and possible workarounds. Not sure if we have that many CLI-specific problems were we need that kind of error system, though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the "when" or replace with "while". The string reads odd to me

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And can you lock the dotnet workload update string? You can inlcude a comment in the resx, like {Locked="dotnet workload update"}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the when and added the formatted {Locked} comment to the resource.

<comment>Do not translate "dotnet workload update" as it is a CLI command.</comment>
</data>
</root>
13 changes: 7 additions & 6 deletions src/Cli/dotnet/commands/dotnet-workload/WorkloadCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ public WorkloadCommandBase(

TempDirectoryPath = !string.IsNullOrWhiteSpace(tempDirPath)
? tempDirPath
: !string.IsNullOrWhiteSpace(parseResult.GetValue(WorkloadInstallCommandParser.TempDirOption))
? parseResult.GetValue(WorkloadInstallCommandParser.TempDirOption)
: PathUtilities.CreateTempSubdirectory();
: (!string.IsNullOrWhiteSpace(parseResult.GetValue(WorkloadInstallCommandParser.TempDirOption))
? parseResult.GetValue(WorkloadInstallCommandParser.TempDirOption)
: PathUtilities.CreateTempSubdirectory());

TempPackagesDirectory = new DirectoryPath(Path.Combine(TempDirectoryPath, "dotnet-sdk-advertising-temp"));

Expand All @@ -123,17 +123,18 @@ public WorkloadCommandBase(
/// <param name="parseResult">The results of parsing the command line.</param>
/// <returns><see langword="true"/> if signatures of packages and installers should be verified.</returns>
/// <exception cref="GracefulException" />
private static bool ShouldVerifySignatures(ParseResult parseResult)
private static bool ShouldVerifySignatures(ParseResult parseResult) =>
ShouldVerifySignatures(parseResult.GetValue(WorkloadInstallCommandParser.SkipSignCheckOption));

public static bool ShouldVerifySignatures(bool skipSignCheck = false)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method needed to be public for workload integrity check.

{
if (!SignCheck.IsDotNetSigned())
{
// Can't enforce anything if we already allowed an unsigned dotnet to be installed.
return false;
}

bool skipSignCheck = parseResult.GetValue(WorkloadInstallCommandParser.SkipSignCheckOption);
bool policyEnabled = SignCheck.IsWorkloadSignVerificationPolicySet();

if (skipSignCheck && policyEnabled)
{
// Can't override the global policy by using the skip option.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Workloads.Workload.Install;
using Microsoft.Extensions.EnvironmentAbstractions;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using NuGet.Common;

namespace Microsoft.DotNet.Workloads.Workload
{
internal static class WorkloadIntegrityChecker
{
public static void RunFirstUseCheck(IReporter reporter)
{
var creationParameters = new WorkloadResolverFactory.CreationParameters()
{
CheckIfFeatureBandManifestExists = true,
UseInstalledSdkVersionForResolver = true
};

var creationResult = WorkloadResolverFactory.Create(creationParameters);
var sdkFeatureBand = new SdkFeatureBand(creationResult.SdkVersion);
var verifySignatures = WorkloadCommandBase.ShouldVerifySignatures();
var tempPackagesDirectory = new DirectoryPath(PathUtilities.CreateTempSubdirectory());
var packageDownloader = new NuGetPackageDownloader(
tempPackagesDirectory,
verboseLogger: new NullLogger(),
verifySignatures: verifySignatures);

var installer = WorkloadInstallerFactory.GetWorkloadInstaller(
reporter,
sdkFeatureBand,
creationResult.WorkloadResolver,
VerbosityOptions.normal,
creationResult.UserProfileDir,
verifySignatures,
packageDownloader,
creationResult.DotnetPath);
var repository = installer.GetWorkloadInstallationRecordRepository();
var installedWorkloads = repository.GetInstalledWorkloads(sdkFeatureBand);

if (installedWorkloads.Any())
{
reporter.WriteLine(LocalizableStrings.WorkloadIntegrityCheck);
CliTransaction.RunNew(context => installer.InstallWorkloads(installedWorkloads, sdkFeatureBand, context));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it may be better to first check if there are any missing workload packs, and only if that's the case try to install them. The difference would be that we could print a message saying that workload packs are missing and that they are being installed. This may be important for msi-based installs, as when it tries to install the workload packs it should pop up an elevation prompt, and a clearer message might help people understand what it's for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to keep the changes simplistic since I wasn't sure if we'd be changing anything else related to workload installs. So, install workloads currently does this kind of check:
image

The check is within the transaction context. Not sure if that is necessary?? Like, we could just pre-filter the packs before creating the context. Looks like it is only done for message purposes. I didn't want to change this stuff too much as testing the first-run scenario is really time consuming.

If we want to change the messaging, should there really be different messaging between this new process and that of workload update/workload install? Testing the CLI command is fairly straight-forward compared to what this PR is fixing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think maybe doing a separate endeavor (different PR) to clean up the workload logging would be appropriate. I've already pointed out in one of the workload meeting chats about how when workloads are already up-to-date, we output 3 lines per pack, which seems a bit noisy.

image

I'll make a separate issue for this work.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created issue to handle this work: #34417

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reducing the spew for updates would be great, but for the first run check, I think it's best if we print out nothing if everything is up to date, not even a "Checking the state of installed workloads..." message. Then if there are updates needed, we could print something like "Installing updated workload packs".

To do so, we could add a HasMissingWorkloadPacks method to IInstaller, and do a bit of refactoring to the installer implementations so that they share code between the new method and the InstallWorkloads method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that effort would go into the #34417 issue since it requires extracting out the logic for handling workload installation/messaging as to not be within the command logic directly. Right now, everything is tightly coupled to the concept of running as a CLI command.

That kind of 'tailored' experience could be unit tested when it is refactored, but I'm not planning on refactoring workload installation logic to simply update the text displayed for the initial implementation of this feature. Upon refactoring, we could add some tests within GivenADotnetFirstTimeUseConfigurer.cs and GivenADotnetFirstTimeUseConfigurerWithStateSetup.cs to account for the appropriate messaging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be ok with a separate cleanup PR since it will likely need to touch code that's unrelated to the first run experience and possibly have some test fallout as well.

reporter.WriteLine("----------------");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ IEnumerable<PackInfo> GetPacksInWorkloads(IEnumerable<WorkloadId> workloadIds)
return packs;
}


public void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, ITransactionContext transactionContext, DirectoryPath? offlineCache = null)
{
var packInfos = GetPacksInWorkloads(workloadIds);
Expand Down Expand Up @@ -189,7 +188,7 @@ public void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand

public void RepairWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand sdkFeatureBand, DirectoryPath? offlineCache = null)
{
// TODO: Actually re-extract the packs to fix any corrupted files.
// TODO: Actually re-extract the packs to fix any corrupted files.
CliTransaction.RunNew(context => InstallWorkloads(workloadIds, sdkFeatureBand, context, offlineCache));
}

Expand Down
3 changes: 0 additions & 3 deletions src/Cli/dotnet/commands/dotnet-workload/install/IInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ internal interface IInstaller : IWorkloadManifestInstaller
void ReplaceWorkloadResolver(IWorkloadResolver workloadResolver);

void Shutdown();


}

// Interface to pass to workload manifest updater
Expand Down Expand Up @@ -63,6 +61,5 @@ public WorkloadDownload(string id, string nuGetPackageId, string nuGetPackageVer
NuGetPackageId = nuGetPackageId;
NuGetPackageVersion = nuGetPackageVersion;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -488,9 +488,7 @@ public void InstallWorkloads(IEnumerable<WorkloadId> workloadIds, SdkFeatureBand
RollBackMsiInstall(msiToInstall);
}
});

}

}

void RollBackMsiInstall(WorkloadDownload msiToRollback, DirectoryPath? offlineCache = null)
Expand Down Expand Up @@ -1023,7 +1021,7 @@ public static NetSdkMsiInstallerClient Create(
string tempDirPath = null,
RestoreActionConfig restoreActionConfig = null)
{
TimestampedFileLogger logger = new(Path.Combine(Path.GetTempPath(), $"Microsoft.NET.Workload_{Environment.ProcessId}_{DateTime.Now:yyyyMMdd_HHmmss}.log"));
TimestampedFileLogger logger = new(Path.Combine(Path.GetTempPath(), $"Microsoft.NET.Workload_{Environment.ProcessId}_{DateTime.Now:yyyyMMdd_HHmmss_fff}.log"));
InstallClientElevationContext elevationContext = new(logger);

if (nugetPackageDownloader == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ internal class WorkloadInstallerFactory
public static IInstaller GetWorkloadInstaller(
IReporter reporter,
SdkFeatureBand sdkFeatureBand,
IWorkloadResolver workloadResolver,
IWorkloadResolver workloadResolver,
VerbosityOptions verbosity,
string userProfileDir,
bool verifySignatures,
INuGetPackageDownloader nugetPackageDownloader = null,
string dotnetDir = null,
string dotnetDir = null,
string tempDirPath = null,
PackageSourceLocation packageSourceLocation = null,
RestoreActionConfig restoreActionConfig = null,
RestoreActionConfig restoreActionConfig = null,
bool elevationRequired = true)
{
dotnetDir = string.IsNullOrWhiteSpace(dotnetDir) ? Path.GetDirectoryName(Environment.ProcessPath) : dotnetDir;
Expand Down Expand Up @@ -61,13 +61,13 @@ public static IInstaller GetWorkloadInstaller(

private static bool CanWriteToDotnetRoot(string dotnetDir = null)
{
dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath);
dotnetDir ??= Path.GetDirectoryName(Environment.ProcessPath);
try
{
var testPath = Path.Combine(dotnetDir, "metadata", Path.GetRandomFileName());
if (Directory.Exists(Path.GetDirectoryName(testPath)))
{
using (FileStream fs = File.Create(testPath, 1, FileOptions.DeleteOnClose)) { }
using FileStream fs = File.Create(testPath, 1, FileOptions.DeleteOnClose);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private static WorkloadManifestUpdater GetInstance(string userProfileDir)
reporter,
verifySignatures: SignCheck.IsDotNetSigned());
var installer = WorkloadInstallerFactory.GetWorkloadInstaller(reporter, new SdkFeatureBand(sdkVersion),
workloadResolver, Cli.VerbosityOptions.normal, userProfileDir, verifySignatures: false);
workloadResolver, VerbosityOptions.normal, userProfileDir, verifySignatures: false);
var workloadRecordRepo = installer.GetWorkloadInstallationRecordRepository();

return new WorkloadManifestUpdater(reporter, workloadResolver, nugetPackageDownloader, userProfileDir, tempPackagesDir.Value, workloadRecordRepo, installer);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Cli.Utils;
Expand Down Expand Up @@ -32,7 +31,6 @@ public class CreationResult
public ReleaseVersion SdkVersion { get; set; }
public ReleaseVersion InstalledSdkVersion { get; set; }
public IWorkloadResolver WorkloadResolver { get; set; }

}

public static CreationResult Create(CreationParameters parameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ public WorkloadUpdateCommand(
_workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, sdkFeatureBand: _sdkFeatureBand);
}



public override int Execute()
{
if (!string.IsNullOrWhiteSpace(_downloadToCacheOption))
Expand All @@ -67,7 +65,7 @@ public override int Execute()
}
else if (_printDownloadLinkOnly)
{
var packageUrls = GetUpdatablePackageUrlsAsync(_includePreviews).GetAwaiter().GetResult();
var packageUrls = GetUpdatablePackageUrlsAsync(_includePreviews).GetAwaiter().GetResult();
Reporter.WriteLine("==allPackageLinksJsonOutputStart==");
Reporter.WriteLine(JsonSerializer.Serialize(packageUrls, new JsonSerializerOptions() { WriteIndented = true }));
Reporter.WriteLine("==allPackageLinksJsonOutputEnd==");
Expand Down Expand Up @@ -164,7 +162,6 @@ private void UpdateWorkloadsWithInstallRecord(
{
// Nothing to roll back at this level, InstallWorkloadManifest and InstallWorkloadPacks handle the transaction rollback
});

}

private async Task DownloadToOfflineCacheAsync(DirectoryPath offlineCache, bool includePreviews)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading