2023-02-24 flutter attach流程解析1

2023-02-23  本文已影响0人  我是小胡胡分胡

flutter attach时候经常出现下面这种错误:

Rerun this command with one of the following passed in as the appId:

  flutter attach --app-id com.test.1
  flutter attach --app-id com.test.1 (2)
  flutter attach --app-id com.test.1 (3)

基于此探索一下与flutter attach相关的内容。

Flutter是一个跨平台的移动应用程序开发框架,Flutter attach是Flutter命令行工具提供的一个命令,用于将开发者的编辑器(如VSCode、Android Studio)连接到正在运行的Flutter应用程序,以便于进行调试。Flutter attach的原理是利用了Dart VM的一个调试协议——VM服务协议,它允许开发者以REST风格的API与Dart VM进行通信。

Flutter attach的连接流程可以大致分为以下几步:

需要注意的是,Flutter attach命令要求开发者在启动Flutter应用程序时启用了VM服务。如果在启动应用程序时未启用VM服务,则无法使用Flutter attach命令进行连接。此外,Flutter attach命令还要求运行中的Flutter应用程序与编辑器在同一台计算机上,或者在通过网络进行通信时,必须通过安全的通道进行连接。

另外flutter attach 命令需要 flutter 应用程序对应的源代码,否则报错:
Target file "lib/main.dart" not found.
因为需要热重载和热重启时,需要比对源代码的修改,做出文件同步,这是可以理解的。

1、attach

连接到 Flutter 应用程序并启动开发工具和调试服务

attach-》 _attachToDevice-》getObservatoryUri-》 _client.start(); -》

  @override
  Future<int> attach({
    Completer<DebugConnectionInfo> connectionInfoCompleter,
    Completer<void> appStartedCompleter,
    bool allowExistingDdsInstance = false,
    bool enableDevTools = false,
  }) async {
    _didAttach = true;
    try {
      await connectToServiceProtocol(
        reloadSources: _reloadSourcesService,
        restart: _restartService,
        compileExpression: _compileExpressionService,
        getSkSLMethod: writeSkSL,
        allowExistingDdsInstance: allowExistingDdsInstance,
      );
    // Catches all exceptions, non-Exception objects are rethrown.
    } catch (error) { // ignore: avoid_catches_without_on_clauses
      if (error is! Exception && error is! String) {
        rethrow;
      }
      globals.printError('Error connecting to the service protocol: $error');
      return 2;
    }

    if (enableDevTools) {
      // The method below is guaranteed never to return a failing future.
      unawaited(residentDevtoolsHandler.serveAndAnnounceDevTools(
        devToolsServerAddress: debuggingOptions.devToolsServerAddress,
        flutterDevices: flutterDevices,
      ));
    }

    for (final FlutterDevice device in flutterDevices) {
      await device.initLogReader();
    }
    try {
      final List<Uri> baseUris = await _initDevFS();
      if (connectionInfoCompleter != null) {
        // Only handle one debugger connection.
        connectionInfoCompleter.complete(
          DebugConnectionInfo(
            httpUri: flutterDevices.first.vmService.httpAddress,
            wsUri: flutterDevices.first.vmService.wsAddress,
            baseUri: baseUris.first.toString(),
          ),
        );
      }
    } on DevFSException catch (error) {
      globals.printError('Error initializing DevFS: $error');
      return 3;
    }

    final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
    final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
    _addBenchmarkData(
      'hotReloadInitialDevFSSyncMilliseconds',
      initialUpdateDevFSsTimer.elapsed.inMilliseconds,
    );
    if (!devfsResult.success) {
      return 3;
    }

    for (final FlutterDevice device in flutterDevices) {
      // VM must have accepted the kernel binary, there will be no reload
      // report, so we let incremental compiler know that source code was accepted.
      if (device.generator != null) {
        device.generator.accept();
      }
      final List<FlutterView> views = await device.vmService.getFlutterViews();
      for (final FlutterView view in views) {
        globals.printTrace('Connected to $view.');
      }
    }

    // In fast-start mode, apps are initialized from a placeholder splashscreen
    // app. We must do a restart here to load the program and assets for the
    // real app.
    if (debuggingOptions.fastStart) {
      await restart(
        fullRestart: true,
        reason: 'restart',
        silent: true,
      );
    }

    appStartedCompleter?.complete();

    if (benchmarkMode) {
      // Wait multiple seconds for the isolate to have fully started.
      await Future<void>.delayed(const Duration(seconds: 10));
      // We are running in benchmark mode.
      globals.printStatus('Running in benchmark mode.');
      // Measure time to perform a hot restart.
      globals.printStatus('Benchmarking hot restart');
      await restart(fullRestart: true);
      // Wait multiple seconds to stabilize benchmark on slower device lab hardware.
      // Hot restart finishes when the new isolate is started, not when the new isolate
      // is ready. This process can actually take multiple seconds.
      await Future<void>.delayed(const Duration(seconds: 10));

      globals.printStatus('Benchmarking hot reload');
      // Measure time to perform a hot reload.
      await restart();
      if (stayResident) {
        await waitForAppToFinish();
      } else {
        globals.printStatus('Benchmark completed. Exiting application.');
        await _cleanupDevFS();
        await stopEchoingDeviceLog();
        await exitApp();
      }
      final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
      benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
      return 0;
    }
    writeVmServiceFile();

    int result = 0;
    if (stayResident) {
      result = await waitForAppToFinish();
    }
    await cleanupAtFinish();
    return result;
  }

