当我开发Werkzeug (以及后来的Flask )时,对我来说,开发人员体验中最重要的部分是实现快速、自动重新加载。 Werkzeug(以及 Flask),这是通过始终使用两个过程来实现的。父进程保留服务器侦听的套接字的文件描述符,子进程获取该文件描述符。该子进程在检测到更改时重新启动。这可以确保无论发生什么情况,都不会有浏览器报告连接错误的窗口。最坏的情况是,浏览器将挂起,直到进程完成重新加载,之后页面加载成功。如果内部进程在重新启动期间无法启动,您会收到一条错误消息。
几年前,我想在使用 Rust 代码时获得相同的体验,这就是我编写systemfd和Listenfd 的原因。然而,我意识到我从来没有真正在这里写过它们是如何工作的,令人失望的是,我认为这些板条箱以及 Rust 中良好的自动重新加载体验在很大程度上是未知的。
观察变化
首先,需要监视文件系统的更改。虽然理论上我可以自己完成此操作,但已经有一个工具可以做到这一点。
当时有货运手表。今天,人们可能会将它与更通用的watchexec一起使用。要么监视工作区的更改,然后执行命令。例如,您可以告诉它重新启动您的程序。其中之一将起作用:
watchexec -r -- 货物运行 货物手表-x运行
您将需要一个类似的工具来完成观看部分。在这一点上,我推荐更通用的watchexec ,您可以在homebrew 和其他地方找到它。
传递套接字
但是插座呢?我选择的这个问题的解决方案来自systemd 。 Systemd 有一个“协议”,可以标准化通过环境变量将文件描述符从一个进程传递到另一个进程。用 systemd 的话说,这称为“套接字激活”,因为它允许 systemd 仅在有人开始向套接字发出请求时启动程序。这个概念最初是由 Apple 作为 launchd 的一部分引入的。
为了使用 Rust 实现此功能,我创建了两个板条箱:
值得注意的是,systemfd 并非只对 Rust 有用。 systemd 协议也可以用其他语言实现,这意味着如果您有一个用 Go 或 Python 编写的套接字服务器,您也可以使用 systemfd。
所以这就是你如何使用它。
首先你需要将listenfd添加到你的项目中:
货物添加listenfd
然后,修改服务器代码以通过 Listenfd 接受套接字,然后再回退到在通过命令行参数或配置文件提供的端口上侦听自身。下面是在 axum 中使用Listenfd的示例:
使用 axum :: {路由::获取, 路由器}; 使用 东京::网络:: TcpListener ; 异步 fn索引() -> & '静态str { “你好世界!” } #[东京::主要] 异步 fn主() ->结果< (), 盒子<动态 std ::错误::错误>> { 让 应用程序 = 路由器:: new ()。路线( ”/” , 获取(索引)); 让 穆特 监听fd = Listenfd :: ListenFd :: from_env (); 让 听众 = 匹配 听。 take_tcp_listener ( 0 ) ? { 一些(听众) => TcpListener :: from_std (侦听器), 没有任何 => TcpListener :: bind ( "0.0.0.0:3000" )。等待, } ? ; axum ::服务(监听器, 应用程序)。等待? ; 好的(()) }
这里的关键点是从环境中接受套接字 0 作为 TCP 侦听器,并在可用时使用它。如果未提供套接字(例如,在没有 systemd/ systemfd 的情况下启动时),代码将回退到打开固定端口。
把它放在一起
最后,您可以将Cargo watch / watchexec与systemfd一起使用:
systemfd --no-pid -s http::8888 -- watchexec -r -- 货物运行 systemfd --no-pid -s http::8888 -- 货物监视 -x 运行
这就是参数的含义:
- systemfd需要首先它是打开套接字的程序。
- --no-pid是一个标志,防止 PID 被传递。这是listenfd接受套接字所必需的。这是套接字传递协议与 systemd 的背离,否则它不允许端口通过另一个程序(如watchexec )传递。简而言之:当没有传递PID信息时,listenfd将无论如何接受套接字。否则它只会从直接父进程接受它。
- -s http::8888告诉systemfd在端口 8888 上打开一个 TCP 套接字。使用http代替tcp是一个小改进,它将导致 systemfd 在启动时打印出 URL。
- -- watchexec -r使watchexec在当前工作目录发生更改时重新启动进程。
- -- Cargo Run是 watchexec 将启动并重新启动 onm 更改的程序。在 Rust 中,这将首先编译更改,然后运行应用程序。因为我们放入了listenfd ,所以它会首先尝试接受来自systemfd的套接字。
最终结果是您可以编辑代码,它将自动重新编译并重新启动服务器,而不会丢失任何请求。当您运行它并执行更改时,它看起来有点像这样:
$ systemfd --no-pid -s http::5555 -- watchexec -r -- 货物运行 ~> 套接字 http://127.0.0.1:5555/ -> fd #3 [运行:货物运行] 在 0.02 秒内完成 `dev` 配置文件 [未优化 + debuginfo] 目标 运行 `target/debug/axum-test` [运行:货物运行] 编译 axum-test v0.1.0 (/private/tmp/axum-test) 在 0.52 秒内完成 `dev` 配置文件 [未优化 + debuginfo] 目标 运行 `target/debug/axum-test`
为了更容易访问,我建议将其放入Makefile或类似文件中,这样您只需运行make devserver即可以监视模式运行服务器。
要安装systemfd,你可以使用curl来bash:
卷曲-sSfL https://github.com/mitsuhiko/systemfd/releases/latest/download/systemfd-installer.sh |嘘
Windows 怎么样?
那么这在 Windows 上是如何工作的呢?答案是systemfd和listenfd有一个自定义的专有协议,该协议也使套接字传递可以在Windows 上工作。这是一个更复杂的系统,涉及本地 RPC 服务器。然而,该系统也支持 Windows,并且其工作原理的细节与您作为用户基本上无关 — 除非您想为另一种编程语言实现该协议。
潜在的改进
我真的很喜欢使用这种组合,但是需要这么多命令可能会非常令人沮丧,而且命令行工作流程也不是最佳的。理想情况下,此功能可以更好地集成到特定的 Rust 框架(如 axum)中,并通过专用的 Cargo 插件提供。在完美的世界中,人们可以简单地运行Cargo devserver ,一切都会无缝运行。
然而,维持这种综合体验比我所付出的努力要复杂得多。希望有人能够受到启发,进一步增强开发人员体验并实现与 Rust 框架的更深入集成,使每个人都更容易访问和方便。