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

5 6
// @dart = 2.8

7 8
import 'dart:async';

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

12
import '../application_package.dart';
13 14 15
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/os.dart';
16
import '../base/utils.dart';
17
import '../build_info.dart';
18
import '../desktop_device.dart';
19
import '../device.dart';
20
import '../device_port_forwarder.dart';
21
import '../features.dart';
22 23 24
import '../project.dart';
import 'application_package.dart';
import 'build_windows.dart';
25
import 'uwptool.dart';
26 27 28
import 'windows_workflow.dart';

/// A device that represents a desktop Windows target.
29
class WindowsDevice extends DesktopDevice {
30 31 32 33 34 35
  WindowsDevice({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
  }) : super(
36
      'windows',
37 38
      platformType: PlatformType.windows,
      ephemeral: false,
39 40 41 42
      processManager: processManager,
      logger: logger,
      fileSystem: fileSystem,
      operatingSystemUtils: operatingSystemUtils,
43
  );
44 45 46 47 48

  @override
  bool isSupported() => true;

  @override
49
  String get name => 'Windows';
50 51

  @override
52
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.windows_x64;
53 54

  @override
55 56 57
  bool isSupportedForProject(FlutterProject flutterProject) {
    return flutterProject.windows.existsSync();
  }
58 59

  @override
60
  Future<void> buildForDevice(
61
    covariant WindowsApp package, {
62
    String mainPath,
63
    BuildInfo buildInfo,
64
  }) async {
65 66 67 68
    await buildWindows(
      FlutterProject.current().windows,
      buildInfo,
      target: mainPath,
69
    );
70 71 72
  }

  @override
73 74
  String executablePathForDevice(covariant WindowsApp package, BuildMode buildMode) {
    return package.executable(buildMode);
75
  }
76 77
}

78
// A device that represents a desktop Windows UWP target.
79
class WindowsUWPDevice extends Device {
80 81 82 83 84
  WindowsUWPDevice({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    @required UwpTool uwptool,
  }) : _logger = logger,
       _processManager = processManager,
       _operatingSystemUtils = operatingSystemUtils,
       _fileSystem = fileSystem,
       _uwptool = uwptool,
       super(
         'winuwp',
         platformType: PlatformType.windows,
         ephemeral: false,
         category: Category.desktop,
       );

  final ProcessManager _processManager;
  final Logger _logger;
  final FileSystem _fileSystem;
  final OperatingSystemUtils _operatingSystemUtils;
  final UwpTool _uwptool;
  BuildMode _buildMode;

  int _processId;
106 107

  @override
108
  bool isSupported() => true;
109 110 111 112 113 114 115 116 117

  @override
  String get name => 'Windows (UWP)';

  @override
  Future<TargetPlatform> get targetPlatform async => TargetPlatform.windows_uwp_x64;

  @override
  bool isSupportedForProject(FlutterProject flutterProject) {
118
    return flutterProject.windowsUwp.existsSync();
119 120 121
  }

  @override
122 123 124 125 126 127 128 129 130 131 132 133 134
  void clearLogs() { }

  @override
  Future<void> dispose() async { }

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

  @override
  FutureOr<DeviceLogReader> getLogReader({covariant BuildableUwpApp app, bool includePastLogs = false}) {
    return NoOpDeviceLogReader('winuwp');
  }

135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
  // Returns `true` if the specified file is a valid package based on file extension.
  bool _isValidPackage(String packagePath) {
    const List<String> validPackageExtensions = <String>[
      '.appx', '.msix',                // Architecture-specific application.
      '.appxbundle', '.msixbundle',    // Architecture-independent application.
      '.eappx', '.emsix',              // Encrypted architecture-specific application.
      '.eappxbundle', '.emsixbundle',  // Encrypted architecture-independent application.
    ];
    return validPackageExtensions.any(packagePath.endsWith);
  }

