Code前端首页关于Code前端联系我们

flutter pub gets error 开始定位Dart SDK问题

terry 2年前 (2023-09-23) 阅读数 65 #移动小程序

当您使用Flutter开发应用程序时,有时需要使用pub工具来获取依赖包。然而国内开发者经常会遇到下载失败的问题。现象是pub进程卡住了。堆栈如下:

Running "flutter packages get" in startup_namer...
The setter 'readEventsEnabled=' was called on null.
Receiver: null
Tried calling: readEventsEnabled=false
package:pub/src/source/hosted.dart 344   BoundHostedSource._throwFriendlyError
package:pub/src/source/hosted.dart 144   BoundHostedSource.doGetVersions
复制代码

长文警告:TLDR版本如下。如果只是想解决下载问题,解决方法如下:
​​禁用代理,设置环境变量,使用国内镜像下载。

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
复制代码

GitHub上已经有几个issue,比如/flutter/issues/25068

Dart Team官方解答的解决方案就是上面的方法。

如果您对问题的原因感兴趣,请阅读以下内容。

作为程序员,我有追求,我对Dart团队的回答并不满意^_^。

因为我有以下问题:

  1. 我的机器上使用的是某台灯,访问fluter官网和飞镖官网没有任何障碍。为什么我无法度过青春期?
  2. pub需要下载文件,如https://pub.dartlang.org/packages/bsdiff/versions/0.1.0.tar.gz,可以通过浏览器快速下载。 Pub只能从中文镜像下载,而且速度很慢。在项目初始化期间这是浪费时间。
  3. 即使无法建立连接,pub也不会崩溃。从日志来看,代码逻辑肯定有问题。

基于以上三点,我怀疑pub没有使用代理,仍然使用原来的网络链接,所以我决定追查这个问题的原因。

STEP 1:分析输入:flutter pub get处理流(flutter_tools)

要解决问题,首先需要找到该项目。在Android Studio项目中,使用命令行命令来更新包:

flutter pub get
复制代码

flutter 是FLUTTER SDK中提供的一个脚本,包含了几个工具。作为一般进入SDK,下面的语句才是真正生效的:

"$DART" --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
复制代码

运行时变量如下:

dart --packages=~/flutter/packages/flutter_tools/.packages  /~/flutter/bin/cache/flutter_tools.snapshot pub get
复制代码

说明:

  • flutter 作为shell脚本,最终通过dart命令调用flutter_tools来执行酒吧负责;
  • --packages是命令运行时依赖的包路径,本场景不使用;
  • .snapshot 文件是由 DART 程序预编译并可执行的快照文件。它可以很容易地与JAVA中的.jar文件进行比较。
  • $@ 将以下命令原封不动的转发给Future_tools进行处理。

flutter_tools代码路径位于FLUTTER SDK文件夹中。它是一个用 DART 语言编写的 CLI 命令行工具:

~/flutter/packages/flutter_tools
复制代码

您可以在 IDE 中创建 DART 命令行工具项目来查看和编译该工具。详情参见:/flutter/wiki/The-flutter-tool

flutter_tool代码逻辑简析:

  • 项目入口:./bin/flutter_tools.dart;在配置运行时文件中的 IDE 中指定它,您就可以在 IDE 中运行它。
void main(List<String> args) {
  executable.main(args);
}
复制代码
  • 命令处理流程:与JAVA、C中一般的CLI程序结构类似,对命令行输入的字符串进行分析,并发送给相应的模块进行处理。比如常用的flutter doctor命令,在commands/doctor.dart中处理:
class DoctorCommand extends FlutterCommand {......}
复制代码
  • pub命令:pub命令很特殊。 Flutter_tools 通过系统的命令行接口调用外部命令:
main() ->
     Executable.main-> 
     FlutterCommandRunner.runCommand -> 
     PackageGetCommand._runPubGet ->
     pubGet() (lib/src/dart/pub.dart)
复制代码

该命令最终是通过 SDK 的 pub 组件执行的

/// The command used for running pub.
List<String> _pubCommand(List<String> arguments) {
  return <String>[ sdkBinaryName('pub') ]..addAll(arguments);
}
复制代码

也就是说,代理连接失败,问题不在于 flutter_tools,你应该继续分析酒吧进程。

STEP 2:缩小范围:pub get(pub)处理流程

  • pub二进制路径位于**~/flutter/bin/cache/dart-sdk/bin/pub**,同样,这个是最终执行的shell脚本。 ./flutter/bin/cache/dart-sdk/bin/snapshots/pub.dart.snapshot
  • 要解决这个问题,我们需要pub的源码。 pub 是 dart sdk 提供的工具,所以源码在 dart-lang 中,./dart-lang/pub
  • 还配置了 Android Studio 中的 Dart Comman Line 项目,不再赘述。pug get -v 可以打印详细日志。
  • 因篇幅限制这里省略Pub流程分析。根据崩溃堆栈分析和代码逻辑,pub使用了dart:io中的HttpClient

STEP 3:问题位置:DEMO复现,编译SDK,遵循SDK逻辑

因为问题出在dart:io ,我单独写了一个DEMO,并在dart:io中用HttpClient进行了测试。我发现这个问题可以很容易地重现。我很热情,解决了问题。绕了一圈终于找到负责人了:

import "dart:io";
import 'dart:convert';
main() async {
  var google = "https://www.google.com/";
  var httpClient = HttpClient();
//  // Lantern proxy, cause crash
  httpClient.findProxy = (uri) {
    return "PROXY 127.0.0.1:45653";
  };
  HttpClientRequest request = await httpClient.getUrl(Uri.parse(baidu));
  HttpClientResponse response = await request.close();
  var responseBody = await response.transform(Utf8Decoder()).join();
  print(responseBody);
}
复制代码

说明:

  • 测试操作系统:Ubuntu 18.04
  • 打开某盏灯:HttpClient使用某盏灯作为代理(设置HttpClient.findProxy()) 。运行,崩溃日志如下。您可以看到 setter 'readEventsEnabled=' 是在 null 上调用的,并且与 pub 崩溃日志类似。我们认为这是由同一问题引起的。
Unhandled exception: NoSuchMethodError: 
    The setter 'readEventsEnabled=' was called on null. 
    Receiver: null Tried calling: readEventsEnabled=false 
#0 _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1112:29) 
#1 _microtaskLoop (dart:async/schedule_microtask.dart:41:21) 
#2 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5) 
#3 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:116:13) 
#4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:173:5)
复制代码

这是对 Dart 的 StackTrace 的抱怨。崩溃日志根本没有打印场景,T_T,但很明显问题一定出在 Dart SDK 的 dart:io 库中。所以,要定位问题,先下载SDK代码,编译并按照:

  • 下载代码github.com/dart-lang/s…
  • 编译说明github.com/dart-lang/s…
  • 编译调试版本
cd dart-sdk/sdk
./tools/build.py --mode debug --arch x64 create_sdk
复制代码

在这里我也想抱怨一下Dart。 SDK编译完成后,调试时不能中断点位跟踪。等以后有时间我再分析一下原因。这里省略定位过程(稍后会添加HttClient源码分析,敬请关注)。按照代码逻辑,最终找到了崩溃地址如下:

  static Future<RawSecureSocket> secure(RawSocket socket,
      {StreamSubscription<RawSocketEvent> subscription,
      host,
      SecurityContext context,
      bool onBadCertificate(X509Certificate certificate),
      List<String> supportedProtocols}) {
    **//crashed at the following line. socket == null**
    socket.readEventsEnabled = false;
    **//crashed at the following line. socket == null**
    socket.writeEventsEnabled = false;
    ......
  }
复制代码

HttpClient设置了代理,这里socket == null;但为什么套接字为零是未知的;

STEP 4 根本原因定位:

