[ Tool ] Support Powershell v6+ to determine Windows version in flutter doctor (#156476)

Powershell v6+ use the executable name `pwsh.exe` instead of
`powershell.exe`. This change adds support for determining the Windows
version on systems that solely use `pwsh.exe` and don't have
`powershell.exe` on the `PATH`.

Fixes #156189.

---------

Co-authored-by: Andrew Kolos <andrewrkolos@gmail.com>
This commit is contained in:
Ben Konyi 2024-10-15 11:48:35 -04:00 committed by GitHub
parent 2b1e71b4a4
commit ad370c8fa7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 135 additions and 3 deletions

View File

@ -37,9 +37,22 @@ class WindowsVersionValidator extends DoctorValidator {
final ProcessLister _processLister;
Future<ValidationResult> _topazScan() async {
if (!_processLister.canRunPowershell()) {
return const ValidationResult(
ValidationType.missing,
<ValidationMessage>[
ValidationMessage.hint('Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH'),
],
);
}
final ProcessResult getProcessesResult = await _processLister.getProcessesWithPath();
if (getProcessesResult.exitCode != 0) {
return const ValidationResult(ValidationType.missing, <ValidationMessage>[ValidationMessage.hint('Get-Process failed to complete')]);
return const ValidationResult(
ValidationType.missing,
<ValidationMessage>[
ValidationMessage.hint('Get-Process failed to complete'),
],
);
}
final RegExp topazRegex = RegExp(kCoreProcessPattern, caseSensitive: false, multiLine: true);
final String processes = getProcessesResult.stdout as String;
@ -106,8 +119,22 @@ class ProcessLister {
final ProcessManager processManager;
static const String powershell = 'powershell';
static const String pwsh = 'pwsh';
bool canRunPowershell() {
return processManager.canRun(powershell) || processManager.canRun(pwsh);
}
Future<ProcessResult> getProcessesWithPath() async {
const String argument = 'Get-Process | Format-List Path';
return processManager.run(<String>['powershell', '-command', argument]);
const List<String> psArgs = <String>['-command', argument];
if (processManager.canRun(powershell)) {
return processManager.run(<String>[powershell, ...psArgs]);
}
if (processManager.canRun(pwsh)) {
return processManager.run(<String>[pwsh, ...psArgs]);
}
throw StateError('Failed to find $powershell or $pwsh on PATH');
}
}

View File

@ -9,6 +9,7 @@ import 'package:flutter_tools/src/windows/windows_version_validator.dart';
import 'package:test/fake.dart';
import '../src/common.dart';
import '../src/context.dart';
/// Fake [_WindowsUtils] to use for testing
class FakeValidOperatingSystemUtils extends Fake
@ -21,14 +22,22 @@ class FakeValidOperatingSystemUtils extends Fake
}
class FakeProcessLister extends Fake implements ProcessLister {
FakeProcessLister({required this.result, this.exitCode = 0});
FakeProcessLister({
required this.result,
this.exitCode = 0,
this.powershellAvailable = true,
});
final String result;
final int exitCode;
final bool powershellAvailable;
@override
Future<ProcessResult> getProcessesWithPath() async {
return ProcessResult(0, exitCode, result, null);
}
@override
bool canRunPowershell() => powershellAvailable;
}
FakeProcessLister ofdRunning() {
@ -43,6 +52,10 @@ FakeProcessLister failure() {
return FakeProcessLister(result: r'Path: "C:\Program Files\Google\Chrome\Application\chrome.exe', exitCode: 10);
}
FakeProcessLister powershellUnavailable() {
return FakeProcessLister(result: '', powershellAvailable: false);
}
/// The expected validation result object for
/// a passing windows version test
const ValidationResult validWindows10ValidationResult = ValidationResult(
@ -70,6 +83,15 @@ const ValidationResult ofdFoundRunning = ValidationResult(
statusInfo: 'Problem detected with Windows installation',
);
const ValidationResult powershellUnavailableResult = ValidationResult(
ValidationType.partial,
<ValidationMessage>[
ValidationMessage.hint('Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH'),
],
statusInfo: 'Problem detected with Windows installation',
);
const ValidationResult getProcessFailed = ValidationResult(
ValidationType.partial,
<ValidationMessage>[
@ -158,6 +180,18 @@ OS 版本: 10.0.22621 暂缺 Build 22621
expect(result.messages[0].message, ofdFoundRunning.messages[0].message, reason: 'The ValidationMessage message should be the same');
});
testWithoutContext('Reports missing powershell', () async {
final WindowsVersionValidator validator =
WindowsVersionValidator(
operatingSystemUtils: FakeValidOperatingSystemUtils(),
processLister: powershellUnavailable());
final ValidationResult result = await validator.validate();
expect(result.type, powershellUnavailableResult.type, reason: 'The ValidationResult type should be the same (partial)');
expect(result.statusInfo, powershellUnavailableResult.statusInfo, reason: 'The ValidationResult statusInfo should be the same');
expect(result.messages.length, 1, reason: 'The ValidationResult should have precisely 1 message');
expect(result.messages[0].message, powershellUnavailableResult.messages[0].message, reason: 'The ValidationMessage message should be the same');
});
testWithoutContext('Reports failure of Get-Process', () async {
final WindowsVersionValidator validator =
WindowsVersionValidator(
@ -169,4 +203,75 @@ OS 版本: 10.0.22621 暂缺 Build 22621
expect(result.messages.length, 1, reason: 'The ValidationResult should have precisely 1 message');
expect(result.messages[0].message, getProcessFailed.messages[0].message, reason: 'The ValidationMessage message should be the same');
});
testWithoutContext('getProcessesWithPath successfully runs with powershell', () async {
final ProcessLister processLister = ProcessLister(
FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>[
ProcessLister.powershell,
'-command',
'Get-Process | Format-List Path',
],
stdout: ProcessLister.powershell,
),
],
)..excludedExecutables.add(ProcessLister.pwsh),
);
try {
final ProcessResult result = await processLister.getProcessesWithPath();
expect(result.stdout, ProcessLister.powershell);
// ignore: avoid_catches_without_on_clauses
} catch (e) {
fail('Unexpected exception: $e');
}
});
testWithoutContext('getProcessesWithPath falls back to pwsh when powershell is not on the path', () async {
final ProcessLister processLister = ProcessLister(
FakeProcessManager.list(
<FakeCommand>[
const FakeCommand(
command: <String>[
ProcessLister.pwsh,
'-command',
'Get-Process | Format-List Path',
],
stdout: ProcessLister.pwsh,
),
],
)..excludedExecutables.add(ProcessLister.powershell),
);
try {
final ProcessResult result = await processLister.getProcessesWithPath();
expect(result.stdout, ProcessLister.pwsh);
// ignore: avoid_catches_without_on_clauses
} catch (e) {
fail('Unexpected exception: $e');
}
});
testWithoutContext('getProcessesWithPath throws if both powershell and pwsh are not on PATH', () async {
final ProcessLister processLister = ProcessLister(
FakeProcessManager.empty()..excludedExecutables.addAll(
<String>[
ProcessLister.powershell,
ProcessLister.pwsh,
],
),
);
try {
final ProcessResult result = await processLister.getProcessesWithPath();
fail('Should have thrown, but successfully ran ${result.stdout}');
} on StateError {
// Expected
// ignore: avoid_catches_without_on_clauses
} catch (e) {
fail('Unexpected exception: $e');
}
});
}