תצורה ומדיניות¶
נניח שהוספנו לשירות הגדרה שקובעת כמה עבודות מותר להריץ במקביל. שמרנו ערך בשם Workers בregistry וקראנו אותו בעלייה. עכשיו המשתמש משנה את הערך בזמן שהשירות רץ, אבל השירות ממשיך עם המספר הישן. זאת לא תקלה בregistry; לא כתבנו שום מנגנון שמחבר שינוי באחסון לשינוי במצב התוכנית.
לפני שנוסיף notification, נגדיר מה המשמעות של ההגדרה. האם שינוי משלוש לשתי עבודות הורג עבודה קיימת, או רק מונע התחלה של עבודה שלישית? האם ערך אפס אומר עצירה או קלט לא חוקי? האם משתמש רגיל יכול להגדיל את המספר למרות מגבלה שהמנהל קבע? האחסון של DWORD לא עונה על אף אחת מהשאלות האלה.
עץ, מפתח וערך¶
הregistry מציג היררכיה של keys שבתוכם values. לכל value יש שם, טיפוס ותוכן. המפתח הוא אובייקט עם זכויות גישה; הערך הוא נתון שאנחנו מפרשים לפי schema. השם Workers לא מכריח את התוכן להיות מספר הגיוני, וגם REG_DWORD לא מכריח אותו להיות בטווח של המוצר.
המיקום HKEY_CURRENT_USER תלוי בהקשר המשתמש. כשאנחנו מריצים שירות בחשבון אחר, "המשתמש הנוכחי" אינו האדם שפתח את חלון ההגדרות. להגדרה ברמת המחשב נבחר מיקום מתאים תחת HKEY_LOCAL_MACHINE וDACL מתאים; להגדרה אישית נבחר אחסון משתמש ונעביר לשירות רק את מה שהפרוטוקול שלו מאפשר.
נתחיל במפתח מעבדה בתוך המשתמש שלנו. פתחו PowerShell והריצו.
$labKey = 'HKCU:\Software\AmitTechCourse\ConfigurationLab'
New-Item -Path $labKey -Force | Out-Null
New-ItemProperty -Path $labKey -Name Workers -PropertyType DWord -Value 3 -Force | Out-Null
קודם נרשמים, אחר כך קוראים¶
נכתוב צופה קטן בwatch_config.cpp. הוא שומר את הערך התקין האחרון, ולא מחליף אותו כאשר כתבו טיפוס שגוי או מספר מחוץ לטווח. הריצה מסתיימת אחרי שלוש התראות או אחרי timeout, כדי שלא נצטרך להשאיר תהליך מעבדה ברקע.
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>
#include <stdexcept>
#include <string>
#pragma comment(lib, "advapi32.lib")
struct event_owner {
HANDLE value = CreateEventW(nullptr, FALSE, FALSE, nullptr);
event_owner() = default;
event_owner(const event_owner&) = delete;
event_owner& operator=(const event_owner&) = delete;
~event_owner() { if (value) CloseHandle(value); }
};
struct key_owner {
HKEY value = nullptr;
key_owner() = default;
key_owner(const key_owner&) = delete;
key_owner& operator=(const key_owner&) = delete;
~key_owner() { if (value) RegCloseKey(value); }
};
void check(LSTATUS status, const char* operation) {
if (status != ERROR_SUCCESS)
throw std::runtime_error(std::string(operation) + ": " + std::to_string(status));
}
int main() {
try {
event_owner changed;
if (!changed.value)
throw std::runtime_error("CreateEvent failed: " + std::to_string(GetLastError()));
key_owner key;
check(RegOpenKeyExW(HKEY_CURRENT_USER,
L"Software\\AmitTechCourse\\ConfigurationLab", 0,
KEY_QUERY_VALUE | KEY_NOTIFY, &key.value), "open key");
DWORD active = 1;
for (unsigned round = 0; round != 4; ++round) {
check(RegNotifyChangeKeyValue(key.value, FALSE,
REG_NOTIFY_CHANGE_LAST_SET | REG_NOTIFY_THREAD_AGNOSTIC,
changed.value, TRUE), "arm notification");
DWORD candidate = 0;
DWORD bytes = sizeof(candidate);
LSTATUS status = RegGetValueW(key.value, nullptr, L"Workers",
RRF_RT_REG_DWORD | RRF_ZEROONFAILURE, nullptr, &candidate, &bytes);
if (status == ERROR_SUCCESS && candidate >= 1 && candidate <= 64) {
active = candidate;
std::cout << "accepted workers=" << active << '\n';
} else {
std::cout << "rejected status=" << status
<< " keeping=" << active << '\n';
}
if (round == 3) break;
DWORD wait = WaitForSingleObject(changed.value, 20000);
if (wait == WAIT_TIMEOUT) break;
if (wait != WAIT_OBJECT_0)
throw std::runtime_error("wait failed: " + std::to_string(GetLastError()));
}
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
בנו והפעילו, ובטרמינל השני שנו את ערך המעבדה.
Set-ItemProperty -Path 'HKCU:\Software\AmitTechCourse\ConfigurationLab' -Name Workers -Value 7
Set-ItemProperty -Path 'HKCU:\Software\AmitTechCourse\ConfigurationLab' -Name Workers -Value 0
המתינו לפלט בין שני השינויים אם רוצים לראות את שניהם. התראה אינה journal של כל כתיבה: כמה שינויים יכולים להתכנס לסימן אחד שאומר לנו לקרוא מחדש. אחרי קבלת התראה נרשמים מחדש, והדגל REG_NOTIFY_THREAD_AGNOSTIC מונע תלות בחיי הthread שרשם אותה בסביבות הנתמכות. אלו פרטים מחוזה ההתראות על מפתח.
סדר הרישום והקריאה מצמצם חלון חשוב. אם היינו קוראים קודם ורק אחר כך נרשמים, שינוי בין שתי הפעולות היה יכול להשאיר אותנו עם ערך ישן בלי התראה שתעיר אותנו. בקוד שלנו שינוי לפני הקריאה יופיע בקריאה, ושינוי אחרי הרישום יכול לסמן את הevent. לפעמים נקרא אותו מצב פעמיים; זה בסדר, כי החלת התצורה צריכה להיות בטוחה גם במקרה הזה.
שימו לב שAPIים של registry מחזירים לעיתים קרובות את קוד השגיאה ישירות. כאן לא נחליף אותו בGetLastError. בקריאת RegGetValueW אנחנו מבקשים במפורש REG_DWORD, והגודל נמדד בbytes. אפשר לבדוק את חוזה קריאת הערך והטיפוס.
אותה כתובת, מבט אחר¶
בחלקים מסוימים של הregistry יש views נפרדים לתוכנות של 32 ביט ושל 64 ביט. במקומות אחרים המפתחות משותפים. לכן לא נאמר שכל הregistry משוכפל, וגם לא נכתוב ידנית Wow6432Node לכל נתיב. נבחר view דרך הדגלים המתועדים ונבדוק את המפתח המסוים. ההסבר נמצא במודל הregistry redirector.
אפשר לבצע ניסוי קריאה בלבד עם שני מבטים על רשימת ההתקנות.
reg.exe query HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall /reg:32
reg.exe query HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall /reg:64
הפלט תלוי בתוכנות שמותקנות במכונה. אם התקבלו רשימות שונות, נבדוק איזה build של כלי התצורה כתב ולאיזה view השירות קורא. לא נפתור את התקלה באמצעות כתיבה כפולה עיוורת לשני המקומות, כי אז ייווצרו שני מקורות אמת שאינם מסכימים.
תצורה שלמה והחלפת גרסה¶
נניח שיש גם MinWorkers וגם MaxWorkers. שתי קריאות נפרדות יכולות לראות ערך ישן של הראשון וערך חדש של השני. אימות של כל מספר לבדו לא מבטיח שהזוג חוקי. נקרא מועמד שלם, נבדוק את היחסים בין השדות, ורק אז נפרסם אובייקט תצורה חדש למנוע.
במערכת עם כותב אחד אפשר לשמור גרסאות immutable במפתחות נפרדים ולפרסם מזהה גרסה פעילה אחרי שהגרסה החדשה שלמה. הקורא קורא את הגרסה המיועדת ובודק schema; ניקוי גרסאות ישנות צריך להתחשב בקוראים שעוד מחזיקים בהן. אפשר גם לבחור blob יחיד עם schema מפורש. הבחירה באחסון אינה פוטרת מתכנון commit, כשל וrollback.
אם גרסה חדשה מוסיפה שדה שהגרסה הישנה אינה מבינה, חזרה לexe הישן לא בהכרח מחזירה מערכת עובדת. נגדיר אם הישן מתעלם משדה נוסף, מסרב לגרסה חדשה או מקבל המרה מוכרת. נשמור את התצורה הישנה כל עוד היא נדרשת לrollback, ונבדוק קריסה בין כתיבת המועמד לפרסום שלו.
העדפה מול מדיניות¶
נניח שהמשתמש בחר 32 workers, אבל מדיניות המחשב מגבילה את המוצר לארבעה. נציג בנפרד את הערך המבוקש ואת הערך האפקטיבי, עם מקור ההחלטה. אחרת המשתמש רואה שהשמירה הצליחה ומניח שהמנוע מתעלם ממנו.
גם חסימת הרצת רכיב יכולה להגיע מapplication control ולא מDACL על הקובץ. נבדוק את לוג המדיניות ואת תצורת המכונה. במצב audit יש אירועים על פעולות שהיו נחסמות תחת enforcement; עצם קיום אירוע אינו אומר שהפעולה נחסמה בפועל. אפשר לקרוא על אירועי audit של App Control.
המוצר שלנו צריך להסביר מה ניסה לעשות ואיזו שכבה דחתה אותו. לא נמליץ לכבות מדיניות רק מפני שהמחשב של המפתח לא הפעיל אותה. נבנה תרחיש בדיקה שמריץ את התוכנה תחת אותה מדיניות ונפתור את התלות או ההרשאה המדויקת שחסרה.