Open voroninp opened 1 month ago
Can confirm this also occurs in the just released .NET 8.0.400, which is the first stable version to include the --all
parameter. The issue which tracked the introduction of the new parameter is #10130 and PR is #38996.
Example output:
~#@❯ dotnet tool list --global
Package Id Version Commands
---------------------------------------------------------------------
csharpier 0.28.2 dotnet-csharpier
dotnet-counters 8.0.532401 dotnet-counters
dotnet-coverage 17.11.3 dotnet-coverage
dotnet-dump 8.0.532401 dotnet-dump
dotnet-gcdump 8.0.532401 dotnet-gcdump
dotnet-monitor 8.0.3 dotnet-monitor
dotnet-outdated-tool 4.6.4 dotnet-outdated
dotnet-stack 8.0.532401 dotnet-stack
dotnet-suggest 1.1.415701 dotnet-suggest
dotnet-symbol 8.0.532401 dotnet-symbol
dotnet-trace 8.0.532401 dotnet-trace
jetbrains.resharper.globaltools 2024.1.4 jb
microsoft.cst.devskim.cli 1.0.33 devskim
microsoft.sbom.dotnettool 2.2.6 sbom-tool
wix 5.0.0 wix
~#@❯ dotnet tool update --global
One must specify either package ID or use the update all option (--all).
~#@❯ dotnet tool update --global --all
Unhandled exception: System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.DotNet.Tools.Tool.List.ToolListGlobalOrToolPathCommand.GetPackages(Nullable`1 toolPath, Nullable`1 packageId)
at Microsoft.DotNet.Tools.Tool.Install.ToolInstallGlobalOrToolPathCommand.Execute()
at Microsoft.DotNet.Tools.Tool.Update.ToolUpdateGlobalOrToolPathCommand.Execute()
at System.CommandLine.Invocation.InvocationPipeline.Invoke(ParseResult parseResult)
at Microsoft.DotNet.Cli.Program.ProcessArgs(String[] args, TimeSpan startupTime, ITelemetry telemetryClient)
still broken in .NET 9 Preview 7
The problem is at ToolListGlobalOrToolPathCommand.cs:87, where _createToolPackageStore(toolPath)
returns null, causing chaining calls to throw NullReferenceException
.
This is eventually traced back to ToolInstallGlobalOrToolPathCommand.cs:123, where ToolListGlobalOrToolPathCommand
constructor is called with argument toolPath => { return _store; }
for parameter createToolPackageStore
. This is used to initialize the _createToolPackageStore
delegate that caused troubles as described earlier.
_store
is definitely null in this scenario. This is assigned by the constructor of ToolInstallGlobalOrToolPathCommand
, which in this case is called at ToolUpdateCommand.cs:48 using the default parameter value null
for _store
.
After reading the implementation I came to believe that _store
being null is expected here for global tool updates. I reached this hypothesis based on the following observations:
The constructor of the concrete store implementation class (ToolPackageStoreAndQuery
) takes an optional parameter nonGlobalLocation
where null
means global package store. This suggests that using null for global store is established practice.
Store being null is somewhat guarded by the ToolListGlobalOrToolPathCommand
constructor, which assigns _createToolPackageStore
to a factory method that creates ToolPackageStoreAndQuery
for the global store if the parameter is null.
Unfortunately, the guard outlined by the second bullet is not effective, because as mentioned in the second paragraph, the argument itself is not null. Instead, it's what the delegate argument returns when called that is null.
// ToolListGlobalOrToolPathCommand.cs
public ToolListGlobalOrToolPathCommand(
ParseResult result,
CreateToolPackageStore createToolPackageStore = null,
IReporter reporter = null)
: base(result)
{
// ...
// _createToolPackageStore assigned here.
// Notes that it only guards `createToolPackageStore` being null, not `createToolPackageStore()` being null.
_createToolPackageStore = createToolPackageStore ?? ToolPackageFactory.CreateToolPackageStoreQuery;
}
public IEnumerable<IToolPackage> GetPackages(DirectoryPath? toolPath, PackageId? packageId)
{
return _createToolPackageStore(toolPath).EnumeratePackages()
// =================================^ null here. Boom.
.Where((p) => PackageHasCommands(p) && PackageIdMatches(p, packageId))
.OrderBy(p => p.Id)
.ToArray();
}
// ToolInstallGlobalOrToolPathCommand.cs
public ToolInstallGlobalOrToolPathCommand(
// ...
IToolPackageStoreQuery store = null)
: base(parseResult)
{
// ...
_store = store; // `_store` assigned here. As we will see this is always null.
// ...
}
public override int Execute()
{
// ...
var toolListCommand = new ToolListGlobalOrToolPathCommand(
_parseResult
// Argument for `_createToolPackageStore` here. `_createToolPackageStore()` is always null.
, toolPath => { return _store; }
);
// ...
}
// ToolUpdateGlobalOrToolPathCommand.cs
public ToolUpdateGlobalOrToolPathCommand(ParseResult parseResult,
CreateToolPackageStoresAndDownloaderAndUninstaller createToolPackageStoreDownloaderUninstaller = null,
CreateShellShimRepository createShellShimRepository = null,
IReporter reporter = null,
IToolPackageStoreQuery _store = null) // < 5th parameter. This is never explicitly passed in and will always be default value.
: base(parseResult)
{
// ...
_toolInstallGlobalOrToolPathCommand = new ToolInstallGlobalOrToolPathCommand(
parseResult,
packageId,
_createToolPackageStoreDownloaderUninstaller,
_createShellShimRepository,
reporter: reporter,
store: _store); // Argument for `ToolInstallGlobalOrToolPathCommand._store` here. This is always null.
}
// ToolUpdateCommand.cs
public ToolUpdateCommand(
// ...
)
: base(result)
{
// ...
_toolUpdateGlobalOrToolPathCommand =
toolUpdateGlobalOrToolPathCommand
?? new ToolUpdateGlobalOrToolPathCommand(
result,
createToolPackageStoreDownloaderUninstaller,
createShellShimRepository,
reporter); // < Only 4 arguments. `_store` will use default value `null`.
// ...
}
// ToolUpdateCommand.cs
public ToolUpdateCommand(
// ...
)
: base(result)
{
// ...
_toolUpdateGlobalOrToolPathCommand =
toolUpdateGlobalOrToolPathCommand
?? new ToolUpdateGlobalOrToolPathCommand(
result,
createToolPackageStoreDownloaderUninstaller,
createShellShimRepository,
- reporter);
+ reporter,
+ ToolPackageFactory.CreateToolPackageStoreQuery());
// ...
}
I'm not sure if this would solve all problems though. I tested it (by first adding the public NuGet gallery to NuGet.config
because the built-in overrides do not have the tools I want to test with) and it no longer throws NRE. Will try to run the tests to catch any regressions.
There are existing test for the tool update -g --all
scenario, so why didn't the test catch the problem?
It is because the test used a mocked store that masked the problematic code path.
Description
When I run
dotnet tool update -g --all
CLI fails with this exception:Reproduction Steps
I am not sure, because when I run this command under
8.0.303
I get the following error:I assume it's a new feature, because when I do not specify
--all
for .NET 9 preview, I get this message:I also could not find
--all
hereExpected behavior
Either runs cucessfully or properly communicates the reason for failure
Actual behavior
Miserably fails.
Regression?
I assume so.
Known Workarounds
No response
Configuration
Here's the output of
dotnet --info
:Other information
Here's the output of
dotnet tool list -g
: