Steven
Steven2 นาทีที่อ่าน

การตรวจสอบเพื่อความปลอดภัยที่ทำให้แอปของเราปิดไม่ได้

ระบบอัปเดตอัตโนมัติเป็นฟีเจอร์ที่ยากที่สุดที่เราเคยปล่อยออกมา — หกรุ่นในสี่วันเพื่อหยุดไม่ให้มันทำแอปพัง บั๊กที่แย่ที่สุดคือบั๊กที่เราสร้างขึ้นเองตอนที่พยายามจะระมัดระวัง: การตรวจสอบ "เพื่อความปลอดภัย" ที่กินเวลา 500 มิลลิวินาที ซึ่งเปลี่ยนการอัปเดตที่ล้มเหลวให้กลายเป็นโปรเซสที่คุณปิดไม่ได้เลยจริง ๆ

วิศวกรรม
Electron
ความน่าเชื่อถือ
การเปิดตัว GeekBye
การตรวจสอบเพื่อความปลอดภัยที่ทำให้แอปของเราปิดไม่ได้

นักพัฒนาแอปเดสก์ท็อปทุกคนประเมินระบบอัปเดตอัตโนมัติต่ำเกินไปอย่างน้อยหนึ่งครั้ง มันดูเหมือนปัญหาที่แก้ได้แล้ว — ไลบรารีดาวน์โหลดเวอร์ชันใหม่มาแล้วรีสตาร์ตแอปของคุณ จากนั้นคุณก็ปล่อยมันออกไป และคุณก็ได้เรียนรู้ว่า "รีสตาร์ตแอปของคุณ" เป็นหนึ่งในสิ่งที่อันตรายที่สุดที่โปรแกรมจะถูกสั่งให้ทำ เพราะมันเกิดขึ้นในช่วงเวลาที่แอปของคุณกำลังรื้อถอนตัวเองและมีขอบเขตความผิดพลาดน้อยที่สุด

ระบบอัปเดตอัตโนมัติของ GeekBye ใช้เวลา หกรุ่นในสี่วัน — v1.5.14 ถึง v1.5.19 — กว่าจะเสถียร นี่คือเรื่องราวของบั๊กที่แย่ที่สุดในช่วงนั้น ซึ่งเราก่อขึ้นเองจากการพยายามจะระมัดระวัง

หกรุ่น กับรุ่นเดียวที่สำคัญ

จุดเริ่มต้นนั้นธรรมดามาก v1.5.14 แก้บั๊กระดับพิมพ์ผิดที่น่าอาย: ฟีดอัปเดตชี้ไปที่ชื่อ repo บน GitHub ที่ไม่มีอยู่จริง ดังนั้นตัวอัปเดตจึงกำลังตรวจสอบหน้า 404 v1.5.15 เพิ่มปุ่ม "Check for Updates" แบบทำเองและข้อความ error ที่ใช้ได้จริง จากนั้นบั๊ก quitAndInstall ก็เริ่มต้นขึ้น และรุ่นต่าง ๆ ก็มาอย่างรวดเร็ว — เพราะเมื่อกลไกอัปเดตของคุณพัง คุณจะไม่สามารถปล่อยตัวแก้ไข ผ่าน กลไกอัปเดตนั้นได้ ทุกการวนซ้ำคือการเสี่ยงติดตั้งใหม่ด้วยมือ

รุ่นที่สำคัญคือ v1.5.18 เนื้อหาทั้งหมดของมันคือคอมมิตเดียวที่มีชื่อซึ่งยังทำให้ผมหน้าเบ้อยู่: restore original quitAndInstall behavior to prevent unkillable app.

"การระมัดระวัง" ทำให้แอปพังได้อย่างไร

นี่คือฉากตั้งต้น เมื่อดาวน์โหลดอัปเดตเสร็จ quitAndInstall ของ Electron ควรจะปิดแอปแล้วสลับเอาเวอร์ชันใหม่เข้ามา ในรุ่นก่อนหน้านี้ มีคนหนึ่ง — อย่างมีเหตุผล — กังวลว่าการปิดแบบ ไม่มีเงื่อนไข นั้นเสี่ยง จะเป็นอย่างไรถ้าการติดตั้งเกิด error ขึ้น? มันจะปลอดภัยกว่าไหมถ้าปิดก็ต่อเมื่อทุกอย่างดูแข็งแรงดี?