  // Walks the build directory for any dependent packages for the specified architecture.
  List<String> _getPackagePaths(String directory) {
    if (!_fileSystem.isDirectorySync(directory)) {
      return <String>[];
    }
    final List<String> packagePaths = <String>[];
    for (final FileSystemEntity entity in _fileSystem.directory(directory).listSync()) {
      if (entity.statSync().type != FileSystemEntityType.file) {
        continue;
      }
      final String packagePath = entity.absolute.path;
      if (_isValidPackage(packagePath)) {
        packagePaths.add(packagePath);
      }
    }
    return packagePaths;
  }

  // Walks the build directory for any dependent packages for the specified architecture.
  String/*?*/ _getAppPackagePath(String buildDirectory) {
    final List<String> packagePaths = _getPackagePaths(buildDirectory);
    return packagePaths.isNotEmpty ? packagePaths.first : null;
  }

  // Walks the build directory for any dependent packages for the specified architecture.
  List<String> _getDependencyPaths(String buildDirectory, String architecture) {
    final String depsDirectory = _fileSystem.path.join(buildDirectory, 'Dependencies', architecture);
    return _getPackagePaths(depsDirectory);
  }

176 177 178 179 180 181 182 183 184 185
  String _getPackageName(String binaryName, String version, String config, {String/*?*/ architecture}) {
    final List<String> components = <String>[
      binaryName,
      version,
      if (architecture != null) architecture,
      config,
    ];
    return components.join('_');
  }

186 187 188 189 190 191 192 193 194
  @override
  Future<bool> installApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
    /// The cmake build generates an install powershell script.
    /// build\winuwp\runner_uwp\AppPackages\<app-name>\<app-name>_<app-version>_<cmake-config>\Add-AppDevPackage.ps1
    final String binaryName = app.name;
    final String packageVersion = app.projectVersion;
    if (packageVersion == null) {
      return false;
    }
195 196
    final String binaryDir = _fileSystem.path.absolute(
        _fileSystem.path.join('build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName));
197
    final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug));
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

    // If a multi-architecture package exists, install that; otherwise install
    // the single-architecture package.
    final List<String> packageNames = <String>[
      // Multi-archtitecture package.
      _getPackageName(binaryName, packageVersion, config),
      // Single-archtitecture package.
      _getPackageName(binaryName, packageVersion, config, architecture: 'x64'),
    ];
    String packageName;
    String buildDirectory;
    String packagePath;
    for (final String name in packageNames) {
      packageName = name;
      buildDirectory = _fileSystem.path.join(binaryDir, '${packageName}_Test');
      if (_fileSystem.isDirectorySync(buildDirectory)) {
        packagePath = _getAppPackagePath(buildDirectory);
        if (packagePath != null && _fileSystem.isFileSync(packagePath)) {
          break;
        }
      }
    }
    if (packagePath == null) {
      _logger.printError('Failed to locate app package to install');
      return false;
    }
224 225 226 227

    // Verify package signature.
    if (!await _uwptool.isSignatureValid(packagePath)) {
      // If signature is invalid, install the developer certificate.
228
      final String certificatePath = _fileSystem.path.join(buildDirectory, '$packageName.cer');
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
      if (_logger.terminal.stdinHasTerminal) {
        final String response = await _logger.terminal.promptForCharInput(
          <String>['Y', 'y', 'N', 'n'],
          logger: _logger,
          prompt: 'Install developer certificate.\n'
          '\n'
          'Windows UWP apps are signed with a developer certificate during the build\n'
          'process. On the first install of an app with a signature from a new\n'
          'certificate, the certificate must be installed.\n'
          '\n'
          'If desired, this certificate can later be removed by launching the \n'
          '"Manage Computer Certificates" control panel from the Start menu and deleting\n'
          'the "CMake Test Cert" certificate from the "Trusted People" > "Certificates"\n'
          'section.\n'
          '\n'
          'Press "Y" to continue, or "N" to cancel.',
          displayAcceptedCharacters: false,
        );
        if (response == 'N' || response == 'n') {
          return false;
        }
      }
      await _uwptool.installCertificate(certificatePath);
    }

