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

import 'dart:async';

7
import 'package:process/process.dart';
8 9 10

import 'application_package.dart';
import 'base/common.dart';
11
import 'base/file_system.dart';
12
import 'base/io.dart';
13
import 'base/logger.dart';
14
import 'base/os.dart';
15 16
import 'build_info.dart';
import 'convert.dart';
17
import 'devfs.dart';
18
import 'device.dart';
19
import 'device_port_forwarder.dart';
20 21 22 23 24
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 {
25
  DesktopDevice(String identifier, {
26 27 28 29 30 31
      required PlatformType platformType,
      required bool ephemeral,
      required Logger logger,
      required ProcessManager processManager,
      required FileSystem fileSystem,
      required OperatingSystemUtils operatingSystemUtils,
32 33 34 35
    }) : _logger = logger,
         _processManager = processManager,
         _fileSystem = fileSystem,
         _operatingSystemUtils = operatingSystemUtils,
36 37 38 39 40 41 42 43 44
         super(
          identifier,
          category: Category.desktop,
          platformType: platformType,
          ephemeral: ephemeral,
        );

  final Logger _logger;
  final ProcessManager _processManager;
45
  final FileSystem _fileSystem;
46
  final OperatingSystemUtils _operatingSystemUtils;
47 48 49
  final Set<Process> _runningProcesses = <Process>{};
  final DesktopLogReader _deviceLogReader = DesktopLogReader();

50 51 52 53
  @override
  DevFSWriter createDevFSWriter(covariant ApplicationPackage app, String userIdentifier) {
    return LocalDevFSWriter(fileSystem: _fileSystem);
  }
54

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

  // 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
71 72
  Future<bool> installApp(
    ApplicationPackage app, {
73
    String? userIdentifier,
74
  }) async => true;
75 76 77 78

  // Since the host and target devices are the same, no work needs to be done
  // to uninstall the application.
  @override
79 80
  Future<bool> uninstallApp(
    ApplicationPackage app, {
81
    String? userIdentifier,
82
  }) async => true;
83 84 85 86 87

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

  @override
88
  Future<String?> get emulatorId async => null;
89 90 91 92 93

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

  @override
94
  Future<String> get sdkNameAndVersion async => _operatingSystemUtils.name;
95

96 97 98
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

99
  @override
100
  DeviceLogReader getLogReader({
101
    ApplicationPackage? app,
102 103 104
    bool includePastLogs = false,
  }) {
    assert(!includePastLogs, 'Past log reading not supported on desktop.');
105 106 107 108 109 110 111 112 113
    return _deviceLogReader;
  }

  @override
  void clearLogs() {}

  @override
  Future<LaunchResult> startApp(
    ApplicationPackage package, {
114 115 116
    String? mainPath,
    String? route,
    required DebuggingOptions debuggingOptions,
117
    Map<String, dynamic> platformArgs = const <String, dynamic>{},
118 119
    bool prebuiltApplication = false,
    bool ipv6 = false,
120
    String? userIdentifier,
121 122 123 124
  }) async {
    if (!prebuiltApplication) {
      await buildForDevice(
        package,
125
        buildInfo: debuggingOptions.buildInfo,
126 127 128 129 130
        mainPath: mainPath,
      );
    }

    // Ensure that the executable is locatable.
131 132 133
    final BuildMode buildMode = debuggingOptions.buildInfo.mode;
    final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
    final String? executable = executablePathForDevice(package, buildMode);
134
    if (executable == null) {
135
      _logger.printError('Unable to find executable to run');
136 137 138
      return LaunchResult.failed();
    }

139 140 141
    Process process;
    final List<String> command = <String>[
      executable,
142
      ...debuggingOptions.dartEntrypointArgs,
143 144 145 146 147 148 149 150 151 152
    ];
    try {
      process = await _processManager.start(
        command,
        environment: _computeEnvironment(debuggingOptions, traceStartup, route),
      );
    } on ProcessException catch (e) {
      _logger.printError('Unable to start executable "${command.join(' ')}": $e');
      rethrow;
    }
153 154 155
    _runningProcesses.add(process);
    unawaited(process.exitCode.then((_) => _runningProcesses.remove(process)));

156
    _deviceLogReader.initializeProcess(process);
157
    if (debuggingOptions.buildInfo.isRelease == true) {
158 159
      return LaunchResult.succeeded();
    }
160
    final ProtocolDiscovery observatoryDiscovery = ProtocolDiscovery.observatory(_deviceLogReader,
161 162
      devicePort: debuggingOptions.deviceVmServicePort,
      hostPort: debuggingOptions.hostVmServicePort,
163
      ipv6: ipv6,
164
      logger: _logger,
165
    );
166
    try {
167
      final Uri? observatoryUri = await observatoryDiscovery.uri;
168 169 170 171
      if (observatoryUri != null) {
        onAttached(package, buildMode, process);
        return LaunchResult.succeeded(observatoryUri: observatoryUri);
      }
172
      _logger.printError(
173
        'Error waiting for a debug connection: '
174
        'The log reader stopped unexpectedly, or never started.',
175
      );
176
    } on Exception catch (error) {
177
      _logger.printError('Error waiting for a debug connection: $error');
178 179 180
    } finally {
      await observatoryDiscovery.cancel();
    }
181
    return LaunchResult.failed();
182 183 184
  }

  @override
185 186
  Future<bool> stopApp(
    ApplicationPackage app, {
187
    String? userIdentifier,
188
  }) async {
189 190 191
    bool succeeded = true;
    // Walk a copy of _runningProcesses, since the exit handler removes from the
    // set.
192
    for (final Process process in Set<Process>.of(_runningProcesses)) {
193
      succeeded &= _processManager.killPid(process.pid);
194 195 196 197
    }
    return succeeded;
  }

198 199
  @override
  Future<void> dispose() async {
200
    await portForwarder.dispose();
201 202
  }

203 204 205
  /// Builds the current project for this device, with the given options.
  Future<void> buildForDevice(
    ApplicationPackage package, {
206
    required BuildInfo buildInfo,
207
    String? mainPath,
208 209 210 211
  });

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

  /// Called after a process is attached, allowing any device-specific extra
  /// steps to be run.
  void onAttached(ApplicationPackage package, BuildMode buildMode, Process process) {}
217 218 219 220 221 222 223 224

  /// Computes a set of environment variables used to pass debugging information
  /// to the engine without interfering with application level command line
  /// arguments.
  ///
  /// The format of the environment variables is:
  ///   * FLUTTER_ENGINE_SWITCHES to the number of switches.
  ///   * FLUTTER_ENGINE_SWITCH_<N> (indexing from 1) to the individual switches.
225
  Map<String, String> _computeEnvironment(DebuggingOptions debuggingOptions, bool traceStartup, String? route) {
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    int flags = 0;
    final Map<String, String> environment = <String, String>{};

    void addFlag(String value) {
      flags += 1;
      environment['FLUTTER_ENGINE_SWITCH_$flags'] = value;
    }
    void finish() {
      environment['FLUTTER_ENGINE_SWITCHES'] = flags.toString();
    }

    addFlag('enable-dart-profiling=true');

    if (traceStartup) {
      addFlag('trace-startup=true');
    }
    if (route != null) {
      addFlag('route=$route');
    }
    if (debuggingOptions.enableSoftwareRendering) {
      addFlag('enable-software-rendering=true');
    }
    if (debuggingOptions.skiaDeterministicRendering) {
      addFlag('skia-deterministic-rendering=true');
    }
    if (debuggingOptions.traceSkia) {
      addFlag('trace-skia=true');
    }
    if (debuggingOptions.traceAllowlist != null) {
      addFlag('trace-allowlist=${debuggingOptions.traceAllowlist}');
    }
257 258 259
    if (debuggingOptions.traceSkiaAllowlist != null) {
      addFlag('trace-skia-allowlist=${debuggingOptions.traceSkiaAllowlist}');
    }
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304
    if (debuggingOptions.traceSystrace) {
      addFlag('trace-systrace=true');
    }
    if (debuggingOptions.endlessTraceBuffer) {
      addFlag('endless-trace-buffer=true');
    }
    if (debuggingOptions.dumpSkpOnShaderCompilation) {
      addFlag('dump-skp-on-shader-compilation=true');
    }
    if (debuggingOptions.cacheSkSL) {
      addFlag('cache-sksl=true');
    }
    if (debuggingOptions.purgePersistentCache) {
      addFlag('purge-persistent-cache=true');
    }
    // Options only supported when there is a VM Service connection between the
    // tool and the device, usually in debug or profile mode.
    if (debuggingOptions.debuggingEnabled) {
      if (debuggingOptions.deviceVmServicePort != null) {
        addFlag('observatory-port=${debuggingOptions.deviceVmServicePort}');
      }
      if (debuggingOptions.buildInfo.isDebug) {
        addFlag('enable-checked-mode=true');
        addFlag('verify-entry-points=true');
      }
      if (debuggingOptions.startPaused) {
        addFlag('start-paused=true');
      }
      if (debuggingOptions.disableServiceAuthCodes) {
        addFlag('disable-service-auth-codes=true');
      }
      final String dartVmFlags = computeDartVmFlags(debuggingOptions);
      if (dartVmFlags.isNotEmpty) {
        addFlag('dart-flags=$dartVmFlags');
      }
      if (debuggingOptions.useTestFonts) {
        addFlag('use-test-fonts=true');
      }
      if (debuggingOptions.verboseSystemLogs) {
        addFlag('verbose-logging=true');
      }
    }
    finish();
    return environment;
  }
305 306
}