โค้ดจึงงอกการ์ดที่ดูสมเหตุสมผลออกมา:

autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
  if (this.updateDownloaded)
    app.quit() // only quit if the update is still "good"
  else console.log('Error detected — keeping app open')
}, 500)

ตรรกะคือ: สั่งติดตั้ง รอครึ่งวินาที แล้วบังคับ app.quit() ขั้นสุดท้ายก็ต่อเมื่อแฟล็ก updateDownloaded ยังเป็น true อยู่ — มิฉะนั้นก็เปิดแอปทิ้งไว้เพื่อไม่ให้ผู้ใช้ค้างคา

กับดักอยู่ห่างไปเพียงบรรทัดเดียว ในตัวจัดการ error ตัวจัดการนั้นตั้งค่า this.updateDownloaded = false ลองนึกภาพการติดตั้งที่ล้มเหลว: อีเวนต์ error ทำงานและล้างแฟล็ก แต่ quitAndInstall ได้ เริ่ม การรื้อถอนไปแล้ว — มันได้ปิดหน้าต่างและลบตัวฟังคำสั่งปิดของแอปออกไป จากนั้นตัวจับเวลา 500ms ก็ตื่นขึ้น ตรวจสอบแฟล็กที่ตอนนี้เป็น false ตัดสินใจว่า "ตรวจพบ error ให้เปิดแอปทิ้งไว้" และ ข้าม app.quit()

ตอนนี้คุณมีสภาพที่แย่ที่สุดเท่าที่จะเป็นไปได้ โดยเฉพาะบน macOS macOS ไม่ปิดแอปเพียงเพราะหน้าต่างสุดท้ายของมันปิดลง — นั่นคือพฤติกรรม window-all-closed ที่แอป Mac ทุกตัวพึ่งพา ดังนั้นโปรเซสจึงยังมีชีวิตอยู่ แต่มัน ไม่มีหน้าต่าง ไม่มีเส้นทางแถบเมนู และตัวฟังคำสั่งปิดของมันถูกรื้อออกไปแล้ว ไม่มีอะไรให้คลิก Cmd-Q ไม่มีอะไรให้สื่อสารด้วย ทางออกเดียวคือ Force Quit จาก Activity Monitor การตรวจสอบ "เพื่อความปลอดภัย" ได้เปลี่ยนการอัปเดตที่ล้มเหลว — ความน่ารำคาญที่กู้คืนได้ — ให้กลายเป็นซอมบี้ที่คุณฆ่าไม่ได้

วิธีแก้: การรื้อถอนต้องไม่มีเงื่อนไข

การแก้ไขใน v1.5.18 นั้นน่าเบื่อจนเกือบก้าวร้าว ซึ่งนั่นแหละคือประเด็น มันลบความฉลาดออกไป:

  1. ลบตัวฟัง window-all-closed และ before-quit ที่อาจเข้ามาแทรกแซง
  2. ทำลาย ทุกหน้าต่าง — window.destroy() ไม่ใช่ window.close() การ close อาจถูกตัวจัดการยับยั้งได้ ส่วน destroy ทำไม่ได้ เมื่อคุณตั้งใจจะปิดตัวลง คุณไม่ถามอย่างสุภาพ
  3. เรียก quitAndInstall
  4. เรียก app.quit() แบบไม่มีเงื่อนไข

ไม่มีแฟล็ก ไม่มีตัวจับเวลา ไม่มี "เปิดทิ้งไว้เผื่อไว้" เพราะความจริงเกี่ยวกับเส้นทางการปิดตัวลงก็คือการปิดที่ทำได้ครึ่ง ๆ กลาง ๆ นั้นแย่กว่าผลลัพธ์แบบใดแบบหนึ่ง การปิดจนสุดก็ดี การเปิดทิ้งไว้ทั้งหมดก็ดี สภาพเดียวที่คุณต้องไม่ไปถึงเด็ดขาดคือ รื้อถอนไปแล้วแต่ยังทำงานอยู่ — และนั่นคือสภาพที่การปิดแบบมีเงื่อนไขจะทิ้งคุณไว้ได้พอดี