    // Install the application and dependencies.
    final String packageUri = Uri.file(packagePath).toString();
256
    final List<String> dependencyUris = _getDependencyPaths(buildDirectory, 'x64')
257 258
        .map((String path) => Uri.file(path).toString())
        .toList();
259
    return _uwptool.installApp(packageUri, dependencyUris);
260 261 262
  }

  @override
263 264 265 266
  Future<bool> isAppInstalled(covariant ApplicationPackage app, {String userIdentifier}) async {
    final String packageName = app.id;
    return await _uwptool.getPackageFamilyName(packageName) != null;
  }
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281

  @override
  Future<bool> isLatestBuildInstalled(covariant ApplicationPackage app) async => false;

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

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

  @override
  Future<String> get sdkNameAndVersion async => '';

  @override
  Future<LaunchResult> startApp(covariant BuildableUwpApp package, {
282
    String mainPath,
283 284 285 286 287 288
    String route,
    DebuggingOptions debuggingOptions,
    Map<String, dynamic> platformArgs,
    bool prebuiltApplication = false,
    bool ipv6 = false,
    String userIdentifier,
289
  }) async {
290 291 292 293 294 295 296 297
    _buildMode = debuggingOptions.buildInfo.mode;
    if (!prebuiltApplication) {
      await buildWindowsUwp(
        package.project,
        debuggingOptions.buildInfo,
        target: mainPath,
      );
    }
298 299 300 301 302
    if (await isAppInstalled(package) && !await uninstallApp(package)) {
      _logger.printError('Failed to uninstall previous app package');
      return LaunchResult.failed();
    }
    if (!await installApp(package)) {
303 304 305 306
      _logger.printError('Failed to install app package');
      return LaunchResult.failed();
    }

307 308
    final String packageName = package.id;
    if (packageName == null) {
309 310 311 312
      _logger.printError('Could not find PACKAGE_GUID in ${package.project.runnerCmakeFile.path}');
      return LaunchResult.failed();
    }

313
    final String packageFamily = await _uwptool.getPackageFamilyName(packageName);
314 315

    if (debuggingOptions.buildInfo.mode.isRelease) {
316
      _processId = await _uwptool.launchApp(packageFamily, <String>[]);
317 318 319 320 321
      return _processId != null ? LaunchResult.succeeded() : LaunchResult.failed();
    }

    /// If the terminal is attached, prompt the user to open the firewall port.
    if (_logger.terminal.stdinHasTerminal) {
322 323 324 325 326 327 328 329 330 331 332 333 334 335
      final String response = await _logger.terminal.promptForCharInput(
        <String>['Y', 'y', 'N', 'n'],
        logger: _logger,
        prompt: 'Enable Flutter debugging from localhost.\n'
        '\n'
        'Windows UWP apps run in a sandboxed environment. To enable Flutter debugging\n'
        'and hot reload, you will need to enable inbound connections to the app from the\n'
        'Flutter tool running on your machine. To do so:\n'
        '  1. Launch PowerShell as an Administrator\n'
        '  2. Enter the following command:\n'
        '     checknetisolation loopbackexempt -is -n=$packageFamily\n'
        '\n'
        'Press "Y" once this is complete, or "N" to abort.',
        displayAcceptedCharacters: false,
336
      );
337 338 339
      if (response == 'N' || response == 'n') {
        return LaunchResult.failed();
      }
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
    }

    /// Currently we do not have a way to discover the VM Service URI.
    final int port = debuggingOptions.deviceVmServicePort ?? await _operatingSystemUtils.findFreePort();
    final List<String> args = <String>[
      '--observatory-port=$port',
      '--disable-service-auth-codes',
      '--enable-dart-profiling',
      if (debuggingOptions.startPaused) '--start-paused',
      if (debuggingOptions.useTestFonts) '--use-test-fonts',
      if (debuggingOptions.debuggingEnabled) ...<String>[
        '--enable-checked-mode',
        '--verify-entry-points',
      ],
      if (debuggingOptions.enableSoftwareRendering) '--enable-software-rendering',
      if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering',
      if (debuggingOptions.traceSkia) '--trace-skia',
      if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"',
358
      if (debuggingOptions.traceSkiaAllowlist != null) '--trace-skia-allowlist="${debuggingOptions.traceSkiaAllowlist}"',
359 360 361 362 363 364 365
      if (debuggingOptions.endlessTraceBuffer) '--endless-trace-buffer',
      if (debuggingOptions.dumpSkpOnShaderCompilation) '--dump-skp-on-shader-compilation',
      if (debuggingOptions.verboseSystemLogs) '--verbose-logging',
      if (debuggingOptions.cacheSkSL) '--cache-sksl',
      if (debuggingOptions.purgePersistentCache) '--purge-persistent-cache',
      if (platformArgs['trace-startup'] as bool ?? false) '--trace-startup',
    ];
366
    _processId = await _uwptool.launchApp(packageFamily, args);
367 368 369 370
    if (_processId == null) {
      return LaunchResult.failed();
    }
    return LaunchResult.succeeded(observatoryUri: Uri.parse('http://localhost:$port'));
371 372 373
  }

  @override
