Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/flutter_tools/lib/src/application_package.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'build_info.dart';
import 'globals.dart';
import 'ios/ios_workflow.dart';
import 'ios/plist_utils.dart' as plist;
import 'macos/application_package.dart';
import 'project.dart';
import 'tester/flutter_tester.dart';

Expand All @@ -42,6 +43,9 @@ class ApplicationPackageFactory {
case TargetPlatform.tester:
return FlutterTesterApp.fromCurrentDirectory();
case TargetPlatform.darwin_x64:
return applicationBinary != null
? MacOSApp.fromPrebuiltApp(applicationBinary)
: null;
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
Expand Down
1 change: 1 addition & 0 deletions packages/flutter_tools/lib/src/ios/plist_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../base/process.dart';

const String kCFBundleIdentifierKey = 'CFBundleIdentifier';
const String kCFBundleShortVersionStringKey = 'CFBundleShortVersionString';
const String kCFBundleExecutable = 'CFBundleExecutable';

// Prefer using [iosWorkflow.getPlistValueFromFile] to enable mocking.
String getValueFromFile(String plistFilePath, String key) {
Expand Down
91 changes: 91 additions & 0 deletions packages/flutter_tools/lib/src/macos/application_package.dart
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;
}
110 changes: 89 additions & 21 deletions packages/flutter_tools/lib/src/macos/macos_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// 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;
Expand All @@ -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();
Expand All @@ -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,
Expand All @@ -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]);
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion packages/flutter_tools/lib/src/macos/macos_workflow.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import '../doctor.dart';
/// The [MacOSWorkflow] instance.
MacOSWorkflow get macOSWorkflow => context[MacOSWorkflow];

/// The macos-specific implementation of a [Workflow].
/// The macOS-specific implementation of a [Workflow].
///
/// This workflow requires the flutter-desktop-embedding as a sibling
/// repository to the flutter repo.
Expand Down
Loading