อีกสองบทเรียนที่สัปดาห์เดียวกันสอน

บั๊กปิดไม่ได้คือพาดหัว แต่การวิ่งวุ่นหกรุ่นนั้นทำให้อีกสองนิสัยแข็งแกร่งขึ้นซึ่งคุ้มค่าที่จะลอกไป

กรอง telemetry ของ crash ด้วยลายเซ็นที่ตรงเป๊ะ ไม่ใช่คีย์เวิร์ดกว้าง ๆ ระหว่างการวิ่งวุ่น เราพบว่าระบบรายงาน error ของเราถูกตั้งค่าให้ทิ้งอะไรก็ตามที่มีคำอย่าง permission, token, หรือ microphone — ความพยายามที่จะตัด noise ซึ่งกำลัง กลืน crash จริง ที่บังเอิญกล่าวถึงคำเหล่านั้นอย่างเงียบ ๆ เราฉีกตัวกรองแบบเหมารวมทิ้งไปและแทนที่ด้วยสตริงการปฏิเสธที่ตรงเป๊ะ (ข้อความเฉพาะที่ macOS ส่งออกมาเมื่อ permission ถูกปฏิเสธ) และรหัสเครือข่ายชั่วคราวเฉพาะอย่าง ERR_NETWORK_CHANGED การลด noise กับการซ่อนบั๊กคือปุ่มเดียวกันที่หมุนไปคนละทาง ถ้าคุณกรองตามความรู้สึก คุณจะกรองสิ่งที่คุณจำเป็นต้องเห็นออกไป

ทุกเส้นทางอัตโนมัติต้องมีช่องทางหนีฉุกเฉินแบบทำเอง การอัปเดตอัตโนมัติเป็นความพยายามให้ดีที่สุดโดยธรรมชาติ — เครือข่ายกระตุก การติดตั้งล้มเหลว ดังนั้นแต่ละโหมดความล้มเหลวจึงได้รับตัวสำรองที่มนุษย์ทำได้: ปุ่ม "Check for Updates" แบบทำเอง การลองใหม่แบบ exponential-backoff ตัวจับเวลาตรวจสอบซ้ำ และ — สงวนไว้เฉพาะสำหรับกรณีที่ความพยายาม ทำเอง ของผู้ใช้ล้มเหลว — ข้อความภาษาชาวบ้านว่า "ลบแอปแล้วติดตั้งใหม่จากเว็บไซต์" เส้นทางอัตโนมัติคือความสะดวก เส้นทางแบบทำเองคือหลักประกัน

บทสรุป

  1. การ์ดรอบ ๆ การกระทำที่ย้อนกลับไม่ได้นั้นอันตรายกว่าตัวการกระทำเอง การปิดแบบมีเงื่อนไขพยายามป้องกันไม่ให้การอัปเดตที่แย่ปิดแอป แต่กลับสร้างสภาพที่แย่กว่าทั้งการปิดหรือไม่ปิด เส้นทางการปิดตัวลงและการติดตั้งควรไม่มีเงื่อนไขและ idempotent — ไม่ควรถูกกั้นด้วยแฟล็กที่เปลี่ยนแปลงได้ซึ่งตัวจัดการอื่นสามารถพลิกออกจากใต้เท้าคุณได้
  2. บน macOS "ไม่มีหน้าต่าง" ไม่ได้แปลว่า "ไม่มีแอป" ตรรกะการรื้อถอนใด ๆ ต้องคำนึงถึงแพลตฟอร์มที่โปรเซสไร้หน้าต่างยังคงทำงานต่อไป ทดสอบเส้นทางความล้มเหลว บนระบบปฏิบัติการจริง ไม่ใช่แค่เส้นทางที่ราบรื่น
  3. ฟีเจอร์ที่คุณปล่อยผ่านระบบอัปเดตนั้นทดสอบผ่านระบบอัปเดตไม่ได้ ความไม่สมมาตรนั้นคือเหตุผลว่าทำไมการอัปเดตอัตโนมัติจึงสมควรได้รับโค้ดที่หวาดระแวง ไม่มีเงื่อนไข และตรวจสอบด้วยมืออย่างหนักหน่วง คุณจะได้แก้มันด้วยวิธีง่าย ๆ ก็ หลังจาก ที่มันทำงานได้แล้วเท่านั้น