374 375 376 377 378
  Future<bool> stopApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
    if (_processId != null) {
      return _processManager.killPid(_processId);
    }
    return false;
379
  }
380 381 382

  @override
  Future<bool> uninstallApp(covariant BuildableUwpApp app, {String userIdentifier}) async {
383 384 385 386 387 388 389 390 391 392 393
    final String packageName = app.id;
    if (packageName == null) {
      _logger.printError('Could not find PACKAGE_GUID in ${app.project.runnerCmakeFile.path}');
      return false;
    }
    final String packageFamily = await _uwptool.getPackageFamilyName(packageName);
    if (packageFamily == null) {
      // App is not installed.
      return true;
    }
    return _uwptool.uninstallApp(packageFamily);
394 395 396 397
  }

  @override
  FutureOr<bool> supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease;
398 399
}

400
class WindowsDevices extends PollingDeviceDiscovery {
401 402 403 404 405 406
  WindowsDevices({
    @required ProcessManager processManager,
    @required Logger logger,
    @required FileSystem fileSystem,
    @required OperatingSystemUtils operatingSystemUtils,
    @required WindowsWorkflow windowsWorkflow,
407
    @required FeatureFlags featureFlags,
408
    @required UwpTool uwptool,
409 410 411 412 413
  }) : _fileSystem = fileSystem,
      _logger = logger,
      _processManager = processManager,
      _operatingSystemUtils = operatingSystemUtils,
      _windowsWorkflow = windowsWorkflow,
414
      _featureFlags = featureFlags,
415
      _uwptool = uwptool,
416 417 418 419 420 421 422
      super('windows devices');

  final FileSystem _fileSystem;
  final Logger _logger;
  final ProcessManager _processManager;
  final OperatingSystemUtils _operatingSystemUtils;
  final WindowsWorkflow _windowsWorkflow;
423
  final FeatureFlags _featureFlags;
424
  final UwpTool _uwptool;
425 426

  @override
427
  bool get supportsPlatform => _windowsWorkflow.appliesToHostPlatform;
428 429

  @override
430
  bool get canListAnything => _windowsWorkflow.canListDevices;
431 432

  @override
433
  Future<List<Device>> pollingGetDevices({ Duration timeout }) async {
434 435 436 437
    if (!canListAnything) {
      return const <Device>[];
    }
    return <Device>[
438 439 440 441 442 443
      WindowsDevice(
        fileSystem: _fileSystem,
        logger: _logger,
        processManager: _processManager,
        operatingSystemUtils: _operatingSystemUtils,
      ),
444 445 446 447 448 449
      if (_featureFlags.isWindowsUwpEnabled)
        WindowsUWPDevice(
          fileSystem: _fileSystem,
          logger: _logger,
          processManager: _processManager,
          operatingSystemUtils: _operatingSystemUtils,
450
          uwptool: _uwptool,
451
        )
452 453 454 455 456
    ];
  }

  @override
  Future<List<String>> getDiagnostics() async => const <String>[];
457 458 459

  @override
  List<String> get wellKnownIds => const <String>['windows', 'winuwp'];
460
}