ขนมปังตัวละครต้นฉบับกำลังเสียบสายเข้ากับหน้าต่างเทอร์มินัลและกล่องโปรเซสบนโต๊ะสีเขียวอ่อน

Bun Terminal: แยกอายุ PTY ออกจาก subprocess ให้ถูก

พี่โชว์Engineering

CLI แบบ interactive ต่างจากคำสั่งที่คืน stdout ธรรมดาครับ โปรแกรมอย่าง REPL, installer หรือ TUI จะตรวจว่าเชื่อมกับ terminal จริงหรือไม่ แล้วจึงเปิดสี การขยับ cursor และ prompt Bun มีตัวเลือก terminal ใน Bun.spawn เพื่อผูก subprocess กับ PTY โดยตรง

WHY: terminal กับ process มีนาฬิกาคนละเรือน

เมื่อเปิด PTY ค่า stdin, stdout และ stderr บน subprocess จะเป็น null เพราะข้อมูลทั้งหมดไหลผ่าน proc.terminal แทน จุดที่พลาดบ่อยคือคิดว่า callback exit ของ terminal เท่ากับ process จบ เอกสาร Bun ระบุว่า callback นั้นบอก lifecycle ของสตรีม PTY ส่วน exit code ของคำสั่งต้องรอ proc.exited

มอง PTY เป็นห้องสนทนาและ subprocess เป็นคนในห้อง คนอาจเดินออกแล้วแต่ห้องยังเปิด หรือห้องถูกปิดขณะที่คนยังทำงาน เราจึงต้องควบคุมสอง lifecycle แยกกัน

HOW: รอ process แล้วปิด terminal อย่างชัดเจน

const chunks = [];
const proc = Bun.spawn(["/bin/sh", "-c", "printf ready"], {
  terminal: {
    cols: 80,
    rows: 24,
    data(_terminal, data) {
      chunks.push(new TextDecoder().decode(data));
    },
  },
});

const exitCode = await proc.exited;
proc.terminal.close();

if (exitCode !== 0 || !chunks.join("").includes("ready")) {
  throw new Error("PTY lifecycle check failed");
}

ในโปรแกรมจริงควรห่อการปิด terminal ด้วย finally และกำหนด timeout ให้ process หากคำสั่งมาจากผู้ใช้ ต้องส่งเป็น array แยก argument ไม่ต่อ shell string เพื่อลด shell injection ถ้าไม่ต้องใช้ expansion หรือ pipeline ก็ไม่ควรเรียก shell กลางเลย

การ resize ควรส่งจาก viewport ไปยัง terminal.resize เมื่อขนาดเปลี่ยน และใช้ callback drain สำหรับงานที่เขียนข้อมูลจำนวนมาก อย่าสมมติว่า escape sequence เหมือนกันทุก byte บน Windows เพราะ ConPTY อาจ coalesce หรือลำดับ control sequence ใหม่ แม้ผลบนหน้าจอเทียบเท่ากัน

เกณฑ์เลือก PTY

ใช้ PTY เมื่อ child ต้องเห็น isTTY, ต้องตอบ prompt หรือวาดหน้าจอ ถ้าเพียงรัน compiler แล้วอ่าน log ให้ใช้ stdout/stderr แบบ pipe ซึ่งทดสอบง่ายกว่าและไม่มี terminal control codes การเปิด PTY โดยไม่จำเป็นเพิ่ม state, platform edge cases และภาระ cleanup

Recap

  • อ่านและเขียนผ่าน proc.terminal เมื่อเปิด terminal option
  • ใช้ proc.exited เป็นแหล่งจริงของ process exit code
  • ปิด PTY ใน finally และมี timeout/kill policy
  • ส่ง command เป็น argument array และหลีกเลี่ยง shell เมื่อไม่จำเป็น
  • ทดสอบผลเชิงความหมาย ไม่ผูก snapshot กับ escape bytes ข้ามแพลตฟอร์ม

ก่อนนำไปครอบ CLI ตัวจริง ให้เริ่มจากคำสั่งสั้น ๆ แล้วทดสอบ exit, timeout, resize และ early close แยกกันครับ