上面的代码是 Flutter 开发框架中的一个函数,它在调试模式下连接到 Flutter 应用程序并启动开发工具和调试服务。它有几个参数,用于控制连接和初始化的行为。

2、_attachToDevice

Future<void> _attachToDevice(Device device) async {
    final FlutterProject flutterProject = FlutterProject.current();

    final Daemon daemon = boolArg('machine')
      ? Daemon(
          DaemonConnection(
            daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
            logger: globals.logger,
          ),
          notifyingLogger: (globals.logger is NotifyingLogger)
            ? globals.logger as NotifyingLogger
            : NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
          logToStdout: true,
        )
      : null;

    Stream<Uri> observatoryUri;
    bool usesIpv6 = ipv6;
    final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
    final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
    final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;

    if (debugPort == null && debugUri == null) {
      if (device is FuchsiaDevice) {
        final String module = stringArg('module');
        if (module == null) {
          throwToolExit("'--module' is required for attaching to a Fuchsia device");
        }
        usesIpv6 = device.ipv6;
        FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
        try {
          isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
          observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
        } on Exception {
          isolateDiscoveryProtocol?.dispose();
          final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
          for (final ForwardedPort port in ports) {
            await device.portForwarder.unforward(port);
          }
          rethrow;
        }
      } else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
        final Uri uriFromMdns =
          await MDnsObservatoryDiscovery.instance.getObservatoryUri(
            appId,
            device,
            usesIpv6: usesIpv6,
            deviceVmservicePort: deviceVmservicePort,
          );
        observatoryUri = uriFromMdns == null
          ? null
          : Stream<Uri>.value(uriFromMdns).asBroadcastStream();
      }
      // If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
      if (observatoryUri == null) {
        final ProtocolDiscovery observatoryDiscovery =
          ProtocolDiscovery.observatory(
            // If it's an Android device, attaching relies on past log searching
            // to find the service protocol.
            await device.getLogReader(includePastLogs: device is AndroidDevice),
            portForwarder: device.portForwarder,
            ipv6: ipv6,
            devicePort: deviceVmservicePort,
            hostPort: hostVmservicePort,
            logger: globals.logger,
          );
        globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
        observatoryUri = observatoryDiscovery.uris;
        // Determine ipv6 status from the scanned logs.
        usesIpv6 = observatoryDiscovery.ipv6;
      }
    } else {
      observatoryUri = Stream<Uri>
        .fromFuture(
          buildObservatoryUri(
            device,
            debugUri?.host ?? hostname,
            debugPort ?? debugUri.port,
            hostVmservicePort,
            debugUri?.path,
          )
        ).asBroadcastStream();
    }

    globals.terminal.usesTerminalUi = daemon == null;

    try {
      int result;
      if (daemon != null) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
        AppInstance app;
        try {
          app = await daemon.appDomain.launch(
            runner,
            ({Completer<DebugConnectionInfo> connectionInfoCompleter,
              Completer<void> appStartedCompleter}) {
              return runner.attach(
                connectionInfoCompleter: connectionInfoCompleter,
                appStartedCompleter: appStartedCompleter,
                allowExistingDdsInstance: true,
                enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
              );
            },
            device,
            null,
            true,
            globals.fs.currentDirectory,
            LaunchMode.attach,
            globals.logger as AppRunLogger,
          );
        } on Exception catch (error) {
          throwToolExit(error.toString());
        }
        result = await app.runner.waitForAppToFinish();
        assert(result != null);
        return;
      }
      while (true) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
        final Completer<void> onAppStart = Completer<void>.sync();
        TerminalHandler terminalHandler;
        unawaited(onAppStart.future.whenComplete(() {
          terminalHandler = TerminalHandler(
            runner,
            logger: globals.logger,
            terminal: globals.terminal,
            signals: globals.signals,
            processInfo: globals.processInfo,
            reportReady: boolArg('report-ready'),
            pidFile: stringArg('pid-file'),
          )
            ..registerSignalHandlers()
            ..setupTerminal();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
          allowExistingDdsInstance: true,
          enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
        );
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        terminalHandler?.stop();
        assert(result != null);
        if (runner.exited || !runner.isWaitingForObservatory) {
          break;
        }
        globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
      }
    } on RPCError catch (err) {
      if (err.code == RPCErrorCodes.kServiceDisappeared) {
        throwToolExit('Lost connection to device.');
      }
      rethrow;
    } finally {
      final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
      for (final ForwardedPort port in ports) {
        await device.portForwarder.unforward(port);
      }
    }
  }

