desktop_device.dart 10.9 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 25 26
  DesktopDevice(super.identifier, {
      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(covariant 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 120
  }) async {
    if (!prebuiltApplication) {
      await buildForDevice(
        package,
121
        buildInfo: debuggingOptions.buildInfo,
122 123 124 125 126
        mainPath: mainPath,
      );
    }

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

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

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

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

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

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

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

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

  /// 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.
221
  Map<String, String> _computeEnvironment(DebuggingOptions debuggingOptions, bool traceStartup, String? route) {
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 251 252
    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}');
    }
253 254 255
    if (debuggingOptions.traceSkiaAllowlist != null) {
      addFlag('trace-skia-allowlist=${debuggingOptions.traceSkiaAllowlist}');
    }
256 257 258 259 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
    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;
  }
301 302
}

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

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

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

  @override
  String get name => 'desktop';
340 341 342 343 344

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