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 26 27
  DesktopDevice(super.identifier, {
      required PlatformType super.platformType,
      required super.ephemeral,
28 29 30 31
      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
         super(
          category: Category.desktop,
        );

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

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

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

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

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

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

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

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

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

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

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

  @override
  void clearLogs() {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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