desktop_device.dart 6.39 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:meta/meta.dart';
8
import 'package:process/process.dart';
9 10 11 12

import 'application_package.dart';
import 'base/common.dart';
import 'base/io.dart';
13
import 'base/logger.dart';
14 15 16
import 'build_info.dart';
import 'convert.dart';
import 'device.dart';
17
import 'globals.dart' as globals;
18 19 20 21 22
import 'protocol_discovery.dart';

/// A partial implementation of Device for desktop-class devices to inherit
/// from, containing implementations that are common to all desktop devices.
abstract class DesktopDevice extends Device {
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
  DesktopDevice(String identifier, {
      @required PlatformType platformType,
      @required bool ephemeral,
      Logger logger,
      ProcessManager processManager,
    }) : _logger = logger ?? globals.logger, // TODO(jonahwilliams): remove after updating google3
         _processManager = processManager ?? globals.processManager,
         super(
          identifier,
          category: Category.desktop,
          platformType: platformType,
          ephemeral: ephemeral,
        );

  final Logger _logger;
  final ProcessManager _processManager;
39 40 41 42 43 44
  final Set<Process> _runningProcesses = <Process>{};
  final DesktopLogReader _deviceLogReader = DesktopLogReader();

  // Since the host and target devices are the same, no work needs to be done
  // to install the application.
  @override
45 46 47 48
  Future<bool> isAppInstalled(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
49 50 51 52 53 54 55 56 57

  // 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) async => true;

  // Since the host and target devices are the same, no work needs to be done
  // to install the application.
  @override
58 59 60 61
  Future<bool> installApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
62 63 64 65

  // Since the host and target devices are the same, no work needs to be done
  // to uninstall the application.
  @override
66 67 68 69
  Future<bool> uninstallApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async => true;
70 71 72 73 74 75 76 77 78 79 80

  @override
  Future<bool> get isLocalEmulator async => false;

  @override
  Future<String> get emulatorId async => null;

  @override
  DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();

  @override
81
  Future<String> get sdkNameAndVersion async => globals.os.name;
82

83 84 85
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

86
  @override
87 88 89 90 91
  DeviceLogReader getLogReader({
    ApplicationPackage app,
    bool includePastLogs = false,
  }) {
    assert(!includePastLogs, 'Past log reading not supported on desktop.');
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
    return _deviceLogReader;
  }

  @override
  void clearLogs() {}

  @override
  Future<LaunchResult> startApp(
    ApplicationPackage package, {
    String mainPath,
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
107
    String userIdentifier,
108 109 110 111 112 113 114 115 116 117 118 119 120
  }) async {
    if (!prebuiltApplication) {
      await buildForDevice(
        package,
        buildInfo: debuggingOptions?.buildInfo,
        mainPath: mainPath,
      );
    }

    // Ensure that the executable is locatable.
    final BuildMode buildMode = debuggingOptions?.buildInfo?.mode;
    final String executable = executablePathForDevice(package, buildMode);
    if (executable == null) {
121
      _logger.printError('Unable to find executable to run');
122 123 124
      return LaunchResult.failed();
    }

125
    final Process process = await _processManager.start(<String>[
126 127 128 129 130
      executable,
    ]);
    _runningProcesses.add(process);
    unawaited(process.exitCode.then((_) => _runningProcesses.remove(process)));

131
    _deviceLogReader.initializeProcess(process);
132 133 134
    if (debuggingOptions?.buildInfo?.isRelease == true) {
      return LaunchResult.succeeded();
    }
135 136 137 138 139
    final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(_deviceLogReader,
      devicePort: debuggingOptions?.deviceVmServicePort,
      hostPort: debuggingOptions?.hostVmServicePort,
      ipv6: ipv6,
    );
140 141
    try {
      final Uri observatoryUri = await observatoryDiscovery.uri;
142 143 144 145
      if (observatoryUri != null) {
        onAttached(package, buildMode, process);
        return LaunchResult.succeeded(observatoryUri: observatoryUri);
      }
146
      _logger.printError(
147 148 149
        'Error waiting for a debug connection: '
        'The log reader stopped unexpectedly.',
      );
150
    } on Exception catch (error) {
151
      _logger.printError('Error waiting for a debug connection: $error');
152 153 154
    } finally {
      await observatoryDiscovery.cancel();
    }
155
    return LaunchResult.failed();
156 157 158
  }

  @override
159 160 161 162
  Future<bool> stopApp(
    ApplicationPackage app, {
    String userIdentifier,
  }) async {
163 164 165
    bool succeeded = true;
    // Walk a copy of _runningProcesses, since the exit handler removes from the
    // set.
166
    for (final Process process in Set<Process>.of(_runningProcesses)) {
167 168 169 170 171
      succeeded &= process.kill();
    }
    return succeeded;
  }

172 173 174 175 176
  @override
  Future<void> dispose() async {
    await portForwarder?.dispose();
  }

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
  /// Builds the current project for this device, with the given options.
  Future<void> buildForDevice(
    ApplicationPackage package, {
    String mainPath,
    BuildInfo buildInfo,
  });

  /// Returns the path to the executable to run for [package] on this device for
  /// the given [buildMode].
  String executablePathForDevice(ApplicationPackage package, BuildMode buildMode);

  /// Called after a process is attached, allowing any device-specific extra
  /// steps to be run.
  void onAttached(ApplicationPackage package, BuildMode buildMode, Process process) {}
}

class DesktopLogReader extends DeviceLogReader {
  final StreamController<List<int>> _inputController = StreamController<List<int>>.broadcast();

  void initializeProcess(Process process) {
    process.stdout.listen(_inputController.add);
    process.stderr.listen(_inputController.add);
199
    process.exitCode.whenComplete(_inputController.close);
200 201 202 203 204 205 206 207 208 209 210
  }

  @override
  Stream<String> get logLines {
    return _inputController.stream
      .transform(utf8.decoder)
      .transform(const LineSplitter());
  }

  @override
  String get name => 'desktop';
211 212 213 214 215

  @override
  void dispose() {
    // Nothing to dispose.
  }
216
}