desktop_device.dart 11.2 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

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

  final Logger _logger;
  final ProcessManager _processManager;
41
  final FileSystem _fileSystem;
42
  final OperatingSystemUtils _operatingSystemUtils;
43 44 45
  final Set<Process> _runningProcesses = <Process>{};
  final DesktopLogReader _deviceLogReader = DesktopLogReader();

46
  @override
47
  DevFSWriter createDevFSWriter(ApplicationPackage? app, String? userIdentifier) {
48 49
    return LocalDevFSWriter(fileSystem: _fileSystem);
  }
50

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

  // 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
67 68
  Future<bool> installApp(
    ApplicationPackage app, {
69
    String? userIdentifier,
70
  }) async => true;
71 72 73 74

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

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

  @override
84
  Future<String?> get emulatorId async => null;
85 86 87 88 89

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

  @override
90
  Future<String> get sdkNameAndVersion async => _operatingSystemUtils.name;
91

92 93 94
  @override
  bool supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;

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

  @override
  void clearLogs() {}

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

    // Ensure that the executable is locatable.
126
    final BuildInfo buildInfo = debuggingOptions.buildInfo;
127
    final bool traceStartup = platformArgs['trace-startup'] as bool? ?? false;
128
    final String? executable = executablePathForDevice(package, buildInfo);
129
    if (executable == null) {
130
      _logger.printError('Unable to find executable to run');
131 132 133
      return LaunchResult.failed();
    }

134 135 136
    Process process;
    final List<String> command = <String>[
      executable,
137
      ...debuggingOptions.dartEntrypointArgs,
138 139 140 141 142 143 144 145 146 147
    ];
    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;
    }
148 149 150
    _runningProcesses.add(process);
    unawaited(process.exitCode.then((_) => _runningProcesses.remove(process)));

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

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

193 194
  @override
  Future<void> dispose() async {
195
    await portForwarder.dispose();
196 197
  }

198
  /// Builds the current project for this device, with the given options.
199
  Future<void> buildForDevice({
200
    required BuildInfo buildInfo,
201
    String? mainPath,
202 203 204 205
  });

  /// Returns the path to the executable to run for [package] on this device for
  /// the given [buildMode].
206
  String? executablePathForDevice(ApplicationPackage package, BuildInfo buildInfo);
207 208 209

  /// Called after a process is attached, allowing any device-specific extra
  /// steps to be run.
210
  void onAttached(ApplicationPackage package, BuildInfo buildInfo, Process process) {}
211 212 213 214 215 216 217 218

  /// 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.
219
  Map<String, String> _computeEnvironment(DebuggingOptions debuggingOptions, bool traceStartup, String? route) {
220 221 222 223 224 225 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
    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}');
    }
251 252 253
    if (debuggingOptions.traceSkiaAllowlist != null) {
      addFlag('trace-skia-allowlist=${debuggingOptions.traceSkiaAllowlist}');
    }
254 255 256
    if (debuggingOptions.traceSystrace) {
      addFlag('trace-systrace=true');
    }
257 258 259
    if (debuggingOptions.traceToFile != null) {
      addFlag('trace-to-file=${debuggingOptions.traceToFile}');
    }
260 261 262 263 264 265 266 267 268 269 270 271
    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');
    }
272 273 274 275 276 277
    switch (debuggingOptions.enableImpeller) {
      case ImpellerStatus.enabled:
        addFlag('enable-impeller=true');
      case ImpellerStatus.disabled:
      case ImpellerStatus.platformDefault:
        addFlag('enable-impeller=false');
278
    }
279 280 281 282
    // 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) {
283
        addFlag('vm-service-port=${debuggingOptions.deviceVmServicePort}');
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
      }
      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;
  }
309 310
}

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

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

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

  @override
  String get name => 'desktop';
348 349 350 351 352

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