לדלג לתוכן

פתרון פרויקט ממשק למנהל התהליכים

נתחיל מהחיבור עצמו. התוכנית הבאה היא dashboard מלא לתוכנית supervisor.exe של פרק 4. שומרים אותה בשם dashboard.cpp ומניחים את שני הקבצים באותה תיקייה. הקוד אינו מתקין שירות ואינו משנה הגדרות מערכת.

הthread ברקע מחזיק את הprocess ואת הjob. הthread של החלון קורא snapshot קטן ומעביר בקשת סיום דרך atomic. כך סגירת handle אינה יכולה להתחרות בקריאה של הממשק לאותו handle.

#define UNICODE
#define _UNICODE
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <atomic>
#include <cstdio>
#include <mutex>
#include <string>
#include <thread>
#include <utility>

struct Handle {
    HANDLE h = nullptr;
    explicit Handle(HANDLE value = nullptr) : h(value) {}
    ~Handle() { if (h) CloseHandle(h); }
    Handle(const Handle&) = delete;
    Handle& operator=(const Handle&) = delete;
};
struct Snapshot { bool done = true; std::wstring text = L"Ready"; };
struct App {
    std::mutex mutex;
    Snapshot value;
    std::atomic<bool> force{false};
    std::jthread worker;
    bool closing = false;
    HWND start = nullptr, stop = nullptr, status = nullptr;
    void publish(bool done, std::wstring text) {
        std::lock_guard lock(mutex);
        value = {done, std::move(text)};
    }
    Snapshot snapshot() {
        std::lock_guard lock(mutex);
        return value;
    }
};

void run(App& app, std::wstring path) {
    auto fail = [&](const wchar_t* stage, DWORD code) {
        app.publish(true, std::wstring(stage) + L": " + std::to_wstring(code));
    };
    Handle job(CreateJobObjectW(nullptr, nullptr));
    if (!job.h) { fail(L"CreateJob", GetLastError()); return; }
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
    limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
    if (!SetInformationJobObject(job.h, JobObjectExtendedLimitInformation,
                                  &limits, sizeof limits)) {
        fail(L"Job limits", GetLastError()); return;
    }
    std::wstring command = L"\"" + path + L"\"";
    STARTUPINFOW startup{sizeof startup};
    PROCESS_INFORMATION info{};
    if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE,
                        CREATE_SUSPENDED | CREATE_NO_WINDOW, nullptr, nullptr,
                        &startup, &info)) {
        fail(L"CreateProcess", GetLastError()); return;
    }
    Handle process(info.hProcess), thread(info.hThread);
    if (!AssignProcessToJobObject(job.h, process.h)) {
        DWORD error = GetLastError();
        BOOL terminated = TerminateProcess(process.h, 201);
        DWORD wait = WaitForSingleObject(process.h, 5000);
        fail(terminated && wait == WAIT_OBJECT_0 ? L"AssignJob" :
             L"AssignJob; child cleanup unconfirmed", error);
        return;
    }
    if (ResumeThread(thread.h) == static_cast<DWORD>(-1)) {
        DWORD error = GetLastError();
        TerminateJobObject(job.h, 202);
        DWORD wait = WaitForSingleObject(process.h, 5000);
        fail(wait == WAIT_OBJECT_0 ? L"ResumeThread" :
             L"ResumeThread; cleanup unconfirmed", error);
        return;
    }
    app.publish(false, L"Running");
    ULONGLONG started = GetTickCount64(), forced_at = 0;
    bool forced = false;
    for (;;) {
        JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting{};
        if (!QueryInformationJobObject(job.h, JobObjectBasicAccountingInformation,
                                      &accounting, sizeof accounting, nullptr)) {
            fail(L"QueryJob; cleanup delegated to job close", GetLastError());
            return;
        }
        DWORD wait = WaitForSingleObject(process.h, 0);
        if (wait == WAIT_FAILED) {
            fail(L"Wait; cleanup delegated to job close", GetLastError()); return;
        }
        if (wait == WAIT_OBJECT_0 && accounting.ActiveProcesses == 0) {
            DWORD code = 0;
            if (!GetExitCodeProcess(process.h, &code)) {
                fail(L"Exit code", GetLastError()); return;
            }
            app.publish(true, std::wstring(forced ? L"Forced; exit=" : L"Finished; exit=")
                              + std::to_wstring(code));
            return;
        }
        ULONGLONG now = GetTickCount64();
        if (!forced && (app.force.load() || now - started >= 15000)) {
            if (!TerminateJobObject(job.h, 203)) {
                fail(L"TerminateJob; cleanup unconfirmed", GetLastError()); return;
            }
            forced = true; forced_at = now;
            app.publish(false, L"Termination requested; waiting for empty job");
        }
        if (forced && now - forced_at >= 5000) {
            app.publish(true, L"Cleanup unconfirmed; closing job handle"); return;
        }
        Sleep(20);
    }
}

