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>(
),
uwptool: UwpTool(
artifacts: globals.artifacts,
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
),
......
......@@ -10,7 +10,6 @@ import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../artifacts.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/process.dart';
......@@ -23,16 +22,13 @@ import '../base/process.dart';
class UwpTool {
UwpTool({
@required Artifacts artifacts,
@required FileSystem fileSystem,
@required Logger logger,
@required ProcessManager processManager,
}) : _artifacts = artifacts,
_fileSystem = fileSystem,
_logger = logger,
_processUtils = ProcessUtils(processManager: processManager, logger: logger);
final Artifacts _artifacts;
final FileSystem _fileSystem;
final Logger _logger;
final ProcessUtils _processUtils;
......@@ -86,23 +82,69 @@ class UwpTool {
return null;
}
// 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.
Future<bool> installApp(String buildDirectory) async {
Future<bool> installCertificate(String certificatePath) async {
final List<String> launchCommand = <String>[
'powershell.exe',
_fileSystem.path.join(buildDirectory, 'install.ps1'),
'start',
'certutil',
'-argumentlist',
'\'-addstore TrustedPeople "$certificatePath"\'',
'-verb',
'runas'
];
final RunResult result = await _processUtils.run(launchCommand);
if (result.exitCode != 0) {
_logger.printError(result.stdout.toString());
_logger.printError(result.stderr.toString());
_logger.printError('Failed to install certificate $certificatePath');
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 {
......@@ -116,6 +158,7 @@ class UwpTool {
_logger.printError('Failed to uninstall $packageFamily');
return false;
}
_logger.printTrace('Uninstalled application $packageFamily');
return true;
}
}
......@@ -132,6 +132,47 @@ class WindowsUWPDevice extends Device {
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
Future<bool> installApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
/// The cmake build generates an install powershell script.
......@@ -142,10 +183,48 @@ class WindowsUWPDevice extends Device {
return false;
}
final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug));
final String generated = '${binaryName}_${packageVersion}_${config}_Test';
final String buildDirectory = _fileSystem.path.join(
'build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName, generated);
return _uwptool.installApp(buildDirectory);
const String arch = 'x64';
final String generatedDir = '${binaryName}_${packageVersion}_${arch}_${config}_Test';
final String generatedApp = '${binaryName}_${packageVersion}_${arch}_$config';
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
......@@ -184,7 +263,11 @@ class WindowsUWPDevice extends Device {
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');
return LaunchResult.failed();
}
......@@ -204,11 +287,24 @@ class WindowsUWPDevice extends Device {
/// If the terminal is attached, prompt the user to open the firewall port.
if (_logger.terminal.stdinHasTerminal) {
await _logger.terminal.promptForCharInput(<String>['Y', 'y'], logger: _logger,
prompt: 'To continue start an admin cmd prompt and run the following command:\n'
' checknetisolation loopbackexempt -is -n=$packageFamily\n'
'Press "Y/y" once this is complete.'
final String response = await _logger.terminal.promptForCharInput(
<String>['Y', 'y', 'N', 'n'],
logger: _logger,
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.
......
......@@ -44,9 +44,18 @@ void main() {
testWithoutContext('WindowsUwpDevice defaults', () async {
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 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(windowsDevice.name, 'Windows (UWP)');
expect(await windowsDevice.installApp(package), true);
......@@ -167,7 +176,7 @@ void main() {
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 = '';
final FakeUwpTool uwptool = FakeUwpTool();
final FileSystem fileSystem = MemoryFileSystem.test();
......@@ -177,6 +186,12 @@ void main() {
);
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(
package,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
......@@ -185,8 +200,9 @@ void main() {
);
expect(result.started, true);
expect(uwptool.launchRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchRequests.single.args, <String>[
expect(uwptool.installCertRequests, isEmpty);
expect(uwptool.launchAppRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchAppRequests.single.args, <String>[
'--observatory-port=12345',
'--disable-service-auth-codes',
'--enable-dart-profiling',
......@@ -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 = '';
final FakeUwpTool uwptool = FakeUwpTool();
final FileSystem fileSystem = MemoryFileSystem.test();
......@@ -205,6 +221,46 @@ void main() {
);
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(
package,
debuggingOptions: DebuggingOptions.enabled(BuildInfo.release),
......@@ -213,8 +269,8 @@ void main() {
);
expect(result.started, true);
expect(uwptool.launchRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchRequests.single.args, <String>[]);
expect(uwptool.launchAppRequests.single.packageFamily, 'PACKAGE-ID_publisher');
expect(uwptool.launchAppRequests.single.args, <String>[]);
});
}
......@@ -273,10 +329,12 @@ class FakeBuildableUwpApp extends Fake implements BuildableUwpApp {
class FakeUwpTool implements UwpTool {
bool isInstalled = false;
bool hasValidSignature = false;
final List<_GetPackageFamilyRequest> getPackageFamilyRequests = <_GetPackageFamilyRequest>[];
final List<_LaunchRequest> launchRequests = <_LaunchRequest>[];
final List<_InstallRequest> installRequests = <_InstallRequest>[];
final List<_UninstallRequest> uninstallRequests = <_UninstallRequest>[];
final List<_LaunchAppRequest> launchAppRequests = <_LaunchAppRequest>[];
final List<_InstallCertRequest> installCertRequests = <_InstallCertRequest>[];
final List<_InstallAppRequest> installAppRequests = <_InstallAppRequest>[];
final List<_UninstallAppRequest> uninstallAppRequests = <_UninstallAppRequest>[];
@override
Future<List<String>> listApps() async {
......@@ -291,20 +349,31 @@ class FakeUwpTool implements UwpTool {
@override
Future<int/*?*/> launchApp(String packageFamily, List<String> args) async {
launchRequests.add(_LaunchRequest(packageFamily, args));
launchAppRequests.add(_LaunchAppRequest(packageFamily, args));
return 42;
}
@override
Future<bool> installApp(String buildDirectory) async {
installRequests.add(_InstallRequest(buildDirectory));
Future<bool> isSignatureValid(String packagePath) async {
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;
return true;
}
@override
Future<bool> uninstallApp(String packageFamily) async {
uninstallRequests.add(_UninstallRequest(packageFamily));
uninstallAppRequests.add(_UninstallAppRequest(packageFamily));
isInstalled = false;
return true;
}
......@@ -316,21 +385,29 @@ class _GetPackageFamilyRequest {
final String packageId;
}
class _LaunchRequest {
const _LaunchRequest(this.packageFamily, this.args);
class _LaunchAppRequest {
const _LaunchAppRequest(this.packageFamily, this.args);
final String packageFamily;
final List<String> args;
}
class _InstallRequest {
const _InstallRequest(this.buildDirectory);
class _InstallCertRequest {
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 {
const _UninstallRequest(this.packageFamily);
class _UninstallAppRequest {
const _UninstallAppRequest(this.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