From ad370c8fa70b31bfaa5b717da204cc62182cf478 Mon Sep 17 00:00:00 2001 From: Ben Konyi Date: Tue, 15 Oct 2024 11:48:35 -0400 Subject: [PATCH] [ 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 --- .../windows/windows_version_validator.dart | 31 ++++- .../windows_version_validator_test.dart | 107 +++++++++++++++++- 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/packages/flutter_tools/lib/src/windows/windows_version_validator.dart b/packages/flutter_tools/lib/src/windows/windows_version_validator.dart index cd90d88449b..afe16a66794 100644 --- a/packages/flutter_tools/lib/src/windows/windows_version_validator.dart +++ b/packages/flutter_tools/lib/src/windows/windows_version_validator.dart @@ -37,9 +37,22 @@ class WindowsVersionValidator extends DoctorValidator { final ProcessLister _processLister; Future _topazScan() async { + if (!_processLister.canRunPowershell()) { + return const ValidationResult( + ValidationType.missing, + [ + 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.hint('Get-Process failed to complete')]); + return const ValidationResult( + ValidationType.missing, + [ + 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 getProcessesWithPath() async { const String argument = 'Get-Process | Format-List Path'; - return processManager.run(['powershell', '-command', argument]); + const List psArgs = ['-command', argument]; + if (processManager.canRun(powershell)) { + return processManager.run([powershell, ...psArgs]); + } + if (processManager.canRun(pwsh)) { + return processManager.run([pwsh, ...psArgs]); + } + throw StateError('Failed to find $powershell or $pwsh on PATH'); } } diff --git a/packages/flutter_tools/test/general.shard/windows_version_validator_test.dart b/packages/flutter_tools/test/general.shard/windows_version_validator_test.dart index f6ebb9d7a9f..8f54cc9af24 100644 --- a/packages/flutter_tools/test/general.shard/windows_version_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/windows_version_validator_test.dart @@ -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 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.hint('Failed to find ${ProcessLister.powershell} or ${ProcessLister.pwsh} on PATH'), + ], + statusInfo: 'Problem detected with Windows installation', +); + + const ValidationResult getProcessFailed = ValidationResult( ValidationType.partial, [ @@ -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( + [ + const FakeCommand( + command: [ + 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( + [ + const FakeCommand( + command: [ + 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( + [ + 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'); + } + }); }