นี่คือบทแรกสุดของงานด้านความน่าเชื่อถือที่สุดท้ายกลายเป็น GeekBye v2 สำหรับที่ที่เส้นทางนั้นพาไป ดูได้ที่ เวอร์ชัน 2 จริง ๆ ต้องใช้อะไรบ้าง (v2.0.0) และเส้นทางทั้งหมดใน กายวิภาคของการปล่อยซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

การหลุดของการเชื่อมต่อครั้งเดียวไม่ควรทำให้ทั้งแอปพัง — แต่ของเราพัง
Steven
Steven2 นาทีที่อ่าน

การหลุดของการเชื่อมต่อครั้งเดียวไม่ควรทำให้ทั้งแอปพัง — แต่ของเราพัง

เมื่อแบ็กเอนด์ของเราหลุดออฟไลน์กลางการประชุม มันไม่ได้แค่หยุดการถอดเสียง — มันทำให้ทั้งแอปพัง สาเหตุคือเหตุการณ์ที่ไม่ถูกจัดการเพียงตัวเดียว และการแก้ไขมีแค่สิบบรรทัด นี่คือชุดรีลีสที่ทำให้ GeekBye ยังคงล็อกอินอยู่และยังคงเชื่อมต่ออยู่ ผ่านสิ่งที่เคยฆ่ามันมาก่อน

วิศวกรรม
ความน่าเชื่อถือ
Electron
ความเงียบคือสิ่งที่รับน้ำหนัก
Steven
Steven3 นาทีที่อ่าน

ความเงียบคือสิ่งที่รับน้ำหนัก

สอง release สุดท้ายของ GeekBye v1 ว่าด้วยความจริงอันน่าอึดอัดข้อเดียวกัน: การถอดเสียงแบบเรียลไทม์บนเครือข่ายจริงไม่ได้ไร้การสูญเสีย และการเดินหมากที่ซื่อสัตย์คือเลิกแกล้งทำเป็นว่ามันไร้การสูญเสีย v1.8.20 เก็บสำเนาของ audio chunk ทุกก้อนไว้บนดิสก์ก่อนจะทิ้งมันระหว่างการเชื่อมต่อใหม่ และเริ่มทำเครื่องหมายช่องว่างใน transcript ออกมาดัง ๆ v1.9.0 เลิกส่งความเงียบเพื่อประหยัด bandwidth — แล้วก็ค้นพบว่าความเงียบนั่นแหละคือสัญญาณเป๊ะ ๆ ที่เครื่องถอดเสียงใช้รู้ว่าประโยคจบแล้ว สอง release ว่าด้วยราคาของการโยนของทิ้ง

วิศวกรรม
Audio
ความน่าเชื่อถือ
พิมพ์การประชุมเป็น PDF โดยไม่มี PDF library
Steven
Steven5 นาทีที่อ่าน

พิมพ์การประชุมเป็น PDF โดยไม่มี PDF library

GeekBye export การประชุมเป็น PDF ทั้งที่ในโค้ดไม่มี PDF library อยู่ที่ไหนเลย มัน render HTML ในหน้าต่าง browser ที่มองไม่เห็นแล้วพิมพ์มันออกมา ทางเลือกนั้นแหละคือทั้งเรื่อง: มันทำให้ฟีเจอร์นี้สร้างง่าย และมอบทุกความล้มเหลวที่ browser จริง ๆ มีให้มัน — แฟลชขาว, ลิมิตความยาว URL, และ page break ที่ตัด screenshot ขาดครึ่ง ทางแก้ของ bug ที่น่าเกลียดที่สุดคือ CSS หนึ่งบรรทัด

วิศวกรรม
Electron
Desktop