Unverified Commit 7c5857d3 authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

[tool] Improve Windows install process (#82659)

This eliminates the use of the Install.ps1 script during Windows app
installation and instead uses uwptool install. Install.ps1 was the
slowest part of app install, and had resource contention issues that
frequently caused it to fail.
parent 61cfa190
...@@ -209,7 +209,6 @@ Future<T> runInContext<T>( ...@@ -209,7 +209,6 @@ Future<T> runInContext<T>(
), ),
uwptool: UwpTool( uwptool: UwpTool(
artifacts: globals.artifacts, artifacts: globals.artifacts,
fileSystem: globals.fs,
logger: globals.logger, logger: globals.logger,
processManager: globals.processManager, processManager: globals.processManager,
), ),
......
...@@ -10,7 +10,6 @@ import 'package:meta/meta.dart'; ...@@ -10,7 +10,6 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart'; import 'package:process/process.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/process.dart'; import '../base/process.dart';
...@@ -23,16 +22,13 @@ import '../base/process.dart'; ...@@ -23,16 +22,13 @@ import '../base/process.dart';
class UwpTool { class UwpTool {
UwpTool({ UwpTool({
@required Artifacts artifacts, @required Artifacts artifacts,
@required FileSystem fileSystem,
@required Logger logger, @required Logger logger,
@required ProcessManager processManager, @required ProcessManager processManager,
}) : _artifacts = artifacts, }) : _artifacts = artifacts,
_fileSystem = fileSystem,
_logger = logger, _logger = logger,
_processUtils = ProcessUtils(processManager: processManager, logger: logger); _processUtils = ProcessUtils(processManager: processManager, logger: logger);
final Artifacts _artifacts; final Artifacts _artifacts;
final FileSystem _fileSystem;
final Logger _logger; final Logger _logger;
final ProcessUtils _processUtils; final ProcessUtils _processUtils;
...@@ -86,23 +82,69 @@ class UwpTool { ...@@ -86,23 +82,69 @@ class UwpTool {
return null; return null;
} }
// Read the process ID from stdout. // Read the process ID from stdout.
return int.tryParse(result.stdout.toString().trim()); final int processId = int.tryParse(result.stdout.toString().trim());
_logger.printTrace('Launched application $packageFamily with process ID $processId');
return processId;
} }
/// Installs the app with the specified build directory. /// Returns `true` if the specified package signature is valid.
Future<bool> isSignatureValid(String packagePath) async {
final List<String> launchCommand = <String>[
'powershell.exe',
'-command',
'if ((Get-AuthenticodeSignature "$packagePath").Status -eq "Valid") { exit 0 } else { exit 1 }'
];
final RunResult result = await _processUtils.run(launchCommand);
if (result.exitCode != 0) {
_logger.printTrace('Invalid signature found for $packagePath');
return false;
}
_logger.printTrace('Valid signature found for $packagePath');
return true;
}
/// Installs a developer signing cerificate.
/// ///
/// Returns `true` on success. /// Returns `true` on success.
Future<bool> installApp(String buildDirectory) async { Future<bool> installCertificate(String certificatePath) async {
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
'powershell.exe', 'powershell.exe',
_fileSystem.path.join(buildDirectory, 'install.ps1'), 'start',
'certutil',
'-argumentlist',
'\'-addstore TrustedPeople "$certificatePath"\'',
'-verb',
'runas'
]; ];
final RunResult result = await _processUtils.run(launchCommand); final RunResult result = await _processUtils.run(launchCommand);
if (result.exitCode != 0) { if (result.exitCode != 0) {
_logger.printError(result.stdout.toString()); _logger.printError('Failed to install certificate $certificatePath');
_logger.printError(result.stderr.toString()); return false;
} }
return result.exitCode == 0; _logger.printTrace('Waiting for certificate store update');
// TODO(cbracken): Determine how we can query for success until some timeout.
// https://github.com/flutter/flutter/issues/82665
await Future<void>.delayed(const Duration(seconds: 1));
_logger.printTrace('Installed certificate $certificatePath');
return true;
}
/// Installs the app with the specified build directory.
///
/// Returns `true` on success.
Future<bool> installApp(String packageUri, List<String> dependencyUris) async {
final List<String> launchCommand = <String>[
_binaryPath,
'install',
packageUri,
] + dependencyUris;
final RunResult result = await _processUtils.run(launchCommand);
if (result.exitCode != 0) {
_logger.printError('Failed to install $packageUri');
return false;
}
_logger.printTrace('Installed application $packageUri');
return true;
} }
Future<bool> uninstallApp(String packageFamily) async { Future<bool> uninstallApp(String packageFamily) async {
...@@ -116,6 +158,7 @@ class UwpTool { ...@@ -116,6 +158,7 @@ class UwpTool {
_logger.printError('Failed to uninstall $packageFamily'); _logger.printError('Failed to uninstall $packageFamily');
return false; return false;
} }
_logger.printTrace('Uninstalled application $packageFamily');
return true; return true;
} }
} }
...@@ -132,6 +132,47 @@ class WindowsUWPDevice extends Device { ...@@ -132,6 +132,47 @@ class WindowsUWPDevice extends Device {
return NoOpDeviceLogReader('winuwp'); return NoOpDeviceLogReader('winuwp');
} }
// Returns `true` if the specified file is a valid package based on file extension.
bool _isValidPackage(String packagePath) {
const List<String> validPackageExtensions = <String>[
'.appx', '.msix', // Architecture-specific application.
'.appxbundle', '.msixbundle', // Architecture-independent application.
'.eappx', '.emsix', // Encrypted architecture-specific application.
'.eappxbundle', '.emsixbundle', // Encrypted architecture-independent application.
];
return validPackageExtensions.any(packagePath.endsWith);
}
// Walks the build directory for any dependent packages for the specified architecture.
List<String> _getPackagePaths(String directory) {
if (!_fileSystem.isDirectorySync(directory)) {
return <String>[];
}
final List<String> packagePaths = <String>[];
for (final FileSystemEntity entity in _fileSystem.directory(directory).listSync()) {
if (entity.statSync().type != FileSystemEntityType.file) {
continue;
}
final String packagePath = entity.absolute.path;
if (_isValidPackage(packagePath)) {
packagePaths.add(packagePath);
}
}
return packagePaths;
}
// Walks the build directory for any dependent packages for the specified architecture.
String/*?*/ _getAppPackagePath(String buildDirectory) {
final List<String> packagePaths = _getPackagePaths(buildDirectory);
return packagePaths.isNotEmpty ? packagePaths.first : null;
}
// Walks the build directory for any dependent packages for the specified architecture.
List<String> _getDependencyPaths(String buildDirectory, String architecture) {
final String depsDirectory = _fileSystem.path.join(buildDirectory, 'Dependencies', architecture);
return _getPackagePaths(depsDirectory);
}
@override @override
Future<bool> installApp(covariant BuildableUwpApp app, {String userIdentifier}) async { Future<bool> installApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
/// The cmake build generates an install powershell script. /// The cmake build generates an install powershell script.
...@@ -142,10 +183,48 @@ class WindowsUWPDevice extends Device { ...@@ -142,10 +183,48 @@ class WindowsUWPDevice extends Device {
return false; return false;
} }
final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug)); final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug));
final String generated = '${binaryName}_${packageVersion}_${config}_Test'; const String arch = 'x64';
final String buildDirectory = _fileSystem.path.join( final String generatedDir = '${binaryName}_${packageVersion}_${arch}_${config}_Test';
'build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName, generated); final String generatedApp = '${binaryName}_${packageVersion}_${arch}_$config';
return _uwptool.installApp(buildDirectory); final String buildDirectory = _fileSystem.path.absolute(_fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName, generatedDir));
// Verify package signature.
final String packagePath = _getAppPackagePath(buildDirectory);
if (!await _uwptool.isSignatureValid(packagePath)) {
// If signature is invalid, install the developer certificate.
final String certificatePath = _fileSystem.path.join(buildDirectory, '$generatedApp.cer');
if (_logger.terminal.stdinHasTerminal) {
final String response = await _logger.terminal.promptForCharInput(
<String>['Y', 'y', 'N', 'n'],
logger: _logger,
prompt: 'Install developer certificate.\n'
'\n'
'Windows UWP apps are signed with a developer certificate during the build\n'
'process. On the first install of an app with a signature from a new\n'
'certificate, the certificate must be installed.\n'
'\n'
'If desired, this certificate can later be removed by launching the \n'
'"Manage Computer Certificates" control panel from the Start menu and deleting\n'
'the "CMake Test Cert" certificate from the "Trusted People" > "Certificates"\n'
'section.\n'
'\n'
'Press "Y" to continue, or "N" to cancel.',
displayAcceptedCharacters: false,
);
if (response == 'N' || response == 'n') {
return false;
}
}
await _uwptool.installCertificate(certificatePath);
}
// Install the application and dependencies.
final String packageUri = Uri.file(packagePath).toString();
final List<String> dependencyUris = _getDependencyPaths(buildDirectory, arch)
.map((String path) => Uri.file(path).toString())
.toList();
return _uwptool.installApp(packageUri.toString(), dependencyUris);
} }
@override @override
...@@ -184,7 +263,11 @@ class WindowsUWPDevice extends Device { ...@@ -184,7 +263,11 @@ class WindowsUWPDevice extends Device {
target: mainPath, target: mainPath,
); );
} }
if (!await isAppInstalled(package) && !await installApp(package)) { if (await isAppInstalled(package) && !await uninstallApp(package)) {
_logger.printError('Failed to uninstall previous app package');
return LaunchResult.failed();
}
if (!await installApp(package)) {
_logger.printError('Failed to install app package'); _logger.printError('Failed to install app package');
return LaunchResult.failed(); return LaunchResult.failed();
} }
...@@ -204,11 +287,24 @@ class WindowsUWPDevice extends Device { ...@@ -204,11 +287,24 @@ class WindowsUWPDevice extends Device {
/// If the terminal is attached, prompt the user to open the firewall port. /// If the terminal is attached, prompt the user to open the firewall port.
if (_logger.terminal.stdinHasTerminal) { if (_logger.terminal.stdinHasTerminal) {
await _logger.terminal.promptForCharInput(<String>['Y', 'y'], logger: _logger, final String response = await _logger.terminal.promptForCharInput(
prompt: 'To continue start an admin cmd prompt and run the following command:\n' <String>['Y', 'y', 'N', 'n'],
' checknetisolation loopbackexempt -is -n=$packageFamily\n' logger: _logger,
'Press "Y/y" once this is complete.' prompt: 'Enable Flutter debugging from localhost.\n'
'\n'
'Windows UWP apps run in a sandboxed environment. To enable Flutter debugging\n'
'and hot reload, you will need to enable inbound connections to the app from the\n'
'Flutter tool running on your machine. To do so:\n'
' 1. Launch PowerShell as an Administrator\n'
' 2. Enter the following command:\n'
' checknetisolation loopbackexempt -is -n=$packageFamily\n'
'\n'
'Press "Y" once this is complete, or "N" to abort.',
displayAcceptedCharacters: false,
); );
if (response == 'N' || response == 'n') {
return LaunchResult.failed();
}
} }
/// Currently we do not have a way to discover the VM Service URI. /// Currently we do not have a way to discover the VM Service URI.
......
...@@ -44,9 +44,18 @@ void main() { ...@@ -44,9 +44,18 @@ void main() {
testWithoutContext('WindowsUwpDevice defaults', () async { testWithoutContext('WindowsUwpDevice defaults', () async {
final FakeUwpTool uwptool = FakeUwpTool(); final FakeUwpTool uwptool = FakeUwpTool();
final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice(uwptool: uwptool); final FileSystem fileSystem = MemoryFileSystem.test();
final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice(
fileSystem: fileSystem,
uwptool: uwptool,
);
final FakeBuildableUwpApp package = FakeBuildableUwpApp(); final FakeBuildableUwpApp package = FakeBuildableUwpApp();
final String packagePath = fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', 'testapp',
'testapp_1.2.3.4_x64_Debug_Test', 'testapp_1.2.3.4_x64_Debug.msix',
);
fileSystem.file(packagePath).createSync(recursive:true);
expect(await windowsDevice.targetPlatform, TargetPlatform.windows_uwp_x64); expect(await windowsDevice.targetPlatform, TargetPlatform.windows_uwp_x64);
expect(windowsDevice.name, 'Windows (UWP)'); expect(windowsDevice.name, 'Windows (UWP)');
expect(await windowsDevice.installApp(package), true); expect(await windowsDevice.installApp(package), true);
...@@ -167,7 +176,7 @@ void main() { ...@@ -167,7 +176,7 @@ void main() {
expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.release), 'release/executable'); expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.release), 'release/executable');
}); });
testWithoutContext('WinUWPDevice can launch application', () async { testWithoutContext('WinUWPDevice can launch application if cert is installed', () async {
Cache.flutterRoot = ''; Cache.flutterRoot = '';
final FakeUwpTool uwptool = FakeUwpTool(); final FakeUwpTool uwptool = FakeUwpTool();
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
...@@ -177,6 +186,12 @@ void main() { ...@@ -177,6 +186,12 @@ void main() {
); );
final FakeBuildableUwpApp package = FakeBuildableUwpApp(); final FakeBuildableUwpApp package = FakeBuildableUwpApp();
uwptool.hasValidSignature = true;
final String packagePath = fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', 'testapp',
'testapp_1.2.3.4_x64_Debug_Test', 'testapp_1.2.3.4_x64_Debug.msix',
);
fileSystem.file(packagePath).createSync(recursive:true);
final LaunchResult result = await windowsDevice.startApp( final LaunchResult result = await windowsDevice.startApp(
package, package,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
...@@ -185,8 +200,9 @@ void main() { ...@@ -185,8 +200,9 @@ void main() {
); );
expect(result.started, true); expect(result.started, true);
expect(uwptool.launchRequests.single.packageFamily, 'PACKAGE-ID_publisher'); expect(uwptool.installCertRequests, isEmpty);
expect(uwptool.launchRequests.single.args, <String>[ expect(uwptool.launchAppRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchAppRequests.single.args, <String>[
'--observatory-port=12345', '--observatory-port=12345',
'--disable-service-auth-codes', '--disable-service-auth-codes',
'--enable-dart-profiling', '--enable-dart-profiling',
...@@ -195,7 +211,7 @@ void main() { ...@@ -195,7 +211,7 @@ void main() {
]); ]);
}); });
testWithoutContext('WinUWPDevice can launch application in release mode', () async { testWithoutContext('WinUWPDevice installs cert and can launch application if cert not installed', () async {
Cache.flutterRoot = ''; Cache.flutterRoot = '';
final FakeUwpTool uwptool = FakeUwpTool(); final FakeUwpTool uwptool = FakeUwpTool();
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
...@@ -205,6 +221,46 @@ void main() { ...@@ -205,6 +221,46 @@ void main() {
); );
final FakeBuildableUwpApp package = FakeBuildableUwpApp(); final FakeBuildableUwpApp package = FakeBuildableUwpApp();
uwptool.hasValidSignature = false;
final String packagePath = fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', 'testapp',
'testapp_1.2.3.4_x64_Debug_Test', 'testapp_1.2.3.4_x64_Debug.msix',
);
fileSystem.file(packagePath).createSync(recursive:true);
final LaunchResult result = await windowsDevice.startApp(
package,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
prebuiltApplication: true,
platformArgs: <String, Object>{},
);
expect(result.started, true);
expect(uwptool.installCertRequests, isNotEmpty);
expect(uwptool.launchAppRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchAppRequests.single.args, <String>[
'--observatory-port=12345',
'--disable-service-auth-codes',
'--enable-dart-profiling',
'--enable-checked-mode',
'--verify-entry-points',
]);
});
testWithoutContext('WinUWPDevice can launch application in release mode', () async {
Cache.flutterRoot = '';
final FakeUwpTool uwptool = FakeUwpTool();
final FileSystem fileSystem = MemoryFileSystem.test();
final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice(
fileSystem: fileSystem,
uwptool: uwptool,
);
final FakeBuildableUwpApp package = FakeBuildableUwpApp();
final String packagePath = fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', 'testapp',
'testapp_1.2.3.4_x64_Release_Test', 'testapp_1.2.3.4_x64_Release.msix',
);
fileSystem.file(packagePath).createSync(recursive:true);
final LaunchResult result = await windowsDevice.startApp( final LaunchResult result = await windowsDevice.startApp(
package, package,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.release), debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
...@@ -213,8 +269,8 @@ void main() { ...@@ -213,8 +269,8 @@ void main() {
); );
expect(result.started, true); expect(result.started, true);
expect(uwptool.launchRequests.single.packageFamily, 'PACKAGE-ID_publisher'); expect(uwptool.launchAppRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchRequests.single.args, <String>[]); expect(uwptool.launchAppRequests.single.args, <String>[]);
}); });
} }
...@@ -273,10 +329,12 @@ class FakeBuildableUwpApp extends Fake implements BuildableUwpApp { ...@@ -273,10 +329,12 @@ class FakeBuildableUwpApp extends Fake implements BuildableUwpApp {
class FakeUwpTool implements UwpTool { class FakeUwpTool implements UwpTool {
bool isInstalled = false; bool isInstalled = false;
bool hasValidSignature = false;
final List<_GetPackageFamilyRequest> getPackageFamilyRequests = <_GetPackageFamilyRequest>[]; final List<_GetPackageFamilyRequest> getPackageFamilyRequests = <_GetPackageFamilyRequest>[];
final List<_LaunchRequest> launchRequests = <_LaunchRequest>[]; final List<_LaunchAppRequest> launchAppRequests = <_LaunchAppRequest>[];
final List<_InstallRequest> installRequests = <_InstallRequest>[]; final List<_InstallCertRequest> installCertRequests = <_InstallCertRequest>[];
final List<_UninstallRequest> uninstallRequests = <_UninstallRequest>[]; final List<_InstallAppRequest> installAppRequests = <_InstallAppRequest>[];
final List<_UninstallAppRequest> uninstallAppRequests = <_UninstallAppRequest>[];
@override @override
Future<List<String>> listApps() async { Future<List<String>> listApps() async {
...@@ -291,20 +349,31 @@ class FakeUwpTool implements UwpTool { ...@@ -291,20 +349,31 @@ class FakeUwpTool implements UwpTool {
@override @override
Future<int/*?*/> launchApp(String packageFamily, List<String> args) async { Future<int/*?*/> launchApp(String packageFamily, List<String> args) async {
launchRequests.add(_LaunchRequest(packageFamily, args)); launchAppRequests.add(_LaunchAppRequest(packageFamily, args));
return 42; return 42;
} }
@override @override
Future<bool> installApp(String buildDirectory) async { Future<bool> isSignatureValid(String packagePath) async {
installRequests.add(_InstallRequest(buildDirectory)); return hasValidSignature;
}
@override
Future<bool> installCertificate(String certificatePath) async {
installCertRequests.add(_InstallCertRequest(certificatePath));
return true;
}
@override
Future<bool> installApp(String packageUri, List<String> dependencyUris) async {
installAppRequests.add(_InstallAppRequest(packageUri, dependencyUris));
isInstalled = true; isInstalled = true;
return true; return true;
} }
@override @override
Future<bool> uninstallApp(String packageFamily) async { Future<bool> uninstallApp(String packageFamily) async {
uninstallRequests.add(_UninstallRequest(packageFamily)); uninstallAppRequests.add(_UninstallAppRequest(packageFamily));
isInstalled = false; isInstalled = false;
return true; return true;
} }
...@@ -316,21 +385,29 @@ class _GetPackageFamilyRequest { ...@@ -316,21 +385,29 @@ class _GetPackageFamilyRequest {
final String packageId; final String packageId;
} }
class _LaunchRequest { class _LaunchAppRequest {
const _LaunchRequest(this.packageFamily, this.args); const _LaunchAppRequest(this.packageFamily, this.args);
final String packageFamily; final String packageFamily;
final List<String> args; final List<String> args;
} }
class _InstallRequest { class _InstallCertRequest {
const _InstallRequest(this.buildDirectory); const _InstallCertRequest(this.certificatePath);
final String certificatePath;
}
class _InstallAppRequest {
const _InstallAppRequest(this.packageUri, this.dependencyUris);
final String buildDirectory; final String packageUri;
final List<String> dependencyUris;
} }
class _UninstallRequest { class _UninstallAppRequest {
const _UninstallRequest(this.packageFamily); const _UninstallAppRequest(this.packageFamily);
final String packageFamily; final String packageFamily;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment