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) และเส้นทางทั้งหมดใน กายวิภาคของการปล่อยซอฟต์แวร์ให้สมบูรณ์แบบ

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

วันที่แอปของเรายิง DDoS ใส่ตัวเอง
Steven
Steven2 นาทีที่อ่าน

วันที่แอปของเรายิง DDoS ใส่ตัวเอง

กองงานอัปโหลดที่ค้างอยู่ ถูกปล่อยออกมาทีเดียวทั้งหมดตอนเปิดแอป กลายเป็นการเปลี่ยนทุก client ของ GeekBye ให้เป็นการโจมตีแบบ denial-of-service เล็กๆ ใส่เซิร์ฟเวอร์ของเราเอง วิธีแก้ — และบันได connection-liveness ที่มันบังคับให้เราต้องสร้าง — เป็นหนึ่งในสิ่งที่มีประโยชน์ที่สุดที่ v2 สอนเรา

ความน่าเชื่อถือ
เครือข่าย
วิศวกรรม
เวอร์ชัน 2 จริง ๆ แล้วต้องแลกด้วยอะไร: 206 คอมมิตแห่งสถานะที่ซื่อสัตย์
Steven
Steven2 นาทีที่อ่าน

เวอร์ชัน 2 จริง ๆ แล้วต้องแลกด้วยอะไร: 206 คอมมิตแห่งสถานะที่ซื่อสัตย์

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

ความน่าเชื่อถือ
วิศวกรรม
การปล่อยเวอร์ชัน
กายวิภาคของการส่งซอฟต์แวร์ให้สมบูรณ์แบบ: เมื่อ Code Review จับสิ่งที่ Test จับไม่ได้
Steven
Steven2 นาทีที่อ่าน

กายวิภาคของการส่งซอฟต์แวร์ให้สมบูรณ์แบบ: เมื่อ Code Review จับสิ่งที่ Test จับไม่ได้

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

วิศวกรรม
กระบวนการ
การรีวิวโค้ด