问题已经找到,但根本原因还不清楚:为什么socket为空,正常的代理进程应该是什么。这就是为什么我考虑使用合适的场景日志作为参考:这台机器设置了两个代理服务器,一个是tinyproxy,另一个是lantern。根据 HTTP 协议,客户端首先向代理服务器发送 Connect 请求:

CONNECT www.google.com:443 HTTP/1.1
user-agent: Dart2.5(dart:io)
接受-编码:gzip
内容长度:0
主机:www.google.com:443

tinyproxy答案:程序正常工作

HTTP/1.0 200连接已建立
代理-代理:tinyproxy/1.8。 4

lantern 回复:

HTTP/1.1 200 OK
日期:2019 年 8 月 14 日星期三下午 4:13:22 CST
保持活动:超时=58
内容长度: 0

崩溃!
所以当我跟踪HttpResponse解析过程时,我发现http_parser.dart、_HttpParser._onData中对Http响应的处理存在差异。 _HttpParser收到Lantern的响应后,由于“Content-Length: 0”关闭了socket,导致上面的socket == null。 Tinyproxy走另一个分支并且socket被保留,所以没有问题。

 http_parser.dart

bool _headersEnd() {
......
if (_transferLength == 0 ||
(_messageType == _MessageType.RESPONSE && _noMessageBody)) {
_reset();
var tmp = _incoming;
 *****socket will closed here as "Content-Length: 0"
_closeIncoming();
_controller.add(tmp);
return false;
} else if (_chunked) {
_state = _State.CHUNK_SIZE;
_remainingContent = 0;
} else if (_transferLength > 0) {
_remainingContent = _transferLength;
_state = _State.BODY;
} else {
*****tinyproxy will go to this branch. not closing socket
// Neither chunked nor content length. End of body
// indicated by close.
_state = _State.BODY;
}
复制代码

这就是为什么改变计划非常简单。添加 _keepAlive 标志。如果Http响应中有Keep-Alive字段,则采取tinyproxy分支,并且不要关闭套接字。

void _doParse() {
...
if (headerField == "keep-alive") {
_keepAlive = true;
}
...

if ((_transferLength == 0 && !_keepAlive) // 不走这个分支,走else
...
复制代码

本地测试,问题解决。

最后

是一篇长文,以流水账的形式记录了Dart SDK的排查过程。回过头来看,pub 使用了代理,但是 dart:io 在使用代理时存在兼容性问题。目前此问题已在 issues/37808 中得到解决,因为它涉及解析 HttpResponse 字段,并且需要对 HTTP 协议进行详细分析才能更改。因此,pub bug问题只能在最终修复发布到数据库后通过更新SDK来解决。但对于我的本地用户来说,使用本地SDK构建的pub已经可以正常工作了。
写下学习DART的一些心得:

  • Dart的优点:现代编程语言,拥有最流行的语言特性(async-await、stream、future),单线程模型减少了编码问题,提高了gc-效率 。最重要的是,基于Dart的flutter框架真正支持跨平台,Android、iOS和Fuchsia一统天下,未来无限光明。
  • Dart的缺陷:太年轻,不够成熟。例如,本文中的这个问题可以归类为兼容性问题。目前,DART 还很年轻,仍然可能会出现许多类似的兼容性问题。我用JAVA的APACHE HttpClient写了一个测试程序,没有出现这样的问题。 JAVA的环境成熟度比Dart高很多
  • DART也需要做出更好的可用性调整。比如目前遇到的StackTrace不太友好以及无法一步追踪SDK中的文件等等,这对于开发来说是非常困难的。人们制造了一些障碍。
  • 一个小建议:对于 dart:io lib,仍然有大量的代码是用 Future.then 编写的。如果异步重写的话会更好理解。

简而言之,DART 存在风险。如果有陷阱的话,就得小心了。道路可能会曲折,但未来是光明的。

作者:TonyBuilder
来源:掘金
版权归作者所有。商业转载请联系作者获得许可。非商业转载请注明出处。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门