std::wstring supervisor_path() {
    std::wstring path(32768, L'\0');
    DWORD count = GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
    if (!count || count >= path.size()) throw 1;
    path.resize(count);
    auto slash = path.find_last_of(L"\\/");
    if (slash == std::wstring::npos) throw 1;
    return path.substr(0, slash + 1) + L"supervisor.exe";
}

LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM w, LPARAM l) {
    auto* app = reinterpret_cast<App*>(GetWindowLongPtrW(window, GWLP_USERDATA));
    if (message == WM_NCCREATE) {
        app = static_cast<App*>(reinterpret_cast<CREATESTRUCTW*>(l)->lpCreateParams);
        SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(app));
    }
    if (!app) return DefWindowProcW(window, message, w, l);
    try {
        switch (message) {
        case WM_CREATE:
            app->start = CreateWindowW(L"BUTTON", L"Start", WS_CHILD|WS_VISIBLE|WS_TABSTOP,
                20,20,120,35,window,reinterpret_cast<HMENU>(1),nullptr,nullptr);
            app->stop = CreateWindowW(L"BUTTON", L"Force stop", WS_CHILD|WS_VISIBLE|WS_TABSTOP,
                160,20,120,35,window,reinterpret_cast<HMENU>(2),nullptr,nullptr);
            app->status = CreateWindowW(L"STATIC", L"Ready", WS_CHILD|WS_VISIBLE,
                20,80,580,70,window,nullptr,nullptr,nullptr);
            if (!app->start || !app->stop || !app->status || !SetTimer(window,1,50,nullptr))
                return -1;
            return 0;
        case WM_COMMAND:
            if (LOWORD(w) == 2) app->force.store(true);
            if (LOWORD(w) == 1 && !app->closing && app->snapshot().done) {
                if (app->worker.joinable()) app->worker.join();
                auto path = supervisor_path();
                app->force.store(false);
                app->publish(false, L"Starting");
                try {
                    app->worker = std::jthread([app, path] {
                        try { run(*app, path); }
                        catch (...) { app->publish(true, L"Worker exception; inspect diagnostic log"); }
                    });
                } catch (...) { app->publish(true, L"Cannot start worker"); }
            }
            return 0;
        case WM_TIMER: {
            auto state = app->snapshot();
            SetWindowTextW(app->status, state.text.c_str());
            EnableWindow(app->start, state.done && !app->closing);
            EnableWindow(app->stop, !state.done);
            if (app->closing && state.done) DestroyWindow(window);
            return 0;
        }
        case WM_CLOSE:
            app->closing = true;
            if (app->snapshot().done) DestroyWindow(window);
            return 0;
        case WM_DESTROY:
            KillTimer(window, 1); PostQuitMessage(0); return 0;
        case WM_NCDESTROY:
            SetWindowLongPtrW(window, GWLP_USERDATA, 0); break;
        }
    } catch (...) { return message == WM_CREATE ? -1 : 0; }
    return DefWindowProcW(window, message, w, l);
}