找到uri http://127.0.0.1:55177/RXKA2jepV60=/
运行while循环接收指令:

while (true) {
        final ResidentRunner runner = await createResidentRunner(
          observatoryUris: observatoryUri,
          device: device,
          flutterProject: flutterProject,
          usesIpv6: usesIpv6,
        );
        final Completer<void> onAppStart = Completer<void>.sync();
        TerminalHandler terminalHandler;
        unawaited(onAppStart.future.whenComplete(() {
          terminalHandler = TerminalHandler(
            runner,
            logger: globals.logger,
            terminal: globals.terminal,
            signals: globals.signals,
            processInfo: globals.processInfo,
            reportReady: boolArg('report-ready'),
            pidFile: stringArg('pid-file'),
          )
            ..registerSignalHandlers()
            ..setupTerminal();
        }));
        result = await runner.attach(
          appStartedCompleter: onAppStart,
          allowExistingDdsInstance: true,
          enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
        );
        if (result != 0) {
          throwToolExit(null, exitCode: result);
        }
        terminalHandler?.stop();
        assert(result != null);
        if (runner.exited || !runner.isWaitingForObservatory) {
          break;
        }
        globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
      }

3、getObservatoryUri

 @visibleForTesting
  Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
    _logger.printTrace('Checking for advertised Dart observatories...');
    try {
      await _client.start();
      final List<PtrResourceRecord> pointerRecords = await _client
        .lookup<PtrResourceRecord>(
          ResourceRecordQuery.serverPointer(dartObservatoryName),
        )
        .toList();
      if (pointerRecords.isEmpty) {
        _logger.printTrace('No pointer records found.');
        return null;
      }
      // We have no guarantee that we won't get multiple hits from the same
      // service on this.
      final Set<String> uniqueDomainNames = pointerRecords
        .map<String>((PtrResourceRecord record) => record.domainName)
        .toSet();

      String? domainName;
      if (applicationId != null) {
        for (final String name in uniqueDomainNames) {
          if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
            domainName = name;
            break;
          }
        }
        if (domainName == null) {
          throwToolExit('Did not find a observatory port advertised for $applicationId.');
        }
      } else if (uniqueDomainNames.length > 1) {
        final StringBuffer buffer = StringBuffer();
        buffer.writeln('There are multiple observatory ports available.');
        buffer.writeln('Rerun this command with one of the following passed in as the appId:');
        buffer.writeln();
        for (final String uniqueDomainName in uniqueDomainNames) {
          buffer.writeln('  flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
        }
        throwToolExit(buffer.toString());
      } else {
        domainName = pointerRecords[0].domainName;
      }
      _logger.printTrace('Checking for available port on $domainName');
      // Here, if we get more than one, it should just be a duplicate.
      final List<SrvResourceRecord> srv = await _client
        .lookup<SrvResourceRecord>(
          ResourceRecordQuery.service(domainName),
        )
        .toList();
      if (srv.isEmpty) {
        return null;
      }
      if (srv.length > 1) {
        _logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
                   '- using first one (${srv.first.port}).');
      }
      _logger.printTrace('Checking for authentication code for $domainName');
      final List<TxtResourceRecord> txt = await _client
        .lookup<TxtResourceRecord>(
            ResourceRecordQuery.text(domainName),
        )
        .toList();
      if (txt == null || txt.isEmpty) {
        return MDnsObservatoryDiscoveryResult(srv.first.port, '');
      }
      const String authCodePrefix = 'authCode=';
      String? raw;
      for (final String record in txt.first.text.split('\n')) {
        if (record.startsWith(authCodePrefix)) {
          raw = record;
          break;
        }
      }
      if (raw == null) {
        return MDnsObservatoryDiscoveryResult(srv.first.port, '');
      }
      String authCode = raw.substring(authCodePrefix.length);
      // The Observatory currently expects a trailing '/' as part of the
      // URI, otherwise an invalid authentication code response is given.
      if (!authCode.endsWith('/')) {
        authCode += '/';
      }
      return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
    } finally {
      _client.stop();
    }
  }

