לדלג לתוכן

מסירה והרחבה עצמאית

נניח שמישהו אחר קיבל את המערכת שלכם. אין לו את התיקיות הזמניות, הPATH וההרגלים שלכם. הוא צריך לבנות גרסה, להתקין אותה, לזהות כשל ולהוסיף סוג עבודה חדש. זהו המבחן האחרון של הקורס, כי הוא חושף ידע שנשאר רק בראש של מי שכתב את הקוד.

נמסור שלושה דברים נפרדים: מקור לבנייה, חבילת התקנה וחבילת אבחון. לכל אחד יש קהל ושימוש אחרים. המקור כולל קבצי build וגרסאות תלויות; החבילה כוללת רק את מה שנדרש לריצה; האבחון כולל symbols ומידע שיכול לעזור לחקור בלי למסור נתונים שאינם נחוצים.

בודקים שהתיקייה היא באמת החבילה

בשיעור 10.10 יצרנו manifest של קבצים. עכשיו נבדוק אותו מהצד המקבל. הסקריפט הבא מניח שהmanifest הגיע ממקור שכבר אימתנו ושעץ הקבצים אינו משתנה בזמן הבדיקה. הוא בודק גם קבצים נוספים, כי DLL מפתיע ליד executable יכול לשנות את התוצאה בלי לשנות את הקבצים המקוריים.

param(
    [Parameter(Mandatory=$true)][string]$ReleaseDirectory,
    [Parameter(Mandatory=$true)][string]$ManifestFile
)
$ErrorActionPreference = 'Stop'
$rootItem = Get-Item -LiteralPath $ReleaseDirectory -Force
if (-not $rootItem.PSIsContainer -or
    ($rootItem.Attributes -band [IO.FileAttributes]::ReparsePoint)) { throw 'Invalid root' }
$rootPath = $rootItem.FullName
$prefix = $rootPath.TrimEnd('\') + '\'
$manifest = Get-Content -LiteralPath $ManifestFile -Raw | ConvertFrom-Json
if ($manifest.format -ne 1 -or [string]::IsNullOrWhiteSpace($manifest.build_id)) {
    throw 'Invalid manifest header'
}
$expected = [Collections.Generic.Dictionary[string,object]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($record in @($manifest.files)) {
    $relative = [string]$record.path
    $parts = $relative -split '[/\\]'
    if ([string]::IsNullOrWhiteSpace($relative) -or [IO.Path]::IsPathRooted($relative) -or
        $relative.Contains(':') -or @($parts | Where-Object { $_ -in '', '.', '..' }).Count -gt 0) {
        throw 'Invalid relative path'
    }
    if ([string]$record.sha256 -notmatch '^[0-9a-fA-F]{64}$') { throw 'Invalid hash' }
    if ($null -eq $record.bytes -or [long]$record.bytes -lt 0) { throw 'Invalid length' }
    $normalized = $relative.Replace('/', '\')
    if ($expected.ContainsKey($normalized)) { throw 'Duplicate path' }
    $expected.Add($normalized, $record)
}
$pending = [Collections.Generic.Stack[string]]::new()
$pending.Push($rootPath)
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
while ($pending.Count -gt 0) {
    foreach ($entry in Get-ChildItem -LiteralPath $pending.Pop() -Force) {
        if ($entry.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Reparse point' }
        if ($entry.PSIsContainer) { $pending.Push($entry.FullName); continue }
        if (-not $entry.FullName.StartsWith($prefix,[StringComparison]::OrdinalIgnoreCase)) {
            throw 'Path outside release'
        }
        $relative = $entry.FullName.Substring($prefix.Length)
        if (-not $expected.ContainsKey($relative)) { throw ('Unexpected file: ' + $relative) }
        $record = $expected[$relative]
        if ($entry.Length -ne [long]$record.bytes) { throw ('Length mismatch: ' + $relative) }
        $actualHash = (Get-FileHash -LiteralPath $entry.FullName -Algorithm SHA256).Hash
        if ($actualHash -ine [string]$record.sha256) { throw ('Hash mismatch: ' + $relative) }
        if (-not $seen.Add($relative)) { throw 'Duplicate observation' }
    }
}
if ($seen.Count -ne $expected.Count) { throw 'Missing file' }
[pscustomobject]@{ build = $manifest.build_id; verifiedFiles = $seen.Count }

הבדיקה אינה מחליפה חתימה, SBOM או בדיקת הרשאות התקנה. היא עונה על שאלה מצומצמת: האם הקבצים שראינו תואמים לרשימה המהימנה שקיבלנו. כאשר עץ staging יכול להשתנות בזמן הבדיקה, יש גם בעיית snapshot שצריך לפתור מחוץ לסקריפט הזה.

בונים שוב בלי זיכרון מקומי

הבנייה מתחילה מעותק נקי של מקור ומרשימת tools נעולה. מתעדים SDK, WDK, runtime, architecture וflags. אם דרייבר הוא חלק מהמעבדה, מפרידים בין חתימת בדיקה בסביבת VM לבין מסלול הפצה אמיתי.

המחשב שבודק התקנה אינו מכונת build עם כל התלויות שכבר מותקנות. בודקים התחלה ממשתמש רגיל, הפעלת שירות לפי החוזה, קריאת מצב, עצירה ושדרוג. כל צעד שדורש הרשאה מיוחדת מסומן במקום שבו היא נדרשת, ולא הופך את כל המוצר לתהליך בעל הרשאות עודפות.

הדרישה החדשה בודקת את התכנון הישן

נוסיף סוג עבודה שמחזיר גם count וגם checksum. המימוש צריך לעדכן protocol version או capability, להרחיב את המנוע, להציג תוצאה ולהישאר תואם לclient ישן לפי החוזה שהוגדר. ההרחבה אינה מסתיימת כשהכפתור החדש עובד במחשב המפתח.

נבדוק בקשה חדשה מול server ישן, בקשה ישנה מול server חדש ותוצאה חדשה מול reader ישן. בכל צירוף כותבים אם הוא נתמך, נדחה או דורש upgrade. דחייה ברורה עדיפה על פענוח שקט של שדה במיקום שגוי.

הסוג החדש מקבל גם תקציב זיכרון וזמן, הרשאות ומסלול cancellation. הוא נכנס לjournal כך שrecovery לא תלוי ביכולת של הגרסה הישנה להבין אותו. כאן נבחן אם הבחירות שעשינו בפרקים 8, 12 ו14 באמת התחברו.

מוסרים חקירה שאדם אחר יכול לשחזר

מדריך האבחון מתחיל מסימפטום ומוביל לאיסוף מינימלי מתאים. בקריסה הוא מסביר איזה dump ואיזה symbols צריך. בתקיעה הוא מסביר אילו threads וwaits לחפש. באיטיות הוא דורש workload, חלון זמן ומזהי בקשות, ולא צילום יחיד של CPU.

המסמך אינו צריך להכיל כל פקודה שנלמדה בקורס. הוא צריך לאפשר לאדם אחר לבחור את הפעולה הבאה בלי לנחש את המבנה הפנימי. שומרים גם דרך להסיר את ההתקנה ולחזור לגרסה עובדת לפי חוזה הנתונים.

בסיום נבקש מהבודק לבצע שינוי שלא תורגל בדיוק באותה צורה. אם הוא יודע למצוא את החוזה, לבנות ניסוי, לשנות קוד ולהוכיח שלא שבר בעלות או recovery, הוא כבר עובד עם המערכת מתוך הבנה ולא מתוך זיכרון של דוגמה.