int main() {
    App app;
    HINSTANCE module = GetModuleHandleW(nullptr);
    WNDCLASSW type{};
    type.hInstance = module; type.lpfnWndProc = window_proc;
    type.lpszClassName = L"CourseDashboard";
    type.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    type.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
    if (!RegisterClassW(&type)) return 1;
    HWND window = CreateWindowW(type.lpszClassName, L"Course supervisor",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,CW_USEDEFAULT,650,230,
        nullptr,nullptr,module,&app);
    if (!window) return 2;
    ShowWindow(window, SW_SHOW);
    MSG message{};
    int result;
    while ((result = GetMessageW(&message,nullptr,0,0)) > 0) {
        if (!IsDialogMessageW(window, &message)) {
            TranslateMessage(&message); DispatchMessageW(&message);
        }
    }
    app.force.store(true);
    return result == -1 ? 3 : 0;
}

הבנייה נעשית בDeveloper PowerShell של Visual Studio. בונים גם את supervisor.cpp מפרק 4 באותה תיקייה.

cl /nologo /std:c++20 /EHsc /W4 /Zi dashboard.cpp user32.lib /Fe:dashboard.exe /link /DEBUG
cl /nologo /std:c++20 /EHsc /W4 /Zi supervisor.cpp /Fe:supervisor.exe /link /DEBUG

למה הסדר הזה חשוב

הילד נוצר suspended כדי שלא ייצור ילד משלו לפני שנכנס לjob. כישלון בשיוך דורש cleanup מפורש, כי הjob עדיין אינו הבעלים שלו. אחרי השיוך, סגירת הjob היא רשת ביטחון; היא אינה ראיה שהסיום כבר נצפה.

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

הפרסום done נעשה לפני חזרת הthread ולכן הjoin הבא עדיין יכול להמתין לרגע קצר של cleanup. הוא אינו ממתין לעבודה עצמה, אך גם אינו הבטחה מתמטית לזמן תגובה קשיח. מוצר עם דרישת זמן קשיחה יעביר גם את השלמת הthread דרך מנגנון reap מתאים.

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

חיבור האבחון וההפצה

הprovider של 10.6 מקבל מזהה הרצה חדש בלחיצה על התחלה. מוסיפים אירועים אחרי יצירת התהליך, אחרי שיוך לjob, בעת בקשת סיום ובפרסום התוצאה. מעבירים את המזהה לsupervisor בארגומנט חדש אחרי שמרחיבים את parser שלו, ולא מוסיפים ארגומנט שהגרסה הישנה אינה מכירה.

הdump שייך לתהליך שקרס. כדי לחקור worker בלי UI צריך את הPDB של הsupervisor ואת מזהה הבנייה שלו. כדי לחקור המתנה של החלון צריך גם את הdashboard. מאגר הסמלים נשמר לפי build, ולא כקובץ יחיד שנדרס בכל שחרור.

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

תוצאות שצריך להוכיח אצלכם

בהרצה רגילה צפוי exit code אפס של הsupervisor וjob ריק. בסיום כפוי צפוי דיווח Forced, גם אם פעולה מסוימת כבר הספיקה להסתיים לפני שהבקשה נצפתה. בקובץ חסר צפויה שגיאת יצירה בלי child חדש. סגירת חלון בזמן עבודה אמורה להשאיר את לולאת ההודעות פעילה עד לפרסום התוצאה.

הערכים כאן הם ציפיות לבדיקה, ולא תיעוד של הרצה במחשב שלכם. מצרפים trace משלכם, רשימת handles לפני ואחרי כמה עשרות מחזורים וחקירת תקלה עם סימנים תואמים. את מבחן ההתקנה מבצעים בVM נקייה, ואת חבילת הrelease משאירים ניתנת לשחזור מאותו commit.