olragon

Vì sao Bun.spawn chẳng thấy biến môi trường mình vừa gán?

🌱 mầmgieo #bun#terminal#bai-hoc

Viết cho Ai viết script hay bộ chạy việc theo lịch bằng Bun, nạp khoá từ file .env lúc chạy rồi gọi tiến trình con.

Mình từng tin rằng gán một biến vô process.env là xong, mọi thứ chạy sau đó đều thấy nó. Hoá ra trong Bun thì chẳng phải vậy.

Mọi con số dưới đây mình đo sáng 1/11 trên Bun 1.4.4, một lần trên máy Mac, một lần trên máy ảo Linux chạy chính mảnh vườn này. Script đo chưa tới 30 dòng, mình để ở cuối bài.

English: Why doesn't Bun.spawn see the env var I just set?.

Cái báo động đến đúng giờ, mỗi sáng

Có một trang web mình đang làm, nó gửi thư báo tin mới cho người đã đăng ký. Việc gửi thư nằm trong một bộ chạy việc theo lịch. Bộ đó khởi động, nạp khoá gửi thư từ file cấu hình vô process.env, rồi mở từng việc thành một tiến trình con bằng Bun.spawn.

Sáng nào cũng vậy, một thư báo động đến hộp thư: "mọi lượt gửi đều hỏng". Nghe thì vô lý. Khoá có trên máy chủ. Hàm nạp khoá đọc nó ra, mình in thử thì thấy đủ.

Cái báo động chạy ngay trong tiến trình cha, nên nó thấy khoá và gửi thư ngon lành. Còn việc gửi thư báo tin mới thì chạy trong tiến trình con, và tiến trình con chẳng có khoá nào hết. Vậy là thứ duy nhất còn gửi nổi thư lại là cái thư than rằng chẳng ai gửi nổi thư. Mình đọc lại chỗ đó mấy lần mới tin.

Bun.spawn lấy môi trường từ lúc khởi động

Thủ phạm nằm ở một dòng mình chẳng ngờ tới. Gọi Bun.spawn mà chẳng truyền env, thì tiến trình con nhận bộ biến môi trường lúc script khởi động, chẳng phải process.env lúc gọi.

Mình đo bảy cách gọi tiến trình con, và chúng chẳng đồng ý với nhau. Script gán OLR_SPAWN_PROBE=set-at-runtime vô process.env, rồi hỏi tiến trình con xem nó thấy gì. Kết quả giống hệt nhau trên macOS và trên Linux:

cách gọitiến trình con thấy biến vừa gán?
Bun.spawn(cmd)không
Bun.spawnSync(cmd)không
Bun.spawn(cmd, { env: process.env })có
Bun.spawn(cmd, { env: { ...process.env } })có
` Bun.$… `có
spawnSync của node:child_processcó
execSync của node:child_processcó

Chỗ làm mình bất ngờ nhất là hai dòng Bun.$ và node:child_process. Cùng một runtime, cùng một biến, mà ba kiểu gọi thấy, hai kiểu chẳng thấy. Ai quen Node sẽ tưởng Bun.spawn cũng giống child_process, và đó đúng là cái bẫy.

Đổi hay xoá một biến cũng hụt luôn

Mình thử thêm hai trường hợp, cho chắc là nó chẳng chỉ hụt biến mới.

  • Khởi động với OLR_PRE=boot, rồi gán lại process.env.OLR_PRE = "changed". Tiến trình con của Bun.spawnSync in ra boot.
  • Xoá HOME bằng delete process.env.HOME. Tiến trình con vẫn thấy HOME cũ, y nguyên.

Vậy là mọi thay đổi trên process.env sau lúc khởi động đều chẳng tới tay tiến trình con, nếu bạn chẳng truyền env. Thêm, đổi, xoá, cả ba. Kiểu lỗi này khó thấy vì nó im lặng: chẳng có lỗi nào bắn ra, chỉ có một biến trống ở bên kia.

Chữa thì một dòng, phòng thì cần thêm

Cái chữa ngắn lắm. Chỗ nào gọi Bun.spawn hay Bun.spawnSync mà muốn tiến trình con thấy môi trường hiện tại, thì truyền nó vô cho rõ:

Bun.spawn(cmd, { env: process.env });

Bộ chạy việc kia giờ truyền env ở chỗ mở mỗi việc, và thư báo tin mới đi lại từ sáng hôm sau.

Nhưng mình chẳng muốn chỉ dựa vô trí nhớ. Nên bộ chạy việc có thêm một việc kiểm tra nhỏ, chạy đúng kiểu một tiến trình con, rồi báo xem nó có thấy khoá gửi thư không. Lý do là cái báo động cũ chạy trong tiến trình cha, nên nó nhìn thấy một bức tranh khác hẳn bức tranh của việc nó canh. Một bài kiểm tra đáng tin thì phải đứng đúng chỗ đứng của thứ nó kiểm tra.

Mình cũng rà lại mấy script của chính mảnh vườn này, xem có chỗ nào dính. Sáu file có gọi Bun.spawn, nhưng chẳng file nào gán vô process.env lúc chạy, nên chưa có chỗ nào hụt. Chưa hụt chẳng có nghĩa là chẳng bao giờ hụt, nên mình giữ script đo lại để chạy mỗi lần nâng Bun.

Bạn tự thử trên máy bạn

Dán đoạn này vô một file probe.ts rồi chạy bun probe.ts:

process.env.PROBE = "set-at-runtime";
const sh = ["sh", "-c", 'printf %s "${PROBE:-<missing>}"'];
console.log("spawnSync:", Bun.spawnSync(sh).stdout.toString());
console.log("with env: ", Bun.spawnSync(sh, { env: process.env }).stdout.toString());

Nếu dòng đầu in <missing> mà dòng sau in set-at-runtime, máy bạn cũng có cái bẫy đó. Bản đầy đủ đo cả bảy kiểu gọi nằm trong kho mã mảnh vườn, scripts/bun/spawn-env-check.ts.

Chuyện này hơi giống lần pkill -f cat tắt sạch app: một lệnh trông vô hại, làm đúng y lời người ta viết cho nó, chỉ là lời đó khác điều mình nghĩ.

Chưa biết gì

  • Mình chưa biết đây là Bun cố ý thiết kế vậy, hay là một lỗi sẽ sửa. Mình chưa tìm trong danh sách lỗi của Bun.
  • Mình chỉ đo trên Bun 1.4.4. Bản cũ hơn hay bản canary có khác chẳng, mình chưa thử.
  • Mình chưa hiểu vì sao Bun.$ thấy còn Bun.spawn thì chẳng thấy, dù cả hai đều của Bun.

Nếu bạn biết lý do, hay từng dính cái bẫy này ở chỗ khác, viết cho mình một lá thư bên dưới nha.

Gửi Ba rồng một lá thư

Đọc xong muốn nói gì thì viết cho mình nha. Thư chỉ tới tay mình, chẳng tự hiện lên trang. Lá nào hay mà bạn cho phép, mình chép lại dưới bài, không kèm tên bạn.

Mảnh vườn giữ gì từ lá thư này

Từ terminal cũng gửi được: curl -d 'msg=…' olragon.com/garden/bun-spawn-env-vi/letter