-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Support running macOS prebuilt application #26593
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0461332
904c33b
68e4492
ebf392a
2b95e27
7a07939
5d64667
b031c94
621d04d
87ee26d
f05a6e7
be1a13c
b60a214
3cf264e
137716c
e9f85b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // Copyright 2019 The Chromium Authors. All rights reserved. | ||
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'package:meta/meta.dart'; | ||
|
|
||
| import '../application_package.dart'; | ||
| import '../base/file_system.dart'; | ||
| import '../globals.dart'; | ||
| import '../ios/plist_utils.dart' as plist; | ||
|
|
||
| /// Tests whether a [FileSystemEntity] is an macOS bundle directory | ||
| bool _isBundleDirectory(FileSystemEntity entity) => | ||
| entity is Directory && entity.path.endsWith('.app'); | ||
|
|
||
| abstract class MacOSApp extends ApplicationPackage { | ||
| MacOSApp({@required String projectBundleId}) : super(id: projectBundleId); | ||
|
|
||
| /// Creates a new [MacOSApp] from an existing app bundle. | ||
| /// | ||
| /// `applicationBinary` is the path to the framework directory created by an | ||
| /// Xcode build. By default, this is located under | ||
| /// "~/Library/Developer/Xcode/DerivedData/" and contains an executable | ||
| /// which is expected to start the application and send the observatory | ||
| /// port over stdout. | ||
| factory MacOSApp.fromPrebuiltApp(FileSystemEntity applicationBinary) { | ||
| final FileSystemEntityType entityType = fs.typeSync(applicationBinary.path); | ||
| if (entityType == FileSystemEntityType.notFound) { | ||
| printError('File "${applicationBinary.path}" does not exist.'); | ||
| return null; | ||
| } | ||
| Directory bundleDir; | ||
| if (entityType == FileSystemEntityType.directory) { | ||
| final Directory directory = fs.directory(applicationBinary); | ||
| if (!_isBundleDirectory(directory)) { | ||
| printError('Folder "${applicationBinary.path}" is not an app bundle.'); | ||
| return null; | ||
| } | ||
| bundleDir = fs.directory(applicationBinary); | ||
| } else { | ||
| printError('Folder "${applicationBinary.path}" is not an app bundle.'); | ||
| return null; | ||
| } | ||
| final String plistPath = fs.path.join(bundleDir.path, 'Contents', 'Info.plist'); | ||
| if (!fs.file(plistPath).existsSync()) { | ||
| printError('Invalid prebuilt macOS app. Does not contain Info.plist.'); | ||
| return null; | ||
| } | ||
| final String id = plist.getValueFromFile(plistPath, plist.kCFBundleIdentifierKey); | ||
| final String executableName = plist.getValueFromFile(plistPath, plist.kCFBundleExecutable); | ||
| if (id == null) { | ||
| printError('Invalid prebuilt macOS app. Info.plist does not contain bundle identifier'); | ||
| return null; | ||
| } | ||
| final String executable = fs.path.join(bundleDir.path, 'Contents', 'MacOS', executableName); | ||
| if (!fs.file(executable).existsSync()) { | ||
| printError('Could not find macOS binary at $executable'); | ||
| return null; | ||
| } | ||
| return PrebuiltMacOSApp( | ||
| bundleDir: bundleDir, | ||
| bundleName: fs.path.basename(bundleDir.path), | ||
| projectBundleId: id, | ||
| executable: executable, | ||
| ); | ||
| } | ||
|
|
||
| @override | ||
| String get displayName => id; | ||
|
|
||
| String get executable; | ||
| } | ||
|
|
||
| class PrebuiltMacOSApp extends MacOSApp { | ||
| PrebuiltMacOSApp({ | ||
| @required this.bundleDir, | ||
| @required this.bundleName, | ||
| @required this.projectBundleId, | ||
| @required this.executable, | ||
| }) : super(projectBundleId: projectBundleId); | ||
|
|
||
| final Directory bundleDir; | ||
| final String bundleName; | ||
| final String projectBundleId; | ||
|
|
||
| @override | ||
| final String executable; | ||
|
|
||
| @override | ||
| String get name => bundleName; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,37 +2,44 @@ | |
| // Use of this source code is governed by a BSD-style license that can be | ||
| // found in the LICENSE file. | ||
|
|
||
| import 'dart:convert'; | ||
|
|
||
| import '../application_package.dart'; | ||
| import '../base/io.dart'; | ||
| import '../base/os.dart'; | ||
| import '../base/platform.dart'; | ||
| import '../base/process_manager.dart'; | ||
| import '../build_info.dart'; | ||
| import '../device.dart'; | ||
| import '../globals.dart'; | ||
| import '../macos/application_package.dart'; | ||
| import '../protocol_discovery.dart'; | ||
| import 'macos_workflow.dart'; | ||
|
|
||
| /// A device that represents a desktop MacOS target. | ||
| class MacOSDevice extends Device { | ||
| MacOSDevice() : super('MacOS'); | ||
| MacOSDevice() : super('macOS'); | ||
|
|
||
| @override | ||
| void clearLogs() {} | ||
|
|
||
| @override | ||
| DeviceLogReader getLogReader({ApplicationPackage app}) => NoOpDeviceLogReader('macos'); | ||
|
|
||
| // Since the host and target devices are the same, no work needs to be done | ||
| // to install the application. | ||
| @override | ||
| Future<bool> installApp(ApplicationPackage app) { | ||
| throw UnimplementedError(); | ||
| } | ||
| Future<bool> installApp(ApplicationPackage app) async => true; | ||
jonahwilliams marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Since the host and target devices are the same, no work needs to be done | ||
| // to install the application. | ||
| @override | ||
| Future<bool> isAppInstalled(ApplicationPackage app) { | ||
| throw UnimplementedError(); | ||
| } | ||
| Future<bool> isAppInstalled(ApplicationPackage app) async => true; | ||
|
|
||
| // Since the host and target devices are the same, no work needs to be done | ||
| // to install the application. | ||
| @override | ||
| Future<bool> isLatestBuildInstalled(ApplicationPackage app) { | ||
| throw UnimplementedError(); | ||
| } | ||
| Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true; | ||
|
|
||
| @override | ||
| Future<bool> get isLocalEmulator async => false; | ||
|
|
@@ -41,7 +48,7 @@ class MacOSDevice extends Device { | |
| bool isSupported() => true; | ||
|
|
||
| @override | ||
| String get name => 'MacOS'; | ||
| String get name => 'macOS'; | ||
|
|
||
| @override | ||
| DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); | ||
|
|
@@ -50,7 +57,7 @@ class MacOSDevice extends Device { | |
| Future<String> get sdkNameAndVersion async => os.name; | ||
|
|
||
| @override | ||
| Future<LaunchResult> startApp(ApplicationPackage package, { | ||
| Future<LaunchResult> startApp(covariant MacOSApp package, { | ||
| String mainPath, | ||
| String route, | ||
| DebuggingOptions debuggingOptions, | ||
|
|
@@ -59,26 +66,72 @@ class MacOSDevice extends Device { | |
| bool applicationNeedsRebuild = false, | ||
| bool usesTerminalUi = true, | ||
| bool ipv6 = false, | ||
| }) { | ||
| throw UnimplementedError(); | ||
| }) async { | ||
| if (!prebuiltApplication) { | ||
| return LaunchResult.failed(); | ||
| } | ||
| // Stop any running applications with the same executable. | ||
| await stopApp(package); | ||
| final Process process = await processManager.start(<String>[package.executable]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to implement stopApp by caching this Process object in a field?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could cache the ids or process in a field on the device, however for the case of "run -> detach -> run" two processes will still be started. I'm not in love with using ps/kill though |
||
| final MacOSLogReader logReader = MacOSLogReader(package, process); | ||
| final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(logReader); | ||
| try { | ||
| final Uri observatoryUri = await observatoryDiscovery.uri; | ||
| return LaunchResult.succeeded(observatoryUri: observatoryUri); | ||
| } catch (error) { | ||
| printError('Error waiting for a debug connection: $error'); | ||
| return LaunchResult.failed(); | ||
| } finally { | ||
| await observatoryDiscovery.cancel(); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| Future<bool> stopApp(ApplicationPackage app) { | ||
| throw UnimplementedError(); | ||
| // TODO(jonahwilliams): implement using process manager. | ||
| // currently we rely on killing the isolate taking down the application. | ||
| @override | ||
| Future<bool> stopApp(covariant MacOSApp app) async { | ||
| final RegExp whitespace = RegExp(r'\s+'); | ||
| bool succeeded = true; | ||
| try { | ||
| final ProcessResult result = await processManager.run(<String>[ | ||
| 'ps', 'aux', | ||
| ]); | ||
| if (result.exitCode != 0) { | ||
| return false; | ||
| } | ||
| final List<String> lines = result.stdout.split('\n'); | ||
| for (String line in lines) { | ||
| if (!line.contains(app.executable)) { | ||
| continue; | ||
| } | ||
| final List<String> values = line.split(whitespace); | ||
| if (values.length < 2) { | ||
| continue; | ||
| } | ||
| final String pid = values[1]; | ||
| final ProcessResult killResult = await processManager.run(<String>[ | ||
| 'kill', pid | ||
| ]); | ||
| succeeded &= killResult.exitCode == 0; | ||
| } | ||
| return true; | ||
| } on ArgumentError { | ||
| succeeded = false; | ||
| } | ||
| return succeeded; | ||
| } | ||
|
|
||
| @override | ||
| Future<TargetPlatform> get targetPlatform async => TargetPlatform.darwin_x64; | ||
|
|
||
| // Since the host and target devices are the same, no work needs to be done | ||
| // to uninstall the application. | ||
| @override | ||
| Future<bool> uninstallApp(ApplicationPackage app) { | ||
| throw UnimplementedError(); | ||
| } | ||
| Future<bool> uninstallApp(ApplicationPackage app) async => true; | ||
| } | ||
|
|
||
| class MacOSDevices extends PollingDeviceDiscovery { | ||
| MacOSDevices() : super('macos devices'); | ||
| MacOSDevices() : super('macOS devices'); | ||
|
|
||
| @override | ||
| bool get supportsPlatform => platform.isMacOS; | ||
|
|
@@ -99,3 +152,18 @@ class MacOSDevices extends PollingDeviceDiscovery { | |
| @override | ||
| Future<List<String>> getDiagnostics() async => const <String>[]; | ||
| } | ||
|
|
||
| class MacOSLogReader extends DeviceLogReader { | ||
| MacOSLogReader(this.macOSApp, this.process); | ||
|
|
||
| final MacOSApp macOSApp; | ||
| final Process process; | ||
|
|
||
| @override | ||
| Stream<String> get logLines { | ||
| return process.stdout.transform(utf8.decoder); | ||
| } | ||
|
|
||
| @override | ||
| String get name => macOSApp.displayName; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.