307 308
/// A log reader for desktop applications that delegates to a [Process] stdout
/// and stderr streams.
309 310 311
class DesktopLogReader extends DeviceLogReader {
  final StreamController<List<int>> _inputController = StreamController<List<int>>.broadcast();

312
  /// Begin listening to the stdout and stderr streams of the provided [process].
313
  void initializeProcess(Process process) {
314 315 316 317 318 319
    final StreamSubscription<List<int>> stdoutSub = process.stdout.listen(
      _inputController.add,
    );
    final StreamSubscription<List<int>> stderrSub = process.stderr.listen(
      _inputController.add,
    );
320 321 322 323
    final Future<void> stdioFuture = Future.wait<void>(<Future<void>>[
      stdoutSub.asFuture<void>(),
      stderrSub.asFuture<void>(),
    ]);
324 325
    process.exitCode.whenComplete(() async {
      // Wait for output to be fully processed.
326 327 328
      await stdioFuture;
      // The streams have already completed, so waiting for the stream
      // cancellation to complete is not needed.
329 330 331 332
      unawaited(stdoutSub.cancel());
      unawaited(stderrSub.cancel());
      await _inputController.close();
    });
333 334 335 336 337 338 339 340 341 342 343
  }

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

  @override
  String get name => 'desktop';
344 345 346 347 348

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