代码流程如下:

总的来说,每一次的attach都会启动一个启动MDNS客户端,如果启动失败,则停止MDNS客户端。

4、 await _client.start();

Future<void> start({
    InternetAddress? listenAddress,
    NetworkInterfacesFactory? interfacesFactory,
    int mDnsPort = mDnsPort,
    InternetAddress? mDnsAddress,
  }) async {
    listenAddress ??= InternetAddress.anyIPv4;
    interfacesFactory ??= allInterfacesFactory;

    assert(listenAddress.address == InternetAddress.anyIPv4.address ||
        listenAddress.address == InternetAddress.anyIPv6.address);

    if (_started || _starting) {
      return;
    }
    _starting = true;

    final int selectedMDnsPort = _mDnsPort = mDnsPort;
    _mDnsAddress = mDnsAddress;

    // Listen on all addresses.
    final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
      listenAddress.address,
      selectedMDnsPort,
      reuseAddress: true,
      reusePort: true,
      ttl: 255,
    );

    // Can't send to IPv6 any address.
    if (incoming.address != InternetAddress.anyIPv6) {
      _sockets.add(incoming);
    } else {
      _toBeClosed.add(incoming);
    }

    _mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
        ? mDnsAddressIPv4
        : mDnsAddressIPv6;

    final List<NetworkInterface> interfaces =
        (await interfacesFactory(listenAddress.type)).toList();

    for (final NetworkInterface interface in interfaces) {
      // Create a socket for sending on each adapter.
      final InternetAddress targetAddress = interface.addresses[0];
      final RawDatagramSocket socket = await _rawDatagramSocketFactory(
        targetAddress,
        selectedMDnsPort,
        reuseAddress: true,
        reusePort: true,
        ttl: 255,
      );
      _sockets.add(socket);
      // Ensure that we're using this address/interface for multicast.
      if (targetAddress.type == InternetAddressType.IPv4) {
        socket.setRawOption(RawSocketOption(
          RawSocketOption.levelIPv4,
          RawSocketOption.IPv4MulticastInterface,
          targetAddress.rawAddress,
        ));
      } else {
        socket.setRawOption(RawSocketOption.fromInt(
          RawSocketOption.levelIPv6,
          RawSocketOption.IPv6MulticastInterface,
          interface.index,
        ));
      }
      // Join multicast on this interface.
      incoming.joinMulticast(_mDnsAddress!, interface);
    }
    incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
    _started = true;
    _starting = false;
  }

_sockets.add(socket);会发现有3个sockets
0.0.0.0,127.0.0.1,253.53.111.111 这三个ip地址应该对应是同一个主机。

  // Process incoming datagrams.
  void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
    if (event == RawSocketEvent.read) {
      final Datagram? datagram = incoming.receive();
      if (datagram == null) {
        return;
      }

      // Check for published responses.
      final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
      if (response != null) {
        _cache.updateRecords(response);
        _resolver.handleResponse(response);
        return;
      }
      // TODO(dnfield): Support queries coming in for published entries.
    }
  }

在_handleIncoming的数据回调中,可看到数据长这样:

image.png

总的来说,在Flutter中,mdnsclient.start是启动一个mDNS客户端的方法,用于在本地网络上发现可用的服务。

mDNS是一种广泛使用的服务发现协议,可以通过在本地网络中进行广播和响应来发现可用的服务。mDNS客户端使用查询报文向本地网络中的所有设备发送请求,以查找可用的服务。一旦某个设备响应了请求,mDNS客户端就会接收到包含服务信息的响应报文。

mdnsclient.start方法会启动一个mDNS客户端,并开始向本地网络中发送查询报文。当发现可用的服务时,客户端将回调一个提供服务信息的回调函数,以便应用程序可以处理这些信息。通过这种方式,应用程序可以在本地网络中发现可用的服务,并使用这些服务进行网络通信。

5、 MDnsObservatoryDiscoveryResult(srv.first.port, authCode)

上一篇下一篇

猜你喜欢

热点阅读