From 8cd4f3c07a4a259e3f36168ae7b293843a4b9038 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:25:45 +0800 Subject: [PATCH 01/41] dynafix: split tables, hint UI, options, screen and PEGRC into included files (P6, no logic change) Pure move: the data tables, the hint UI and find_holder, read_options / enable_options, work_area / patch_vb_screen_walk and the PEGRC redirect (plus clear_runasinvoker) now live in src/dynafix_*.inc.c, #included from dynafix.c at the place the code used to be. Still one compilation unit (tests/logtest.c includes dynafix.c); build.cmd is unchanged. Co-Authored-By: Claude Opus 5.5 --- src/dynafix.c | 610 +------------------------------------- src/dynafix_hint.inc.c | 142 +++++++++ src/dynafix_options.inc.c | 125 ++++++++ src/dynafix_pegrc.inc.c | 252 ++++++++++++++++ src/dynafix_screen.inc.c | 72 +++++ src/dynafix_tables.inc.c | 32 ++ 6 files changed, 628 insertions(+), 605 deletions(-) create mode 100644 src/dynafix_hint.inc.c create mode 100644 src/dynafix_options.inc.c create mode 100644 src/dynafix_pegrc.inc.c create mode 100644 src/dynafix_screen.inc.c create mode 100644 src/dynafix_tables.inc.c diff --git a/src/dynafix.c b/src/dynafix.c index 009492d..8340c7f 100644 --- a/src/dynafix.c +++ b/src/dynafix.c @@ -373,136 +373,9 @@ static BOOL restart_dynarun(void) // returns only when the restart failed (on // afterwards with the same REG_SZ "-1" values that 工程模式 -> 系統組態設定 -> 存檔並離開 writes. #define SETUP_KEY "Software\\DynaPro\\System_Setup" #define N_OPT 4 -static const char *g_optname[N_OPT] = { "auto climate", "water cooler", "cooling fans", "AFR" }; -static const char *g_optval[N_OPT][3] = { - { "Auto_Climate_Enable", "Property_Auto_Climate", 0 }, - { "Water_Cooler_Enable", 0 }, - { "Cooling_Fan_Enable", 0 }, - { "Int_AF_Ratio_Enable", 0 }, -}; - -typedef struct { HWND form, frame; RECT fr; int n; HWND btn[8]; } sel_t; - -static void utf8_text(HWND h, char *a, int size) -{ - WCHAR w[60]; - w[0] = 0; GetWindowTextW(h, w, 60); - w2u(a, size, w); -} - -// The options frame is the lowest frame in the right half of the form. -static BOOL CALLBACK find_frame(HWND h, LPARAM l) -{ - sel_t *s = (sel_t *)l; char cls[40]; RECT r, f; - if (!GetClassNameA(h, cls, sizeof(cls)) || lstrcmpA(cls, "ThunderRT6Frame") || !IsWindowVisible(h)) return TRUE; - GetWindowRect(h, &r); GetWindowRect(s->form, &f); - if ((r.left + r.right) / 2 > (f.left + f.right) / 2 && (!s->frame || r.top > s->fr.top)) { s->frame = h; s->fr = r; } - return TRUE; -} - -// The buttons are the controls lying inside the frame: not all of them are its children (the cooling fan -// button is not), so all controls of the form are checked by position. -static BOOL CALLBACK find_buttons(HWND h, LPARAM l) -{ - sel_t *s = (sel_t *)l; RECT r, q; POINT c; char cls[40]; int i; - if (s->n >= 8 || !IsWindowVisible(h) || !GetClassNameA(h, cls, sizeof(cls)) || !lstrcmpA(cls, "ThunderRT6Frame")) return TRUE; - GetWindowRect(h, &r); - c.x = (r.left + r.right) / 2; c.y = (r.top + r.bottom) / 2; - if (!PtInRect(&s->fr, c)) return TRUE; - for (i = s->n++; i > 0; i--) { // keep them sorted top to bottom - GetWindowRect(s->btn[i - 1], &q); - if (q.top <= r.top) break; - s->btn[i] = s->btn[i - 1]; - } - s->btn[i] = h; - return TRUE; -} - -static BOOL CALLBACK find_form(HWND h, LPARAM l) -{ - sel_t *s = (sel_t *)l; - if (!IsWindowVisible(h)) return TRUE; - s->form = h; s->frame = NULL; - EnumChildWindows(h, find_frame, l); - return !s->frame; // stop at the first form with a frame -} - -// A sampled colour is "picked" (green), "plain" (no green to speak of) or in neither class (anything in between: a theme, high -// contrast or scaling made the colour unclear). -#define CLS_PICKED 1 -#define CLS_PLAIN 0 -#define CLS_UNSURE (-1) -static int colour_class(COLORREF c) -{ - int r = GetRValue(c), g = GetGValue(c), b = GetBValue(c), m = r > b ? r : b; - if (g >= 160 && r < 100 && b < 100) return CLS_PICKED; - if (g - m < 40) return CLS_PLAIN; // grey, white, black, red, blue...: no green dominance - return CLS_UNSURE; -} - -// Which optional features are drawn as picked (green). Returns FALSE on any doubt (group not found, wrong number of buttons, a -// colour that is clearly neither class, samples of one button that disagree, high-contrast mode): the caller then writes nothing. -static BOOL read_options(BOOL *on) -{ - typedef COLORREF (WINAPI *gp_t)(HDC, int, int); - gp_t gp = (gp_t)GetProcAddress(GetModuleHandleA("gdi32.dll"), "GetPixel"); - sel_t s; int i, k = 0; HIGHCONTRASTA hc; - { volatile char *z = (volatile char *)&s; for (i = 0; i < (int)sizeof(s); i++) z[i] = 0; } - EnumThreadWindows(GetCurrentThreadId(), find_form, (LPARAM)&s); - if (!s.frame) { logf("optional features: selection form not found %u %u %u\r\n", 0, 0, 0); return FALSE; } - hc.cbSize = sizeof(hc); hc.dwFlags = 0; - if (SystemParametersInfoA(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0) && (hc.dwFlags & HCF_HIGHCONTRASTON)) { - logf("optional features: high-contrast mode is on, colours cannot be trusted %u %u %u\r\n", 0, 0, 0); - return FALSE; - } - EnumChildWindows(s.form, find_buttons, (LPARAM)&s); - { char a[150]; utf8_text(s.frame, a, sizeof(a)); logf("optional features group '%s' has %u controls %u\r\n", (DWORD)(UINT_PTR)a, s.n, 0); } - for (i = 0; i < s.n; i++) { - char cls[40], a[150]; RECT r; HDC dc; COLORREF c = CLR_INVALID, c2; BOOL g; int k1, k2, k3; - GetClassNameA(s.btn[i], cls, sizeof(cls)); utf8_text(s.btn[i], a, sizeof(a)); - GetClientRect(s.btn[i], &r); - k1 = k2 = k3 = CLS_UNSURE; - if (gp && r.right >= 14 && r.bottom >= 9 && (dc = GetDC(s.btn[i]))) { - c = gp(dc, 6, r.bottom / 2); - // two more samples at other heights of the same column: all three must tell the same story - c2 = gp(dc, 6, r.bottom / 4); if (c2 != CLR_INVALID) k2 = colour_class(c2); - c2 = gp(dc, 6, r.bottom * 3 / 4); if (c2 != CLR_INVALID) k3 = colour_class(c2); - ReleaseDC(s.btn[i], dc); - } - logf(" %s '%s' colour %06X", (DWORD)(UINT_PTR)cls, (DWORD)(UINT_PTR)a, c); - // Controls of other groups (EB-150 / EB-250 engine dynamometers) also lie in this area, hidden - // (clipped away): nothing of them can be read, so they are skipped. - if (c == CLR_INVALID) { logf(" -> hidden, skipped %u %u %u\r\n", 0, 0, 0); continue; } - k1 = colour_class(c); - if (k1 == CLS_UNSURE || k1 != k2 || k1 != k3) { - logf(" -> unclear (samples %d/%d/%d)\r\n", (DWORD)k1, (DWORD)k2, (DWORD)k3); - logf("optional features: a button colour is not clearly picked or not picked %u %u %u\r\n", 0, 0, 0); - return FALSE; - } - g = k1 == CLS_PICKED; - if (k < N_OPT) on[k] = g; - logf(" -> %s %s %u\r\n", (DWORD)(UINT_PTR)(k < N_OPT ? g_optname[k] : "?"), (DWORD)(UINT_PTR)(g ? "picked" : "not picked"), 0); - k++; - } - if (k != N_OPT) { logf("optional features: expected %u buttons, found %u %u\r\n", N_OPT, k, 0); return FALSE; } - return TRUE; -} +#include "dynafix_tables.inc.c" -static void enable_options(const BOOL *on) -{ - HKEY k; int i, j; - for (i = 0; i < N_OPT && !on[i]; i++); - if (i == N_OPT) return; // nothing picked (or nothing readable): nothing is written - if (RegOpenKeyExA(HKEY_CURRENT_USER, SETUP_KEY, 0, KEY_SET_VALUE, &k)) { - logf("optional features: cannot open System_Setup %u %u %u\r\n", 0, 0, 0); - return; - } - for (i = 0; i < N_OPT; i++) - for (j = 0; on[i] && g_optval[i][j]; j++) - logf("enabled %s: System_Setup\\%s = \"-1\" (%s)\r\n", (DWORD)(UINT_PTR)g_optname[i], (DWORD)(UINT_PTR)g_optval[i][j], - (DWORD)(UINT_PTR)(RegSetValueExA(k, g_optval[i][j], 0, REG_SZ, (const BYTE *)"-1", 3) ? "failed" : "ok")); - RegCloseKey(k); -} +#include "dynafix_options.inc.c" // The helpers live in "System Data" under the per-user data folder (%APPDATA%\Dyna Pro Dynamometers\Dyna Run V3); // the folder next to DynaRun's exe is accepted too, for installs that keep it there. @@ -631,145 +504,7 @@ static DWORD g_conncalls, g_failn, g_failfirst, g_faillast, g_failhint, g_cfn, g static BOOL zh_ui(void) { return PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE; } -typedef DWORD (WINAPI *RmStart_t)(DWORD *, DWORD, WCHAR *); -typedef DWORD (WINAPI *RmReg_t)(DWORD, UINT, LPCWSTR *, UINT, RM_UNIQUE_PROCESS *, UINT, LPCWSTR *); -typedef DWORD (WINAPI *RmGet_t)(DWORD, UINT *, UINT *, RM_PROCESS_INFO *, LPDWORD); -typedef DWORD (WINAPI *RmEnd_t)(DWORD); - -// Who holds the file/device `path`? Restart Manager answers for files; for device paths it may know nothing. -static BOOL find_holder(const char *path, WCHAR *name, int n, DWORD *pid) -{ - HMODULE rm = load_sys("rstrtmgr.dll"); BOOL found = FALSE; - RmStart_t st; RmReg_t reg; RmGet_t get; RmEnd_t end; - if (!rm) return FALSE; - st = (RmStart_t)GetProcAddress(rm, "RmStartSession"); reg = (RmReg_t)GetProcAddress(rm, "RmRegisterResources"); - get = (RmGet_t)GetProcAddress(rm, "RmGetList"); end = (RmEnd_t)GetProcAddress(rm, "RmEndSession"); - if (st && reg && get && end) { - DWORD ses; WCHAR key[CCH_RM_SESSION_KEY + 1]; WCHAR wp[MAX_PATH]; LPCWSTR files[1]; - MultiByteToWideChar(CP_ACP, 0, path, -1, wp, MAX_PATH); - files[0] = wp; - if (!st(&ses, 0, key)) { - RM_PROCESS_INFO *pi = (RM_PROCESS_INFO *)HeapAlloc(GetProcessHeap(), 0, 8 * sizeof(*pi)); - if (pi && !reg(ses, 1, files, 0, NULL, 0, NULL)) { - UINT need = 0, cnt = 8; DWORD reasons = 0; - if (!get(ses, &need, &cnt, pi, &reasons) && cnt > 0) { - lstrcpynW(name, pi[0].strAppName, n); - *pid = pi[0].Process.dwProcessId; - found = TRUE; - } - } - if (pi) HeapFree(GetProcessHeap(), 0, pi); - end(ses); - } - } - FreeLibrary(rm); - return found; -} - -static LRESULT CALLBACK HintProc(HWND h, UINT m, WPARAM w, LPARAM l) -{ - hintwin_t *d; - if (m == WM_NCCREATE) SetWindowLongPtrA(h, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTA *)l)->lpCreateParams); - d = (hintwin_t *)GetWindowLongPtrA(h, GWLP_USERDATA); - switch (m) { - case WM_MOUSEACTIVATE: return MA_NOACTIVATE; - case WM_LBUTTONDOWN: case WM_TIMER: DestroyWindow(h); return 0; - case WM_DESTROY: PostQuitMessage(0); return 0; - case WM_PAINT: { - PAINTSTRUCT ps; HDC dc = BeginPaint(h, &ps); RECT r; HGDIOBJ o; - GetClientRect(h, &r); - FillRect(dc, &r, GetSysColorBrush(COLOR_INFOBK)); - if (d) { - o = SelectObject(dc, d->font); - SetBkMode(dc, TRANSPARENT); SetTextColor(dc, GetSysColor(COLOR_INFOTEXT)); - InflateRect(&r, -12, -10); - DrawTextW(dc, d->text, -1, &r, DT_WORDBREAK | DT_NOPREFIX); - SelectObject(dc, o); - } - EndPaint(h, &ps); - return 0; } - } - return DefWindowProcA(h, m, w, l); -} - -static void show_hint(const WCHAR *text) -{ - static const char cls[] = "dynafix.hint"; - WNDCLASSA wc; hintwin_t d; HWND h; HDC dc; RECT r, wa; MSG msg; NONCLIENTMETRICSW ncm; HGDIOBJ o; - SecureZeroMemory(&wc, sizeof(wc)); - wc.lpfnWndProc = HintProc; wc.hInstance = g_self; wc.lpszClassName = cls; wc.hCursor = LoadCursorA(NULL, (LPCSTR)IDC_ARROW); - RegisterClassA(&wc); - lstrcpynW(d.text, text, 320); - SecureZeroMemory(&ncm, sizeof(ncm)); ncm.cbSize = sizeof(ncm); - if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) { - ncm.cbSize -= sizeof(int); // before Vista: no iPaddedBorderWidth - if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0)) ncm.lfMessageFont.lfHeight = -12; - } - d.font = CreateFontIndirectW(&ncm.lfMessageFont); - dc = GetDC(NULL); - o = SelectObject(dc, d.font); - r.left = r.top = 0; r.right = 380 * GetDeviceCaps(dc, LOGPIXELSX) / 96; r.bottom = 0; - DrawTextW(dc, d.text, -1, &r, DT_WORDBREAK | DT_NOPREFIX | DT_CALCRECT); - SelectObject(dc, o); - ReleaseDC(NULL, dc); - r.right += 24; r.bottom += 20; - if (!SystemParametersInfoA(SPI_GETWORKAREA, 0, &wa, 0)) { wa.left = wa.top = 0; wa.right = GetSystemMetrics(SM_CXSCREEN); wa.bottom = GetSystemMetrics(SM_CYSCREEN); } - h = CreateWindowExA(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, cls, "DynaRunFix", WS_POPUP | WS_BORDER, - wa.right - r.right - 18, wa.bottom - r.bottom - 18, r.right + 2, r.bottom + 2, NULL, NULL, g_self, &d); - if (h) { - SetTimer(h, 1, 10000, NULL); - ShowWindow(h, SW_SHOWNOACTIVATE); - while (GetMessageA(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageA(&msg); } - } - if (d.font) DeleteObject(d.font); - UnregisterClassA(cls, g_self); -} - -static DWORD WINAPI HintThread(LPVOID p) -{ - hintreq_t *q = (hintreq_t *)p; WCHAR t[320], who[128]; DWORD pid = 0; BOOL zh = zh_ui(); - if (q->kind == 1) { - BOOL known = find_holder(q->path, who, 128, &pid); - if (known) { - char a[128]; - WideCharToMultiByte(CP_ACP, 0, who, -1, a, sizeof(a), NULL, NULL); - logf("device holder: pid %u, %s %u\r\n", pid, (DWORD)(UINT_PTR)a, 0); - } else logf("device holder: not found (Restart Manager does not know this device) %u %u %u\r\n", 0, 0, 0); - if (!g_hints) goto done; - if (known) wsprintfW(t, zh ? L"Dyna Pro 裝置正被其他程式使用(%s,PID %u)。\n關閉該程式後,DynaRun 會自動重新連線。" - : L"The Dyna Pro device is in use by another program (%s, PID %u).\nClose it and DynaRun will reconnect.", who, pid); - else lstrcpyW(t, zh ? L"Dyna Pro 裝置正被其他程式使用。\n關閉該程式後,DynaRun 會自動重新連線。" - : L"The Dyna Pro device is in use by another program.\nClose it and DynaRun will reconnect."); - } else if (q->kind == 3) { - if (!g_hints) goto done; - lstrcpyW(t, zh ? L"與 Dyna Pro 的 USB 資料呼叫長時間沒有回應,DynaRun 的資料寫入已被略過。\n請關閉並重新啟動 DynaRun。" - : L"A USB data call to the Dyna Pro has not returned for a long time, so DynaRun's data writes are being skipped.\nPlease close and restart DynaRun."); - } else if (q->kind == 4) { - lstrcpyW(t, zh ? L"無法自動重新啟動 DynaRun。\n請關閉後再手動啟動 DynaRun,以載入新的設定。" - : L"DynaRun could not be restarted automatically.\nPlease close it and start it again to load the new configuration."); - } else { - if (!g_hints) goto done; - lstrcpyW(t, zh ? L"無法連線到 Dyna Pro。\n請檢查 USB 線與裝置電源。" - : L"Cannot connect to the Dyna Pro.\nCheck the USB cable and the device's power."); - } - show_hint(t); -done: - HeapFree(GetProcessHeap(), 0, q); - InterlockedExchange(&g_hintbusy, 0); - return 0; -} - -// one hint at a time; never on the caller's thread -static void start_hint(int kind, DWORD err, const char *path) -{ - hintreq_t *q; HANDLE t; - if (InterlockedCompareExchange(&g_hintbusy, 1, 0)) return; - q = (hintreq_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*q)); - if (q) { q->kind = kind; q->err = err; if (path) lstrcpynA(q->path, path, MAX_PATH); } - t = q ? CreateThread(NULL, 0, HintThread, q, 0, NULL) : NULL; - if (t) CloseHandle(t); - else { if (q) HeapFree(GetProcessHeap(), 0, q); InterlockedExchange(&g_hintbusy, 0); } -} +#include "dynafix_hint.inc.c" // dynapro's CreateFileA: only the device (\\?\ path) is looked at; everything else passes untouched static HANDLE WINAPI H_CreateFileA(LPCSTR path, DWORD acc, DWORD share, LPSECURITY_ATTRIBUTES sa, DWORD disp, DWORD fl, HANDLE tmpl) @@ -917,20 +652,6 @@ static BYTE g_charset; // DYNAFIX_CHARSET: replaces ANSI_CHARSET, 0 = of static BYTE g_dbcs; // charset of a DBCS code page, for swapped ANSI_CHARSET fonts #define CS(c) (g_charset && (c) == ANSI_CHARSET ? g_charset : (c)) -// Faces that are swapped, with their line height (ascent + descent) in 1/1000 em. -static const struct { const WCHAR *face; short line; } g_swap[] = { - { L"Arial", 1117 }, { L"Times New Roman", 1107 }, { L"MS Sans Serif", 1180 }, { L"Microsoft Sans Serif", 1150 }, - { L"MS Shell Dlg", 1150 }, { L"MS Shell Dlg 2", 1207 }, { L"MS Serif", 1150 }, { L"Tahoma", 1207 }, - { L"Verdana", 1215 }, { L"Segoe UI", 1330 }, - { L"\x65B0\x7D30\x660E\x9AD4", 1000 }, { L"\x7D30\x660E\x9AD4", 1000 }, // 新細明體 細明體 - { L"PMingLiU", 1000 }, { L"MingLiU", 1000 }, { L"Microsoft JhengHei", 1250 }, { L"Microsoft JhengHei UI", 1270 }, - { L"SimSun", 1000 }, { L"\x5B8B\x4F53", 1000 }, { L"NSimSun", 1000 }, // 宋体 - { L"Microsoft YaHei", 1320 }, { L"Microsoft YaHei UI", 1320 }, - { L"MS UI Gothic", 1000 }, { L"MS PGothic", 1000 }, { L"\xFF2D\xFF33 \xFF30\x30B4\x30B7\x30C3\x30AF", 1000 }, // MS Pゴシック - { L"Meiryo UI", 1300 }, { L"Yu Gothic UI", 1330 }, - { L"Gulim", 1000 }, { L"\xAD74\xB9BC", 1000 }, { L"Dotum", 1000 }, { L"\xB3CB\xC6C0", 1000 }, // 굴림 돋움 - { L"Malgun Gothic", 1330 }, { 0 } -}; // 1: face to be swapped, 2: already the UI face (a copy of a swapped font, or asked for directly), 0: keep static int swap_face(const WCHAR *face, BYTE cs) @@ -1821,12 +1542,6 @@ static void pick_font(void) logf("UI font line height %u/1000 em %u %u\r\n", g_uiline, 0, 0); } -// Modules whose font imports are redirected: those in DynaRun's folder, OCX controls wherever they are, -// and these. Modules load later (an OCX with the first form that uses it), so the module list is checked -// again whenever a window is created. Not this dll's folder (Locale Emulator's dlls are there). -static const char *g_fontmods[] = { - "MSVBVM60.DLL", "ole32.dll", "oleaut32.dll", "MFC42.DLL", "comctl32.dll", "THBRes25.dll", "PEGRP32E.dll", 0 -}; static HMODULE g_fontdone[256]; static int g_nfontdone; static DWORD g_nmods, g_modsig; @@ -2145,75 +1860,7 @@ static BOOL is_main_form(HWND h, BOOL any) && GetClassNameA(h, cls, sizeof(cls)) && !lstrcmpA(cls, "ThunderRT6FormDC"); } -// Work area of monitor m (and its full rect), less 2 px on every edge that has an auto-hide taskbar. -static BOOL work_area(HMONITOR m, RECT *wa, RECT *mon) -{ - static SHAppBarMessage_t sab; static BOOL tried; - MONITORINFO mi; APPBARDATA ab; UINT e; - mi.cbSize = sizeof(mi); - if (!m || !GetMonitorInfoA(m, &mi)) return FALSE; - *wa = mi.rcWork; *mon = mi.rcMonitor; - if (!tried) { HMODULE s = LoadLibraryA("shell32.dll"); tried = TRUE; if (s) sab = (SHAppBarMessage_t)GetProcAddress(s, "SHAppBarMessage"); } - if (!sab) return TRUE; - for (e = ABE_LEFT; e <= ABE_BOTTOM; e++) { - HWND bar; - SecureZeroMemory(&ab, sizeof(ab)); ab.cbSize = sizeof(ab); ab.uEdge = e; ab.rc = mi.rcMonitor; - bar = (HWND)sab(ABM_GETAUTOHIDEBAREX, &ab); // Vista and later: per monitor - if (!bar && (mi.dwFlags & MONITORINFOF_PRIMARY)) { // XP: primary monitor only - SecureZeroMemory(&ab, sizeof(ab)); ab.cbSize = sizeof(ab); ab.uEdge = e; - bar = (HWND)sab(ABM_GETAUTOHIDEBAR, &ab); - } - if (!bar) continue; - if (e == ABE_LEFT && wa->left - mon->left < 2) wa->left = mon->left + 2; - if (e == ABE_TOP && wa->top - mon->top < 2) wa->top = mon->top + 2; - if (e == ABE_RIGHT && mon->right - wa->right < 2) wa->right = mon->right - 2; - if (e == ABE_BOTTOM && mon->bottom - wa->bottom < 2) wa->bottom = mon->bottom - 2; - } - return TRUE; -} - -// This adjusts the VB6 runtime's in-memory screen size values (the ones behind Screen.Width / Height) to the primary monitor's -// work area, so that DynaRun lays its dashboard out for that area. The runtime (MSVBVM60.DLL, Microsoft's) fills them from the -// screen size at start-up, before dynafix is loaded. The values are located in the loaded runtime and changed only if they equal -// the real screen size; otherwise they are left alone. Nothing is written to disk. -typedef struct { BYTE *base; const RECT *wa; int scx, scy; } vbwalk_t; -static void patch_vb_screen_walk(BYTE *base, const RECT *wa, int scx, int scy); -static void __stdcall walk_vb(void *a) { vbwalk_t *w = (vbwalk_t *)a; patch_vb_screen_walk(w->base, w->wa, w->scx, w->scy); } - -static void patch_vb_screen_walk(BYTE *base, const RECT *wa, int scx, int scy) -{ - IMAGE_NT_HEADERS *nt; IMAGE_SECTION_HEADER *sec; WORD i; - if (!(nt = nt_of(base)) || nt->FileHeader.NumberOfSections > 96) return; - sec = IMAGE_FIRST_SECTION(nt); - for (i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) { - BYTE *c, *e; - if (!(sec->Characteristics & IMAGE_SCN_MEM_EXECUTE)) continue; - if (sec->Misc.VirtualSize < 24 || sec->VirtualAddress >= nt->OptionalHeader.SizeOfImage - || sec->Misc.VirtualSize > nt->OptionalHeader.SizeOfImage - sec->VirtualAddress) continue; - c = base + sec->VirtualAddress; - e = c + sec->Misc.VirtualSize - 24; - for (; c < e; c++) { - int *px, *py; DWORD old; - if (c[0] != 0x6A || c[1] != 0x00 || c[2] != 0xA3 || c[7] != 0xFF || c[9] != 0x6A || c[10] != 0x01 || c[11] != 0xA3 - || c[16] != 0xFF || c[18] != 0x6A || c[19] != 0x20 || c[20] != 0xA3) continue; - px = *(int **)(c + 12); py = *(int **)(c + 21); - if ((BYTE *)px < base || (BYTE *)px >= base + nt->OptionalHeader.SizeOfImage - || (BYTE *)py < base || (BYTE *)py >= base + nt->OptionalHeader.SizeOfImage) continue; - if (*px != scx || *py != scy) { - logf("VB's screen size %ux%u is not the screen size, left alone %u\r\n", *px, *py, 0); - return; - } - VirtualProtect(px, sizeof(int), PAGE_READWRITE, &old); *px = wa->right - wa->left; VirtualProtect(px, sizeof(int), old, &old); - VirtualProtect(py, sizeof(int), PAGE_READWRITE, &old); *py = wa->bottom - wa->top; VirtualProtect(py, sizeof(int), old, &old); - { - char t[60]; wsprintfA(t, "%ux%u -> %ux%u", scx, scy, *px, *py); - logf("screen-size values adjusted (work area): %s %u %u\r\n", (DWORD)(UINT_PTR)t, 0, 0); - } - return; - } - } - logf("VB's screen size not found in MSVBVM60.DLL %u %u %u\r\n", 0, 0, 0); -} +#include "dynafix_screen.inc.c" static void patch_vb_screen(void) { @@ -2364,215 +2011,7 @@ static BOOL CALLBACK count_win(HWND h, LPARAM l) { (void)h; (*(int *)l)++; retur static LPARAM g_cwp_l; static void cwp_work(CWPSTRUCT *c); -// ---- PEGRC32D.dll: the graph control's language resource ---- -// The graph control (Gigasoft ProEssentials: Pesgo32e.ocx on PEGRP32E.dll) shows its popup menu in the language of a resource file -// "PEGRC32D.dll" that it looks up by name: DynaRun's folder, then the system folder. DynaRun's installer puts the copy for the chosen -// language in %APPDATA%\Dyna Pro Dynamometers\Dyna Run V3\Language\\ and, at every start, tries to copy it into the system -// folder; without administrator rights that is refused and the menu stays English. Here the control's request for that name is answered -// with that per-user file instead, nothing is written anywhere. Only a resource-only load (LOAD_LIBRARY_AS_DATAFILE / AS_IMAGE_RESOURCE, -// which is what the control asks for) is redirected: the user-writable file is never run as code; a request that loads code is left alone. -// Only when DynaRun is not elevated (elevated, DynaRun's own copy works), and only for this one file name: the path is built here from -// the profile and the language value, checked (regular file, no reparse point below the profile) and never taken from the request. -// DYNAFIX_PEGRC=0 switches it off. The imports of the control's two modules are patched from the loader's notification (before the -// control's entry point runs: it asks for the file while loading), and from peg_check, which looks for them as DynaRun's windows come. -static BOOL g_pegrc = TRUE; -static int g_pegel = -1; // 1 elevated, 0 not, set once -static FARPROC r_pLLxA, r_pLLxW; // the control's LoadLibraryEx imports -static HMODULE g_pegmod[2]; -static BOOL g_pegmsg; - -static BOOL peg_elevated(void) -{ - if (g_pegel < 0) { - HANDLE t; DWORD el = 0, n = 0; BOOL ok = FALSE; - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t)) { - ok = GetTokenInformation(t, (TOKEN_INFORMATION_CLASS)20 /* TokenElevation */, &el, sizeof(el), &n); - CloseHandle(t); - } - // Before Vista there is no UAC and no TokenElevation: not "elevated". On Vista and later a failed query counts as elevated - // (no redirect): the safe side. - if (ok) g_pegel = el ? 1 : 0; - else { -#pragma warning(suppress: 4996) - DWORD ver = GetVersion(); // only the major version is needed (before Vista = 5 or less) - g_pegel = LOBYTE(LOWORD(ver)) < 6 ? 0 : 1; - } - } - return g_pegel == 1; -} - -typedef HRESULT (WINAPI *gfpw_t)(HWND, int, HANDLE, DWORD, LPWSTR); - -// Fills path with the validated per-user file, or returns FALSE (reason in *why). -static BOOL peg_path(WCHAR *path, const char **why) -{ - static const WCHAR tail[] = L"\\Dyna Pro Dynamometers\\Dyna Run V3\\Language\\"; - WCHAR prof[MAX_PATH], app[MAX_PATH], lang[40], tmp[MAX_PATH], root[MAX_PATH]; DWORD t = 0, n = sizeof(lang) - 2; HKEY k; HMODULE sh; gfpw_t gfp; - int i, rl, bad = 0; DWORD a; - *why = "no profile folder"; - sh = GetModuleHandleA("shell32.dll"); - gfp = sh ? (gfpw_t)GetProcAddress(sh, "SHGetFolderPathW") : NULL; - if (!gfp || gfp(NULL, 0x001a /* CSIDL_APPDATA */, NULL, 0, app) != S_OK || gfp(NULL, 0x0028 /* CSIDL_PROFILE */, NULL, 0, prof) != S_OK) return FALSE; - *why = "no Default_Language"; - lang[0] = 0; - if (RegOpenKeyExW(HKEY_CURRENT_USER, L"" LANG_KEY, 0, KEY_QUERY_VALUE, &k)) return FALSE; - if (RegQueryValueExW(k, L"Default_Language", NULL, &t, (BYTE *)lang, &n) || t != REG_SZ) lang[0] = 0; - RegCloseKey(k); - lang[n / 2 < 39 ? n / 2 : 39] = 0; - if (!lang[0]) return FALSE; - for (i = 0; lang[i]; i++) // a folder name, nothing else - if (!((lang[i] >= L'A' && lang[i] <= L'Z') || (lang[i] >= L'a' && lang[i] <= L'z') || (lang[i] >= L'0' && lang[i] <= L'9') || lang[i] == L'_' || lang[i] == L' ' || lang[i] == L'-')) bad = 1; - *why = "Default_Language is not a plain folder name"; - if (bad) return FALSE; - *why = "path too long"; - if (lstrlenW(app) + lstrlenW(tail) + lstrlenW(lang) + 14 >= MAX_PATH) return FALSE; - lstrcpyW(tmp, app); lstrcatW(tmp, tail); lstrcatW(tmp, lang); lstrcatW(tmp, L"\\PEGRC32D.dll"); - *why = "path not canonical"; - if (!GetFullPathNameW(tmp, MAX_PATH, path, NULL) || lstrcmpiW(tmp, path)) return FALSE; - // reparse points are checked below the profile (or below AppData when that lies elsewhere) - rl = lstrlenW(prof); - if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, rl, prof, rl) == CSTR_EQUAL && path[rl] == L'\\') lstrcpynW(root, prof, MAX_PATH); - else lstrcpynW(root, app, MAX_PATH); - rl = lstrlenW(root); - *why = "a folder on the way is missing or a reparse point"; - for (i = rl + 1; path[i]; i++) { - if (path[i] != L'\\') continue; - lstrcpynW(tmp, path, i + 1); - a = GetFileAttributesW(tmp); - if (a == INVALID_FILE_ATTRIBUTES || !(a & FILE_ATTRIBUTE_DIRECTORY) || (a & FILE_ATTRIBUTE_REPARSE_POINT)) return FALSE; - } - *why = "file missing, not a regular file, or a reparse point"; - a = GetFileAttributesW(path); - if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return FALSE; - *why = ""; - return TRUE; -} - -// The request is for PEGRC32D.dll by bare name or with the system folder's path. -static BOOL peg_name(const WCHAR *n) -{ - WCHAR sys[MAX_PATH + 16]; int l; - if (!n || (UINT_PTR)n < 0x10000) return FALSE; - if (!lstrcmpiW(n, L"PEGRC32D.dll")) return TRUE; - l = lstrlenW(n); - if (l < 14 || lstrcmpiW(n + l - 13, L"\\PEGRC32D.dll")) return FALSE; - { // the same file name behind a path: only the system folders (what the control would find by itself) - typedef UINT (WINAPI *gsw_t)(LPWSTR, UINT); - gsw_t g = (gsw_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetSystemWow64DirectoryW"); - int k; - for (k = 0; k < 2; k++) { - UINT m = k == 0 ? GetSystemDirectoryW(sys, MAX_PATH) : (g ? g(sys, MAX_PATH) : 0); - if (m && m < MAX_PATH && l == (int)m + 13 && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, n, m, sys, m) == CSTR_EQUAL) return TRUE; - } - } - return FALSE; -} - -// The load of PEGRC32D.dll asked as (api, name, flags): the handle of the per-user file, or NULL to go on with the original call. -static HMODULE peg_load(const char *api, const WCHAR *name, DWORD flags) -{ - WCHAR path[MAX_PATH]; const char *why = ""; char u[MAX_PATH * 2], msg[MAX_PATH * 3 + 200]; HMODULE h; - typedef HMODULE (WINAPI *llx_t)(LPCWSTR, HANDLE, DWORD); - w2u(u, sizeof(u), name); - if (peg_elevated()) { - if (!g_pegmsg) { g_pegmsg = TRUE; wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): elevated, left alone (DynaRun's own copy works)", api, u, flags); logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); } - return NULL; - } - if (!(flags & (0x00000002 /* AS_DATAFILE */ | 0x00000020 /* AS_IMAGE_RESOURCE */ | 0x00000040 /* AS_DATAFILE_EXCLUSIVE */))) { - wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): not redirected, the caller loads code, not just resources", api, u, flags); - logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); - return NULL; - } - if (!peg_path(path, &why)) { - wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): not redirected, %s", api, u, flags, why); - logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); - return NULL; - } - // The per-user file is only ever loaded as a resource file, whatever the caller asked for. - h = ((llx_t)r_pLLxW)(path, NULL, 0x00000002 /* LOAD_LIBRARY_AS_DATAFILE */ | 0x00000020 /* LOAD_LIBRARY_AS_IMAGE_RESOURCE */); - w2u(u, sizeof(u), path); - wsprintfA(msg, "PEGRC32D: %s redirected to '%s' (asked flags %08X, loaded as a resource file only): %s", api, u, flags, h ? "loaded" : "failed, the original call goes on"); - logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); - return h; -} - -static HMODULE WINAPI H_pLLxW(LPCWSTR n, HANDLE f, DWORD fl) -{ - HMODULE h = NULL; - if (g_pegrc && !f && peg_name(n)) h = peg_load("LoadLibraryExW", n, fl); - return h ? h : ((HMODULE (WINAPI *)(LPCWSTR, HANDLE, DWORD))r_pLLxW)(n, f, fl); -} -static HMODULE peg_a(const char *api, LPCSTR n, DWORD fl) -{ - WCHAR w[MAX_PATH]; - if (!g_pegrc || !n || (UINT_PTR)n < 0x10000 || !MultiByteToWideChar(CP_ACP, 0, n, -1, w, MAX_PATH) || !peg_name(w)) return NULL; - return peg_load(api, w, fl); -} -static HMODULE WINAPI H_pLLxA(LPCSTR n, HANDLE f, DWORD fl) -{ - HMODULE h = f ? NULL : peg_a("LoadLibraryExA", n, fl); - return h ? h : ((HMODULE (WINAPI *)(LPCSTR, HANDLE, DWORD))r_pLLxA)(n, f, fl); -} - -static hook_t g_peg_hooks[] = { - { "LoadLibraryExA", &r_pLLxA, (FARPROC)H_pLLxA, NULL, "kernel32" }, - { "LoadLibraryExW", &r_pLLxW, (FARPROC)H_pLLxW, NULL, "kernel32" }, - { 0 } -}; - -static const char *g_pegmods[2] = { "Pesgo32e.ocx", "PEGRP32E.dll" }; - -// The control may ask for the file while it is being loaded (its entry point), i.e. before the load call returns: the loader's -// notification for the module comes before that, so the control's imports are patched from there too (nothing but plain memory -// and the log are touched; the log is written with the names already in hand). -typedef struct { USHORT Length, MaximumLength; PWSTR Buffer; } peg_us; -static volatile LONG g_pegready; -static BOOL g_pegnote; -static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) -{ - WCHAR n[32]; const peg_us *u; int i, k; - (void)ctx; - if (reason != 1 /* LOADED */ || !d || !g_pegready) return; - u = (const peg_us *)d->BaseDllName; - if (!u || !u->Buffer || u->Length / 2 >= 32) return; - for (i = 0; i < (int)(u->Length / 2); i++) n[i] = u->Buffer[i]; - n[i] = 0; - for (k = 0; k < 2; k++) { - WCHAR w[32]; int j; - for (j = 0; g_pegmods[k][j]; j++) w[j] = (WCHAR)g_pegmods[k][j]; - w[j] = 0; - if (!lstrcmpiW(n, w)) { - logf("PEGRC32D: loader notification for %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[k], 0, 0); - patch_base((BYTE *)d->DllBase, g_pegmods[k], "peg-note", g_peg_hooks); - } - } -} - -// Patch the control's modules once they are loaded (also the fallback when no loader notifications exist); idempotent. -static void peg_check(void) -{ - static BOOL resolved, off; static volatile LONG busy; - int i; - if (!g_pegrc || off || !GetModuleHandleA("MSVBVM60.DLL")) return; - if (InterlockedCompareExchange(&busy, 1, 0)) return; // not from inside our own patching - if (!resolved) { - if (peg_elevated()) { off = TRUE; logf("PEGRC32D: DynaRun is elevated: no redirect %u %u %u\r\n", 0, 0, 0); InterlockedExchange(&busy, 0); return; } - resolved = TRUE; resolve(g_peg_hooks); - { // see peg_ldr_note - HMODULE nt = GetModuleHandleA("ntdll.dll"); ldrreg_t rg = nt ? (ldrreg_t)GetProcAddress(nt, "LdrRegisterDllNotification") : NULL; PVOID ck; - if (rg && !g_pegnote) { g_pegnote = TRUE; InterlockedExchange(&g_pegready, 1); if (rg(0, peg_ldr_note, NULL, &ck)) InterlockedExchange(&g_pegready, 0); } - } - } - for (i = 0; i < 2; i++) { - HMODULE m = GetModuleHandleA(g_pegmods[i]); - if (m && m != g_pegmod[i]) { - g_pegmod[i] = m; - logf("PEGRC32D: patching the LoadLibrary imports of %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[i], 0, 0); - patch_base((BYTE *)m, g_pegmods[i], "peg", g_peg_hooks); - } else if (!m) g_pegmod[i] = NULL; - } - InterlockedExchange(&busy, 0); -} +#include "dynafix_pegrc.inc.c" __declspec(dllexport) LRESULT CALLBACK CwpProc(int code, WPARAM w, LPARAM l) @@ -2629,45 +2068,6 @@ static void cwp_work(CWPSTRUCT *c) } } -// The launcher starts DynaRun with "RunAsInvoker" appended to __COMPAT_LAYER (so that DynaRun itself never runs elevated). That -// environment variable is inherited by DynaRun's own child processes, e.g. its first-start Setup helper, for which Windows would -// otherwise ask for elevation; so here the word is taken out again (what was there before stays; the variable is removed when -// nothing else is left). The launcher process itself (which loads this dll too) is left alone. -// no CRT in this DLL: stop newer MSVC from turning the copy loops below into memcpy calls -#pragma optimize("", off) -static void clear_runasinvoker(void) -{ - char exe[MAX_PATH], v[512], o[512], msg[400], *p, *q; DWORD n; - n = GetModuleFileNameA(NULL, exe, MAX_PATH); - if (n && n < MAX_PATH) { - for (p = exe + n; p > exe && p[-1] != '\\'; p--); - if (!lstrcmpiA(p, "DynaRunFix.exe")) return; - } - n = GetEnvironmentVariableA("__COMPAT_LAYER", v, sizeof(v)); - if (!n || n >= sizeof(v)) return; - o[0] = 0; q = o; p = v; - while (*p) { - char *e; int len, keep; char tok[16]; - while (*p == ' ') p++; - if (!*p) break; - for (e = p; *e && *e != ' '; e++); - len = (int)(e - p); - keep = 1; - if (len == 12) { int c; for (c = 0; c < 12; c++) tok[c] = p[c]; tok[12] = 0; keep = lstrcmpiA(tok, "RunAsInvoker") != 0; } - if (keep) { - if (q != o) *q++ = ' '; - while (p < e) *q++ = *p++; - } - p = e; - } - *q = 0; - if (lstrcmpA(o, v)) { - SetEnvironmentVariableA("__COMPAT_LAYER", o[0] ? o : NULL); - wsprintfA(msg, "RunAsInvoker taken out of __COMPAT_LAYER for DynaRun's child processes (left: '%.200s')\r\n", o); - logf("%s", (DWORD)(UINT_PTR)msg, 0, 0); - } -} -#pragma optimize("", on) BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID p) { diff --git a/src/dynafix_hint.inc.c b/src/dynafix_hint.inc.c new file mode 100644 index 0000000..820c62a --- /dev/null +++ b/src/dynafix_hint.inc.c @@ -0,0 +1,142 @@ +// dynafix_hint.inc.c - the small hint windows (device in use, USB call stuck, restart failed) and the Restart Manager lookup of who holds the device. +// Included by dynafix.c (a single compilation unit, also pulled in by tests\logtest.c); not compiled on its own. + +typedef DWORD (WINAPI *RmStart_t)(DWORD *, DWORD, WCHAR *); +typedef DWORD (WINAPI *RmReg_t)(DWORD, UINT, LPCWSTR *, UINT, RM_UNIQUE_PROCESS *, UINT, LPCWSTR *); +typedef DWORD (WINAPI *RmGet_t)(DWORD, UINT *, UINT *, RM_PROCESS_INFO *, LPDWORD); +typedef DWORD (WINAPI *RmEnd_t)(DWORD); + +// Who holds the file/device `path`? Restart Manager answers for files; for device paths it may know nothing. +static BOOL find_holder(const char *path, WCHAR *name, int n, DWORD *pid) +{ + HMODULE rm = load_sys("rstrtmgr.dll"); BOOL found = FALSE; + RmStart_t st; RmReg_t reg; RmGet_t get; RmEnd_t end; + if (!rm) return FALSE; + st = (RmStart_t)GetProcAddress(rm, "RmStartSession"); reg = (RmReg_t)GetProcAddress(rm, "RmRegisterResources"); + get = (RmGet_t)GetProcAddress(rm, "RmGetList"); end = (RmEnd_t)GetProcAddress(rm, "RmEndSession"); + if (st && reg && get && end) { + DWORD ses; WCHAR key[CCH_RM_SESSION_KEY + 1]; WCHAR wp[MAX_PATH]; LPCWSTR files[1]; + MultiByteToWideChar(CP_ACP, 0, path, -1, wp, MAX_PATH); + files[0] = wp; + if (!st(&ses, 0, key)) { + RM_PROCESS_INFO *pi = (RM_PROCESS_INFO *)HeapAlloc(GetProcessHeap(), 0, 8 * sizeof(*pi)); + if (pi && !reg(ses, 1, files, 0, NULL, 0, NULL)) { + UINT need = 0, cnt = 8; DWORD reasons = 0; + if (!get(ses, &need, &cnt, pi, &reasons) && cnt > 0) { + lstrcpynW(name, pi[0].strAppName, n); + *pid = pi[0].Process.dwProcessId; + found = TRUE; + } + } + if (pi) HeapFree(GetProcessHeap(), 0, pi); + end(ses); + } + } + FreeLibrary(rm); + return found; +} + +static LRESULT CALLBACK HintProc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + hintwin_t *d; + if (m == WM_NCCREATE) SetWindowLongPtrA(h, GWLP_USERDATA, (LONG_PTR)((CREATESTRUCTA *)l)->lpCreateParams); + d = (hintwin_t *)GetWindowLongPtrA(h, GWLP_USERDATA); + switch (m) { + case WM_MOUSEACTIVATE: return MA_NOACTIVATE; + case WM_LBUTTONDOWN: case WM_TIMER: DestroyWindow(h); return 0; + case WM_DESTROY: PostQuitMessage(0); return 0; + case WM_PAINT: { + PAINTSTRUCT ps; HDC dc = BeginPaint(h, &ps); RECT r; HGDIOBJ o; + GetClientRect(h, &r); + FillRect(dc, &r, GetSysColorBrush(COLOR_INFOBK)); + if (d) { + o = SelectObject(dc, d->font); + SetBkMode(dc, TRANSPARENT); SetTextColor(dc, GetSysColor(COLOR_INFOTEXT)); + InflateRect(&r, -12, -10); + DrawTextW(dc, d->text, -1, &r, DT_WORDBREAK | DT_NOPREFIX); + SelectObject(dc, o); + } + EndPaint(h, &ps); + return 0; } + } + return DefWindowProcA(h, m, w, l); +} + +static void show_hint(const WCHAR *text) +{ + static const char cls[] = "dynafix.hint"; + WNDCLASSA wc; hintwin_t d; HWND h; HDC dc; RECT r, wa; MSG msg; NONCLIENTMETRICSW ncm; HGDIOBJ o; + SecureZeroMemory(&wc, sizeof(wc)); + wc.lpfnWndProc = HintProc; wc.hInstance = g_self; wc.lpszClassName = cls; wc.hCursor = LoadCursorA(NULL, (LPCSTR)IDC_ARROW); + RegisterClassA(&wc); + lstrcpynW(d.text, text, 320); + SecureZeroMemory(&ncm, sizeof(ncm)); ncm.cbSize = sizeof(ncm); + if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0)) { + ncm.cbSize -= sizeof(int); // before Vista: no iPaddedBorderWidth + if (!SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ncm.cbSize, &ncm, 0)) ncm.lfMessageFont.lfHeight = -12; + } + d.font = CreateFontIndirectW(&ncm.lfMessageFont); + dc = GetDC(NULL); + o = SelectObject(dc, d.font); + r.left = r.top = 0; r.right = 380 * GetDeviceCaps(dc, LOGPIXELSX) / 96; r.bottom = 0; + DrawTextW(dc, d.text, -1, &r, DT_WORDBREAK | DT_NOPREFIX | DT_CALCRECT); + SelectObject(dc, o); + ReleaseDC(NULL, dc); + r.right += 24; r.bottom += 20; + if (!SystemParametersInfoA(SPI_GETWORKAREA, 0, &wa, 0)) { wa.left = wa.top = 0; wa.right = GetSystemMetrics(SM_CXSCREEN); wa.bottom = GetSystemMetrics(SM_CYSCREEN); } + h = CreateWindowExA(WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, cls, "DynaRunFix", WS_POPUP | WS_BORDER, + wa.right - r.right - 18, wa.bottom - r.bottom - 18, r.right + 2, r.bottom + 2, NULL, NULL, g_self, &d); + if (h) { + SetTimer(h, 1, 10000, NULL); + ShowWindow(h, SW_SHOWNOACTIVATE); + while (GetMessageA(&msg, NULL, 0, 0) > 0) { TranslateMessage(&msg); DispatchMessageA(&msg); } + } + if (d.font) DeleteObject(d.font); + UnregisterClassA(cls, g_self); +} + +static DWORD WINAPI HintThread(LPVOID p) +{ + hintreq_t *q = (hintreq_t *)p; WCHAR t[320], who[128]; DWORD pid = 0; BOOL zh = zh_ui(); + if (q->kind == 1) { + BOOL known = find_holder(q->path, who, 128, &pid); + if (known) { + char a[128]; + WideCharToMultiByte(CP_ACP, 0, who, -1, a, sizeof(a), NULL, NULL); + logf("device holder: pid %u, %s %u\r\n", pid, (DWORD)(UINT_PTR)a, 0); + } else logf("device holder: not found (Restart Manager does not know this device) %u %u %u\r\n", 0, 0, 0); + if (!g_hints) goto done; + if (known) wsprintfW(t, zh ? L"Dyna Pro 裝置正被其他程式使用(%s,PID %u)。\n關閉該程式後,DynaRun 會自動重新連線。" + : L"The Dyna Pro device is in use by another program (%s, PID %u).\nClose it and DynaRun will reconnect.", who, pid); + else lstrcpyW(t, zh ? L"Dyna Pro 裝置正被其他程式使用。\n關閉該程式後,DynaRun 會自動重新連線。" + : L"The Dyna Pro device is in use by another program.\nClose it and DynaRun will reconnect."); + } else if (q->kind == 3) { + if (!g_hints) goto done; + lstrcpyW(t, zh ? L"與 Dyna Pro 的 USB 資料呼叫長時間沒有回應,DynaRun 的資料寫入已被略過。\n請關閉並重新啟動 DynaRun。" + : L"A USB data call to the Dyna Pro has not returned for a long time, so DynaRun's data writes are being skipped.\nPlease close and restart DynaRun."); + } else if (q->kind == 4) { + lstrcpyW(t, zh ? L"無法自動重新啟動 DynaRun。\n請關閉後再手動啟動 DynaRun,以載入新的設定。" + : L"DynaRun could not be restarted automatically.\nPlease close it and start it again to load the new configuration."); + } else { + if (!g_hints) goto done; + lstrcpyW(t, zh ? L"無法連線到 Dyna Pro。\n請檢查 USB 線與裝置電源。" + : L"Cannot connect to the Dyna Pro.\nCheck the USB cable and the device's power."); + } + show_hint(t); +done: + HeapFree(GetProcessHeap(), 0, q); + InterlockedExchange(&g_hintbusy, 0); + return 0; +} + +// one hint at a time; never on the caller's thread +static void start_hint(int kind, DWORD err, const char *path) +{ + hintreq_t *q; HANDLE t; + if (InterlockedCompareExchange(&g_hintbusy, 1, 0)) return; + q = (hintreq_t *)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(*q)); + if (q) { q->kind = kind; q->err = err; if (path) lstrcpynA(q->path, path, MAX_PATH); } + t = q ? CreateThread(NULL, 0, HintThread, q, 0, NULL) : NULL; + if (t) CloseHandle(t); + else { if (q) HeapFree(GetProcessHeap(), 0, q); InterlockedExchange(&g_hintbusy, 0); } +} diff --git a/src/dynafix_options.inc.c b/src/dynafix_options.inc.c new file mode 100644 index 0000000..a4d3b53 --- /dev/null +++ b/src/dynafix_options.inc.c @@ -0,0 +1,125 @@ +// dynafix_options.inc.c - reading the optional features picked on the first-start system selection, and switching them on. +// Included by dynafix.c (a single compilation unit, also pulled in by tests\logtest.c); not compiled on its own. + +typedef struct { HWND form, frame; RECT fr; int n; HWND btn[8]; } sel_t; + +static void utf8_text(HWND h, char *a, int size) +{ + WCHAR w[60]; + w[0] = 0; GetWindowTextW(h, w, 60); + w2u(a, size, w); +} + +// The options frame is the lowest frame in the right half of the form. +static BOOL CALLBACK find_frame(HWND h, LPARAM l) +{ + sel_t *s = (sel_t *)l; char cls[40]; RECT r, f; + if (!GetClassNameA(h, cls, sizeof(cls)) || lstrcmpA(cls, "ThunderRT6Frame") || !IsWindowVisible(h)) return TRUE; + GetWindowRect(h, &r); GetWindowRect(s->form, &f); + if ((r.left + r.right) / 2 > (f.left + f.right) / 2 && (!s->frame || r.top > s->fr.top)) { s->frame = h; s->fr = r; } + return TRUE; +} + +// The buttons are the controls lying inside the frame: not all of them are its children (the cooling fan +// button is not), so all controls of the form are checked by position. +static BOOL CALLBACK find_buttons(HWND h, LPARAM l) +{ + sel_t *s = (sel_t *)l; RECT r, q; POINT c; char cls[40]; int i; + if (s->n >= 8 || !IsWindowVisible(h) || !GetClassNameA(h, cls, sizeof(cls)) || !lstrcmpA(cls, "ThunderRT6Frame")) return TRUE; + GetWindowRect(h, &r); + c.x = (r.left + r.right) / 2; c.y = (r.top + r.bottom) / 2; + if (!PtInRect(&s->fr, c)) return TRUE; + for (i = s->n++; i > 0; i--) { // keep them sorted top to bottom + GetWindowRect(s->btn[i - 1], &q); + if (q.top <= r.top) break; + s->btn[i] = s->btn[i - 1]; + } + s->btn[i] = h; + return TRUE; +} + +static BOOL CALLBACK find_form(HWND h, LPARAM l) +{ + sel_t *s = (sel_t *)l; + if (!IsWindowVisible(h)) return TRUE; + s->form = h; s->frame = NULL; + EnumChildWindows(h, find_frame, l); + return !s->frame; // stop at the first form with a frame +} + +// A sampled colour is "picked" (green), "plain" (no green to speak of) or in neither class (anything in between: a theme, high +// contrast or scaling made the colour unclear). +#define CLS_PICKED 1 +#define CLS_PLAIN 0 +#define CLS_UNSURE (-1) +static int colour_class(COLORREF c) +{ + int r = GetRValue(c), g = GetGValue(c), b = GetBValue(c), m = r > b ? r : b; + if (g >= 160 && r < 100 && b < 100) return CLS_PICKED; + if (g - m < 40) return CLS_PLAIN; // grey, white, black, red, blue...: no green dominance + return CLS_UNSURE; +} + +// Which optional features are drawn as picked (green). Returns FALSE on any doubt (group not found, wrong number of buttons, a +// colour that is clearly neither class, samples of one button that disagree, high-contrast mode): the caller then writes nothing. +static BOOL read_options(BOOL *on) +{ + typedef COLORREF (WINAPI *gp_t)(HDC, int, int); + gp_t gp = (gp_t)GetProcAddress(GetModuleHandleA("gdi32.dll"), "GetPixel"); + sel_t s; int i, k = 0; HIGHCONTRASTA hc; + { volatile char *z = (volatile char *)&s; for (i = 0; i < (int)sizeof(s); i++) z[i] = 0; } + EnumThreadWindows(GetCurrentThreadId(), find_form, (LPARAM)&s); + if (!s.frame) { logf("optional features: selection form not found %u %u %u\r\n", 0, 0, 0); return FALSE; } + hc.cbSize = sizeof(hc); hc.dwFlags = 0; + if (SystemParametersInfoA(SPI_GETHIGHCONTRAST, sizeof(hc), &hc, 0) && (hc.dwFlags & HCF_HIGHCONTRASTON)) { + logf("optional features: high-contrast mode is on, colours cannot be trusted %u %u %u\r\n", 0, 0, 0); + return FALSE; + } + EnumChildWindows(s.form, find_buttons, (LPARAM)&s); + { char a[150]; utf8_text(s.frame, a, sizeof(a)); logf("optional features group '%s' has %u controls %u\r\n", (DWORD)(UINT_PTR)a, s.n, 0); } + for (i = 0; i < s.n; i++) { + char cls[40], a[150]; RECT r; HDC dc; COLORREF c = CLR_INVALID, c2; BOOL g; int k1, k2, k3; + GetClassNameA(s.btn[i], cls, sizeof(cls)); utf8_text(s.btn[i], a, sizeof(a)); + GetClientRect(s.btn[i], &r); + k1 = k2 = k3 = CLS_UNSURE; + if (gp && r.right >= 14 && r.bottom >= 9 && (dc = GetDC(s.btn[i]))) { + c = gp(dc, 6, r.bottom / 2); + // two more samples at other heights of the same column: all three must tell the same story + c2 = gp(dc, 6, r.bottom / 4); if (c2 != CLR_INVALID) k2 = colour_class(c2); + c2 = gp(dc, 6, r.bottom * 3 / 4); if (c2 != CLR_INVALID) k3 = colour_class(c2); + ReleaseDC(s.btn[i], dc); + } + logf(" %s '%s' colour %06X", (DWORD)(UINT_PTR)cls, (DWORD)(UINT_PTR)a, c); + // Controls of other groups (EB-150 / EB-250 engine dynamometers) also lie in this area, hidden + // (clipped away): nothing of them can be read, so they are skipped. + if (c == CLR_INVALID) { logf(" -> hidden, skipped %u %u %u\r\n", 0, 0, 0); continue; } + k1 = colour_class(c); + if (k1 == CLS_UNSURE || k1 != k2 || k1 != k3) { + logf(" -> unclear (samples %d/%d/%d)\r\n", (DWORD)k1, (DWORD)k2, (DWORD)k3); + logf("optional features: a button colour is not clearly picked or not picked %u %u %u\r\n", 0, 0, 0); + return FALSE; + } + g = k1 == CLS_PICKED; + if (k < N_OPT) on[k] = g; + logf(" -> %s %s %u\r\n", (DWORD)(UINT_PTR)(k < N_OPT ? g_optname[k] : "?"), (DWORD)(UINT_PTR)(g ? "picked" : "not picked"), 0); + k++; + } + if (k != N_OPT) { logf("optional features: expected %u buttons, found %u %u\r\n", N_OPT, k, 0); return FALSE; } + return TRUE; +} + +static void enable_options(const BOOL *on) +{ + HKEY k; int i, j; + for (i = 0; i < N_OPT && !on[i]; i++); + if (i == N_OPT) return; // nothing picked (or nothing readable): nothing is written + if (RegOpenKeyExA(HKEY_CURRENT_USER, SETUP_KEY, 0, KEY_SET_VALUE, &k)) { + logf("optional features: cannot open System_Setup %u %u %u\r\n", 0, 0, 0); + return; + } + for (i = 0; i < N_OPT; i++) + for (j = 0; on[i] && g_optval[i][j]; j++) + logf("enabled %s: System_Setup\\%s = \"-1\" (%s)\r\n", (DWORD)(UINT_PTR)g_optname[i], (DWORD)(UINT_PTR)g_optval[i][j], + (DWORD)(UINT_PTR)(RegSetValueExA(k, g_optval[i][j], 0, REG_SZ, (const BYTE *)"-1", 3) ? "failed" : "ok")); + RegCloseKey(k); +} diff --git a/src/dynafix_pegrc.inc.c b/src/dynafix_pegrc.inc.c new file mode 100644 index 0000000..e714b25 --- /dev/null +++ b/src/dynafix_pegrc.inc.c @@ -0,0 +1,252 @@ +// dynafix_pegrc.inc.c - PEGRC32D.dll language resource redirect for the graph control, and clear_runasinvoker. +// Included by dynafix.c (a single compilation unit, also pulled in by tests\logtest.c); not compiled on its own. + +// ---- PEGRC32D.dll: the graph control's language resource ---- +// The graph control (Gigasoft ProEssentials: Pesgo32e.ocx on PEGRP32E.dll) shows its popup menu in the language of a resource file +// "PEGRC32D.dll" that it looks up by name: DynaRun's folder, then the system folder. DynaRun's installer puts the copy for the chosen +// language in %APPDATA%\Dyna Pro Dynamometers\Dyna Run V3\Language\\ and, at every start, tries to copy it into the system +// folder; without administrator rights that is refused and the menu stays English. Here the control's request for that name is answered +// with that per-user file instead, nothing is written anywhere. Only a resource-only load (LOAD_LIBRARY_AS_DATAFILE / AS_IMAGE_RESOURCE, +// which is what the control asks for) is redirected: the user-writable file is never run as code; a request that loads code is left alone. +// Only when DynaRun is not elevated (elevated, DynaRun's own copy works), and only for this one file name: the path is built here from +// the profile and the language value, checked (regular file, no reparse point below the profile) and never taken from the request. +// DYNAFIX_PEGRC=0 switches it off. The imports of the control's two modules are patched from the loader's notification (before the +// control's entry point runs: it asks for the file while loading), and from peg_check, which looks for them as DynaRun's windows come. +static BOOL g_pegrc = TRUE; +static int g_pegel = -1; // 1 elevated, 0 not, set once +static FARPROC r_pLLxA, r_pLLxW; // the control's LoadLibraryEx imports +static HMODULE g_pegmod[2]; +static BOOL g_pegmsg; + +static BOOL peg_elevated(void) +{ + if (g_pegel < 0) { + HANDLE t; DWORD el = 0, n = 0; BOOL ok = FALSE; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &t)) { + ok = GetTokenInformation(t, (TOKEN_INFORMATION_CLASS)20 /* TokenElevation */, &el, sizeof(el), &n); + CloseHandle(t); + } + // Before Vista there is no UAC and no TokenElevation: not "elevated". On Vista and later a failed query counts as elevated + // (no redirect): the safe side. + if (ok) g_pegel = el ? 1 : 0; + else { +#pragma warning(suppress: 4996) + DWORD ver = GetVersion(); // only the major version is needed (before Vista = 5 or less) + g_pegel = LOBYTE(LOWORD(ver)) < 6 ? 0 : 1; + } + } + return g_pegel == 1; +} + +typedef HRESULT (WINAPI *gfpw_t)(HWND, int, HANDLE, DWORD, LPWSTR); + +// Fills path with the validated per-user file, or returns FALSE (reason in *why). +static BOOL peg_path(WCHAR *path, const char **why) +{ + static const WCHAR tail[] = L"\\Dyna Pro Dynamometers\\Dyna Run V3\\Language\\"; + WCHAR prof[MAX_PATH], app[MAX_PATH], lang[40], tmp[MAX_PATH], root[MAX_PATH]; DWORD t = 0, n = sizeof(lang) - 2; HKEY k; HMODULE sh; gfpw_t gfp; + int i, rl, bad = 0; DWORD a; + *why = "no profile folder"; + sh = GetModuleHandleA("shell32.dll"); + gfp = sh ? (gfpw_t)GetProcAddress(sh, "SHGetFolderPathW") : NULL; + if (!gfp || gfp(NULL, 0x001a /* CSIDL_APPDATA */, NULL, 0, app) != S_OK || gfp(NULL, 0x0028 /* CSIDL_PROFILE */, NULL, 0, prof) != S_OK) return FALSE; + *why = "no Default_Language"; + lang[0] = 0; + if (RegOpenKeyExW(HKEY_CURRENT_USER, L"" LANG_KEY, 0, KEY_QUERY_VALUE, &k)) return FALSE; + if (RegQueryValueExW(k, L"Default_Language", NULL, &t, (BYTE *)lang, &n) || t != REG_SZ) lang[0] = 0; + RegCloseKey(k); + lang[n / 2 < 39 ? n / 2 : 39] = 0; + if (!lang[0]) return FALSE; + for (i = 0; lang[i]; i++) // a folder name, nothing else + if (!((lang[i] >= L'A' && lang[i] <= L'Z') || (lang[i] >= L'a' && lang[i] <= L'z') || (lang[i] >= L'0' && lang[i] <= L'9') || lang[i] == L'_' || lang[i] == L' ' || lang[i] == L'-')) bad = 1; + *why = "Default_Language is not a plain folder name"; + if (bad) return FALSE; + *why = "path too long"; + if (lstrlenW(app) + lstrlenW(tail) + lstrlenW(lang) + 14 >= MAX_PATH) return FALSE; + lstrcpyW(tmp, app); lstrcatW(tmp, tail); lstrcatW(tmp, lang); lstrcatW(tmp, L"\\PEGRC32D.dll"); + *why = "path not canonical"; + if (!GetFullPathNameW(tmp, MAX_PATH, path, NULL) || lstrcmpiW(tmp, path)) return FALSE; + // reparse points are checked below the profile (or below AppData when that lies elsewhere) + rl = lstrlenW(prof); + if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, rl, prof, rl) == CSTR_EQUAL && path[rl] == L'\\') lstrcpynW(root, prof, MAX_PATH); + else lstrcpynW(root, app, MAX_PATH); + rl = lstrlenW(root); + *why = "a folder on the way is missing or a reparse point"; + for (i = rl + 1; path[i]; i++) { + if (path[i] != L'\\') continue; + lstrcpynW(tmp, path, i + 1); + a = GetFileAttributesW(tmp); + if (a == INVALID_FILE_ATTRIBUTES || !(a & FILE_ATTRIBUTE_DIRECTORY) || (a & FILE_ATTRIBUTE_REPARSE_POINT)) return FALSE; + } + *why = "file missing, not a regular file, or a reparse point"; + a = GetFileAttributesW(path); + if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return FALSE; + *why = ""; + return TRUE; +} + +// The request is for PEGRC32D.dll by bare name or with the system folder's path. +static BOOL peg_name(const WCHAR *n) +{ + WCHAR sys[MAX_PATH + 16]; int l; + if (!n || (UINT_PTR)n < 0x10000) return FALSE; + if (!lstrcmpiW(n, L"PEGRC32D.dll")) return TRUE; + l = lstrlenW(n); + if (l < 14 || lstrcmpiW(n + l - 13, L"\\PEGRC32D.dll")) return FALSE; + { // the same file name behind a path: only the system folders (what the control would find by itself) + typedef UINT (WINAPI *gsw_t)(LPWSTR, UINT); + gsw_t g = (gsw_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetSystemWow64DirectoryW"); + int k; + for (k = 0; k < 2; k++) { + UINT m = k == 0 ? GetSystemDirectoryW(sys, MAX_PATH) : (g ? g(sys, MAX_PATH) : 0); + if (m && m < MAX_PATH && l == (int)m + 13 && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, n, m, sys, m) == CSTR_EQUAL) return TRUE; + } + } + return FALSE; +} + +// The load of PEGRC32D.dll asked as (api, name, flags): the handle of the per-user file, or NULL to go on with the original call. +static HMODULE peg_load(const char *api, const WCHAR *name, DWORD flags) +{ + WCHAR path[MAX_PATH]; const char *why = ""; char u[MAX_PATH * 2], msg[MAX_PATH * 3 + 200]; HMODULE h; + typedef HMODULE (WINAPI *llx_t)(LPCWSTR, HANDLE, DWORD); + w2u(u, sizeof(u), name); + if (peg_elevated()) { + if (!g_pegmsg) { g_pegmsg = TRUE; wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): elevated, left alone (DynaRun's own copy works)", api, u, flags); logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); } + return NULL; + } + if (!(flags & (0x00000002 /* AS_DATAFILE */ | 0x00000020 /* AS_IMAGE_RESOURCE */ | 0x00000040 /* AS_DATAFILE_EXCLUSIVE */))) { + wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): not redirected, the caller loads code, not just resources", api, u, flags); + logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); + return NULL; + } + if (!peg_path(path, &why)) { + wsprintfA(msg, "PEGRC32D: %s('%s', flags %08X): not redirected, %s", api, u, flags, why); + logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); + return NULL; + } + // The per-user file is only ever loaded as a resource file, whatever the caller asked for. + h = ((llx_t)r_pLLxW)(path, NULL, 0x00000002 /* LOAD_LIBRARY_AS_DATAFILE */ | 0x00000020 /* LOAD_LIBRARY_AS_IMAGE_RESOURCE */); + w2u(u, sizeof(u), path); + wsprintfA(msg, "PEGRC32D: %s redirected to '%s' (asked flags %08X, loaded as a resource file only): %s", api, u, flags, h ? "loaded" : "failed, the original call goes on"); + logf("%s\r\n", (DWORD)(UINT_PTR)msg, 0, 0); + return h; +} + +static HMODULE WINAPI H_pLLxW(LPCWSTR n, HANDLE f, DWORD fl) +{ + HMODULE h = NULL; + if (g_pegrc && !f && peg_name(n)) h = peg_load("LoadLibraryExW", n, fl); + return h ? h : ((HMODULE (WINAPI *)(LPCWSTR, HANDLE, DWORD))r_pLLxW)(n, f, fl); +} +static HMODULE peg_a(const char *api, LPCSTR n, DWORD fl) +{ + WCHAR w[MAX_PATH]; + if (!g_pegrc || !n || (UINT_PTR)n < 0x10000 || !MultiByteToWideChar(CP_ACP, 0, n, -1, w, MAX_PATH) || !peg_name(w)) return NULL; + return peg_load(api, w, fl); +} +static HMODULE WINAPI H_pLLxA(LPCSTR n, HANDLE f, DWORD fl) +{ + HMODULE h = f ? NULL : peg_a("LoadLibraryExA", n, fl); + return h ? h : ((HMODULE (WINAPI *)(LPCSTR, HANDLE, DWORD))r_pLLxA)(n, f, fl); +} + +static hook_t g_peg_hooks[] = { + { "LoadLibraryExA", &r_pLLxA, (FARPROC)H_pLLxA, NULL, "kernel32" }, + { "LoadLibraryExW", &r_pLLxW, (FARPROC)H_pLLxW, NULL, "kernel32" }, + { 0 } +}; + +static const char *g_pegmods[2] = { "Pesgo32e.ocx", "PEGRP32E.dll" }; + +// The control may ask for the file while it is being loaded (its entry point), i.e. before the load call returns: the loader's +// notification for the module comes before that, so the control's imports are patched from there too (nothing but plain memory +// and the log are touched; the log is written with the names already in hand). +typedef struct { USHORT Length, MaximumLength; PWSTR Buffer; } peg_us; +static volatile LONG g_pegready; +static BOOL g_pegnote; +static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) +{ + WCHAR n[32]; const peg_us *u; int i, k; + (void)ctx; + if (reason != 1 /* LOADED */ || !d || !g_pegready) return; + u = (const peg_us *)d->BaseDllName; + if (!u || !u->Buffer || u->Length / 2 >= 32) return; + for (i = 0; i < (int)(u->Length / 2); i++) n[i] = u->Buffer[i]; + n[i] = 0; + for (k = 0; k < 2; k++) { + WCHAR w[32]; int j; + for (j = 0; g_pegmods[k][j]; j++) w[j] = (WCHAR)g_pegmods[k][j]; + w[j] = 0; + if (!lstrcmpiW(n, w)) { + logf("PEGRC32D: loader notification for %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[k], 0, 0); + patch_base((BYTE *)d->DllBase, g_pegmods[k], "peg-note", g_peg_hooks); + } + } +} + +// Patch the control's modules once they are loaded (also the fallback when no loader notifications exist); idempotent. +static void peg_check(void) +{ + static BOOL resolved, off; static volatile LONG busy; + int i; + if (!g_pegrc || off || !GetModuleHandleA("MSVBVM60.DLL")) return; + if (InterlockedCompareExchange(&busy, 1, 0)) return; // not from inside our own patching + if (!resolved) { + if (peg_elevated()) { off = TRUE; logf("PEGRC32D: DynaRun is elevated: no redirect %u %u %u\r\n", 0, 0, 0); InterlockedExchange(&busy, 0); return; } + resolved = TRUE; resolve(g_peg_hooks); + { // see peg_ldr_note + HMODULE nt = GetModuleHandleA("ntdll.dll"); ldrreg_t rg = nt ? (ldrreg_t)GetProcAddress(nt, "LdrRegisterDllNotification") : NULL; PVOID ck; + if (rg && !g_pegnote) { g_pegnote = TRUE; InterlockedExchange(&g_pegready, 1); if (rg(0, peg_ldr_note, NULL, &ck)) InterlockedExchange(&g_pegready, 0); } + } + } + for (i = 0; i < 2; i++) { + HMODULE m = GetModuleHandleA(g_pegmods[i]); + if (m && m != g_pegmod[i]) { + g_pegmod[i] = m; + logf("PEGRC32D: patching the LoadLibrary imports of %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[i], 0, 0); + patch_base((BYTE *)m, g_pegmods[i], "peg", g_peg_hooks); + } else if (!m) g_pegmod[i] = NULL; + } + InterlockedExchange(&busy, 0); +} + +// The launcher starts DynaRun with "RunAsInvoker" appended to __COMPAT_LAYER (so that DynaRun itself never runs elevated). That +// environment variable is inherited by DynaRun's own child processes, e.g. its first-start Setup helper, for which Windows would +// otherwise ask for elevation; so here the word is taken out again (what was there before stays; the variable is removed when +// nothing else is left). The launcher process itself (which loads this dll too) is left alone. +// no CRT in this DLL: stop newer MSVC from turning the copy loops below into memcpy calls +#pragma optimize("", off) +static void clear_runasinvoker(void) +{ + char exe[MAX_PATH], v[512], o[512], msg[400], *p, *q; DWORD n; + n = GetModuleFileNameA(NULL, exe, MAX_PATH); + if (n && n < MAX_PATH) { + for (p = exe + n; p > exe && p[-1] != '\\'; p--); + if (!lstrcmpiA(p, "DynaRunFix.exe")) return; + } + n = GetEnvironmentVariableA("__COMPAT_LAYER", v, sizeof(v)); + if (!n || n >= sizeof(v)) return; + o[0] = 0; q = o; p = v; + while (*p) { + char *e; int len, keep; char tok[16]; + while (*p == ' ') p++; + if (!*p) break; + for (e = p; *e && *e != ' '; e++); + len = (int)(e - p); + keep = 1; + if (len == 12) { int c; for (c = 0; c < 12; c++) tok[c] = p[c]; tok[12] = 0; keep = lstrcmpiA(tok, "RunAsInvoker") != 0; } + if (keep) { + if (q != o) *q++ = ' '; + while (p < e) *q++ = *p++; + } + p = e; + } + *q = 0; + if (lstrcmpA(o, v)) { + SetEnvironmentVariableA("__COMPAT_LAYER", o[0] ? o : NULL); + wsprintfA(msg, "RunAsInvoker taken out of __COMPAT_LAYER for DynaRun's child processes (left: '%.200s')\r\n", o); + logf("%s", (DWORD)(UINT_PTR)msg, 0, 0); + } +} +#pragma optimize("", on) diff --git a/src/dynafix_screen.inc.c b/src/dynafix_screen.inc.c new file mode 100644 index 0000000..365833d --- /dev/null +++ b/src/dynafix_screen.inc.c @@ -0,0 +1,72 @@ +// dynafix_screen.inc.c - work area of a monitor and the patch of the VB6 runtime's screen size values. +// Included by dynafix.c (a single compilation unit, also pulled in by tests\logtest.c); not compiled on its own. + +// Work area of monitor m (and its full rect), less 2 px on every edge that has an auto-hide taskbar. +static BOOL work_area(HMONITOR m, RECT *wa, RECT *mon) +{ + static SHAppBarMessage_t sab; static BOOL tried; + MONITORINFO mi; APPBARDATA ab; UINT e; + mi.cbSize = sizeof(mi); + if (!m || !GetMonitorInfoA(m, &mi)) return FALSE; + *wa = mi.rcWork; *mon = mi.rcMonitor; + if (!tried) { HMODULE s = LoadLibraryA("shell32.dll"); tried = TRUE; if (s) sab = (SHAppBarMessage_t)GetProcAddress(s, "SHAppBarMessage"); } + if (!sab) return TRUE; + for (e = ABE_LEFT; e <= ABE_BOTTOM; e++) { + HWND bar; + SecureZeroMemory(&ab, sizeof(ab)); ab.cbSize = sizeof(ab); ab.uEdge = e; ab.rc = mi.rcMonitor; + bar = (HWND)sab(ABM_GETAUTOHIDEBAREX, &ab); // Vista and later: per monitor + if (!bar && (mi.dwFlags & MONITORINFOF_PRIMARY)) { // XP: primary monitor only + SecureZeroMemory(&ab, sizeof(ab)); ab.cbSize = sizeof(ab); ab.uEdge = e; + bar = (HWND)sab(ABM_GETAUTOHIDEBAR, &ab); + } + if (!bar) continue; + if (e == ABE_LEFT && wa->left - mon->left < 2) wa->left = mon->left + 2; + if (e == ABE_TOP && wa->top - mon->top < 2) wa->top = mon->top + 2; + if (e == ABE_RIGHT && mon->right - wa->right < 2) wa->right = mon->right - 2; + if (e == ABE_BOTTOM && mon->bottom - wa->bottom < 2) wa->bottom = mon->bottom - 2; + } + return TRUE; +} + +// This adjusts the VB6 runtime's in-memory screen size values (the ones behind Screen.Width / Height) to the primary monitor's +// work area, so that DynaRun lays its dashboard out for that area. The runtime (MSVBVM60.DLL, Microsoft's) fills them from the +// screen size at start-up, before dynafix is loaded. The values are located in the loaded runtime and changed only if they equal +// the real screen size; otherwise they are left alone. Nothing is written to disk. +typedef struct { BYTE *base; const RECT *wa; int scx, scy; } vbwalk_t; +static void patch_vb_screen_walk(BYTE *base, const RECT *wa, int scx, int scy); +static void __stdcall walk_vb(void *a) { vbwalk_t *w = (vbwalk_t *)a; patch_vb_screen_walk(w->base, w->wa, w->scx, w->scy); } + +static void patch_vb_screen_walk(BYTE *base, const RECT *wa, int scx, int scy) +{ + IMAGE_NT_HEADERS *nt; IMAGE_SECTION_HEADER *sec; WORD i; + if (!(nt = nt_of(base)) || nt->FileHeader.NumberOfSections > 96) return; + sec = IMAGE_FIRST_SECTION(nt); + for (i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) { + BYTE *c, *e; + if (!(sec->Characteristics & IMAGE_SCN_MEM_EXECUTE)) continue; + if (sec->Misc.VirtualSize < 24 || sec->VirtualAddress >= nt->OptionalHeader.SizeOfImage + || sec->Misc.VirtualSize > nt->OptionalHeader.SizeOfImage - sec->VirtualAddress) continue; + c = base + sec->VirtualAddress; + e = c + sec->Misc.VirtualSize - 24; + for (; c < e; c++) { + int *px, *py; DWORD old; + if (c[0] != 0x6A || c[1] != 0x00 || c[2] != 0xA3 || c[7] != 0xFF || c[9] != 0x6A || c[10] != 0x01 || c[11] != 0xA3 + || c[16] != 0xFF || c[18] != 0x6A || c[19] != 0x20 || c[20] != 0xA3) continue; + px = *(int **)(c + 12); py = *(int **)(c + 21); + if ((BYTE *)px < base || (BYTE *)px >= base + nt->OptionalHeader.SizeOfImage + || (BYTE *)py < base || (BYTE *)py >= base + nt->OptionalHeader.SizeOfImage) continue; + if (*px != scx || *py != scy) { + logf("VB's screen size %ux%u is not the screen size, left alone %u\r\n", *px, *py, 0); + return; + } + VirtualProtect(px, sizeof(int), PAGE_READWRITE, &old); *px = wa->right - wa->left; VirtualProtect(px, sizeof(int), old, &old); + VirtualProtect(py, sizeof(int), PAGE_READWRITE, &old); *py = wa->bottom - wa->top; VirtualProtect(py, sizeof(int), old, &old); + { + char t[60]; wsprintfA(t, "%ux%u -> %ux%u", scx, scy, *px, *py); + logf("screen-size values adjusted (work area): %s %u %u\r\n", (DWORD)(UINT_PTR)t, 0, 0); + } + return; + } + } + logf("VB's screen size not found in MSVBVM60.DLL %u %u %u\r\n", 0, 0, 0); +} diff --git a/src/dynafix_tables.inc.c b/src/dynafix_tables.inc.c new file mode 100644 index 0000000..5df9d71 --- /dev/null +++ b/src/dynafix_tables.inc.c @@ -0,0 +1,32 @@ +// dynafix_tables.inc.c - static data tables of dynafix.c: optional-feature names and registry values, swapped font faces, font-import modules. +// Included by dynafix.c (a single compilation unit, also pulled in by tests\logtest.c); not compiled on its own. + +static const char *g_optname[N_OPT] = { "auto climate", "water cooler", "cooling fans", "AFR" }; +static const char *g_optval[N_OPT][3] = { + { "Auto_Climate_Enable", "Property_Auto_Climate", 0 }, + { "Water_Cooler_Enable", 0 }, + { "Cooling_Fan_Enable", 0 }, + { "Int_AF_Ratio_Enable", 0 }, +}; + +// Faces that are swapped, with their line height (ascent + descent) in 1/1000 em. +static const struct { const WCHAR *face; short line; } g_swap[] = { + { L"Arial", 1117 }, { L"Times New Roman", 1107 }, { L"MS Sans Serif", 1180 }, { L"Microsoft Sans Serif", 1150 }, + { L"MS Shell Dlg", 1150 }, { L"MS Shell Dlg 2", 1207 }, { L"MS Serif", 1150 }, { L"Tahoma", 1207 }, + { L"Verdana", 1215 }, { L"Segoe UI", 1330 }, + { L"\x65B0\x7D30\x660E\x9AD4", 1000 }, { L"\x7D30\x660E\x9AD4", 1000 }, // 新細明體 細明體 + { L"PMingLiU", 1000 }, { L"MingLiU", 1000 }, { L"Microsoft JhengHei", 1250 }, { L"Microsoft JhengHei UI", 1270 }, + { L"SimSun", 1000 }, { L"\x5B8B\x4F53", 1000 }, { L"NSimSun", 1000 }, // 宋体 + { L"Microsoft YaHei", 1320 }, { L"Microsoft YaHei UI", 1320 }, + { L"MS UI Gothic", 1000 }, { L"MS PGothic", 1000 }, { L"\xFF2D\xFF33 \xFF30\x30B4\x30B7\x30C3\x30AF", 1000 }, // MS Pゴシック + { L"Meiryo UI", 1300 }, { L"Yu Gothic UI", 1330 }, + { L"Gulim", 1000 }, { L"\xAD74\xB9BC", 1000 }, { L"Dotum", 1000 }, { L"\xB3CB\xC6C0", 1000 }, // 굴림 돋움 + { L"Malgun Gothic", 1330 }, { 0 } +}; + +// Modules whose font imports are redirected: those in DynaRun's folder, OCX controls wherever they are, +// and these. Modules load later (an OCX with the first form that uses it), so the module list is checked +// again whenever a window is created. Not this dll's folder (Locale Emulator's dlls are there). +static const char *g_fontmods[] = { + "MSVBVM60.DLL", "ole32.dll", "oleaut32.dll", "MFC42.DLL", "comctl32.dll", "THBRes25.dll", "PEGRP32E.dll", 0 +}; From e0010585dd789e97084cb1f305abead67db250dd Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:28:04 +0800 Subject: [PATCH 02/41] Share the log lock, clock and update value names of launcher and updater (applog.h) Header-only src/applog.h (static functions, build.cmd unchanged): make_sa and its SDDL, the dynafix.log mutex and append, now() and DAY, the HKCU Update* value names and the launcher <-> setup command-line verbs. launcher.c and update.c use it. Mutex name, line format, trim threshold and line length limits are unchanged; only the launcher trims, as before. dynafix.dll keeps its own copy. Co-Authored-By: Claude Opus 5.5 --- src/applog.h | 108 ++++++++++++++++++++++++++++++++++++++++++++++++ src/launcher.c | 110 +++++++++++-------------------------------------- src/update.c | 87 +++++++++----------------------------- 3 files changed, 151 insertions(+), 154 deletions(-) create mode 100644 src/applog.h diff --git a/src/applog.h b/src/applog.h new file mode 100644 index 0000000..315609a --- /dev/null +++ b/src/applog.h @@ -0,0 +1,108 @@ +// What the launcher (launcher.c) and the updater (update.c, in the setup) share: the lock and the append of %TEMP%\dynafix.log, +// the security attributes of the objects shared with dynafix.dll, the clock, the registry value names of the update and the +// command-line verbs the launcher and the setup pass each other. Header-only (static functions, one copy per program): the two +// are built as separate programs and build.cmd stays as it is. dynafix.dll keeps its own copy of the log lock (built apart). +// Everything here crosses process boundaries: the mutex name, the value names and the verbs must not change. +#ifndef DRF_APPLOG_H +#define DRF_APPLOG_H +#include +#include + +#ifndef WIDEN +#define WIDEN2(x) L##x +#define WIDEN(x) WIDEN2(x) +#endif + +#define DAY 864000000000ULL // FILETIME units (100 ns) + +/* ---------- HKCU\Software\DynaRunFix ---------- */ +#define APP_KEY "Software\\DynaRunFix" +#define RV_UPDATE_VERSION "UpdateVersion" // REG_SZ: tag of a newer release (noted by /checkupdate) +#define RV_UPDATE_SHA256 "UpdateSha256" // REG_SZ: the SHA-256 GitHub lists for its DynaRunFix-Setup.exe +#define RV_UPDATE_CHECKED "UpdateChecked" // REG_QWORD: when GitHub last answered (once a day at most) +#define RV_UPDATE_ASK "UpdateAsk" // REG_QWORD: not offered before this time +#define RV_UPDATE_SKIP "UpdateSkip" // REG_SZ: the tag the user chose to skip +#define RV_UPDATE_ETAG "UpdateETag" // the download's ETag (pkg_fetch) + +/* ---------- command-line verbs between the launcher and the setup ---------- */ +#define VERB_CHECKUPDATE "/checkupdate" // launcher -> setup: look for a newer release, no UI +#define VERB_OFFER "/offer" // launcher -> setup: offer the noted release beside the running DynaRun +#define VERB_UPDATE "/update" // older launcher -> setup: ask, install, start the launcher with /noupdate +#define VERB_NOUPDATE "/noupdate" // setup -> launcher: do not deal with an update in this start +#define VERB_AUTOSTART "/autostart" // sign-in Run value -> launcher +#define VERB_RESTART "/restart" // dynafix.dll -> launcher: "/restart " +#define VERB_WASADMIN "/wasadmin" // elevated launcher -> its non-elevated copy + +static ULONGLONG now(void) +{ + FILETIME f; ULARGE_INTEGER u; + GetSystemTimeAsFileTime(&f); + u.LowPart = f.dwLowDateTime; u.HighPart = f.dwHighDateTime; + return u.QuadPart; +} + +// Security attributes for the objects shared with dynafix.dll (the log mutex, the ready event): full access for the current +// user, SYSTEM and Administrators only (no NULL DACL: every other user and any low-integrity process could otherwise open them). +// Free *psd with LocalFree. FALSE: no explicit DACL could be built, the caller then takes the default one. +static BOOL make_sa(SECURITY_ATTRIBUTES *sa, PSECURITY_DESCRIPTOR *psd) +{ + HANDLE tk; DWORD buf[64], n = 0; char *sid = NULL, sddl[200]; BOOL ok = FALSE; + *psd = NULL; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk)) return FALSE; + if (GetTokenInformation(tk, TokenUser, buf, sizeof(buf), &n) && ConvertSidToStringSidA(((TOKEN_USER *)buf)->User.Sid, &sid)) { + if (lstrlenA(sid) < 120) { + wsprintfA(sddl, "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;%s)", sid); + ok = ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, psd, NULL); + } + LocalFree(sid); + } + CloseHandle(tk); + if (ok) { sa->nLength = sizeof(*sa); sa->lpSecurityDescriptor = *psd; sa->bInheritHandle = FALSE; } + return ok; +} + +// dynafix.dll, the launcher and the updater all append to dynafix.log (and any number of threads in each). One named mutex +// ("Local\DynaRunFix.dynafix.log", DACL: current user, SYSTEM, Administrators) is held around every append and around the +// trim. It waits at most 2 s: a line is then written anyway (an append is atomic by itself), but the log is never trimmed +// without the lock. If the mutex cannot be created there is no locking at all. +static HANDLE g_logmtx; + +static HANDLE log_lock(void) +{ + HANDLE m = g_logmtx; DWORD r; + if (!m) { + SECURITY_ATTRIBUTES sa; PSECURITY_DESCRIPTOR sd; HANDLE n; BOOL have = make_sa(&sa, &sd); + n = CreateMutexA(have ? &sa : NULL, FALSE, "Local\\DynaRunFix.dynafix.log"); + if (sd) LocalFree(sd); + if (!n) n = INVALID_HANDLE_VALUE; + m = (HANDLE)InterlockedCompareExchangePointer((PVOID volatile *)&g_logmtx, n, NULL); // lost the race: use the winner's + if (m) { if (n != INVALID_HANDLE_VALUE) CloseHandle(n); } else m = n; + } + if (m == INVALID_HANDLE_VALUE) return NULL; + r = WaitForSingleObject(m, 2000); + return r == WAIT_OBJECT_0 || r == WAIT_ABANDONED ? m : NULL; // an abandoned mutex is ours now; the log itself is fine +} + +// Opens the log. A reparse point (symbolic link, junction) is never followed: INVALID_HANDLE_VALUE then. no_links: also refuse +// a file with several hard links (the launcher when elevated). +static HANDLE log_open(const char *path, DWORD access, DWORD disp, BOOL no_links) +{ + HANDLE h = CreateFileA(path, access | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, disp, FILE_FLAG_OPEN_REPARSE_POINT, NULL); + BY_HANDLE_FILE_INFORMATION bi; + if (h == INVALID_HANDLE_VALUE) return h; + if (!GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) || + (no_links && bi.nNumberOfLinks > 1)) { CloseHandle(h); return INVALID_HANDLE_VALUE; } + return h; +} + +// Appends n chars of line (a whole line, with its "\r\n") to the log at path, under the log lock: one WriteFile per line, so it +// is appended atomically. +static void log_append(const char *path, const char *line, DWORD n, BOOL no_links) +{ + HANDLE h, m = log_lock(); + h = log_open(path, FILE_APPEND_DATA, OPEN_ALWAYS, no_links); + if (h != INVALID_HANDLE_VALUE) { WriteFile(h, line, n, &n, NULL); CloseHandle(h); } + if (m) ReleaseMutex(m); +} + +#endif diff --git a/src/launcher.c b/src/launcher.c index 0271af7..7f04809 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -2,7 +2,7 @@ // Usage: DynaRunFix.exe ["path\to\DynaRun V3.exe"] #include #include -#include +#include "applog.h" #include "version.h" #define DEFAULT_EXE "C:\\Program Files (x86)\\Dyna Pro Dynamometers\\DynaRun V3.exe" @@ -95,27 +95,7 @@ static ULONGLONG tick64(void) return ((ULONGLONG)hi << 32) | t; } -// Security attributes for the objects shared with dynafix.dll (the log mutex, the ready event): full access for the current -// user, SYSTEM and Administrators only (no NULL DACL: every other user and any low-integrity process could otherwise open them). -// Free *psd with LocalFree. FALSE: no explicit DACL could be built, the caller then takes the default one. -static BOOL make_sa(SECURITY_ATTRIBUTES *sa, PSECURITY_DESCRIPTOR *psd) -{ - HANDLE tk; DWORD buf[64], n = 0; char *sid = NULL, sddl[200]; BOOL ok = FALSE; - *psd = NULL; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk)) return FALSE; - if (GetTokenInformation(tk, TokenUser, buf, sizeof(buf), &n) && ConvertSidToStringSidA(((TOKEN_USER *)buf)->User.Sid, &sid)) { - if (lstrlenA(sid) < 120) { - wsprintfA(sddl, "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;%s)", sid); - ok = ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, psd, NULL); - } - LocalFree(sid); - } - CloseHandle(tk); - if (ok) { sa->nLength = sizeof(*sa); sa->lpSecurityDescriptor = *psd; sa->bInheritHandle = FALSE; } - return ok; -} - -// Local\dynafix_ready_: the dll signals it when it is in place in DynaRun (explicit DACL, see make_sa) +// Local\dynafix_ready_: the dll signals it when it is in place in DynaRun (explicit DACL, see make_sa in applog.h) static HANDLE ready_event(DWORD pid) { char ev[64]; SECURITY_ATTRIBUTES sa; PSECURITY_DESCRIPTOR sd; HANDLE e; BOOL have; @@ -129,28 +109,6 @@ static HANDLE ready_event(DWORD pid) static BOOL g_elevated; // this launcher process runs with an elevated token (normally only on the way to the relaunch; also true in the // relaunched copy when the desktop shell itself is elevated) -// dynafix.dll and the launcher both append to dynafix.log (and any number of threads in each). One named mutex -// ("Local\DynaRunFix.dynafix.log", DACL: current user, SYSTEM, Administrators) is held around every append and around the -// trim. It waits at most 2 s: a line is then written anyway (an append is atomic by itself), but the log is never trimmed -// without the lock. If the mutex cannot be created there is no locking at all. -static HANDLE g_logmtx; - -static HANDLE log_lock(void) -{ - HANDLE m = g_logmtx; DWORD r; - if (!m) { - SECURITY_ATTRIBUTES sa; PSECURITY_DESCRIPTOR sd; HANDLE n; BOOL have = make_sa(&sa, &sd); - n = CreateMutexA(have ? &sa : NULL, FALSE, "Local\\DynaRunFix.dynafix.log"); - if (sd) LocalFree(sd); - if (!n) n = INVALID_HANDLE_VALUE; - m = (HANDLE)InterlockedCompareExchangePointer((PVOID volatile *)&g_logmtx, n, NULL); // lost the race: use the winner's - if (m) { if (n != INVALID_HANDLE_VALUE) CloseHandle(n); } else m = n; - } - if (m == INVALID_HANDLE_VALUE) return NULL; - r = WaitForSingleObject(m, 2000); - return r == WAIT_OBJECT_0 || r == WAIT_ABANDONED ? m : NULL; // an abandoned mutex is ours now; the log itself is fine -} - // %TEMP%\dynafix.log into path (MAX_PATH). FALSE when TEMP is too long for that (or unusable): then nothing is logged. static BOOL log_path(char *path) { @@ -161,17 +119,8 @@ static BOOL log_path(char *path) return TRUE; } -// Opens the log. A reparse point (symbolic link, junction) is never followed: INVALID_HANDLE_VALUE then. An elevated process -// also refuses a file with several hard links. -static HANDLE open_log(const char *path, DWORD access, DWORD disp) -{ - HANDLE h = CreateFileA(path, access | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, disp, FILE_FLAG_OPEN_REPARSE_POINT, NULL); - BY_HANDLE_FILE_INFORMATION bi; - if (h == INVALID_HANDLE_VALUE) return h; - if (!GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) || - (g_elevated && bi.nNumberOfLinks > 1)) { CloseHandle(h); return INVALID_HANDLE_VALUE; } - return h; -} +// Opens the log (log_open, applog.h); an elevated process also refuses a file with several hard links. +static HANDLE open_log(const char *path, DWORD access, DWORD disp) { return log_open(path, access, disp, g_elevated); } // above 10 MB, dynafix.log is cut to its newest 8 MB, from a full line on (dynafix.dll does the same; whichever runs first). // Never when elevated. @@ -203,17 +152,14 @@ static void trim_log(void) // one line in %TEMP%\dynafix.log, next to what dynafix.dll writes from inside DynaRun static void llog(const char *fmt, DWORD a, DWORD b) { - char path[MAX_PATH], line[LOG_MAX + 100]; DWORD n; HANDLE h, m; SYSTEMTIME t; + char path[MAX_PATH], line[LOG_MAX + 100]; DWORD n; SYSTEMTIME t; if (!log_path(path)) return; GetLocalTime(&t); n = wsprintfA(line, "%04u-%02u-%02u %02u:%02u:%02u.%03u ", t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); wsprintfA(line + n, fmt, a, b); n = lstrlenA(line); if (n > LOG_MAX) { lstrcpyA(line + LOG_MAX - 5, "...\r\n"); n = LOG_MAX; } - m = log_lock(); - h = open_log(path, FILE_APPEND_DATA, OPEN_ALWAYS); // one WriteFile per line: appended atomically - if (h != INVALID_HANDLE_VALUE) { WriteFile(h, line, n, &n, NULL); CloseHandle(h); } - if (m) ReleaseMutex(m); + log_append(path, line, n, g_elevated); // one WriteFile per line: appended atomically } // the log line for this start: how we were called and by whom @@ -226,7 +172,7 @@ static void log_start(const char *cmd) if (Process32First(s, &pe)) do { if (pe.th32ProcessID == me) pp = pe.th32ParentProcessID; } while (!pp && Process32Next(s, &pe)); if (pp && Process32First(s, &pe)) do { if (pe.th32ProcessID == pp) lstrcpynA(parent, pe.szExeFile, sizeof(parent)); } while (lstrcmpA(parent, "?") == 0 && Process32Next(s, &pe)); CloseHandle(s); - { char *c = (char *)cmd; while (prefix(&c, "/noupdate")); if (prefix(&c, "/autostart")) how = "/autostart"; else if (prefix(&c, "/restart")) how = "/restart"; } + { char *c = (char *)cmd; while (prefix(&c, VERB_NOUPDATE)); if (prefix(&c, VERB_AUTOSTART)) how = VERB_AUTOSTART; else if (prefix(&c, VERB_RESTART)) how = VERB_RESTART; } lstrcpynA(args, cmd, sizeof(args)); { volatile char *z = (volatile char *)&st; int i; for (i = 0; i < (int)sizeof(st); i++) z[i] = 0; } st.cb = sizeof(st); GetStartupInfoA(&st); if (!GetCurrentDirectoryA(sizeof(cwd), cwd)) lstrcpyA(cwd, "?"); @@ -413,7 +359,7 @@ static int relaunch_non_elevated(void) while (*rest == ' ' || *rest == '\t') rest++; cmd = (WCHAR *)HeapAlloc(GetProcessHeap(), 0, (lstrlenW(self) + lstrlenW(rest) + 24) * sizeof(WCHAR)); if (!cmd) { CloseHandle(tok); return 0; } - lstrcpyW(cmd, L"\""); lstrcatW(cmd, self); lstrcatW(cmd, L"\" /wasadmin "); lstrcatW(cmd, rest); + lstrcpyW(cmd, L"\""); lstrcatW(cmd, self); lstrcatW(cmd, L"\" " WIDEN(VERB_WASADMIN) L" "); lstrcatW(cmd, rest); env = token_env(tok); { volatile char *z = (volatile char *)&sw; for (i = 0; i < (int)sizeof(sw); i++) z[i] = 0; } sw.cb = sizeof(sw); ok = cpwt(tok, 0, self, cmd, env ? CREATE_UNICODE_ENVIRONMENT : 0, env, NULL, &sw, &pi); @@ -465,7 +411,7 @@ static DWORD WINAPI notice_thread(LPVOID unused) ask = zh ? L"下次還要顯示這個通知嗎?" : L"Show this notice again next time?"; lstrcpyW(msg, head); lstrcatW(msg, body); lstrcatW(msg, ask); if (MessageBoxW(NULL, msg, L"DynaRunFix", MB_YESNO | MB_DEFBUTTON2 | MB_ICONINFORMATION | MB_SETFOREGROUND) == IDNO && - !RegCreateKeyExA(HKEY_CURRENT_USER, "Software\\DynaRunFix", 0, NULL, 0, KEY_SET_VALUE, NULL, &k, NULL)) { + !RegCreateKeyExA(HKEY_CURRENT_USER, APP_KEY, 0, NULL, 0, KEY_SET_VALUE, NULL, &k, NULL)) { DWORD one = 1; RegSetValueExA(k, notice_value(), 0, REG_DWORD, (const BYTE *)&one, 4); RegCloseKey(k); } return 0; @@ -474,7 +420,7 @@ static void show_notice(int kind) { HKEY k; DWORD v = 0, n = 4, id; g_notice_kind = kind; - if (!RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\DynaRunFix", 0, KEY_QUERY_VALUE, &k)) { + if (!RegOpenKeyExA(HKEY_CURRENT_USER, APP_KEY, 0, KEY_QUERY_VALUE, &k)) { if (RegQueryValueExA(k, notice_value(), NULL, NULL, (BYTE *)&v, &n)) v = 0; RegCloseKey(k); } @@ -549,14 +495,6 @@ static void early_hook(HMODULE hd, DWORD pid, DWORD tid, HANDLE proc) if (hd) FreeLibrary(hd); } -static ULONGLONG now(void) -{ - FILETIME f; ULARGE_INTEGER u; - GetSystemTimeAsFileTime(&f); - u.LowPart = f.dwLowDateTime; u.HighPart = f.dwHighDateTime; - return u.QuadPart; -} - static ULONGLONG reg_time(HKEY k, const char *name) { ULONGLONG t = 0; DWORD n = sizeof(t), type; @@ -593,9 +531,9 @@ static BOOL env_on(const char *name) static void check_update(void) { HKEY k; ULONGLONG t = 0, n = now(); - if (!RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\DynaRunFix", 0, KEY_QUERY_VALUE, &k)) { t = reg_time(k, "UpdateChecked"); RegCloseKey(k); } - if (t <= n && n - t < 864000000000ULL) return; - llog("launcher: looking for an update (0 no setup, 1 started): %u %u\r\n", run_setup("/checkupdate", "", 0, "the last check is more than a day old"), 0); + if (!RegOpenKeyExA(HKEY_CURRENT_USER, APP_KEY, 0, KEY_QUERY_VALUE, &k)) { t = reg_time(k, RV_UPDATE_CHECKED); RegCloseKey(k); } + if (t <= n && n - t < DAY) return; + llog("launcher: looking for an update (0 no setup, 1 started): %u %u\r\n", run_setup(VERB_CHECKUPDATE, "", 0, "the last check is more than a day old"), 0); } // DynaRun has been started by this launcher (and hooked): only now is an update dealt with, so that DynaRun never waits for @@ -606,16 +544,16 @@ static void update_after_start(void) { char v[64], s[64]; DWORD n = sizeof(v), m = sizeof(s), type; HKEY k; BOOL has, skipped; ULONGLONG ask, next, t = now(); if (env_on("DYNAFIX_NOUPDATE")) { llog("launcher: update check skipped (DYNAFIX_NOUPDATE) %u %u\r\n", 0, 0); return; } - if (RegOpenKeyExA(HKEY_CURRENT_USER, "Software\\DynaRunFix", 0, KEY_QUERY_VALUE | KEY_SET_VALUE, &k)) { check_update(); return; } - has = !RegQueryValueExA(k, "UpdateVersion", NULL, &type, (BYTE *)v, &n) && type == REG_SZ && n > 1; - skipped = has && !RegQueryValueExA(k, "UpdateSkip", NULL, &type, (BYTE *)s, &m) && type == REG_SZ && !lstrcmpA(s, v); - ask = reg_time(k, "UpdateAsk"); + if (RegOpenKeyExA(HKEY_CURRENT_USER, APP_KEY, 0, KEY_QUERY_VALUE | KEY_SET_VALUE, &k)) { check_update(); return; } + has = !RegQueryValueExA(k, RV_UPDATE_VERSION, NULL, &type, (BYTE *)v, &n) && type == REG_SZ && n > 1; + skipped = has && !RegQueryValueExA(k, RV_UPDATE_SKIP, NULL, &type, (BYTE *)s, &m) && type == REG_SZ && !lstrcmpA(s, v); + ask = reg_time(k, RV_UPDATE_ASK); if (has && !skipped && t >= ask) { - next = t + 864000000000ULL; - RegSetValueExA(k, "UpdateAsk", 0, REG_QWORD, (const BYTE *)&next, 8); // not again today + next = t + DAY; + RegSetValueExA(k, RV_UPDATE_ASK, 0, REG_QWORD, (const BYTE *)&next, 8); // not again today RegCloseKey(k); llog("launcher: offering the update to %s after DynaRun was started %u\r\n", (DWORD_PTR)v, 0); - run_setup("/offer", "", 0, "a newer release is noted, not skipped, not asked today; DynaRun is already running"); + run_setup(VERB_OFFER, "", 0, "a newer release is noted, not skipped, not asked today; DynaRun is already running"); return; } RegCloseKey(k); @@ -685,7 +623,7 @@ void WinMainCRTStartup(void) while (*cmd == ' ') cmd++; // "/wasadmin": this is the copy an elevated launcher started with the shell's token (see relaunch_non_elevated). Only // believed when this process is not elevated (see below); an elevated process that is given it ignores it. - wasadmin = prefix(&cmd, "/wasadmin"); + wasadmin = prefix(&cmd, VERB_WASADMIN); { // An elevated launcher starts nothing but its copy with the shell's token: no DynaRun, no LEProc, no updater. HANDLE tk; g_elevated = !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk) || (i = token_elevated(tk), CloseHandle(tk), i); @@ -710,15 +648,15 @@ void WinMainCRTStartup(void) trim_log(); log_start(cmd); ask = TRUE; - while (flag(&cmd, "/noupdate")) ask = FALSE; // from the setup's /update: it has just asked + while (flag(&cmd, VERB_NOUPDATE)) ask = FALSE; // from the setup's /update: it has just asked // "/autostart" (the sign-in Run value): at sign-in the desktop is still being set up (taskbar, resolution, DPI) and // DynaRun lays its screen out from the size it sees at start: wait for the taskbar and 3 s of an unchanged size - if (flag(&cmd, "/autostart")) { + if (flag(&cmd, VERB_AUTOSTART)) { ask = FALSE; // an unattended start (sign-in) never goes near the updater if (!running()) wait_desktop(); } // "/restart " (from dynafix.dll after the first-time setup): wait for that DynaRun to end, then start normally - if (prefix(&cmd, "/restart")) { + if (prefix(&cmd, VERB_RESTART)) { DWORD old = 0; HANDLE h0; p = cmd; while (*p >= '0' && *p <= '9') old = old * 10 + (*p++ - '0'); diff --git a/src/update.c b/src/update.c index b8db962..1e9b46b 100644 --- a/src/update.c +++ b/src/update.c @@ -17,19 +17,15 @@ #include #include #include -#include #include "setup.h" +#include "applog.h" #include "version.h" -#define WIDEN2(x) L##x -#define WIDEN(x) WIDEN2(x) - #define API_LATEST L"https://api.github.com/repos/timliudev/DynaRunFix/releases/latest" #define DL_PREFIX "https://github.com/timliudev/DynaRunFix/releases/download/" #define ASSET "DynaRunFix-Setup.exe" -#define DAY 864000000000ULL // FILETIME units (100 ns) -static const WCHAR KEY[] = L"Software\\DynaRunFix"; +static const WCHAR KEY[] = WIDEN(APP_KEY); /* ---------- versions ---------- */ @@ -55,62 +51,17 @@ static BOOL newer(const DWORD a[3], const DWORD b[3]) return FALSE; } -static ULONGLONG now(void) -{ - FILETIME f; ULARGE_INTEGER u; - GetSystemTimeAsFileTime(&f); - u.LowPart = f.dwLowDateTime; u.HighPart = f.dwHighDateTime; - return u.QuadPart; -} - -// The log is shared with the launcher and dynafix.dll: one named mutex ("Local\DynaRunFix.dynafix.log", the same as theirs, open to -// every user so an elevated and a normal process share it) is held around an append; at most 2 s are waited, a line is then -// written anyway (an append is atomic by itself) - only the trim of the log needs the lock for certain. -static HANDLE log_lock(void) -{ - static HANDLE m; DWORD r; - if (!m) { - // explicit DACL (as in the launcher): current user, SYSTEM, Administrators - not a NULL DACL - SECURITY_ATTRIBUTES sa; PSECURITY_DESCRIPTOR sd = NULL; HANDLE tk; DWORD buf[64], n = 0; char *sid = NULL, sddl[200]; BOOL have = FALSE; - if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk)) { - if (GetTokenInformation(tk, TokenUser, buf, sizeof(buf), &n) && ConvertSidToStringSidA(((TOKEN_USER *)buf)->User.Sid, &sid)) { - if (lstrlenA(sid) < 120) { - wsprintfA(sddl, "D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;%s)", sid); - have = ConvertStringSecurityDescriptorToSecurityDescriptorA(sddl, SDDL_REVISION_1, &sd, NULL); - } - LocalFree(sid); - } - CloseHandle(tk); - } - sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = sd; sa.bInheritHandle = FALSE; - m = CreateMutexA(have ? &sa : NULL, FALSE, "Local\\DynaRunFix.dynafix.log"); - if (sd) LocalFree(sd); - if (!m) m = INVALID_HANDLE_VALUE; - } - if (m == INVALID_HANDLE_VALUE) return NULL; - r = WaitForSingleObject(m, 2000); - return r == WAIT_OBJECT_0 || r == WAIT_ABANDONED ? m : NULL; -} - // one line in %TEMP%\dynafix.log, next to the launcher's and dynafix.dll's static void uplog(const char *fmt, const char *a, int b) { - char path[MAX_PATH], line[300]; DWORD n; HANDLE h, m; SYSTEMTIME t; BY_HANDLE_FILE_INFORMATION bi; + char path[MAX_PATH], line[300]; DWORD n; SYSTEMTIME t; n = GetEnvironmentVariableA("TEMP", path, MAX_PATH - 16); if (!n || n >= MAX_PATH - 16) return; // not set, or too long for the buffer (n = the size it needs) lstrcatA(path, "\\dynafix.log"); GetLocalTime(&t); n = wsprintfA(line, "%04d-%02d-%02d %02d:%02d:%02d.%03d update (v" DRF_DISPLAY "): ", t.wYear, t.wMonth, t.wDay, t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); wsprintfA(line + n, fmt, a, b); lstrcatA(line, "\r\n"); - m = log_lock(); - h = CreateFileA(path, FILE_APPEND_DATA | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, NULL); - if (h != INVALID_HANDLE_VALUE) { - // a symbolic link or junction in place of the log is not followed - if (GetFileInformationByHandle(h, &bi) && !(bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) - WriteFile(h, line, lstrlenA(line), &n, NULL); - CloseHandle(h); - } - if (m) ReleaseMutex(m); + log_append(path, line, lstrlenA(line), FALSE); // a symbolic link or junction in place of the log is not followed (applog.h) } static void set_time(HKEY k, const WCHAR *name, ULONGLONG t) { RegSetValueExW(k, name, 0, REG_QWORD, (const BYTE *)&t, 8); } @@ -242,17 +193,17 @@ int update_check(void) if (!(j = g_http_get(API_LATEST, &st))) { uplog("%sno answer from GitHub (%d)", "", (int)st); RegCloseKey(k); return 1; } if (!json_str(j, "tag_name", tag, sizeof(tag)) || !parse_ver(tag, v)) uplog("%sGitHub's answer has no release tag %d", "", 0); else { - set_time(k, L"UpdateChecked", now()); // answered: once a day at most + set_time(k, WIDEN(RV_UPDATE_CHECKED), now()); // answered: once a day at most if (!newer(v, cur)) { uplog("latest is %s: nothing to do %d", tag, 0); - RegDeleteValueW(k, L"UpdateVersion"); RegDeleteValueW(k, L"UpdateSha256"); RegDeleteValueW(k, L"UpdateAsk"); RegDeleteValueW(k, L"UpdateSkip"); + RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SKIP)); } else { if (release_asset(j, tag, hex)) { MultiByteToWideChar(CP_ACP, 0, tag, -1, w, 100); - RegSetValueExW(k, L"UpdateVersion", 0, REG_SZ, (const BYTE *)w, (lstrlenW(w) + 1) * sizeof(WCHAR)); + RegSetValueExW(k, WIDEN(RV_UPDATE_VERSION), 0, REG_SZ, (const BYTE *)w, (lstrlenW(w) + 1) * sizeof(WCHAR)); MultiByteToWideChar(CP_ACP, 0, hex, -1, w, 100); - RegSetValueExW(k, L"UpdateSha256", 0, REG_SZ, (const BYTE *)w, (lstrlenW(w) + 1) * sizeof(WCHAR)); + RegSetValueExW(k, WIDEN(RV_UPDATE_SHA256), 0, REG_SZ, (const BYTE *)w, (lstrlenW(w) + 1) * sizeof(WCHAR)); uplog("%s is newer: noted, asked at the next start of DynaRun %d", tag, 0); } else uplog("%s is newer, but its " ASSET " or its SHA-256 is missing %d", tag, 0); } @@ -338,7 +289,7 @@ static int install(const WCHAR *tag, const WCHAR *sha) wsprintfW(name, L"DynaRunFix-Setup-%s.exe", tag); show_window(); say(T(L"Downloading the update...", L"正在下載更新…")); - rc = pkg_fetch(url, name, L"UpdateETag", GITHUB_HOSTS, out, dl_progress, &cancel, &err); + rc = pkg_fetch(url, name, WIDEN(RV_UPDATE_ETAG), GITHUB_HOSTS, out, dl_progress, &cancel, &err); if (rc != PK_OK) { uplog("%sdownload failed (%d)", "", (int)err); return 100 + rc; } // %TEMP% is writable by every program of this user: the file is opened so that nobody can write to or delete it, hashed through // that handle, and started while the handle is still open (the file that was checked is the file that runs) @@ -360,7 +311,7 @@ static void start_launcher(const WCHAR *args, BOOL updated) { static WCHAR cmd[MAX_PATH * 4]; STARTUPINFOW si; PROCESS_INFORMATION pi; zero(&si, sizeof(si)); si.cb = sizeof(si); - wsprintfW(cmd, L"\"%s\"%s %s", g_launcher, updated ? L"" : L" /noupdate", args); + wsprintfW(cmd, L"\"%s\"%s %s", g_launcher, updated ? L"" : L" " WIDEN(VERB_NOUPDATE), args); if (CreateProcessW(g_launcher, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) { AllowSetForegroundWindow(pi.dwProcessId); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); @@ -373,8 +324,8 @@ int update_run(const WCHAR *args) HANDLE mx = CreateMutexW(NULL, FALSE, L"Local\\DynaRunFix-update"); if (GetLastError() != ERROR_ALREADY_EXISTS && !(running_programs() & RUN_DYNARUN) && !RegOpenKeyExW(HKEY_CURRENT_USER, KEY, 0, KEY_ALL_ACCESS, &k)) { - n = sizeof(tag) - 2; if (RegQueryValueExW(k, L"UpdateVersion", NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; - n = sizeof(sha) - 2; if (RegQueryValueExW(k, L"UpdateSha256", NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; + n = sizeof(tag) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_VERSION), NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; + n = sizeof(sha) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_SHA256), NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; if (tag[0] && lstrlenW(sha) == 64) { wsprintfW(txt, T(L"A new version of DynaRunFix is available: %s (this is v%s).\n\n" L"Update now? It takes about a minute; DynaRun opens afterwards.\n\n" @@ -386,9 +337,9 @@ int update_run(const WCHAR *args) { char a[64]; WideCharToMultiByte(CP_ACP, 0, tag, -1, a, sizeof(a), NULL, NULL); uplog("asked about %s: %d (1 = yes)", a, rc); } if (rc) { rc = install(tag, sha); - if (!rc) { updated = TRUE; RegDeleteValueW(k, L"UpdateVersion"); RegDeleteValueW(k, L"UpdateSha256"); RegDeleteValueW(k, L"UpdateAsk"); } + if (!rc) { updated = TRUE; RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); } } else rc = 2; - if (rc) set_time(k, L"UpdateAsk", now() + DAY); + if (rc) set_time(k, WIDEN(RV_UPDATE_ASK), now() + DAY); if (g_hwnd) { DestroyWindow(g_hwnd); g_hwnd = NULL; } if (rc && rc != 2) { // 2: "No", or the UAC prompt was cancelled (the new setup said so already) wsprintfW(txt, T(L"The update did not finish (error %d). DynaRun opens as usual; the update is tried again tomorrow.", @@ -413,8 +364,8 @@ int update_offer(void) static WCHAR tag[64], sha[80], txt[700]; HKEY k; DWORD n, t0; int rc = 0, ans; HANDLE mx = CreateMutexW(NULL, FALSE, L"Local\\DynaRunFix-update"); if (GetLastError() != ERROR_ALREADY_EXISTS && !RegOpenKeyExW(HKEY_CURRENT_USER, KEY, 0, KEY_ALL_ACCESS, &k)) { - n = sizeof(tag) - 2; if (RegQueryValueExW(k, L"UpdateVersion", NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; - n = sizeof(sha) - 2; if (RegQueryValueExW(k, L"UpdateSha256", NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; + n = sizeof(tag) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_VERSION), NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; + n = sizeof(sha) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_SHA256), NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; // DynaRun is still opening: do not put a window over its start-up for (t0 = GetTickCount(); GetTickCount() - t0 < 10000 && (running_programs() & RUN_DYNARUN); ) Sleep(500); // a deadline, not a count of sleeps if (tag[0] && lstrlenW(sha) == 64 && (running_programs() & RUN_DYNARUN)) { @@ -432,15 +383,15 @@ int update_offer(void) // Cancel and the window's close button (X, Esc) both answer IDCANCEL: they must not skip a version for good, // so they mean "tomorrow" (UpdateAsk is already set by the launcher); the explicit skip is "No" if (ans == IDNO) { - RegSetValueExW(k, L"UpdateSkip", 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); + RegSetValueExW(k, WIDEN(RV_UPDATE_SKIP), 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); } else if (ans == IDYES) { while (running_programs()) Sleep(2000); // until DynaRun (and what the setup replaces) is closed Sleep(3000); rc = running_programs() ? 3 : install(tag, sha); if (g_hwnd) { DestroyWindow(g_hwnd); g_hwnd = NULL; } - if (!rc) { RegDeleteValueW(k, L"UpdateVersion"); RegDeleteValueW(k, L"UpdateSha256"); RegDeleteValueW(k, L"UpdateAsk"); RegDeleteValueW(k, L"UpdateSkip"); } + if (!rc) { RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SKIP)); } else { - set_time(k, L"UpdateAsk", now() + DAY); + set_time(k, WIDEN(RV_UPDATE_ASK), now() + DAY); if (rc != 2) { wsprintfW(txt, T(L"The update did not finish (error %d). It is tried again tomorrow.", L"更新沒有完成(錯誤 %d)。明天會再試一次。"), rc); From d3b9deb50265cad5eed5605bddfeb61ce5ebd151 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:32:23 +0800 Subject: [PATCH 03/41] Merge update_run and update_offer into update_prompt(policy) One flow for both ways a noted release is offered: BEFORE_DYNARUN (/update: Yes/No, DynaRun not running, launcher started afterwards) and BESIDE_DYNARUN (/offer: Yes/No/Cancel beside the running DynaRun, install once it is closed). Texts, buttons, log lines, registry writes and their order are unchanged; update_run and update_offer keep their signatures. The repeated RegDeleteValueW blocks become clear_note(k, skip). Co-Authored-By: Claude Opus 5.5 --- src/update.c | 140 +++++++++++++++++++++++++-------------------------- 1 file changed, 69 insertions(+), 71 deletions(-) diff --git a/src/update.c b/src/update.c index 1e9b46b..5ac7bcd 100644 --- a/src/update.c +++ b/src/update.c @@ -66,6 +66,13 @@ static void uplog(const char *fmt, const char *a, int b) static void set_time(HKEY k, const WCHAR *name, ULONGLONG t) { RegSetValueExW(k, name, 0, REG_QWORD, (const BYTE *)&t, 8); } +// Nothing is noted any more (it was installed, or GitHub lists nothing newer); skip: also forget the version the user chose to skip +static void clear_note(HKEY k, BOOL skip) +{ + RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); + if (skip) RegDeleteValueW(k, WIDEN(RV_UPDATE_SKIP)); +} + /* ---------- the release, from GitHub's API ---------- */ // GET url over https (INTERNET_FLAG_SECURE; certificate errors are never ignored) and the answer's body; *status = the HTTP status, @@ -196,7 +203,7 @@ int update_check(void) set_time(k, WIDEN(RV_UPDATE_CHECKED), now()); // answered: once a day at most if (!newer(v, cur)) { uplog("latest is %s: nothing to do %d", tag, 0); - RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SKIP)); + clear_note(k, TRUE); } else { if (release_asset(j, tag, hex)) { @@ -318,90 +325,81 @@ static void start_launcher(const WCHAR *args, BOOL updated) } } -int update_run(const WCHAR *args) +// The two ways a noted release is offered (one function, update_prompt; the policy picks the differences): +enum prompt_policy { + BEFORE_DYNARUN, // /update : DynaRun is not running. Yes / No (No = tomorrow); the launcher is started afterwards + BESIDE_DYNARUN // /offer: the launcher has started DynaRun. Yes = install once DynaRun is closed, No = skip this version, + // Cancel or the window's close button (X, Esc) = tomorrow (the launcher has already set UpdateAsk) +}; + +// Asks about the release noted in HKCU\Software\DynaRunFix and installs it on Yes. Only one at a time (Local\DynaRunFix-update). +// BEFORE_DYNARUN asks only while DynaRun is not running and starts the launcher with args at the end, whatever happened; +// BESIDE_DYNARUN asks only while DynaRun runs (after it has had up to 10 s to open) and does not start the launcher again. +static int update_prompt(enum prompt_policy p, const WCHAR *args) { - static WCHAR tag[64], sha[80], txt[600]; HKEY k; DWORD n; int rc = 0; BOOL updated = FALSE; + static WCHAR tag[64], sha[80], txt[700]; HKEY k; DWORD n, t0; int rc = 0, ans; BOOL beside = p == BESIDE_DYNARUN, updated = FALSE; char a[64]; HANDLE mx = CreateMutexW(NULL, FALSE, L"Local\\DynaRunFix-update"); - if (GetLastError() != ERROR_ALREADY_EXISTS && !(running_programs() & RUN_DYNARUN) && + if (GetLastError() != ERROR_ALREADY_EXISTS && (beside || !(running_programs() & RUN_DYNARUN)) && !RegOpenKeyExW(HKEY_CURRENT_USER, KEY, 0, KEY_ALL_ACCESS, &k)) { n = sizeof(tag) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_VERSION), NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; n = sizeof(sha) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_SHA256), NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; - if (tag[0] && lstrlenW(sha) == 64) { - wsprintfW(txt, T(L"A new version of DynaRunFix is available: %s (this is v%s).\n\n" - L"Update now? It takes about a minute; DynaRun opens afterwards.\n\n" - L"\"No\" asks again tomorrow.", - L"DynaRunFix 有新版本:%s(目前是 v%s)。\n\n" - L"現在更新嗎?大約一分鐘,更新完 DynaRun 會自動開啟。\n\n" - L"按「否」明天會再問一次。"), tag, WIDEN(DRF_VERSION)); - rc = msg(txt, MB_YESNO | MB_ICONQUESTION) == IDYES; - { char a[64]; WideCharToMultiByte(CP_ACP, 0, tag, -1, a, sizeof(a), NULL, NULL); uplog("asked about %s: %d (1 = yes)", a, rc); } - if (rc) { - rc = install(tag, sha); - if (!rc) { updated = TRUE; RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); } - } else rc = 2; + // DynaRun is still opening: do not put a window over its start-up + if (beside) for (t0 = GetTickCount(); GetTickCount() - t0 < 10000 && (running_programs() & RUN_DYNARUN); ) Sleep(500); // a deadline, not a count of sleeps + if (tag[0] && lstrlenW(sha) == 64 && (!beside || (running_programs() & RUN_DYNARUN))) { + if (beside) + wsprintfW(txt, T(L"A new version of DynaRunFix is available: %s (this is v%s).\n\n" + L"Yes: install it after you close DynaRun (DynaRun keeps running now).\n" + L"No: skip this version (it is not offered again; a newer one is).\n" + L"Cancel, or closing this window: ask again tomorrow.", + L"DynaRunFix 有新版本:%s(目前是 v%s)。\n\n" + L"是:等您關閉 DynaRun 後再安裝(DynaRun 照常使用)。\n" + L"否:略過這個版本(不再詢問;有更新的版本才會再問)。\n" + L"取消,或直接關閉這個視窗:明天再問。"), tag, WIDEN(DRF_VERSION)); + else + wsprintfW(txt, T(L"A new version of DynaRunFix is available: %s (this is v%s).\n\n" + L"Update now? It takes about a minute; DynaRun opens afterwards.\n\n" + L"\"No\" asks again tomorrow.", + L"DynaRunFix 有新版本:%s(目前是 v%s)。\n\n" + L"現在更新嗎?大約一分鐘,更新完 DynaRun 會自動開啟。\n\n" + L"按「否」明天會再問一次。"), tag, WIDEN(DRF_VERSION)); + ans = msg(txt, (beside ? MB_YESNOCANCEL : MB_YESNO) | MB_ICONQUESTION); + WideCharToMultiByte(CP_ACP, 0, tag, -1, a, sizeof(a), NULL, NULL); + if (beside) uplog("asked about %s while DynaRun runs: %d (6 = after DynaRun closes, 7 = skip, 2 = Cancel/closed: tomorrow)", a, ans); + else uplog("asked about %s: %d (1 = yes)", a, ans == IDYES); + if (ans == IDYES) { + if (beside) { + while (running_programs()) Sleep(2000); // until DynaRun (and what the setup replaces) is closed + Sleep(3000); + } + rc = beside && running_programs() ? 3 : install(tag, sha); + if (!rc) { updated = TRUE; clear_note(k, beside); } + } + else if (!beside) rc = 2; // "No": tomorrow + // Cancel and the window's close button (X, Esc) both answer IDCANCEL: they must not skip a version for good, + // so they mean "tomorrow" (UpdateAsk is already set by the launcher); the explicit skip is "No" + else if (ans == IDNO) RegSetValueExW(k, WIDEN(RV_UPDATE_SKIP), 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); if (rc) set_time(k, WIDEN(RV_UPDATE_ASK), now() + DAY); if (g_hwnd) { DestroyWindow(g_hwnd); g_hwnd = NULL; } if (rc && rc != 2) { // 2: "No", or the UAC prompt was cancelled (the new setup said so already) - wsprintfW(txt, T(L"The update did not finish (error %d). DynaRun opens as usual; the update is tried again tomorrow.", - L"更新沒有完成(錯誤 %d)。DynaRun 會照常開啟,明天會再試一次。"), rc); + if (beside) + wsprintfW(txt, T(L"The update did not finish (error %d). It is tried again tomorrow.", + L"更新沒有完成(錯誤 %d)。明天會再試一次。"), rc); + else + wsprintfW(txt, T(L"The update did not finish (error %d). DynaRun opens as usual; the update is tried again tomorrow.", + L"更新沒有完成(錯誤 %d)。DynaRun 會照常開啟,明天會再試一次。"), rc); msg(txt, MB_ICONWARNING); } } RegCloseKey(k); } - start_launcher(args, updated); + if (!beside) start_launcher(args, updated); if (mx) CloseHandle(mx); return rc; } -// Called by the launcher AFTER it has started DynaRun (DynaRun never waits for this): the question is asked beside the -// running DynaRun, and an accepted update is installed once DynaRun has been closed (the installer replaces files DynaRun -// holds). Yes = install after DynaRun closes, No = skip this version (UpdateSkip, not offered again; a newer release is), -// Cancel or the window's close button (X, Esc) = ask again tomorrow (the launcher has already set UpdateAsk). The launcher is not -// started again. -int update_offer(void) -{ - static WCHAR tag[64], sha[80], txt[700]; HKEY k; DWORD n, t0; int rc = 0, ans; - HANDLE mx = CreateMutexW(NULL, FALSE, L"Local\\DynaRunFix-update"); - if (GetLastError() != ERROR_ALREADY_EXISTS && !RegOpenKeyExW(HKEY_CURRENT_USER, KEY, 0, KEY_ALL_ACCESS, &k)) { - n = sizeof(tag) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_VERSION), NULL, NULL, (BYTE *)tag, &n)) tag[0] = 0; - n = sizeof(sha) - 2; if (RegQueryValueExW(k, WIDEN(RV_UPDATE_SHA256), NULL, NULL, (BYTE *)sha, &n)) sha[0] = 0; - // DynaRun is still opening: do not put a window over its start-up - for (t0 = GetTickCount(); GetTickCount() - t0 < 10000 && (running_programs() & RUN_DYNARUN); ) Sleep(500); // a deadline, not a count of sleeps - if (tag[0] && lstrlenW(sha) == 64 && (running_programs() & RUN_DYNARUN)) { - wsprintfW(txt, T(L"A new version of DynaRunFix is available: %s (this is v%s).\n\n" - L"Yes: install it after you close DynaRun (DynaRun keeps running now).\n" - L"No: skip this version (it is not offered again; a newer one is).\n" - L"Cancel, or closing this window: ask again tomorrow.", - L"DynaRunFix 有新版本:%s(目前是 v%s)。\n\n" - L"是:等您關閉 DynaRun 後再安裝(DynaRun 照常使用)。\n" - L"否:略過這個版本(不再詢問;有更新的版本才會再問)。\n" - L"取消,或直接關閉這個視窗:明天再問。"), tag, WIDEN(DRF_VERSION)); - ans = msg(txt, MB_YESNOCANCEL | MB_ICONQUESTION); - { char a[64]; WideCharToMultiByte(CP_ACP, 0, tag, -1, a, sizeof(a), NULL, NULL); - uplog("asked about %s while DynaRun runs: %d (6 = after DynaRun closes, 7 = skip, 2 = Cancel/closed: tomorrow)", a, ans); } - // Cancel and the window's close button (X, Esc) both answer IDCANCEL: they must not skip a version for good, - // so they mean "tomorrow" (UpdateAsk is already set by the launcher); the explicit skip is "No" - if (ans == IDNO) { - RegSetValueExW(k, WIDEN(RV_UPDATE_SKIP), 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); - } else if (ans == IDYES) { - while (running_programs()) Sleep(2000); // until DynaRun (and what the setup replaces) is closed - Sleep(3000); - rc = running_programs() ? 3 : install(tag, sha); - if (g_hwnd) { DestroyWindow(g_hwnd); g_hwnd = NULL; } - if (!rc) { RegDeleteValueW(k, WIDEN(RV_UPDATE_VERSION)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SHA256)); RegDeleteValueW(k, WIDEN(RV_UPDATE_ASK)); RegDeleteValueW(k, WIDEN(RV_UPDATE_SKIP)); } - else { - set_time(k, WIDEN(RV_UPDATE_ASK), now() + DAY); - if (rc != 2) { - wsprintfW(txt, T(L"The update did not finish (error %d). It is tried again tomorrow.", - L"更新沒有完成(錯誤 %d)。明天會再試一次。"), rc); - msg(txt, MB_ICONWARNING); - } - } - } - } - RegCloseKey(k); - } - if (mx) CloseHandle(mx); - return rc; -} \ No newline at end of file +// /update (a launcher of an earlier version runs it when DynaRun is not running): ask, install, start the launcher +int update_run(const WCHAR *args) { return update_prompt(BEFORE_DYNARUN, args); } + +// /offer: called by the launcher AFTER it has started DynaRun (DynaRun never waits for this): the question is asked beside the +// running DynaRun, and an accepted update is installed once DynaRun has been closed (the installer replaces files DynaRun holds). +int update_offer(void) { return update_prompt(BESIDE_DYNARUN, NULL); } From b4a59b6b68483a056ad9b8169017f4cd22fae800 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:32:24 +0800 Subject: [PATCH 04/41] Split the launcher's entry point into parse_args, elevation_gate, start_le, start_plain and hook_and_finish Code moved as it was: "/wasadmin" is still taken before the elevation check, log_start still runs before "/noupdate", "/autostart" and "/restart" are parsed, and the start order (attach / notice / dll load / LE or plain / hook) is the same. Co-Authored-By: Claude Opus 5.5 --- src/launcher.c | 289 ++++++++++++++++++++++++++++--------------------- 1 file changed, 164 insertions(+), 125 deletions(-) diff --git a/src/launcher.c b/src/launcher.c index 7f04809..117b018 100644 --- a/src/launcher.c +++ b/src/launcher.c @@ -601,53 +601,41 @@ static void read_dynarun_exe(HKEY root, const char *sub, char *exe, DWORD size) } #ifndef DRF_NO_MAIN -void WinMainCRTStartup(void) +// The launcher's arguments: the command line after the program name. +static char *own_args(void) { - char exe[MAX_PATH], dir[MAX_PATH], dll[MAX_PATH], le[MAX_PATH], line[3 * MAX_PATH], ev[64], *p, *cmd, *args; - PROCESS_INFORMATION pi; HMODULE hd; HHOOK hk; HANDLE e, h; DWORD n = MAX_PATH; int i; BOOL ask, wasadmin, reported; ULONGLONG dl; - - // From here on DLLs are looked for in System32 only (not the current folder, not the PATH). dynafix.dll is loaded by its full path. - { - typedef BOOL (WINAPI *sddd_t)(DWORD); - sddd_t sddd = (sddd_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "SetDefaultDllDirectories"); // Vista+ / KB2533623 - if (sddd) sddd(0x00000800 /* LOAD_LIBRARY_SEARCH_SYSTEM32 */); - } - - // dynafix.dll lives next to this launcher - n = GetModuleFileNameA(NULL, dll, MAX_PATH); - if (!n || n >= MAX_PATH - 16) fail_w(L"The folder of DynaRunFix.exe is too deep.", L"DynaRunFix.exe 所在的資料夾路徑太長。"); - p = dll + lstrlenA(dll); while (p > dll && *p != '\\') p--; lstrcpyA(p, "\\dynafix.dll"); - - cmd = GetCommandLineA(); + char *cmd = GetCommandLineA(); if (*cmd == '"') { cmd++; while (*cmd && *cmd != '"') cmd++; if (*cmd) cmd++; } else while (*cmd && *cmd != ' ') cmd++; while (*cmd == ' ') cmd++; - // "/wasadmin": this is the copy an elevated launcher started with the shell's token (see relaunch_non_elevated). Only - // believed when this process is not elevated (see below); an elevated process that is given it ignores it. - wasadmin = prefix(&cmd, VERB_WASADMIN); - { // An elevated launcher starts nothing but its copy with the shell's token: no DynaRun, no LEProc, no updater. - HANDLE tk; - g_elevated = !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk) || (i = token_elevated(tk), CloseHandle(tk), i); - if (g_elevated) { - { - int rl; - if (wasadmin) llog("launcher: /wasadmin ignored: this process is elevated, so it was not started by us with a non-elevated token\r\n", 0, 0); - rl = relaunch_non_elevated(); - if (!rl) - fail_w(L"DynaRunFix was started as administrator, but Windows did not give it the token of the desktop user, " - L"so DynaRun is not started: DynaRunFix never runs DynaRun with more rights than a normal start from the desktop.\n\n" - L"Possible reasons: the desktop (Explorer) is not running, or the \"Secondary Logon\" service is disabled.\n\n" - L"Start DynaRunFix again in the normal way (not \"Run as administrator\").", - L"DynaRunFix 是以系統管理員身分啟動的,但 Windows 沒有提供桌面使用者的權杖," - L"因此不會啟動 DynaRun:DynaRunFix 一律不會以高於「從桌面正常啟動」的權限執行 DynaRun。\n\n" - L"可能原因:桌面(檔案總管)未執行,或「Secondary Logon」服務已停用。\n\n" - L"請以一般方式(不要選「以系統管理員身分執行」)重新開啟 DynaRunFix。"); - if (rl == 1) ExitProcess(0); // the copy carries on; rl == 2: the shell token is elevated too, carry on here - } - } - } - trim_log(); - log_start(cmd); - ask = TRUE; + return cmd; +} + +// An elevated launcher starts nothing but its copy with the shell's token: no DynaRun, no LEProc, no updater. Sets g_elevated; +// returns only when this process carries on (not elevated, or the shell token is elevated too). +static void elevation_gate(BOOL wasadmin) +{ + HANDLE tk; int i, rl; + g_elevated = !OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tk) || (i = token_elevated(tk), CloseHandle(tk), i); + if (!g_elevated) return; + if (wasadmin) llog("launcher: /wasadmin ignored: this process is elevated, so it was not started by us with a non-elevated token\r\n", 0, 0); + rl = relaunch_non_elevated(); + if (!rl) + fail_w(L"DynaRunFix was started as administrator, but Windows did not give it the token of the desktop user, " + L"so DynaRun is not started: DynaRunFix never runs DynaRun with more rights than a normal start from the desktop.\n\n" + L"Possible reasons: the desktop (Explorer) is not running, or the \"Secondary Logon\" service is disabled.\n\n" + L"Start DynaRunFix again in the normal way (not \"Run as administrator\").", + L"DynaRunFix 是以系統管理員身分啟動的,但 Windows 沒有提供桌面使用者的權杖," + L"因此不會啟動 DynaRun:DynaRunFix 一律不會以高於「從桌面正常啟動」的權限執行 DynaRun。\n\n" + L"可能原因:桌面(檔案總管)未執行,或「Secondary Logon」服務已停用。\n\n" + L"請以一般方式(不要選「以系統管理員身分執行」)重新開啟 DynaRunFix。"); + if (rl == 1) ExitProcess(0); // the copy carries on; rl == 2: the shell token is elevated too, carry on here +} + +// The arguments after "/wasadmin" (already taken) and after log_start: "/noupdate"..., "/autostart", "/restart ", then the +// DynaRun V3.exe to start (the recorded one when none is given) into exe, its folder into dir (both MAX_PATH). Sets g_update. +static void parse_args(char *cmd, char *exe, char *dir) +{ + char *p; BOOL ask = TRUE; while (flag(&cmd, VERB_NOUPDATE)) ask = FALSE; // from the setup's /update: it has just asked // "/autostart" (the sign-in Run value): at sign-in the desktop is still being set up (taskbar, resolution, DPI) and // DynaRun lays its screen out from the size it sees at start: wait for the taskbar and 3 s of an unchanged size @@ -670,100 +658,100 @@ void WinMainCRTStartup(void) cmd = p; while (*cmd == ' ') cmd++; ask = FALSE; } - args = cmd; // DynaRun always starts first; the update (check, offer) is dealt with after it runs (finish), only when it was started // here and neither "/noupdate", "/autostart" nor "/restart" was given g_update = ask; if (*cmd == '"') { cmd++; lstrcpynA(exe, cmd, MAX_PATH); p = exe; while (*p && *p != '"') p++; *p = 0; } else if (*cmd) lstrcpynA(exe, cmd, MAX_PATH); else { - read_dynarun_exe(HKEY_LOCAL_MACHINE, "SOFTWARE\\DynaRunFix", exe, sizeof(exe)); + read_dynarun_exe(HKEY_LOCAL_MACHINE, "SOFTWARE\\DynaRunFix", exe, MAX_PATH); } lstrcpyA(dir, exe); p = dir + lstrlenA(dir); while (p > dir && *p != '\\') p--; *p = 0; +} - g_pid = running(); - if (g_pid) { - g_update = FALSE; // attaching to a DynaRun that is already there: nothing is offered - h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, g_pid); - if (h) CloseHandle(h); - else if (GetLastError() == ERROR_ACCESS_DENIED) { // DynaRun is never raised - llog("launcher: the running DynaRun cannot be opened (it runs with more rights than this launcher)\r\n", 0, 0); - fail_w(L"DynaRun is running with administrator rights. Close it and start it again through DynaRunFix.", L"DynaRun 正以系統管理員權限執行。請關閉後,再透過 DynaRunFix 啟動。"); - } - } else { - { // never with more rights than a normal start; a one-time notice says so when the launcher was started as administrator or the layer is set - if (wasadmin && !g_elevated) { llog("launcher: was started as administrator and started itself again non-elevated\r\n", 0, 0); show_notice(1); } - else if (runasadmin(exe)) { llog("launcher: RUNASADMIN compatibility layer is set for DynaRun and ignored\r\n", 0, 0); show_notice(2); } - } - { // before DynaRun runs (see early_hook) - DWORD t0 = GetTickCount(); - hd = LoadLibraryA(dll); - llog("launcher: dynafix.dll loaded in %u ms (error %u)\r\n", GetTickCount() - t0, hd ? 0 : GetLastError()); +// With Windows' UTF-8 option on, DynaRun is started through Locale Emulator (le\LEProc.exe next to this launcher, profile +// LE_PROFILE in le\LEConfig.xml) so GDI-drawn labels use the zh-TW code page too. Only for a Traditional Chinese system locale +// (legacy code page 950): the LE profile is zh-TW. Other locales get just the manifest, which selects their own legacy code page. +// TRUE (le = its path, MAX_PATH) when DynaRun is to be started that way. +static BOOL use_le(const char *dll, char *le) +{ + char ev[64], *p; + lstrcpyA(le, dll); p = le + lstrlenA(le); while (p > le && *p != '\\') p--; lstrcpyA(p, "\\le\\LEProc.exe"); + if (!GetLocaleInfoA(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTANSICODEPAGE, ev, sizeof(ev))) ev[0] = 0; + // Also for a zh-TW system locale whose FontAssoc lacks ANSI(00)=YES (Windows installed in English, + // locale changed later): LE gives DynaRun the Big5 code page and charset for every window, including + // tooltips and OCX controls that dynafix's font hooks do not reach. + return !lstrcmpA(ev, "950") && (GetACP() == CP_UTF8 || (GetACP() == 950 && !fontassoc())) && + GetFileAttributesA(le) != INVALID_FILE_ATTRIBUTES; +} + +// DynaRun through Locale Emulator: sets g_pid, and hooks DynaRun early (early_hook) when its first thread is found in time. +static void start_le(HMODULE hd, const char *le, const char *exe, const char *dir) +{ + char line[3 * MAX_PATH]; PROCESS_INFORMATION pi; ULONGLONG dl; + wsprintfA(line, "\"%s\" -runas " LE_PROFILE " \"%s\"", le, exe); + // LE creates DynaRun suspended (to load its own dll) and resumes it: catch it right away. LEProc runs in a + // job, so DynaRun's creation is reported at once (polling the process list can be late: VB reads the + // screen size before its first form, and dynafix must be in place by then); polling is the fallback. + { + HANDLE job = CreateJobObjectA(NULL, NULL), port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); + JOBOBJECT_ASSOCIATE_COMPLETION_PORT jp; BOOL injob = FALSE; + if (job && port) { + jp.CompletionKey = job; jp.CompletionPort = port; + injob = SetInformationJobObject(job, JobObjectAssociateCompletionPortInformation, &jp, sizeof(jp)); } - // With Windows' UTF-8 option on, start DynaRun through Locale Emulator (le\LEProc.exe next to this - // launcher, profile LE_PROFILE in le\LEConfig.xml) so GDI-drawn labels use the zh-TW code page too. - // Only for a Traditional Chinese system locale (legacy code page 950): the LE profile is zh-TW. - // Other locales get just the manifest, which selects their own legacy code page. - lstrcpyA(le, dll); p = le + lstrlenA(le); while (p > le && *p != '\\') p--; lstrcpyA(p, "\\le\\LEProc.exe"); - if (!GetLocaleInfoA(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTANSICODEPAGE, ev, sizeof(ev))) ev[0] = 0; - // Also for a zh-TW system locale whose FontAssoc lacks ANSI(00)=YES (Windows installed in English, - // locale changed later): LE gives DynaRun the Big5 code page and charset for every window, including - // tooltips and OCX controls that dynafix's font hooks do not reach. - if (!lstrcmpA(ev, "950") && (GetACP() == CP_UTF8 || (GetACP() == 950 && !fontassoc())) && - GetFileAttributesA(le) != INVALID_FILE_ATTRIBUTES) { - wsprintfA(line, "\"%s\" -runas " LE_PROFILE " \"%s\"", le, exe); - // LE creates DynaRun suspended (to load its own dll) and resumes it: catch it right away. LEProc runs in a - // job, so DynaRun's creation is reported at once (polling the process list can be late: VB reads the - // screen size before its first form, and dynafix must be in place by then); polling is the fallback. - { - HANDLE job = CreateJobObjectA(NULL, NULL), port = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 1); - JOBOBJECT_ASSOCIATE_COMPLETION_PORT jp; BOOL injob = FALSE; - if (job && port) { - jp.CompletionKey = job; jp.CompletionPort = port; - injob = SetInformationJobObject(job, JobObjectAssociateCompletionPortInformation, &jp, sizeof(jp)); - } - if (!spawn(le, line, dir, CREATE_SUSPENDED, &pi)) fail("Cannot start Locale Emulator"); - injob = injob && AssignProcessToJobObject(job, pi.hProcess); - ResumeThread(pi.hThread); - if (injob) { - DWORD msg; ULONG_PTR key; LPOVERLAPPED ov; ULONGLONG t, jdl = tick64() + 30000; - while (!g_pid && (t = tick64()) < jdl && GetQueuedCompletionStatus(port, &msg, &key, &ov, (DWORD)(jdl - t))) - if (msg == JOB_OBJECT_MSG_NEW_PROCESS && (DWORD)(UINT_PTR)ov != pi.dwProcessId && is_dynarun((DWORD)(UINT_PTR)ov)) - g_pid = (DWORD)(UINT_PTR)ov; - if (g_pid) { // its first thread, at once (a thread list of the whole system takes too long) - typedef LONG (NTAPI *gnt_t)(HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, HANDLE *); - typedef DWORD (WINAPI *gti_t)(HANDLE); - gnt_t gnt = (gnt_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtGetNextThread"); - gti_t gti = (gti_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetThreadId"); - HANDLE hp = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, g_pid), ht = NULL; - if (hp && gnt && gti && !gnt(hp, NULL, THREAD_QUERY_INFORMATION, 0, 0, &ht)) { g_first = gti(ht); CloseHandle(ht); } - if (hp) CloseHandle(hp); - } - } - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - } - for (dl = tick64() + 30000; !g_pid && !(g_pid = running()) && tick64() < dl; ) Sleep(1); - if (!g_pid) fail("DynaRun V3.exe did not start under Locale Emulator"); - { - DWORD tid = g_first; HANDLE h0 = OpenProcess(SYNCHRONIZE, FALSE, g_pid); - for (dl = tick64() + 3000; !tid && !(tid = first_thread(g_pid)) && tick64() < dl; ) Sleep(1); - if (tid && h0) early_hook(hd, g_pid, tid, h0); - if (h0) CloseHandle(h0); - } - } else { - charset_env(); - if (!spawn(exe, NULL, dir, CREATE_SUSPENDED, &pi)) { - if (GetLastError() == ERROR_ELEVATION_REQUIRED) llog("launcher: DynaRun V3.exe insists on elevation, not started\r\n", 0, 0); - fail("Cannot start DynaRun V3.exe"); + if (!spawn(le, line, dir, CREATE_SUSPENDED, &pi)) fail("Cannot start Locale Emulator"); + injob = injob && AssignProcessToJobObject(job, pi.hProcess); + ResumeThread(pi.hThread); + if (injob) { + DWORD msg; ULONG_PTR key; LPOVERLAPPED ov; ULONGLONG t, jdl = tick64() + 30000; + while (!g_pid && (t = tick64()) < jdl && GetQueuedCompletionStatus(port, &msg, &key, &ov, (DWORD)(jdl - t))) + if (msg == JOB_OBJECT_MSG_NEW_PROCESS && (DWORD)(UINT_PTR)ov != pi.dwProcessId && is_dynarun((DWORD)(UINT_PTR)ov)) + g_pid = (DWORD)(UINT_PTR)ov; + if (g_pid) { // its first thread, at once (a thread list of the whole system takes too long) + typedef LONG (NTAPI *gnt_t)(HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, HANDLE *); + typedef DWORD (WINAPI *gti_t)(HANDLE); + gnt_t gnt = (gnt_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtGetNextThread"); + gti_t gti = (gti_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "GetThreadId"); + HANDLE hp = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, g_pid), ht = NULL; + if (hp && gnt && gti && !gnt(hp, NULL, THREAD_QUERY_INFORMATION, 0, 0, &ht)) { g_first = gti(ht); CloseHandle(ht); } + if (hp) CloseHandle(hp); } - g_pid = pi.dwProcessId; - // Without the early hook the dll is attached once the first window shows up. - ResumeThread(pi.hThread); - early_hook(hd, g_pid, pi.dwThreadId, pi.hProcess); - WaitForInputIdle(pi.hProcess, 30000); - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } + for (dl = tick64() + 30000; !g_pid && !(g_pid = running()) && tick64() < dl; ) Sleep(1); + if (!g_pid) fail("DynaRun V3.exe did not start under Locale Emulator"); + { + DWORD tid = g_first; HANDLE h0 = OpenProcess(SYNCHRONIZE, FALSE, g_pid); + for (dl = tick64() + 3000; !tid && !(tid = first_thread(g_pid)) && tick64() < dl; ) Sleep(1); + if (tid && h0) early_hook(hd, g_pid, tid, h0); + if (h0) CloseHandle(h0); + } +} + +// DynaRun directly: sets g_pid and hooks DynaRun early (early_hook). +static void start_plain(HMODULE hd, const char *exe, const char *dir) +{ + PROCESS_INFORMATION pi; + charset_env(); + if (!spawn(exe, NULL, dir, CREATE_SUSPENDED, &pi)) { + if (GetLastError() == ERROR_ELEVATION_REQUIRED) llog("launcher: DynaRun V3.exe insists on elevation, not started\r\n", 0, 0); + fail("Cannot start DynaRun V3.exe"); + } + g_pid = pi.dwProcessId; + // Without the early hook the dll is attached once the first window shows up. + ResumeThread(pi.hThread); + early_hook(hd, g_pid, pi.dwThreadId, pi.hProcess); + WaitForInputIdle(pi.hProcess, 30000); + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); +} + +// dynafix.dll has not reported in from DynaRun g_pid (yet; an early hook that worked has finished already): find DynaRun's GUI +// thread, hook it with the dll at path dll, and finish. +static void hook_and_finish(const char *dll) +{ + char line[3 * MAX_PATH]; HMODULE hd; HHOOK hk; HANDLE e; BOOL reported; ULONGLONG dl; for (dl = tick64() + 60000; !g_tid; ) { g_best = 0; EnumWindows(findwin, 0); if (g_tid || tick64() >= dl) break; Sleep(100); } if (!g_tid) fail("DynaRun window not found"); // just started: the VB main form may not exist yet (a splash or helper window comes first); wait a moment for it @@ -795,4 +783,55 @@ void WinMainCRTStartup(void) if (e) CloseHandle(e); finish(); } + +void WinMainCRTStartup(void) +{ + char exe[MAX_PATH], dir[MAX_PATH], dll[MAX_PATH], le[MAX_PATH], *p, *cmd; + HMODULE hd; HANDLE h; DWORD n; BOOL wasadmin; + + // From here on DLLs are looked for in System32 only (not the current folder, not the PATH). dynafix.dll is loaded by its full path. + { + typedef BOOL (WINAPI *sddd_t)(DWORD); + sddd_t sddd = (sddd_t)GetProcAddress(GetModuleHandleA("kernel32.dll"), "SetDefaultDllDirectories"); // Vista+ / KB2533623 + if (sddd) sddd(0x00000800 /* LOAD_LIBRARY_SEARCH_SYSTEM32 */); + } + + // dynafix.dll lives next to this launcher + n = GetModuleFileNameA(NULL, dll, MAX_PATH); + if (!n || n >= MAX_PATH - 16) fail_w(L"The folder of DynaRunFix.exe is too deep.", L"DynaRunFix.exe 所在的資料夾路徑太長。"); + p = dll + lstrlenA(dll); while (p > dll && *p != '\\') p--; lstrcpyA(p, "\\dynafix.dll"); + + cmd = own_args(); + // "/wasadmin": this is the copy an elevated launcher started with the shell's token (see relaunch_non_elevated). Only + // believed when this process is not elevated (see elevation_gate); an elevated process that is given it ignores it. + wasadmin = prefix(&cmd, VERB_WASADMIN); + elevation_gate(wasadmin); + trim_log(); + log_start(cmd); + parse_args(cmd, exe, dir); + + g_pid = running(); + if (g_pid) { + g_update = FALSE; // attaching to a DynaRun that is already there: nothing is offered + h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, g_pid); + if (h) CloseHandle(h); + else if (GetLastError() == ERROR_ACCESS_DENIED) { // DynaRun is never raised + llog("launcher: the running DynaRun cannot be opened (it runs with more rights than this launcher)\r\n", 0, 0); + fail_w(L"DynaRun is running with administrator rights. Close it and start it again through DynaRunFix.", L"DynaRun 正以系統管理員權限執行。請關閉後,再透過 DynaRunFix 啟動。"); + } + } else { + { // never with more rights than a normal start; a one-time notice says so when the launcher was started as administrator or the layer is set + if (wasadmin && !g_elevated) { llog("launcher: was started as administrator and started itself again non-elevated\r\n", 0, 0); show_notice(1); } + else if (runasadmin(exe)) { llog("launcher: RUNASADMIN compatibility layer is set for DynaRun and ignored\r\n", 0, 0); show_notice(2); } + } + { // before DynaRun runs (see early_hook) + DWORD t0 = GetTickCount(); + hd = LoadLibraryA(dll); + llog("launcher: dynafix.dll loaded in %u ms (error %u)\r\n", GetTickCount() - t0, hd ? 0 : GetLastError()); + } + if (use_le(dll, le)) start_le(hd, le, exe, dir); + else start_plain(hd, exe, dir); + } + hook_and_finish(dll); +} #endif From 314e7d5288926526afeb2e8168b6876c32968268 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:32:54 +0800 Subject: [PATCH 05/41] Tests: VM regression checklist Co-Authored-By: Claude Opus 5.5 --- tests/VM_CHECKLIST.md | 128 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 tests/VM_CHECKLIST.md diff --git a/tests/VM_CHECKLIST.md b/tests/VM_CHECKLIST.md new file mode 100644 index 0000000..9106f8a --- /dev/null +++ b/tests/VM_CHECKLIST.md @@ -0,0 +1,128 @@ +# VM regression checklist + +Manual checks for a build of `DynaRunFix-Setup.exe` that the automated tests cannot cover (UAC, the wizard, the restart for the driver, the network). +Run them in a throw-away VM (Windows 10 or 11, snapshot before each section). Do not run them on the development host. +The texts in quotes are the English ones from the source (`src\wizard.c`, `src\setup.c`, `src\update.c`); a Traditional Chinese Windows shows the Chinese pair. + +Build first with `build.cmd` (the setup is `build\DynaRunFix-Setup.exe`). A dev build is version `dev` and never takes part in the update check. + +## 0. Logs and state to look at after every section + +| What | Where | Written by | +|---|---|---| +| Setup / wizard log | `%TEMP%\DynaRunFix-setup.log` (user, not elevated) | wizard, `user_log` | +| Elevated-stage log | `\setup.log` (`%ProgramFiles(x86)%\DynaRunFix` on 64-bit Windows) | machine stage, `setup_log` | +| Driver log | `%ProgramData%\DynaRunFix\driver.log` (+ `bitlocker-before.txt`) | driver steps, only while the driver option is used | +| Launcher / dll / update log | `%TEMP%\dynafix.log` (shared by `DynaRunFix.exe`, `dynafix.dll` and the update check; `launcher:` and `update` lines) | launcher, dll, `update.c` | +| Exit code | `echo %errorlevel%` after `start /wait DynaRunFix-Setup.exe ...` | see the table in `docs\MANUAL.md` (0 ok, 1 error, 2 cancelled, 3 over-the-shoulder, 4 MSI failed, 5 exe not found, 6 DynaRun running) | +| Registry | `HKLM\SOFTWARE\DynaRunFix`, `HKCU\Software\DynaRunFix`, `HKLM\...\Uninstall\DynaRunFix`, `HKCU\...\Run` value `DynaRunFix` | | +| Task / boot option | `schtasks /query /tn DynaRunFix-DriverContinue`, `bcdedit /enum {current}` (`onetimeadvancedoptions`) | driver option only | + +A test passes only if the expected result holds AND the logs show no unexpected error line. Note the build (`DRF_DISPLAY`) and the VM snapshot in the result. + +## 1. Full install (DynaRun V3 not installed, driver ticked) + +Start: snapshot "clean", administrator account, network on, no DynaRun, no files in Downloads. Have Dyna Pro's setup password ready. + +1. Run `DynaRunFix-Setup.exe`. Expected: first page "Install DynaRun V3" with "This program will: 1. download ... 2. install ... 3. add the fixes ...", buttons Start / Cancel / "Installed elsewhere...", check boxes: pin to the taskbar, start DynaRun when Windows starts, "Also install the Dyna Pro USB driver (one restart needed)" (all three ticked; the last only on Windows 10/11 when the driver is missing). +2. Click Start. Expected: page "Getting the DynaRun V3 setup" with a progress bar, Pause / Cancel / "Open website"; then the page "Enter the setup password". +3. Type a wrong password, Next. Expected: "Wrong password. Please check it (upper/lower case matters)." and the page stays. Type the right one, Next. Expected: "Opening the setup file", then "Check the installer" (source, path, size, SHA-256, MSI version and manufacturer), Next. +4. License page. Expected: Dyna Pro's license in the box; the Install button is disabled until "I accept the license agreement" is ticked. +5. Click Install. Expected: the driver pages come BEFORE the install: "Install the Dyna Pro USB driver" (intro, with the line "DynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once"), Next, then "Getting the Dyna Pro USB driver" (a copy on the computer is looked for first, else downloaded from dynapro.co.uk), then "After the restart you will see this: press 7" with the picture and the buttons "Install and restart" / Cancel. +6. Click "Install and restart". Expected: exactly ONE UAC prompt (administrator account: Yes), page "Installing" / "Please wait". No second UAC prompt for the driver preparation. `setup.log` in the install folder shows the machine stage, then the driver preparation (`driver.log` exists). +7. Expected: the page "Restarting" ("The computer restarts now. When the menu appears, press 7...") with the pin hint ("DynaRun will appear on the taskbar after you sign out and back in" on Windows 11) and, on a non-zh-TW system with a Chinese UI, the code-page warning. The computer restarts into the Windows advanced start-up menu. +8. Press 7 (Disable driver signature enforcement). Sign in. Expected: the wizard continues by itself (task `DynaRunFix-DriverContinue`, `/driver-continue`) on the page "Last step: install the driver" with the red-window picture; no UAC prompt on an administrator account. Click Install; answer Windows' red warning "Install this driver software anyway" twice. +9. Expected: "The driver is installed" with "Start DynaRun" / Close. The task and the boot option are gone (`schtasks`, `bcdedit`); `pnputil /enum-drivers` lists the Dyna Pro ftdibus package; BitLocker (if it was on) is protecting again. +10. Click "Start DynaRun". Expected: DynaRun starts through the launcher, non-elevated; the main screen does not flicker. The "start DynaRun when Windows starts" value `HKCU\...\Run\DynaRunFix` exists only now (after the driver step; `AutostartPending` is cleared). +11. `%TEMP%\dynafix.log`: lines starting `launcher: DynaRun pid=...`, `launcher: dynafix.dll loaded in N ms (error 0)`, no `did not report in`. + +Full install without the driver: untick the driver box on page 1. Expected: after the license, a single UAC prompt, the page "Installing", then "Done" ("Everything is installed ...", buttons "Start DynaRun" / Close). Exit code 0. The Run value exists at once. + +## 2. Fix only (DynaRun already installed) + +Start: snapshot with DynaRun V3 installed and never started through the fix. + +1. Run the setup. Expected: page "Fix DynaRun V3 for this Windows" ("DynaRun V3 was found: ... This installs the fixes ..."), button Install, the check boxes. +2. Untick the driver box, click Install. Expected: ONE UAC prompt, "Installing", then "Done". +3. Expected state: `` has `DynaRunFix.exe`, `dynafix.dll`, `DynaRunFix-Setup.exe`, `le\`, `setup.log`; the DynaRun desktop/Start menu shortcuts point at `DynaRunFix.exe`; the uninstall entry shows the version; a desktop shortcut exists; the pin follows the Windows version (Windows 11: after sign-out/in). +4. Run the setup a second time with the same build and click Install again. Expected: it finishes with exit code 0, the shortcut backups (`HKCU\Software\DynaRunFix\Shortcuts`) stay the original shortcut files (not our own launcher shortcuts). +5. `DynaRunFix-Setup.exe /quiet` from an administrator command prompt (DynaRun not running). Expected: no window, ONE UAC prompt (or none if already elevated), exit code 0. `/quiet` never installs the driver. With DynaRun not installed: exit code 1. +6. DynaRun running: start DynaRun, then run the wizard. Expected: page "DynaRun is running" with "Close DynaRun and install" / Cancel; the button closes DynaRun and installs. With `/quiet`: exit code 6, nothing changed; `/quiet /close`: DynaRun is closed and the install finishes with 0. `setup.log` / `DynaRunFix-setup.log` say which program blocked it. +7. `/quiet /notaskbar /noautostart` and `/quiet /keep`: no pin, no Run value / pin, Run value and desktop shortcut left as they were. + +## 3. Fix + driver (DynaRun installed) + +1. Run the setup, leave the driver box ticked, click Install. Expected: the driver pages first (intro, "Getting ...", "press 7"), then "Install and restart" and ONE UAC prompt that covers the fix and the driver preparation. +2. Click "Skip" on the driver intro (or on the download error page): Expected: the fix is installed without the driver, `driver: skipped, the fix is installed without it` in `DynaRunFix-setup.log`, the Run value written by the install, page "Done". +3. Cancel on the "press 7" page before "Install and restart": nothing is installed, exit code 2, no task, no boot option. +4. Decline the UAC prompt of "Install and restart" (No): Expected: back on the "press 7" page with "Nothing was changed: Windows needs your "Yes" first." / "Nothing was installed: Windows needs your "Yes" to install."; nothing in `%ProgramData%\DynaRunFix`. +5. After the restart do NOT press 7 (let Windows boot normally; the task starts the wizard): Expected: page "7 was not pressed" with Retry / Cancel. Retry restarts once more. Cancel (or closing the window, elevated) removes the task, the boot option and takes BitLocker back (`/driver-cancel` behaviour). +6. BitLocker VM (if available): protection on. Expected: BitLocker is suspended before the restart (`manage-bde -status` is not run by the tester: read `bitlocker-before.txt` / `driver.log`), resumes after the install. If the suspension fails the page "BitLocker could not be suspended" offers "Restart anyway" / Later / "Find the recovery key". +7. A driver zip that differs from the pinned files (Choose file... with a modified copy): Expected: the page "The driver differs from the known version" with Cancel as the default button, "Install anyway" second; `driver.log` records the choice. + +## 4. Driver only + +1. Fix current and driver missing: run the setup. Expected: page "Install the Dyna Pro USB driver" ("The fix for Windows 10/11 is already installed (version X). The Dyna Pro USB driver is not installed yet ..."), buttons "Install the driver" / Cancel / (hint) / "Reinstall the fix...", no driver check box. +2. "Install the driver": intro, download/check, "press 7" page with "Restart now" / Later (the fix is not installed in this step), ONE UAC prompt, restart, press 7, "Last step", Install, "The driver is installed". +3. `DynaRunFix-Setup.exe /driver` (also the Start menu shortcut "Install Dyna Pro USB driver"): the driver pages only, no skip button (Cancel). Driver already installed: page "The driver is already installed". +4. `DynaRunFix-Setup.exe /driver-cancel` from a NON-elevated prompt: exit code 1, nothing changes. From an administrator prompt after a prepared-but-not-restarted driver step: the task, the boot option and the BitLocker suspension are removed. +5. Windows 8 / 7 / XP VM (optional): `/driver` shows "The driver was not installed" with the manual steps for that Windows and "Open the manual steps". + +## 5. Standard (non-admin) user + +Start: a standard user account; an administrator account whose password is known. + +1. Run the setup as the standard user, fix only. Expected: the UAC prompt asks for an administrator's user name and password. Enter the administrator's. The install completes; the standard user's own shortcuts are retargeted (not the administrator's), pin/Run value are written for the standard user (over-the-shoulder: the `/machine ` argument names the starting user). Exit code 0. +2. Cancel the prompt. Expected: "Administrator permission was not granted. Nothing was changed." and exit code 2; nothing installed. +3. Full install (DynaRun missing) as a standard user, typing the administrator's credentials: Expected either the normal flow, or the over-the-shoulder path (DynaRun's MSI runs as the standard user, then the fix is installed): the install finishes and `HKCU\Software\DynaPro` is the standard user's. Exit code 0. +4. With the driver: after the restart a standard user is asked for an administrator's password again at the page "Last step" (the task cannot run elevated for a standard user). Expected: the install finishes after the credentials; "The driver is installed". +5. Uninstall as the standard user: administrator prompt first; after cancelling it nothing is changed (shortcuts still go through the fix). + +## 6. Reinstall and uninstall + +1. Reinstall over the same build (section 2 step 4) and over an older release if one is available (an update: `/quiet /keep` leaves the pin, Run value and desktop shortcut as they were). +2. Reinstall with the pin box cleared after a pinned install: Expected: the taskbar policy values (Windows 11) are taken back; with `/keep` they stay. +3. Uninstall from Programs and Features (or `DynaRunFix-Setup.exe /uninstall`), DynaRun V3 installed. Expected: the question "Uninstall DynaRunFix?" with "Fix only" / "Remove all" / Cancel; one UAC prompt first (cancelling it changes nothing; exit code 2). "Fix only": shortcuts go back to the original ones (DynaRun V3.exe), Run value and policy values removed, the install folder removed (or at the next restart), `setup.log` kept, "DynaRunFix has been uninstalled.", exit code 0. DynaRun V3 stays and starts normally. +4. "Remove all": Dyna Pro's own uninstaller runs afterwards; "DynaRunFix and DynaRun V3 have been uninstalled." Cancelling Dyna Pro's uninstaller: "DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled)." (exit code 2). +5. Uninstall with DynaRun running: "DynaRun is running and must be closed to uninstall DynaRunFix." OK closes it; Cancel exits 2. `/uninstall /quiet`: nothing asked, DynaRun closed only with `/close`, exit 0. +6. Uninstall after a driver step that was prepared but not finished: the task, the boot option and `%ProgramData%\DynaRunFix` are removed; BitLocker is resumed if the suspension flag was set. An installed driver stays. +7. Uninstall when not installed: "DynaRunFix is not installed." exit 0. + +## 7. Network failure + +Use the VM's virtual network adapter (disconnect / host-only) and a hosts-file entry for the unreachable case. Start each case from page 1 of a full install (DynaRun missing) and repeat the driver case from the driver intro. + +1. No network (adapter disconnected): click Start. Expected: the error page at once, no waiting through retries, title "The DynaRun V3 setup could not be downloaded", text "This computer is not connected to a network (Windows reports no connection). Connect it and try again." plus "Get it yourself: 1. click "Open website" ...", buttons Retry / Cancel (Skip on the driver page in the main flow) / "Choose file..." / "Open website". +2. Reconnect, click Retry. Expected: the download starts and the flow continues. The status line shows the progress "x of y MB". +3. Unreachable host (`dynapro.co.uk` -> an address that does not answer, or the firewall drops it): Expected: the attempt gives up after about 10 seconds per try; retries after 1, 2, 4, 8, 15 s; then "No connection to the Dyna Pro website (error N). Check the internet connection." with the first real error. +4. Bad certificate / date set far off: "The security certificate of the Dyna Pro website could not be verified (error N) ... Check the date and time of this computer." Nothing downloaded. +5. Redirect off the host (hosts file pointing to a local server that redirects): "The download was refused: the address did not stay on https://dynapro.co.uk ...". +6. Slow download (throttle the adapter): click Pause. Expected: "Pausing...", then "Paused: x of y MB downloaded." with the button Resume; Resume continues from the `.part` (Range request) and the final file is accepted; Cancel while paused finishes the page at once. +7. Drop the network in the middle of a download: it retries by itself; after the last retry the error page offers "Continue download" (the `.part` is kept). Continue after reconnecting finishes without starting over. +8. "Choose file..." on the error page with a valid `Dyna Pro USB.zip` / DynaRun zip, and with a file that is not DynaRun's ("This is not the DynaRun V3 setup"): the first is accepted, the second refused with a message; neither is deleted by the wizard. +9. With the fix already installed and the network off: the first page must still appear; the "newer DynaRun" hint simply is not shown (the check says nothing on failure; a line in `DynaRunFix-setup.log`). +10. No network during the driver step after the restart: the install uses the staged copy, it needs no network. + +## 8. Update check and offer + +Needs a release build (a dev build never updates) and a GitHub release newer than the installed version, or a lower-version build installed on purpose. Network on. Start DynaRun only through the shortcut (the launcher). + +1. Start DynaRun through the launcher with `UpdateChecked` older than a day (delete the value). Expected in `dynafix.log`: `launcher: run_setup: ... /checkupdate -- reason: the last check is more than a day old`, then `launcher: looking for an update (0 no setup, 1 started): 1`. DynaRun opens first and is not delayed. `update:` lines say `latest is vX: nothing to do` or `vX is newer: noted, asked at the next start of DynaRun`. +2. Start DynaRun again. Expected: the question beside the running DynaRun: "A new version of DynaRunFix is available: vX (this is vY). Yes: install it after you close DynaRun ... No: skip this version ... Cancel, or closing this window: ask again tomorrow." and `dynafix.log` `launcher: offering the update to vX after DynaRun was started`. +3. Answer Cancel: asked again tomorrow (`UpdateAsk` set), `update` line `asked about vX while DynaRun runs: 2`. Start again the same day: `launcher: noted update vX not offered (already asked today)`. +4. Answer No: `UpdateSkip` = vX; later starts log `... not offered (skipped by the user)`. A still newer release is offered again. +5. Answer Yes, then close DynaRun. Expected: the page "Downloading the update..." / "Installing the update...", `DynaRunFix-Setup-vX.exe /quiet /keep` ONE UAC prompt, the pin / Run value / desktop shortcut unchanged; log `the new setup ended with 0`. The next start shows the new version. +6. `DYNAFIX_NOUPDATE=1` in the environment: `launcher: update check skipped (DYNAFIX_NOUPDATE)`, no question. `/autostart`, `/restart` and `/noupdate` launches never ask. +7. Tamper cases (optional): SHA mismatch (release file replaced) -> `SHA-256 does not match: not installed`, nothing runs; no network -> the check says nothing. + +## 9. Launcher log (`%TEMP%\dynafix.log`) + +Every line starts with the date and time and ends with CRLF; a line has at most 1000 bytes (longer ones end in `...`). Start from a deleted log. + +1. Start DynaRun via the shortcut. Expected, in order: `launcher: DynaRun pid=N gui-thread=N`, `launcher: gui-thread chosen from window class [..], rank 4 ...`, `launcher: dynafix.dll loaded in N ms (error 0)` (or the early-hook lines `launcher: DynaRun pid=N hooked from start (thread N)` and `launcher: hook set N ms after DynaRun was resumed, dll reported in after N ms`); then dll lines (`skipped THBResize 0x591 after duplicate WM_SIZE ...`, fonts, `cleared cloud attributes ...`). +2. No `launcher: dynafix.dll did not report in` and no `early hook did not report in` / `early hook failed`. +3. First start of a fresh profile (no `HKCU\Software\DynaPro`): `setup helper started, waiting for it`, `setup helper finished, exit code 0`, `restarting DynaRun to load the new configuration`; DynaRun restarts by itself. +4. Start the launcher elevated (Run as administrator on `DynaRunFix.exe`): `launcher: elevated; started itself with the desktop shell's token: 1 (error 0)`, DynaRun runs non-elevated. With UAC off: `launcher: the desktop shell token is elevated ... carrying on in this process`. +5. Start the launcher while DynaRun already runs (second click): the existing instance is used, no second DynaRun; if that DynaRun is elevated: `launcher: the running DynaRun cannot be opened (it runs with more rights than this launcher)`. +6. A log larger than 10 MB at start is cut to its newest 8 MB from a full line on (copy a big file over `dynafix.log` first). Two DynaRuns / the launcher and the dll writing at once leave no torn lines. +7. `%TEMP%\DynaRunFix-setup.log` and `setup.log` lines are time-stamped; the mutex `Local\DynaRunFix.dynafix.log` is shared by the launcher, the dll and `update` (a log written by an elevated process and a normal one in turn must not fail). From 2db02630adc4e326fd2aa6f803288eef117780d3 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:32:55 +0800 Subject: [PATCH 06/41] Tests: characterization tests for command line, elevated arguments and result codes Locks today's behaviour: the switch parsing of setup.c and driver_verb, the argument string run_elevated builds (ShellExecuteEx stubbed), FIX_DONE for 0..40 and the PK_/DRV_/RC_ values. The two parsing loops are moved unchanged into static helpers (parse_switches, prep_accept_arg) so they can be called; no behaviour change. test_hardening.c gets the missing driver_prep_stage stub. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 8 ++- src/setup.c | 30 +++++--- tests/test_driver.c | 62 +++++++++++++++++ tests/test_hardening.c | 153 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 12 deletions(-) diff --git a/src/driver.c b/src/driver.c index 5974ba1..2d9c940 100644 --- a/src/driver.c +++ b/src/driver.c @@ -1329,6 +1329,12 @@ void driver_machine_uninstall(void) /* ---------- command line ---------- */ +// /driver-prep [/unverified ]: the accepted hash only as argv[4] /unverified argv[5] with a 64 character value, else NULL (tests/test_driver.c locks this) +static const WCHAR *prep_accept_arg(int argc, WCHAR **argv) +{ + return argc >= 6 && !lstrcmpiW(argv[4], L"/unverified") && lstrlenW(argv[5]) == 64 ? argv[5] : NULL; +} + int driver_verb(int argc, WCHAR **argv) { const WCHAR *v = argv[1]; @@ -1336,7 +1342,7 @@ int driver_verb(int argc, WCHAR **argv) if (!lstrcmpiW(v, L"/driver-continue")) { g_drvmode = DRV_MODE_CONTINUE; return wizard(); } if (lstrcmpiW(v, L"/driver-prep") && lstrcmpiW(v, L"/driver-install") && lstrcmpiW(v, L"/driver-cancel")) return DRV_NOVERB; if (!is_admin()) return 1; // the elevated parts: only with the rights they need - if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], argc >= 6 && !lstrcmpiW(argv[4], L"/unverified") && lstrlenW(argv[5]) == 64 ? argv[5] : NULL) : 1; + if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], prep_accept_arg(argc, argv)) : 1; if (!lstrcmpiW(v, L"/driver-install")) return install_locked(); return driver_cancel(); } diff --git a/src/setup.c b/src/setup.c index f0141b3..aa4c108 100644 --- a/src/setup.c +++ b/src/setup.c @@ -2314,25 +2314,33 @@ static const WCHAR *after_args(int n) return c; } -void WinMainCRTStartup(void) +// The switches of the command line, at any position (tests/test_hardening.c locks the rules). g_quiet, g_close, g_pin, g_autostart and g_keep are set; +// *pin: /pin seen; *dzip, *daccept: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). +static void parse_switches(int argc, WCHAR **argv, BOOL *pin, const WCHAR **dzip, const WCHAR **daccept) { - typedef BOOL (WINAPI *SetDefaultDllDirectories_t)(DWORD); - SetDefaultDllDirectories_t sdd = (SetDefaultDllDirectories_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetDefaultDllDirectories"); - int argc, i, rc; BOOL pin = FALSE; WCHAR **argv; const WCHAR *dzip = NULL, *daccept = NULL; - if (sdd) sdd(0x800); // LOAD_LIBRARY_SEARCH_SYSTEM32: no DLL is searched for in the exe's folder or the current folder - argv = CommandLineToArgvW(GetCommandLineW(), &argc); - g_zh = PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE; - init_paths(); + int i; for (i = 1; i < argc; i++) { if (!lstrcmpiW(argv[i], L"/quiet")) g_quiet = TRUE; else if (!lstrcmpiW(argv[i], L"/close")) g_close = TRUE; else if (!lstrcmpiW(argv[i], L"/notaskbar")) g_pin = FALSE; else if (!lstrcmpiW(argv[i], L"/noautostart")) g_autostart = FALSE; - else if (!lstrcmpiW(argv[i], L"/pin")) pin = TRUE; + else if (!lstrcmpiW(argv[i], L"/pin")) *pin = TRUE; else if (!lstrcmpiW(argv[i], L"/keep")) g_keep = TRUE; - else if (i >= 4 && !lstrcmpiW(argv[i], L"/with-driver") && i + 1 < argc) dzip = argv[++i]; // after the SID and the path: see install_fix_driver - else if (i >= 4 && !lstrcmpiW(argv[i], L"/unverified") && i + 1 < argc) { if (lstrlenW(argv[i + 1]) == 64) daccept = argv[i + 1]; i++; } + else if (i >= 4 && !lstrcmpiW(argv[i], L"/with-driver") && i + 1 < argc) *dzip = argv[++i]; // after the SID and the path: see install_fix_driver + else if (i >= 4 && !lstrcmpiW(argv[i], L"/unverified") && i + 1 < argc) { if (lstrlenW(argv[i + 1]) == 64) *daccept = argv[i + 1]; i++; } } +} + +void WinMainCRTStartup(void) +{ + typedef BOOL (WINAPI *SetDefaultDllDirectories_t)(DWORD); + SetDefaultDllDirectories_t sdd = (SetDefaultDllDirectories_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetDefaultDllDirectories"); + int argc, rc; BOOL pin = FALSE; WCHAR **argv; const WCHAR *dzip = NULL, *daccept = NULL; + if (sdd) sdd(0x800); // LOAD_LIBRARY_SEARCH_SYSTEM32: no DLL is searched for in the exe's folder or the current folder + argv = CommandLineToArgvW(GetCommandLineW(), &argc); + g_zh = PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE; + init_paths(); + parse_switches(argc, argv, &pin, &dzip, &daccept); if (argc >= 2 && (!lstrcmpiW(argv[1], L"/machine") || !lstrcmpiW(argv[1], L"/full"))) g_pin = pin; // the elevated stage: as asked if (argc >= 4 && !lstrcmpiW(argv[1], L"/machine")) { lstrcpynW(g_exe, argv[3], MAX_PATH); diff --git a/tests/test_driver.c b/tests/test_driver.c index 939a527..8deb3ca 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -879,6 +879,67 @@ static void test_real_zip(void) } } +/* ---------- characterization tests of the command line of driver.c (lock today's behaviour) ---------- */ + +// driver_verb: /driver-prep [/unverified ]. The accepted hash is taken ONLY as argv[4] == /unverified with argv[5] of exactly +// 64 characters; anywhere else (or another length) it is silently NULL (the prep then asks for the exact hash by other means / refuses). +// Differs from setup.c's parse_switches (tests\test_hardening.c), which finds /unverified at any index >= 4, takes any following argument as its +// value and drops one that is not 64 characters long: there a bad value is consumed, here it simply is not at argv[4]. +// The verb itself: /driver and /driver-continue open the wizard (not run here); /driver-prep, /driver-install and /driver-cancel need an +// administrator (return 1 otherwise) and /driver-prep needs argc >= 4 (return 1 otherwise); anything else is DRV_NOVERB. +static void test_cmdline(void) +{ + WCHAR h64[80], h63[80], h65[80]; + WCHAR *a1[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/unverified", h64 }; + WCHAR *a2[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/unverified", h63 }; + WCHAR *a3[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/unverified", h65 }; + WCHAR *a4[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/UNVERIFIED", h64 }; + WCHAR *a5[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/unverified" }; + WCHAR *a6[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/quiet", L"/unverified", h64 }; + WCHAR *a7[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", h64 }; + WCHAR *a8[] = { L"s", L"/driver-prep", L"S-1", L"z.zip", L"/with-driver", h64 }; + WCHAR *a9[] = { L"s", L"/driver-prep", L"S-1", L"z.zip" }; + WCHAR *n1[] = { L"s", L"/nothing" }; + WCHAR *n2[] = { L"s", L"/machine", L"S-1", L"e" }; + WCHAR *n3[] = { L"s", L"/driver-prepx" }; + WCHAR *n4[] = { L"s", L"driver-prep" }; + WCHAR *n5[] = { L"s", L"/uninstall" }; + WCHAR *p1[] = { L"s", L"/driver-prep" }; + WCHAR *p2[] = { L"s", L"/DRIVER-PREP", L"S-1" }; + WCHAR *i1[] = { L"s", L"/driver-install" }; + WCHAR *c1[] = { L"s", L"/Driver-Cancel" }; + const WCHAR *r; + lstrcpyW(h64, L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); lstrcpyW(h63, h64); h63[63] = 0; + lstrcpyW(h65, h64); lstrcatW(h65, L"0"); + CHECK(lstrlenW(h64) == 64 && lstrlenW(h63) == 63 && lstrlenW(h65) == 65, L"cmdline: test hashes have 64, 63 and 65 characters"); + + // prep_accept_arg: the accepted hash of /driver-prep + r = prep_accept_arg(6, a1); CHECK(r == h64, L"accept: /unverified + 64 characters at argv[4], argv[5]"); + CHECK(prep_accept_arg(6, a2) == NULL, L"accept: 63 characters: NULL"); + CHECK(prep_accept_arg(6, a3) == NULL, L"accept: 65 characters: NULL"); + CHECK(prep_accept_arg(6, a4) == h64, L"accept: /UNVERIFIED, case-insensitive"); + CHECK(prep_accept_arg(5, a5) == NULL, L"accept: /unverified without a value: NULL"); + CHECK(prep_accept_arg(7, a6) == NULL, L"accept: /unverified at argv[5] (not argv[4]): NULL (setup.c would take it at index 5)"); + CHECK(prep_accept_arg(5, a7) == NULL, L"accept: a bare hash without /unverified: NULL"); + CHECK(prep_accept_arg(6, a8) == NULL, L"accept: /with-driver is not understood here: NULL"); + CHECK(prep_accept_arg(4, a9) == NULL, L"accept: nothing after the zip: NULL"); + + // the verb dispatch; only what is safe to run in any account + CHECK(driver_verb(2, n1) == DRV_NOVERB, L"verb: unknown verb is DRV_NOVERB"); + CHECK(driver_verb(4, n2) == DRV_NOVERB, L"verb: /machine is not a driver verb"); + CHECK(driver_verb(2, n3) == DRV_NOVERB, L"verb: exact match only (/driver-prepx)"); + CHECK(driver_verb(2, n4) == DRV_NOVERB, L"verb: no slash, no verb"); + CHECK(driver_verb(2, n5) == DRV_NOVERB, L"verb: /uninstall is setup.c's"); + CHECK(driver_verb(2, p1) == 1, L"verb: /driver-prep without arguments returns 1 (not admin: 1 as well)"); + CHECK(driver_verb(3, p2) == 1, L"verb: /DRIVER-PREP without a zip returns 1 (case-insensitive verb)"); + if (!is_admin()) { + // elevated verbs refuse a normal process before doing anything + CHECK(driver_verb(2, i1) == 1, L"verb: /driver-install not elevated returns 1"); + CHECK(driver_verb(2, c1) == 1, L"verb: /driver-cancel not elevated returns 1"); + CHECK(driver_verb(6, a1) == 1, L"verb: /driver-prep with arguments, not elevated, returns 1"); + } else wprintf(L" (info: running elevated - /driver-install, /driver-cancel and /driver-prep with arguments are not called)\n"); +} + int wmain(void) { g_quiet = TRUE; g_zh = FALSE; @@ -902,6 +963,7 @@ int wmain(void) wprintf(L"[job table]\n"); test_job_table(); wprintf(L"[shared package machinery]\n"); test_shared(); wprintf(L"[update hint]\n"); test_hint(); + wprintf(L"[command line]\n"); test_cmdline(); wprintf(L"[real zip]\n"); test_real_zip(); delete_tree(g_base, NULL, 0); wprintf(L"\n%d passed, %d failed\n", g_pass, g_fail); diff --git a/tests/test_hardening.c b/tests/test_hardening.c index 1c8f380..d2c943f 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -5,6 +5,20 @@ // HKCU\Software\DynaRunFixTest- (deleted at the end). The real DynaRun and DynaRunFix installs, HKLM and the settings // of the machine are not changed; the one look at the machine is read-only (the ACL of an installed DynaRun folder, printed). #define WinMainCRTStartup setup_main_not_used_here +// run_elevated is tested without a UAC prompt: its one ShellExecuteExW call is replaced by a recorder (setup.c calls it nowhere else) +#define COBJMACROS +#include +#include +static int g_se_calls, g_se_err = ERROR_CANCELLED; static WCHAR g_se_verb[64], g_se_file[MAX_PATH], g_se_params[2000]; +static BOOL WINAPI test_ShellExecuteExW(SHELLEXECUTEINFOW *se) +{ + g_se_calls++; + lstrcpynW(g_se_verb, se->lpVerb ? se->lpVerb : L"(null)", 64); lstrcpynW(g_se_file, se->lpFile ? se->lpFile : L"(null)", MAX_PATH); + lstrcpynW(g_se_params, se->lpParameters ? se->lpParameters : L"(null)", 2000); + SetLastError(g_se_err); + return FALSE; +} +#define ShellExecuteExW test_ShellExecuteExW #include "../src/setup.c" #include @@ -15,6 +29,7 @@ int update_offer(void) { return 0; } int update_run(const WCHAR *a) { (void)a; return 0; } // driver.c is not part of this test (it would touch ProgramData and the Start menu): tests\test_driver.c covers it int driver_verb(int argc, WCHAR **argv) { (void)argc; (void)argv; return DRV_NOVERB; } +int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { (void)sid; (void)zip; (void)accept; return 0; } // called by install_fix_driver / install_all_driver (not run here) void driver_machine_stage(const WCHAR *p) { (void)p; } void driver_machine_uninstall(void) { } @@ -774,6 +789,141 @@ static void test_shortcut_paths(void) CHECK(!launcher_path_like(L"DynaRunFix.exe"), L"no folder"); } +/* ---------- characterization tests: they lock what the code does TODAY (refactor safety net), odd or not ---------- */ + +// run_elevated builds "[ /quiet][ /close][ /pin][ /keep]" in this order, whatever the caller put in args. Only these four globals are +// forwarded: g_autostart and g_autostart_later (and the /notaskbar, /noautostart switches) never reach the elevated stage. The 1024 character +// buffer is not checked (oddity: a longer args overflows it), so no test goes near that. +static void test_run_elevated_args(void) +{ + BOOL sq = g_quiet, sc = g_close, sp = g_pin, sk = g_keep, sa = g_autostart, sl = g_autostart_later; + int m, au, rc; WCHAR want[400]; const WCHAR *base = L"/machine S-1-5-21-1-2-3-1001 \"C:\\Program Files\\DynaRun\\DynaRun V3.exe\""; + const WCHAR *hash = L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + for (m = 0; m < 16; m++) { + for (au = 0; au < 4; au++) { + g_quiet = (m & 1) != 0; g_close = (m & 2) != 0; g_pin = (m & 4) != 0; g_keep = (m & 8) != 0; + g_autostart = (au & 1) != 0; g_autostart_later = (au & 2) != 0; // must not change the result + lstrcpyW(want, base); + if (m & 1) lstrcatW(want, L" /quiet"); + if (m & 2) lstrcatW(want, L" /close"); + if (m & 4) lstrcatW(want, L" /pin"); + if (m & 8) lstrcatW(want, L" /keep"); + g_se_calls = 0; g_se_err = ERROR_CANCELLED; + rc = run_elevated(base, L"C:\\x\\setup.exe"); + CHECK(g_se_calls == 1 && rc == 2, L"run_elevated: one call; a cancelled UAC prompt returns 2"); + CHECK(!lstrcmpW(g_se_params, want), L"run_elevated: args then /quiet /close /pin /keep, in that order"); + CHECK(!lstrcmpW(g_se_verb, L"runas") && !lstrcmpW(g_se_file, L"C:\\x\\setup.exe"), L"run_elevated: verb runas, the file given"); + } + } + // the driver arguments stay where the caller put them: before the forwarded switches + g_quiet = TRUE; g_close = TRUE; g_pin = TRUE; g_keep = TRUE; g_autostart = FALSE; g_autostart_later = TRUE; + wsprintfW(want, L"%s /with-driver \"C:\\z\\Dyna Pro USB.zip\" /unverified %s", base, hash); + run_elevated(want, L"C:\\x\\setup.exe"); + wsprintfW(want, L"%s /with-driver \"C:\\z\\Dyna Pro USB.zip\" /unverified %s /quiet /close /pin /keep", base, hash); + CHECK(!lstrcmpW(g_se_params, want), L"run_elevated: /with-driver ... /unverified ... first, then the forwarded switches"); + // no deduplication: a switch already in args is repeated + run_elevated(L"/driver-install /quiet /pin", L"C:\\x\\setup.exe"); + CHECK(!lstrcmpW(g_se_params, L"/driver-install /quiet /pin /quiet /close /pin /keep"), L"run_elevated: no deduplication of switches already in args"); + // empty args: the switches follow a leading blank + g_quiet = TRUE; g_close = FALSE; g_pin = FALSE; g_keep = FALSE; + run_elevated(L"", L"C:\\x\\setup.exe"); + CHECK(!lstrcmpW(g_se_params, L" /quiet"), L"run_elevated: empty args give a leading blank"); + // other failures of ShellExecuteEx return 1 + g_se_err = ERROR_ACCESS_DENIED; g_se_calls = 0; + CHECK(run_elevated(L"/driver-cancel", L"C:\\x\\setup.exe") == 1 && g_se_calls == 1, L"run_elevated: another ShellExecuteEx error returns 1"); + g_se_err = ERROR_CANCELLED; + g_quiet = sq; g_close = sc; g_pin = sp; g_keep = sk; g_autostart = sa; g_autostart_later = sl; +} + +// parse_switches (the command line loop of WinMainCRTStartup, moved into a helper unchanged). Rules today: +// - every argument from argv[1] on is looked at, also the SID and the path of /machine (argv[2], argv[3]); argv[0] is not; compare is case-insensitive; +// - /with-driver and /unverified are honoured only at index 4 or later (after verb, SID, path) and only with a following argument; +// - they take the next argument as their value WHATEVER it is (oddity: "/unverified /quiet" swallows /quiet; so does "/with-driver /quiet"); +// - /unverified keeps its value only when exactly 64 characters long, otherwise the value is consumed and dropped (no error); +// - unknown arguments are ignored; the last /with-driver wins; a later /unverified with a bad length does not clear an earlier good one. +// (driver.c's driver_verb parses /driver-prep differently: see test_driver.c.) The dispatch on argv[1] (/machine needs argc >= 4, ...) is not covered here. +static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da; +static void parse_case(int argc, WCHAR **argv) +{ + g_quiet = FALSE; g_close = FALSE; g_pin = TRUE; g_autostart = TRUE; g_keep = FALSE; + g_pin_seen = FALSE; g_dz = NULL; g_da = NULL; + parse_switches(argc, argv, &g_pin_seen, &g_dz, &g_da); +} +#define PARSE(...) do { WCHAR *av_[] = { __VA_ARGS__ }; parse_case((int)(sizeof(av_) / sizeof(av_[0])), av_); } while (0) +static void test_parse_switches(void) +{ + BOOL sq = g_quiet, sc = g_close, sp = g_pin, sk = g_keep, sa = g_autostart; + WCHAR h64[80], h63[80]; + lstrcpyW(h64, L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); lstrcpyW(h63, h64); h63[63] = 0; + CHECK(lstrlenW(h64) == 64 && lstrlenW(h63) == 63, L"parse: test hashes have 64 and 63 characters"); + + PARSE(L"setup.exe"); + CHECK(!g_quiet && !g_close && g_pin && g_autostart && !g_keep && !g_pin_seen && !g_dz && !g_da, L"parse: no arguments change nothing"); + PARSE(L"setup.exe", L"/quiet", L"/close", L"/keep", L"/pin"); + CHECK(g_quiet && g_close && g_keep && g_pin_seen && g_pin && g_autostart, L"parse: /quiet /close /keep /pin"); + PARSE(L"setup.exe", L"/notaskbar", L"/noautostart"); + CHECK(!g_pin && !g_autostart && !g_pin_seen && !g_quiet, L"parse: /notaskbar clears g_pin, /noautostart clears g_autostart (the local pin stays FALSE)"); + PARSE(L"setup.exe", L"/QUIET", L"/Close", L"/KEEP", L"/PIN", L"/NoTaskbar"); + CHECK(g_quiet && g_close && g_keep && g_pin_seen && !g_pin, L"parse: case-insensitive"); + PARSE(L"/quiet"); + CHECK(!g_quiet, L"parse: argv[0] is never looked at"); + PARSE(L"setup.exe", L"/frobnicate", L"quiet", L"-quiet", L"/quiet:1", L"/quiet "); + CHECK(!g_quiet && !g_close && !g_keep && !g_pin_seen, L"parse: unknown, dash and decorated switches are ignored (exact match only)"); + PARSE(L"setup.exe", L"/machine", L"/quiet", L"/close"); + CHECK(g_quiet && g_close, L"parse: switches are found at the SID and path positions too"); + + PARSE(L"s", L"/machine", L"S-1-5-21-1", L"C:\\e.exe", L"/with-driver", L"C:\\z.zip"); + CHECK(g_dz && !lstrcmpW(g_dz, L"C:\\z.zip") && !g_da, L"parse: /with-driver at index 4 takes the zip"); + PARSE(L"s", L"/machine", L"S-1-5-21-1", L"/with-driver", L"C:\\z.zip"); + CHECK(!g_dz, L"parse: /with-driver at index 3 is ignored (index >= 4 only)"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/with-driver"); + CHECK(!g_dz, L"parse: /with-driver as the last argument (no value) is ignored"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h64); + CHECK(g_da && lstrlenW(g_da) == 64 && !lstrcmpW(g_da, h64), L"parse: /unverified at index 4 with a 64 character value"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h63, L"/quiet"); + CHECK(!g_da && g_quiet, L"parse: /unverified with a 63 character value: value consumed and dropped, the next switch still counts"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", L"/quiet"); + CHECK(!g_da && !g_quiet, L"parse: ODDITY /unverified swallows a following switch as its value"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/with-driver", L"/quiet"); + CHECK(g_dz && !lstrcmpW(g_dz, L"/quiet") && !g_quiet, L"parse: ODDITY /with-driver swallows a following switch as the zip"); + PARSE(L"s", L"/machine", L"S-1", L"/unverified", h64); + CHECK(!g_da, L"parse: /unverified at index 3 is ignored"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h64, L"/with-driver", L"z.zip", L"/pin"); + CHECK(g_da && g_dz && !lstrcmpW(g_dz, L"z.zip") && g_pin_seen, L"parse: /unverified before /with-driver, then /pin"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/with-driver", L"a.zip", L"/with-driver", L"b.zip"); + CHECK(g_dz && !lstrcmpW(g_dz, L"b.zip"), L"parse: the last /with-driver wins"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h64, L"/unverified", h63); + CHECK(g_da && !lstrcmpW(g_da, h64), L"parse: a later short /unverified does not clear an earlier accepted one"); + PARSE(L"s", L"/machine", L"S-1", L"e", L"/WITH-DRIVER", L"z", L"/UNVERIFIED", h64); + CHECK(g_da && g_dz, L"parse: the driver switches are case-insensitive too"); + g_quiet = sq; g_close = sc; g_pin = sp; g_keep = sk; g_autostart = sa; +} + +// FIX_DONE(rc): the elevated install stage "installed the fix" = rc 0, or the driver step gave a code >= DRV_E_HASH (the fix is in place, the driver +// step reports). Everything else (1..19, a negative, 2 = cancelled, 6 = RC_RUNNING) means the fix did not complete. Oddities: no upper bound +// (1000 and any exit code above 19 count as done), negative numbers are not done, 31 (DRV_W_HASH) is a driver_check result, never an exit +// code of the stage, but would count as done. +static void test_result_codes(void) +{ + static const int done[41] = { 1,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1, 1 }; + int rc; + for (rc = 0; rc <= 40; rc++) { + WCHAR w[60]; wsprintfW(w, L"FIX_DONE(%d) as today", rc); + CHECK(!!FIX_DONE(rc) == done[rc], w); + } + CHECK(!FIX_DONE(-1) && !FIX_DONE(DRV_NOVERB), L"FIX_DONE: negative codes are not done"); + CHECK(FIX_DONE(41) && FIX_DONE(255) && FIX_DONE(1000), L"FIX_DONE: no upper bound"); + CHECK(!FIX_DONE(2) && !FIX_DONE(RC_RUNNING) && !FIX_DONE(3) && !FIX_DONE(4) && !FIX_DONE(5) && !FIX_DONE(19), L"FIX_DONE: cancel, running, over-the-shoulder and the small failures are not done"); + + CHECK(PK_OK == 0 && PK_CANCEL == 1 && PK_NET == 2 && PK_HTTP == 3 && PK_IO == 4 && PK_FORMAT == 5 && PK_PASSWORD == 6 && PK_CERT == 7 && PK_URL == 8, L"PK_* values 0..8"); + CHECK(RC_RUNNING == 6, L"RC_RUNNING is 6 (the same number as PK_PASSWORD: different domains, an exit code vs a download result)"); + CHECK(DRV_SUCCESS == 0 && DRV_E_HASH == 20 && DRV_E_EXTRACT == 21 && DRV_E_INF == 22 && DRV_E_COPY == 23 && DRV_E_BCD == 24 && + DRV_E_TASK == 25 && DRV_E_PNP == 26 && DRV_E_DIR == 27 && DRV_E_SID == 28 && DRV_E_ENFORCED == 29, L"DRV_E_* values 20..29"); + CHECK(DRV_W_BDE == 30 && DRV_W_HASH == 31 && DRV_NOVERB == -1000, L"DRV_W_BDE 30, DRV_W_HASH 31, DRV_NOVERB -1000"); + // Not named in the code, used as bare numbers: 1 = failure, 2 = cancelled (also PK_NET's value), 3 = machine stage asks for over-the-shoulder + // handling (install_all_driver), 4 = the DynaRun MSI failed, 5 = DynaRun not found after the MSI. +} + int wmain(void) { HKEY k; @@ -796,6 +946,9 @@ int wmain(void) wprintf(L"[append line]\n"); test_append_line(); wprintf(L"[misc]\n"); test_misc(); wprintf(L"[shortcut paths]\n"); test_shortcut_paths(); + wprintf(L"[run_elevated arguments]\n"); test_run_elevated_args(); + wprintf(L"[command line switches]\n"); test_parse_switches(); + wprintf(L"[result codes]\n"); test_result_codes(); { // clean up WCHAR c[1000]; wsprintfW(c, L"reg delete \"HKCU\\%s\" /f >nul 2>nul", g_testkey); sh(c); From 06091158f006b9aba44786269b41d8a5f73c7eae Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:39:08 +0800 Subject: [PATCH 07/41] Move promote_com_in and zip_extract_all out of the shipped code into the tests Neither function has a caller in the shipped exe: the installer no longer copies COM registrations (promote_com_in, with reg_export, skipped and g_export_root, only used by it), and nothing unpacks a whole zip (zip_extract_all, xall_visit, make_dirs, ZIP_MAX_*). They now live in tests\promote_com.h (included by test_hardening.c) and tests\zip_extract_all.h (included by test_driver.c), so the tests that use them keep compiling and the code does not rot. No change to the shipped behaviour. Co-Authored-By: Claude Opus 5.5 --- src/package.c | 51 -------------------------- src/setup.c | 78 +--------------------------------------- src/setup.h | 3 -- tests/promote_com.h | 80 +++++++++++++++++++++++++++++++++++++++++ tests/test_driver.c | 1 + tests/test_hardening.c | 1 + tests/zip_extract_all.h | 55 ++++++++++++++++++++++++++++ 7 files changed, 138 insertions(+), 131 deletions(-) create mode 100644 tests/promote_com.h create mode 100644 tests/zip_extract_all.h diff --git a/src/package.c b/src/package.c index 59c6220..ea7a002 100644 --- a/src/package.c +++ b/src/package.c @@ -844,57 +844,6 @@ BOOL zip_name_safe(const char *name, DWORD nlen, BOOL *isdir) return TRUE; } -#define ZIP_MAX_FILES 500 -#define ZIP_MAX_ENTRY (32UL * 1024UL * 1024UL) -#define ZIP_MAX_TOTAL (96UL * 1024UL * 1024UL) - -typedef struct { const WCHAR *dest; volatile LONG *cancel; int files, entries; DWORD total; int rc; } xall; - -static BOOL make_dirs(WCHAR *path, int from) // creates every folder of path beyond the first `from` characters -{ - int i; - for (i = from; path[i]; i++) { - if (path[i] != '\\') continue; - path[i] = 0; - if (!CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { path[i] = '\\'; return FALSE; } - path[i] = '\\'; - } - return TRUE; -} - -static BOOL xall_visit(const char *name, DWORD nlen, const zipent *e, void *ctx) -{ - xall *x = (xall *)ctx; static WCHAR path[MAX_PATH]; BOOL isdir; int dl = lstrlenW(x->dest), n, i; DWORD a; - if (*x->cancel) { x->rc = PK_CANCEL; return FALSE; } - if (!zip_name_safe(name, nlen, &isdir) || (e->flags & 1) || (e->method != 0 && e->method != 8) || - ++x->entries > ZIP_MAX_FILES || e->usize > ZIP_MAX_ENTRY || (x->total += e->usize) > ZIP_MAX_TOTAL || - dl + 2 + (int)nlen >= MAX_PATH - 1) { x->rc = PK_FORMAT; return FALSE; } - lstrcpyW(path, x->dest); path[dl] = '\\'; - n = MultiByteToWideChar((e->flags & 0x800) ? CP_UTF8 : CP_OEMCP, 0, name, isdir ? (int)nlen - 1 : (int)nlen, path + dl + 1, MAX_PATH - dl - 2); - if (n <= 0) { x->rc = PK_FORMAT; return FALSE; } - path[dl + 1 + n] = 0; - for (i = dl + 1; path[i]; i++) if (path[i] == '/') path[i] = '\\'; - if (!make_dirs(path, dl + 1) || (isdir && !CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)) { x->rc = PK_IO; return FALSE; } - if (isdir) return TRUE; - a = GetFileAttributesW(path); - if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY)) { x->rc = PK_FORMAT; return FALSE; } // a file named like a folder made earlier - x->rc = zip_extract(e, "", path, NULL, x->cancel); - if (x->rc == PK_PASSWORD) x->rc = PK_FORMAT; // CRC or size mismatch: the entry is damaged - if (x->rc != PK_OK) return FALSE; - x->files++; - return TRUE; -} - -int zip_extract_all(const WCHAR *zip, const WCHAR *dest, volatile LONG *cancel, int *nfiles) -{ - xall x; int rc; - zero(&x, sizeof(x)); x.dest = dest; x.cancel = cancel; x.rc = PK_OK; - rc = zip_walk(zip, xall_visit, &x); - if (nfiles) *nfiles = x.files; - if (rc == PK_CANCEL) return x.rc != PK_OK ? x.rc : PK_CANCEL; - return rc; -} - /* ---------- the files of a package named in a pinned table: safe unpacking, copying and checking ---------- */ // Used by the driver (its 18 files) and by the DynaRun setup (pkg_prepare_target / pkg_extract_checked below): nothing a zip says about a path is ever used diff --git a/src/setup.c b/src/setup.c index aa4c108..606f8ba 100644 --- a/src/setup.c +++ b/src/setup.c @@ -739,7 +739,6 @@ static BOOL running_early(const WCHAR *stage) enum { K_CLSID = 1, K_PROGID, K_TYPELIB }; #define MAXKEYS 1024 static WCHAR *g_keys[MAXKEYS], *g_ids[MAXKEYS]; static int g_kinds[MAXKEYS], g_nkeys; -static const WCHAR *g_export_root = L"HKLM\\SOFTWARE\\Classes\\"; // where reg_export reads from (a test points it elsewhere) static void add_key(const WCHAR *k, int kind, const WCHAR *id) { @@ -964,82 +963,7 @@ static void delete_key_tree(HKEY root, const WCHAR *key) // the key and ev if (del) del(root, key, WOW, 0); else RegDeleteKeyW(root, key); } -static void reg_export(const WCHAR *key, const WCHAR *file) // backup with the native reg.exe -{ - static WCHAR exe[MAX_PATH], cmd[1024]; STARTUPINFOW si; PROCESS_INFORMATION pi; - GetWindowsDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\Sysnative\\reg.exe"); - if (!exists(exe)) { GetWindowsDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\System32\\reg.exe"); } - wsprintfW(cmd, L"reg.exe export \"%s%s\" \"%s\"", g_export_root, key, file); - zero(&si, sizeof(si)); si.cb = sizeof(si); - if (CreateProcessW(exe, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { - WaitForSingleObject(pi.hProcess, 30000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - } -} - -static void skipped(const WCHAR *key, const WCHAR *why) -{ - static WCHAR log[MAX_PATH], line[700]; - wsprintfW(line, L"%.400s: %s", key, why); - append_line(cat3(log, g_dir, L"\\com-skipped.txt", NULL), line); -} - -// hu: the user's Software\Classes, hm: HKLM\SOFTWARE\Classes (both opened with the 64-bit view). Returns the number of keys copied. -static int promote_com_in(HKEY hu, HKEY hm, WCHAR roots[][MAX_PATH], int nr) -{ - static const WCHAR *const views[] = { L"WOW6432Node\\CLSID", L"CLSID" }; - static WCHAR path[600], rel[300], id[128], v[MAX_PATH], prog[300], log[MAX_PATH], bak[MAX_PATH], line[700], tl[128], tlkey[200], bn[48]; - const WCHAR *why; HKEY list, s, d; DWORD i, n; int vi, j, copied = 0; WCHAR *dot; BOOL had, ok; - - g_nkeys = 0; - for (vi = 0; vi < 2; vi++) { - if (RegOpenKeyExW(hu, views[vi], 0, KEY_READ | WOW, &list)) continue; - for (i = 0; n = 128, !RegEnumKeyExW(list, i, id, &n, NULL, NULL, NULL, NULL); i++) { - cat3(rel, views[vi], L"\\", id); - if (!clsid_ok(hu, rel, roots, nr, &why)) { if (lstrcmpW(why, L"no server")) skipped(rel, why); continue; } - if (!reg_str(hm, cat3(path, rel, L"\\InprocServer32", NULL), NULL, v, MAX_PATH, WOW) && - !reg_str(hm, cat3(path, rel, L"\\LocalServer32", NULL), NULL, v, MAX_PATH, WOW)) add_key(rel, K_CLSID, id); - if (reg_str(hu, cat3(path, rel, L"\\ProgID", NULL), NULL, prog, 250, WOW)) { - for (j = 0; j < 2; j++) { // "Lib.Class.1" and its version-independent "Lib.Class" - if (j) { for (dot = prog + lstrlenW(prog); dot > prog && dot[-1] >= '0' && dot[-1] <= '9'; dot--); if (dot > prog && dot[-1] == '.' && *dot) dot[-1] = 0; else break; } - if (has_key(hu, prog) && !reg_str(hm, cat3(path, prog, L"\\CLSID", NULL), NULL, v, MAX_PATH, WOW)) { - if (progid_ok(hu, prog, id, &why)) add_key(prog, K_PROGID, id); else skipped(prog, why); - } - } - } - if (reg_str(hu, cat3(path, rel, L"\\TypeLib", NULL), NULL, tl, 128, WOW) && - has_key(hu, cat3(tlkey, L"TypeLib\\", tl, NULL)) && !typelib_complete(hm, tl)) { - if (typelib_ok(hu, tlkey, roots, nr, &why)) add_key(tlkey, K_TYPELIB, NULL); else skipped(tlkey, why); - } - } - RegCloseKey(list); - } - cat3(log, g_dir, L"\\com-copied.txt", NULL); - for (j = 0; j < g_nkeys; j++) { - had = has_key(hm, g_keys[j]); - if (had) { // keep what was there before - cat3(bak, g_dir, L"\\com-backup", NULL); CreateDirectoryW(bak, NULL); - wsprintfW(bn, L"\\%lu-%d.reg", GetTickCount(), j); - if (lstrlenW(bak) + lstrlenW(bn) < MAX_PATH) { lstrcatW(bak, bn); reg_export(g_keys[j], bak); } // bounded: a long install path just skips the backup file - } - if (RegOpenKeyExW(hu, g_keys[j], 0, KEY_READ | WOW, &s)) continue; - if (!RegCreateKeyExW(hm, g_keys[j], 0, NULL, 0, KEY_ALL_ACCESS | WOW, NULL, &d, NULL)) { - copy_tree(s, d); RegCloseKey(d); - // the copy in HKLM is what counts: only administrators can change it from here on - ok = g_kinds[j] == K_CLSID ? clsid_ok(hm, g_keys[j], roots, nr, &why) : - g_kinds[j] == K_PROGID ? progid_ok(hm, g_keys[j], g_ids[j], &why) : typelib_ok(hm, g_keys[j], roots, nr, &why); - if (ok) { append_line(log, cat3(line, L"HKLM\\SOFTWARE\\Classes\\", g_keys[j], NULL)); copied++; } - else { - skipped(g_keys[j], why); - if (!had) delete_key_tree(hm, g_keys[j]); // the key was not there before: it is not there now - } - } - RegCloseKey(s); - } - free_keys(); - return copied; -} - -// The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in above is kept only for the tests). +// The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in lives in tests\promote_com.h). /* ---------- shortcuts ---------- */ diff --git a/src/setup.h b/src/setup.h index eab6b84..4f89100 100644 --- a/src/setup.h +++ b/src/setup.h @@ -100,9 +100,6 @@ int zip_walk(const WCHAR *zip, zip_visit_fn fn, void *ctx); // An entry name that may be created below a folder: relative, '/' separators, no "..", ".", empty, "x:" / backslash / control or // reserved characters, no device names, no trailing dot or space. *isdir: the name ends in '/'. BOOL zip_name_safe(const char *name, DWORD nlen, BOOL *isdir); -// Extracts every entry (stored or deflate, not encrypted) below dest (which must exist); an unsafe name, a damaged entry or a zip -// larger than the limits fails the whole call (PK_FORMAT). *nfiles: files written. -int zip_extract_all(const WCHAR *zip, const WCHAR *dest, volatile LONG *cancel, int *nfiles); // The files of a package named in a pinned table (the driver's 18 files): safe unpacking / copying and checking (package.c). Used by the driver; the DynaRun setup // uses pkg_prepare_target / pkg_extract_checked. Nothing a zip says about a path is used, destinations are built from the table; links are refused; sizes capped. typedef struct { const WCHAR *rel; DWORD size; const WCHAR *sha; } pkg_file; // name below the package folder ('/' separators), size, SHA-256 (lower case) diff --git a/tests/promote_com.h b/tests/promote_com.h new file mode 100644 index 0000000..f6c7f73 --- /dev/null +++ b/tests/promote_com.h @@ -0,0 +1,80 @@ +// promote_com_in: copies COM registrations from HKU\\Software\Classes to HKLM\SOFTWARE\Classes, each only after the checks in src\setup.c +// (clsid_ok, progid_ok, typelib_ok, ...). The installer no longer does this, so the function is not part of the shipped code; it is kept here, compiled +// by tests\test_hardening.c (include it after src\setup.c), so that the checks stay exercised. + +static const WCHAR *g_export_root = L"HKLM\\SOFTWARE\\Classes\\"; // where reg_export reads from (a test points it elsewhere) + +static void reg_export(const WCHAR *key, const WCHAR *file) // backup with the native reg.exe +{ + static WCHAR exe[MAX_PATH], cmd[1024]; STARTUPINFOW si; PROCESS_INFORMATION pi; + GetWindowsDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\Sysnative\\reg.exe"); + if (!exists(exe)) { GetWindowsDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\System32\\reg.exe"); } + wsprintfW(cmd, L"reg.exe export \"%s%s\" \"%s\"", g_export_root, key, file); + zero(&si, sizeof(si)); si.cb = sizeof(si); + if (CreateProcessW(exe, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) { + WaitForSingleObject(pi.hProcess, 30000); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + } +} + +static void skipped(const WCHAR *key, const WCHAR *why) +{ + static WCHAR log[MAX_PATH], line[700]; + wsprintfW(line, L"%.400s: %s", key, why); + append_line(cat3(log, g_dir, L"\\com-skipped.txt", NULL), line); +} + +// hu: the user's Software\Classes, hm: HKLM\SOFTWARE\Classes (both opened with the 64-bit view). Returns the number of keys copied. +static int promote_com_in(HKEY hu, HKEY hm, WCHAR roots[][MAX_PATH], int nr) +{ + static const WCHAR *const views[] = { L"WOW6432Node\\CLSID", L"CLSID" }; + static WCHAR path[600], rel[300], id[128], v[MAX_PATH], prog[300], log[MAX_PATH], bak[MAX_PATH], line[700], tl[128], tlkey[200], bn[48]; + const WCHAR *why; HKEY list, s, d; DWORD i, n; int vi, j, copied = 0; WCHAR *dot; BOOL had, ok; + + g_nkeys = 0; + for (vi = 0; vi < 2; vi++) { + if (RegOpenKeyExW(hu, views[vi], 0, KEY_READ | WOW, &list)) continue; + for (i = 0; n = 128, !RegEnumKeyExW(list, i, id, &n, NULL, NULL, NULL, NULL); i++) { + cat3(rel, views[vi], L"\\", id); + if (!clsid_ok(hu, rel, roots, nr, &why)) { if (lstrcmpW(why, L"no server")) skipped(rel, why); continue; } + if (!reg_str(hm, cat3(path, rel, L"\\InprocServer32", NULL), NULL, v, MAX_PATH, WOW) && + !reg_str(hm, cat3(path, rel, L"\\LocalServer32", NULL), NULL, v, MAX_PATH, WOW)) add_key(rel, K_CLSID, id); + if (reg_str(hu, cat3(path, rel, L"\\ProgID", NULL), NULL, prog, 250, WOW)) { + for (j = 0; j < 2; j++) { // "Lib.Class.1" and its version-independent "Lib.Class" + if (j) { for (dot = prog + lstrlenW(prog); dot > prog && dot[-1] >= '0' && dot[-1] <= '9'; dot--); if (dot > prog && dot[-1] == '.' && *dot) dot[-1] = 0; else break; } + if (has_key(hu, prog) && !reg_str(hm, cat3(path, prog, L"\\CLSID", NULL), NULL, v, MAX_PATH, WOW)) { + if (progid_ok(hu, prog, id, &why)) add_key(prog, K_PROGID, id); else skipped(prog, why); + } + } + } + if (reg_str(hu, cat3(path, rel, L"\\TypeLib", NULL), NULL, tl, 128, WOW) && + has_key(hu, cat3(tlkey, L"TypeLib\\", tl, NULL)) && !typelib_complete(hm, tl)) { + if (typelib_ok(hu, tlkey, roots, nr, &why)) add_key(tlkey, K_TYPELIB, NULL); else skipped(tlkey, why); + } + } + RegCloseKey(list); + } + cat3(log, g_dir, L"\\com-copied.txt", NULL); + for (j = 0; j < g_nkeys; j++) { + had = has_key(hm, g_keys[j]); + if (had) { // keep what was there before + cat3(bak, g_dir, L"\\com-backup", NULL); CreateDirectoryW(bak, NULL); + wsprintfW(bn, L"\\%lu-%d.reg", GetTickCount(), j); + if (lstrlenW(bak) + lstrlenW(bn) < MAX_PATH) { lstrcatW(bak, bn); reg_export(g_keys[j], bak); } // bounded: a long install path just skips the backup file + } + if (RegOpenKeyExW(hu, g_keys[j], 0, KEY_READ | WOW, &s)) continue; + if (!RegCreateKeyExW(hm, g_keys[j], 0, NULL, 0, KEY_ALL_ACCESS | WOW, NULL, &d, NULL)) { + copy_tree(s, d); RegCloseKey(d); + // the copy in HKLM is what counts: only administrators can change it from here on + ok = g_kinds[j] == K_CLSID ? clsid_ok(hm, g_keys[j], roots, nr, &why) : + g_kinds[j] == K_PROGID ? progid_ok(hm, g_keys[j], g_ids[j], &why) : typelib_ok(hm, g_keys[j], roots, nr, &why); + if (ok) { append_line(log, cat3(line, L"HKLM\\SOFTWARE\\Classes\\", g_keys[j], NULL)); copied++; } + else { + skipped(g_keys[j], why); + if (!had) delete_key_tree(hm, g_keys[j]); // the key was not there before: it is not there now + } + } + RegCloseKey(s); + } + free_keys(); + return copied; +} diff --git a/tests/test_driver.c b/tests/test_driver.c index 8deb3ca..5cb217b 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -10,6 +10,7 @@ #define WinMainCRTStartup setup_main_not_used_here #include "../src/setup.c" #include "../src/driver.c" +#include "zip_extract_all.h" #include // what setup.c calls elsewhere and the tests do not need diff --git a/tests/test_hardening.c b/tests/test_hardening.c index d2c943f..ad8a5eb 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -20,6 +20,7 @@ static BOOL WINAPI test_ShellExecuteExW(SHELLEXECUTEINFOW *se) } #define ShellExecuteExW test_ShellExecuteExW #include "../src/setup.c" +#include "promote_com.h" #include // what setup.c calls elsewhere and the tests do not need diff --git a/tests/zip_extract_all.h b/tests/zip_extract_all.h new file mode 100644 index 0000000..12ffa85 --- /dev/null +++ b/tests/zip_extract_all.h @@ -0,0 +1,55 @@ +// zip_extract_all: extracts every entry (stored or deflate, not encrypted) of a zip below dest (which must exist); an unsafe name, a damaged entry or a zip +// larger than the limits fails the whole call (PK_FORMAT). *nfiles: files written. Nothing in the shipped code unpacks a whole zip (the driver and the +// DynaRun setup unpack only the entries a pinned table names), so it is not part of src\package.c; it is kept here, compiled by tests est_driver.c +// (include it after src\setup.c), as a user of zip_walk / zip_extract / zip_name_safe and for the extraction tests. + +#define ZIP_MAX_FILES 500 +#define ZIP_MAX_ENTRY (32UL * 1024UL * 1024UL) +#define ZIP_MAX_TOTAL (96UL * 1024UL * 1024UL) + +typedef struct { const WCHAR *dest; volatile LONG *cancel; int files, entries; DWORD total; int rc; } xall; + +static BOOL make_dirs(WCHAR *path, int from) // creates every folder of path beyond the first `from` characters +{ + int i; + for (i = from; path[i]; i++) { + if (path[i] != '\\') continue; + path[i] = 0; + if (!CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) { path[i] = '\\'; return FALSE; } + path[i] = '\\'; + } + return TRUE; +} + +static BOOL xall_visit(const char *name, DWORD nlen, const zipent *e, void *ctx) +{ + xall *x = (xall *)ctx; static WCHAR path[MAX_PATH]; BOOL isdir; int dl = lstrlenW(x->dest), n, i; DWORD a; + if (*x->cancel) { x->rc = PK_CANCEL; return FALSE; } + if (!zip_name_safe(name, nlen, &isdir) || (e->flags & 1) || (e->method != 0 && e->method != 8) || + ++x->entries > ZIP_MAX_FILES || e->usize > ZIP_MAX_ENTRY || (x->total += e->usize) > ZIP_MAX_TOTAL || + dl + 2 + (int)nlen >= MAX_PATH - 1) { x->rc = PK_FORMAT; return FALSE; } + lstrcpyW(path, x->dest); path[dl] = '\\'; + n = MultiByteToWideChar((e->flags & 0x800) ? CP_UTF8 : CP_OEMCP, 0, name, isdir ? (int)nlen - 1 : (int)nlen, path + dl + 1, MAX_PATH - dl - 2); + if (n <= 0) { x->rc = PK_FORMAT; return FALSE; } + path[dl + 1 + n] = 0; + for (i = dl + 1; path[i]; i++) if (path[i] == '/') path[i] = '\\'; + if (!make_dirs(path, dl + 1) || (isdir && !CreateDirectoryW(path, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)) { x->rc = PK_IO; return FALSE; } + if (isdir) return TRUE; + a = GetFileAttributesW(path); + if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY)) { x->rc = PK_FORMAT; return FALSE; } // a file named like a folder made earlier + x->rc = zip_extract(e, "", path, NULL, x->cancel); + if (x->rc == PK_PASSWORD) x->rc = PK_FORMAT; // CRC or size mismatch: the entry is damaged + if (x->rc != PK_OK) return FALSE; + x->files++; + return TRUE; +} + +int zip_extract_all(const WCHAR *zip, const WCHAR *dest, volatile LONG *cancel, int *nfiles) +{ + xall x; int rc; + zero(&x, sizeof(x)); x.dest = dest; x.cancel = cancel; x.rc = PK_OK; + rc = zip_walk(zip, xall_visit, &x); + if (nfiles) *nfiles = x.files; + if (rc == PK_CANCEL) return x.rc != PK_OK ? x.rc : PK_CANCEL; + return rc; +} From 2a1faa530a324f1a64985ccc2ad22d62b69e77b4 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:42:43 +0800 Subject: [PATCH 08/41] Wizard: the "Close DynaRun and install" consent covers one install job only The button on the "DynaRun is running" page set g_close to TRUE and nothing ever reset it. run_elevated forwards g_close as /close to every later elevated stage, and the stage then ends a running DynaRun with TerminateProcess without asking. So after one consent, a retry (UAC cancelled, install failed) that met a DynaRun started during the UAC prompt killed it instead of returning RC_RUNNING and showing the page again; running_early also skipped its check. The button now records that it set the flag (g_close_asked) and job_done takes it back when that job has ended. A /close given on the command line is not touched. The text check of wizard.c in test_driver.c covers both places. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 4 +++- tests/test_driver.c | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/wizard.c b/src/wizard.c index 8cb9ddc..7aa79cb 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -32,6 +32,7 @@ enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, static int g_page, g_job, g_dpi, g_lastpct = -1, g_pending, g_lastrun; // g_pending: install job waiting for DynaRun to close static volatile LONG g_cancel; +static BOOL g_close_asked; // g_close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) static BOOL g_busy, g_closing, g_installed, g_pkg_ours, g_pkg_found, g_status_err, g_badfile, g_nolicense; // g_nolicense: the setup's license text could not be read static int g_nfound; // packages pkg_find_local has handed us so far static HANDLE g_thread; @@ -1186,6 +1187,7 @@ static void choose_driver_source(void) static void job_done(int job, int rc) { WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; + if (g_close_asked) { g_close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) if (g_closing) { DestroyWindow(g_hwnd); return; } switch (job) { case JOB_ACQUIRE: acq_done(rc); break; @@ -1275,7 +1277,7 @@ static void on_primary(void) case P_CHECK: set_page(P_LICENSE); break; case P_LICENSE: go_install(JOB_INSTALL_ALL); break; case P_RUNNING: - g_close = TRUE; // the elevated stage closes DynaRun (see close_programs in setup.c) + if (!g_close) { g_close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only start_job(g_pending, P_INSTALL); break; case P_DONE: diff --git a/tests/test_driver.c b/tests/test_driver.c index 5cb217b..85088ef 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -776,6 +776,14 @@ static void test_job_table(void) if (!(next && next < end)) break; } CHECK(labels >= 7, L"all seven jobs were looked at"); + // g_close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it + // records that (g_close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since + a = strstr(t, "static void job_done"); end = a ? strstr(a, "switch (job)") : NULL; + CHECK(a && end, L"job_done is in src\\wizard.c"); + for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_close = FALSE", 15)) break; + CHECK(a && end && c < end, L"job_done takes back the \"close DynaRun\" consent of the job that has ended"); + for (c = t, labels = 0; (c = strstr(c, "g_close = TRUE")) != NULL; c += 14) labels++; + CHECK(labels == 1 && strstr(t, "g_close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_close only together with g_close_asked"); release(f); } From e66d23db53a9b3a43c84b50eef33221719d34d70 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:45:18 +0800 Subject: [PATCH 09/41] Move the remaining test-only COM helpers into the tests Since the installer stopped copying COM registrations, the whole check machinery that promote_com_in used stayed behind in src/setup.c with no shipped caller: add_key, free_keys, has_key, g_keys/g_ids/g_kinds/g_nkeys, com_roots, g_com_nfolder, under, com_path_ok, value_state, has_subkey, clsid_ok, progid_ok, typelib_ok, typelib_complete, copy_tree and delete_key_tree. They are only compiled into the shipped installer, never called. Move them to tests/promote_com.h, which test_hardening.c already includes after setup.c, so the same checks are still exercised. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 240 -------------------------------------------- tests/promote_com.h | 240 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 240 deletions(-) diff --git a/src/setup.c b/src/setup.c index 606f8ba..a8c6637 100644 --- a/src/setup.c +++ b/src/setup.c @@ -723,246 +723,6 @@ static BOOL running_early(const WCHAR *stage) return r && !g_close; } -/* ---------- COM registrations: HKU\\Software\Classes -> HKLM\SOFTWARE\Classes ---------- */ - -// DynaRun's installer registers its ActiveX controls for the installing user only (HKCU). Elevated DynaRun needs them in -// HKLM. A registration is copied only when every file it names (InprocServer32, LocalServer32, and the type library) is -// - a regular file that exists, given as an absolute path on a local drive (environment variables are expanded; relative, -// UNC and device paths are refused), -// - inside DynaRun's own folder (the one the installer found; only when that folder is not changeable by ordinary users) -// or inside the Windows System32 / SysWOW64 folder, -// - reached through no link (junction, symbolic link) below that folder, and -// - neither the file nor its folder changeable by ordinary users (owner, ACL: see user_writable). -// No file name is trusted. The registration is checked in the user's key before the copy and again in HKLM after it (the user -// may change their key in between); a copy that fails the second check is removed. - -enum { K_CLSID = 1, K_PROGID, K_TYPELIB }; -#define MAXKEYS 1024 -static WCHAR *g_keys[MAXKEYS], *g_ids[MAXKEYS]; static int g_kinds[MAXKEYS], g_nkeys; - -static void add_key(const WCHAR *k, int kind, const WCHAR *id) -{ - int i; - for (i = 0; i < g_nkeys; i++) if (!lstrcmpiW(g_keys[i], k)) return; - if (g_nkeys < MAXKEYS && (g_keys[g_nkeys] = alloc((lstrlenW(k) + 1) * sizeof(WCHAR)))) { - lstrcpyW(g_keys[g_nkeys], k); g_kinds[g_nkeys] = kind; - g_ids[g_nkeys] = id && (g_ids[g_nkeys] = alloc((lstrlenW(id) + 1) * sizeof(WCHAR))) ? lstrcpyW(g_ids[g_nkeys], id) : NULL; - g_nkeys++; - } -} - -static void free_keys(void) -{ - while (g_nkeys > 0) { g_nkeys--; release(g_keys[g_nkeys]); release(g_ids[g_nkeys]); g_keys[g_nkeys] = g_ids[g_nkeys] = NULL; } -} - -static BOOL has_key(HKEY root, const WCHAR *p) -{ HKEY k; if (RegOpenKeyExW(root, p, 0, KEY_READ | WOW, &k)) return FALSE; RegCloseKey(k); return TRUE; } - -// roots: the folders a COM server may be in -static int g_com_nfolder; // roots[0 .. g_com_nfolder-1] are DynaRun's folder (any file type), the rest are system folders (.ocx servers only) - -static int com_roots(WCHAR roots[][MAX_PATH]) -{ - WCHAR *e; int n = 0; - g_com_nfolder = 0; - if (g_exe_trusted && g_exe[0]) { - lstrcpyW(roots[n], g_exe); for (e = roots[n] + lstrlenW(roots[n]); e > roots[n] && *e != '\\'; e--); *e = 0; - if (roots[n][0]) g_com_nfolder = ++n; - } - if (GetWindowsDirectoryW(roots[n], MAX_PATH - 20)) { - lstrcpyW(roots[n + 1], roots[n]); - lstrcatW(roots[n], L"\\System32"); lstrcatW(roots[n + 1], L"\\SysWOW64"); - n += exists(roots[n + 1]) ? 2 : 1; - } - return n; -} - -static BOOL under(const WCHAR *path, const WCHAR *root) // path is a file or folder below root -{ - int n = lstrlenW(root); - return n && lstrlenW(path) > n + 1 && path[n] == '\\' && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, n, root, n) == CSTR_EQUAL; -} - -// raw: the default value of an InprocServer32 / LocalServer32 key (maybe quoted, with arguments, with %variables%) or of a -// type library's win32 key (maybe followed by "\") -static BOOL com_path_ok(const WCHAR *raw, BOOL typelib, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) -{ - static WCHAR q[2 * MAX_PATH], e[2 * MAX_PATH], full[MAX_PATH], lng[MAX_PATH], par[MAX_PATH]; - WCHAR *p, *s, c; int i, root = -1; DWORD a; HANDLE h; BOOL quoted; - *why = L""; - lstrcpynW(q, raw, 2 * MAX_PATH); - for (p = q; *p == ' '; p++); - quoted = *p == '"'; - if (quoted) { p++; for (s = p; *s && *s != '"'; s++); *s = 0; } - i = (int)ExpandEnvironmentStringsW(p, e, 2 * MAX_PATH); - if (i <= 0 || i > 2 * MAX_PATH || lstrlenW(e) >= MAX_PATH - 1) { *why = L"path empty or too long"; return FALSE; } - if (typelib && (a = GetFileAttributesW(e)) == INVALID_FILE_ATTRIBUTES) { // "file.tlb\2": a resource number - for (s = e + lstrlenW(e); s > e && *s != '\\'; s--); - if (s > e && s[1]) { for (p = s + 1; *p >= '0' && *p <= '9'; p++); if (!*p) *s = 0; } - } else if (!quoted && GetFileAttributesW(e) == INVALID_FILE_ATTRIBUTES) // unquoted, with arguments: "C:\x\a.exe /Embedding" - for (s = e; *s; s++) if (s[0] == ' ' && (s[1] == '/' || s[1] == '-')) { *s = 0; break; } - for (s = e + lstrlenW(e); s > e && s[-1] == ' '; s--) s[-1] = 0; - if (!((e[0] >= 'A' && e[0] <= 'Z') || (e[0] >= 'a' && e[0] <= 'z')) || e[1] != ':' || e[2] != '\\') { *why = L"not an absolute local path"; return FALSE; } - for (s = e + 2; *s; s++) - if (*s == ':' || *s == '*' || *s == '?' || *s == '<' || *s == '>' || *s == '|' || *s == '"' || *s == '%') { *why = L"unusable character in the path"; return FALSE; } - i = (int)GetFullPathNameW(e, MAX_PATH, full, NULL); - if (i <= 0 || i >= MAX_PATH) { *why = L"path too long"; return FALSE; } - if (!GetLongPathNameW(full, lng, MAX_PATH)) { *why = L"file does not exist"; return FALSE; } - for (i = 0; i < nr; i++) if (under(lng, roots[i])) root = i; - if (root < 0) { *why = L"outside DynaRun's folder and the system folders"; return FALSE; } - // System32 / SysWOW64: only ActiveX controls (.ocx; a type library may also be .tlb / .olb), so that other software's DLLs - // (shell extensions and the like) are never made machine-wide. Any file type inside DynaRun's own folder. - if (root >= g_com_nfolder) { - s = lng + lstrlenW(lng); - if (!(lstrlenW(lng) > 4 && (!lstrcmpiW(s - 4, L".ocx") || (typelib && (!lstrcmpiW(s - 4, L".tlb") || !lstrcmpiW(s - 4, L".olb")))))) { - *why = L"in a system folder and not an .ocx file"; return FALSE; - } - } - // no link (junction, symbolic link) anywhere below the folder, and the end must be a plain file - for (s = lng + lstrlenW(roots[root]) + 1; ; s++) { - if (*s != '\\' && *s) continue; - c = *s; *s = 0; a = GetFileAttributesW(lng); *s = c; - if (a == INVALID_FILE_ATTRIBUTES) { *why = L"file does not exist"; return FALSE; } - if (a & FILE_ATTRIBUTE_REPARSE_POINT) { *why = L"a link (junction or symbolic link) is in the path"; return FALSE; } - if (!c) { if (a & FILE_ATTRIBUTE_DIRECTORY) { *why = L"a folder, not a file"; return FALSE; } break; } - } - h = CreateFileW(lng, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); - if (h == INVALID_HANDLE_VALUE) { *why = L"file cannot be opened"; return FALSE; } - i = GetFileType(h) == FILE_TYPE_DISK; - CloseHandle(h); - if (!i) { *why = L"not a disk file"; return FALSE; } - lstrcpyW(par, lng); for (s = par + lstrlenW(par); s > par && *s != '\\'; s--); *s = 0; - if (g_is_writable(lng) || g_is_writable(par)) { *why = L"the file or its folder can be changed by ordinary users"; return FALSE; } - return TRUE; -} - -// 0: no default value, 1: a string of usable size, 2: there but not usable -static int value_state(HKEY root, const WCHAR *path) -{ - HKEY k; DWORD type, n = 0; LONG r; - if (RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | WOW, &k)) return 0; - r = RegQueryValueExW(k, NULL, NULL, &type, NULL, &n); - RegCloseKey(k); - if (r == ERROR_FILE_NOT_FOUND) return 0; - if (r) return 2; - return (type == REG_SZ || type == REG_EXPAND_SZ) && n && n < 2 * MAX_PATH * sizeof(WCHAR) ? 1 : 2; -} - -static BOOL has_subkey(HKEY root, const WCHAR *rel, const WCHAR *const *names, const WCHAR **which) -{ - HKEY k; DWORD i, n; WCHAR name[260]; int j; BOOL r = FALSE; - if (RegOpenKeyExW(root, rel, 0, KEY_READ | WOW, &k)) return FALSE; - for (i = 0; !r && (n = 260, !RegEnumKeyExW(k, i, name, &n, NULL, NULL, NULL, NULL)); i++) - for (j = 0; names[j]; j++) if (!lstrcmpiW(name, names[j])) { r = TRUE; if (which) *which = names[j]; } - RegCloseKey(k); - return r; -} - -// CLSID\{...}: every server it has must pass com_path_ok, and it has no handler, redirection or surrogate -static BOOL clsid_ok(HKEY root, const WCHAR *rel, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) -{ - static const WCHAR *const bad[] = { L"InprocHandler", L"InprocHandler32", L"InprocServer", L"LocalServer", L"TreatAs", L"AutoTreatAs", L"ScriptletURL", L"Elevation", NULL }; - static const WCHAR *const srv[] = { L"InprocServer32", L"LocalServer32", NULL }; - static WCHAR path[600], v[2 * MAX_PATH]; HKEY k; int j, st, found = 0; LONG r; - *why = L""; - if (has_subkey(root, rel, bad, NULL)) { *why = L"has a handler or redirection key"; return FALSE; } - if (RegOpenKeyExW(root, rel, 0, KEY_QUERY_VALUE | WOW, &k)) { *why = L"cannot be opened"; return FALSE; } - r = RegQueryValueExW(k, L"AppID", NULL, NULL, NULL, NULL); - RegCloseKey(k); - if (r != ERROR_FILE_NOT_FOUND) { *why = L"names an AppID (surrogate or remote activation)"; return FALSE; } - for (j = 0; srv[j]; j++) { - cat3(path, rel, L"\\", srv[j]); - if (!(st = value_state(root, path))) continue; - if (st == 2 || !reg_str(root, path, NULL, v, 2 * MAX_PATH, WOW)) { *why = L"server path unusable"; return FALSE; } - found++; - if (!com_path_ok(v, FALSE, roots, nr, why)) return FALSE; - } - if (!found) *why = L"no server"; - return found > 0; -} - -// ProgID: no shell verbs or other hooks; it points back to the CLSID that is being promoted -static BOOL progid_ok(HKEY root, const WCHAR *prog, const WCHAR *id, const WCHAR **why) -{ - static const WCHAR *const bad[] = { L"shell", L"shellex", L"protocol", L"OLEScript", L"ScriptEngine", L"PersistentHandler", L"DefaultIcon", NULL }; - static WCHAR path[600], v[128]; - *why = L""; - if (has_subkey(root, prog, bad, NULL)) { *why = L"ProgID has shell or handler keys"; return FALSE; } - if (!reg_str(root, cat3(path, prog, L"\\CLSID", NULL), NULL, v, 128, WOW) || !id || lstrcmpiW(v, id)) { *why = L"ProgID does not name this CLSID"; return FALSE; } - return TRUE; -} - -// TypeLib\{guid}: every file it names must pass com_path_ok (and there must be one) -static BOOL typelib_ok(HKEY root, const WCHAR *rel, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) -{ - static const WCHAR *const plat[] = { L"win32", L"win64", NULL }; - static WCHAR path[700], v[2 * MAX_PATH]; HKEY t, k; DWORD i, j, n; WCHAR ver[64], lcid[64]; int p, st, found = 0; BOOL ok = TRUE; - *why = L""; - if (RegOpenKeyExW(root, rel, 0, KEY_READ | WOW, &t)) { *why = L"type library cannot be opened"; return FALSE; } - for (i = 0; ok && (n = 64, !RegEnumKeyExW(t, i, ver, &n, NULL, NULL, NULL, NULL)); i++) { - if (RegOpenKeyExW(t, ver, 0, KEY_READ | WOW, &k)) continue; - for (j = 0; ok && (n = 64, !RegEnumKeyExW(k, j, lcid, &n, NULL, NULL, NULL, NULL)); j++) - for (p = 0; ok && plat[p]; p++) { - cat3(path, lcid, L"\\", plat[p]); - if (!(st = value_state(k, path))) continue; - if (st == 2 || !reg_str(k, path, NULL, v, 2 * MAX_PATH, WOW)) { *why = L"type library path unusable"; ok = FALSE; } - else { found++; ok = com_path_ok(v, TRUE, roots, nr, why); } - } - RegCloseKey(k); - } - RegCloseKey(t); - if (ok && !found) { *why = L"type library names no file"; return FALSE; } - return ok; -} - -static BOOL typelib_complete(HKEY hm, const WCHAR *tlb) // some version\lcid\win32 has a path -{ - WCHAR p[300], ver[64], lcid[64], v[MAX_PATH]; HKEY t, k; DWORD i, j, n; BOOL r = FALSE; - if (RegOpenKeyExW(hm, cat3(p, L"TypeLib\\", tlb, NULL), 0, KEY_READ | WOW, &t)) return FALSE; - for (i = 0; !r && (n = 64, !RegEnumKeyExW(t, i, ver, &n, NULL, NULL, NULL, NULL)); i++) { - if (RegOpenKeyExW(t, ver, 0, KEY_READ | WOW, &k)) continue; - for (j = 0; !r && (n = 64, !RegEnumKeyExW(k, j, lcid, &n, NULL, NULL, NULL, NULL)); j++) - r = reg_str(k, cat3(p, lcid, L"\\win32", NULL), NULL, v, MAX_PATH, WOW) > 0; - RegCloseKey(k); - } - RegCloseKey(t); - return r; -} - -static void copy_tree(HKEY s, HKEY d) -{ - DWORD i, nmax = 0, dmax = 0, n, dn, type; WCHAR *name; BYTE *data; HKEY cs, cd; - RegQueryInfoKeyW(s, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &nmax, &dmax, NULL, NULL); - name = alloc((nmax + 300) * sizeof(WCHAR)); data = alloc(dmax + 2); - if (!name || !data) goto out; - for (i = 0; n = nmax + 1, dn = dmax, !RegEnumValueW(s, i, name, &n, NULL, &type, data, &dn); i++) - RegSetValueExW(d, name, 0, type, data, dn); - for (i = 0; n = 300, !RegEnumKeyExW(s, i, name, &n, NULL, NULL, NULL, NULL); i++) { - if (RegOpenKeyExW(s, name, 0, KEY_READ | WOW, &cs)) continue; - if (!RegCreateKeyExW(d, name, 0, NULL, 0, KEY_ALL_ACCESS | WOW, NULL, &cd, NULL)) { copy_tree(cs, cd); RegCloseKey(cd); } - RegCloseKey(cs); - } -out: - release(name); release(data); -} - -typedef LONG (WINAPI *RegDeleteKeyExW_t)(HKEY, LPCWSTR, REGSAM, DWORD); - -static void delete_key_tree(HKEY root, const WCHAR *key) // the key and everything below it, in the 64-bit view -{ - RegDeleteKeyExW_t del = (RegDeleteKeyExW_t)GetProcAddress(GetModuleHandleW(L"advapi32.dll"), "RegDeleteKeyExW"); - HKEY k; WCHAR name[260], *sub; DWORD n; int guard = 0; - if (!RegOpenKeyExW(root, key, 0, KEY_ALL_ACCESS | WOW, &k)) { - while (guard++ < 10000 && (n = 260, !RegEnumKeyExW(k, 0, name, &n, NULL, NULL, NULL, NULL))) { - if ((sub = alloc((lstrlenW(key) + n + 2) * sizeof(WCHAR)))) { cat3(sub, key, L"\\", name); delete_key_tree(root, sub); release(sub); } - else break; - } - RegCloseKey(k); - } - if (del) del(root, key, WOW, 0); else RegDeleteKeyW(root, key); -} - // The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in lives in tests\promote_com.h). /* ---------- shortcuts ---------- */ diff --git a/tests/promote_com.h b/tests/promote_com.h index f6c7f73..0360ae5 100644 --- a/tests/promote_com.h +++ b/tests/promote_com.h @@ -2,6 +2,246 @@ // (clsid_ok, progid_ok, typelib_ok, ...). The installer no longer does this, so the function is not part of the shipped code; it is kept here, compiled // by tests\test_hardening.c (include it after src\setup.c), so that the checks stay exercised. +// The checks and helpers below (moved here from src\setup.c: the installer no longer calls them) are what promote_com_in and the tests use. + +// DynaRun's installer registers its ActiveX controls for the installing user only (HKCU). Elevated DynaRun needs them in +// HKLM. A registration is copied only when every file it names (InprocServer32, LocalServer32, and the type library) is +// - a regular file that exists, given as an absolute path on a local drive (environment variables are expanded; relative, +// UNC and device paths are refused), +// - inside DynaRun's own folder (the one the installer found; only when that folder is not changeable by ordinary users) +// or inside the Windows System32 / SysWOW64 folder, +// - reached through no link (junction, symbolic link) below that folder, and +// - neither the file nor its folder changeable by ordinary users (owner, ACL: see user_writable). +// No file name is trusted. The registration is checked in the user's key before the copy and again in HKLM after it (the user +// may change their key in between); a copy that fails the second check is removed. + +enum { K_CLSID = 1, K_PROGID, K_TYPELIB }; +#define MAXKEYS 1024 +static WCHAR *g_keys[MAXKEYS], *g_ids[MAXKEYS]; static int g_kinds[MAXKEYS], g_nkeys; + +static void add_key(const WCHAR *k, int kind, const WCHAR *id) +{ + int i; + for (i = 0; i < g_nkeys; i++) if (!lstrcmpiW(g_keys[i], k)) return; + if (g_nkeys < MAXKEYS && (g_keys[g_nkeys] = alloc((lstrlenW(k) + 1) * sizeof(WCHAR)))) { + lstrcpyW(g_keys[g_nkeys], k); g_kinds[g_nkeys] = kind; + g_ids[g_nkeys] = id && (g_ids[g_nkeys] = alloc((lstrlenW(id) + 1) * sizeof(WCHAR))) ? lstrcpyW(g_ids[g_nkeys], id) : NULL; + g_nkeys++; + } +} + +static void free_keys(void) +{ + while (g_nkeys > 0) { g_nkeys--; release(g_keys[g_nkeys]); release(g_ids[g_nkeys]); g_keys[g_nkeys] = g_ids[g_nkeys] = NULL; } +} + +static BOOL has_key(HKEY root, const WCHAR *p) +{ HKEY k; if (RegOpenKeyExW(root, p, 0, KEY_READ | WOW, &k)) return FALSE; RegCloseKey(k); return TRUE; } + +// roots: the folders a COM server may be in +static int g_com_nfolder; // roots[0 .. g_com_nfolder-1] are DynaRun's folder (any file type), the rest are system folders (.ocx servers only) + +static int com_roots(WCHAR roots[][MAX_PATH]) +{ + WCHAR *e; int n = 0; + g_com_nfolder = 0; + if (g_exe_trusted && g_exe[0]) { + lstrcpyW(roots[n], g_exe); for (e = roots[n] + lstrlenW(roots[n]); e > roots[n] && *e != '\\'; e--); *e = 0; + if (roots[n][0]) g_com_nfolder = ++n; + } + if (GetWindowsDirectoryW(roots[n], MAX_PATH - 20)) { + lstrcpyW(roots[n + 1], roots[n]); + lstrcatW(roots[n], L"\\System32"); lstrcatW(roots[n + 1], L"\\SysWOW64"); + n += exists(roots[n + 1]) ? 2 : 1; + } + return n; +} + +static BOOL under(const WCHAR *path, const WCHAR *root) // path is a file or folder below root +{ + int n = lstrlenW(root); + return n && lstrlenW(path) > n + 1 && path[n] == '\\' && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, n, root, n) == CSTR_EQUAL; +} + +// raw: the default value of an InprocServer32 / LocalServer32 key (maybe quoted, with arguments, with %variables%) or of a +// type library's win32 key (maybe followed by "\") +static BOOL com_path_ok(const WCHAR *raw, BOOL typelib, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) +{ + static WCHAR q[2 * MAX_PATH], e[2 * MAX_PATH], full[MAX_PATH], lng[MAX_PATH], par[MAX_PATH]; + WCHAR *p, *s, c; int i, root = -1; DWORD a; HANDLE h; BOOL quoted; + *why = L""; + lstrcpynW(q, raw, 2 * MAX_PATH); + for (p = q; *p == ' '; p++); + quoted = *p == '"'; + if (quoted) { p++; for (s = p; *s && *s != '"'; s++); *s = 0; } + i = (int)ExpandEnvironmentStringsW(p, e, 2 * MAX_PATH); + if (i <= 0 || i > 2 * MAX_PATH || lstrlenW(e) >= MAX_PATH - 1) { *why = L"path empty or too long"; return FALSE; } + if (typelib && (a = GetFileAttributesW(e)) == INVALID_FILE_ATTRIBUTES) { // "file.tlb\2": a resource number + for (s = e + lstrlenW(e); s > e && *s != '\\'; s--); + if (s > e && s[1]) { for (p = s + 1; *p >= '0' && *p <= '9'; p++); if (!*p) *s = 0; } + } else if (!quoted && GetFileAttributesW(e) == INVALID_FILE_ATTRIBUTES) // unquoted, with arguments: "C:\x\a.exe /Embedding" + for (s = e; *s; s++) if (s[0] == ' ' && (s[1] == '/' || s[1] == '-')) { *s = 0; break; } + for (s = e + lstrlenW(e); s > e && s[-1] == ' '; s--) s[-1] = 0; + if (!((e[0] >= 'A' && e[0] <= 'Z') || (e[0] >= 'a' && e[0] <= 'z')) || e[1] != ':' || e[2] != '\\') { *why = L"not an absolute local path"; return FALSE; } + for (s = e + 2; *s; s++) + if (*s == ':' || *s == '*' || *s == '?' || *s == '<' || *s == '>' || *s == '|' || *s == '"' || *s == '%') { *why = L"unusable character in the path"; return FALSE; } + i = (int)GetFullPathNameW(e, MAX_PATH, full, NULL); + if (i <= 0 || i >= MAX_PATH) { *why = L"path too long"; return FALSE; } + if (!GetLongPathNameW(full, lng, MAX_PATH)) { *why = L"file does not exist"; return FALSE; } + for (i = 0; i < nr; i++) if (under(lng, roots[i])) root = i; + if (root < 0) { *why = L"outside DynaRun's folder and the system folders"; return FALSE; } + // System32 / SysWOW64: only ActiveX controls (.ocx; a type library may also be .tlb / .olb), so that other software's DLLs + // (shell extensions and the like) are never made machine-wide. Any file type inside DynaRun's own folder. + if (root >= g_com_nfolder) { + s = lng + lstrlenW(lng); + if (!(lstrlenW(lng) > 4 && (!lstrcmpiW(s - 4, L".ocx") || (typelib && (!lstrcmpiW(s - 4, L".tlb") || !lstrcmpiW(s - 4, L".olb")))))) { + *why = L"in a system folder and not an .ocx file"; return FALSE; + } + } + // no link (junction, symbolic link) anywhere below the folder, and the end must be a plain file + for (s = lng + lstrlenW(roots[root]) + 1; ; s++) { + if (*s != '\\' && *s) continue; + c = *s; *s = 0; a = GetFileAttributesW(lng); *s = c; + if (a == INVALID_FILE_ATTRIBUTES) { *why = L"file does not exist"; return FALSE; } + if (a & FILE_ATTRIBUTE_REPARSE_POINT) { *why = L"a link (junction or symbolic link) is in the path"; return FALSE; } + if (!c) { if (a & FILE_ATTRIBUTE_DIRECTORY) { *why = L"a folder, not a file"; return FALSE; } break; } + } + h = CreateFileW(lng, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); + if (h == INVALID_HANDLE_VALUE) { *why = L"file cannot be opened"; return FALSE; } + i = GetFileType(h) == FILE_TYPE_DISK; + CloseHandle(h); + if (!i) { *why = L"not a disk file"; return FALSE; } + lstrcpyW(par, lng); for (s = par + lstrlenW(par); s > par && *s != '\\'; s--); *s = 0; + if (g_is_writable(lng) || g_is_writable(par)) { *why = L"the file or its folder can be changed by ordinary users"; return FALSE; } + return TRUE; +} + +// 0: no default value, 1: a string of usable size, 2: there but not usable +static int value_state(HKEY root, const WCHAR *path) +{ + HKEY k; DWORD type, n = 0; LONG r; + if (RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | WOW, &k)) return 0; + r = RegQueryValueExW(k, NULL, NULL, &type, NULL, &n); + RegCloseKey(k); + if (r == ERROR_FILE_NOT_FOUND) return 0; + if (r) return 2; + return (type == REG_SZ || type == REG_EXPAND_SZ) && n && n < 2 * MAX_PATH * sizeof(WCHAR) ? 1 : 2; +} + +static BOOL has_subkey(HKEY root, const WCHAR *rel, const WCHAR *const *names, const WCHAR **which) +{ + HKEY k; DWORD i, n; WCHAR name[260]; int j; BOOL r = FALSE; + if (RegOpenKeyExW(root, rel, 0, KEY_READ | WOW, &k)) return FALSE; + for (i = 0; !r && (n = 260, !RegEnumKeyExW(k, i, name, &n, NULL, NULL, NULL, NULL)); i++) + for (j = 0; names[j]; j++) if (!lstrcmpiW(name, names[j])) { r = TRUE; if (which) *which = names[j]; } + RegCloseKey(k); + return r; +} + +// CLSID\{...}: every server it has must pass com_path_ok, and it has no handler, redirection or surrogate +static BOOL clsid_ok(HKEY root, const WCHAR *rel, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) +{ + static const WCHAR *const bad[] = { L"InprocHandler", L"InprocHandler32", L"InprocServer", L"LocalServer", L"TreatAs", L"AutoTreatAs", L"ScriptletURL", L"Elevation", NULL }; + static const WCHAR *const srv[] = { L"InprocServer32", L"LocalServer32", NULL }; + static WCHAR path[600], v[2 * MAX_PATH]; HKEY k; int j, st, found = 0; LONG r; + *why = L""; + if (has_subkey(root, rel, bad, NULL)) { *why = L"has a handler or redirection key"; return FALSE; } + if (RegOpenKeyExW(root, rel, 0, KEY_QUERY_VALUE | WOW, &k)) { *why = L"cannot be opened"; return FALSE; } + r = RegQueryValueExW(k, L"AppID", NULL, NULL, NULL, NULL); + RegCloseKey(k); + if (r != ERROR_FILE_NOT_FOUND) { *why = L"names an AppID (surrogate or remote activation)"; return FALSE; } + for (j = 0; srv[j]; j++) { + cat3(path, rel, L"\\", srv[j]); + if (!(st = value_state(root, path))) continue; + if (st == 2 || !reg_str(root, path, NULL, v, 2 * MAX_PATH, WOW)) { *why = L"server path unusable"; return FALSE; } + found++; + if (!com_path_ok(v, FALSE, roots, nr, why)) return FALSE; + } + if (!found) *why = L"no server"; + return found > 0; +} + +// ProgID: no shell verbs or other hooks; it points back to the CLSID that is being promoted +static BOOL progid_ok(HKEY root, const WCHAR *prog, const WCHAR *id, const WCHAR **why) +{ + static const WCHAR *const bad[] = { L"shell", L"shellex", L"protocol", L"OLEScript", L"ScriptEngine", L"PersistentHandler", L"DefaultIcon", NULL }; + static WCHAR path[600], v[128]; + *why = L""; + if (has_subkey(root, prog, bad, NULL)) { *why = L"ProgID has shell or handler keys"; return FALSE; } + if (!reg_str(root, cat3(path, prog, L"\\CLSID", NULL), NULL, v, 128, WOW) || !id || lstrcmpiW(v, id)) { *why = L"ProgID does not name this CLSID"; return FALSE; } + return TRUE; +} + +// TypeLib\{guid}: every file it names must pass com_path_ok (and there must be one) +static BOOL typelib_ok(HKEY root, const WCHAR *rel, WCHAR roots[][MAX_PATH], int nr, const WCHAR **why) +{ + static const WCHAR *const plat[] = { L"win32", L"win64", NULL }; + static WCHAR path[700], v[2 * MAX_PATH]; HKEY t, k; DWORD i, j, n; WCHAR ver[64], lcid[64]; int p, st, found = 0; BOOL ok = TRUE; + *why = L""; + if (RegOpenKeyExW(root, rel, 0, KEY_READ | WOW, &t)) { *why = L"type library cannot be opened"; return FALSE; } + for (i = 0; ok && (n = 64, !RegEnumKeyExW(t, i, ver, &n, NULL, NULL, NULL, NULL)); i++) { + if (RegOpenKeyExW(t, ver, 0, KEY_READ | WOW, &k)) continue; + for (j = 0; ok && (n = 64, !RegEnumKeyExW(k, j, lcid, &n, NULL, NULL, NULL, NULL)); j++) + for (p = 0; ok && plat[p]; p++) { + cat3(path, lcid, L"\\", plat[p]); + if (!(st = value_state(k, path))) continue; + if (st == 2 || !reg_str(k, path, NULL, v, 2 * MAX_PATH, WOW)) { *why = L"type library path unusable"; ok = FALSE; } + else { found++; ok = com_path_ok(v, TRUE, roots, nr, why); } + } + RegCloseKey(k); + } + RegCloseKey(t); + if (ok && !found) { *why = L"type library names no file"; return FALSE; } + return ok; +} + +static BOOL typelib_complete(HKEY hm, const WCHAR *tlb) // some version\lcid\win32 has a path +{ + WCHAR p[300], ver[64], lcid[64], v[MAX_PATH]; HKEY t, k; DWORD i, j, n; BOOL r = FALSE; + if (RegOpenKeyExW(hm, cat3(p, L"TypeLib\\", tlb, NULL), 0, KEY_READ | WOW, &t)) return FALSE; + for (i = 0; !r && (n = 64, !RegEnumKeyExW(t, i, ver, &n, NULL, NULL, NULL, NULL)); i++) { + if (RegOpenKeyExW(t, ver, 0, KEY_READ | WOW, &k)) continue; + for (j = 0; !r && (n = 64, !RegEnumKeyExW(k, j, lcid, &n, NULL, NULL, NULL, NULL)); j++) + r = reg_str(k, cat3(p, lcid, L"\\win32", NULL), NULL, v, MAX_PATH, WOW) > 0; + RegCloseKey(k); + } + RegCloseKey(t); + return r; +} + +static void copy_tree(HKEY s, HKEY d) +{ + DWORD i, nmax = 0, dmax = 0, n, dn, type; WCHAR *name; BYTE *data; HKEY cs, cd; + RegQueryInfoKeyW(s, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &nmax, &dmax, NULL, NULL); + name = alloc((nmax + 300) * sizeof(WCHAR)); data = alloc(dmax + 2); + if (!name || !data) goto out; + for (i = 0; n = nmax + 1, dn = dmax, !RegEnumValueW(s, i, name, &n, NULL, &type, data, &dn); i++) + RegSetValueExW(d, name, 0, type, data, dn); + for (i = 0; n = 300, !RegEnumKeyExW(s, i, name, &n, NULL, NULL, NULL, NULL); i++) { + if (RegOpenKeyExW(s, name, 0, KEY_READ | WOW, &cs)) continue; + if (!RegCreateKeyExW(d, name, 0, NULL, 0, KEY_ALL_ACCESS | WOW, NULL, &cd, NULL)) { copy_tree(cs, cd); RegCloseKey(cd); } + RegCloseKey(cs); + } +out: + release(name); release(data); +} + +typedef LONG (WINAPI *RegDeleteKeyExW_t)(HKEY, LPCWSTR, REGSAM, DWORD); + +static void delete_key_tree(HKEY root, const WCHAR *key) // the key and everything below it, in the 64-bit view +{ + RegDeleteKeyExW_t del = (RegDeleteKeyExW_t)GetProcAddress(GetModuleHandleW(L"advapi32.dll"), "RegDeleteKeyExW"); + HKEY k; WCHAR name[260], *sub; DWORD n; int guard = 0; + if (!RegOpenKeyExW(root, key, 0, KEY_ALL_ACCESS | WOW, &k)) { + while (guard++ < 10000 && (n = 260, !RegEnumKeyExW(k, 0, name, &n, NULL, NULL, NULL, NULL))) { + if ((sub = alloc((lstrlenW(key) + n + 2) * sizeof(WCHAR)))) { cat3(sub, key, L"\\", name); delete_key_tree(root, sub); release(sub); } + else break; + } + RegCloseKey(k); + } + if (del) del(root, key, WOW, 0); else RegDeleteKeyW(root, key); +} + static const WCHAR *g_export_root = L"HKLM\\SOFTWARE\\Classes\\"; // where reg_export reads from (a test points it elsewhere) static void reg_export(const WCHAR *key, const WCHAR *file) // backup with the native reg.exe From cb1aa28a498a33f9f11c2000b4ff754cbcd32c08 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:46:25 +0800 Subject: [PATCH 10/41] dynafix: PEGRC loader callback takes peg_check's busy flag, defers when held peg_ldr_note patched through patch_base without the busy flag peg_check uses, so on two threads both could modify a hook_t's raw/chain at once. The flag is now file-scope and shared; the callback only try-acquires it (it runs under the loader lock and must not wait). When it is taken, the callback sets a pending flag and logs nothing; peg_check then forgets its per-module bookkeeping and patches the loaded modules again (idempotent). Co-Authored-By: Claude Opus 5.5 --- src/dynafix_pegrc.inc.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/dynafix_pegrc.inc.c b/src/dynafix_pegrc.inc.c index e714b25..3b8a605 100644 --- a/src/dynafix_pegrc.inc.c +++ b/src/dynafix_pegrc.inc.c @@ -165,6 +165,8 @@ static const char *g_pegmods[2] = { "Pesgo32e.ocx", "PEGRP32E.dll" }; typedef struct { USHORT Length, MaximumLength; PWSTR Buffer; } peg_us; static volatile LONG g_pegready; static BOOL g_pegnote; +static volatile LONG g_pegbusy; // 1 while hook_t's raw/chain are being patched (peg_check or the notification) +static volatile LONG g_pegdefer; // a notification found g_pegbusy taken: peg_check patches the modules again static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) { WCHAR n[32]; const peg_us *u; int i, k; @@ -179,8 +181,11 @@ static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) for (j = 0; g_pegmods[k][j]; j++) w[j] = (WCHAR)g_pegmods[k][j]; w[j] = 0; if (!lstrcmpiW(n, w)) { + // under the loader lock: never wait; when peg_check is patching (on any thread) it is left to peg_check, no log here + if (InterlockedCompareExchange(&g_pegbusy, 1, 0)) { InterlockedExchange(&g_pegdefer, 1); continue; } logf("PEGRC32D: loader notification for %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[k], 0, 0); patch_base((BYTE *)d->DllBase, g_pegmods[k], "peg-note", g_peg_hooks); + InterlockedExchange(&g_pegbusy, 0); } } } @@ -188,18 +193,22 @@ static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) // Patch the control's modules once they are loaded (also the fallback when no loader notifications exist); idempotent. static void peg_check(void) { - static BOOL resolved, off; static volatile LONG busy; + static BOOL resolved, off; int i; if (!g_pegrc || off || !GetModuleHandleA("MSVBVM60.DLL")) return; - if (InterlockedCompareExchange(&busy, 1, 0)) return; // not from inside our own patching + if (InterlockedCompareExchange(&g_pegbusy, 1, 0)) return; // not from inside our own patching if (!resolved) { - if (peg_elevated()) { off = TRUE; logf("PEGRC32D: DynaRun is elevated: no redirect %u %u %u\r\n", 0, 0, 0); InterlockedExchange(&busy, 0); return; } + if (peg_elevated()) { off = TRUE; logf("PEGRC32D: DynaRun is elevated: no redirect %u %u %u\r\n", 0, 0, 0); InterlockedExchange(&g_pegbusy, 0); return; } resolved = TRUE; resolve(g_peg_hooks); { // see peg_ldr_note HMODULE nt = GetModuleHandleA("ntdll.dll"); ldrreg_t rg = nt ? (ldrreg_t)GetProcAddress(nt, "LdrRegisterDllNotification") : NULL; PVOID ck; if (rg && !g_pegnote) { g_pegnote = TRUE; InterlockedExchange(&g_pegready, 1); if (rg(0, peg_ldr_note, NULL, &ck)) InterlockedExchange(&g_pegready, 0); } } } + if (InterlockedExchange(&g_pegdefer, 0)) { // a notification was skipped meanwhile: patch whatever is loaded now + g_pegmod[0] = g_pegmod[1] = NULL; + logf("PEGRC32D: loader notification came while patching, checking the modules again %u %u %u\r\n", 0, 0, 0); + } for (i = 0; i < 2; i++) { HMODULE m = GetModuleHandleA(g_pegmods[i]); if (m && m != g_pegmod[i]) { @@ -208,7 +217,7 @@ static void peg_check(void) patch_base((BYTE *)m, g_pegmods[i], "peg", g_peg_hooks); } else if (!m) g_pegmod[i] = NULL; } - InterlockedExchange(&busy, 0); + InterlockedExchange(&g_pegbusy, 0); } // The launcher starts DynaRun with "RunAsInvoker" appended to __COMPAT_LAYER (so that DynaRun itself never runs elevated). That From 0c729d655ef29794b2f1842729d8e864c0e6bcdc Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:52:14 +0800 Subject: [PATCH 11/41] Name the result codes in errors.h (values unchanged) New src/errors.h (included by setup.h) holds every result code: RC_* for the install/uninstall result and process exit code (RC_OK, RC_FAIL, RC_CANCEL, RC_OVER_SHOULDER, RC_MSI_FAILED, RC_NO_DYNARUN, RC_RUNNING), PK_*, PKC_* (file check status), DRV_*, and small enums for the pin result, the update hint, the BitLocker answer and remove_dynarun's "not installed". Bare 2/3/4/5 in setup.c, wizard.c, package.c and driver.c now use the name of their own meaning (cancel 2 vs PK_NET 2, RC_RUNNING 6 vs PK_PASSWORD 6 stay separate). FIX_DONE moves to errors.h with the done codes listed; same truth value for every int. The compiled code is identical (checked by disassembly). Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 24 +++++++------- src/errors.h | 54 +++++++++++++++++++++++++++++++ src/package.c | 22 ++++++------- src/setup.c | 89 +++++++++++++++++++++++++-------------------------- src/setup.h | 12 +++---- src/wizard.c | 34 ++++++++++---------- 6 files changed, 143 insertions(+), 92 deletions(-) create mode 100644 src/errors.h diff --git a/src/driver.c b/src/driver.c index 2d9c940..65cdd7b 100644 --- a/src/driver.c +++ b/src/driver.c @@ -391,8 +391,8 @@ static void check_report(const drv_check *c, WCHAR *out, int cch) WCHAR line[200], a[17], e[17]; int i; out[0] = 0; for (i = 0; i < DRV_NFILES; i++) { - if (c->st[i] == 0) continue; - if (c->st[i] == 2) wsprintfW(line, T(L" %s: missing\n", L" %s:缺少\n"), DRV_FILES[i].rel); + if (c->st[i] == PKC_OK) continue; + if (c->st[i] == PKC_MISSING) wsprintfW(line, T(L" %s: missing\n", L" %s:缺少\n"), DRV_FILES[i].rel); else { lstrcpynW(a, c->hex[i][0] ? c->hex[i] : L"?", 17); lstrcpynW(e, DRV_FILES[i].sha, 17); wsprintfW(line, T(L" %s: %s... (expected %s...)\n", L" %s:%s…(已知 %s…)\n"), DRV_FILES[i].rel, a, e); @@ -502,7 +502,7 @@ static BOOL digest_of(const drv_check *c, const WCHAR *tmp, WCHAR *hex) static WCHAR t[DRV_MAXF * 100]; ULONGLONG sz; int i; BOOL ok; t[0] = 0; for (i = 0; i < DRV_NFILES; i++) { - if (c->st[i] == 2 || !c->hex[i][0]) return FALSE; + if (c->st[i] == PKC_MISSING || !c->hex[i][0]) return FALSE; lstrcatW(t, L";"); lstrcatW(t, DRV_FILES[i].rel); lstrcatW(t, L"|"); lstrcatW(t, c->hex[i]); } if (!write_utf16_file(tmp, t, lstrlenW(t))) return FALSE; @@ -881,7 +881,7 @@ static BOOL write_accepted(const WCHAR *base, const drv_check *c) static WCHAR t[DRV_MAXF * 100]; WCHAR f[MAX_PATH]; int i; t[0] = 0; for (i = 0; i < DRV_NFILES; i++) { - if (c->st[i] == 2 || !c->hex[i][0]) return FALSE; + if (c->st[i] == PKC_MISSING || !c->hex[i][0]) return FALSE; lstrcatW(t, L";"); lstrcatW(t, DRV_FILES[i].rel); lstrcatW(t, L"|"); lstrcatW(t, c->hex[i]); } lstrcatW(t, L";"); @@ -1249,7 +1249,7 @@ static int install_locked(void) int driver_prep(const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 300]; - if (!user_sid(sid, 200)) return 1; + if (!user_sid(sid, 200)) return RC_FAIL; if (is_admin()) return prep_locked(sid, zip, accept); if (accept && accept[0]) wsprintfW(args, L"/driver-prep %s \"%s\" /unverified %s", sid, zip, accept); else wsprintfW(args, L"/driver-prep %s \"%s\"", sid, zip); @@ -1260,18 +1260,18 @@ int driver_prep(const WCHAR *zip, const WCHAR *accept) // one UAC prompt serves both. sid: the user the stage was started for. int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { - if (!is_admin()) return 1; + if (!is_admin()) return RC_FAIL; return prep_locked(sid, zip, accept); } int driver_bitlocker(void) { WCHAR dr[4]; int ps; bde_info bi; - if (!is_admin()) return -1; // WMI's BitLocker namespace refuses a process that is not elevated: do not even try (it can take many seconds) + if (!is_admin()) return BL_UNKNOWN; // WMI's BitLocker namespace refuses a process that is not elevated: do not even try (it can take many seconds) sys_drive(dr); ps = bde_query(dr, &bi); - if (ps == BDE_PS_ERROR) return -1; // not elevated: WMI refuses, nothing can be told - return drv_bde_suspend(ps, bi.conv, bi.nprot) ? 1 : 0; // the same rule as prep's, so the hint on the restart page agrees with what will happen + if (ps == BDE_PS_ERROR) return BL_UNKNOWN; // not elevated: WMI refuses, nothing can be told + return drv_bde_suspend(ps, bi.conv, bi.nprot) ? BL_ON : BL_OFF; // the same rule as prep's, so the hint on the restart page agrees with what will happen } int driver_install(void) @@ -1283,7 +1283,7 @@ int driver_install(void) int driver_cancel(void) { static WCHAR base[MAX_PATH]; - if (is_admin()) { base_dir(base); log_in_base(base, NULL); cancel_locked(); return 0; } + if (is_admin()) { base_dir(base); log_in_base(base, NULL); cancel_locked(); return RC_OK; } return run_elevated(L"/driver-cancel", NULL); } @@ -1341,8 +1341,8 @@ int driver_verb(int argc, WCHAR **argv) if (!lstrcmpiW(v, L"/driver")) { g_drvmode = DRV_MODE_STANDALONE; return wizard(); } if (!lstrcmpiW(v, L"/driver-continue")) { g_drvmode = DRV_MODE_CONTINUE; return wizard(); } if (lstrcmpiW(v, L"/driver-prep") && lstrcmpiW(v, L"/driver-install") && lstrcmpiW(v, L"/driver-cancel")) return DRV_NOVERB; - if (!is_admin()) return 1; // the elevated parts: only with the rights they need - if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], prep_accept_arg(argc, argv)) : 1; + if (!is_admin()) return RC_FAIL; // the elevated parts: only with the rights they need + if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], prep_accept_arg(argc, argv)) : RC_FAIL; if (!lstrcmpiW(v, L"/driver-install")) return install_locked(); return driver_cancel(); } diff --git a/src/errors.h b/src/errors.h new file mode 100644 index 0000000..3c907e3 --- /dev/null +++ b/src/errors.h @@ -0,0 +1,54 @@ +// The result codes of DynaRunFix-Setup.exe (setup.c, wizard.c, package.c, driver.c), in one place. Included by setup.h. +// The VALUES are part of the program's behaviour (process exit codes, what the elevated stage reports to the wizard, tests\test_hardening.c locks them): +// do not renumber. The same number means different things in different domains; each meaning has its own name here. +#pragma once + +/* ---- 1. Install / uninstall result and process exit code ---- + Returned by the install and uninstall stages (setup.c), by run_elevated (the exit code of the elevated process) and by wizard(); the process + exit code of DynaRunFix-Setup.exe is the same number. */ +enum { + RC_OK = 0, // done + RC_FAIL = 1, // failed (the reason was shown, or logged with /quiet) + RC_CANCEL = 2, // cancelled: the UAC prompt was declined, a question was answered with Cancel, the Windows Installer was cancelled; also wizard(): closed before the end + // (the same number as PK_NET: a different domain, see 2.) + RC_OVER_SHOULDER = 3, // machine_full only: the elevated account is not the user (over-the-shoulder elevation): the caller runs the DynaRun MSI as the user instead + RC_MSI_FAILED = 4, // the DynaRun MSI could not be staged, is not DynaRun's, or its install / uninstall failed + RC_NO_DYNARUN = 5, // the MSI was installed, but DynaRun V3.exe was not found afterwards + RC_RUNNING = 6 // DynaRun (or the launcher / LEProc) still runs, nothing was changed (the same number as PK_PASSWORD: a different domain) +}; + +/* ---- 2. Download / zip / package result (package.c, driver_fetch, driver_check) ---- */ +// PK_CERT: certificate / TLS failure; PK_URL: not https, or redirected off the allowed host +enum { PK_OK, PK_CANCEL, PK_NET, PK_HTTP, PK_IO, PK_FORMAT, PK_PASSWORD, PK_CERT, PK_URL }; // 0..8 + +/* ---- 3. Status of one file of a pinned table (pkg_check.st) ---- */ +enum { PKC_OK = 0, PKC_DIFFER = 1, PKC_MISSING = 2 }; + +/* ---- 4. The driver step (driver.c): result of driver_prep / driver_install / driver_cancel, also an exit code of the elevated stage ---- + DRV_SUCCESS is RC_OK. DRV_E_* 20..29 are failures; DRV_W_BDE 30 is "done, but BitLocker could not be suspended" (the wizard asks); DRV_W_HASH 31 is a + driver_fetch / driver_check result (some files differ, the user is asked), never an exit code. RC_CANCEL (2) is also what the driver functions return + when the UAC prompt was declined. DRV_NOVERB is driver_verb's "not a driver command". */ +enum { + DRV_SUCCESS = 0, + DRV_E_FIRST = 20, // the first driver code; see FIX_DONE + DRV_E_HASH = 20, DRV_E_EXTRACT = 21, DRV_E_INF = 22, DRV_E_COPY = 23, DRV_E_BCD = 24, DRV_E_TASK = 25, DRV_E_PNP = 26, DRV_E_DIR = 27, DRV_E_SID = 28, + DRV_E_ENFORCED = 29, + DRV_W_BDE = 30, + DRV_W_HASH = 31, + DRV_NOVERB = -1000 +}; + +/* The elevated install stage (install_fix_driver / install_all_driver, setup.c) "installed the fix" when its result is one of + RC_OK (0) the fix is installed, no driver step asked for + DRV_SUCCESS (0) the same number: the fix and the driver's preparation are done + DRV_E_HASH .. DRV_E_ENFORCED (20..29), DRV_W_BDE (30), DRV_W_HASH (31, not a stage result, would count) + the fix is installed and the driver step gave this code + and every number above DRV_W_HASH (the rule is "at least DRV_E_FIRST", with no upper bound: any exit code of the elevated process from 20 on). + NOT done: RC_FAIL, RC_CANCEL, RC_OVER_SHOULDER, RC_MSI_FAILED, RC_NO_DYNARUN, RC_RUNNING (1..6), 7..19 and every negative number (DRV_NOVERB too). */ +#define FIX_DONE(rc) ((int)(rc) == (int)RC_OK || (int)(rc) >= (int)DRV_E_FIRST) /* int casts: the codes are enumerators of different enums (C5287) */ + +/* ---- 5. Smaller answers ---- */ +enum { RM_NONE = -1 }; // remove_dynarun(): DynaRun V3 is not installed (else the msiexec exit code) +enum { BL_UNKNOWN = -1, BL_OFF = 0, BL_ON = 1 }; // driver_bitlocker(): BitLocker protection of the system drive (not elevated: cannot be told) +enum { PIN_NOT_ASKED = 0, PIN_DONE = 1, PIN_NEXT_SIGNIN = 2, PIN_FAILED = 3 }; // g_pinresult: after the install, 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin +enum { HINT_NONE = 0, HINT_NEWER = 1, HINT_ZIPDIFF = 2 }; // pkg_update_hint: nothing to say, the site names a newer version, the site's zip differs from the last download diff --git a/src/package.c b/src/package.c index ea7a002..8443978 100644 --- a/src/package.c +++ b/src/package.c @@ -663,21 +663,21 @@ static BOOL recorded_download(WCHAR *etag, DWORD *len, WCHAR *mod) // zip is not the one the last download recorded: "there may be a newer one". Nothing is changed, nothing is asked; every outcome is one line in the log (log may be NULL). int pkg_update_hint(WCHAR *installed, WCHAR *latest, int cch, void (*log)(const WCHAR *line)) { - static char page[262144]; DWORD len; WCHAR l[300], e1[200], m1[64], e2[200], m2[64]; DWORD l1, l2; int r = 0; + static char page[262144]; DWORD len; WCHAR l[300], e1[200], m1[64], e2[200], m2[64]; DWORD l1, l2; int r = HINT_NONE; latest[0] = 0; - if (!dynarun_version(installed, cch)) { if (log) log(L"DynaRun update hint: no installed DynaRun version (not installed by its MSI)"); return 0; } - if (pkg_net_state() == NET_NO) { if (log) log(L"DynaRun update hint: no network"); return 0; } + if (!dynarun_version(installed, cch)) { if (log) log(L"DynaRun update hint: no installed DynaRun version (not installed by its MSI)"); return HINT_NONE; } + if (pkg_net_state() == NET_NO) { if (log) log(L"DynaRun update hint: no network"); return HINT_NONE; } if (fetch_text(DYNAPRO_PAGE, page, sizeof(page), &len) && pkg_page_version(page, len, latest, cch)) { - r = ver_compare(latest, installed) > 0 ? 1 : 0; + r = ver_compare(latest, installed) > 0 ? HINT_NEWER : HINT_NONE; if (log) { wsprintfW(l, L"DynaRun update hint: installed %s, the website names %s: %s", installed, latest, r ? L"newer version available" : L"nothing newer"); log(l); } return r; } if (log) log(L"DynaRun update hint: the website names no version (or could not be read): comparing its zip with the last download"); - if (!probe_zip(e1, &l1, m1) || !recorded_download(e2, &l2, m2)) { if (log) log(L"DynaRun update hint: nothing to compare"); return 0; } + if (!probe_zip(e1, &l1, m1) || !recorded_download(e2, &l2, m2)) { if (log) log(L"DynaRun update hint: nothing to compare"); return HINT_NONE; } if (e1[0] && e2[0]) r = lstrcmpW(e1, e2) != 0; else r = (l1 && l2 && l1 != l2) || (m1[0] && m2[0] && lstrcmpW(m1, m2)); if (log) log(r ? L"DynaRun update hint: the zip on the site differs from the one downloaded last (there may be a newer version)" : L"DynaRun update hint: the zip on the site is the one downloaded last"); - return r ? 2 : 0; + return r ? HINT_ZIPDIFF : HINT_NONE; } @@ -1002,9 +1002,9 @@ void pkg_check_files(const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int WCHAR p[MAX_PATH]; ULONGLONG sz; int i; DWORD a; zero(c, sizeof(*c)); for (i = 0; i < n; i++) { - if (!pkg_file_path(p, dir, sub, t, i) || (a = GetFileAttributesW(p)) == INVALID_FILE_ATTRIBUTES) { c->st[i] = 2; c->missing++; continue; } - if ((a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || !pkg_sha256(p, c->hex[i], &sz)) { c->st[i] = 1; c->differ++; c->hex[i][0] = 0; continue; } - if (lstrcmpiW(c->hex[i], expect ? expect[i] : t[i].sha)) { c->st[i] = 1; c->differ++; continue; } + if (!pkg_file_path(p, dir, sub, t, i) || (a = GetFileAttributesW(p)) == INVALID_FILE_ATTRIBUTES) { c->st[i] = PKC_MISSING; c->missing++; continue; } + if ((a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || !pkg_sha256(p, c->hex[i], &sz)) { c->st[i] = PKC_DIFFER; c->differ++; c->hex[i][0] = 0; continue; } + if (lstrcmpiW(c->hex[i], expect ? expect[i] : t[i].sha)) { c->st[i] = PKC_DIFFER; c->differ++; continue; } c->ok++; } } @@ -1271,12 +1271,12 @@ BOOL dynarun_product(WCHAR *out) return !MsiEnumRelatedProductsW(L"" DYNARUN_UPGRADE_CODE, 0, 0, out); } -// Dyna Pro's own uninstall of DynaRun V3 (basic UI, like the install). msiexec exit code; -1 = not installed. +// Dyna Pro's own uninstall of DynaRun V3 (basic UI, like the install). msiexec exit code; RM_NONE = not installed. int remove_dynarun(void) { static WCHAR exe[MAX_PATH], cmd[MAX_PATH + 96], prod[40]; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD rc = 1; - if (!dynarun_product(prod)) return -1; + if (!dynarun_product(prod)) return RM_NONE; GetSystemDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\msiexec.exe"); wsprintfW(cmd, L"\"%s\" /x %s /qb! REBOOT=ReallySuppress", exe, prod); zero(&si, sizeof(si)); si.cb = sizeof(si); diff --git a/src/setup.c b/src/setup.c index a8c6637..bf18728 100644 --- a/src/setup.c +++ b/src/setup.c @@ -1209,8 +1209,8 @@ static BOOL pin_taskbar(const WCHAR *lnk) return TRUE; } if (ma >= 10 && b >= 22000) { // Windows 11: see taskbar_policy - if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) g_pinresult = 2; - return g_pinresult == 2; + if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) g_pinresult = PIN_NEXT_SIGNIN; + return g_pinresult == PIN_NEXT_SIGNIN; } if (pinned_ours()) return TRUE; if (ma >= 10 && b >= 17763) pin_list(lnk); else pin_verb(lnk); @@ -1409,15 +1409,15 @@ static int machine_install_locked(const WCHAR *sid) { static WCHAR p[MAX_PATH + 16]; HKEY k; - if (!valid_user_sid(sid)) { error2(T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return 1; } + if (!valid_user_sid(sid)) { error2(T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } lstrcpynW(g_user_sid, sid, 200); if (still_running(L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since - if (!check_exe()) { error2(T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return 1; } + if (!check_exe()) { error2(T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } CreateDirectoryW(g_dir, NULL); if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { error2(T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); - return 1; + return RC_FAIL; } { // Locale Emulator, resources 10.. (see setup.rc) static const WCHAR *le[] = { L"LEProc.exe", L"LoaderDll.dll", L"LocaleEmulator.dll", L"LECommonLibrary.dll", @@ -1427,16 +1427,16 @@ static int machine_install_locked(const WCHAR *sid) for (i = 0; i < (int)(sizeof(le) / sizeof(le[0])); i++) if (!extract(10 + i, cat3(p, g_dir, L"\\le\\", le[i]))) { error2(T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); - return 1; + return RC_FAIL; } } cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); if (lstrcmpiW(p, g_self) && ((!CopyFileW(g_self, p, FALSE) && !(move_aside(p) && CopyFileW(g_self, p, FALSE))) || !same_file(g_self, p))) { error2(T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); - return 1; + return RC_FAIL; } driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) - if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, APPKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) return 1; + if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, APPKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) return RC_FAIL; set_str(k, L"InstallDir", g_dir); if (g_exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use else RegDeleteValueW(k, L"DynaRunExe"); @@ -1466,13 +1466,13 @@ static int machine_install_locked(const WCHAR *sid) set_dword(k, L"NoModify", 1); set_dword(k, L"NoRepair", 1); set_dword(k, L"EstimatedSize", 100); RegCloseKey(k); } - return 0; + return RC_OK; } static int machine_install(const WCHAR *sid) { int rc; - if (!stage_lock()) { error2(busy_text(), NULL); return 1; } + if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } rc = machine_install_locked(sid); stage_unlock(); return rc; @@ -1552,7 +1552,7 @@ static int machine_uninstall(const WCHAR *skip_sid) { static WCHAR p[MAX_PATH + 16], polsid[200]; static const WCHAR KEEP[] = L"com-copied.txt\0com-backup\0com-skipped.txt\0setup.log\0"; // the record of the COM copies stays HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; - if (!stage_lock()) { error2(busy_text(), NULL); return 1; } + if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(g_user_sid, skip_sid, 200); else skip_sid = NULL; if (g_close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open if (!g_exe[0]) locate_dynarun(); // not recorded (an untrusted path is not): the default folders @@ -1592,7 +1592,7 @@ static int machine_uninstall(const WCHAR *skip_sid) RegDeleteKeyW(HKEY_LOCAL_MACHINE, UNINSTKEY); delete_tree(g_dir, KEEP, 0); stage_unlock(); - return 0; + return RC_OK; } static int memcmp_(const BYTE *x, const BYTE *y, DWORD n) { while (n--) if (*x++ != *y++) return 1; return 0; } @@ -1631,7 +1631,7 @@ static HANDLE copy_and_hold(const WCHAR *src, const WCHAR *dst) // Runs " " elevated (one UAC prompt) and returns its exit code; 2 = cancelled. exe: the program to run, NULL = this one. int run_elevated(const WCHAR *args, const WCHAR *exe_path) { - SHELLEXECUTEINFOW se; DWORD rc = 1, err, tn; static WCHAR a[1024], root[4], tmp[MAX_PATH]; const WCHAR *exe = exe_path ? exe_path : g_self; HANDLE hold = INVALID_HANDLE_VALUE; LARGE_INTEGER q; + SHELLEXECUTEINFOW se; DWORD rc = RC_FAIL, err, tn; static WCHAR a[1024], root[4], tmp[MAX_PATH]; const WCHAR *exe = exe_path ? exe_path : g_self; HANDLE hold = INVALID_HANDLE_VALUE; LARGE_INTEGER q; lstrcpyW(a, args); if (g_quiet) lstrcatW(a, L" /quiet"); if (g_close) lstrcatW(a, L" /close"); if (g_pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin if (g_keep) lstrcatW(a, L" /keep"); @@ -1649,7 +1649,7 @@ int run_elevated(const WCHAR *args, const WCHAR *exe_path) if (!ShellExecuteExW(&se)) { err = GetLastError(); if (hold != INVALID_HANDLE_VALUE) { CloseHandle(hold); DeleteFileW(tmp); } - return err == ERROR_CANCELLED ? 2 : 1; + return err == ERROR_CANCELLED ? RC_CANCEL : RC_FAIL; } WaitForSingleObject(se.hProcess, INFINITE); GetExitCodeProcess(se.hProcess, &rc); @@ -1724,31 +1724,31 @@ static BOOL stage_msi(const WCHAR *msi, WCHAR *dir, WCHAR *staged) static int machine_full(const WCHAR *sid, const WCHAR *msi) { static WCHAR me[200], txt[300], dir[MAX_PATH], staged[MAX_PATH]; int rc; - if (!user_sid(me, 200) || lstrcmpiW(me, sid)) return 3; - if (!stage_lock()) { error2(busy_text(), NULL); return 1; } + if (!user_sid(me, 200) || lstrcmpiW(me, sid)) return RC_OVER_SHOULDER; + if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } if (still_running(L"full install")) { stage_unlock(); return RC_RUNNING; } if (!stage_msi(msi, dir, staged)) { stage_unlock(); msg(T(L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。"), MB_ICONERROR); - return 4; + return RC_MSI_FAILED; } if (msi_check(staged, NULL, 0) != MSI_DYNARUN) { // the check of the copy that is going to be run drop_admin_dir(dir); stage_unlock(); msg(T(L"This is not the DynaRun V3 setup, so it will not be installed.", L"這不是 DynaRun V3 的安裝檔,所以不會安裝它。"), MB_ICONERROR); - return 4; + return RC_MSI_FAILED; } rc = run_msiexec(staged); drop_admin_dir(dir); stage_unlock(); if (rc != 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { - if (rc == ERROR_INSTALL_USEREXIT) return 2; + if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; wsprintfW(txt, T(L"The DynaRun V3 setup failed (Windows Installer error %d).", L"DynaRun V3 安裝失敗(Windows Installer 錯誤 %d)。"), rc); if (rc == ERROR_INSTALL_ALREADY_RUNNING) lstrcatW(txt, T(L"\n\nAnother installation is running. Wait until it finishes and try again.", L"\n\n有其他程式正在安裝,請等它完成後再試一次。")); msg(txt, MB_ICONERROR); - return 4; + return RC_MSI_FAILED; } - if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return 5; } + if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return RC_NO_DYNARUN; } return machine_install(sid); } @@ -1756,7 +1756,7 @@ static int machine_full(const WCHAR *sid, const WCHAR *msi) static void user_stage(void) { static WCHAR p[MAX_PATH]; HKEY b, s; - g_pinresult = 0; + g_pinresult = PIN_NOT_ASKED; if (RegCreateKeyExW(HKEY_CURRENT_USER, USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) return; // another version is installed now: what update.c noted no longer applies; the first start of DynaRun looks again RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); @@ -1768,8 +1768,8 @@ static void user_stage(void) save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); if (g_pin && !g_keep) { - g_pinresult = 3; - if (desktop_link(p, s) && pin_taskbar(p) && g_pinresult == 3) g_pinresult = 1; + g_pinresult = PIN_FAILED; + if (desktop_link(p, s) && pin_taskbar(p) && g_pinresult == PIN_FAILED) g_pinresult = PIN_DONE; } RegCloseKey(s); } @@ -1814,8 +1814,7 @@ BOOL fix_current(void) } // The wizard's one elevated stage may do the driver's preparation too (zip != NULL; see driver_prep_stage): the fix and the driver then share ONE UAC prompt. -// The result: the fix's failure code, or, when the fix is installed (user stage done), 0 or the driver's code (DRV_E_*, DRV_W_BDE: >= DRV_E_HASH). -#define FIX_DONE(rc) ((rc) == 0 || (rc) >= DRV_E_HASH) +// The result: the fix's failure code, or, when the fix is installed (user stage done), RC_OK or the driver's code (DRV_E_*, DRV_W_BDE: >= DRV_E_FIRST). FIX_DONE (errors.h) tells which. static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) // the command line of the elevated stage { @@ -1827,14 +1826,14 @@ static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) int install_fix_driver(const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; - if (!user_sid(sid, 200)) return 1; + if (!user_sid(sid, 200)) return RC_FAIL; if (running_early(L"install (before elevation)")) return RC_RUNNING; // before the UAC prompt; /quiet without /close ends here CoInitialize(NULL); if (is_admin()) { rc = machine_install(sid); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } else { wsprintfW(args, L"/machine %s \"%s\"", sid, g_exe); add_driver_args(args, zip, accept); - if ((rc = run_elevated(args, NULL)) == 2) { msg(no_admin(), MB_ICONWARNING); return 2; } + if ((rc = run_elevated(args, NULL)) == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } } if (!FIX_DONE(rc)) return rc; user_stage(); @@ -1846,7 +1845,7 @@ int install_fix(void) { return install_fix_driver(NULL, NULL); } int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; - if (!user_sid(sid, 200)) return 1; + if (!user_sid(sid, 200)) return RC_FAIL; if (running_early(L"full install (before elevation)")) return RC_RUNNING; CoInitialize(NULL); pkg_prepare_documents(msi); // as this user, before the SYSTEM-side install (error 1305 otherwise) @@ -1856,14 +1855,14 @@ int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) add_driver_args(args, zip, accept); rc = run_elevated(args, NULL); } - if (rc == 3) { // over-the-shoulder elevation: setup as this user, then the fix (and the driver) + if (rc == RC_OVER_SHOULDER) { // over-the-shoulder elevation: setup as this user, then the fix (and the driver) rc = run_msiexec(msi); - if (rc == ERROR_INSTALL_USEREXIT) return 2; - if (rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED) return 4; - if (!locate_dynarun()) return 5; + if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; + if (rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED) return RC_MSI_FAILED; + if (!locate_dynarun()) return RC_NO_DYNARUN; return install_fix_driver(zip, accept); } - if (rc == 2) return 2; + if (rc == RC_CANCEL) return RC_CANCEL; if (!FIX_DONE(rc)) return rc; locate_dynarun(); user_stage(); @@ -1912,10 +1911,10 @@ static int uninstall(void) drop_old_temp_copies(); // copies that earlier builds left in %TEMP% if (!exists(p) && !reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0)) { info(T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); - return 0; + return RC_OK; } if (!g_quiet && !dynarun_product(prod)) { - if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return 2; + if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; } else if (!g_quiet) { g_cbt = SetWindowsHookExW(WH_CBT, relabel, NULL, GetCurrentThreadId()); rc = msg(T(L"Uninstall DynaRunFix?\n\n" @@ -1925,7 +1924,7 @@ static int uninstall(void) L"只移除修正:DynaRun 的捷徑改回開啟原版,DynaRun V3 保留。\n\n" L"全部移除:接著再用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3。"), MB_YESNOCANCEL | MB_ICONQUESTION); if (g_cbt) { UnhookWindowsHookEx(g_cbt); g_cbt = NULL; } - if (rc == IDCANCEL) return 2; + if (rc == IDCANCEL) return RC_CANCEL; all = rc == IDNO; } // Running DynaRun / launcher / LEProc hold files of the folder: ask first, the elevated stage then closes them. @@ -1934,7 +1933,7 @@ static int uninstall(void) if (msg(T(L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" - L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return 2; + L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; g_close = TRUE; } reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); @@ -1952,14 +1951,14 @@ static int uninstall(void) if (g_is_writable(g_dir) || g_is_writable(p)) { error2(T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); - return 1; + return RC_FAIL; } elev = p; } wsprintfW(args, L"/unmachine %s", sid); rc = run_elevated(sid[0] ? args : L"/unmachine", elev); } - if (rc == 2) { msg(no_admin(), MB_ICONWARNING); return 2; } + if (rc == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } if (rc) { // the elevated stage did not finish: nothing user-side is undone either if (!g_quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } return rc; @@ -1972,18 +1971,18 @@ static int uninstall(void) SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); if (all) { rc = remove_dynarun(); - if (rc == ERROR_INSTALL_USEREXIT) { info(T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return 2; } + if (rc == ERROR_INSTALL_USEREXIT) { info(T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } if (rc > 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { wsprintfW(p, T(L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。"), rc); msg(p, MB_ICONERROR); - return 4; + return RC_MSI_FAILED; } info(T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); - return 0; + return RC_OK; } info(T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); - return 0; + return RC_OK; } // the command line after its first n arguments, as typed (the launcher's arguments for /update) @@ -2045,7 +2044,7 @@ void WinMainCRTStartup(void) else if (argc >= 2 && !lstrcmpiW(argv[1], L"/checkupdate")) rc = update_check(); else if (argc >= 2 && !lstrcmpiW(argv[1], L"/update")) rc = update_run(after_args(2)); else if (argc >= 2 && !lstrcmpiW(argv[1], L"/offer")) rc = update_offer(); - else if (g_quiet) rc = locate_dynarun() ? install_fix() : 1; + else if (g_quiet) rc = locate_dynarun() ? install_fix() : RC_FAIL; else rc = wizard(); // the elevated part (/machine, /full) ends here: let the wizard that started it take the foreground back AllowSetForegroundWindow(ASFW_ANY); diff --git a/src/setup.h b/src/setup.h index 4f89100..aed8c69 100644 --- a/src/setup.h +++ b/src/setup.h @@ -2,6 +2,7 @@ #pragma once #include #include +#include "errors.h" // RC_*, PK_*, PKC_*, DRV_E_* / DRV_W_*, FIX_DONE: the result codes extern BOOL g_zh, g_quiet, g_close; // g_close: close a running DynaRun before installing (/close) extern BOOL g_pin, g_autostart; // pin DynaRun to the taskbar / start it at sign-in (default on; /notaskbar, /noautostart) @@ -27,20 +28,19 @@ BOOL user_sid(WCHAR *out, int cch); BOOL fix_current(void); // the fix is installed and is the version of this setup (uninstall entry, launcher and dll there) BOOL locate_dynarun(void); // fills g_exe from the registry or the default folders; no UI void start_dynarun(void); // the wizard's "Start DynaRun": through our shortcut, like a double click -int install_fix(void); // DynaRun is installed: elevated stage + user shortcuts. 0 ok, 2 cancelled -int install_all(const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. 0 ok, 2 cancelled +int install_fix(void); // DynaRun is installed: elevated stage + user shortcuts. RC_OK, RC_CANCEL +int install_all(const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL // The same, and the driver's preparation (driver_prep) in the SAME elevated stage: one UAC prompt for both. zip/accept as for driver_prep. -// 0 ok; 2 cancelled; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). +// RC_OK; RC_CANCEL; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). int install_fix_driver(const WCHAR *zip, const WCHAR *accept); int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); // shared with driver.c void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth); // a link is removed itself, never followed; in-use files go at the next restart BOOL same_file(const WCHAR *a, const WCHAR *b); -int run_elevated(const WCHAR *args, const WCHAR *exe_path); // " " with one UAC prompt; its exit code, 2 = cancelled +int run_elevated(const WCHAR *args, const WCHAR *exe_path); // " " with one UAC prompt; its exit code, RC_CANCEL = cancelled BOOL valid_user_sid(const WCHAR *sid); // a SID of a real account with a profile here BOOL path_user_writable(const WCHAR *path, const WCHAR *sid); // could ordinary users (or the user sid) change what is at path (or is that unknown) BOOL enable_privilege(const WCHAR *name); -#define RC_RUNNING 6 // exit code: DynaRun (or the launcher / LEProc) still runs, nothing was changed enum { RUN_DYNARUN = 1, RUN_LAUNCHER = 2, RUN_LEPROC = 4, RUN_CANCLOSE = 8, RUN_NOCLOSE = 16 }; int running_programs(void); // RUN_* bits of the programs that hold files the setup replaces. RUN_CANCLOSE: one of them can be closed // by the setup; RUN_NOCLOSE: one cannot (another session, or its exe path cannot be told) @@ -63,7 +63,6 @@ int wizard(void); #define GITHUB_HOSTS L"github.com;api.github.com;objects.githubusercontent.com;release-assets.githubusercontent.com" #define DYNAPRO_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20Dynamometers.zip" typedef void (*progress_fn)(DWORD done, DWORD total); -enum { PK_OK, PK_CANCEL, PK_NET, PK_HTTP, PK_IO, PK_FORMAT, PK_PASSWORD, PK_CERT, PK_URL }; // PK_CERT: certificate/TLS failure; PK_URL: not https, or redirected off the allowed host typedef struct { WCHAR path[MAX_PATH]; @@ -133,7 +132,6 @@ int pkg_prepare_documents(const WCHAR *msi); // download OneDrive "online-only Windows installs it only with driver signature enforcement off for one start: see docs/DRIVER.md) */ #define DYNAPRO_USB_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20USB.zip" #define DRIVER_DOC_URL L"https://github.com/timliudev/DynaRunFix/blob/main/docs/DRIVER.md" -enum { DRV_SUCCESS = 0, DRV_E_HASH = 20, DRV_E_EXTRACT, DRV_E_INF, DRV_E_COPY, DRV_E_BCD, DRV_E_TASK, DRV_E_PNP, DRV_E_DIR, DRV_E_SID, DRV_E_ENFORCED, DRV_W_BDE = 30, DRV_W_HASH = 31, DRV_NOVERB = -1000 }; enum { DRV_CI_ENFORCED, DRV_CI_OFF, DRV_CI_UNKNOWN }; // driver signature enforcement now BOOL driver_supported(void); // Windows 10 (build 10240) or later: older systems install the driver by hand BOOL driver_installed(void); // a Dyna Pro ftdibus package is in the driver store (%windir%\INF\oem*.inf) diff --git a/src/wizard.c b/src/wizard.c index 7aa79cb..6ded690 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -54,7 +54,7 @@ static int g_drv_how; // DRV_SRC_*: the drive static WCHAR g_drv_hex[65]; // SHA-256 and size of a driver zip that is not the pinned file (the warning page shows them) static ULONGLONG g_drv_size; static WCHAR g_drv_rep[1500]; // the driver files that differ from the pinned ones, one per line (the warning page lists them) -static int g_drv_bl = -1; // BitLocker hint for the press-7 page, asked in the worker (WMI can take seconds): 1 on, 0 off, -1 cannot be told +static int g_drv_bl = BL_UNKNOWN; // BitLocker hint for the press-7 page, asked in the worker (WMI can take seconds): 1 on, 0 off, -1 cannot be told // What the acquire flow needs to know about the thing it gets: the DynaRun V3 setup and the Dyna Pro USB driver go through the same pages (looking for // a copy on this computer, download with pause / resume, one error page); only these texts, the "partial download" test, the search and the check differ. typedef struct { @@ -320,11 +320,11 @@ static BOOL can_skip_driver(void) { return g_drvmode == DRV_MODE_NONE && !g_driv static void hint_line(WCHAR *b) { WCHAR t[400]; - if (g_hint == 1) { + if (g_hint == HINT_NEWER) { wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), g_hint_new, g_hint_inst); lstrcatW(b, t); - } else if (g_hint == 2) + } else if (g_hint == HINT_ZIPDIFF) lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); } @@ -343,8 +343,8 @@ static void cat_to(WCHAR *d, int cap, const WCHAR *s) // the install (g_fix_done: the page "Done" is skipped), on the restart page. static void pin_hint(WCHAR *b) { - if (g_pinresult == 2) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); - if (g_pinresult == 3) lstrcatW(b, os_build() >= 22000 + if (g_pinresult == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (g_pinresult == PIN_FAILED) lstrcatW(b, os_build() >= 22000 ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", @@ -720,7 +720,7 @@ static void on_progress(DWORD done, DWORD total) static DWORD WINAPI job_proc(LPVOID unused) { - int rc = 1; + int rc = RC_FAIL; switch (g_job) { case JOB_ACQUIRE: // the DynaRun setup or the driver: a copy on this computer first, else the download on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts @@ -1210,17 +1210,17 @@ static void job_done(int job, int rc) break; case JOB_INSTALL_ALL: case JOB_INSTALL_FIX: - if (g_pre && (rc == 0 || rc >= DRV_E_HASH)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) + if (g_pre && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) cleanup_files(); g_fix_done = TRUE; - if (rc == 0) drv_reboot_now(); + if (rc == DRV_SUCCESS) drv_reboot_now(); else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); else drv_error(rc); break; } - if (rc == 0) { cleanup_files(); set_page(P_DONE); break; } + if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); break; } if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); break; } // DynaRun was started meanwhile - if (rc == 2) { + if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); set_page(g_pre ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // g_pre: the page with the button that started it break; @@ -1230,9 +1230,9 @@ static void job_done(int job, int rc) set_page(P_ERROR); break; case JOB_DRV_PREP: - if (rc == 0) { drv_reboot_now(); break; } + if (rc == DRV_SUCCESS) { drv_reboot_now(); break; } if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); break; } // armed, but BitLocker is still on: the user decides - if (rc == 2) { + if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); set_page(g_drvmode == DRV_MODE_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); break; @@ -1240,8 +1240,8 @@ static void job_done(int job, int rc) drv_error(rc); break; case JOB_DRV_INSTALL: - if (rc == 0) { g_drv_have = FALSE; set_page(P_DRV_DONE); break; } - if (rc == 2) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); break; } + if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); break; } + if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); break; } if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); set_page(P_DRV_RETRY); break; @@ -1721,7 +1721,7 @@ int wizard(void) } g_hwnd = NULL; if (g_page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it - if (g_page == P_DONE || g_page == P_DRV_DONE || g_page == P_DRV_REBOOT) return 0; // ended on a success page - if (g_page == P_FAIL || g_page == P_ERROR || g_page == P_DRV_ERROR) return 1; // ended on an error page - return 2; // closed before the end + if (g_page == P_DONE || g_page == P_DRV_DONE || g_page == P_DRV_REBOOT) return RC_OK; // ended on a success page + if (g_page == P_FAIL || g_page == P_ERROR || g_page == P_DRV_ERROR) return RC_FAIL; // ended on an error page + return RC_CANCEL; // closed before the end } From dee0b654e2aba02e90118b3664f7cf9c2a492c39 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:01:25 +0800 Subject: [PATCH 12/41] Shared helpers: path join, run_process, directory walk, app key setup.c now defines (declared in setup.h) path_join (dir + name with the old `lstrlen + 2 >= limit` test), run_process (CreateProcess + wait with timeout), dir_walk (one folder level with a callback; skips '.' names and too long paths) and app_key_open / app_key_create (HKCU\Software\DynaRunFix, HKLM\SOFTWARE\DynaRunFix). Used by package.c (scan, find_zip_in, find_dir_in, hydrate_tree, the msiexec calls, the registry notes), driver.c (run_tool, the BitLocker flag, two joins) and setup.c (delete_tree, the lnk name check, the install keys). The four shortcut walks in setup.c and the differing process starts (run_tool_to_file, touch, run_elevated) are left as they are. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 25 ++----- src/package.c | 204 +++++++++++++++++++++++++++----------------------- src/setup.c | 58 +++++++++++--- src/setup.h | 21 ++++++ 4 files changed, 185 insertions(+), 123 deletions(-) diff --git a/src/driver.c b/src/driver.c index 65cdd7b..490757a 100644 --- a/src/driver.c +++ b/src/driver.c @@ -38,7 +38,6 @@ #define TASK_NAME L"DynaRunFix-DriverContinue" enum { BDE_PS_ERROR = -2, BDE_PS_NONE = -1, BDE_PS_OFF = 0, BDE_PS_ON = 1, BDE_PS_UNKNOWN = 2 }; // protection status (WMI values, plus ours) #define BDE_VALUE L"DriverBitLockerSuspended" // HKLM\SOFTWARE\DynaRunFix: 1 while we have BitLocker suspended until the driver is installed -#define DRV_APPKEY L"SOFTWARE\\DynaRunFix" #define SETUP_NAME L"DynaRunFix-Setup.exe" #define BUS_INF_REL L"\\driver\\Dyna Pro USB\\ftdibus.inf" #define PORT_INF_REL L"\\driver\\Dyna Pro USB\\ftdiport.inf" @@ -257,8 +256,7 @@ BOOL driver_installed(void) lstrcpyW(pat, dir); lstrcatW(pat, L"\\oem*.inf"); if ((h = FindFirstFileW(pat, &fd)) == INVALID_HANDLE_VALUE) return FALSE; do { - if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= MAX_PATH) continue; - lstrcpyW(f, dir); lstrcatW(f, L"\\"); lstrcatW(f, fd.cFileName); + if ((fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || !path_join(f, MAX_PATH, dir, fd.cFileName)) continue; if ((d = slurp(f, &n, 2 * 1024 * 1024))) { found = drv_inf_matches(d, n); release(d); } } while (!found && FindNextFileW(h, &fd)); FindClose(h); @@ -679,16 +677,11 @@ static const WCHAR *g_tool_ok_text; static DWORD run_tool(const WCHAR *tool, const WCHAR *args) { - static WCHAR win[MAX_PATH], exe[MAX_PATH]; WCHAR *cmd; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD rc = (DWORD)-1; + static WCHAR win[MAX_PATH], exe[MAX_PATH]; WCHAR *cmd; DWORD rc = (DWORD)-1; if (!GetWindowsDirectoryW(win, MAX_PATH - 40) || !drv_sys_tool(win, is_wow64(), tool, exe, MAX_PATH) || !exists(exe)) { drv_log2(tool, L"not found", rc); return rc; } if (!(cmd = alloc((lstrlenW(exe) + lstrlenW(args) + 8) * sizeof(WCHAR)))) return rc; lstrcpyW(cmd, L"\""); lstrcatW(cmd, exe); lstrcatW(cmd, L"\" "); lstrcatW(cmd, args); - zero(&si, sizeof(si)); si.cb = sizeof(si); si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; - if (CreateProcessW(exe, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, win, &si, &pi)) { - if (WaitForSingleObject(pi.hProcess, 180000) != WAIT_OBJECT_0) TerminateProcess(pi.hProcess, 1); - else GetExitCodeProcess(pi.hProcess, &rc); - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - } + run_process(exe, cmd, win, CREATE_NO_WINDOW, TRUE, 180000, &rc); // (not started, or no end in 3 minutes: rc stays -1) if (g_tool_ok_text && rc == g_tool_ok_code) { static WCHAR l[2 * MAX_PATH + 240]; lstrcpyW(l, tool); lstrcatW(l, L" "); lstrcatW(l, args); lstrcatW(l, L" -> "); lstrcatW(l, g_tool_ok_text); @@ -841,8 +834,7 @@ static BOOL make_base(const WCHAR *base, BOOL keep_exe, const WCHAR *sid) if (!exists(base) || path_user_writable(base, sid)) { base_fail(L"the folder is not protected (owner or permissions)"); return FALSE; } if (keep_exe) { // the kept setup copy: its owner and permissions are ours too WCHAR f[MAX_PATH]; - if (lstrlenW(base) + lstrlenW(KEEP) + 2 >= MAX_PATH) { base_fail(L"the path is too long"); return FALSE; } - lstrcpyW(f, base); lstrcatW(f, L"\\"); lstrcatW(f, KEEP); + if (!path_join(f, MAX_PATH, base, KEEP)) { base_fail(L"the path is too long"); return FALSE; } if (exists(f)) { if (!plain_single_file(f)) { base_fail(L"the kept setup file is a link or has several links"); return FALSE; } reset_security(f, SDDL_FILE); @@ -1069,7 +1061,7 @@ static int bde_protection(const WCHAR *drive) { return bde_query(drive, NULL); } static BOOL bde_flag(void) { HKEY k; DWORD v = 0, n = 4; - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, DRV_APPKEY, 0, KEY_QUERY_VALUE, &k)) return FALSE; + if (!app_key_open(APP_MACHINE, KEY_QUERY_VALUE, &k)) return FALSE; if (RegQueryValueExW(k, BDE_VALUE, NULL, NULL, (BYTE *)&v, &n)) v = 0; RegCloseKey(k); return v == 1; @@ -1079,8 +1071,8 @@ static void set_bde_flag(BOOL on) { HKEY k; DWORD v = 1; if (on) { - if (!RegCreateKeyExW(HKEY_LOCAL_MACHINE, DRV_APPKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { RegSetValueExW(k, BDE_VALUE, 0, REG_DWORD, (const BYTE *)&v, 4); RegCloseKey(k); } - } else if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, DRV_APPKEY, 0, KEY_SET_VALUE, &k)) { RegDeleteValueW(k, BDE_VALUE); RegCloseKey(k); } + if (app_key_create(APP_MACHINE, &k)) { RegSetValueExW(k, BDE_VALUE, 0, REG_DWORD, (const BYTE *)&v, 4); RegCloseKey(k); } + } else if (app_key_open(APP_MACHINE, KEY_SET_VALUE, &k)) { RegDeleteValueW(k, BDE_VALUE); RegCloseKey(k); } } static void bde_resume(void) // BitLocker protection on again, if we are the ones who suspended it @@ -1294,8 +1286,7 @@ static const WCHAR *lnk_names[2] = { L"Install Dyna Pro USB driver.lnk", L"安 static BOOL lnk_path(WCHAR *out, int which) { WCHAR d[MAX_PATH]; - if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_COMMON_PROGRAMS, FALSE) || lstrlenW(d) + lstrlenW(lnk_names[which]) + 2 >= MAX_PATH) return FALSE; - lstrcpyW(out, d); lstrcatW(out, L"\\"); lstrcatW(out, lnk_names[which]); + if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_COMMON_PROGRAMS, FALSE) || !path_join(out, MAX_PATH, d, lnk_names[which])) return FALSE; return TRUE; } diff --git a/src/package.c b/src/package.c index 8443978..d84a5df 100644 --- a/src/package.c +++ b/src/package.c @@ -65,33 +65,37 @@ typedef struct { WCHAR path[MAX_PATH]; int score; FILETIME t; } candidate; // Ranking: a checked DynaRun MSI first, then a zip named like Dyna Pro's download, then any other // password-protected zip with an .msi inside; newest first within each. The name only orders the zips, it proves nothing. +static void scan(const WCHAR *dir, BOOL subdirs, candidate *best); +typedef struct { candidate *best; BOOL subdirs; } scan_ctx; + +static BOOL scan_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + scan_ctx *c = vc; candidate *best = c->best; const WCHAR *e; int score; zipent z; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (c->subdirs && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) scan(p, FALSE, best); + return TRUE; + } + if (fd->dwFileAttributes & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN)) return TRUE; // cloud-only: do not download it just to look + e = fd->cFileName + lstrlenW(fd->cFileName) - 4; + if (e < fd->cFileName) return TRUE; + if (!lstrcmpiW(e, L".msi")) score = 3; + else if (!lstrcmpiW(e, L".zip")) score = CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, fd->cFileName, 4, L"dyna", 4) == CSTR_EQUAL ? 2 : 1; + else return TRUE; + if (score < best->score || (score == best->score && CompareFileTime(&fd->ftLastWriteTime, &best->t) <= 0)) return TRUE; + if (rejected(p)) return TRUE; + // Dyna Pro's zip is password-protected: an unencrypted zip that happens to hold some .msi + // (another vendor's tool) is not offered unasked, only through "Other file..." + if (score == 3 ? msi_check(p, NULL, 0) != MSI_DYNARUN : zip_open(p, &z) != PK_OK || !(z.flags & 1)) return TRUE; + lstrcpyW(best->path, p); best->score = score; best->t = fd->ftLastWriteTime; + return TRUE; +} + static void scan(const WCHAR *dir, BOOL subdirs, candidate *best) { - WCHAR p[MAX_PATH * 2], *e; WIN32_FIND_DATAW fd; HANDLE f; int score; zipent z; + WCHAR p[MAX_PATH * 2]; scan_ctx c; if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH) return; - lstrcpyW(p, dir); lstrcatW(p, L"\\*"); - if ((f = FindFirstFileW(p, &fd)) == INVALID_HANDLE_VALUE) return; - do { - if (fd.cFileName[0] == '.' || lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= MAX_PATH) continue; - lstrcpyW(p, dir); lstrcatW(p, L"\\"); lstrcatW(p, fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (subdirs && !(fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) scan(p, FALSE, best); - continue; - } - if (fd.dwFileAttributes & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN)) continue; // cloud-only: do not download it just to look - e = fd.cFileName + lstrlenW(fd.cFileName) - 4; - if (e < fd.cFileName) continue; - if (!lstrcmpiW(e, L".msi")) score = 3; - else if (!lstrcmpiW(e, L".zip")) score = CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, fd.cFileName, 4, L"dyna", 4) == CSTR_EQUAL ? 2 : 1; - else continue; - if (score < best->score || (score == best->score && CompareFileTime(&fd.ftLastWriteTime, &best->t) <= 0)) continue; - if (rejected(p)) continue; - // Dyna Pro's zip is password-protected: an unencrypted zip that happens to hold some .msi - // (another vendor's tool) is not offered unasked, only through "Other file..." - if (score == 3 ? msi_check(p, NULL, 0) != MSI_DYNARUN : zip_open(p, &z) != PK_OK || !(z.flags & 1)) continue; - lstrcpyW(best->path, p); best->score = score; best->t = fd.ftLastWriteTime; - } while (FindNextFileW(f, &fd)); - FindClose(f); + c.best = best; c.subdirs = subdirs; + dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, scan_entry, &c); } // The folders a package the user already has is looked for in: 0 where this setup is, 1 Downloads (also one level into it), 2 Desktop, 3 Documents. @@ -124,34 +128,45 @@ static void plog(const WCHAR *a, const WCHAR *b) // Two modes. accept given: the first zip (at most maxsize bytes) that accept() approves - by its content, whatever it is called. prefix given // instead: the newest zip whose name starts with prefix, whatever it contains (*bt: its time, in/out). +static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, zip_accept_fn accept, const WCHAR *prefix, FILETIME *bt, WCHAR *out); +typedef struct { BOOL subdirs; DWORD maxsize; zip_accept_fn accept; const WCHAR *prefix; FILETIME *bt; WCHAR *out; BOOL found; int plen; } zipfind_ctx; + +static void zip_one(zipfind_ctx *c, const WCHAR *p, const WIN32_FIND_DATAW *fd) +{ + int n; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (c->subdirs && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && find_zip_in(p, FALSE, c->maxsize, c->accept, c->prefix, c->bt, c->out)) c->found = TRUE; + return; + } + if (fd->dwFileAttributes & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN)) return; // cloud-only: do not download it just to look + n = lstrlenW(fd->cFileName); + if (n < 4 || lstrcmpiW(fd->cFileName + n - 4, L".zip")) return; + if (c->prefix) { + if (n < c->plen || CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, fd->cFileName, c->plen, c->prefix, c->plen) != CSTR_EQUAL) return; + if (c->out[0] && CompareFileTime(&fd->ftLastWriteTime, c->bt) <= 0) return; + lstrcpyW(c->out, p); *c->bt = fd->ftLastWriteTime; c->found = TRUE; + return; + } + if (fd->nFileSizeHigh || fd->nFileSizeLow > c->maxsize) { plog(L" skipped (too big for a driver package): ", p); return; } // the driver package is a few MB + if (c->accept(p)) { lstrcpyW(c->out, p); c->found = TRUE; } +} + +// (the walk goes on after every entry, except once a zip is found when not looking for the newest by name) +static BOOL zip_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + zipfind_ctx *c = vc; + zip_one(c, p, fd); + return c->prefix || !c->found; +} + static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, zip_accept_fn accept, const WCHAR *prefix, FILETIME *bt, WCHAR *out) { - WCHAR p[MAX_PATH * 2]; WIN32_FIND_DATAW fd; HANDLE f; BOOL found = FALSE; int n, plen = prefix ? lstrlenW(prefix) : 0; + WCHAR p[MAX_PATH * 2]; zipfind_ctx c; if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH) { plog(L"zip search: a folder with no usable path", L""); return FALSE; } plog(prefix ? L"zip search by name in " : L"zip search by content in ", dir); - lstrcpyW(p, dir); lstrcatW(p, L"\\*"); - if ((f = FindFirstFileW(p, &fd)) == INVALID_HANDLE_VALUE) { plog(L" the folder cannot be read or does not exist", L""); return FALSE; } - do { - if (fd.cFileName[0] == '.' || lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= MAX_PATH) continue; - lstrcpyW(p, dir); lstrcatW(p, L"\\"); lstrcatW(p, fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (subdirs && !(fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && find_zip_in(p, FALSE, maxsize, accept, prefix, bt, out)) found = TRUE; - continue; - } - if (fd.dwFileAttributes & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN)) continue; // cloud-only: do not download it just to look - n = lstrlenW(fd.cFileName); - if (n < 4 || lstrcmpiW(fd.cFileName + n - 4, L".zip")) continue; - if (prefix) { - if (n < plen || CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, fd.cFileName, plen, prefix, plen) != CSTR_EQUAL) continue; - if (out[0] && CompareFileTime(&fd.ftLastWriteTime, bt) <= 0) continue; - lstrcpyW(out, p); *bt = fd.ftLastWriteTime; found = TRUE; - continue; - } - if (fd.nFileSizeHigh || fd.nFileSizeLow > maxsize) { plog(L" skipped (too big for a driver package): ", p); continue; } // the driver package is a few MB - if (accept(p)) { lstrcpyW(out, p); found = TRUE; } - } while ((prefix || !found) && FindNextFileW(f, &fd)); - FindClose(f); - return found; + c.subdirs = subdirs; c.maxsize = maxsize; c.accept = accept; c.prefix = prefix; c.bt = bt; c.out = out; c.found = FALSE; c.plen = prefix ? lstrlenW(prefix) : 0; + if (dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, zip_entry, &c) == WALK_NOOPEN) { plog(L" the folder cannot be read or does not exist", L""); return FALSE; } + return c.found; } // The first zip (at most maxsize bytes) in the folders pkg_find_local looks in (and one level into Downloads) that accept() approves. @@ -176,22 +191,26 @@ BOOL pkg_find_zip_named(const WCHAR *prefix, WCHAR *out) // Depth-limited walk: accept(dir) for dir itself, then for its sub-folders down to `depth` levels (never into a link). *budget: the most folders // still to be looked at in all (so a big Documents folder cannot make it slow). +static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, BOOL (*accept)(const WCHAR *dir), WCHAR *out); +typedef struct { int depth, *budget; BOOL (*accept)(const WCHAR *dir); WCHAR *out; BOOL found; } dirfind_ctx; + +static BOOL dir_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + dirfind_ctx *c = vc; + if ((fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) + c->found = find_dir_in(p, c->depth - 1, c->budget, c->accept, c->out); + return !c->found && *c->budget >= 0; +} + static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, BOOL (*accept)(const WCHAR *dir), WCHAR *out) { - WCHAR p[MAX_PATH * 2]; WIN32_FIND_DATAW fd; HANDLE f; BOOL found = FALSE; + WCHAR p[MAX_PATH * 2]; dirfind_ctx c; if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH || --*budget < 0) return FALSE; if (accept(dir)) { lstrcpyW(out, dir); return TRUE; } if (depth <= 0) return FALSE; - lstrcpyW(p, dir); lstrcatW(p, L"\\*"); - if ((f = FindFirstFileW(p, &fd)) == INVALID_HANDLE_VALUE) return FALSE; - do { - if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) || (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || fd.cFileName[0] == '.' || - lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= MAX_PATH) continue; - lstrcpyW(p, dir); lstrcatW(p, L"\\"); lstrcatW(p, fd.cFileName); - found = find_dir_in(p, depth - 1, budget, accept, out); - } while (!found && *budget >= 0 && FindNextFileW(f, &fd)); - FindClose(f); - return found; + c.depth = depth; c.budget = budget; c.accept = accept; c.out = out; c.found = FALSE; + dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, dir_entry, &c); + return c.found; } // The first folder that accept() approves, in the folders pkg_find_local looks in: the setup's folder, Desktop and Documents one level down, @@ -416,7 +435,7 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W if (pkg_net_state() == NET_NO) { *err = ERROR_INTERNET_DISCONNECTED; return PK_NET; } // clearly no network at all (cable out, adapter off): the error page at once, no waiting lstrcpyW(part, out); lstrcatW(part, L".part"); oldtag[0] = 0; - if (!RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { + if (app_key_create(APP_USER, &k)) { tn = sizeof(oldtag) - 2; if (RegQueryValueExW(k, etagval, NULL, NULL, (BYTE *)oldtag, &tn)) oldtag[0] = 0; RegCloseKey(k); @@ -451,7 +470,7 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W if (status == 200) { have = 0; SetFilePointer(f, 0, NULL, FILE_BEGIN); SetEndOfFile(f); } else SetFilePointer(f, 0, NULL, FILE_END); if (query_str(h, HTTP_QUERY_ETAG, tag, 200) && lstrcmpW(tag, oldtag) && - !RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { + app_key_create(APP_USER, &k)) { RegSetValueExW(k, etagval, 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); RegCloseKey(k); lstrcpyW(oldtag, tag); } @@ -488,7 +507,7 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W if (rc == PK_NET) { if (first) *err = first; if (*cancel) rc = PK_CANCEL; } // the first error of the failed run, not the last one; stopped by the user between attempts InternetCloseHandle(in); if (rc == PK_OK && !MoveFileExW(part, out, MOVEFILE_REPLACE_EXISTING)) { *err = GetLastError(); rc = PK_IO; } - if (rc == PK_OK && !RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { // what a completed download was (for the hint "the site has another file") + if (rc == PK_OK && app_key_create(APP_USER, &k)) { // what a completed download was (for the hint "the site has another file") WCHAR rec[320], nm[80]; if (lstrlenW(etagval) + 5 < 80) { wsprintfW(rec, L"%s|%lu|%s", tag, have, lmod); lstrcpyW(nm, etagval); lstrcatW(nm, L"Done"); @@ -648,7 +667,7 @@ static BOOL recorded_download(WCHAR *etag, DWORD *len, WCHAR *mod) { WCHAR v[320], *a, *b; HKEY k; DWORD n = sizeof(v) - 2; BOOL ok = FALSE; etag[0] = mod[0] = 0; *len = 0; v[0] = 0; - if (!RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, KEY_QUERY_VALUE, &k)) { + if (app_key_open(APP_USER, KEY_QUERY_VALUE, &k)) { ok = !RegQueryValueExW(k, L"DownloadETagDone", NULL, NULL, (BYTE *)v, &n) && v[0]; RegCloseKey(k); } @@ -690,7 +709,7 @@ BOOL pkg_partial(const WCHAR *name, const WCHAR *etagval, DWORD *have) static WCHAR p[MAX_PATH], tag[200]; HKEY k; DWORD tn = sizeof(tag) - 2; HANDLE f; LARGE_INTEGER sz; BOOL ok = FALSE; pkg_temp_dir(p); lstrcatW(p, L"\\"); lstrcatW(p, name); lstrcatW(p, L".part"); tag[0] = 0; - if (!RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, KEY_QUERY_VALUE, &k)) { + if (app_key_open(APP_USER, KEY_QUERY_VALUE, &k)) { if (RegQueryValueExW(k, etagval, NULL, NULL, (BYTE *)tag, &tn)) tag[0] = 0; RegCloseKey(k); } @@ -1253,15 +1272,11 @@ WCHAR *msi_external_files(const WCHAR *msi) int run_msiexec(const WCHAR *msi) { static WCHAR exe[MAX_PATH], cmd[MAX_PATH * 2 + 64]; - STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD rc = 1; + DWORD rc = 1; GetSystemDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\msiexec.exe"); wsprintfW(cmd, L"\"%s\" /i \"%s\" /qb! REBOOT=ReallySuppress", exe, msi); - zero(&si, sizeof(si)); si.cb = sizeof(si); AllowSetForegroundWindow(ASFW_ANY); // msiexec's progress window may come to the front - if (!CreateProcessW(exe, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) return (int)GetLastError(); - WaitForSingleObject(pi.hProcess, INFINITE); - GetExitCodeProcess(pi.hProcess, &rc); - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + if (run_process(exe, cmd, NULL, 0, FALSE, INFINITE, &rc) == PROC_NOSTART) return (int)GetLastError(); return (int)rc; } @@ -1275,16 +1290,12 @@ BOOL dynarun_product(WCHAR *out) int remove_dynarun(void) { static WCHAR exe[MAX_PATH], cmd[MAX_PATH + 96], prod[40]; - STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD rc = 1; + DWORD rc = 1; if (!dynarun_product(prod)) return RM_NONE; GetSystemDirectoryW(exe, MAX_PATH); lstrcatW(exe, L"\\msiexec.exe"); wsprintfW(cmd, L"\"%s\" /x %s /qb! REBOOT=ReallySuppress", exe, prod); - zero(&si, sizeof(si)); si.cb = sizeof(si); AllowSetForegroundWindow(ASFW_ANY); - if (!CreateProcessW(exe, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) return (int)GetLastError(); - WaitForSingleObject(pi.hProcess, INFINITE); - GetExitCodeProcess(pi.hProcess, &rc); - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + if (run_process(exe, cmd, NULL, 0, FALSE, INFINITE, &rc) == PROC_NOSTART) return (int)GetLastError(); return (int)rc; } @@ -1297,29 +1308,32 @@ static BOOL name_listed(const WCHAR *list, const WCHAR *name) return FALSE; } -static int hydrate_tree(const WCHAR *dir, const WCHAR *names) +static int hydrate_tree(const WCHAR *dir, const WCHAR *names); +typedef struct { const WCHAR *names; int count; } hydrate_ctx; + +static BOOL hydrate_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) { static BYTE buf[CHUNK]; - WCHAR *p = alloc(2 * MAX_PATH * sizeof(WCHAR)); WIN32_FIND_DATAW fd; HANDLE f, h; DWORD n; int count = 0; + hydrate_ctx *c = vc; HANDLE h; DWORD n; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { c->count += hydrate_tree(p, c->names); return TRUE; } + if (!(fd->dwFileAttributes & (FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_OFFLINE))) return TRUE; + if (!name_listed(c->names, fd->cFileName)) return TRUE; + h = CreateFileW(p, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); + if (h == INVALID_HANDLE_VALUE) return TRUE; + while (ReadFile(h, buf, sizeof(buf), &n, NULL) && n) ; + CloseHandle(h); + c->count++; + return TRUE; +} + +static int hydrate_tree(const WCHAR *dir, const WCHAR *names) +{ + WCHAR *p = alloc(2 * MAX_PATH * sizeof(WCHAR)); hydrate_ctx c; if (!p) return 0; - lstrcpyW(p, dir); lstrcatW(p, L"\\*"); - if ((f = FindFirstFileW(p, &fd)) != INVALID_HANDLE_VALUE) { - do { - if (fd.cFileName[0] == '.' || lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= MAX_PATH) continue; - lstrcpyW(p, dir); lstrcatW(p, L"\\"); lstrcatW(p, fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { count += hydrate_tree(p, names); continue; } - if (!(fd.dwFileAttributes & (FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN | FILE_ATTRIBUTE_OFFLINE))) continue; - if (!name_listed(names, fd.cFileName)) continue; - h = CreateFileW(p, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); - if (h == INVALID_HANDLE_VALUE) continue; - while (ReadFile(h, buf, sizeof(buf), &n, NULL) && n) ; - CloseHandle(h); - count++; - } while (FindNextFileW(f, &fd)); - FindClose(f); - } + c.names = names; c.count = 0; + dir_walk(dir, p, 2 * MAX_PATH, MAX_PATH, hydrate_entry, &c); release(p); - return count; + return c.count; } // The setup also installs manuals and example data to Documents\Dyna Pro Dynamometers. When Documents diff --git a/src/setup.c b/src/setup.c index bf18728..702f774 100644 --- a/src/setup.c +++ b/src/setup.c @@ -123,6 +123,45 @@ void release(void *p) { if (p) HeapFree(GetProcessHeap(), 0, p); } static WCHAR *cat3(WCHAR *d, const WCHAR *a, const WCHAR *b, const WCHAR *c) { lstrcpyW(d, a); lstrcatW(d, b); if (c) lstrcatW(d, c); return d; } BOOL exists(const WCHAR *p) { return GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES; } + +BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name) +{ + if (lstrlenW(dir) + lstrlenW(name) + 2 >= limit) return FALSE; + if (out != dir) lstrcpyW(out, dir); + lstrcatW(out, L"\\"); lstrcatW(out, name); + return TRUE; +} + +int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code) +{ + STARTUPINFOW si; PROCESS_INFORMATION pi; int r = PROC_EXITED; + zero(&si, sizeof(si)); si.cb = sizeof(si); + if (hide) { si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } + if (!CreateProcessW(exe, cmd, NULL, NULL, FALSE, flags, NULL, cwd, &si, &pi)) return PROC_NOSTART; + if (WaitForSingleObject(pi.hProcess, ms) == WAIT_OBJECT_0 || ms == INFINITE) GetExitCodeProcess(pi.hProcess, code); + else { TerminateProcess(pi.hProcess, 1); r = PROC_TIMEOUT; } + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + return r; +} + +int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx) +{ + WIN32_FIND_DATAW fd; HANDLE f; int r = WALK_DONE; + if (lstrlenW(dir) + 3 > bufcch) return WALK_NOOPEN; + lstrcpyW(buf, dir); lstrcatW(buf, L"\\*"); + if ((f = FindFirstFileW(buf, &fd)) == INVALID_HANDLE_VALUE) return WALK_NOOPEN; + do { + if (fd.cFileName[0] == '.' || !path_join(buf, limit, dir, fd.cFileName)) continue; + if (!fn(buf, &fd, ctx)) { r = WALK_STOPPED; break; } + } while (FindNextFileW(f, &fd)); + FindClose(f); + return r; +} + +BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k) +{ return !RegOpenKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, sam, k); } +BOOL app_key_create(BOOL machine, HKEY *k) +{ return !RegCreateKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, k, NULL); } int msg(const WCHAR *text, UINT flags) { return MessageBoxW(g_hwnd, text, TITLE, flags | MB_SETFOREGROUND); } static void info(const WCHAR *text) { if (!g_quiet) msg(text, MB_ICONINFORMATION); } static void error2(const WCHAR *text, const WCHAR *what) @@ -242,9 +281,7 @@ void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth) if (f != INVALID_HANDLE_VALUE) { do { if (!lstrcmpW(fd.cFileName, L".") || !lstrcmpW(fd.cFileName, L"..")) continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 >= 2 * MAX_PATH) { kept = TRUE; continue; } - if (depth == 0 && name_in(keep, fd.cFileName)) { kept = TRUE; continue; } - cat3(p, dir, L"\\", fd.cFileName); + if ((depth == 0 && name_in(keep, fd.cFileName)) || !path_join(p, 2 * MAX_PATH, dir, fd.cFileName)) { kept = TRUE; continue; } // kept, or a path too long for the buffer if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { if (!RemoveDirectoryW(p)) MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); } else delete_tree(p, NULL, depth + 1); @@ -1005,8 +1042,7 @@ static BOOL lnk_path_ok(const WCHAR *name, BOOL common) lstrcpyW(par, name); for (e = par + len; e > par && *e != '\\'; e--); *e = 0; if (!lnk_name_in_dirs(name, list, nd)) { lstrcpyW(lname, par); long_form(lname); // the same, with the folder of the file in long form - if (lstrlenW(lname) + lstrlenW(e + 1) + 2 >= MAX_PATH) return FALSE; - lstrcatW(lname, L"\\"); lstrcatW(lname, e + 1); + if (!path_join(lname, MAX_PATH, lname, e + 1)) return FALSE; if (!lnk_name_in_dirs(lname, list, nd)) return FALSE; } a = GetFileAttributesW(par); @@ -1436,7 +1472,7 @@ static int machine_install_locked(const WCHAR *sid) return RC_FAIL; } driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) - if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, APPKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) return RC_FAIL; + if (!app_key_create(APP_MACHINE, &k)) return RC_FAIL; set_str(k, L"InstallDir", g_dir); if (g_exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use else RegDeleteValueW(k, L"DynaRunExe"); @@ -1564,7 +1600,7 @@ static int machine_uninstall(const WCHAR *skip_sid) polsid[0] = 0; reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"TaskbarPolicySid", polsid, 200, 0); drop_policy_of(skip_sid); drop_policy_of(polsid); unretarget_all_users(skip_sid); - if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, APPKEY, 0, KEY_READ, &k)) { RegQueryValueExW(k, L"ManifestInstalled", NULL, NULL, (BYTE *)&man, &n); RegCloseKey(k); } + if (app_key_open(APP_MACHINE, KEY_READ, &k)) { RegQueryValueExW(k, L"ManifestInstalled", NULL, NULL, (BYTE *)&man, &n); RegCloseKey(k); } if (man == 1 && g_exe[0] && !g_exe_trusted && exists(cat3(p, g_exe, L".manifest", NULL))) setup_log(L"the manifest next to DynaRun V3.exe was left in place: DynaRun's folder or exe can be changed by ordinary users, so nothing is deleted or scheduled for deletion there"); else if (man == 1 && g_exe[0] && exists(cat3(p, g_exe, L".manifest", NULL))) { @@ -1586,7 +1622,7 @@ static int machine_uninstall(const WCHAR *skip_sid) // stuck: the original time stamp of DynaRun V3.exe could not be put back; ExeOrigTimePath / ExeOrigTime stay for the next install or // uninstall (touch_report says so) - the key is not deleted then if (!keep && !stuck) RegDeleteKeyW(HKEY_LOCAL_MACHINE, APPKEY); - else if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, APPKEY, 0, KEY_SET_VALUE, &k)) { // what is no longer needed; ManifestInstalled, DynaRunExe (and the time stamp record) stay + else if (app_key_open(APP_MACHINE, KEY_SET_VALUE, &k)) { // what is no longer needed; ManifestInstalled, DynaRunExe (and the time stamp record) stay RegDeleteValueW(k, L"InstallDir"); RegDeleteValueW(k, L"TaskbarPolicy"); RegDeleteValueW(k, L"TaskbarPolicySid"); RegCloseKey(k); } RegDeleteKeyW(HKEY_LOCAL_MACHINE, UNINSTKEY); @@ -1757,7 +1793,7 @@ static void user_stage(void) { static WCHAR p[MAX_PATH]; HKEY b, s; g_pinresult = PIN_NOT_ASKED; - if (RegCreateKeyExW(HKEY_CURRENT_USER, USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) return; + if (!app_key_create(APP_USER, &b)) return; // another version is installed now: what update.c noted no longer applies; the first start of DynaRun looks again RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { @@ -1780,7 +1816,7 @@ static void user_stage(void) if (g_autostart && !g_autostart_later) set_str(s, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); else RegDeleteValueW(s, L"DynaRunFix"); RegCloseKey(s); - if (!RegCreateKeyExW(HKEY_CURRENT_USER, USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) { + if (app_key_create(APP_USER, &b)) { if (g_autostart && g_autostart_later) set_dword(b, L"AutostartPending", 1); else RegDeleteValueW(b, L"AutostartPending"); RegCloseKey(b); @@ -1794,7 +1830,7 @@ static void user_stage(void) void autostart_flush(void) { static WCHAR p[MAX_PATH]; HKEY k, r; DWORD v = 0, n = 4, t = 0; BOOL pending; - if (RegOpenKeyExW(HKEY_CURRENT_USER, USERKEY, 0, KEY_ALL_ACCESS, &k)) return; + if (!app_key_open(APP_USER, KEY_ALL_ACCESS, &k)) return; pending = RegQueryValueExW(k, L"AutostartPending", NULL, &t, (BYTE *)&v, &n) == ERROR_SUCCESS && t == REG_DWORD && v; if (pending) RegDeleteValueW(k, L"AutostartPending"); RegCloseKey(k); diff --git a/src/setup.h b/src/setup.h index aed8c69..99a00fb 100644 --- a/src/setup.h +++ b/src/setup.h @@ -34,6 +34,27 @@ int install_all(const WCHAR *msi); // runs the DynaRun MSI, then instal // RC_OK; RC_CANCEL; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). int install_fix_driver(const WCHAR *zip, const WCHAR *accept); int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); +// shared helpers (setup.c): used by setup.c, package.c, driver.c +// out = "dir\name", when the result is short enough for a buffer of `limit` characters (it must be at least 2 characters shorter than limit: the old +// `lstrlenW(dir) + lstrlenW(name) + 2 >= limit` test); FALSE and out untouched otherwise. out may be dir itself (not name). +BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name); +// Starts exe with the command line cmd (a writable buffer; creation flags `flags`, working folder cwd or NULL, window hidden when `hide`) and waits for it. +// PROC_EXITED: it ended, *code is its exit code (left alone if the code cannot be read); PROC_TIMEOUT: still running after ms (INFINITE never), it was +// terminated (exit code 1), *code is left alone; PROC_NOSTART: CreateProcess failed, GetLastError() is its error. Handles are not inherited. +enum { PROC_NOSTART = 0, PROC_EXITED = 1, PROC_TIMEOUT = 2 }; +int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code); +// Calls fn(path, fd, ctx) for each entry of dir; names starting with '.' and entries that do not pass path_join(.., limit, ..) are skipped. +// path is "dir\name" in buf (the caller's buffer of bufcch characters, which first holds the pattern "dir\*"). fn may call dir_walk again for a subfolder, +// with a buffer of its own: that is how the walks recurse. fn returns FALSE to stop. WALK_DONE: all entries visited; WALK_STOPPED: fn stopped it; +// WALK_NOOPEN: the folder cannot be read (or the pattern does not fit in buf). +enum { WALK_DONE = 0, WALK_STOPPED = 1, WALK_NOOPEN = 2 }; +typedef BOOL (*walk_fn)(const WCHAR *path, const WIN32_FIND_DATAW *fd, void *ctx); +int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx); +// HKCU\Software\DynaRunFix (APP_USER) or HKLM\SOFTWARE\DynaRunFix (APP_MACHINE): TRUE and *k open (KEY_ALL_ACCESS when created), else FALSE. The caller closes *k. +#define APP_USER FALSE +#define APP_MACHINE TRUE +BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k); +BOOL app_key_create(BOOL machine, HKEY *k); // shared with driver.c void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth); // a link is removed itself, never followed; in-use files go at the next restart BOOL same_file(const WCHAR *a, const WCHAR *b); From cccfb3d4c7f3d328ed4daa8b234356f85ba775eb Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:09:19 +0800 Subject: [PATCH 13/41] install_opts: pass the install options explicitly instead of through globals g_quiet, g_close, g_pin, g_autostart, g_autostart_later and g_keep become the fields of install_opts. The command line fills one in main, the wizard owns one (and hands each job a copy, taken in start_job) and run_elevated builds its forwarded switches from the struct it is given: no hidden dependency on globals. Same switches in the same order ( /quiet /close /pin /keep). The wizard's "close DynaRun" consent (g_close_asked) still covers one install job only. g_pinresult was a result, not an option: it is install_result.pin, written by the user stage and read by the wizard once the job is done. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 20 ++-- src/errors.h | 2 +- src/setup.c | 212 +++++++++++++++++++++-------------------- src/setup.h | 41 +++++--- src/wizard.c | 55 ++++++----- tests/test_driver.c | 33 +++---- tests/test_hardening.c | 64 ++++++------- tests/test_update.c | 2 +- 8 files changed, 222 insertions(+), 207 deletions(-) diff --git a/src/driver.c b/src/driver.c index 490757a..7421a0e 100644 --- a/src/driver.c +++ b/src/driver.c @@ -1238,14 +1238,14 @@ static int install_locked(void) /* ---------- from the wizard (not elevated): run the elevated parts ---------- */ -int driver_prep(const WCHAR *zip, const WCHAR *accept) +int driver_prep(const install_opts *o, const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 300]; if (!user_sid(sid, 200)) return RC_FAIL; if (is_admin()) return prep_locked(sid, zip, accept); if (accept && accept[0]) wsprintfW(args, L"/driver-prep %s \"%s\" /unverified %s", sid, zip, accept); else wsprintfW(args, L"/driver-prep %s \"%s\"", sid, zip); - return run_elevated(args, NULL); + return run_elevated(args, NULL, o); } // Called by the elevated install stage (setup.c, /machine /full ... /with-driver): the driver's preparation in the same process as the fix, so that @@ -1266,17 +1266,17 @@ int driver_bitlocker(void) return drv_bde_suspend(ps, bi.conv, bi.nprot) ? BL_ON : BL_OFF; // the same rule as prep's, so the hint on the restart page agrees with what will happen } -int driver_install(void) +int driver_install(const install_opts *o) { if (is_admin()) return install_locked(); - return run_elevated(L"/driver-install", NULL); + return run_elevated(L"/driver-install", NULL, o); } -int driver_cancel(void) +int driver_cancel(const install_opts *o) { static WCHAR base[MAX_PATH]; if (is_admin()) { base_dir(base); log_in_base(base, NULL); cancel_locked(); return RC_OK; } - return run_elevated(L"/driver-cancel", NULL); + return run_elevated(L"/driver-cancel", NULL, o); } /* ---------- Start menu shortcut, uninstall ---------- */ @@ -1326,14 +1326,14 @@ static const WCHAR *prep_accept_arg(int argc, WCHAR **argv) return argc >= 6 && !lstrcmpiW(argv[4], L"/unverified") && lstrlenW(argv[5]) == 64 ? argv[5] : NULL; } -int driver_verb(int argc, WCHAR **argv) +int driver_verb(int argc, WCHAR **argv, const install_opts *o) { const WCHAR *v = argv[1]; - if (!lstrcmpiW(v, L"/driver")) { g_drvmode = DRV_MODE_STANDALONE; return wizard(); } - if (!lstrcmpiW(v, L"/driver-continue")) { g_drvmode = DRV_MODE_CONTINUE; return wizard(); } + if (!lstrcmpiW(v, L"/driver")) { g_drvmode = DRV_MODE_STANDALONE; return wizard(o); } + if (!lstrcmpiW(v, L"/driver-continue")) { g_drvmode = DRV_MODE_CONTINUE; return wizard(o); } if (lstrcmpiW(v, L"/driver-prep") && lstrcmpiW(v, L"/driver-install") && lstrcmpiW(v, L"/driver-cancel")) return DRV_NOVERB; if (!is_admin()) return RC_FAIL; // the elevated parts: only with the rights they need if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], prep_accept_arg(argc, argv)) : RC_FAIL; if (!lstrcmpiW(v, L"/driver-install")) return install_locked(); - return driver_cancel(); + return driver_cancel(o); } diff --git a/src/errors.h b/src/errors.h index 3c907e3..be43b54 100644 --- a/src/errors.h +++ b/src/errors.h @@ -50,5 +50,5 @@ enum { /* ---- 5. Smaller answers ---- */ enum { RM_NONE = -1 }; // remove_dynarun(): DynaRun V3 is not installed (else the msiexec exit code) enum { BL_UNKNOWN = -1, BL_OFF = 0, BL_ON = 1 }; // driver_bitlocker(): BitLocker protection of the system drive (not elevated: cannot be told) -enum { PIN_NOT_ASKED = 0, PIN_DONE = 1, PIN_NEXT_SIGNIN = 2, PIN_FAILED = 3 }; // g_pinresult: after the install, 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin +enum { PIN_NOT_ASKED = 0, PIN_DONE = 1, PIN_NEXT_SIGNIN = 2, PIN_FAILED = 3 }; // install_result.pin: after the install, 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin enum { HINT_NONE = 0, HINT_NEWER = 1, HINT_ZIPDIFF = 2 }; // pkg_update_hint: nothing to say, the site names a newer version, the site's zip differs from the last download diff --git a/src/setup.c b/src/setup.c index 702f774..adaebcf 100644 --- a/src/setup.c +++ b/src/setup.c @@ -83,9 +83,8 @@ typedef struct IPinnedList3 { const struct { static const WCHAR UNINSTKEY[] = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\DynaRunFix"; static const WCHAR DYNARUN_REL[] = L"\\Dyna Pro Dynamometers\\DynaRun V3.exe"; -BOOL g_zh, g_quiet, g_close, g_pin = TRUE, g_autostart = TRUE, g_autostart_later, g_keep; +BOOL g_zh; int g_drvmode; // DRV_MODE_*: the wizard starts at the driver pages (driver.c, wizard.c) -int g_pinresult; HWND g_hwnd; WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; static WCHAR g_user_sid[200]; // the user who started the setup (elevated stage: from its command line) @@ -118,6 +117,7 @@ const PfnDliHook __pfnDliNotifyHook2 = dll_hook; /* ---------- small helpers (no C runtime) ---------- */ void zero(void *p, SIZE_T n) { volatile char *z = (volatile char *)p; while (n--) *z++ = 0; } +void install_opts_default(install_opts *o) { zero(o, sizeof(*o)); o->pin = TRUE; o->autostart = TRUE; } void *alloc(SIZE_T n) { return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, n); } void release(void *p) { if (p) HeapFree(GetProcessHeap(), 0, p); } static WCHAR *cat3(WCHAR *d, const WCHAR *a, const WCHAR *b, const WCHAR *c) @@ -163,9 +163,9 @@ BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k) BOOL app_key_create(BOOL machine, HKEY *k) { return !RegCreateKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, k, NULL); } int msg(const WCHAR *text, UINT flags) { return MessageBoxW(g_hwnd, text, TITLE, flags | MB_SETFOREGROUND); } -static void info(const WCHAR *text) { if (!g_quiet) msg(text, MB_ICONINFORMATION); } -static void error2(const WCHAR *text, const WCHAR *what) -{ static WCHAR b[1024]; if (g_quiet) return; lstrcpynW(b, text, 600); if (what) { lstrcatW(b, L"\n\n"); lstrcatW(b, what); } msg(b, MB_ICONERROR); } +static void info(const install_opts *o, const WCHAR *text) { if (!o->quiet) msg(text, MB_ICONINFORMATION); } +static void error2(const install_opts *o, const WCHAR *text, const WCHAR *what) +{ static WCHAR b[1024]; if (o->quiet) return; lstrcpynW(b, text, 600); if (what) { lstrcatW(b, L"\n\n"); lstrcatW(b, what); } msg(b, MB_ICONERROR); } BOOL is_admin(void) { @@ -357,7 +357,7 @@ static void setup_log(const WCHAR *line) if (g_dir[0] && exists(g_dir)) append_line(cat3(p, g_dir, L"\\setup.log", NULL), line); } -static void warn(const WCHAR *text) { setup_log(text); if (!g_quiet) msg(text, MB_ICONWARNING); } +static void warn(const install_opts *o, const WCHAR *text) { setup_log(text); if (!o->quiet) msg(text, MB_ICONWARNING); } static BOOL sid_in(PSID s, const WCHAR *const *list) { @@ -527,10 +527,10 @@ static int touch(const WCHAR *p) } // what the caller does with touch's result -static int touch_report(int t) +static int touch_report(const install_opts *o, int t) { if (t == TOUCH_STUCK) - warn(T(L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", + warn(o, T(L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", L"DynaRun V3.exe 的修改時間無法還原。已記錄在登錄檔,下次安裝或解除安裝 DynaRunFix 時會再還原。")); else if (t == TOUCH_NOTDONE) setup_log(L"the manifest was not refreshed: DynaRun V3.exe could not be touched"); return t; @@ -686,7 +686,7 @@ void user_log(const WCHAR *line) LeaveCriticalSection(&g_ulog_cs); } -static void log_running(const WCHAR *stage, const runproc *l, int n, int r) +static void log_running(const WCHAR *stage, const runproc *l, int n, int r, BOOL close) { static const WCHAR *const names[] = { L"DynaRun V3.exe", L"DynaRunFix.exe", L"LEProc.exe" }; static const WCHAR *const reasons[] = { L"can be closed", L"cannot be closed: runs in another session", @@ -695,8 +695,8 @@ static void log_running(const WCHAR *stage, const runproc *l, int n, int r) GetLocalTime(&st); ProcessIdToSessionId(GetCurrentProcessId(), &me); wsprintfW(t, L"%04u-%02u-%02u %02u:%02u:%02u %s: stopped, exit code 6 (DynaRun is running), nothing was changed. %s", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, stage, - (r & RUN_NOCLOSE) ? (g_close ? L"/close was not used: a program that cannot be closed is running." : L"A program that cannot be closed is running.") - : (g_close ? L"It was still running after /close." : L"Run with /close to close it.")); + (r & RUN_NOCLOSE) ? (close ? L"/close was not used: a program that cannot be closed is running." : L"A program that cannot be closed is running.") + : (close ? L"It was still running after /close." : L"Run with /close to close it.")); run_log(t); for (i = 0; i < n; i++) { k = l[i].bit == RUN_DYNARUN ? 0 : l[i].bit == RUN_LAUNCHER ? 1 : 2; @@ -742,22 +742,22 @@ static void close_programs(void) // The elevated install stages: closes DynaRun first when the user agreed (or /close), then TRUE (and the reason logged) when something // still holds up the install. Nothing is closed when part of it cannot be closed: the install stops anyway, and a test run would end for nothing. -static BOOL still_running(const WCHAR *stage) +static BOOL still_running(const install_opts *o, const WCHAR *stage) { runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); - if (g_close && r && !(r & RUN_NOCLOSE)) { close_programs(); r = scan_programs(l, l ? 16 : 0, &n); } - if (r && l) log_running(stage, l, n, r); + if (o->close && r && !(r & RUN_NOCLOSE)) { close_programs(); r = scan_programs(l, l ? 16 : 0, &n); } + if (r && l) log_running(stage, l, n, r, o->close); release(l); return r != 0; } // Before the UAC prompt: TRUE (logged) when /quiet without /close ends here. With /close the elevated stage decides: it can read more. -static BOOL running_early(const WCHAR *stage) +static BOOL running_early(const install_opts *o, const WCHAR *stage) { runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); - if (r && !g_close && l) log_running(stage, l, n, r); + if (r && !o->close && l) log_running(stage, l, n, r, o->close); release(l); - return r && !g_close; + return r && !o->close; } // The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in lives in tests\promote_com.h). @@ -1139,13 +1139,13 @@ static const WCHAR XML_HEAD[] = // Windows 11 (elevated stage): no program may pin itself any more; Microsoft's taskbar layout policy // (learn.microsoft.com/windows/configuration/taskbar/pinned-apps) adds the pin at the user's next sign-in. // The policy is left alone when an organization has already set a layout file. -static void taskbar_policy(const WCHAR *sid, HKEY app) +static void taskbar_policy(const install_opts *o, const WCHAR *sid, HKEY app) { static WCHAR x[MAX_PATH], l[MAX_PATH], xml[2400], path[300], v[MAX_PATH]; static char u[7200]; DWORD ma, mi, b, ubr = 0, n = 4; HKEY k; BOOL gen; os_ver(&ma, &mi, &b); - if (g_keep) return; - if (!g_pin) { // reinstalled without the pin: take our policy back + if (o->keep) return; + if (!o->pin) { // reinstalled without the pin: take our policy back if (reg_str(app, NULL, L"TaskbarPolicySid", v, MAX_PATH, 0) && !RegOpenKeyExW(HKEY_USERS, v, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } RegDeleteValueW(app, L"TaskbarPolicy"); RegDeleteValueW(app, L"TaskbarPolicySid"); return; @@ -1228,8 +1228,8 @@ static BOOL pin_list(const WCHAR *lnk) // Windows 10 1809 and later } // Pins DynaRun (the shortcut lnk, which starts the launcher) to the taskbar of the current user. TRUE: done, or -// on Windows 11 under way (g_pinresult 2: the policy set by the machine stage applies at the next sign-in). -static BOOL pin_taskbar(const WCHAR *lnk) +// on Windows 11 under way (*res = PIN_NEXT_SIGNIN: the policy set by the machine stage applies at the next sign-in). +static BOOL pin_taskbar(const WCHAR *lnk, int *res) { static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; os_ver(&ma, &mi, &b); @@ -1245,8 +1245,8 @@ static BOOL pin_taskbar(const WCHAR *lnk) return TRUE; } if (ma >= 10 && b >= 22000) { // Windows 11: see taskbar_policy - if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) g_pinresult = PIN_NEXT_SIGNIN; - return g_pinresult == PIN_NEXT_SIGNIN; + if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) *res = PIN_NEXT_SIGNIN; + return *res == PIN_NEXT_SIGNIN; } if (pinned_ours()) return TRUE; if (ma >= 10 && b >= 17763) pin_list(lnk); else pin_verb(lnk); @@ -1441,18 +1441,18 @@ BOOL valid_user_sid(const WCHAR *sid) return ok; } -static int machine_install_locked(const WCHAR *sid) +static int machine_install_locked(const install_opts *o, const WCHAR *sid) { static WCHAR p[MAX_PATH + 16]; HKEY k; - if (!valid_user_sid(sid)) { error2(T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } + if (!valid_user_sid(sid)) { error2(o, T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } lstrcpynW(g_user_sid, sid, 200); - if (still_running(L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since - if (!check_exe()) { error2(T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } + if (still_running(o, L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since + if (!check_exe()) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } CreateDirectoryW(g_dir, NULL); if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { - error2(T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); + error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); return RC_FAIL; } { // Locale Emulator, resources 10.. (see setup.rc) @@ -1462,13 +1462,13 @@ static int machine_install_locked(const WCHAR *sid) CreateDirectoryW(cat3(p, g_dir, L"\\le", NULL), NULL); for (i = 0; i < (int)(sizeof(le) / sizeof(le[0])); i++) if (!extract(10 + i, cat3(p, g_dir, L"\\le\\", le[i]))) { - error2(T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); + error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); return RC_FAIL; } } cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); if (lstrcmpiW(p, g_self) && ((!CopyFileW(g_self, p, FALSE) && !(move_aside(p) && CopyFileW(g_self, p, FALSE))) || !same_file(g_self, p))) { - error2(T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); + error2(o, T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); return RC_FAIL; } driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) @@ -1477,18 +1477,18 @@ static int machine_install_locked(const WCHAR *sid) if (g_exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use else RegDeleteValueW(k, L"DynaRunExe"); - touch_report(recover_exe_time()); // a time stamp an earlier run could not put back + touch_report(o, recover_exe_time()); // a time stamp an earlier run could not put back // the per-user COM registrations are not copied to HKLM any more (records of older versions are left alone) if (!g_exe_trusted) setup_log(L"DynaRun's folder or exe can be changed by ordinary users (or its path contains a link): no manifest was written"); // garbled text under "Beta: Use Unicode UTF-8": external manifest with activeCodePage=Legacy if (GetACP() == CP_UTF8 && g_exe_trusted && !exists(cat3(p, g_exe, L".manifest", NULL)) && extract(3, p)) { set_dword(k, L"ManifestInstalled", 1); - touch_report(touch(g_exe)); + touch_report(o, touch(g_exe)); } { HKEY b; if (!RegCreateKeyExW(k, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) { fix_shortcuts(TRUE, b); RegCloseKey(b); } } - taskbar_policy(sid, k); + taskbar_policy(o, sid, k); RegCloseKey(k); if (!RegCreateKeyExW(HKEY_LOCAL_MACHINE, UNINSTKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { @@ -1505,11 +1505,11 @@ static int machine_install_locked(const WCHAR *sid) return RC_OK; } -static int machine_install(const WCHAR *sid) +static int machine_install(const install_opts *o, const WCHAR *sid) { int rc; - if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } - rc = machine_install_locked(sid); + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } + rc = machine_install_locked(o, sid); stage_unlock(); return rc; } @@ -1584,16 +1584,16 @@ static void drop_policy_of(const WCHAR *sid) if (!RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } } -static int machine_uninstall(const WCHAR *skip_sid) +static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) { static WCHAR p[MAX_PATH + 16], polsid[200]; static const WCHAR KEEP[] = L"com-copied.txt\0com-backup\0com-skipped.txt\0setup.log\0"; // the record of the COM copies stays HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; - if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(g_user_sid, skip_sid, 200); else skip_sid = NULL; - if (g_close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open + if (o->close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open if (!g_exe[0]) locate_dynarun(); // not recorded (an untrusted path is not): the default folders check_exe(); // g_exe is from HKLM; g_exe_trusted says whether to run it - if (touch_report(recover_exe_time()) == TOUCH_STUCK) stuck = TRUE; + if (touch_report(o, recover_exe_time()) == TOUCH_STUCK) stuck = TRUE; restore_shortcuts(HKEY_LOCAL_MACHINE, APPKEY); unretarget_shell(TRUE); // the taskbar policy (Windows 11) of the user who runs the uninstall and of the user the install recorded: read before the key goes @@ -1608,14 +1608,14 @@ static int machine_uninstall(const WCHAR *skip_sid) else if (DeleteFileW(p)) deleted = TRUE; else if (move_aside(p)) { // in use: renamed at once (a reinstall before the restart writes its own file), the renamed one goes at the next restart deleted = TRUE; - warn(T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); } else { // cannot even be renamed: it goes at the next restart, and the record that it is ours stays MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); keep = TRUE; - warn(T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); } } - if (deleted) { if (g_exe_trusted) { if (touch_report(touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } + if (deleted) { if (g_exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } // Before the key goes: the driver part ends a waiting task and resumes BitLocker if DynaRunFix suspended it (the flag is in the key). // An installed driver stays. driver_machine_uninstall(); @@ -1665,12 +1665,12 @@ static HANDLE copy_and_hold(const WCHAR *src, const WCHAR *dst) } // Runs " " elevated (one UAC prompt) and returns its exit code; 2 = cancelled. exe: the program to run, NULL = this one. -int run_elevated(const WCHAR *args, const WCHAR *exe_path) +int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o) { SHELLEXECUTEINFOW se; DWORD rc = RC_FAIL, err, tn; static WCHAR a[1024], root[4], tmp[MAX_PATH]; const WCHAR *exe = exe_path ? exe_path : g_self; HANDLE hold = INVALID_HANDLE_VALUE; LARGE_INTEGER q; - lstrcpyW(a, args); if (g_quiet) lstrcatW(a, L" /quiet"); if (g_close) lstrcatW(a, L" /close"); - if (g_pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin - if (g_keep) lstrcatW(a, L" /keep"); + lstrcpyW(a, args); if (o->quiet) lstrcatW(a, L" /quiet"); if (o->close) lstrcatW(a, L" /close"); + if (o->pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin + if (o->keep) lstrcatW(a, L" /keep"); // Mapped network drives (e.g. a shared folder) do not exist for the elevated process: run a copy in %TEMP% lstrcpynW(root, g_self, 4); if (!exe_path && root[1] == ':' && GetDriveTypeW(root) == DRIVE_REMOTE && (tn = GetTempPathW(MAX_PATH - 60, tmp)) != 0 && tn < MAX_PATH - 60) { // room for the longest name below (17 + 3 x 10 digits + 2 + 4 + NUL) @@ -1757,12 +1757,12 @@ static BOOL stage_msi(const WCHAR *msi, WCHAR *dir, WCHAR *staged) return TRUE; } -static int machine_full(const WCHAR *sid, const WCHAR *msi) +static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *msi) { static WCHAR me[200], txt[300], dir[MAX_PATH], staged[MAX_PATH]; int rc; if (!user_sid(me, 200) || lstrcmpiW(me, sid)) return RC_OVER_SHOULDER; - if (!stage_lock()) { error2(busy_text(), NULL); return RC_FAIL; } - if (still_running(L"full install")) { stage_unlock(); return RC_RUNNING; } + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } + if (still_running(o, L"full install")) { stage_unlock(); return RC_RUNNING; } if (!stage_msi(msi, dir, staged)) { stage_unlock(); msg(T(L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。"), MB_ICONERROR); @@ -1785,14 +1785,14 @@ static int machine_full(const WCHAR *sid, const WCHAR *msi) return RC_MSI_FAILED; } if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return RC_NO_DYNARUN; } - return machine_install(sid); + return machine_install(o, sid); } // Not elevated: the current user's shortcuts; a desktop shortcut when there is none at all. -static void user_stage(void) +static void user_stage(const install_opts *o, install_result *res) { static WCHAR p[MAX_PATH]; HKEY b, s; - g_pinresult = PIN_NOT_ASKED; + res->pin = PIN_NOT_ASKED; if (!app_key_create(APP_USER, &b)) return; // another version is installed now: what update.c noted no longer applies; the first start of DynaRun looks again RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); @@ -1800,24 +1800,24 @@ static void user_stage(void) int desk = fix_shortcuts(FALSE, s); if (!desk && SHGetSpecialFolderPathW(NULL, p, CSIDL_COMMON_DESKTOPDIRECTORY, FALSE)) desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here - if (!desk && !g_keep && SHGetSpecialFolderPathW(NULL, p, CSIDL_DESKTOPDIRECTORY, FALSE) && + if (!desk && !o->keep && SHGetSpecialFolderPathW(NULL, p, CSIDL_DESKTOPDIRECTORY, FALSE) && save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); - if (g_pin && !g_keep) { - g_pinresult = PIN_FAILED; - if (desktop_link(p, s) && pin_taskbar(p) && g_pinresult == PIN_FAILED) g_pinresult = PIN_DONE; + if (o->pin && !o->keep) { + res->pin = PIN_FAILED; + if (desktop_link(p, s) && pin_taskbar(p, &res->pin) && res->pin == PIN_FAILED) res->pin = PIN_DONE; } RegCloseKey(s); } RegCloseKey(b); - if (!g_keep && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { // start at sign-in + if (!o->keep && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { // start at sign-in // with the driver step ahead the value is NOT written now: the sign-in after the driver restart would start DynaRun while the // driver is being installed. It is only noted (AutostartPending) and registered by autostart_flush when the driver steps are over. - if (g_autostart && !g_autostart_later) set_str(s, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); + if (o->autostart && !o->autostart_later) set_str(s, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); else RegDeleteValueW(s, L"DynaRunFix"); RegCloseKey(s); if (app_key_create(APP_USER, &b)) { - if (g_autostart && g_autostart_later) set_dword(b, L"AutostartPending", 1); + if (o->autostart && o->autostart_later) set_dword(b, L"AutostartPending", 1); else RegDeleteValueW(b, L"AutostartPending"); RegCloseKey(b); } @@ -1859,53 +1859,53 @@ static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) if (accept && accept[0]) { lstrcatW(args, L" /unverified "); lstrcatW(args, accept); } } -int install_fix_driver(const WCHAR *zip, const WCHAR *accept) +int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; if (!user_sid(sid, 200)) return RC_FAIL; - if (running_early(L"install (before elevation)")) return RC_RUNNING; // before the UAC prompt; /quiet without /close ends here + if (running_early(o, L"install (before elevation)")) return RC_RUNNING; // before the UAC prompt; /quiet without /close ends here CoInitialize(NULL); - if (is_admin()) { rc = machine_install(sid); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } + if (is_admin()) { rc = machine_install(o, sid); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } else { wsprintfW(args, L"/machine %s \"%s\"", sid, g_exe); add_driver_args(args, zip, accept); - if ((rc = run_elevated(args, NULL)) == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } + if ((rc = run_elevated(args, NULL, o)) == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } } if (!FIX_DONE(rc)) return rc; - user_stage(); + user_stage(o, res); return rc; } -int install_fix(void) { return install_fix_driver(NULL, NULL); } +int install_fix(const install_opts *o, install_result *res) { return install_fix_driver(o, res, NULL, NULL); } -int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) { static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; if (!user_sid(sid, 200)) return RC_FAIL; - if (running_early(L"full install (before elevation)")) return RC_RUNNING; + if (running_early(o, L"full install (before elevation)")) return RC_RUNNING; CoInitialize(NULL); pkg_prepare_documents(msi); // as this user, before the SYSTEM-side install (error 1305 otherwise) - if (is_admin()) { rc = machine_full(sid, msi); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } + if (is_admin()) { rc = machine_full(o, sid, msi); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } else { wsprintfW(args, L"/full %s \"%s\"", sid, msi); add_driver_args(args, zip, accept); - rc = run_elevated(args, NULL); + rc = run_elevated(args, NULL, o); } if (rc == RC_OVER_SHOULDER) { // over-the-shoulder elevation: setup as this user, then the fix (and the driver) rc = run_msiexec(msi); if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; if (rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED) return RC_MSI_FAILED; if (!locate_dynarun()) return RC_NO_DYNARUN; - return install_fix_driver(zip, accept); + return install_fix_driver(o, res, zip, accept); } if (rc == RC_CANCEL) return RC_CANCEL; if (!FIX_DONE(rc)) return rc; locate_dynarun(); - user_stage(); + user_stage(o, res); return rc; } -int install_all(const WCHAR *msi) { return install_all_driver(msi, NULL, NULL); } +int install_all(const install_opts *o, install_result *res, const WCHAR *msi) { return install_all_driver(o, res, msi, NULL, NULL); } // The uninstall question's Yes / No buttons become "Fix only" / "Remove all". static HHOOK g_cbt; @@ -1937,21 +1937,22 @@ static void drop_old_temp_copies(void) FindClose(h); } -static int uninstall(void) +static int uninstall(const install_opts *given) { static WCHAR p[MAX_PATH + 160], prod[40], sid[200], args[260]; int rc; BOOL all = FALSE; + install_opts mine = *given, *o = &mine; // the "close DynaRun" consent asked below is this uninstall's own const WCHAR *elev = NULL; // The uninstaller runs from the installed program (no copy in %TEMP%): the elevated stage renames what it cannot delete (this very // file, while the user-side stage still runs) and Windows deletes it at the next restart. cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); drop_old_temp_copies(); // copies that earlier builds left in %TEMP% if (!exists(p) && !reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0)) { - info(T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); + info(o, T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); return RC_OK; } - if (!g_quiet && !dynarun_product(prod)) { + if (!o->quiet && !dynarun_product(prod)) { if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; - } else if (!g_quiet) { + } else if (!o->quiet) { g_cbt = SetWindowsHookExW(WH_CBT, relabel, NULL, GetCurrentThreadId()); rc = msg(T(L"Uninstall DynaRunFix?\n\n" L"Fix only: the DynaRun shortcuts start the original DynaRun again; DynaRun V3 stays installed.\n\n" @@ -1965,12 +1966,12 @@ static int uninstall(void) } // Running DynaRun / launcher / LEProc hold files of the folder: ask first, the elevated stage then closes them. // /quiet without /close goes on; what is still in use is deleted at the next restart. - if (!g_quiet && running_programs()) { + if (!o->quiet && running_programs()) { if (msg(T(L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; - g_close = TRUE; + o->close = TRUE; } reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); if (!g_exe[0]) locate_dynarun(); @@ -1978,25 +1979,25 @@ static int uninstall(void) if (!user_sid(sid, 200)) sid[0] = 0; // The elevated stage comes first: when the UAC prompt is cancelled (or fails to start) nothing has been changed yet and the // user's shortcuts, Run value and pins still work with the installed launcher. - if (is_admin()) rc = machine_uninstall(sid); + if (is_admin()) rc = machine_uninstall(o, sid); else { // What runs elevated is the installed program in Program Files (administrators-only folder), never a copy in a folder the user can // write. Only when there is no installed copy (a broken install) is this file itself run, if it is not one of our %TEMP% copies. lstrcpynW(g_user_sid, sid, 200); if (exists(p)) { if (g_is_writable(g_dir) || g_is_writable(p)) { - error2(T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", + error2(o, T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); return RC_FAIL; } elev = p; } wsprintfW(args, L"/unmachine %s", sid); - rc = run_elevated(sid[0] ? args : L"/unmachine", elev); + rc = run_elevated(sid[0] ? args : L"/unmachine", elev, o); } if (rc == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } if (rc) { // the elevated stage did not finish: nothing user-side is undone either - if (!g_quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } + if (!o->quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } return rc; } restore_shortcuts(HKEY_CURRENT_USER, USERKEY); @@ -2007,17 +2008,17 @@ static int uninstall(void) SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); if (all) { rc = remove_dynarun(); - if (rc == ERROR_INSTALL_USEREXIT) { info(T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } + if (rc == ERROR_INSTALL_USEREXIT) { info(o, T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } if (rc > 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { wsprintfW(p, T(L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。"), rc); msg(p, MB_ICONERROR); return RC_MSI_FAILED; } - info(T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); + info(o, T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); return RC_OK; } - info(T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); + info(o, T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); return RC_OK; } @@ -2033,18 +2034,19 @@ static const WCHAR *after_args(int n) return c; } -// The switches of the command line, at any position (tests/test_hardening.c locks the rules). g_quiet, g_close, g_pin, g_autostart and g_keep are set; +// The switches of the command line, at any position (tests/test_hardening.c locks the rules). *o (the defaults, install_opts_default) gets quiet, close, +// keep set and pin, autostart cleared by what the command line says; // *pin: /pin seen; *dzip, *daccept: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). -static void parse_switches(int argc, WCHAR **argv, BOOL *pin, const WCHAR **dzip, const WCHAR **daccept) +static void parse_switches(int argc, WCHAR **argv, install_opts *o, BOOL *pin, const WCHAR **dzip, const WCHAR **daccept) { int i; for (i = 1; i < argc; i++) { - if (!lstrcmpiW(argv[i], L"/quiet")) g_quiet = TRUE; - else if (!lstrcmpiW(argv[i], L"/close")) g_close = TRUE; - else if (!lstrcmpiW(argv[i], L"/notaskbar")) g_pin = FALSE; - else if (!lstrcmpiW(argv[i], L"/noautostart")) g_autostart = FALSE; + if (!lstrcmpiW(argv[i], L"/quiet")) o->quiet = TRUE; + else if (!lstrcmpiW(argv[i], L"/close")) o->close = TRUE; + else if (!lstrcmpiW(argv[i], L"/notaskbar")) o->pin = FALSE; + else if (!lstrcmpiW(argv[i], L"/noautostart")) o->autostart = FALSE; else if (!lstrcmpiW(argv[i], L"/pin")) *pin = TRUE; - else if (!lstrcmpiW(argv[i], L"/keep")) g_keep = TRUE; + else if (!lstrcmpiW(argv[i], L"/keep")) o->keep = TRUE; else if (i >= 4 && !lstrcmpiW(argv[i], L"/with-driver") && i + 1 < argc) *dzip = argv[++i]; // after the SID and the path: see install_fix_driver else if (i >= 4 && !lstrcmpiW(argv[i], L"/unverified") && i + 1 < argc) { if (lstrlenW(argv[i + 1]) == 64) *daccept = argv[i + 1]; i++; } } @@ -2054,34 +2056,36 @@ void WinMainCRTStartup(void) { typedef BOOL (WINAPI *SetDefaultDllDirectories_t)(DWORD); SetDefaultDllDirectories_t sdd = (SetDefaultDllDirectories_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetDefaultDllDirectories"); - int argc, rc; BOOL pin = FALSE; WCHAR **argv; const WCHAR *dzip = NULL, *daccept = NULL; + int argc, rc; BOOL pin = FALSE; WCHAR **argv; const WCHAR *dzip = NULL, *daccept = NULL; install_opts opts; install_result res; if (sdd) sdd(0x800); // LOAD_LIBRARY_SEARCH_SYSTEM32: no DLL is searched for in the exe's folder or the current folder argv = CommandLineToArgvW(GetCommandLineW(), &argc); g_zh = PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE; init_paths(); - parse_switches(argc, argv, &pin, &dzip, &daccept); - if (argc >= 2 && (!lstrcmpiW(argv[1], L"/machine") || !lstrcmpiW(argv[1], L"/full"))) g_pin = pin; // the elevated stage: as asked + install_opts_default(&opts); + res.pin = PIN_NOT_ASKED; // only the wizard shows it + parse_switches(argc, argv, &opts, &pin, &dzip, &daccept); + if (argc >= 2 && (!lstrcmpiW(argv[1], L"/machine") || !lstrcmpiW(argv[1], L"/full"))) opts.pin = pin; // the elevated stage: as asked if (argc >= 4 && !lstrcmpiW(argv[1], L"/machine")) { lstrcpynW(g_exe, argv[3], MAX_PATH); CoInitialize(NULL); - rc = machine_install(argv[2]); + rc = machine_install(&opts, argv[2]); if (!rc && dzip) rc = driver_prep_stage(argv[2], dzip, daccept); } else if (argc >= 4 && !lstrcmpiW(argv[1], L"/full")) { CoInitialize(NULL); - rc = machine_full(argv[2], argv[3]); + rc = machine_full(&opts, argv[2], argv[3]); if (!rc && dzip) rc = driver_prep_stage(argv[2], dzip, daccept); } else if (argc >= 2 && !lstrcmpiW(argv[1], L"/unmachine")) { reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); CoInitialize(NULL); - rc = machine_uninstall(argc >= 3 && argv[2][0] == 'S' && argv[2][1] == '-' ? argv[2] : NULL); - } else if (argc >= 2 && (rc = driver_verb(argc, argv)) != DRV_NOVERB) { + rc = machine_uninstall(&opts, argc >= 3 && argv[2][0] == 'S' && argv[2][1] == '-' ? argv[2] : NULL); + } else if (argc >= 2 && (rc = driver_verb(argc, argv, &opts)) != DRV_NOVERB) { // /driver, /driver-continue (the wizard at the driver pages), /driver-prep, /driver-install, /driver-cancel (driver.c) - } else if (argc >= 2 && !lstrcmpiW(argv[1], L"/uninstall")) rc = uninstall(); + } else if (argc >= 2 && !lstrcmpiW(argv[1], L"/uninstall")) rc = uninstall(&opts); else if (argc >= 2 && !lstrcmpiW(argv[1], L"/checkupdate")) rc = update_check(); else if (argc >= 2 && !lstrcmpiW(argv[1], L"/update")) rc = update_run(after_args(2)); else if (argc >= 2 && !lstrcmpiW(argv[1], L"/offer")) rc = update_offer(); - else if (g_quiet) rc = locate_dynarun() ? install_fix() : RC_FAIL; - else rc = wizard(); + else if (opts.quiet) rc = locate_dynarun() ? install_fix(&opts, &res) : RC_FAIL; + else rc = wizard(&opts); // the elevated part (/machine, /full) ends here: let the wizard that started it take the foreground back AllowSetForegroundWindow(ASFW_ANY); ExitProcess(rc); diff --git a/src/setup.h b/src/setup.h index 99a00fb..6b00ddf 100644 --- a/src/setup.h +++ b/src/setup.h @@ -4,11 +4,22 @@ #include #include "errors.h" // RC_*, PK_*, PKC_*, DRV_E_* / DRV_W_*, FIX_DONE: the result codes -extern BOOL g_zh, g_quiet, g_close; // g_close: close a running DynaRun before installing (/close) -extern BOOL g_pin, g_autostart; // pin DynaRun to the taskbar / start it at sign-in (default on; /notaskbar, /noautostart) -extern BOOL g_autostart_later; // the driver follows: the sign-in start is registered once the driver steps are over (autostart_flush) -extern BOOL g_keep; // /keep: leave the pin, the sign-in start and the desktop shortcut as they are -extern int g_pinresult; // after the install: 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin +extern BOOL g_zh; +// What the user asked for. Built from the command line (setup.c) or by the wizard's pages (wizard.c) and handed down explicitly: the install, the +// uninstall and run_elevated read nothing else. Only quiet, close, pin and keep reach the elevated stage (run_elevated forwards them as switches). +typedef struct { + BOOL quiet; // /quiet: no message boxes + BOOL close; // close a running DynaRun before installing (/close, or the wizard's consent for one install job) + BOOL pin; // pin DynaRun to the taskbar (default on; /notaskbar) + BOOL autostart; // start it at sign-in (default on; /noautostart) + BOOL autostart_later; // the driver follows: the sign-in start is registered once the driver steps are over (autostart_flush) + BOOL keep; // /keep: leave the pin, the sign-in start and the desktop shortcut as they are +} install_opts; +void install_opts_default(install_opts *o); // pin and autostart on, the rest off +// What the install reports besides its exit code (written by the user stage, read by the wizard). +typedef struct { + int pin; // PIN_*: 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin +} install_result; extern HWND g_hwnd; // wizard window, owner of UAC prompts extern WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; #define T(en, zh) (g_zh ? (zh) : (en)) @@ -28,12 +39,12 @@ BOOL user_sid(WCHAR *out, int cch); BOOL fix_current(void); // the fix is installed and is the version of this setup (uninstall entry, launcher and dll there) BOOL locate_dynarun(void); // fills g_exe from the registry or the default folders; no UI void start_dynarun(void); // the wizard's "Start DynaRun": through our shortcut, like a double click -int install_fix(void); // DynaRun is installed: elevated stage + user shortcuts. RC_OK, RC_CANCEL -int install_all(const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL +int install_fix(const install_opts *o, install_result *res); // DynaRun is installed: elevated stage + user shortcuts. RC_OK, RC_CANCEL +int install_all(const install_opts *o, install_result *res, const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL // The same, and the driver's preparation (driver_prep) in the SAME elevated stage: one UAC prompt for both. zip/accept as for driver_prep. // RC_OK; RC_CANCEL; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). -int install_fix_driver(const WCHAR *zip, const WCHAR *accept); -int install_all_driver(const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); +int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept); +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); // shared helpers (setup.c): used by setup.c, package.c, driver.c // out = "dir\name", when the result is short enough for a buffer of `limit` characters (it must be at least 2 characters shorter than limit: the old // `lstrlenW(dir) + lstrlenW(name) + 2 >= limit` test); FALSE and out untouched otherwise. out may be dir itself (not name). @@ -58,7 +69,7 @@ BOOL app_key_create(BOOL machine, HKEY *k); // shared with driver.c void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth); // a link is removed itself, never followed; in-use files go at the next restart BOOL same_file(const WCHAR *a, const WCHAR *b); -int run_elevated(const WCHAR *args, const WCHAR *exe_path); // " " with one UAC prompt; its exit code, RC_CANCEL = cancelled +int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o); // " [ /quiet][ /close][ /pin][ /keep]" (from *o) with one UAC prompt; its exit code, RC_CANCEL = cancelled BOOL valid_user_sid(const WCHAR *sid); // a SID of a real account with a profile here BOOL path_user_writable(const WCHAR *path, const WCHAR *sid); // could ordinary users (or the user sid) change what is at path (or is that unknown) BOOL enable_privilege(const WCHAR *name); @@ -74,7 +85,7 @@ int update_run(const WCHAR *args); // /update : asks, in void autostart_flush(void); // registers the sign-in start that user_stage deferred (HKCU AutostartPending); no-op otherwise /* wizard.c */ -int wizard(void); +int wizard(const install_opts *init); // init: the options the command line gave (the pages change them) /* package.c */ @@ -168,14 +179,14 @@ int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, const WCHAR *driver_pinned_sha256(void); // the SHA-256 of the zip this program knows void driver_forget(const WCHAR *zip); // deletes the zip when it is our own cache file in %TEMP%\DynaRunFix (a refused download), never a file of the user's BOOL driver_partial(DWORD *have); // a partial driver download that driver_fetch can continue; *have: bytes so far -int driver_prep(const WCHAR *zip, const WCHAR *accept); // elevated: stage the driver, arm the restart menu and the task (zip "-": the staged copy); 0 or DRV_E_*, 2 = UAC cancelled +int driver_prep(const install_opts *o, const WCHAR *zip, const WCHAR *accept); // elevated: stage the driver, arm the restart menu and the task (zip "-": the staged copy); 0 or DRV_E_*, 2 = UAC cancelled int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept); // elevated, inside the install stage (setup.c): driver_prep for sid, no further elevation int driver_bitlocker(void); // BitLocker protection of the system drive, not elevated: 1 on, 0 off / none, -1 cannot be told (needs administrator rights) -int driver_install(void); // elevated: pnputil, then clean up. 0 or DRV_E_*, 2 = UAC cancelled -int driver_cancel(void); // elevated: task, restart menu and BitLocker suspension back. 2 = UAC cancelled +int driver_install(const install_opts *o); // elevated: pnputil, then clean up. 0 or DRV_E_*, 2 = UAC cancelled +int driver_cancel(const install_opts *o); // elevated: task, restart menu and BitLocker suspension back. 2 = UAC cancelled BOOL driver_reboot(void); // restarts Windows (not elevated) int driver_ci_state(void); // DRV_CI_* -int driver_verb(int argc, WCHAR **argv); // command line verbs; DRV_NOVERB when argv[1] is none of ours, else the exit code +int driver_verb(int argc, WCHAR **argv, const install_opts *o); // command line verbs; DRV_NOVERB when argv[1] is none of ours, else the exit code void driver_machine_stage(const WCHAR *installed_setup); // elevated install: the Start menu shortcut "Install Dyna Pro USB driver" int drv_file_count(void); // the files that get installed (pinned: name below "Dyna Pro USB\", size, SHA-256) int drv_file_find(const WCHAR *rel); // index of that name (any case, / or \), or -1 diff --git a/src/wizard.c b/src/wizard.c index 6ded690..b938d44 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -32,7 +32,10 @@ enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, static int g_page, g_job, g_dpi, g_lastpct = -1, g_pending, g_lastrun; // g_pending: install job waiting for DynaRun to close static volatile LONG g_cancel; -static BOOL g_close_asked; // g_close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) +static install_opts g_opts; // what the pages ask for (starts as the command line gave it); only the UI thread changes it +static install_opts g_jobopts; // the copy a job runs with (start_job): the worker never reads g_opts while the pages may change it +static install_result g_result; // what the install job reports besides its code (the pin hint); written by the worker, read once the job is done +static BOOL g_close_asked; // g_opts.close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) static BOOL g_busy, g_closing, g_installed, g_pkg_ours, g_pkg_found, g_status_err, g_badfile, g_nolicense; // g_nolicense: the setup's license text could not be read static int g_nfound; // packages pkg_find_local has handed us so far static HANDLE g_thread; @@ -339,12 +342,12 @@ static void cat_to(WCHAR *d, int cap, const WCHAR *s) if (n < cap - 1) lstrcpynW(d + n, s, cap - n); } -// What is said about the taskbar pin (g_pinresult, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with +// What is said about the taskbar pin (g_result.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with // the install (g_fix_done: the page "Done" is skipped), on the restart page. static void pin_hint(WCHAR *b) { - if (g_pinresult == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); - if (g_pinresult == PIN_FAILED) lstrcatW(b, os_build() >= 22000 + if (g_result.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (g_result.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", @@ -511,7 +514,7 @@ static void set_page(int p) L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); pin_hint(b); - if (g_autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); + if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); text(ID_BODY, b); buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); @@ -737,15 +740,15 @@ static DWORD WINAPI job_proc(LPVOID unused) } break; case JOB_EXTRACT: rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); SecureZeroMemory(g_pw, sizeof(g_pw)); break; // a size cap, and a free target (shared with the driver) // the password is not needed any more - case JOB_INSTALL_ALL: rc = g_pre ? install_all_driver(g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(g_msi); break; // g_pre: the driver's preparation in the same elevated stage - case JOB_INSTALL_FIX: rc = g_pre ? install_fix_driver(g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(); break; + case JOB_INSTALL_ALL: rc = g_pre ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); break; // g_pre: the driver's preparation in the same elevated stage + case JOB_INSTALL_FIX: rc = g_pre ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); break; case JOB_DRV_PREP: // "install anyway" accepts exactly this zip (its SHA-256), nothing else. (No comment after the call on the line of a break: it once swallowed the // break, and the install followed the prep at once, without the restart.) - rc = driver_prep(g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); + rc = driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); break; - case JOB_DRV_INSTALL: rc = driver_install(); break; - case JOB_DRV_CANCEL: rc = driver_cancel(); break; + case JOB_DRV_INSTALL: rc = driver_install(&g_jobopts); break; + case JOB_DRV_CANCEL: rc = driver_cancel(&g_jobopts); break; } PostMessageW(g_hwnd, WM_JOBDONE, g_job, rc); (void)unused; @@ -769,6 +772,7 @@ static void start_job(int job, int page) g_job = job; g_cancel = 0; g_busy = TRUE; if (job == JOB_ACQUIRE && g_dlacq != g_acq) { g_dlacq = g_acq; g_dltotal = g_dlhave = 0; } // a resume of the same download keeps what is known set_page(page); + g_jobopts = g_opts; // the job runs with the options as they are now g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); if (job == JOB_INSTALL_ALL || job == JOB_INSTALL_FIX || job >= JOB_DRV_PREP) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); } @@ -806,7 +810,7 @@ static void go_install(int job) static void skip_driver(void) { user_log(L"driver: skipped, the fix is installed without it"); - g_drv = FALSE; g_autostart_later = FALSE; g_pre = FALSE; + g_drv = FALSE; g_opts.autostart_later = FALSE; g_pre = FALSE; install(g_inst_job); } @@ -1019,7 +1023,7 @@ static void drv_start(void) } g_drv_have = driver_installed(); if (g_drvmode == DRV_MODE_CONTINUE) { - if (g_drv_have) { if (is_admin()) driver_cancel(); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes + if (g_drv_have) { if (is_admin()) driver_cancel(&g_opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes else set_page(driver_ci_state() == DRV_CI_ENFORCED ? P_DRV_RETRY : P_DRV_WARN); } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); } @@ -1187,7 +1191,7 @@ static void choose_driver_source(void) static void job_done(int job, int rc) { WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; - if (g_close_asked) { g_close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) + if (g_close_asked) { g_opts.close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) if (g_closing) { DestroyWindow(g_hwnd); return; } switch (job) { case JOB_ACQUIRE: acq_done(rc); break; @@ -1258,11 +1262,11 @@ static void on_primary(void) { switch (g_page) { case P_HELLO: - g_pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; - if (g_driver_only) { g_drv = TRUE; g_autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver + g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; + if (g_driver_only) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it + g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } break; case P_FAIL: acq_start(g_acq); break; // Retry / Continue download @@ -1277,7 +1281,7 @@ static void on_primary(void) case P_CHECK: set_page(P_LICENSE); break; case P_LICENSE: go_install(JOB_INSTALL_ALL); break; case P_RUNNING: - if (!g_close) { g_close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only + if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only start_job(g_pending, P_INSTALL); break; case P_DONE: @@ -1312,7 +1316,7 @@ static void on_close(void) if (g_page == P_INSTALL || g_page == P_DRV_PREP || g_page == P_DRV_INSTALL) return; // msiexec / registry / system tool work must not be interrupted if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } // after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) - if (g_drvmode == DRV_MODE_CONTINUE && (g_page == P_DRV_RETRY || g_page == P_DRV_WARN) && is_admin()) driver_cancel(); + if (g_drvmode == DRV_MODE_CONTINUE && (g_page == P_DRV_RETRY || g_page == P_DRV_WARN) && is_admin()) driver_cancel(&g_opts); if (g_page == P_DRV_BDE) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); return; } // the task, the menu flag and the suspension were armed: take them back first DestroyWindow(g_hwnd); } @@ -1336,10 +1340,10 @@ static void on_third(void) { if (g_page == P_FAIL) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } else if (g_page == P_HELLO && g_hint) { // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) - g_pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; g_drv = !g_driver_only && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_autostart_later = g_drv; + g_opts.autostart_later = g_drv; user_log(L"DynaRun update hint: the user chose to download the new version"); g_acq_fresh = TRUE; // an older package found on this computer would not be the new version acq_start(&ACQ_SETUP); @@ -1379,8 +1383,8 @@ static void create_controls(void) SendMessageW(H(ID_DRV), BM_SETCHECK, BST_CHECKED, 0); SetWindowTextW(H(ID_PIN), T(L"Pin DynaRun to the &taskbar", L"釘選到工作列(&T)")); SetWindowTextW(H(ID_AUTO), T(L"Start DynaRun when Windows &starts", L"開機時自動啟動 DynaRun(&S)")); - SendMessageW(H(ID_PIN), BM_SETCHECK, g_pin ? BST_CHECKED : BST_UNCHECKED, 0); - SendMessageW(H(ID_AUTO), BM_SETCHECK, g_autostart ? BST_CHECKED : BST_UNCHECKED, 0); + SendMessageW(H(ID_PIN), BM_SETCHECK, g_opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); + SendMessageW(H(ID_AUTO), BM_SETCHECK, g_opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); SetWindowTextW(H(ID_CHECK), T(L"I &accept the license agreement", L"我接受授權合約(&A)")); add(PROGRESS_CLASSW, 0, ID_PROG); add(L"STATIC", SS_NOPREFIX, ID_STATUS); @@ -1657,10 +1661,11 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) return DefWindowProcW(h, m, w, l); } -int wizard(void) +int wizard(const install_opts *init) { INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; static WCHAR title[100]; + g_opts = *init; // the command line's options; the pages change this copy icc.dwSize = sizeof(icc); icc.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; InitCommonControlsEx(&icc); dc = GetDC(NULL); g_dpi = GetDeviceCaps(dc, LOGPIXELSY); ReleaseDC(NULL, dc); diff --git a/tests/test_driver.c b/tests/test_driver.c index 85088ef..037fbc9 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -14,7 +14,7 @@ #include // what setup.c calls elsewhere and the tests do not need -int wizard(void) { return 0; } +int wizard(const install_opts *init) { (void)init; return 0; } int update_check(void) { return 0; } int update_offer(void) { return 0; } int update_run(const WCHAR *a) { (void)a; return 0; } @@ -776,14 +776,14 @@ static void test_job_table(void) if (!(next && next < end)) break; } CHECK(labels >= 7, L"all seven jobs were looked at"); - // g_close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it + // g_opts.close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it // records that (g_close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since a = strstr(t, "static void job_done"); end = a ? strstr(a, "switch (job)") : NULL; CHECK(a && end, L"job_done is in src\\wizard.c"); - for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_close = FALSE", 15)) break; + for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_opts.close = FALSE", 20)) break; CHECK(a && end && c < end, L"job_done takes back the \"close DynaRun\" consent of the job that has ended"); - for (c = t, labels = 0; (c = strstr(c, "g_close = TRUE")) != NULL; c += 14) labels++; - CHECK(labels == 1 && strstr(t, "g_close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_close only together with g_close_asked"); + for (c = t, labels = 0; (c = strstr(c, "g_opts.close = TRUE")) != NULL; c += 19) labels++; + CHECK(labels == 1 && strstr(t, "g_opts.close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_opts.close only together with g_close_asked"); release(f); } @@ -896,6 +896,7 @@ static void test_real_zip(void) // value and drops one that is not 64 characters long: there a bad value is consumed, here it simply is not at argv[4]. // The verb itself: /driver and /driver-continue open the wizard (not run here); /driver-prep, /driver-install and /driver-cancel need an // administrator (return 1 otherwise) and /driver-prep needs argc >= 4 (return 1 otherwise); anything else is DRV_NOVERB. +static int driver_verb_o(int argc, WCHAR **argv) { install_opts o; install_opts_default(&o); return driver_verb(argc, argv, &o); } static void test_cmdline(void) { WCHAR h64[80], h63[80], h65[80]; @@ -934,24 +935,24 @@ static void test_cmdline(void) CHECK(prep_accept_arg(4, a9) == NULL, L"accept: nothing after the zip: NULL"); // the verb dispatch; only what is safe to run in any account - CHECK(driver_verb(2, n1) == DRV_NOVERB, L"verb: unknown verb is DRV_NOVERB"); - CHECK(driver_verb(4, n2) == DRV_NOVERB, L"verb: /machine is not a driver verb"); - CHECK(driver_verb(2, n3) == DRV_NOVERB, L"verb: exact match only (/driver-prepx)"); - CHECK(driver_verb(2, n4) == DRV_NOVERB, L"verb: no slash, no verb"); - CHECK(driver_verb(2, n5) == DRV_NOVERB, L"verb: /uninstall is setup.c's"); - CHECK(driver_verb(2, p1) == 1, L"verb: /driver-prep without arguments returns 1 (not admin: 1 as well)"); - CHECK(driver_verb(3, p2) == 1, L"verb: /DRIVER-PREP without a zip returns 1 (case-insensitive verb)"); + CHECK(driver_verb_o(2, n1) == DRV_NOVERB, L"verb: unknown verb is DRV_NOVERB"); + CHECK(driver_verb_o(4, n2) == DRV_NOVERB, L"verb: /machine is not a driver verb"); + CHECK(driver_verb_o(2, n3) == DRV_NOVERB, L"verb: exact match only (/driver-prepx)"); + CHECK(driver_verb_o(2, n4) == DRV_NOVERB, L"verb: no slash, no verb"); + CHECK(driver_verb_o(2, n5) == DRV_NOVERB, L"verb: /uninstall is setup.c's"); + CHECK(driver_verb_o(2, p1) == 1, L"verb: /driver-prep without arguments returns 1 (not admin: 1 as well)"); + CHECK(driver_verb_o(3, p2) == 1, L"verb: /DRIVER-PREP without a zip returns 1 (case-insensitive verb)"); if (!is_admin()) { // elevated verbs refuse a normal process before doing anything - CHECK(driver_verb(2, i1) == 1, L"verb: /driver-install not elevated returns 1"); - CHECK(driver_verb(2, c1) == 1, L"verb: /driver-cancel not elevated returns 1"); - CHECK(driver_verb(6, a1) == 1, L"verb: /driver-prep with arguments, not elevated, returns 1"); + CHECK(driver_verb_o(2, i1) == 1, L"verb: /driver-install not elevated returns 1"); + CHECK(driver_verb_o(2, c1) == 1, L"verb: /driver-cancel not elevated returns 1"); + CHECK(driver_verb_o(6, a1) == 1, L"verb: /driver-prep with arguments, not elevated, returns 1"); } else wprintf(L" (info: running elevated - /driver-install, /driver-cancel and /driver-prep with arguments are not called)\n"); } int wmain(void) { - g_quiet = TRUE; g_zh = FALSE; + g_zh = FALSE; setvbuf(stdout, NULL, _IONBF, 0); GetTempPathW(MAX_PATH, g_base); { WCHAR lp[MAX_PATH]; DWORD n = GetLongPathNameW(g_base, lp, MAX_PATH); if (n && n < MAX_PATH) lstrcpyW(g_base, lp); } diff --git a/tests/test_hardening.c b/tests/test_hardening.c index ad8a5eb..ebcb725 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -24,12 +24,12 @@ static BOOL WINAPI test_ShellExecuteExW(SHELLEXECUTEINFOW *se) #include // what setup.c calls elsewhere and the tests do not need -int wizard(void) { return 0; } +int wizard(const install_opts *init) { (void)init; return 0; } int update_check(void) { return 0; } int update_offer(void) { return 0; } int update_run(const WCHAR *a) { (void)a; return 0; } // driver.c is not part of this test (it would touch ProgramData and the Start menu): tests\test_driver.c covers it -int driver_verb(int argc, WCHAR **argv) { (void)argc; (void)argv; return DRV_NOVERB; } +int driver_verb(int argc, WCHAR **argv, const install_opts *o) { (void)argc; (void)argv; (void)o; return DRV_NOVERB; } int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { (void)sid; (void)zip; (void)accept; return 0; } // called by install_fix_driver / install_all_driver (not run here) void driver_machine_stage(const WCHAR *p) { (void)p; } void driver_machine_uninstall(void) { } @@ -596,8 +596,7 @@ static void test_running(void) lstrcpyW(g_dir, g_base); lstrcatW(g_dir, L"\\no-such-folder"); l[0].pid = 4242; l[0].bit = RUN_DYNARUN; l[0].sess = 7; l[0].why = JUDGE_OTHERSESSION; lstrcpyW(l[0].path, exe); l[1].pid = 4243; l[1].bit = RUN_LAUNCHER; l[1].sess = 7; l[1].why = JUDGE_NOPATH; l[1].path[0] = 0; - g_close = FALSE; - log_running(L"testing", l, 2, RUN_DYNARUN | RUN_LAUNCHER | RUN_NOCLOSE); + log_running(L"testing", l, 2, RUN_DYNARUN | RUN_LAUNCHER | RUN_NOCLOSE, FALSE); wsprintfW(logp, L"%s\\DynaRunFix-setup.log", g_base); log = read_file(logp, &n); CHECK(log != NULL, L"setup log written to %TEMP%"); @@ -628,13 +627,11 @@ static void test_running(void) r = scan_programs(l, 16, &cnt); CHECK((r & RUN_DYNARUN) && (r & RUN_NOCLOSE) && !(r & RUN_CANCLOSE), L"scan: DynaRun with its folder not known: in the way, not closable"); lstrcpyW(g_exe, exe); - g_close = TRUE; close_programs(); CHECK(WaitForSingleObject(p, 15000) == WAIT_OBJECT_0, L"close_programs ends the known DynaRun"); CHECK(scan_programs(l, 16, &cnt) == 0, L"scan: nothing left"); CloseHandle(p); } else CHECK(FALSE, L"could not start the DynaRun stand-in"); - g_close = FALSE; lstrcpyW(g_exe, sv_exe); lstrcpyW(g_dir, sv_dir); } @@ -792,48 +789,47 @@ static void test_shortcut_paths(void) /* ---------- characterization tests: they lock what the code does TODAY (refactor safety net), odd or not ---------- */ -// run_elevated builds "[ /quiet][ /close][ /pin][ /keep]" in this order, whatever the caller put in args. Only these four globals are -// forwarded: g_autostart and g_autostart_later (and the /notaskbar, /noautostart switches) never reach the elevated stage. The 1024 character +// run_elevated builds "[ /quiet][ /close][ /pin][ /keep]" in this order, whatever the caller put in args. Only these four options are +// forwarded: autostart and autostart_later (and the /notaskbar, /noautostart switches) never reach the elevated stage. The 1024 character // buffer is not checked (oddity: a longer args overflows it), so no test goes near that. static void test_run_elevated_args(void) { - BOOL sq = g_quiet, sc = g_close, sp = g_pin, sk = g_keep, sa = g_autostart, sl = g_autostart_later; + install_opts o; int m, au, rc; WCHAR want[400]; const WCHAR *base = L"/machine S-1-5-21-1-2-3-1001 \"C:\\Program Files\\DynaRun\\DynaRun V3.exe\""; const WCHAR *hash = L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; for (m = 0; m < 16; m++) { for (au = 0; au < 4; au++) { - g_quiet = (m & 1) != 0; g_close = (m & 2) != 0; g_pin = (m & 4) != 0; g_keep = (m & 8) != 0; - g_autostart = (au & 1) != 0; g_autostart_later = (au & 2) != 0; // must not change the result + o.quiet = (m & 1) != 0; o.close = (m & 2) != 0; o.pin = (m & 4) != 0; o.keep = (m & 8) != 0; + o.autostart = (au & 1) != 0; o.autostart_later = (au & 2) != 0; // must not change the result lstrcpyW(want, base); if (m & 1) lstrcatW(want, L" /quiet"); if (m & 2) lstrcatW(want, L" /close"); if (m & 4) lstrcatW(want, L" /pin"); if (m & 8) lstrcatW(want, L" /keep"); g_se_calls = 0; g_se_err = ERROR_CANCELLED; - rc = run_elevated(base, L"C:\\x\\setup.exe"); + rc = run_elevated(base, L"C:\\x\\setup.exe", &o); CHECK(g_se_calls == 1 && rc == 2, L"run_elevated: one call; a cancelled UAC prompt returns 2"); CHECK(!lstrcmpW(g_se_params, want), L"run_elevated: args then /quiet /close /pin /keep, in that order"); CHECK(!lstrcmpW(g_se_verb, L"runas") && !lstrcmpW(g_se_file, L"C:\\x\\setup.exe"), L"run_elevated: verb runas, the file given"); } } // the driver arguments stay where the caller put them: before the forwarded switches - g_quiet = TRUE; g_close = TRUE; g_pin = TRUE; g_keep = TRUE; g_autostart = FALSE; g_autostart_later = TRUE; + o.quiet = TRUE; o.close = TRUE; o.pin = TRUE; o.keep = TRUE; o.autostart = FALSE; o.autostart_later = TRUE; wsprintfW(want, L"%s /with-driver \"C:\\z\\Dyna Pro USB.zip\" /unverified %s", base, hash); - run_elevated(want, L"C:\\x\\setup.exe"); + run_elevated(want, L"C:\\x\\setup.exe", &o); wsprintfW(want, L"%s /with-driver \"C:\\z\\Dyna Pro USB.zip\" /unverified %s /quiet /close /pin /keep", base, hash); CHECK(!lstrcmpW(g_se_params, want), L"run_elevated: /with-driver ... /unverified ... first, then the forwarded switches"); // no deduplication: a switch already in args is repeated - run_elevated(L"/driver-install /quiet /pin", L"C:\\x\\setup.exe"); + run_elevated(L"/driver-install /quiet /pin", L"C:\\x\\setup.exe", &o); CHECK(!lstrcmpW(g_se_params, L"/driver-install /quiet /pin /quiet /close /pin /keep"), L"run_elevated: no deduplication of switches already in args"); // empty args: the switches follow a leading blank - g_quiet = TRUE; g_close = FALSE; g_pin = FALSE; g_keep = FALSE; - run_elevated(L"", L"C:\\x\\setup.exe"); + o.quiet = TRUE; o.close = FALSE; o.pin = FALSE; o.keep = FALSE; + run_elevated(L"", L"C:\\x\\setup.exe", &o); CHECK(!lstrcmpW(g_se_params, L" /quiet"), L"run_elevated: empty args give a leading blank"); // other failures of ShellExecuteEx return 1 g_se_err = ERROR_ACCESS_DENIED; g_se_calls = 0; - CHECK(run_elevated(L"/driver-cancel", L"C:\\x\\setup.exe") == 1 && g_se_calls == 1, L"run_elevated: another ShellExecuteEx error returns 1"); + CHECK(run_elevated(L"/driver-cancel", L"C:\\x\\setup.exe", &o) == 1 && g_se_calls == 1, L"run_elevated: another ShellExecuteEx error returns 1"); g_se_err = ERROR_CANCELLED; - g_quiet = sq; g_close = sc; g_pin = sp; g_keep = sk; g_autostart = sa; g_autostart_later = sl; } // parse_switches (the command line loop of WinMainCRTStartup, moved into a helper unchanged). Rules today: @@ -843,35 +839,34 @@ static void test_run_elevated_args(void) // - /unverified keeps its value only when exactly 64 characters long, otherwise the value is consumed and dropped (no error); // - unknown arguments are ignored; the last /with-driver wins; a later /unverified with a bad length does not clear an earlier good one. // (driver.c's driver_verb parses /driver-prep differently: see test_driver.c.) The dispatch on argv[1] (/machine needs argc >= 4, ...) is not covered here. -static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da; +static install_opts g_po; static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da; static void parse_case(int argc, WCHAR **argv) { - g_quiet = FALSE; g_close = FALSE; g_pin = TRUE; g_autostart = TRUE; g_keep = FALSE; + install_opts_default(&g_po); g_pin_seen = FALSE; g_dz = NULL; g_da = NULL; - parse_switches(argc, argv, &g_pin_seen, &g_dz, &g_da); + parse_switches(argc, argv, &g_po, &g_pin_seen, &g_dz, &g_da); } #define PARSE(...) do { WCHAR *av_[] = { __VA_ARGS__ }; parse_case((int)(sizeof(av_) / sizeof(av_[0])), av_); } while (0) static void test_parse_switches(void) { - BOOL sq = g_quiet, sc = g_close, sp = g_pin, sk = g_keep, sa = g_autostart; WCHAR h64[80], h63[80]; lstrcpyW(h64, L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); lstrcpyW(h63, h64); h63[63] = 0; CHECK(lstrlenW(h64) == 64 && lstrlenW(h63) == 63, L"parse: test hashes have 64 and 63 characters"); PARSE(L"setup.exe"); - CHECK(!g_quiet && !g_close && g_pin && g_autostart && !g_keep && !g_pin_seen && !g_dz && !g_da, L"parse: no arguments change nothing"); + CHECK(!g_po.quiet && !g_po.close && g_po.pin && g_po.autostart && !g_po.keep && !g_pin_seen && !g_dz && !g_da, L"parse: no arguments change nothing"); PARSE(L"setup.exe", L"/quiet", L"/close", L"/keep", L"/pin"); - CHECK(g_quiet && g_close && g_keep && g_pin_seen && g_pin && g_autostart, L"parse: /quiet /close /keep /pin"); + CHECK(g_po.quiet && g_po.close && g_po.keep && g_pin_seen && g_po.pin && g_po.autostart, L"parse: /quiet /close /keep /pin"); PARSE(L"setup.exe", L"/notaskbar", L"/noautostart"); - CHECK(!g_pin && !g_autostart && !g_pin_seen && !g_quiet, L"parse: /notaskbar clears g_pin, /noautostart clears g_autostart (the local pin stays FALSE)"); + CHECK(!g_po.pin && !g_po.autostart && !g_pin_seen && !g_po.quiet, L"parse: /notaskbar clears pin, /noautostart clears autostart (the local pin stays FALSE)"); PARSE(L"setup.exe", L"/QUIET", L"/Close", L"/KEEP", L"/PIN", L"/NoTaskbar"); - CHECK(g_quiet && g_close && g_keep && g_pin_seen && !g_pin, L"parse: case-insensitive"); + CHECK(g_po.quiet && g_po.close && g_po.keep && g_pin_seen && !g_po.pin, L"parse: case-insensitive"); PARSE(L"/quiet"); - CHECK(!g_quiet, L"parse: argv[0] is never looked at"); + CHECK(!g_po.quiet, L"parse: argv[0] is never looked at"); PARSE(L"setup.exe", L"/frobnicate", L"quiet", L"-quiet", L"/quiet:1", L"/quiet "); - CHECK(!g_quiet && !g_close && !g_keep && !g_pin_seen, L"parse: unknown, dash and decorated switches are ignored (exact match only)"); + CHECK(!g_po.quiet && !g_po.close && !g_po.keep && !g_pin_seen, L"parse: unknown, dash and decorated switches are ignored (exact match only)"); PARSE(L"setup.exe", L"/machine", L"/quiet", L"/close"); - CHECK(g_quiet && g_close, L"parse: switches are found at the SID and path positions too"); + CHECK(g_po.quiet && g_po.close, L"parse: switches are found at the SID and path positions too"); PARSE(L"s", L"/machine", L"S-1-5-21-1", L"C:\\e.exe", L"/with-driver", L"C:\\z.zip"); CHECK(g_dz && !lstrcmpW(g_dz, L"C:\\z.zip") && !g_da, L"parse: /with-driver at index 4 takes the zip"); @@ -882,11 +877,11 @@ static void test_parse_switches(void) PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h64); CHECK(g_da && lstrlenW(g_da) == 64 && !lstrcmpW(g_da, h64), L"parse: /unverified at index 4 with a 64 character value"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h63, L"/quiet"); - CHECK(!g_da && g_quiet, L"parse: /unverified with a 63 character value: value consumed and dropped, the next switch still counts"); + CHECK(!g_da && g_po.quiet, L"parse: /unverified with a 63 character value: value consumed and dropped, the next switch still counts"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", L"/quiet"); - CHECK(!g_da && !g_quiet, L"parse: ODDITY /unverified swallows a following switch as its value"); + CHECK(!g_da && !g_po.quiet, L"parse: ODDITY /unverified swallows a following switch as its value"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/with-driver", L"/quiet"); - CHECK(g_dz && !lstrcmpW(g_dz, L"/quiet") && !g_quiet, L"parse: ODDITY /with-driver swallows a following switch as the zip"); + CHECK(g_dz && !lstrcmpW(g_dz, L"/quiet") && !g_po.quiet, L"parse: ODDITY /with-driver swallows a following switch as the zip"); PARSE(L"s", L"/machine", L"S-1", L"/unverified", h64); CHECK(!g_da, L"parse: /unverified at index 3 is ignored"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/unverified", h64, L"/with-driver", L"z.zip", L"/pin"); @@ -897,7 +892,6 @@ static void test_parse_switches(void) CHECK(g_da && !lstrcmpW(g_da, h64), L"parse: a later short /unverified does not clear an earlier accepted one"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/WITH-DRIVER", L"z", L"/UNVERIFIED", h64); CHECK(g_da && g_dz, L"parse: the driver switches are case-insensitive too"); - g_quiet = sq; g_close = sc; g_pin = sp; g_keep = sk; g_autostart = sa; } // FIX_DONE(rc): the elevated install stage "installed the fix" = rc 0, or the driver step gave a code >= DRV_E_HASH (the fix is in place, the driver @@ -929,7 +923,7 @@ int wmain(void) { HKEY k; if (wcsstr(GetCommandLineW(), L"--idle")) { Sleep(120000); return 0; } // the DynaRun stand-in of test_running - g_quiet = TRUE; g_zh = FALSE; + g_zh = FALSE; setvbuf(stdout, NULL, _IONBF, 0); setup_tree(); wprintf(L"[user_writable]\n"); test_user_writable(); diff --git a/tests/test_update.c b/tests/test_update.c index a73f941..dc5eb1f 100644 --- a/tests/test_update.c +++ b/tests/test_update.c @@ -5,7 +5,7 @@ #include "../src/update.c" #include -BOOL g_zh, g_quiet; HWND g_hwnd; WCHAR g_launcher[MAX_PATH]; +BOOL g_zh; HWND g_hwnd; WCHAR g_launcher[MAX_PATH]; void zero(void *p, SIZE_T n) { memset(p, 0, n); } void *alloc(SIZE_T n) { return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, n); } void release(void *p) { if (p) HeapFree(GetProcessHeap(), 0, p); } From 50bd20d057bb28560c0163f2952ff930670e84d7 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:13:05 +0800 Subject: [PATCH 14/41] Command line: one verb table for the setup and driver stages setup.c's if/else chain on argv[1] and driver.c's driver_verb become one table {name, min_args, handler} (VERBS); driver.c supplies the handlers of its five verbs. The command line is parsed once (cmd_parse) into a cmdline struct. Verbs, argument positions, the argc needed (/machine, /full: 4) and exit codes are unchanged. /unverified keeps its two rules, now computed by the same loop: daccept (any position from argv[4], the last valid one: /machine, /full) and daccept4 (only argv[4] argv[5]: /driver-prep). The setup builds /unverified at argv[4] (/driver-prep) or argv[6] (/machine, /full) where both agree; they differ only for hand-typed lines (a valid /unverified after another switch, or two of them), which the tests lock. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 30 +++++---- src/errors.h | 2 +- src/setup.c | 145 +++++++++++++++++++++++++++++------------ src/setup.h | 18 ++++- tests/test_driver.c | 19 +++++- tests/test_hardening.c | 44 +++++++++++-- 6 files changed, 195 insertions(+), 63 deletions(-) diff --git a/src/driver.c b/src/driver.c index 7421a0e..96b8db5 100644 --- a/src/driver.c +++ b/src/driver.c @@ -1320,20 +1320,26 @@ void driver_machine_uninstall(void) /* ---------- command line ---------- */ -// /driver-prep [/unverified ]: the accepted hash only as argv[4] /unverified argv[5] with a 64 character value, else NULL (tests/test_driver.c locks this) -static const WCHAR *prep_accept_arg(int argc, WCHAR **argv) +// The driver's verbs of the table in setup.c. /driver-prep [/unverified ] takes the accepted hash only as argv[4] /unverified argv[5] +// with a 64 character value (c->daccept4, set by cmd_parse), else NULL (tests/test_driver.c locks this). +int cmd_driver(cmdline *c) { g_drvmode = DRV_MODE_STANDALONE; return wizard(&c->opts); } +int cmd_driver_continue(cmdline *c) { g_drvmode = DRV_MODE_CONTINUE; return wizard(&c->opts); } + +int cmd_driver_prep(cmdline *c) { - return argc >= 6 && !lstrcmpiW(argv[4], L"/unverified") && lstrlenW(argv[5]) == 64 ? argv[5] : NULL; + if (!is_admin()) return RC_FAIL; // the elevated parts: only with the rights they need + return c->argc >= 4 ? prep_locked(c->argv[2], c->argv[3], c->daccept4) : RC_FAIL; } -int driver_verb(int argc, WCHAR **argv, const install_opts *o) +int cmd_driver_install(cmdline *c) { - const WCHAR *v = argv[1]; - if (!lstrcmpiW(v, L"/driver")) { g_drvmode = DRV_MODE_STANDALONE; return wizard(o); } - if (!lstrcmpiW(v, L"/driver-continue")) { g_drvmode = DRV_MODE_CONTINUE; return wizard(o); } - if (lstrcmpiW(v, L"/driver-prep") && lstrcmpiW(v, L"/driver-install") && lstrcmpiW(v, L"/driver-cancel")) return DRV_NOVERB; - if (!is_admin()) return RC_FAIL; // the elevated parts: only with the rights they need - if (!lstrcmpiW(v, L"/driver-prep")) return argc >= 4 ? prep_locked(argv[2], argv[3], prep_accept_arg(argc, argv)) : RC_FAIL; - if (!lstrcmpiW(v, L"/driver-install")) return install_locked(); - return driver_cancel(o); + (void)c; + if (!is_admin()) return RC_FAIL; + return install_locked(); +} + +int cmd_driver_cancel(cmdline *c) +{ + if (!is_admin()) return RC_FAIL; + return driver_cancel(&c->opts); } diff --git a/src/errors.h b/src/errors.h index be43b54..7440da1 100644 --- a/src/errors.h +++ b/src/errors.h @@ -27,7 +27,7 @@ enum { PKC_OK = 0, PKC_DIFFER = 1, PKC_MISSING = 2 }; /* ---- 4. The driver step (driver.c): result of driver_prep / driver_install / driver_cancel, also an exit code of the elevated stage ---- DRV_SUCCESS is RC_OK. DRV_E_* 20..29 are failures; DRV_W_BDE 30 is "done, but BitLocker could not be suspended" (the wizard asks); DRV_W_HASH 31 is a driver_fetch / driver_check result (some files differ, the user is asked), never an exit code. RC_CANCEL (2) is also what the driver functions return - when the UAC prompt was declined. DRV_NOVERB is driver_verb's "not a driver command". */ + when the UAC prompt was declined. DRV_NOVERB is verb_run's (setup.c) "argv[1] is no verb of the table". */ enum { DRV_SUCCESS = 0, DRV_E_FIRST = 20, // the first driver code; see FIX_DONE diff --git a/src/setup.c b/src/setup.c index adaebcf..39b7378 100644 --- a/src/setup.c +++ b/src/setup.c @@ -2034,58 +2034,123 @@ static const WCHAR *after_args(int n) return c; } -// The switches of the command line, at any position (tests/test_hardening.c locks the rules). *o (the defaults, install_opts_default) gets quiet, close, -// keep set and pin, autostart cleared by what the command line says; -// *pin: /pin seen; *dzip, *daccept: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). -static void parse_switches(int argc, WCHAR **argv, install_opts *o, BOOL *pin, const WCHAR **dzip, const WCHAR **daccept) +// The /unverified rule of both stages: argv[i] is /unverified and a value of exactly 64 characters follows it, else NULL. The elevated install stage +// (/machine, /full) looks for it at every position from argv[4] on (cmd_parse), /driver-prep only at argv[4]: the two differ for a hand-typed +// "/driver-prep /quiet /unverified " (see cmd_parse). +static const WCHAR *unverified_value(int argc, WCHAR **argv, int i) +{ + return i + 1 < argc && !lstrcmpiW(argv[i], L"/unverified") && lstrlenW(argv[i + 1]) == 64 ? argv[i + 1] : NULL; +} + +// The switches of the command line, at any position (tests/test_hardening.c and test_driver.c lock the rules). c->opts is set to the defaults +// (install_opts_default), then quiet, close, keep are set and pin, autostart cleared by what the command line says; +// c->pin: /pin seen; c->dzip, c->daccept, c->daccept4: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). +// /with-driver and /unverified take the next argument whatever it is; an /unverified value that is not 64 characters long is dropped. daccept is the last +// valid /unverified at index >= 4 (the install stage's rule), daccept4 the one at index 4 (the driver prep's rule); they differ when +// - a valid /unverified comes later (argv[5] on, e.g. after /quiet): daccept has it, daccept4 is NULL; +// - two valid ones are given: daccept is the later one, daccept4 the one at argv[4]. +// Each verb keeps using the rule it always had (/machine, /full: daccept; /driver-prep: daccept4), so no command line behaves differently from before. +static void cmd_parse(cmdline *c) +{ + int i; const WCHAR *v; + install_opts_default(&c->opts); + c->pin = FALSE; c->dzip = c->daccept = c->daccept4 = NULL; + for (i = 1; i < c->argc; i++) { + WCHAR *a = c->argv[i]; + if (!lstrcmpiW(a, L"/quiet")) c->opts.quiet = TRUE; + else if (!lstrcmpiW(a, L"/close")) c->opts.close = TRUE; + else if (!lstrcmpiW(a, L"/notaskbar")) c->opts.pin = FALSE; + else if (!lstrcmpiW(a, L"/noautostart")) c->opts.autostart = FALSE; + else if (!lstrcmpiW(a, L"/pin")) c->pin = TRUE; + else if (!lstrcmpiW(a, L"/keep")) c->opts.keep = TRUE; + else if (i >= 4 && !lstrcmpiW(a, L"/with-driver") && i + 1 < c->argc) c->dzip = c->argv[++i]; // after the SID and the path: see install_fix_driver + else if (i >= 4 && !lstrcmpiW(a, L"/unverified") && i + 1 < c->argc) { + if ((v = unverified_value(c->argc, c->argv, i)) != NULL) { c->daccept = v; if (i == 4) c->daccept4 = v; } + i++; + } + } +} + +/* ---------- the verbs: argv[1] ---------- */ + +static int cmd_machine(cmdline *c) // /machine [/with-driver [/unverified ]]: the elevated install stage +{ + int rc; + lstrcpynW(g_exe, c->argv[3], MAX_PATH); + CoInitialize(NULL); + rc = machine_install(&c->opts, c->argv[2]); + if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); + return rc; +} + +static int cmd_full(cmdline *c) // /full [/with-driver ...]: the same, with the DynaRun MSI first +{ + int rc; + CoInitialize(NULL); + rc = machine_full(&c->opts, c->argv[2], c->argv[3]); + if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); + return rc; +} + +static int cmd_unmachine(cmdline *c) // /unmachine []: the elevated uninstall stage +{ + reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); + CoInitialize(NULL); + return machine_uninstall(&c->opts, c->argc >= 3 && c->argv[2][0] == 'S' && c->argv[2][1] == '-' ? c->argv[2] : NULL); +} + +static int cmd_uninstall(cmdline *c) { return uninstall(&c->opts); } +static int cmd_checkupdate(cmdline *c) { (void)c; return update_check(); } +static int cmd_update(cmdline *c) { (void)c; return update_run(after_args(2)); } +static int cmd_offer(cmdline *c) { (void)c; return update_offer(); } + +// min_args: the verb is only taken with that many arguments (the program counts); with fewer, the command line is no verb and the wizard (or /quiet install) runs. +// The elevated driver verbs take 2 and check their own arguments (a /driver-prep without them is an error exit, not the wizard). +static const verb VERBS[] = { + { L"/machine", 4, cmd_machine }, + { L"/full", 4, cmd_full }, + { L"/unmachine", 2, cmd_unmachine }, + { L"/driver", 2, cmd_driver }, // driver.c + { L"/driver-continue", 2, cmd_driver_continue }, + { L"/driver-prep", 2, cmd_driver_prep }, + { L"/driver-install", 2, cmd_driver_install }, + { L"/driver-cancel", 2, cmd_driver_cancel }, + { L"/uninstall", 2, cmd_uninstall }, + { L"/checkupdate", 2, cmd_checkupdate }, + { L"/update", 2, cmd_update }, + { L"/offer", 2, cmd_offer }, +}; + +static const verb *verb_find(int argc, WCHAR **argv) { int i; - for (i = 1; i < argc; i++) { - if (!lstrcmpiW(argv[i], L"/quiet")) o->quiet = TRUE; - else if (!lstrcmpiW(argv[i], L"/close")) o->close = TRUE; - else if (!lstrcmpiW(argv[i], L"/notaskbar")) o->pin = FALSE; - else if (!lstrcmpiW(argv[i], L"/noautostart")) o->autostart = FALSE; - else if (!lstrcmpiW(argv[i], L"/pin")) *pin = TRUE; - else if (!lstrcmpiW(argv[i], L"/keep")) o->keep = TRUE; - else if (i >= 4 && !lstrcmpiW(argv[i], L"/with-driver") && i + 1 < argc) *dzip = argv[++i]; // after the SID and the path: see install_fix_driver - else if (i >= 4 && !lstrcmpiW(argv[i], L"/unverified") && i + 1 < argc) { if (lstrlenW(argv[i + 1]) == 64) *daccept = argv[i + 1]; i++; } - } + if (argc < 2) return NULL; + for (i = 0; i < (int)(sizeof(VERBS) / sizeof(VERBS[0])); i++) + if (argc >= VERBS[i].min_args && !lstrcmpiW(argv[1], VERBS[i].name)) return &VERBS[i]; + return NULL; +} + +// Runs the verb of the command line: its exit code, or DRV_NOVERB when argv[1] is none (the caller then installs or opens the wizard). +static int verb_run(cmdline *c) +{ + const verb *v = verb_find(c->argc, c->argv); + return v ? v->run(c) : DRV_NOVERB; } void WinMainCRTStartup(void) { typedef BOOL (WINAPI *SetDefaultDllDirectories_t)(DWORD); SetDefaultDllDirectories_t sdd = (SetDefaultDllDirectories_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "SetDefaultDllDirectories"); - int argc, rc; BOOL pin = FALSE; WCHAR **argv; const WCHAR *dzip = NULL, *daccept = NULL; install_opts opts; install_result res; + int rc; cmdline c; install_result res; if (sdd) sdd(0x800); // LOAD_LIBRARY_SEARCH_SYSTEM32: no DLL is searched for in the exe's folder or the current folder - argv = CommandLineToArgvW(GetCommandLineW(), &argc); + c.argv = CommandLineToArgvW(GetCommandLineW(), &c.argc); g_zh = PRIMARYLANGID(GetUserDefaultUILanguage()) == LANG_CHINESE; init_paths(); - install_opts_default(&opts); res.pin = PIN_NOT_ASKED; // only the wizard shows it - parse_switches(argc, argv, &opts, &pin, &dzip, &daccept); - if (argc >= 2 && (!lstrcmpiW(argv[1], L"/machine") || !lstrcmpiW(argv[1], L"/full"))) opts.pin = pin; // the elevated stage: as asked - if (argc >= 4 && !lstrcmpiW(argv[1], L"/machine")) { - lstrcpynW(g_exe, argv[3], MAX_PATH); - CoInitialize(NULL); - rc = machine_install(&opts, argv[2]); - if (!rc && dzip) rc = driver_prep_stage(argv[2], dzip, daccept); - } else if (argc >= 4 && !lstrcmpiW(argv[1], L"/full")) { - CoInitialize(NULL); - rc = machine_full(&opts, argv[2], argv[3]); - if (!rc && dzip) rc = driver_prep_stage(argv[2], dzip, daccept); - } else if (argc >= 2 && !lstrcmpiW(argv[1], L"/unmachine")) { - reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); - CoInitialize(NULL); - rc = machine_uninstall(&opts, argc >= 3 && argv[2][0] == 'S' && argv[2][1] == '-' ? argv[2] : NULL); - } else if (argc >= 2 && (rc = driver_verb(argc, argv, &opts)) != DRV_NOVERB) { - // /driver, /driver-continue (the wizard at the driver pages), /driver-prep, /driver-install, /driver-cancel (driver.c) - } else if (argc >= 2 && !lstrcmpiW(argv[1], L"/uninstall")) rc = uninstall(&opts); - else if (argc >= 2 && !lstrcmpiW(argv[1], L"/checkupdate")) rc = update_check(); - else if (argc >= 2 && !lstrcmpiW(argv[1], L"/update")) rc = update_run(after_args(2)); - else if (argc >= 2 && !lstrcmpiW(argv[1], L"/offer")) rc = update_offer(); - else if (opts.quiet) rc = locate_dynarun() ? install_fix(&opts, &res) : RC_FAIL; - else rc = wizard(&opts); + cmd_parse(&c); + if (c.argc >= 2 && (!lstrcmpiW(c.argv[1], L"/machine") || !lstrcmpiW(c.argv[1], L"/full"))) c.opts.pin = c.pin; // the elevated stage: as asked + rc = verb_run(&c); + if (rc == DRV_NOVERB) rc = c.opts.quiet ? (locate_dynarun() ? install_fix(&c.opts, &res) : RC_FAIL) : wizard(&c.opts); // the elevated part (/machine, /full) ends here: let the wizard that started it take the foreground back AllowSetForegroundWindow(ASFW_ANY); ExitProcess(rc); diff --git a/src/setup.h b/src/setup.h index 6b00ddf..78cbf42 100644 --- a/src/setup.h +++ b/src/setup.h @@ -20,6 +20,17 @@ void install_opts_default(install_opts *o); // pin and autostart on, the res typedef struct { int pin; // PIN_*: 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin } install_result; +// The command line, parsed once (setup.c, cmd_parse). The verb table (setup.c) hands it to the verb's function. +typedef struct { + int argc; WCHAR **argv; // as CommandLineToArgvW gave them; argv[0] is the program, argv[1] the verb + install_opts opts; // the switches that apply to every verb: /quiet /close /notaskbar /noautostart /keep + BOOL pin; // /pin seen (the elevated stage sets up the Windows 11 pin only when it is asked to) + const WCHAR *dzip; // /with-driver : only from argv[4] on (after the verb, the SID and the path), any value is taken + const WCHAR *daccept; // /unverified from argv[4] on, at any position (/machine, /full): the last one with 64 characters + const WCHAR *daccept4; // ... only as argv[4] argv[5] (/driver-prep): the same value, or NULL +} cmdline; +typedef int (*verb_fn)(cmdline *c); // returns the exit code of the process +typedef struct { const WCHAR *name; int min_args; verb_fn run; } verb; // argv[1] (any case) is name and argc >= min_args extern HWND g_hwnd; // wizard window, owner of UAC prompts extern WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; #define T(en, zh) (g_zh ? (zh) : (en)) @@ -186,7 +197,12 @@ int driver_install(const install_opts *o); // elevated: pnputi int driver_cancel(const install_opts *o); // elevated: task, restart menu and BitLocker suspension back. 2 = UAC cancelled BOOL driver_reboot(void); // restarts Windows (not elevated) int driver_ci_state(void); // DRV_CI_* -int driver_verb(int argc, WCHAR **argv, const install_opts *o); // command line verbs; DRV_NOVERB when argv[1] is none of ours, else the exit code +// The driver's entries in the verb table (setup.c). The elevated three refuse a process that is not elevated (RC_FAIL). +int cmd_driver(cmdline *c); // /driver: the wizard at the driver pages +int cmd_driver_continue(cmdline *c); // /driver-continue: the same, after the restart +int cmd_driver_prep(cmdline *c); // /driver-prep [/unverified ] +int cmd_driver_install(cmdline *c); // /driver-install +int cmd_driver_cancel(cmdline *c); // /driver-cancel void driver_machine_stage(const WCHAR *installed_setup); // elevated install: the Start menu shortcut "Install Dyna Pro USB driver" int drv_file_count(void); // the files that get installed (pinned: name below "Dyna Pro USB\", size, SHA-256) int drv_file_find(const WCHAR *rel); // index of that name (any case, / or \), or -1 diff --git a/tests/test_driver.c b/tests/test_driver.c index 037fbc9..0d4536f 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -892,11 +892,23 @@ static void test_real_zip(void) // driver_verb: /driver-prep [/unverified ]. The accepted hash is taken ONLY as argv[4] == /unverified with argv[5] of exactly // 64 characters; anywhere else (or another length) it is silently NULL (the prep then asks for the exact hash by other means / refuses). -// Differs from setup.c's parse_switches (tests\test_hardening.c), which finds /unverified at any index >= 4, takes any following argument as its +// Differs from the install stage's rule in cmd_parse (setup.c; tests\test_hardening.c), which finds /unverified at any index >= 4, takes any following argument as its // value and drops one that is not 64 characters long: there a bad value is consumed, here it simply is not at argv[4]. // The verb itself: /driver and /driver-continue open the wizard (not run here); /driver-prep, /driver-install and /driver-cancel need an // administrator (return 1 otherwise) and /driver-prep needs argc >= 4 (return 1 otherwise); anything else is DRV_NOVERB. -static int driver_verb_o(int argc, WCHAR **argv) { install_opts o; install_opts_default(&o); return driver_verb(argc, argv, &o); } +// The driver's rows of the verb table (driver.c). Other verbs of the table (/machine, /uninstall: setup.c) are only looked up, never run here: +// they are "not a driver verb" as driver_verb used to say (DRV_NOVERB). +static BOOL is_driver_verb(const verb *e) +{ return e && (e->run == cmd_driver || e->run == cmd_driver_continue || e->run == cmd_driver_prep || e->run == cmd_driver_install || e->run == cmd_driver_cancel); } +static int driver_verb_o(int argc, WCHAR **argv) +{ + cmdline c; const verb *e = verb_find(argc, argv); + c.argc = argc; c.argv = argv; cmd_parse(&c); + return is_driver_verb(e) ? e->run(&c) : DRV_NOVERB; +} +// the accepted hash of /driver-prep (c.daccept4); setup_accept_arg: the install stage's rule (c.daccept) on the same command line +static const WCHAR *prep_accept_arg(int argc, WCHAR **argv) { cmdline c; c.argc = argc; c.argv = argv; cmd_parse(&c); return c.daccept4; } +static const WCHAR *setup_accept_arg(int argc, WCHAR **argv) { cmdline c; c.argc = argc; c.argv = argv; cmd_parse(&c); return c.daccept; } static void test_cmdline(void) { WCHAR h64[80], h63[80], h65[80]; @@ -929,7 +941,8 @@ static void test_cmdline(void) CHECK(prep_accept_arg(6, a3) == NULL, L"accept: 65 characters: NULL"); CHECK(prep_accept_arg(6, a4) == h64, L"accept: /UNVERIFIED, case-insensitive"); CHECK(prep_accept_arg(5, a5) == NULL, L"accept: /unverified without a value: NULL"); - CHECK(prep_accept_arg(7, a6) == NULL, L"accept: /unverified at argv[5] (not argv[4]): NULL (setup.c would take it at index 5)"); + CHECK(prep_accept_arg(7, a6) == NULL, L"accept: /unverified at argv[5] (not argv[4]): NULL (the install stage's rule takes it at index 5)"); + CHECK(setup_accept_arg(7, a6) == h64 && setup_accept_arg(6, a1) == h64, L"accept: the install stage's rule (c.daccept) takes it at argv[5] too: the two rules differ, each verb keeps its own"); CHECK(prep_accept_arg(5, a7) == NULL, L"accept: a bare hash without /unverified: NULL"); CHECK(prep_accept_arg(6, a8) == NULL, L"accept: /with-driver is not understood here: NULL"); CHECK(prep_accept_arg(4, a9) == NULL, L"accept: nothing after the zip: NULL"); diff --git a/tests/test_hardening.c b/tests/test_hardening.c index ebcb725..2ca03ea 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -29,7 +29,11 @@ int update_check(void) { return 0; } int update_offer(void) { return 0; } int update_run(const WCHAR *a) { (void)a; return 0; } // driver.c is not part of this test (it would touch ProgramData and the Start menu): tests\test_driver.c covers it -int driver_verb(int argc, WCHAR **argv, const install_opts *o) { (void)argc; (void)argv; (void)o; return DRV_NOVERB; } +int cmd_driver(cmdline *c) { (void)c; return 0; } +int cmd_driver_continue(cmdline *c) { (void)c; return 0; } +int cmd_driver_prep(cmdline *c) { (void)c; return 0; } +int cmd_driver_install(cmdline *c) { (void)c; return 0; } +int cmd_driver_cancel(cmdline *c) { (void)c; return 0; } int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { (void)sid; (void)zip; (void)accept; return 0; } // called by install_fix_driver / install_all_driver (not run here) void driver_machine_stage(const WCHAR *p) { (void)p; } void driver_machine_uninstall(void) { } @@ -838,13 +842,14 @@ static void test_run_elevated_args(void) // - they take the next argument as their value WHATEVER it is (oddity: "/unverified /quiet" swallows /quiet; so does "/with-driver /quiet"); // - /unverified keeps its value only when exactly 64 characters long, otherwise the value is consumed and dropped (no error); // - unknown arguments are ignored; the last /with-driver wins; a later /unverified with a bad length does not clear an earlier good one. -// (driver.c's driver_verb parses /driver-prep differently: see test_driver.c.) The dispatch on argv[1] (/machine needs argc >= 4, ...) is not covered here. -static install_opts g_po; static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da; +// (/driver-prep takes its hash by the narrower rule daccept4, see test_driver.c.) The verb table is test_verb_table. +static install_opts g_po; static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da, *g_da4; static void parse_case(int argc, WCHAR **argv) { - install_opts_default(&g_po); - g_pin_seen = FALSE; g_dz = NULL; g_da = NULL; - parse_switches(argc, argv, &g_po, &g_pin_seen, &g_dz, &g_da); + cmdline c; + c.argc = argc; c.argv = argv; + cmd_parse(&c); + g_po = c.opts; g_pin_seen = c.pin; g_dz = c.dzip; g_da = c.daccept; g_da4 = c.daccept4; } #define PARSE(...) do { WCHAR *av_[] = { __VA_ARGS__ }; parse_case((int)(sizeof(av_) / sizeof(av_[0])), av_); } while (0) static void test_parse_switches(void) @@ -892,6 +897,32 @@ static void test_parse_switches(void) CHECK(g_da && !lstrcmpW(g_da, h64), L"parse: a later short /unverified does not clear an earlier accepted one"); PARSE(L"s", L"/machine", L"S-1", L"e", L"/WITH-DRIVER", L"z", L"/UNVERIFIED", h64); CHECK(g_da && g_dz, L"parse: the driver switches are case-insensitive too"); + // the second rule, for /driver-prep: the same value, but only as argv[4] argv[5] + PARSE(L"s", L"/driver-prep", L"S-1", L"z", L"/unverified", h64); + CHECK(g_da && !lstrcmpW(g_da, h64) && g_da4 == g_da, L"parse: /unverified at argv[4]: both rules have it"); + PARSE(L"s", L"/driver-prep", L"S-1", L"z", L"/quiet", L"/unverified", h64); + CHECK(g_da && !lstrcmpW(g_da, h64) && !g_da4 && g_po.quiet, L"parse: DIVERGENCE /unverified at argv[5]: the install stage's rule takes it, the prep's does not"); + { WCHAR other[80]; + lstrcpyW(other, h64); other[0] = L'f'; + PARSE(L"s", L"/driver-prep", L"S-1", L"z", L"/unverified", h64, L"/unverified", other); + CHECK(g_da && !lstrcmpW(g_da, other) && g_da4 && !lstrcmpW(g_da4, h64), L"parse: DIVERGENCE two valid /unverified: later one for the install stage, the one at argv[4] for the prep"); + PARSE(L"s", L"/driver-prep", L"S-1", L"z", L"/unverified", h63, L"/unverified", other); + CHECK(g_da && !g_da4, L"parse: a short /unverified at argv[4] is dropped for both, a valid later one counts for the install stage only"); + } +} + +// The verb table: argv[1] (any case, exact) and enough arguments. With too few arguments /machine and /full are no verb at all (the wizard or a /quiet +// install runs instead), the elevated driver verbs and the others are taken with the verb alone. Only the lookup is called, nothing is run. +static void test_verb_table(void) +{ + WCHAR *m3[] = { L"s", L"/machine", L"S-1" }; WCHAR *m4[] = { L"s", L"/MACHINE", L"S-1", L"e" }; + WCHAR *f3[] = { L"s", L"/full", L"S-1" }; WCHAR *f4[] = { L"s", L"/full", L"S-1", L"m" }; + WCHAR *u2[] = { L"s", L"/unmachine" }; WCHAR *d2[] = { L"s", L"/driver-prep" }; WCHAR *x2[] = { L"s", L"/driver-prepx" }; + WCHAR *n1[] = { L"s" }; WCHAR *q2[] = { L"s", L"/quiet" }; WCHAR *p2[] = { L"s", L"driver" }; WCHAR *o2[] = { L"s", L"/offer" }; + CHECK(verb_find(3, m3) == NULL && verb_find(4, m4) != NULL, L"verbs: /machine needs argc >= 4"); + CHECK(verb_find(3, f3) == NULL && verb_find(4, f4) != NULL, L"verbs: /full needs argc >= 4"); + CHECK(verb_find(2, u2) != NULL && verb_find(2, d2) != NULL && verb_find(2, o2) != NULL, L"verbs: /unmachine, /driver-prep, /offer need the verb only"); + CHECK(verb_find(2, x2) == NULL && verb_find(2, q2) == NULL && verb_find(2, p2) == NULL && verb_find(1, n1) == NULL, L"verbs: exact match only, no verb without argv[1]"); } // FIX_DONE(rc): the elevated install stage "installed the fix" = rc 0, or the driver step gave a code >= DRV_E_HASH (the fix is in place, the driver @@ -943,6 +974,7 @@ int wmain(void) wprintf(L"[shortcut paths]\n"); test_shortcut_paths(); wprintf(L"[run_elevated arguments]\n"); test_run_elevated_args(); wprintf(L"[command line switches]\n"); test_parse_switches(); + wprintf(L"[verb table]\n"); test_verb_table(); wprintf(L"[result codes]\n"); test_result_codes(); { // clean up WCHAR c[1000]; From 40893098e1f13a94a886c683b787e1c5fa23a4f9 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:20:18 +0800 Subject: [PATCH 15/41] Shortcuts: one list of shortcut folders, walks use dir_walk The shell folders that can hold a DynaRun shortcut were listed in six places (icon search, fix, point-back, search for our own shortcut, restore path check, other accounts' point-back). They are now one table (SF[]) with a flag per use and a deep flag per use, read through shortcut_folders(); each user keeps exactly the folders and recursion it had. The four walks (find_icon, scan, find_ours, unretarget) use dir_walk. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 339 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 199 insertions(+), 140 deletions(-) diff --git a/src/setup.c b/src/setup.c index 39b7378..ce91b38 100644 --- a/src/setup.c +++ b/src/setup.c @@ -821,42 +821,100 @@ static void set_appid(IShellLinkW *sl, const WCHAR *id) // file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries // its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. static BOOL g_icontried; static WCHAR g_icon[MAX_PATH]; static int g_iconidx; -static BOOL find_icon(const WCHAR *dir, BOOL recurse) +static BOOL is_lnk(const WCHAR *p) { int n = lstrlenW(p); return n >= 4 && !lstrcmpiW(p + n - 4, L".lnk"); } + +// The shell folders that can hold a DynaRun shortcut: ONE list, for every walk and check that looks at them (the icon search, the fix, the +// uninstall's point-back, the search for our own shortcut, and the check of the paths a backup is put back to). A row says which of them +// include the folder (SFU_*) and which of those also look into its subfolders (deep); nothing else lists a shortcut folder, so a folder that +// is not here is neither scanned nor accepted by restore_shortcuts. +enum { SFU_FIX = 1, SFU_UNRET = 2, SFU_FIND = 4, SFU_ICON = 8, SFU_RESTORE = 16 }; +enum { SF_DESKTOP, SF_STARTMENU, SF_PROGRAMS, SF_QUICKLAUNCH, SF_OWN, SF_COUNT }; // the index in SF[]; the first two are searched first +enum { SF_USER = 1, SF_COMMON = 2, SF_BOTH = 3 }; // which of the two folders of a row (bit 0 the user's, bit 1 all users') +typedef struct { + int csidl[2]; // [0] the user's folder, [1] the all users' folder (0: there is none) + const WCHAR *sub; // below that folder + const WCHAR *reg, *rel; // the folder of another account (unretarget_user): its name in User Shell Folders, and the default below the profile + unsigned uses, deep; +} shortcut_folder; +static const WCHAR QUICKLAUNCH[] = L"\\Microsoft\\Internet Explorer\\Quick Launch"; // incl. pinned taskbar items +static const shortcut_folder SF[SF_COUNT] = { + { { CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY }, NULL, L"Desktop", L"Desktop", + SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, 0 }, + // the Start menu root (not just Programs): some DynaRun installs put their shortcut there. The fix and the point-back look at the root + // alone and at Programs on its own; the two searches look below the whole Start menu. + { { CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU }, NULL, L"Start Menu", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu", + SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, SFU_FIND | SFU_ICON }, + { { CSIDL_PROGRAMS, CSIDL_COMMON_PROGRAMS }, NULL, L"Programs", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs", + SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, + { { CSIDL_APPDATA, 0 }, QUICKLAUNCH, L"AppData", L"AppData\\Roaming", + SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, + { { CSIDL_APPDATA, 0 }, L"\\DynaRunFix", NULL, NULL, SFU_RESTORE, 0 }, // desktop_link(): the shortcut it creates when there is none +}; + +// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none, or when check is set and the path is +// too long for what goes below it (the uninstall's point-back and the restore path check have always checked; the other callers have not). +static BOOL sf_path(int id, BOOL common, WCHAR *out, BOOL check) { - WCHAR *p = alloc(3 * MAX_PATH * sizeof(WCHAR)), *ic, *x; WIN32_FIND_DATAW fd; HANDLE f; IShellLinkW *sl; int idx; BOOL r = FALSE; - if (!p) return FALSE; - ic = p + MAX_PATH; x = ic + MAX_PATH; - // p has room for MAX_PATH characters only (ic and x follow it): longer paths are skipped - f = lstrlenW(dir) + 3 > MAX_PATH ? INVALID_HANDLE_VALUE : FindFirstFileW(cat3(p, dir, L"\\*", NULL), &fd); - if (f != INVALID_HANDLE_VALUE) { - do { - if (fd.cFileName[0] == '.' || (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 > MAX_PATH) continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 > 2 * MAX_PATH) continue; // a path too long for the buffer - cat3(p, dir, L"\\", fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (recurse) r = find_icon(p, TRUE); continue; } - if (lstrlenW(p) < 4 || lstrcmpiW(p + lstrlenW(p) - 4, L".lnk") || classify(p) == LNK_OTHER || !(sl = load_link(p))) continue; - ic[0] = 0; idx = 0; - IShellLinkW_GetIconLocation(sl, ic, MAX_PATH, &idx); - IShellLinkW_Release(sl); - if (ic[0] && lstrcmpiW(ic, g_exe) && ExpandEnvironmentStringsW(ic, x, MAX_PATH) && exists(x)) { - lstrcpyW(g_icon, ic); g_iconidx = idx; r = TRUE; - } - } while (!r && FindNextFileW(f, &fd)); - FindClose(f); + const shortcut_folder *f = &SF[id]; int csidl = f->csidl[common ? 1 : 0]; + if (!csidl || !SHGetSpecialFolderPathW(NULL, out, csidl, FALSE)) return FALSE; + if (f->sub) { if (check && lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } + return TRUE; +} + +// fn(folder, row, common, deep, ctx) is called for each folder that the use (one SFU_*) lists, in the scope (SF_USER, SF_COMMON, SF_BOTH): a row's +// user folder before its all users' one. fn returns FALSE to stop; then this returns TRUE. +typedef BOOL (*sf_fn)(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx); +static BOOL shortcut_folders(unsigned use, int scope, sf_fn fn, void *ctx) +{ + WCHAR d[MAX_PATH]; int i, id, c; + for (i = 0; i < SF_COUNT; i++) { + id = use == SFU_ICON && i < 2 ? 1 - i : i; // the icon is looked for in the Start menu before the Desktop + if (!(SF[id].uses & use)) continue; + for (c = 0; c < 2; c++) + if ((scope & (1 << c)) && sf_path(id, c, d, use == SFU_UNRET || use == SFU_RESTORE) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; } - release(p); + return FALSE; +} + +// DynaRun's own icon. DynaRun V3.exe holds only the chart icon of its windows; the blue Dyna Run logo is in the icon +// file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries +// its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. +// The walks below (dir_walk) skip names that start with '.' and paths longer than their buffer; all but the fix do not follow links. +typedef struct { WCHAR p[MAX_PATH], ic[MAX_PATH], x[MAX_PATH]; BOOL recurse, found; } icon_walk; +static BOOL find_icon(const WCHAR *dir, BOOL recurse); +static BOOL icon_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + icon_walk *w = vc; IShellLinkW *sl; int idx; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_icon(p, TRUE); return !w->found; } + if (!is_lnk(p) || classify(p) == LNK_OTHER || !(sl = load_link(p))) return TRUE; + w->ic[0] = 0; idx = 0; + IShellLinkW_GetIconLocation(sl, w->ic, MAX_PATH, &idx); + IShellLinkW_Release(sl); + if (w->ic[0] && lstrcmpiW(w->ic, g_exe) && ExpandEnvironmentStringsW(w->ic, w->x, MAX_PATH) && exists(w->x)) { + lstrcpyW(g_icon, w->ic); g_iconidx = idx; w->found = TRUE; + } + return !w->found; +} + +static BOOL find_icon(const WCHAR *dir, BOOL recurse) +{ + icon_walk *w = alloc(sizeof(*w)); BOOL r; + if (!w) return FALSE; + w->recurse = recurse; + dir_walk(dir, w->p, MAX_PATH, MAX_PATH + 1, icon_entry, w); // paths of at most MAX_PATH characters (dir_walk's limit is "shorter than") + r = w->found; + release(w); return r; } +static BOOL icon_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { return !find_icon(dir, deep); } + static void dynarun_icon(void) { - static const int dirs[] = { CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU, CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY }; - WCHAR d[MAX_PATH]; int i; if (g_icontried) return; g_icontried = TRUE; - for (i = 0; i < 4; i++) - if (SHGetSpecialFolderPathW(NULL, d, dirs[i], FALSE) && find_icon(d, i < 2)) return; + shortcut_folders(SFU_ICON, SF_BOTH, icon_folder, NULL); } static BOOL save_link(const WCHAR *p, const WCHAR *target, const WCHAR *icon, int idx, WORD hotkey, int show, BOOL runas) @@ -910,76 +968,83 @@ static void retarget(const WCHAR *p, HKEY backup) release(orig); } +typedef struct { WCHAR p[2 * MAX_PATH]; HKEY backup; BOOL recurse; int found; } scan_walk; +static int scan(const WCHAR *dir, BOOL recurse, HKEY backup); +static BOOL scan_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) // (this walk follows links; the others do not) +{ + scan_walk *w = vc; int c; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found += scan(p, TRUE, w->backup); return TRUE; } + if (!is_lnk(p)) return TRUE; + if ((c = classify(p)) == LNK_DYNARUN) retarget(p, w->backup); + // ours from an older version (before v1.3.0 they had no AppUserModelID): written again with the ID, else the + // running DynaRun (ID set by dynafix.dll) matches no shortcut and Windows shows its window icon, not DynaRun's + else if (c == LNK_OURS) relink(p, g_launcher); + if (c != LNK_OTHER) w->found++; + return TRUE; +} + static int scan(const WCHAR *dir, BOOL recurse, HKEY backup) // returns number of DynaRun shortcuts { - WCHAR *p = alloc(2 * MAX_PATH * sizeof(WCHAR)); WIN32_FIND_DATAW fd; HANDLE f; int found = 0, c; WCHAR *ext; - if (!p) return 0; - f = lstrlenW(dir) + 3 > 2 * MAX_PATH ? INVALID_HANDLE_VALUE : FindFirstFileW(cat3(p, dir, L"\\*", NULL), &fd); - if (f != INVALID_HANDLE_VALUE) { - do { - if (fd.cFileName[0] == '.') continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 > 2 * MAX_PATH) continue; // a path too long for the buffer - cat3(p, dir, L"\\", fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (recurse) found += scan(p, TRUE, backup); continue; } - ext = p + lstrlenW(p) - 4; - if (ext < p || lstrcmpiW(ext, L".lnk")) continue; - if ((c = classify(p)) == LNK_DYNARUN) retarget(p, backup); - // ours from an older version (before v1.3.0 they had no AppUserModelID): written again with the ID, else the - // running DynaRun (ID set by dynafix.dll) matches no shortcut and Windows shows its window icon, not DynaRun's - else if (c == LNK_OURS) relink(p, g_launcher); - if (c != LNK_OTHER) found++; - } while (FindNextFileW(f, &fd)); - FindClose(f); - } - release(p); + scan_walk *w = alloc(sizeof(*w)); int found; + if (!w) return 0; + w->backup = backup; w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, scan_lnk, w); + found = w->found; + release(w); return found; } +typedef struct { WCHAR p[2 * MAX_PATH]; WCHAR *out; BOOL recurse, found; } ours_walk; +static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out); +static BOOL ours_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + ours_walk *w = vc; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_ours(p, TRUE, w->out); return !w->found; } + if (is_lnk(p) && classify(p) == LNK_OURS) { lstrcpyW(w->out, p); w->found = TRUE; } + return !w->found; +} + static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out) // the first shortcut of ours below dir { - WCHAR *p = alloc(2 * MAX_PATH * sizeof(WCHAR)); WIN32_FIND_DATAW fd; HANDLE f; WCHAR *ext; BOOL r = FALSE; - if (!p) return FALSE; - f = lstrlenW(dir) + 3 > 2 * MAX_PATH ? INVALID_HANDLE_VALUE : FindFirstFileW(cat3(p, dir, L"\\*", NULL), &fd); - if (f != INVALID_HANDLE_VALUE) { - do { - if (fd.cFileName[0] == '.' || (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 > 2 * MAX_PATH) continue; // a path too long for the buffer - cat3(p, dir, L"\\", fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (recurse) r = find_ours(p, TRUE, out); continue; } - ext = p + lstrlenW(p) - 4; - if (ext >= p && !lstrcmpiW(ext, L".lnk") && classify(p) == LNK_OURS) { lstrcpyW(out, p); r = TRUE; } - } while (!r && FindNextFileW(f, &fd)); - FindClose(f); - } - release(p); + ours_walk *w = alloc(sizeof(*w)); BOOL r; + if (!w) return FALSE; + w->out = out; w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, ours_lnk, w); + r = w->found; + release(w); return r; } -static int fix_shortcuts(BOOL common, HKEY backup) +// The first shortcut of ours where a user sees it: the Desktop, then the Start menu (the user's folder before the all users' one). +static BOOL ours_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *out) { return !find_ours(dir, deep, out); } +static BOOL find_our_shortcut(WCHAR *out) { return shortcut_folders(SFU_FIND, SF_BOTH, ours_folder, out); } + +typedef struct { HKEY backup; int desk; } fix_ctx; +static BOOL fix_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) +{ + fix_ctx *c = vc; int n = scan(dir, deep, c->backup); + if (id == SF_DESKTOP) c->desk = n; + return TRUE; +} + +static int fix_shortcuts(BOOL common, HKEY backup) // returns the number of DynaRun shortcuts on the Desktop { - // the Start menu root (not just Programs): some DynaRun installs put their shortcut there - static const int user[] = { CSIDL_DESKTOPDIRECTORY, CSIDL_STARTMENU, CSIDL_PROGRAMS, CSIDL_APPDATA }, - all[] = { CSIDL_COMMON_DESKTOPDIRECTORY, CSIDL_COMMON_STARTMENU, CSIDL_COMMON_PROGRAMS }; - WCHAR d[MAX_PATH]; int i, desk = 0, n = common ? 3 : 4, csidl; + fix_ctx c; HMODULE msi = load_system_dll(L"msi.dll"); if (msi) { pMsiGetShortcutTarget = (MsiGetShortcutTargetW_t)GetProcAddress(msi, "MsiGetShortcutTargetW"); pMsiGetComponentPath = (MsiGetComponentPathW_t)GetProcAddress(msi, "MsiGetComponentPathW"); if (!pMsiGetComponentPath) pMsiGetShortcutTarget = NULL; } - for (i = 0; i < n; i++) { - csidl = common ? all[i] : user[i]; - if (!SHGetSpecialFolderPathW(NULL, d, csidl, FALSE)) continue; - if (csidl == CSIDL_APPDATA) lstrcatW(d, L"\\Microsoft\\Internet Explorer\\Quick Launch"); // incl. pinned taskbar items - if (i == 0) desk = scan(d, FALSE, backup); - else scan(d, csidl != CSIDL_STARTMENU && csidl != CSIDL_COMMON_STARTMENU, backup); // Programs is scanned on its own - } - return desk; + c.backup = backup; c.desk = 0; + shortcut_folders(SFU_FIX, common ? SF_COMMON : SF_USER, fix_folder, &c); + return c.desk; } // restore_shortcuts writes files at paths that come from the registry. A path is used only when it is a .lnk file below one of the -// shell folders the backups were made from (user: Desktop, Start menu, Programs, Quick Launch, %APPDATA%\DynaRunFix; all users: the -// common ones), with no ".." component and no link (junction) as its folder. When this process is elevated and the backups are in +// shortcut folders the backups were made from (SF[]: the rows that list SFU_RESTORE; user's or all users', as the backup is), with no ".." +// component and no link (junction) as its folder. When this process is elevated and the backups are in // HKCU, which a program of the same user can write, the file must also be below the user's profile folder. static BOOL under_dir(const WCHAR *path, const WCHAR *dir) { @@ -1014,12 +1079,22 @@ static void long_form(WCHAR *d) if (n && n < MAX_PATH) lstrcpyW(d, t); } +typedef struct { WCHAR dirs[10][MAX_PATH]; const WCHAR *list[10]; int n; } restore_dirs; +static BOOL restore_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) +{ + restore_dirs *r = vc; + lstrcpyW(r->dirs[r->n], dir); r->list[r->n] = r->dirs[r->n]; r->n++; + if (id != SF_OWN) { // (the folder of desktop_link() is listed as the shell reports it) + lstrcpyW(r->dirs[r->n], dir); long_form(r->dirs[r->n]); // the backup may hold the long form of a folder that the shell reports in 8.3 form + r->list[r->n] = r->dirs[r->n]; r->n++; + } + return TRUE; +} + static BOOL lnk_path_ok(const WCHAR *name, BOOL common) { - static const int user[] = { CSIDL_DESKTOPDIRECTORY, CSIDL_STARTMENU, CSIDL_PROGRAMS, CSIDL_APPDATA }, - all[] = { CSIDL_COMMON_DESKTOPDIRECTORY, CSIDL_COMMON_STARTMENU, CSIDL_COMMON_PROGRAMS }; - static WCHAR dirs[10][MAX_PATH]; const WCHAR *list[10]; - WCHAR d[MAX_PATH], *e, par[MAX_PATH], lname[MAX_PATH]; int i, len = lstrlenW(name), n = common ? 3 : 4, nd = 0; DWORD a; + static restore_dirs rd; + WCHAR d[MAX_PATH], *e, par[MAX_PATH], lname[MAX_PATH]; int len = lstrlenW(name); DWORD a; if (len < 8 || len >= MAX_PATH) return FALSE; if (!common && is_admin()) { // elevated: the user's own registry is not trusted beyond their profile if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_PROFILE, FALSE)) return FALSE; @@ -1028,22 +1103,13 @@ static BOOL lnk_path_ok(const WCHAR *name, BOOL common) if (!under_dir(lname, d)) return FALSE; } } - for (i = 0; i < n; i++) { - if (!SHGetSpecialFolderPathW(NULL, d, common ? all[i] : user[i], FALSE)) continue; - if ((common ? all[i] : user[i]) == CSIDL_APPDATA) { - if (lstrlenW(d) + 50 >= MAX_PATH) continue; - lstrcpyW(dirs[nd], d); lstrcatW(dirs[nd], L"\\DynaRunFix"); list[nd] = dirs[nd]; nd++; // desktop_link() - lstrcatW(d, L"\\Microsoft\\Internet Explorer\\Quick Launch"); - } - lstrcpyW(dirs[nd], d); list[nd] = dirs[nd]; nd++; - long_form(d); // the backup may hold the long form of a folder that the shell reports in 8.3 form - lstrcpyW(dirs[nd], d); list[nd] = dirs[nd]; nd++; - } + rd.n = 0; + shortcut_folders(SFU_RESTORE, common ? SF_COMMON : SF_USER, restore_folder, &rd); lstrcpyW(par, name); for (e = par + len; e > par && *e != '\\'; e--); *e = 0; - if (!lnk_name_in_dirs(name, list, nd)) { + if (!lnk_name_in_dirs(name, rd.list, rd.n)) { lstrcpyW(lname, par); long_form(lname); // the same, with the folder of the file in long form if (!path_join(lname, MAX_PATH, lname, e + 1)) return FALSE; - if (!lnk_name_in_dirs(lname, list, nd)) return FALSE; + if (!lnk_name_in_dirs(lname, rd.list, rd.n)) return FALSE; } a = GetFileAttributesW(par); if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_REPARSE_POINT)) return FALSE; @@ -1074,37 +1140,34 @@ static void restore_shortcuts(HKEY root, const WCHAR *key) // the launcher (which is about to be deleted) are pointed back at DynaRun V3.exe instead. Only files that // are our shortcuts are rewritten, so a folder read from another user's registry can do no harm; reparse // points (junctions) are not followed. +typedef struct { WCHAR p[2 * MAX_PATH]; BOOL recurse; } unret_walk; +static void unretarget(const WCHAR *dir, BOOL recurse); +static BOOL unret_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + unret_walk *w = vc; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) unretarget(p, TRUE); return TRUE; } + if (is_lnk(p) && g_exe[0] && classify(p) == LNK_OURS) relink(p, g_exe); + return TRUE; +} + static void unretarget(const WCHAR *dir, BOOL recurse) { - WCHAR *p = alloc(2 * MAX_PATH * sizeof(WCHAR)); WIN32_FIND_DATAW fd; HANDLE f; WCHAR *ext; - if (!p) return; - f = lstrlenW(dir) + 3 > 2 * MAX_PATH ? INVALID_HANDLE_VALUE : FindFirstFileW(cat3(p, dir, L"\\*", NULL), &fd); - if (f != INVALID_HANDLE_VALUE) { - do { - if (fd.cFileName[0] == '.' || (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) continue; - if (lstrlenW(dir) + lstrlenW(fd.cFileName) + 2 > 2 * MAX_PATH) continue; // a path too long for the buffer - cat3(p, dir, L"\\", fd.cFileName); - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (recurse) unretarget(p, TRUE); continue; } - ext = p + lstrlenW(p) - 4; - if (ext >= p && !lstrcmpiW(ext, L".lnk") && g_exe[0] && classify(p) == LNK_OURS) relink(p, g_exe); - } while (FindNextFileW(f, &fd)); - FindClose(f); - } - release(p); + unret_walk *w = alloc(sizeof(*w)); + if (!w) return; + w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, unret_lnk, w); + release(w); } // What is left of our shortcuts in the shell folders after the backups were put back (a backup that is missing, as when an older version did -// not record one, or was refused): they point to DynaRun V3.exe again instead of the launcher that is being removed. +// not record one, or was refused): they point to DynaRun V3.exe again instead of the launcher that is being removed. The folders include the +// pins the user made of the launcher (Quick Launch). +static BOOL unret_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { unretarget(dir, deep); return TRUE; } static void unretarget_shell(BOOL common) { - WCHAR d[MAX_PATH]; if (!g_exe[0]) return; - if (SHGetSpecialFolderPathW(NULL, d, common ? CSIDL_COMMON_DESKTOPDIRECTORY : CSIDL_DESKTOPDIRECTORY, FALSE)) unretarget(d, FALSE); - if (SHGetSpecialFolderPathW(NULL, d, common ? CSIDL_COMMON_STARTMENU : CSIDL_STARTMENU, FALSE)) unretarget(d, FALSE); - if (SHGetSpecialFolderPathW(NULL, d, common ? CSIDL_COMMON_PROGRAMS : CSIDL_PROGRAMS, FALSE)) unretarget(d, TRUE); - if (!common && SHGetSpecialFolderPathW(NULL, d, CSIDL_APPDATA, FALSE) && lstrlenW(d) + 50 < MAX_PATH) { // pins the user made of the launcher - lstrcatW(d, L"\\Microsoft\\Internet Explorer\\Quick Launch"); unretarget(d, TRUE); - } + shortcut_folders(SFU_UNRET, common ? SF_COMMON : SF_USER, unret_folder, NULL); } /* ---------- taskbar pin and sign-in start ---------- */ @@ -1175,8 +1238,8 @@ static void taskbar_policy(const install_opts *o, const WCHAR *sid, HKEY app) static BOOL pinned_ours(void) // a shortcut to the launcher among the taskbar's pinned items (Windows 7+) { static WCHAR d[MAX_PATH], f[MAX_PATH]; - if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_APPDATA, FALSE)) return FALSE; - lstrcatW(d, L"\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar"); + if (!sf_path(SF_QUICKLAUNCH, FALSE, d, FALSE)) return FALSE; + lstrcatW(d, L"\\User Pinned\\TaskBar"); return find_ours(d, FALSE, f); } @@ -1234,8 +1297,7 @@ static BOOL pin_taskbar(const WCHAR *lnk, int *res) static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; os_ver(&ma, &mi, &b); if (ma == 5 || (ma == 6 && !mi)) { // XP, Vista: the Quick Launch toolbar - if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_APPDATA, FALSE)) return FALSE; - lstrcatW(d, L"\\Microsoft\\Internet Explorer\\Quick Launch"); + if (!sf_path(SF_QUICKLAUNCH, FALSE, d, FALSE)) return FALSE; CreateDirectoryW(d, NULL); if (!save_link(lstrcatW(d, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; if (!RegOpenKeyExW(HKEY_CURRENT_USER, cat3(sub, USERKEY, L"\\Shortcuts", NULL), 0, KEY_SET_VALUE, &k)) { @@ -1255,12 +1317,10 @@ static BOOL pin_taskbar(const WCHAR *lnk, int *res) static BOOL desktop_link(WCHAR *out, HKEY backup) // one of our shortcuts to pin: desktop, Start menu, else a new one { - static const int dirs[] = { CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY, CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU }; - WCHAR d[MAX_PATH]; int i; - for (i = 0; i < 4; i++) - if (SHGetSpecialFolderPathW(NULL, d, dirs[i], FALSE) && find_ours(d, i >= 2, out)) return TRUE; - if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_APPDATA, FALSE)) return FALSE; - lstrcatW(d, L"\\DynaRunFix"); CreateDirectoryW(d, NULL); + WCHAR d[MAX_PATH]; + if (find_our_shortcut(out)) return TRUE; + if (!sf_path(SF_OWN, FALSE, d, FALSE)) return FALSE; + CreateDirectoryW(d, NULL); if (!save_link(cat3(out, d, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; RegSetValueExW(backup, out, 0, REG_BINARY, (const BYTE *)"", 0); return TRUE; @@ -1325,11 +1385,8 @@ static void launch_normal(const WCHAR *file, const WCHAR *dir) // running the launcher directly. Falls back to the launcher with DynaRun's folder as working directory. void start_dynarun(void) { - static const int dirs[] = { CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY, CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU }; - static WCHAR lnk[MAX_PATH], dir[MAX_PATH], d[MAX_PATH]; - int i; WCHAR *q; BOOL have = FALSE; - for (i = 0; i < 4 && !have; i++) - have = SHGetSpecialFolderPathW(NULL, d, dirs[i], FALSE) && find_ours(d, i >= 2, lnk); + static WCHAR lnk[MAX_PATH], dir[MAX_PATH]; + WCHAR *q; BOOL have = find_our_shortcut(lnk); if (!have && exists(cat3(lnk, g_dir, L"\\DynaRun V3.lnk", NULL))) have = TRUE; if (have) { launch_normal(lnk, NULL); return; } lstrcpyW(dir, g_exe); q = dir + lstrlenW(dir); while (q > dir && *q != '\\') q--; *q = 0; @@ -1384,7 +1441,7 @@ static void unload_leftover_hives(void) static void unretarget_user(const WCHAR *sid, const WCHAR *profile) { static int seq; - static WCHAR p[FOLDER_CCH], sub[300], tmphive[80]; HKEY hive = NULL; BOOL loaded = FALSE; + static WCHAR p[FOLDER_CCH], sub[300], tmphive[80]; HKEY hive = NULL; BOOL loaded = FALSE; int id; wsprintfW(tmphive, HIVE_PREFIX L"-%lu-%lu-%d", GetCurrentProcessId(), GetTickCount(), seq++); // unique: never a name another load uses if (lstrlenW(profile) + 12 >= MAX_PATH) return; // (the caller checked; NTUSER.DAT must fit) if (RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &hive)) { // not signed in: load their hive @@ -1393,11 +1450,13 @@ static void unretarget_user(const WCHAR *sid, const WCHAR *profile) !RegOpenKeyExW(HKEY_USERS, tmphive, 0, KEY_READ | KEY_WRITE, &hive)) loaded = TRUE; else hive = NULL; } - user_folder(hive, profile, L"Desktop", L"Desktop", p, FOLDER_CCH - 64); if (p[0]) unretarget(p, FALSE); - user_folder(hive, profile, L"Start Menu", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu", p, FOLDER_CCH - 64); if (p[0]) unretarget(p, FALSE); - user_folder(hive, profile, L"Programs", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs", p, FOLDER_CCH - 64); if (p[0]) unretarget(p, TRUE); - user_folder(hive, profile, L"AppData", L"AppData\\Roaming", p, FOLDER_CCH - 64); - if (p[0]) { lstrcatW(p, L"\\Microsoft\\Internet Explorer\\Quick Launch"); unretarget(p, TRUE); } + for (id = 0; id < SF_COUNT; id++) { // the same folders as unretarget_shell, of their account + if (!(SF[id].uses & SFU_UNRET)) continue; + user_folder(hive, profile, SF[id].reg, SF[id].rel, p, FOLDER_CCH - 64); + if (!p[0]) continue; + if (SF[id].sub) lstrcatW(p, SF[id].sub); + unretarget(p, (SF[id].deep & SFU_UNRET) != 0); + } if (hive) { drop_run(hive, TRUE); drop_policy(hive); // start at sign-in, Windows 11 pin // their backups are of no use any more @@ -1798,9 +1857,9 @@ static void user_stage(const install_opts *o, install_result *res) RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { int desk = fix_shortcuts(FALSE, s); - if (!desk && SHGetSpecialFolderPathW(NULL, p, CSIDL_COMMON_DESKTOPDIRECTORY, FALSE)) + if (!desk && sf_path(SF_DESKTOP, TRUE, p, FALSE)) desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here - if (!desk && !o->keep && SHGetSpecialFolderPathW(NULL, p, CSIDL_DESKTOPDIRECTORY, FALSE) && + if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p, FALSE) && save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); if (o->pin && !o->keep) { From 0791e0ece3f867601072d69308dc457a869f5e33 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:05:54 +0800 Subject: [PATCH 16/41] Shortcuts: skip a shortcut folder whose path is too long instead of overflowing The unified sf_path() now always checks the length of the %APPDATA% path before the sub folder is appended. Until now only the uninstall's point-back (unretarget_shell) and the restore path check (lnk_path_ok) did; fix_shortcuts, desktop_link, pin_taskbar, pinned_ours and the creation of the DynaRunFix shortcut appended "\Microsoft\Internet Explorer\Quick Launch" or "\DynaRunFix" to a d[MAX_PATH] buffer without looking, so an %APPDATA% path of about 210 characters or more overflowed it. Before: those callers wrote past the end of the buffer for such a profile. After: the folder is skipped (fix_shortcuts does not scan the Quick Launch folder, desktop_link/pin_taskbar report that there is none to use). A shorter path behaves as before. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/setup.c b/src/setup.c index ce91b38..497c6a0 100644 --- a/src/setup.c +++ b/src/setup.c @@ -851,13 +851,12 @@ static const shortcut_folder SF[SF_COUNT] = { { { CSIDL_APPDATA, 0 }, L"\\DynaRunFix", NULL, NULL, SFU_RESTORE, 0 }, // desktop_link(): the shortcut it creates when there is none }; -// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none, or when check is set and the path is -// too long for what goes below it (the uninstall's point-back and the restore path check have always checked; the other callers have not). -static BOOL sf_path(int id, BOOL common, WCHAR *out, BOOL check) +// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none or the path is too long for what goes below it. +static BOOL sf_path(int id, BOOL common, WCHAR *out) { const shortcut_folder *f = &SF[id]; int csidl = f->csidl[common ? 1 : 0]; if (!csidl || !SHGetSpecialFolderPathW(NULL, out, csidl, FALSE)) return FALSE; - if (f->sub) { if (check && lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } + if (f->sub) { if (lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } return TRUE; } @@ -871,7 +870,7 @@ static BOOL shortcut_folders(unsigned use, int scope, sf_fn fn, void *ctx) id = use == SFU_ICON && i < 2 ? 1 - i : i; // the icon is looked for in the Start menu before the Desktop if (!(SF[id].uses & use)) continue; for (c = 0; c < 2; c++) - if ((scope & (1 << c)) && sf_path(id, c, d, use == SFU_UNRET || use == SFU_RESTORE) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; + if ((scope & (1 << c)) && sf_path(id, c, d) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; } return FALSE; } @@ -1238,7 +1237,7 @@ static void taskbar_policy(const install_opts *o, const WCHAR *sid, HKEY app) static BOOL pinned_ours(void) // a shortcut to the launcher among the taskbar's pinned items (Windows 7+) { static WCHAR d[MAX_PATH], f[MAX_PATH]; - if (!sf_path(SF_QUICKLAUNCH, FALSE, d, FALSE)) return FALSE; + if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; lstrcatW(d, L"\\User Pinned\\TaskBar"); return find_ours(d, FALSE, f); } @@ -1297,7 +1296,7 @@ static BOOL pin_taskbar(const WCHAR *lnk, int *res) static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; os_ver(&ma, &mi, &b); if (ma == 5 || (ma == 6 && !mi)) { // XP, Vista: the Quick Launch toolbar - if (!sf_path(SF_QUICKLAUNCH, FALSE, d, FALSE)) return FALSE; + if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; CreateDirectoryW(d, NULL); if (!save_link(lstrcatW(d, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; if (!RegOpenKeyExW(HKEY_CURRENT_USER, cat3(sub, USERKEY, L"\\Shortcuts", NULL), 0, KEY_SET_VALUE, &k)) { @@ -1319,7 +1318,7 @@ static BOOL desktop_link(WCHAR *out, HKEY backup) // one of our shortcuts to p { WCHAR d[MAX_PATH]; if (find_our_shortcut(out)) return TRUE; - if (!sf_path(SF_OWN, FALSE, d, FALSE)) return FALSE; + if (!sf_path(SF_OWN, FALSE, d)) return FALSE; CreateDirectoryW(d, NULL); if (!save_link(cat3(out, d, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; RegSetValueExW(backup, out, 0, REG_BINARY, (const BYTE *)"", 0); @@ -1857,9 +1856,9 @@ static void user_stage(const install_opts *o, install_result *res) RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { int desk = fix_shortcuts(FALSE, s); - if (!desk && sf_path(SF_DESKTOP, TRUE, p, FALSE)) + if (!desk && sf_path(SF_DESKTOP, TRUE, p)) desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here - if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p, FALSE) && + if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p) && save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); if (o->pin && !o->keep) { From a2197ffb7b74f21290d57945a3cf01634ae74480 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:21:59 +0800 Subject: [PATCH 17/41] trust_ctx and msi_api_init: no hidden state for the user SID, exe trust and the MSI API g_user_sid and g_exe_trusted are now a trust_ctx that each stage makes and passes to user_writable, check_exe and g_is_writable (path_user_writable makes its own from the sid it is given). The MSI function pointers are looked up by one idempotent msi_api_init(), called by classify before its check of advertised shortcuts, so classify no longer depends on fix_shortcuts having run first. The tests pass a trust_ctx of their own. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 81 +++++++++++++++++++++++++----------------- tests/promote_com.h | 4 +-- tests/test_driver.c | 8 ++--- tests/test_hardening.c | 35 +++++++++--------- 4 files changed, 72 insertions(+), 56 deletions(-) diff --git a/src/setup.c b/src/setup.c index 497c6a0..fc0d603 100644 --- a/src/setup.c +++ b/src/setup.c @@ -87,8 +87,11 @@ BOOL g_zh; int g_drvmode; // DRV_MODE_*: the wizard starts at the driver pages (driver.c, wizard.c) HWND g_hwnd; WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; -static WCHAR g_user_sid[200]; // the user who started the setup (elevated stage: from its command line) -static BOOL g_exe_trusted; // check_exe: DynaRun's exe and folder cannot be changed by ordinary users +// What the ACL decisions of a stage depend on, passed to the functions that decide (never kept in a global): each stage makes one. +typedef struct { + WCHAR user_sid[200]; // the user who started the setup (elevated stage: from its command line); "" = none known + BOOL exe_trusted; // set by check_exe: DynaRun's exe and folder cannot be changed by ordinary users +} trust_ctx; #define TITLE T(L"DynaRunFix Setup", L"DynaRunFix 安裝程式") /* ---------- DLLs: only from the system folder ---------- */ @@ -371,11 +374,11 @@ static BOOL sid_in(PSID s, const WCHAR *const *list) // TRUE when ordinary users could change what is at path (or when that cannot be told). Not user-writable: owned by SYSTEM, // the Administrators or TrustedInstaller, and no ACE that applies to the object itself gives Everyone, Users, Authenticated Users, // Interactive, Power Users or the user who started the setup a write, delete or permission right. -static BOOL user_writable(const WCHAR *path) +static BOOL user_writable(const trust_ctx *t, const WCHAR *path) { static const WCHAR *const owners[] = { L"S-1-5-18", L"S-1-5-32-544", L"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", NULL }; static const WCHAR *const others[] = { L"S-1-1-0", L"S-1-5-32-545", L"S-1-5-11", L"S-1-5-4", L"S-1-5-32-547", NULL }; - const WCHAR *const me[2] = { g_user_sid, NULL }; + const WCHAR *const me[2] = { t->user_sid, NULL }; PSID owner = NULL; PACL dacl = NULL; PSECURITY_DESCRIPTOR sd = NULL; BOOL bad = TRUE; DWORD i; ACCESS_ALLOWED_ACE *a; if (GetNamedSecurityInfoW((LPWSTR)path, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner, NULL, &dacl, NULL, &sd)) return TRUE; if (owner && dacl && sid_in(owner, owners)) { // no DACL = full access for everyone: bad @@ -383,20 +386,22 @@ static BOOL user_writable(const WCHAR *path) for (i = 0; i < dacl->AceCount && !bad; i++) { if (!GetAce(dacl, i, (void **)&a)) { bad = TRUE; break; } if (a->Header.AceType != ACCESS_ALLOWED_ACE_TYPE || (a->Header.AceFlags & INHERIT_ONLY_ACE) || !(a->Mask & WRITE_MASK)) continue; - if (sid_in((PSID)&a->SidStart, others) || (g_user_sid[0] && sid_in((PSID)&a->SidStart, me))) bad = TRUE; + if (sid_in((PSID)&a->SidStart, others) || (t->user_sid[0] && sid_in((PSID)&a->SidStart, me))) bad = TRUE; } } LocalFree(sd); return bad; } -static BOOL (*g_is_writable)(const WCHAR *path) = user_writable; // the tests replace this +static BOOL (*g_is_writable)(const trust_ctx *t, const WCHAR *path) = user_writable; // the tests replace this -// driver.c: may ordinary users (and the user who started the setup, sid) change what is at path? +// driver.c: may ordinary users (and the user who started the setup, sid; NULL: no such user is known) change what is at path? BOOL path_user_writable(const WCHAR *path, const WCHAR *sid) { - if (sid && sid[0]) lstrcpynW(g_user_sid, sid, 200); - return g_is_writable(path); + trust_ctx t; + zero(&t, sizeof(t)); + if (sid) lstrcpynW(t.user_sid, sid, 200); + return g_is_writable(&t, path); } static BOOL is_dynarun_name(const WCHAR *p) @@ -409,13 +414,13 @@ static BOOL is_dynarun_name(const WCHAR *p) typedef DWORD (WINAPI *GetFinalPathNameByHandleW_t)(HANDLE, LPWSTR, DWORD, DWORD); // The elevated stage does not trust the DynaRun path it was given on its command line. It must be an absolute path on a -// fixed drive, named DynaRun V3.exe, an existing file. g_exe_trusted says whether what the elevated stage writes there +// fixed drive, named DynaRun V3.exe, an existing file. t->exe_trusted says whether what the elevated stage writes there // (manifest), starts (the borrowed time stamp) and registers (COM servers in that folder) is safe: no link in the path, and -// neither the folder nor the exe changeable by ordinary users. FALSE: the path is unusable; g_exe_trusted FALSE only: skip those steps. -static BOOL check_exe(void) +// neither the folder nor the exe changeable by ordinary users. FALSE: the path is unusable; exe_trusted FALSE only: skip those steps. +static BOOL check_exe(trust_ctx *t) { WCHAR root[4], fin[MAX_PATH + 8], dir[MAX_PATH], *e; HANDLE h; GetFinalPathNameByHandleW_t f; DWORD n; - g_exe_trusted = FALSE; + t->exe_trusted = FALSE; if (!g_exe[0] || lstrlenW(g_exe) >= MAX_PATH - 1 || !is_dynarun_name(g_exe)) return FALSE; if (!((g_exe[0] >= 'A' && g_exe[0] <= 'Z') || (g_exe[0] >= 'a' && g_exe[0] <= 'z')) || g_exe[1] != ':' || g_exe[2] != '\\') return FALSE; lstrcpynW(root, g_exe, 4); @@ -432,7 +437,7 @@ static BOOL check_exe(void) // with GetFinalPathNameByHandleW (Vista and later) the final path must be the path given: no link anywhere in it if (f && (!n || n >= MAX_PATH + 8 || fin[0] != '\\' || fin[1] != '\\' || fin[2] != '?' || fin[3] != '\\' || lstrcmpiW(fin + 4, g_exe))) return TRUE; lstrcpyW(dir, g_exe); for (e = dir + lstrlenW(dir); e > dir && *e != '\\'; e--); *e = 0; - g_exe_trusted = !g_is_writable(dir) && !g_is_writable(g_exe); + t->exe_trusted = !g_is_writable(t, dir) && !g_is_writable(t, g_exe); return TRUE; } @@ -769,6 +774,19 @@ typedef int (WINAPI *MsiGetComponentPathW_t)(LPCWSTR, LPCWSTR, LPWSTR, DWORD *); static MsiGetShortcutTargetW_t pMsiGetShortcutTarget; static MsiGetComponentPathW_t pMsiGetComponentPath; +// Looks up the two MSI functions, once (more calls do nothing). msi.dll is loaded by its full path in the system folder (load_system_dll), never +// searched for. Both pointers stay NULL when msi.dll or a function is missing: classify then skips its check of advertised shortcuts. +static void msi_api_init(void) +{ + static BOOL done; HMODULE msi; + if (done) return; + done = TRUE; + if (!(msi = load_system_dll(L"msi.dll"))) return; + pMsiGetShortcutTarget = (MsiGetShortcutTargetW_t)GetProcAddress(msi, "MsiGetShortcutTargetW"); + pMsiGetComponentPath = (MsiGetComponentPathW_t)GetProcAddress(msi, "MsiGetComponentPathW"); + if (!pMsiGetComponentPath) pMsiGetShortcutTarget = NULL; +} + static IShellLinkW *load_link(const WCHAR *p) { IShellLinkW *sl; IPersistFile *pf; HRESULT hr = E_FAIL; @@ -799,6 +817,7 @@ static int classify(const WCHAR *p) if (!lstrcmpiW(t, g_launcher) || launcher_path_like(t)) return LNK_OURS; if (!lstrcmpiW(t, g_exe)) return LNK_DYNARUN; // the original setup is an MSI: its shortcuts are "advertised" and point to the product, not the exe + msi_api_init(); if (pMsiGetShortcutTarget && !pMsiGetShortcutTarget(p, prod, feat, comp) && pMsiGetComponentPath(prod, comp, t, &n) >= 0 && !lstrcmpiW(t, g_exe)) r = LNK_DYNARUN; return r; @@ -1030,12 +1049,6 @@ static BOOL fix_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *v static int fix_shortcuts(BOOL common, HKEY backup) // returns the number of DynaRun shortcuts on the Desktop { fix_ctx c; - HMODULE msi = load_system_dll(L"msi.dll"); - if (msi) { - pMsiGetShortcutTarget = (MsiGetShortcutTargetW_t)GetProcAddress(msi, "MsiGetShortcutTargetW"); - pMsiGetComponentPath = (MsiGetComponentPathW_t)GetProcAddress(msi, "MsiGetComponentPathW"); - if (!pMsiGetComponentPath) pMsiGetShortcutTarget = NULL; - } c.backup = backup; c.desk = 0; shortcut_folders(SFU_FIX, common ? SF_COMMON : SF_USER, fix_folder, &c); return c.desk; @@ -1502,11 +1515,11 @@ BOOL valid_user_sid(const WCHAR *sid) static int machine_install_locked(const install_opts *o, const WCHAR *sid) { static WCHAR p[MAX_PATH + 16]; - HKEY k; + HKEY k; trust_ctx t; if (!valid_user_sid(sid)) { error2(o, T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } - lstrcpynW(g_user_sid, sid, 200); + zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); if (still_running(o, L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since - if (!check_exe()) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } + if (!check_exe(&t)) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } CreateDirectoryW(g_dir, NULL); if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { @@ -1532,15 +1545,15 @@ static int machine_install_locked(const install_opts *o, const WCHAR *sid) driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) if (!app_key_create(APP_MACHINE, &k)) return RC_FAIL; set_str(k, L"InstallDir", g_dir); - if (g_exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use + if (t.exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use else RegDeleteValueW(k, L"DynaRunExe"); touch_report(o, recover_exe_time()); // a time stamp an earlier run could not put back // the per-user COM registrations are not copied to HKLM any more (records of older versions are left alone) - if (!g_exe_trusted) setup_log(L"DynaRun's folder or exe can be changed by ordinary users (or its path contains a link): no manifest was written"); + if (!t.exe_trusted) setup_log(L"DynaRun's folder or exe can be changed by ordinary users (or its path contains a link): no manifest was written"); // garbled text under "Beta: Use Unicode UTF-8": external manifest with activeCodePage=Legacy - if (GetACP() == CP_UTF8 && g_exe_trusted && !exists(cat3(p, g_exe, L".manifest", NULL)) && extract(3, p)) { + if (GetACP() == CP_UTF8 && t.exe_trusted && !exists(cat3(p, g_exe, L".manifest", NULL)) && extract(3, p)) { set_dword(k, L"ManifestInstalled", 1); touch_report(o, touch(g_exe)); } @@ -1645,12 +1658,13 @@ static void drop_policy_of(const WCHAR *sid) static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) { static WCHAR p[MAX_PATH + 16], polsid[200]; static const WCHAR KEEP[] = L"com-copied.txt\0com-backup\0com-skipped.txt\0setup.log\0"; // the record of the COM copies stays - HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; + HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; trust_ctx t; if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } - if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(g_user_sid, skip_sid, 200); else skip_sid = NULL; + zero(&t, sizeof(t)); + if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(t.user_sid, skip_sid, 200); else skip_sid = NULL; if (o->close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open if (!g_exe[0]) locate_dynarun(); // not recorded (an untrusted path is not): the default folders - check_exe(); // g_exe is from HKLM; g_exe_trusted says whether to run it + check_exe(&t); // g_exe is from HKLM; t.exe_trusted says whether to run it if (touch_report(o, recover_exe_time()) == TOUCH_STUCK) stuck = TRUE; restore_shortcuts(HKEY_LOCAL_MACHINE, APPKEY); unretarget_shell(TRUE); @@ -1659,7 +1673,7 @@ static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) drop_policy_of(skip_sid); drop_policy_of(polsid); unretarget_all_users(skip_sid); if (app_key_open(APP_MACHINE, KEY_READ, &k)) { RegQueryValueExW(k, L"ManifestInstalled", NULL, NULL, (BYTE *)&man, &n); RegCloseKey(k); } - if (man == 1 && g_exe[0] && !g_exe_trusted && exists(cat3(p, g_exe, L".manifest", NULL))) + if (man == 1 && g_exe[0] && !t.exe_trusted && exists(cat3(p, g_exe, L".manifest", NULL))) setup_log(L"the manifest next to DynaRun V3.exe was left in place: DynaRun's folder or exe can be changed by ordinary users, so nothing is deleted or scheduled for deletion there"); else if (man == 1 && g_exe[0] && exists(cat3(p, g_exe, L".manifest", NULL))) { if (!manifest_is_ours(p)) setup_log(L"the manifest next to DynaRun V3.exe is not the file DynaRunFix wrote (it was changed): left in place"); @@ -1673,7 +1687,7 @@ static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); } } - if (deleted) { if (g_exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } + if (deleted) { if (t.exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } // Before the key goes: the driver part ends a waiting task and resumes BitLocker if DynaRunFix suspended it (the flag is in the key). // An installed driver stays. driver_machine_uninstall(); @@ -2039,11 +2053,12 @@ static int uninstall(const install_opts *given) // user's shortcuts, Run value and pins still work with the installed launcher. if (is_admin()) rc = machine_uninstall(o, sid); else { + trust_ctx t; // What runs elevated is the installed program in Program Files (administrators-only folder), never a copy in a folder the user can // write. Only when there is no installed copy (a broken install) is this file itself run, if it is not one of our %TEMP% copies. - lstrcpynW(g_user_sid, sid, 200); + zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); if (exists(p)) { - if (g_is_writable(g_dir) || g_is_writable(p)) { + if (g_is_writable(&t, g_dir) || g_is_writable(&t, p)) { error2(o, T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); return RC_FAIL; diff --git a/tests/promote_com.h b/tests/promote_com.h index 0360ae5..b5fadd4 100644 --- a/tests/promote_com.h +++ b/tests/promote_com.h @@ -45,7 +45,7 @@ static int com_roots(WCHAR roots[][MAX_PATH]) { WCHAR *e; int n = 0; g_com_nfolder = 0; - if (g_exe_trusted && g_exe[0]) { + if (g_tc.exe_trusted && g_exe[0]) { lstrcpyW(roots[n], g_exe); for (e = roots[n] + lstrlenW(roots[n]); e > roots[n] && *e != '\\'; e--); *e = 0; if (roots[n][0]) g_com_nfolder = ++n; } @@ -112,7 +112,7 @@ static BOOL com_path_ok(const WCHAR *raw, BOOL typelib, WCHAR roots[][MAX_PATH], CloseHandle(h); if (!i) { *why = L"not a disk file"; return FALSE; } lstrcpyW(par, lng); for (s = par + lstrlenW(par); s > par && *s != '\\'; s--); *s = 0; - if (g_is_writable(lng) || g_is_writable(par)) { *why = L"the file or its folder can be changed by ordinary users"; return FALSE; } + if (g_is_writable(&g_tc, lng) || g_is_writable(&g_tc, par)) { *why = L"the file or its folder can be changed by ordinary users"; return FALSE; } return TRUE; } diff --git a/tests/test_driver.c b/tests/test_driver.c index 0d4536f..72f7dc9 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -487,9 +487,9 @@ static void test_misc(void) } else wprintf(L"info: could not create the protected folder here (%lu): ACL checks skipped\n", GetLastError()); } -static BOOL fake_ok(const WCHAR *p) { (void)p; return FALSE; } // "not user-writable" -static BOOL fake_bad(const WCHAR *p) { (void)p; return TRUE; } -static BOOL fake_bad_exe(const WCHAR *p) { int n = lstrlenW(p); return n > 4 && !lstrcmpiW(p + n - 4, L".exe"); } // only a file ending in .exe +static BOOL fake_ok(const trust_ctx *t, const WCHAR *p) { (void)t; (void)p; return FALSE; } // "not user-writable" +static BOOL fake_bad(const trust_ctx *t, const WCHAR *p) { (void)t; (void)p; return TRUE; } +static BOOL fake_bad_exe(const trust_ctx *t, const WCHAR *p) { int n = lstrlenW(p); return n > 4 && !lstrcmpiW(p + n - 4, L".exe"); } // only a file ending in .exe static void mb_file(const WCHAR *dir, const WCHAR *name) { @@ -504,7 +504,7 @@ static void test_make_base(void) { static const WCHAR SID[] = L"S-1-5-21-1-2-3-1001"; WCHAR b[MAX_PATH], f[MAX_PATH], g[MAX_PATH]; HANDLE h; - BOOL (*saved)(const WCHAR *) = g_is_writable; + BOOL (*saved)(const trust_ctx *, const WCHAR *) = g_is_writable; g_is_writable = fake_ok; wsprintfW(b, L"%s\\mb_new", g_base); CHECK(make_base(b, FALSE, SID) && exists(b), L"no folder yet: created"); diff --git a/tests/test_hardening.c b/tests/test_hardening.c index 2ca03ea..b8036d4 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -20,6 +20,7 @@ static BOOL WINAPI test_ShellExecuteExW(SHELLEXECUTEINFOW *se) } #define ShellExecuteExW test_ShellExecuteExW #include "../src/setup.c" +static trust_ctx g_tc; // the user SID and the exe trust of the checks below (a stage of the installer makes its own) #include "promote_com.h" #include @@ -61,7 +62,7 @@ static void sh(const WCHAR *cmdline) } } -static BOOL fake_writable(const WCHAR *p) { return wcsstr(p, L"\\writable") != NULL; } +static BOOL fake_writable(const trust_ctx *t, const WCHAR *p) { (void)t; return wcsstr(p, L"\\writable") != NULL; } static void setup_tree(void) { @@ -90,17 +91,17 @@ static void setup_tree(void) static void test_user_writable(void) { WCHAR c[1000], sid[200], pf[MAX_PATH]; - CHECK(!user_writable(L"C:\\Windows\\System32\\kernel32.dll"), L"kernel32.dll is not user-writable"); - CHECK(!user_writable(L"C:\\Windows\\System32"), L"System32 is not user-writable"); - CHECK(user_writable(L"C:\\this\\does\\not\\exist.dll"), L"a path that cannot be read counts as writable"); - user_sid(sid, 200); lstrcpyW(g_user_sid, sid); + CHECK(!user_writable(&g_tc, L"C:\\Windows\\System32\\kernel32.dll"), L"kernel32.dll is not user-writable"); + CHECK(!user_writable(&g_tc, L"C:\\Windows\\System32"), L"System32 is not user-writable"); + CHECK(user_writable(&g_tc, L"C:\\this\\does\\not\\exist.dll"), L"a path that cannot be read counts as writable"); + user_sid(sid, 200); lstrcpyW(g_tc.user_sid, sid); // an Everyone ACE, whoever owns the folder wsprintfW(c, L"icacls \"%s\\sub\" /grant *S-1-1-0:(OI)(CI)M >nul", g_root); sh(c); - { WCHAR s[MAX_PATH]; wsprintfW(s, L"%s\\sub", g_root); CHECK(user_writable(s), L"folder with an Everyone-modify ACE is writable"); } + { WCHAR s[MAX_PATH]; wsprintfW(s, L"%s\\sub", g_root); CHECK(user_writable(&g_tc, s), L"folder with an Everyone-modify ACE is writable"); } // the folder of an installed DynaRun, read only: printed, not asserted if (SHGetSpecialFolderPathW(NULL, pf, CSIDL_PROGRAM_FILESX86, FALSE)) { lstrcatW(pf, L"\\Dyna Pro Dynamometers"); - if (exists(pf)) wprintf(L"info: installed DynaRun folder: user_writable = %d\n", user_writable(pf)); + if (exists(pf)) wprintf(L"info: installed DynaRun folder: user_writable = %d\n", user_writable(&g_tc, pf)); } } @@ -148,7 +149,7 @@ static void test_paths(void) for (i = 0; i < 590; i++) longp[i] = 'a'; longp[590] = 0; wsprintfW(r, L"%s\\%s.ocx", g_root, longp); CHECK(!com_path_ok(r, FALSE, roots, 2, &why), L"path far too long"); // DynaRun's folder not trusted: only the system roots count (com_roots) - g_com_nfolder = 0; g_exe_trusted = FALSE; g_exe[0] = 0; + g_com_nfolder = 0; g_tc.exe_trusted = FALSE; g_exe[0] = 0; { WCHAR rr[3][MAX_PATH]; int n = com_roots(rr); CHECK(n >= 1 && under(rr[0], L"C:\\Windows") == under(rr[0], L"C:\\Windows") && g_com_nfolder == 0, L"no DynaRun root when untrusted"); } g_com_nfolder = 1; // the real check (not the fake): a file the current user created is not trusted by user_writable's owner rule unless elevated @@ -379,20 +380,20 @@ static void test_check_exe(void) wsprintfW(bin, L"%s\\bin", g_base); g_is_writable = fake_writable; wsprintfW(g_exe, L"%s\\DynaRun V3.exe", bin); - CHECK(check_exe() && g_exe_trusted, L"a good exe in a folder users cannot change: trusted"); + CHECK(check_exe(&g_tc) && g_tc.exe_trusted, L"a good exe in a folder users cannot change: trusted"); g_is_writable = user_writable; - CHECK(check_exe() && (!g_exe_trusted || is_admin()), L"with the real ACL check a user-owned folder is not trusted (unless elevated)"); + CHECK(check_exe(&g_tc) && (!g_tc.exe_trusted || is_admin()), L"with the real ACL check a user-owned folder is not trusted (unless elevated)"); g_is_writable = fake_writable; - wsprintfW(g_exe, L"%s\\Other.exe", bin); CHECK(!check_exe(), L"another file name is refused"); - wsprintfW(g_exe, L"%s\\nothere\\DynaRun V3.exe", g_base); CHECK(!check_exe(), L"a missing exe is refused"); - g_exe[0] = 0; CHECK(!check_exe(), L"an empty path is refused"); - lstrcpyW(g_exe, L"DynaRun V3.exe"); CHECK(!check_exe(), L"a relative path is refused"); - lstrcpyW(g_exe, L"\\\\localhost\\c$\\x\\DynaRun V3.exe"); CHECK(!check_exe(), L"a UNC path is refused"); + wsprintfW(g_exe, L"%s\\Other.exe", bin); CHECK(!check_exe(&g_tc), L"another file name is refused"); + wsprintfW(g_exe, L"%s\\nothere\\DynaRun V3.exe", g_base); CHECK(!check_exe(&g_tc), L"a missing exe is refused"); + g_exe[0] = 0; CHECK(!check_exe(&g_tc), L"an empty path is refused"); + lstrcpyW(g_exe, L"DynaRun V3.exe"); CHECK(!check_exe(&g_tc), L"a relative path is refused"); + lstrcpyW(g_exe, L"\\\\localhost\\c$\\x\\DynaRun V3.exe"); CHECK(!check_exe(&g_tc), L"a UNC path is refused"); wsprintfW(c, L"mklink /J \"%s\\binlink\" \"%s\" >nul", g_base, bin); sh(c); wsprintfW(g_exe, L"%s\\binlink\\DynaRun V3.exe", g_base); - CHECK(check_exe() && !g_exe_trusted, L"a path through a junction is usable but not trusted"); + CHECK(check_exe(&g_tc) && !g_tc.exe_trusted, L"a path through a junction is usable but not trusted"); h = CreateFileW(L"C:\\Windows\\System32\\hostname.exe", 0, 7, NULL, OPEN_EXISTING, 0, NULL); CloseHandle(h); - g_exe[0] = 0; g_exe_trusted = FALSE; + g_exe[0] = 0; g_tc.exe_trusted = FALSE; } /* ---------- 6. delete_tree ---------- */ From 487e6c7ba28f16123495cb3bf7693933586bfb86 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:25:20 +0800 Subject: [PATCH 18/41] Split setup.c into topic files included by setup.c (one compilation unit) src\setup.c keeps the header comment, the system includes and the entry point; the rest moved unchanged, in the original order, into src\setup\*.h (defs, util, stage, trust, exetime, procs, shortcuts, taskbar, users, machine, elevate, msi_stage, userstage, uninstall, cmdline). They are fragments of the same translation unit: build.cmd and the tests still compile and include only setup.c. Object code of setup.c is identical before and after. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 2172 +---------------------------------------- src/setup/cmdline.h | 116 +++ src/setup/defs.h | 40 + src/setup/elevate.h | 64 ++ src/setup/exetime.h | 102 ++ src/setup/machine.h | 206 ++++ src/setup/msi_stage.h | 95 ++ src/setup/procs.h | 224 +++++ src/setup/shortcuts.h | 418 ++++++++ src/setup/stage.h | 30 + src/setup/taskbar.h | 224 +++++ src/setup/trust.h | 80 ++ src/setup/uninstall.h | 117 +++ src/setup/users.h | 94 ++ src/setup/userstage.h | 120 +++ src/setup/util.h | 240 +++++ 16 files changed, 2188 insertions(+), 2154 deletions(-) create mode 100644 src/setup/cmdline.h create mode 100644 src/setup/defs.h create mode 100644 src/setup/elevate.h create mode 100644 src/setup/exetime.h create mode 100644 src/setup/machine.h create mode 100644 src/setup/msi_stage.h create mode 100644 src/setup/procs.h create mode 100644 src/setup/shortcuts.h create mode 100644 src/setup/stage.h create mode 100644 src/setup/taskbar.h create mode 100644 src/setup/trust.h create mode 100644 src/setup/uninstall.h create mode 100644 src/setup/users.h create mode 100644 src/setup/userstage.h create mode 100644 src/setup/util.h diff --git a/src/setup.c b/src/setup.c index fc0d603..39b46cc 100644 --- a/src/setup.c +++ b/src/setup.c @@ -55,2160 +55,24 @@ #include "setup.h" #include "version.h" -#ifndef SLDF_RUNAS_USER -#define SLDF_RUNAS_USER 0x2000 -#endif -#define WOW KEY_WOW64_64KEY // DynaRun's COM keys: physical view, like 64-bit reg.exe sees them -#define WIDEN2(x) L##x -#define WIDEN(x) WIDEN2(x) - -static const WCHAR APPKEY[] = L"SOFTWARE\\DynaRunFix"; -static const WCHAR USERKEY[] = L"Software\\DynaRunFix"; -static const WCHAR RUNKEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; -static const WCHAR POLKEY[] = L"Software\\Policies\\Microsoft\\Windows\\Explorer"; -static const WCHAR APPID[] = L"DynaRunFix.DynaRunV3"; // AppUserModelID of the launcher shortcuts and of DynaRun (dynafix.c) -static const PROPERTYKEY PKEY_AUMID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 5 }; -// technique described by Gee Law (geelaw.blog/entries/msedge-pins/), also used by Firefox's PinToTaskbar plugin (MPL 2.0): calling the -// undocumented IPinnedList3::Modify of Explorer's taskband. The GUIDs and the vtable layout are facts of that interface; the code is -// our own (compared with Firefox's PinToTaskbar.cpp: different structure and wording, nothing copied). -static const GUID CLSID_TaskbandPin = { 0x90aa3a4e, 0x1cba, 0x4233, { 0xb8, 0xbb, 0x53, 0x57, 0x73, 0xd4, 0x84, 0x49 } }; -static const GUID IID_IPinnedList3 = { 0x0dd79ae2, 0xd156, 0x45d4, { 0x9e, 0xeb, 0x3b, 0x54, 0x97, 0x69, 0xe9, 0x40 } }; -typedef struct IPinnedList3 { const struct { - HRESULT (STDMETHODCALLTYPE *QueryInterface)(struct IPinnedList3 *, REFIID, void **); - ULONG (STDMETHODCALLTYPE *AddRef)(struct IPinnedList3 *); - ULONG (STDMETHODCALLTYPE *Release)(struct IPinnedList3 *); - void *others[13]; - HRESULT (STDMETHODCALLTYPE *Modify)(struct IPinnedList3 *, PCIDLIST_ABSOLUTE unpin, PCIDLIST_ABSOLUTE pin, int caller); -} *lpVtbl; } IPinnedList3; -static const WCHAR UNINSTKEY[] = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\DynaRunFix"; -static const WCHAR DYNARUN_REL[] = L"\\Dyna Pro Dynamometers\\DynaRun V3.exe"; - -BOOL g_zh; -int g_drvmode; // DRV_MODE_*: the wizard starts at the driver pages (driver.c, wizard.c) -HWND g_hwnd; -WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; -// What the ACL decisions of a stage depend on, passed to the functions that decide (never kept in a global): each stage makes one. -typedef struct { - WCHAR user_sid[200]; // the user who started the setup (elevated stage: from its command line); "" = none known - BOOL exe_trusted; // set by check_exe: DynaRun's exe and folder cannot be changed by ordinary users -} trust_ctx; -#define TITLE T(L"DynaRunFix Setup", L"DynaRunFix 安裝程式") - -/* ---------- DLLs: only from the system folder ---------- */ - -// msi.dll and wininet.dll are delay-loaded (build.cmd: /DELAYLOAD) and loaded here by their full path in the system folder, never -// searched for (a planted msi.dll / wininet.dll next to the setup, e.g. in Downloads, is not loaded). The linker flag -// /DEPENDENTLOADFLAG:0x800 and SetDefaultDllDirectories (WinMainCRTStartup) do the same for every other DLL on systems that have them -// (Windows 7 with KB2533623 and later); on older systems the delay-load hook and the full paths below are what protects. -static HMODULE load_system_dll(const WCHAR *name) -{ - WCHAR p[MAX_PATH]; UINT n = GetSystemDirectoryW(p, MAX_PATH - 40); - if (!n || n >= MAX_PATH - 40 || lstrlenW(name) > 30) return NULL; - lstrcatW(p, L"\\"); lstrcatW(p, name); - return LoadLibraryW(p); -} - -static FARPROC WINAPI dll_hook(unsigned notify, PDelayLoadInfo info) -{ - WCHAR n[40]; - if (notify != dliNotePreLoadLibrary || !info || !info->szDll) return NULL; - if (MultiByteToWideChar(CP_ACP, 0, info->szDll, -1, n, 40) <= 0) return NULL; - return (FARPROC)load_system_dll(n); // NULL: the delay-load helper then raises its usual error -} -const PfnDliHook __pfnDliNotifyHook2 = dll_hook; - -/* ---------- small helpers (no C runtime) ---------- */ - -void zero(void *p, SIZE_T n) { volatile char *z = (volatile char *)p; while (n--) *z++ = 0; } -void install_opts_default(install_opts *o) { zero(o, sizeof(*o)); o->pin = TRUE; o->autostart = TRUE; } -void *alloc(SIZE_T n) { return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, n); } -void release(void *p) { if (p) HeapFree(GetProcessHeap(), 0, p); } -static WCHAR *cat3(WCHAR *d, const WCHAR *a, const WCHAR *b, const WCHAR *c) -{ lstrcpyW(d, a); lstrcatW(d, b); if (c) lstrcatW(d, c); return d; } -BOOL exists(const WCHAR *p) { return GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES; } - -BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name) -{ - if (lstrlenW(dir) + lstrlenW(name) + 2 >= limit) return FALSE; - if (out != dir) lstrcpyW(out, dir); - lstrcatW(out, L"\\"); lstrcatW(out, name); - return TRUE; -} - -int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code) -{ - STARTUPINFOW si; PROCESS_INFORMATION pi; int r = PROC_EXITED; - zero(&si, sizeof(si)); si.cb = sizeof(si); - if (hide) { si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } - if (!CreateProcessW(exe, cmd, NULL, NULL, FALSE, flags, NULL, cwd, &si, &pi)) return PROC_NOSTART; - if (WaitForSingleObject(pi.hProcess, ms) == WAIT_OBJECT_0 || ms == INFINITE) GetExitCodeProcess(pi.hProcess, code); - else { TerminateProcess(pi.hProcess, 1); r = PROC_TIMEOUT; } - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - return r; -} - -int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx) -{ - WIN32_FIND_DATAW fd; HANDLE f; int r = WALK_DONE; - if (lstrlenW(dir) + 3 > bufcch) return WALK_NOOPEN; - lstrcpyW(buf, dir); lstrcatW(buf, L"\\*"); - if ((f = FindFirstFileW(buf, &fd)) == INVALID_HANDLE_VALUE) return WALK_NOOPEN; - do { - if (fd.cFileName[0] == '.' || !path_join(buf, limit, dir, fd.cFileName)) continue; - if (!fn(buf, &fd, ctx)) { r = WALK_STOPPED; break; } - } while (FindNextFileW(f, &fd)); - FindClose(f); - return r; -} - -BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k) -{ return !RegOpenKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, sam, k); } -BOOL app_key_create(BOOL machine, HKEY *k) -{ return !RegCreateKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, k, NULL); } -int msg(const WCHAR *text, UINT flags) { return MessageBoxW(g_hwnd, text, TITLE, flags | MB_SETFOREGROUND); } -static void info(const install_opts *o, const WCHAR *text) { if (!o->quiet) msg(text, MB_ICONINFORMATION); } -static void error2(const install_opts *o, const WCHAR *text, const WCHAR *what) -{ static WCHAR b[1024]; if (o->quiet) return; lstrcpynW(b, text, 600); if (what) { lstrcatW(b, L"\n\n"); lstrcatW(b, what); } msg(b, MB_ICONERROR); } - -BOOL is_admin(void) -{ - SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY; PSID sid; BOOL r = FALSE; - if (AllocateAndInitializeSid(&nt, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid)) { - if (!CheckTokenMembership(NULL, sid, &r)) r = FALSE; - FreeSid(sid); - } - return r; -} - -BOOL user_sid(WCHAR *out, int cch) -{ - HANDLE tok; static BYTE buf[256]; DWORD n; WCHAR *s; BOOL r = FALSE; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok)) return FALSE; - if (GetTokenInformation(tok, TokenUser, buf, sizeof(buf), &n) && ConvertSidToStringSidW(((TOKEN_USER *)buf)->User.Sid, &s)) { - lstrcpynW(out, s, cch); LocalFree(s); r = TRUE; - } - CloseHandle(tok); - return r; -} - -static void *read_file(const WCHAR *p, DWORD *n) -{ - HANDLE h = CreateFileW(p, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); void *d; - if (h == INVALID_HANDLE_VALUE) return NULL; - *n = GetFileSize(h, NULL); - d = alloc(*n + 1); - if (d && !ReadFile(h, d, *n, n, NULL)) { release(d); d = NULL; } - CloseHandle(h); - return d; -} - -// In-use files (dynafix.dll inside a running DynaRun) cannot be overwritten or deleted, but can be -// renamed; the renamed file goes away at the next restart. -static BOOL move_aside(const WCHAR *p) -{ - static WCHAR old[MAX_PATH + 32]; - wsprintfW(old, L"%s.%lu.old", p, GetTickCount()); - if (!MoveFileExW(p, old, MOVEFILE_REPLACE_EXISTING)) return FALSE; - // "delete at restart" is administrators-only: in the elevated stage the renamed file is registered for that; without - // the right, delete it right away if nothing holds it (a stale .old is all that can be left) - if (!MoveFileExW(old, NULL, MOVEFILE_DELAY_UNTIL_REBOOT)) DeleteFileW(old); - return TRUE; -} - -static BOOL write_file(const WCHAR *p, const void *d, DWORD n) -{ - HANDLE h = CreateFileW(p, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD w; BOOL ok; - if (h == INVALID_HANDLE_VALUE && exists(p) && move_aside(p)) - h = CreateFileW(p, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (h == INVALID_HANDLE_VALUE) return FALSE; - ok = WriteFile(h, d, n, &w, NULL) && w == n; - CloseHandle(h); - return ok; -} - -// Appends one line to a log. A link is never followed: a reparse point (symbolic link, junction) or a file with several hard links -// is refused, so the elevated stage cannot be made to write somewhere else through a file planted in %TEMP%. -static void append_line(const WCHAR *p, const WCHAR *line) -{ - static char u[4096]; int n = WideCharToMultiByte(CP_UTF8, 0, line, -1, u, sizeof(u) - 2, NULL, NULL); DWORD w; - BY_HANDLE_FILE_INFORMATION bi; - HANDLE h = CreateFileW(p, FILE_APPEND_DATA | FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, NULL); - if (h == INVALID_HANDLE_VALUE) return; - if (n <= 0 || !GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) || - bi.nNumberOfLinks > 1) { CloseHandle(h); return; } - u[n - 1] = '\r'; u[n] = '\n'; - WriteFile(h, u, n + 1, &w, NULL); - CloseHandle(h); -} - -static BOOL same_bytes(const WCHAR *p, const void *d, DWORD n) // the file on disk is exactly d -{ - DWORD got, i; BYTE *f = read_file(p, &got); BOOL r = f && got == n; - for (i = 0; r && i < n; i++) r = f[i] == ((const BYTE *)d)[i]; - release(f); - return r; -} - -BOOL same_file(const WCHAR *a, const WCHAR *b) -{ - DWORD n; void *d = read_file(a, &n); BOOL r = d && same_bytes(b, d, n); - release(d); - return r; -} - -static BOOL extract(int id, const WCHAR *p) // written and read back: the installed file is the payload -{ - HRSRC r = FindResourceW(NULL, MAKEINTRESOURCEW(id), (LPCWSTR)RT_RCDATA); const void *d; DWORD n; - if (!r) return FALSE; - d = LockResource(LoadResource(NULL, r)); n = SizeofResource(NULL, r); - return write_file(p, d, n) && same_bytes(p, d, n); -} - -static BOOL name_in(const WCHAR *list, const WCHAR *n) -{ - if (list) for (; *list; list += lstrlenW(list) + 1) if (!lstrcmpiW(list, n)) return TRUE; - return FALSE; -} - -// Deletes dir and everything in it, except the top-level names in keep (a multi-string; dir then stays). A link (junction, -// symbolic link) is removed itself and never followed. What is in use is renamed aside and goes at the next restart. -void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth) -{ - WCHAR *p; WIN32_FIND_DATAW fd; HANDLE f; DWORD a = GetFileAttributesW(dir); BOOL kept = FALSE; - if (a == INVALID_FILE_ATTRIBUTES) return; - if (a & FILE_ATTRIBUTE_REPARSE_POINT) { if (!RemoveDirectoryW(dir)) MoveFileExW(dir, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); return; } - if (depth > 40 || !(p = alloc(2 * MAX_PATH * sizeof(WCHAR)))) return; - if (lstrlenW(dir) + 3 < 2 * MAX_PATH) { - cat3(p, dir, L"\\*", NULL); - f = FindFirstFileW(p, &fd); - if (f != INVALID_HANDLE_VALUE) { - do { - if (!lstrcmpW(fd.cFileName, L".") || !lstrcmpW(fd.cFileName, L"..")) continue; - if ((depth == 0 && name_in(keep, fd.cFileName)) || !path_join(p, 2 * MAX_PATH, dir, fd.cFileName)) { kept = TRUE; continue; } // kept, or a path too long for the buffer - if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { if (!RemoveDirectoryW(p)) MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); } - else delete_tree(p, NULL, depth + 1); - } - else if (!DeleteFileW(p) && !move_aside(p)) MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); // locked: gone at the next restart - } while (FindNextFileW(f, &fd)); - FindClose(f); - } - } - if (!kept && !RemoveDirectoryW(dir)) MoveFileExW(dir, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); - release(p); -} - -static DWORD reg_str(HKEY root, const WCHAR *path, const WCHAR *name, WCHAR *out, DWORD cch, REGSAM extra) -{ - HKEY k; DWORD type, n = (cch - 1) * sizeof(WCHAR); LONG r; - out[0] = 0; - if (RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | extra, &k)) return 0; - r = RegQueryValueExW(k, name, NULL, &type, (BYTE *)out, &n); - RegCloseKey(k); - if (r || (type != REG_SZ && type != REG_EXPAND_SZ)) { out[0] = 0; return 0; } - out[n / sizeof(WCHAR)] = 0; - return lstrlenW(out); -} - -static void set_str(HKEY k, const WCHAR *name, const WCHAR *v) -{ RegSetValueExW(k, name, 0, REG_SZ, (const BYTE *)v, (lstrlenW(v) + 1) * sizeof(WCHAR)); } -static void set_dword(HKEY k, const WCHAR *name, DWORD v) { RegSetValueExW(k, name, 0, REG_DWORD, (const BYTE *)&v, 4); } - -static BOOL contains_i(const WCHAR *s, const WCHAR *sub) -{ - int n = lstrlenW(sub); - for (; lstrlenW(s) >= n; s++) if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, s, n, sub, n) == CSTR_EQUAL) return TRUE; - return FALSE; -} - -// The real Windows version (GetVersion lies to programs without a matching manifest). -static void os_ver(DWORD *major, DWORD *minor, DWORD *build) -{ - typedef LONG (WINAPI *RtlGetVersion_t)(OSVERSIONINFOW *); - RtlGetVersion_t f = (RtlGetVersion_t)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"); OSVERSIONINFOW v; - zero(&v, sizeof(v)); v.dwOSVersionInfoSize = sizeof(v); - if (f) f(&v); - *major = v.dwMajorVersion; *minor = v.dwMinorVersion; *build = v.dwBuildNumber; -} - -DWORD os_build(void) { DWORD a, b, c; os_ver(&a, &b, &c); return c; } - -/* ---------- elevated stage: one at a time, trust checks, the borrowed time stamp ---------- */ - -// The elevated stages (install, uninstall, MSI) of two setups must not interleave. Recursive for the owning thread. -static HANDLE g_stage_mx; static int g_stage_depth; - -static BOOL stage_lock(void) -{ - DWORD r; - if (!g_stage_mx && !(g_stage_mx = CreateMutexW(NULL, FALSE, L"Global\\DynaRunFix-machine-stage"))) return FALSE; - r = WaitForSingleObject(g_stage_mx, 60000); - if (r != WAIT_OBJECT_0 && r != WAIT_ABANDONED) return FALSE; // an abandoned mutex (a setup that was killed) is ours now - g_stage_depth++; - return TRUE; -} - -static void stage_unlock(void) { if (g_stage_depth > 0) { g_stage_depth--; ReleaseMutex(g_stage_mx); } } - -static const WCHAR *busy_text(void) -{ return T(L"Another DynaRunFix setup is running. Wait until it finishes and try again.", L"另一個 DynaRunFix 安裝程式正在執行,請等它完成後再試一次。"); } - -// A line in \setup.log (only the elevated stage writes there; the folder is administrators-only) -static void setup_log(const WCHAR *line) -{ - static WCHAR p[MAX_PATH]; - if (g_dir[0] && exists(g_dir)) append_line(cat3(p, g_dir, L"\\setup.log", NULL), line); -} - -static void warn(const install_opts *o, const WCHAR *text) { setup_log(text); if (!o->quiet) msg(text, MB_ICONWARNING); } - -static BOOL sid_in(PSID s, const WCHAR *const *list) -{ - PSID t; BOOL r = FALSE; int i; - for (i = 0; !r && list[i]; i++) if (ConvertStringSidToSidW(list[i], &t)) { r = EqualSid(s, t); LocalFree(t); } - return r; -} - -#define WRITE_MASK (FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_DELETE_CHILD | DELETE | WRITE_DAC | WRITE_OWNER | GENERIC_WRITE | GENERIC_ALL) - -// TRUE when ordinary users could change what is at path (or when that cannot be told). Not user-writable: owned by SYSTEM, -// the Administrators or TrustedInstaller, and no ACE that applies to the object itself gives Everyone, Users, Authenticated Users, -// Interactive, Power Users or the user who started the setup a write, delete or permission right. -static BOOL user_writable(const trust_ctx *t, const WCHAR *path) -{ - static const WCHAR *const owners[] = { L"S-1-5-18", L"S-1-5-32-544", L"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", NULL }; - static const WCHAR *const others[] = { L"S-1-1-0", L"S-1-5-32-545", L"S-1-5-11", L"S-1-5-4", L"S-1-5-32-547", NULL }; - const WCHAR *const me[2] = { t->user_sid, NULL }; - PSID owner = NULL; PACL dacl = NULL; PSECURITY_DESCRIPTOR sd = NULL; BOOL bad = TRUE; DWORD i; ACCESS_ALLOWED_ACE *a; - if (GetNamedSecurityInfoW((LPWSTR)path, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner, NULL, &dacl, NULL, &sd)) return TRUE; - if (owner && dacl && sid_in(owner, owners)) { // no DACL = full access for everyone: bad - bad = FALSE; - for (i = 0; i < dacl->AceCount && !bad; i++) { - if (!GetAce(dacl, i, (void **)&a)) { bad = TRUE; break; } - if (a->Header.AceType != ACCESS_ALLOWED_ACE_TYPE || (a->Header.AceFlags & INHERIT_ONLY_ACE) || !(a->Mask & WRITE_MASK)) continue; - if (sid_in((PSID)&a->SidStart, others) || (t->user_sid[0] && sid_in((PSID)&a->SidStart, me))) bad = TRUE; - } - } - LocalFree(sd); - return bad; -} - -static BOOL (*g_is_writable)(const trust_ctx *t, const WCHAR *path) = user_writable; // the tests replace this - -// driver.c: may ordinary users (and the user who started the setup, sid; NULL: no such user is known) change what is at path? -BOOL path_user_writable(const WCHAR *path, const WCHAR *sid) -{ - trust_ctx t; - zero(&t, sizeof(t)); - if (sid) lstrcpynW(t.user_sid, sid, 200); - return g_is_writable(&t, path); -} - -static BOOL is_dynarun_name(const WCHAR *p) -{ - const WCHAR *n = p + lstrlenW(p); - while (n > p && n[-1] != '\\') n--; - return !lstrcmpiW(n, L"DynaRun V3.exe"); -} - -typedef DWORD (WINAPI *GetFinalPathNameByHandleW_t)(HANDLE, LPWSTR, DWORD, DWORD); - -// The elevated stage does not trust the DynaRun path it was given on its command line. It must be an absolute path on a -// fixed drive, named DynaRun V3.exe, an existing file. t->exe_trusted says whether what the elevated stage writes there -// (manifest), starts (the borrowed time stamp) and registers (COM servers in that folder) is safe: no link in the path, and -// neither the folder nor the exe changeable by ordinary users. FALSE: the path is unusable; exe_trusted FALSE only: skip those steps. -static BOOL check_exe(trust_ctx *t) -{ - WCHAR root[4], fin[MAX_PATH + 8], dir[MAX_PATH], *e; HANDLE h; GetFinalPathNameByHandleW_t f; DWORD n; - t->exe_trusted = FALSE; - if (!g_exe[0] || lstrlenW(g_exe) >= MAX_PATH - 1 || !is_dynarun_name(g_exe)) return FALSE; - if (!((g_exe[0] >= 'A' && g_exe[0] <= 'Z') || (g_exe[0] >= 'a' && g_exe[0] <= 'z')) || g_exe[1] != ':' || g_exe[2] != '\\') return FALSE; - lstrcpynW(root, g_exe, 4); - if (GetDriveTypeW(root) != DRIVE_FIXED) return FALSE; - h = CreateFileW(g_exe, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); - if (h == INVALID_HANDLE_VALUE) return FALSE; - f = (GetFinalPathNameByHandleW_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetFinalPathNameByHandleW"); - if (GetFileType(h) != FILE_TYPE_DISK) { CloseHandle(h); return FALSE; } - n = f ? f(h, fin, MAX_PATH + 8, 0) : 0; - CloseHandle(h); - // Windows XP has no GetFinalPathNameByHandleW: a junction in the path to DynaRun V3.exe cannot be detected there, only the - // ACL checks below apply (limitation; later Windows versions check for links) - if (!f) setup_log(L"Windows XP: links (junctions) in the path to DynaRun V3.exe cannot be detected"); - // with GetFinalPathNameByHandleW (Vista and later) the final path must be the path given: no link anywhere in it - if (f && (!n || n >= MAX_PATH + 8 || fin[0] != '\\' || fin[1] != '\\' || fin[2] != '?' || fin[3] != '\\' || lstrcmpiW(fin + 4, g_exe))) return TRUE; - lstrcpyW(dir, g_exe); for (e = dir + lstrlenW(dir); e > dir && *e != '\\'; e--); *e = 0; - t->exe_trusted = !g_is_writable(t, dir) && !g_is_writable(t, g_exe); - return TRUE; -} - -// Observed in testing: Windows keeps using the manifest state it saw at the exe's last start until it sees a different LastWriteTime -// on a process creation. So: read the LastWriteTime, set another value, let Windows see it by creating the exe's process suspended -// and terminating it at once (none of the exe's code runs), then set the original value back. The exe's content and its -// timestamps end up exactly as they were. (Setting the value and restoring it without the process creation in between does not -// make Windows read the manifest.) -// The original time is written to HKLM\SOFTWARE\DynaRunFix (ExeOrigTimePath, ExeOrigTime) before it is changed and removed -// after it has been put back and read back; recover_exe_time puts back what a run that was killed or failed left. -enum { TOUCH_OK, TOUCH_NOTDONE, TOUCH_STUCK }; // NOTDONE: nothing was changed. STUCK: the original time could not be put back -static const WCHAR ORIGPATH[] = L"ExeOrigTimePath", ORIGTIME[] = L"ExeOrigTime"; -static HKEY g_app_root = HKEY_LOCAL_MACHINE; static const WCHAR *g_app_key = APPKEY; // where the record is kept (the tests use a key of their own) - -static BOOL same_time(const FILETIME *a, const FILETIME *b) { return a->dwLowDateTime == b->dwLowDateTime && a->dwHighDateTime == b->dwHighDateTime; } - -static BOOL time_set_verified(HANDLE h, const FILETIME *t) // set, read back, retry a few times -{ - FILETIME c; int i; - for (i = 0; i < 5; i++) { - if (SetFileTime(h, NULL, NULL, t) && GetFileTime(h, NULL, NULL, &c) && same_time(&c, t)) return TRUE; - Sleep(300); - } - return FALSE; -} - -static HANDLE open_times(const WCHAR *p) -{ return CreateFileW(p, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); } - -static void forget_exe_time(HKEY k) { RegDeleteValueW(k, ORIGPATH); RegDeleteValueW(k, ORIGTIME); } - -static int recover_exe_time(void) -{ - HKEY k; static WCHAR path[MAX_PATH]; FILETIME saved, cur; DWORD n = sizeof(saved), type; HANDLE h; int rc = TOUCH_OK; - if (RegOpenKeyExW(g_app_root, g_app_key, 0, KEY_ALL_ACCESS, &k)) return TOUCH_OK; - if (reg_str(g_app_root, g_app_key, ORIGPATH, path, MAX_PATH, 0) && is_dynarun_name(path) && - !RegQueryValueExW(k, ORIGTIME, NULL, &type, (BYTE *)&saved, &n) && type == REG_BINARY && n == sizeof(saved)) { - if ((h = open_times(path)) == INVALID_HANDLE_VALUE) { if (!exists(path)) forget_exe_time(k); else rc = TOUCH_STUCK; } - else { - if (GetFileTime(h, NULL, NULL, &cur) && same_time(&cur, &saved)) forget_exe_time(k); - else if (time_set_verified(h, &saved)) { forget_exe_time(k); setup_log(L"restored the time stamp of DynaRun V3.exe that an earlier run had left changed"); } - else rc = TOUCH_STUCK; - CloseHandle(h); - } - } - RegCloseKey(k); - return rc; -} - -// The temporary time: now, but at least 4 s from the original (the file systems keep 2 s at the coarsest, FAT), on an even second. -// (After rounding down to an even second it is still more than 2 s away.) -static void pick_temp_time(const FILETIME *orig, FILETIME *out) -{ - ULARGE_INTEGER u, o; SYSTEMTIME st; - GetSystemTimeAsFileTime(out); - u.LowPart = out->dwLowDateTime; u.HighPart = out->dwHighDateTime; o.LowPart = orig->dwLowDateTime; o.HighPart = orig->dwHighDateTime; - if ((u.QuadPart > o.QuadPart ? u.QuadPart - o.QuadPart : o.QuadPart - u.QuadPart) < 40000000ULL) u.QuadPart = o.QuadPart + 100000000ULL; - out->dwLowDateTime = u.LowPart; out->dwHighDateTime = u.HighPart; - if (FileTimeToSystemTime(out, &st)) { st.wSecond &= ~1; st.wMilliseconds = 0; SystemTimeToFileTime(&st, out); } -} - -static int touch(const WCHAR *p) -{ - FILETIME orig, ft; HANDLE h; HKEY k; STARTUPINFOW si; PROCESS_INFORMATION pi; WCHAR *cmd; int rc = TOUCH_NOTDONE; - if (!p[0] || !is_dynarun_name(p) || !stage_lock()) return TOUCH_NOTDONE; - h = open_times(p); - if (h != INVALID_HANDLE_VALUE) { - if (GetFileTime(h, NULL, NULL, &orig) && !RegCreateKeyExW(g_app_root, g_app_key, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { - if (!RegSetValueExW(k, ORIGPATH, 0, REG_SZ, (const BYTE *)p, (lstrlenW(p) + 1) * sizeof(WCHAR)) && - !RegSetValueExW(k, ORIGTIME, 0, REG_BINARY, (const BYTE *)&orig, sizeof(orig))) { - pick_temp_time(&orig, &ft); - if (SetFileTime(h, NULL, NULL, &ft)) { - zero(&si, sizeof si); si.cb = sizeof si; - if ((cmd = alloc((MAX_PATH + 16) * sizeof(WCHAR)))) { - cat3(cmd, L"\"", p, L"\""); - if (CreateProcessW(p, cmd, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) { - TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 5000); - CloseHandle(pi.hThread); CloseHandle(pi.hProcess); - } - release(cmd); - } - if (time_set_verified(h, &orig)) { forget_exe_time(k); rc = TOUCH_OK; } - else rc = TOUCH_STUCK; // the record stays: the next install or uninstall puts it back - } else forget_exe_time(k); // the time was not changed - } else forget_exe_time(k); - RegCloseKey(k); - } - CloseHandle(h); - } - stage_unlock(); - return rc; -} - -// what the caller does with touch's result -static int touch_report(const install_opts *o, int t) -{ - if (t == TOUCH_STUCK) - warn(o, T(L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", - L"DynaRun V3.exe 的修改時間無法還原。已記錄在登錄檔,下次安裝或解除安裝 DynaRunFix 時會再還原。")); - else if (t == TOUCH_NOTDONE) setup_log(L"the manifest was not refreshed: DynaRun V3.exe could not be touched"); - return t; -} - - -/* ---------- paths ---------- */ - -static void init_paths(void) -{ - GetModuleFileNameW(NULL, g_self, MAX_PATH); - if (!SHGetSpecialFolderPathW(NULL, g_dir, CSIDL_PROGRAM_FILES, FALSE)) lstrcpyW(g_dir, L"C:\\Program Files"); - lstrcatW(g_dir, L"\\DynaRunFix"); - cat3(g_launcher, g_dir, L"\\DynaRunFix.exe", NULL); -} - -BOOL locate_dynarun(void) -{ - static const int csidl[] = { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES }; - WCHAR pf[MAX_PATH]; int i; - if (reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0) && exists(g_exe)) return TRUE; - // the program folders from the system (not from environment variables, which a process can set for the elevated stage) - for (i = 0; i < 2; i++) - if (SHGetSpecialFolderPathW(NULL, pf, csidl[i], FALSE) && lstrlenW(pf) + lstrlenW(DYNARUN_REL) < MAX_PATH && exists(cat3(g_exe, pf, DYNARUN_REL, NULL))) return TRUE; - g_exe[0] = 0; - return FALSE; -} - -/* ---------- programs that hold the files the setup replaces ---------- */ - -typedef BOOL (WINAPI *QueryFullProcessImageNameW_t)(HANDLE, DWORD, LPWSTR, PDWORD); - -static void process_path(DWORD pid, WCHAR *out, DWORD cch) // the exe of a process, or "" when it cannot be read -{ - static QueryFullProcessImageNameW_t q; static BOOL tried; HANDLE h; MODULEENTRY32W me; - out[0] = 0; - if (!tried) { tried = TRUE; q = (QueryFullProcessImageNameW_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "QueryFullProcessImageNameW"); } - // Vista and later: also for an elevated process when this one is not (PROCESS_QUERY_LIMITED_INFORMATION) - if (q && (h = OpenProcess(0x1000, FALSE, pid))) { - if (!q(h, 0, out, &cch)) out[0] = 0; - CloseHandle(h); - if (out[0]) return; - } - h = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); // XP: the first module is the exe - if (h == INVALID_HANDLE_VALUE) return; - me.dwSize = sizeof(me); - if (Module32FirstW(h, &me)) lstrcpynW(out, me.szExePath, cch); - CloseHandle(h); -} - -// A running DynaRun V3 has dynafix.dll (and under Locale Emulator LoaderDll.dll / LocaleEmulator.dll) -// of the install folder loaded. Windows lets the setup rename such files and put new ones in place, -// but that DynaRun keeps the old code, and "Start DynaRun" would only find it already running, so -// DynaRun is closed first (the user agrees in the wizard, or passes /close). -// What holds up the install (judge_proc): the DynaRun V3.exe that is the known one (g_exe; a DynaRun of another folder does not use -// our files), and the launcher and LEProc when they run from the install folder. A program whose exe path cannot be read counts as -// ours (better than a half-updated install). The setup closes only what it can tell is ours and that runs in its own session; the -// rest still holds up the install, but "close" would not help, and the wizard and the log say so. -typedef struct { DWORD pid; int bit; DWORD sess; int why; WCHAR path[MAX_PATH]; } runproc; -enum { JUDGE_SKIP, JUDGE_CLOSE, JUDGE_OTHERSESSION, JUDGE_NOPATH, JUDGE_NOEXE }; // not in the way / can be closed / cannot: why - -static BOOL same_path(const WCHAR *a, const WCHAR *b) // case-insensitive; 8.3 and long names of one file match -{ - WCHAR x[MAX_PATH], y[MAX_PATH]; DWORD m, n; - if (!a[0] || !b[0]) return FALSE; - if (!lstrcmpiW(a, b)) return TRUE; - m = GetLongPathNameW(a, x, MAX_PATH); n = GetLongPathNameW(b, y, MAX_PATH); - return m && m < MAX_PATH && n && n < MAX_PATH && !lstrcmpiW(x, y); -} - -// bit: RUN_* of the program; path: its exe ("" when unreadable); exe: the known DynaRun V3.exe ("" when not known yet); -// dir: the install folder with a trailing backslash -static int judge_proc(int bit, BOOL same_session, const WCHAR *path, const WCHAR *exe, const WCHAR *dir) -{ - int len = lstrlenW(dir); - if (bit == RUN_DYNARUN) { - if (!path[0]) return JUDGE_NOPATH; - if (!exe[0]) return JUDGE_NOEXE; - if (!same_path(path, exe)) return JUDGE_SKIP; - } else { - if (!path[0]) return JUDGE_NOPATH; - if (!(lstrlenW(path) > len && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, len, dir, len) == CSTR_EQUAL)) return JUDGE_SKIP; - } - return same_session ? JUDGE_CLOSE : JUDGE_OTHERSESSION; -} - -// The programs in the way (up to max in list); the result has their RUN_* bits, with RUN_CANCLOSE / RUN_NOCLOSE for the two kinds. -static int scan_programs(runproc *list, int max, int *count) -{ - PROCESSENTRY32W pe; HANDLE s; WCHAR path[MAX_PATH], dir[MAX_PATH + 2]; int r = 0, bit, why; DWORD mysess = 0, sess; - *count = 0; - ProcessIdToSessionId(GetCurrentProcessId(), &mysess); - s = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (s == INVALID_HANDLE_VALUE) return 0; - cat3(dir, g_dir, L"\\", NULL); - pe.dwSize = sizeof(pe); - if (Process32FirstW(s, &pe)) do { - bit = !lstrcmpiW(pe.szExeFile, L"DynaRun V3.exe") ? RUN_DYNARUN : !lstrcmpiW(pe.szExeFile, L"DynaRunFix.exe") ? RUN_LAUNCHER : - !lstrcmpiW(pe.szExeFile, L"LEProc.exe") ? RUN_LEPROC : 0; - if (!bit) continue; - if (!ProcessIdToSessionId(pe.th32ProcessID, &sess)) sess = (DWORD)-1; - process_path(pe.th32ProcessID, path, MAX_PATH); - why = judge_proc(bit, sess == mysess, path, g_exe, dir); - if (why == JUDGE_SKIP) continue; - r |= bit | (why == JUDGE_CLOSE ? RUN_CANCLOSE : RUN_NOCLOSE); - if (list && *count < max) { - list[*count].pid = pe.th32ProcessID; list[*count].bit = bit; list[*count].sess = sess; list[*count].why = why; - lstrcpyW(list[*count].path, path); (*count)++; - } - } while (Process32NextW(s, &pe)); - CloseHandle(s); - return r; -} - -int running_programs(void) -{ int n; return scan_programs(NULL, 0, &n); } - -// Why the setup stops with RC_RUNNING (nothing is shown with /quiet): one line per program in a log. The elevated stage writes -// setup.log in the install folder; before that folder exists (or without administrator rights) %TEMP%\DynaRunFix-setup.log. -static void run_log(const WCHAR *line) -{ - static WCHAR p[MAX_PATH]; - if (is_admin() && g_dir[0] && exists(g_dir)) { setup_log(line); return; } - if (GetTempPathW(MAX_PATH - 24, p)) append_line(lstrcatW(p, L"DynaRunFix-setup.log"), line); -} - -// For the other files (the wizard side of the driver step): the line with a time stamp, so that durations can be read from the log. -// The wizard thread, its worker and the update-hint thread all log: one lock covers the formatting (static buffers) and the write. -static CRITICAL_SECTION g_ulog_cs; -static volatile LONG g_ulog_state; // 0 not set up, 1 being set up, 2 ready - -void user_log(const WCHAR *line) -{ - static WCHAR l[2 * MAX_PATH + 400]; SYSTEMTIME t; const WCHAR *p, *q; - if (g_ulog_state != 2) { - if (InterlockedCompareExchange(&g_ulog_state, 1, 0) == 0) { InitializeCriticalSection(&g_ulog_cs); g_ulog_state = 2; } - else while (g_ulog_state != 2) Sleep(1); - } - EnterCriticalSection(&g_ulog_cs); - GetLocalTime(&t); - for (p = line; *p; p = *q ? q + 1 : q) { // a text of several lines is several lines, each with its time; the log is UTF-8 (append_line) - int n, pre, cnt; - for (q = p; *q && *q != '\n'; q++); - n = (int)(q - p); - while (n > 0 && p[n - 1] == '\r') n--; - if (n <= 0) continue; - wsprintfW(l, L"[%02u:%02u:%02u.%03u] ", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); - pre = lstrlenW(l); cnt = n + 1; - if (cnt > (int)(sizeof(l) / sizeof(l[0])) - pre) cnt = (int)(sizeof(l) / sizeof(l[0])) - pre; - lstrcpynW(l + pre, p, cnt); - run_log(l); - } - LeaveCriticalSection(&g_ulog_cs); -} - -static void log_running(const WCHAR *stage, const runproc *l, int n, int r, BOOL close) -{ - static const WCHAR *const names[] = { L"DynaRun V3.exe", L"DynaRunFix.exe", L"LEProc.exe" }; - static const WCHAR *const reasons[] = { L"can be closed", L"cannot be closed: runs in another session", - L"cannot be closed: its exe path cannot be read", L"cannot be closed: the DynaRun folder is not known" }; - WCHAR t[900]; SYSTEMTIME st; int i, k; DWORD me = 0; - GetLocalTime(&st); ProcessIdToSessionId(GetCurrentProcessId(), &me); - wsprintfW(t, L"%04u-%02u-%02u %02u:%02u:%02u %s: stopped, exit code 6 (DynaRun is running), nothing was changed. %s", st.wYear, st.wMonth, st.wDay, - st.wHour, st.wMinute, st.wSecond, stage, - (r & RUN_NOCLOSE) ? (close ? L"/close was not used: a program that cannot be closed is running." : L"A program that cannot be closed is running.") - : (close ? L"It was still running after /close." : L"Run with /close to close it.")); - run_log(t); - for (i = 0; i < n; i++) { - k = l[i].bit == RUN_DYNARUN ? 0 : l[i].bit == RUN_LAUNCHER ? 1 : 2; - wsprintfW(t, L" %s pid %lu, session %lu (this setup: %lu), %s%s%s", names[k], l[i].pid, l[i].sess, me, reasons[l[i].why - JUDGE_CLOSE], - l[i].path[0] ? L", " : L"", l[i].path); - run_log(t); - } -} - -static BOOL CALLBACK close_window(HWND h, LPARAM pid) -{ - DWORD p; - GetWindowThreadProcessId(h, &p); - if (p == (DWORD)pid && IsWindowVisible(h)) PostMessageW(h, WM_CLOSE, 0, 0); - return TRUE; -} - -static void wait_gone(runproc *l, int n, int bits, DWORD ms) -{ - HANDLE h[16]; int i, k = 0; - for (i = 0; i < n; i++) if ((l[i].bit & bits) && l[i].why == JUDGE_CLOSE && (h[k] = OpenProcess(SYNCHRONIZE, FALSE, l[i].pid))) k++; - if (k) WaitForMultipleObjects(k, h, TRUE, ms); - while (k) CloseHandle(h[--k]); -} - -// Closes DynaRun as if its window were closed (WM_CLOSE); what is still running 10 s later (e.g. a -// question DynaRun asks on exit) is ended. Runs in the elevated stage: an elevated DynaRun accepts -// neither messages nor TerminateProcess from a process that is not elevated. -static void close_programs(void) -{ - runproc *l = alloc(16 * sizeof(runproc)); int n, i; HANDLE h; // on the heap: a stack of that size would need the C runtime's probe - if (!l || !(scan_programs(l, 16, &n) & RUN_CANCLOSE)) { release(l); return; } - for (i = 0; i < n; i++) if (l[i].bit == RUN_DYNARUN && l[i].why == JUDGE_CLOSE) EnumWindows(close_window, l[i].pid); - wait_gone(l, n, RUN_DYNARUN, 10000); - wait_gone(l, n, RUN_LAUNCHER | RUN_LEPROC, 2000); // LEProc waits for DynaRun, the launcher ends by itself - scan_programs(l, 16, &n); - for (i = 0; i < n; i++) - if (l[i].why == JUDGE_CLOSE && (h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, l[i].pid))) { - TerminateProcess(h, 1); WaitForSingleObject(h, 5000); CloseHandle(h); - } - release(l); -} - -// The elevated install stages: closes DynaRun first when the user agreed (or /close), then TRUE (and the reason logged) when something -// still holds up the install. Nothing is closed when part of it cannot be closed: the install stops anyway, and a test run would end for nothing. -static BOOL still_running(const install_opts *o, const WCHAR *stage) -{ - runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); - if (o->close && r && !(r & RUN_NOCLOSE)) { close_programs(); r = scan_programs(l, l ? 16 : 0, &n); } - if (r && l) log_running(stage, l, n, r, o->close); - release(l); - return r != 0; -} - -// Before the UAC prompt: TRUE (logged) when /quiet without /close ends here. With /close the elevated stage decides: it can read more. -static BOOL running_early(const install_opts *o, const WCHAR *stage) -{ - runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); - if (r && !o->close && l) log_running(stage, l, n, r, o->close); - release(l); - return r && !o->close; -} - -// The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in lives in tests\promote_com.h). - -/* ---------- shortcuts ---------- */ - -typedef UINT (WINAPI *MsiGetShortcutTargetW_t)(LPCWSTR, LPWSTR, LPWSTR, LPWSTR); -typedef int (WINAPI *MsiGetComponentPathW_t)(LPCWSTR, LPCWSTR, LPWSTR, DWORD *); -static MsiGetShortcutTargetW_t pMsiGetShortcutTarget; -static MsiGetComponentPathW_t pMsiGetComponentPath; - -// Looks up the two MSI functions, once (more calls do nothing). msi.dll is loaded by its full path in the system folder (load_system_dll), never -// searched for. Both pointers stay NULL when msi.dll or a function is missing: classify then skips its check of advertised shortcuts. -static void msi_api_init(void) -{ - static BOOL done; HMODULE msi; - if (done) return; - done = TRUE; - if (!(msi = load_system_dll(L"msi.dll"))) return; - pMsiGetShortcutTarget = (MsiGetShortcutTargetW_t)GetProcAddress(msi, "MsiGetShortcutTargetW"); - pMsiGetComponentPath = (MsiGetComponentPathW_t)GetProcAddress(msi, "MsiGetComponentPathW"); - if (!pMsiGetComponentPath) pMsiGetShortcutTarget = NULL; -} - -static IShellLinkW *load_link(const WCHAR *p) -{ - IShellLinkW *sl; IPersistFile *pf; HRESULT hr = E_FAIL; - if (FAILED(CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **)&sl))) return NULL; - if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IPersistFile, (void **)&pf))) { hr = IPersistFile_Load(pf, p, STGM_READ); IPersistFile_Release(pf); } - if (SUCCEEDED(hr)) return sl; - IShellLinkW_Release(sl); - return NULL; -} - -enum { LNK_OTHER, LNK_DYNARUN, LNK_OURS }; -// A shortcut target that is the launcher of some DynaRunFix install: "...\DynaRunFix\DynaRunFix.exe". The path of this install -// (g_launcher) is compared first; this also catches the launcher as an older version or Windows (32-bit program on 64-bit Windows: -// "Program Files" vs "Program Files (x86)") recorded it, so such a shortcut is still taken for ours when it is restored or pointed back. -static BOOL launcher_path_like(const WCHAR *t) -{ - static const WCHAR tail[] = L"\\DynaRunFix\\DynaRunFix.exe"; int n = lstrlenW(t), m = (int)(sizeof(tail) / sizeof(WCHAR)) - 1; - return n > m && !lstrcmpiW(t + n - m, tail); -} - -static int classify(const WCHAR *p) -{ - WCHAR t[MAX_PATH], prod[40], feat[40], comp[40]; DWORD n = MAX_PATH; IShellLinkW *sl = load_link(p); int r = LNK_OTHER; - if (!sl) return r; - t[0] = 0; - IShellLinkW_GetPath(sl, t, MAX_PATH, NULL, 0); - IShellLinkW_Release(sl); - if (!lstrcmpiW(t, g_launcher) || launcher_path_like(t)) return LNK_OURS; - if (!lstrcmpiW(t, g_exe)) return LNK_DYNARUN; - // the original setup is an MSI: its shortcuts are "advertised" and point to the product, not the exe - msi_api_init(); - if (pMsiGetShortcutTarget && !pMsiGetShortcutTarget(p, prod, feat, comp) && - pMsiGetComponentPath(prod, comp, t, &n) >= 0 && !lstrcmpiW(t, g_exe)) r = LNK_DYNARUN; - return r; -} - -// Windows 7+: an explicit AppUserModelID on the launcher shortcuts, so that a running DynaRun (same ID, set by -// dynafix.dll) groups under the pinned icon; other targets get none. -static void set_appid(IShellLinkW *sl, const WCHAR *id) -{ - IPropertyStore *ps; PROPVARIANT pv; DWORD ma, mi, b; - os_ver(&ma, &mi, &b); - if (ma < 6 || (ma == 6 && mi < 1) || FAILED(IShellLinkW_QueryInterface(sl, &IID_IPropertyStore, (void **)&ps))) return; - zero(&pv, sizeof(pv)); - if (id) { pv.vt = VT_LPWSTR; pv.pwszVal = (LPWSTR)id; } // VT_EMPTY removes it - if (SUCCEEDED(IPropertyStore_SetValue(ps, &PKEY_AUMID, &pv))) IPropertyStore_Commit(ps); - IPropertyStore_Release(ps); -} - -// DynaRun's own icon. DynaRun V3.exe holds only the chart icon of its windows; the blue Dyna Run logo is in the icon -// file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries -// its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. -static BOOL g_icontried; static WCHAR g_icon[MAX_PATH]; static int g_iconidx; -static BOOL is_lnk(const WCHAR *p) { int n = lstrlenW(p); return n >= 4 && !lstrcmpiW(p + n - 4, L".lnk"); } - -// The shell folders that can hold a DynaRun shortcut: ONE list, for every walk and check that looks at them (the icon search, the fix, the -// uninstall's point-back, the search for our own shortcut, and the check of the paths a backup is put back to). A row says which of them -// include the folder (SFU_*) and which of those also look into its subfolders (deep); nothing else lists a shortcut folder, so a folder that -// is not here is neither scanned nor accepted by restore_shortcuts. -enum { SFU_FIX = 1, SFU_UNRET = 2, SFU_FIND = 4, SFU_ICON = 8, SFU_RESTORE = 16 }; -enum { SF_DESKTOP, SF_STARTMENU, SF_PROGRAMS, SF_QUICKLAUNCH, SF_OWN, SF_COUNT }; // the index in SF[]; the first two are searched first -enum { SF_USER = 1, SF_COMMON = 2, SF_BOTH = 3 }; // which of the two folders of a row (bit 0 the user's, bit 1 all users') -typedef struct { - int csidl[2]; // [0] the user's folder, [1] the all users' folder (0: there is none) - const WCHAR *sub; // below that folder - const WCHAR *reg, *rel; // the folder of another account (unretarget_user): its name in User Shell Folders, and the default below the profile - unsigned uses, deep; -} shortcut_folder; -static const WCHAR QUICKLAUNCH[] = L"\\Microsoft\\Internet Explorer\\Quick Launch"; // incl. pinned taskbar items -static const shortcut_folder SF[SF_COUNT] = { - { { CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY }, NULL, L"Desktop", L"Desktop", - SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, 0 }, - // the Start menu root (not just Programs): some DynaRun installs put their shortcut there. The fix and the point-back look at the root - // alone and at Programs on its own; the two searches look below the whole Start menu. - { { CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU }, NULL, L"Start Menu", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu", - SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, SFU_FIND | SFU_ICON }, - { { CSIDL_PROGRAMS, CSIDL_COMMON_PROGRAMS }, NULL, L"Programs", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs", - SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, - { { CSIDL_APPDATA, 0 }, QUICKLAUNCH, L"AppData", L"AppData\\Roaming", - SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, - { { CSIDL_APPDATA, 0 }, L"\\DynaRunFix", NULL, NULL, SFU_RESTORE, 0 }, // desktop_link(): the shortcut it creates when there is none -}; - -// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none or the path is too long for what goes below it. -static BOOL sf_path(int id, BOOL common, WCHAR *out) -{ - const shortcut_folder *f = &SF[id]; int csidl = f->csidl[common ? 1 : 0]; - if (!csidl || !SHGetSpecialFolderPathW(NULL, out, csidl, FALSE)) return FALSE; - if (f->sub) { if (lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } - return TRUE; -} - -// fn(folder, row, common, deep, ctx) is called for each folder that the use (one SFU_*) lists, in the scope (SF_USER, SF_COMMON, SF_BOTH): a row's -// user folder before its all users' one. fn returns FALSE to stop; then this returns TRUE. -typedef BOOL (*sf_fn)(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx); -static BOOL shortcut_folders(unsigned use, int scope, sf_fn fn, void *ctx) -{ - WCHAR d[MAX_PATH]; int i, id, c; - for (i = 0; i < SF_COUNT; i++) { - id = use == SFU_ICON && i < 2 ? 1 - i : i; // the icon is looked for in the Start menu before the Desktop - if (!(SF[id].uses & use)) continue; - for (c = 0; c < 2; c++) - if ((scope & (1 << c)) && sf_path(id, c, d) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; - } - return FALSE; -} - -// DynaRun's own icon. DynaRun V3.exe holds only the chart icon of its windows; the blue Dyna Run logo is in the icon -// file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries -// its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. -// The walks below (dir_walk) skip names that start with '.' and paths longer than their buffer; all but the fix do not follow links. -typedef struct { WCHAR p[MAX_PATH], ic[MAX_PATH], x[MAX_PATH]; BOOL recurse, found; } icon_walk; -static BOOL find_icon(const WCHAR *dir, BOOL recurse); -static BOOL icon_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) -{ - icon_walk *w = vc; IShellLinkW *sl; int idx; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_icon(p, TRUE); return !w->found; } - if (!is_lnk(p) || classify(p) == LNK_OTHER || !(sl = load_link(p))) return TRUE; - w->ic[0] = 0; idx = 0; - IShellLinkW_GetIconLocation(sl, w->ic, MAX_PATH, &idx); - IShellLinkW_Release(sl); - if (w->ic[0] && lstrcmpiW(w->ic, g_exe) && ExpandEnvironmentStringsW(w->ic, w->x, MAX_PATH) && exists(w->x)) { - lstrcpyW(g_icon, w->ic); g_iconidx = idx; w->found = TRUE; - } - return !w->found; -} - -static BOOL find_icon(const WCHAR *dir, BOOL recurse) -{ - icon_walk *w = alloc(sizeof(*w)); BOOL r; - if (!w) return FALSE; - w->recurse = recurse; - dir_walk(dir, w->p, MAX_PATH, MAX_PATH + 1, icon_entry, w); // paths of at most MAX_PATH characters (dir_walk's limit is "shorter than") - r = w->found; - release(w); - return r; -} - -static BOOL icon_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { return !find_icon(dir, deep); } - -static void dynarun_icon(void) -{ - if (g_icontried) return; - g_icontried = TRUE; - shortcut_folders(SFU_ICON, SF_BOTH, icon_folder, NULL); -} - -static BOOL save_link(const WCHAR *p, const WCHAR *target, const WCHAR *icon, int idx, WORD hotkey, int show, BOOL runas) -{ - IShellLinkW *sl; IPersistFile *pf; IShellLinkDataList *dl; WCHAR wd[MAX_PATH], *e; HRESULT hr = E_FAIL; DWORD fl; - if (FAILED(CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **)&sl))) return FALSE; - lstrcpyW(wd, g_exe); for (e = wd + lstrlenW(wd); e > wd && *e != '\\'; e--); *e = 0; - IShellLinkW_SetPath(sl, target); - IShellLinkW_SetWorkingDirectory(sl, wd); - if (target == g_launcher && (!icon || !icon[0] || !lstrcmpiW(icon, g_exe))) { // the exe's chart icon: DynaRun's logo instead - dynarun_icon(); - if (g_icon[0]) { icon = g_icon; idx = g_iconidx; } - } - IShellLinkW_SetIconLocation(sl, icon, idx); - if (target == g_launcher) IShellLinkW_SetDescription(sl, L"DynaRun V3 (DynaRunFix)"); - set_appid(sl, target == g_launcher ? APPID : NULL); - if (hotkey) IShellLinkW_SetHotkey(sl, hotkey); - if (show) IShellLinkW_SetShowCmd(sl, show); - if (runas && SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IShellLinkDataList, (void **)&dl))) { - if (SUCCEEDED(IShellLinkDataList_GetFlags(dl, &fl))) IShellLinkDataList_SetFlags(dl, fl | SLDF_RUNAS_USER); - IShellLinkDataList_Release(dl); - } - if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IPersistFile, (void **)&pf))) { hr = IPersistFile_Save(pf, p, TRUE); IPersistFile_Release(pf); } - IShellLinkW_Release(sl); - return SUCCEEDED(hr); -} - -// Points shortcut p at target; keeps its name, icon and hotkey. Not the "Run as administrator" flag (the launcher never elevates DynaRun). -static BOOL relink(const WCHAR *p, const WCHAR *target) -{ - IShellLinkW *sl = load_link(p); IShellLinkDataList *dl; WCHAR icon[MAX_PATH]; int idx = 0, show = 0; - WORD hotkey = 0; DWORD fl = 0; - if (!sl) return FALSE; - icon[0] = 0; - IShellLinkW_GetIconLocation(sl, icon, MAX_PATH, &idx); - IShellLinkW_GetHotkey(sl, &hotkey); - IShellLinkW_GetShowCmd(sl, &show); - if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IShellLinkDataList, (void **)&dl))) { IShellLinkDataList_GetFlags(dl, &fl); IShellLinkDataList_Release(dl); } - IShellLinkW_Release(sl); - if (!icon[0]) { lstrcpyW(icon, g_exe); idx = 0; } - (void)fl; // SLDF_RUNAS_USER is not carried over to the retargeted shortcut - return save_link(p, target, icon, idx, hotkey, show, FALSE); -} - -// Points an existing DynaRun shortcut at the launcher. The original file is saved in \Shortcuts. -static void retarget(const WCHAR *p, HKEY backup) -{ - DWORD n; void *orig = read_file(p, &n); - if (!orig) return; - if (!RegSetValueExW(backup, p, 0, REG_BINARY, orig, n)) relink(p, g_launcher); - release(orig); -} - -typedef struct { WCHAR p[2 * MAX_PATH]; HKEY backup; BOOL recurse; int found; } scan_walk; -static int scan(const WCHAR *dir, BOOL recurse, HKEY backup); -static BOOL scan_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) // (this walk follows links; the others do not) -{ - scan_walk *w = vc; int c; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found += scan(p, TRUE, w->backup); return TRUE; } - if (!is_lnk(p)) return TRUE; - if ((c = classify(p)) == LNK_DYNARUN) retarget(p, w->backup); - // ours from an older version (before v1.3.0 they had no AppUserModelID): written again with the ID, else the - // running DynaRun (ID set by dynafix.dll) matches no shortcut and Windows shows its window icon, not DynaRun's - else if (c == LNK_OURS) relink(p, g_launcher); - if (c != LNK_OTHER) w->found++; - return TRUE; -} - -static int scan(const WCHAR *dir, BOOL recurse, HKEY backup) // returns number of DynaRun shortcuts -{ - scan_walk *w = alloc(sizeof(*w)); int found; - if (!w) return 0; - w->backup = backup; w->recurse = recurse; - dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, scan_lnk, w); - found = w->found; - release(w); - return found; -} - -typedef struct { WCHAR p[2 * MAX_PATH]; WCHAR *out; BOOL recurse, found; } ours_walk; -static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out); -static BOOL ours_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) -{ - ours_walk *w = vc; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_ours(p, TRUE, w->out); return !w->found; } - if (is_lnk(p) && classify(p) == LNK_OURS) { lstrcpyW(w->out, p); w->found = TRUE; } - return !w->found; -} - -static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out) // the first shortcut of ours below dir -{ - ours_walk *w = alloc(sizeof(*w)); BOOL r; - if (!w) return FALSE; - w->out = out; w->recurse = recurse; - dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, ours_lnk, w); - r = w->found; - release(w); - return r; -} - -// The first shortcut of ours where a user sees it: the Desktop, then the Start menu (the user's folder before the all users' one). -static BOOL ours_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *out) { return !find_ours(dir, deep, out); } -static BOOL find_our_shortcut(WCHAR *out) { return shortcut_folders(SFU_FIND, SF_BOTH, ours_folder, out); } - -typedef struct { HKEY backup; int desk; } fix_ctx; -static BOOL fix_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) -{ - fix_ctx *c = vc; int n = scan(dir, deep, c->backup); - if (id == SF_DESKTOP) c->desk = n; - return TRUE; -} - -static int fix_shortcuts(BOOL common, HKEY backup) // returns the number of DynaRun shortcuts on the Desktop -{ - fix_ctx c; - c.backup = backup; c.desk = 0; - shortcut_folders(SFU_FIX, common ? SF_COMMON : SF_USER, fix_folder, &c); - return c.desk; -} - -// restore_shortcuts writes files at paths that come from the registry. A path is used only when it is a .lnk file below one of the -// shortcut folders the backups were made from (SF[]: the rows that list SFU_RESTORE; user's or all users', as the backup is), with no ".." -// component and no link (junction) as its folder. When this process is elevated and the backups are in -// HKCU, which a program of the same user can write, the file must also be below the user's profile folder. -static BOOL under_dir(const WCHAR *path, const WCHAR *dir) -{ - int n = lstrlenW(dir); - while (n > 0 && dir[n - 1] == '\\') n--; - return n > 0 && lstrlenW(path) > n + 1 && path[n] == '\\' && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, n, dir, n) == CSTR_EQUAL; -} - -static BOOL has_dotdot(const WCHAR *p) -{ - const WCHAR *s = p; - for (; *p; p++) if (p[0] == '.' && p[1] == '.' && (p == s || p[-1] == '\\') && (p[2] == 0 || p[2] == '\\')) return TRUE; - return FALSE; -} - -// The syntax part of lnk_path_ok, without any folder lookup (tested with Windows 7 style paths): an absolute drive or UNC path that ends in -// ".lnk", has no ".." component and lies below one of dirs[0..n) (case-insensitive; trailing backslashes of a folder do not matter). -static BOOL lnk_name_in_dirs(const WCHAR *name, const WCHAR *const *dirs, int n) -{ - int i, len = lstrlenW(name); - if (len < 8 || len >= MAX_PATH || lstrcmpiW(name + len - 4, L".lnk") || name[len - 5] == '\\' || has_dotdot(name)) return FALSE; // not a file called just ".lnk" - if (!(((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')) && name[1] == ':' && name[2] == '\\') && - !(name[0] == '\\' && name[1] == '\\' && name[2] != '?' && name[2] != '.')) return FALSE; // an absolute drive or UNC path, no device path - for (i = 0; i < n; i++) if (dirs[i] && dirs[i][0] && under_dir(name, dirs[i])) return TRUE; - return FALSE; -} - -// d: the long form of a folder or file name (8.3 components expanded) when Windows can tell, else unchanged -static void long_form(WCHAR *d) -{ - WCHAR t[MAX_PATH]; DWORD n = GetLongPathNameW(d, t, MAX_PATH); - if (n && n < MAX_PATH) lstrcpyW(d, t); -} - -typedef struct { WCHAR dirs[10][MAX_PATH]; const WCHAR *list[10]; int n; } restore_dirs; -static BOOL restore_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) -{ - restore_dirs *r = vc; - lstrcpyW(r->dirs[r->n], dir); r->list[r->n] = r->dirs[r->n]; r->n++; - if (id != SF_OWN) { // (the folder of desktop_link() is listed as the shell reports it) - lstrcpyW(r->dirs[r->n], dir); long_form(r->dirs[r->n]); // the backup may hold the long form of a folder that the shell reports in 8.3 form - r->list[r->n] = r->dirs[r->n]; r->n++; - } - return TRUE; -} - -static BOOL lnk_path_ok(const WCHAR *name, BOOL common) -{ - static restore_dirs rd; - WCHAR d[MAX_PATH], *e, par[MAX_PATH], lname[MAX_PATH]; int len = lstrlenW(name); DWORD a; - if (len < 8 || len >= MAX_PATH) return FALSE; - if (!common && is_admin()) { // elevated: the user's own registry is not trusted beyond their profile - if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_PROFILE, FALSE)) return FALSE; - if (!under_dir(name, d)) { - lstrcpyW(lname, name); long_form(lname); long_form(d); // 8.3 form on one side only - if (!under_dir(lname, d)) return FALSE; - } - } - rd.n = 0; - shortcut_folders(SFU_RESTORE, common ? SF_COMMON : SF_USER, restore_folder, &rd); - lstrcpyW(par, name); for (e = par + len; e > par && *e != '\\'; e--); *e = 0; - if (!lnk_name_in_dirs(name, rd.list, rd.n)) { - lstrcpyW(lname, par); long_form(lname); // the same, with the folder of the file in long form - if (!path_join(lname, MAX_PATH, lname, e + 1)) return FALSE; - if (!lnk_name_in_dirs(lname, rd.list, rd.n)) return FALSE; - } - a = GetFileAttributesW(par); - if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_REPARSE_POINT)) return FALSE; - a = GetFileAttributesW(name); - return a == INVALID_FILE_ATTRIBUTES || !(a & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)); -} - -// The backup stays (key not deleted) when a shortcut could not be put back or was refused: nothing is lost, a later uninstall can retry. -static void restore_shortcuts(HKEY root, const WCHAR *key) -{ - HKEY k; DWORD i, nmax = 0, dmax = 0, n, dn, type; WCHAR *name; BYTE *data; WCHAR sub[64]; BOOL failed = FALSE, common = root == HKEY_LOCAL_MACHINE; - if (RegOpenKeyExW(root, cat3(sub, key, L"\\Shortcuts", NULL), 0, KEY_READ, &k)) return; - RegQueryInfoKeyW(k, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &nmax, &dmax, NULL, NULL); - name = alloc((nmax + 2) * sizeof(WCHAR)); data = alloc(dmax + 2); - if (!name || !data) failed = TRUE; - for (i = 0; name && data && (n = nmax + 1, dn = dmax, !RegEnumValueW(k, i, name, &n, NULL, &type, data, &dn)); i++) { - if (type != REG_BINARY || !lnk_path_ok(name, common)) { failed = TRUE; setup_log(L"a shortcut backup was not restored: its path is not a .lnk file in a shell folder"); continue; } - if (classify(name) != LNK_OURS && exists(name)) continue; // changed by the user since: leave it - if (!dn) { if (exists(name) && !DeleteFileW(name)) failed = TRUE; } // a shortcut we created - else if (dn < 76 || data[0] != 0x4C || data[1] || data[2] || data[3] || !write_file(name, data, dn)) failed = TRUE; // a .lnk starts with its 0x4C header size - } - release(name); release(data); - RegCloseKey(k); - if (!failed) RegDeleteKeyW(root, sub); -} - -// Uninstall, other accounts: their backups are in their own HKCU, so their shortcuts that still start -// the launcher (which is about to be deleted) are pointed back at DynaRun V3.exe instead. Only files that -// are our shortcuts are rewritten, so a folder read from another user's registry can do no harm; reparse -// points (junctions) are not followed. -typedef struct { WCHAR p[2 * MAX_PATH]; BOOL recurse; } unret_walk; -static void unretarget(const WCHAR *dir, BOOL recurse); -static BOOL unret_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) -{ - unret_walk *w = vc; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; - if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) unretarget(p, TRUE); return TRUE; } - if (is_lnk(p) && g_exe[0] && classify(p) == LNK_OURS) relink(p, g_exe); - return TRUE; -} - -static void unretarget(const WCHAR *dir, BOOL recurse) -{ - unret_walk *w = alloc(sizeof(*w)); - if (!w) return; - w->recurse = recurse; - dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, unret_lnk, w); - release(w); -} - -// What is left of our shortcuts in the shell folders after the backups were put back (a backup that is missing, as when an older version did -// not record one, or was refused): they point to DynaRun V3.exe again instead of the launcher that is being removed. The folders include the -// pins the user made of the launcher (Quick Launch). -static BOOL unret_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { unretarget(dir, deep); return TRUE; } -static void unretarget_shell(BOOL common) -{ - if (!g_exe[0]) return; - shortcut_folders(SFU_UNRET, common ? SF_COMMON : SF_USER, unret_folder, NULL); -} - -/* ---------- taskbar pin and sign-in start ---------- */ - -static void drop_run(HKEY hive, BOOL only_ours) // the "Run" value; of other accounts only when it starts our launcher -{ - static WCHAR v[MAX_PATH]; HKEY k; - if (only_ours && !(reg_str(hive, RUNKEY, L"DynaRunFix", v, MAX_PATH, 0) && contains_i(v, g_launcher))) return; - if (!RegOpenKeyExW(hive, RUNKEY, 0, KEY_SET_VALUE, &k)) { RegDeleteValueW(k, L"DynaRunFix"); RegCloseKey(k); } -} - -static WCHAR *layout_xml(WCHAR *out) { return cat3(out, g_dir, L"\\TaskbarLayout.xml", NULL); } - -static void drop_policy(HKEY hive) // the Windows 11 taskbar layout policy, when it is still ours -{ - static WCHAR v[MAX_PATH], x[MAX_PATH]; HKEY k; - if (reg_str(hive, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(x)) && - !RegOpenKeyExW(hive, POLKEY, 0, KEY_SET_VALUE, &k)) { - RegDeleteValueW(k, L"StartLayoutFile"); RegDeleteValueW(k, L"LockedStartLayout"); RegCloseKey(k); - } -} - -static const WCHAR XML_HEAD[] = - L"\n" - L"\n" - L" \n \n \n" - L" keep) return; - if (!o->pin) { // reinstalled without the pin: take our policy back - if (reg_str(app, NULL, L"TaskbarPolicySid", v, MAX_PATH, 0) && !RegOpenKeyExW(HKEY_USERS, v, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } - RegDeleteValueW(app, L"TaskbarPolicy"); RegDeleteValueW(app, L"TaskbarPolicySid"); - return; - } - if (ma < 10 || b < 22000) return; - cat3(path, sid, L"\\", POLKEY); - layout_xml(x); - if (reg_str(HKEY_USERS, path, L"StartLayoutFile", v, MAX_PATH, 0) && lstrcmpiW(v, x)) return; - if (!save_link(cat3(l, g_dir, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return; - if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_QUERY_VALUE | WOW, &k)) { - RegQueryValueExW(k, L"UBR", NULL, NULL, (BYTE *)&ubr, &n); RegCloseKey(k); - } - // PinGeneration only on builds that know it: elsewhere it makes Windows ignore the pin - gen = b >= 26200 || (b == 26100 && ubr >= 4484) || (b == 22631 && ubr >= 5549); - lstrcpyW(xml, XML_HEAD); lstrcatW(xml, l); - lstrcatW(xml, gen ? L"\" PinGeneration=\"1\"/>\n" : L"\"/>\n"); - lstrcatW(xml, L" \n \n \n\n"); - n = WideCharToMultiByte(CP_UTF8, 0, xml, -1, u, sizeof(u), NULL, NULL); - if (!n || !write_file(x, u, n - 1)) return; - if (RegCreateKeyExW(HKEY_USERS, path, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) return; - RegSetValueExW(k, L"StartLayoutFile", 0, REG_EXPAND_SZ, (const BYTE *)x, (lstrlenW(x) + 1) * sizeof(WCHAR)); - set_dword(k, L"LockedStartLayout", 1); - RegCloseKey(k); - set_dword(app, L"TaskbarPolicy", 1); set_str(app, L"TaskbarPolicySid", sid); -} - -static BOOL pinned_ours(void) // a shortcut to the launcher among the taskbar's pinned items (Windows 7+) -{ - static WCHAR d[MAX_PATH], f[MAX_PATH]; - if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; - lstrcatW(d, L"\\User Pinned\\TaskBar"); - return find_ours(d, FALSE, f); -} - -static BOOL wait_pinned(void) // Explorer writes the shortcut a moment after the call returned -{ - int i; - for (i = 0; i < 30; i++) { if (pinned_ours()) return TRUE; Sleep(100); } - return FALSE; -} - -typedef HRESULT (WINAPI *SHParseDisplayName_t)(PCWSTR, IBindCtx *, PIDLIST_ABSOLUTE *, ULONG, ULONG *); -typedef HRESULT (WINAPI *SHBindToParent_t)(PCIDLIST_ABSOLUTE, REFIID, void **, PCUITEMID_CHILD *); - -static PIDLIST_ABSOLUTE item_id(const WCHAR *p) // not on XP's shell32: looked up -{ - SHParseDisplayName_t f = (SHParseDisplayName_t)GetProcAddress(GetModuleHandleW(L"shell32.dll"), "SHParseDisplayName"); PIDLIST_ABSOLUTE id = NULL; - if (!f || FAILED(f(p, NULL, &id, 0, NULL))) return NULL; - return id; -} - -static BOOL pin_verb(const WCHAR *lnk) // Windows 7 .. Windows 10 1803: the shell's "Pin to taskbar" verb -{ - SHBindToParent_t bind = (SHBindToParent_t)GetProcAddress(GetModuleHandleW(L"shell32.dll"), "SHBindToParent"); - PIDLIST_ABSOLUTE id = item_id(lnk); IShellFolder *sf; PCUITEMID_CHILD ch; IContextMenu *cm; HMENU m; CMINVOKECOMMANDINFO ci; BOOL r = FALSE; - if (id && bind && SUCCEEDED(bind(id, &IID_IShellFolder, (void **)&sf, &ch))) { - if (SUCCEEDED(IShellFolder_GetUIObjectOf(sf, NULL, 1, &ch, &IID_IContextMenu, NULL, (void **)&cm))) { - m = CreatePopupMenu(); - if (SUCCEEDED(IContextMenu_QueryContextMenu(cm, m, 0, 1, 0x7fff, CMF_NORMAL))) { - zero(&ci, sizeof(ci)); ci.cbSize = sizeof(ci); ci.lpVerb = "taskbarpin"; ci.nShow = SW_HIDE; - r = SUCCEEDED(IContextMenu_InvokeCommand(cm, &ci)); - } - DestroyMenu(m); IContextMenu_Release(cm); - } - IShellFolder_Release(sf); - } - CoTaskMemFree(id); - return r; -} - -static BOOL pin_list(const WCHAR *lnk) // Windows 10 1809 and later: Explorer's IPinnedList3 -{ - IPinnedList3 *pl; PIDLIST_ABSOLUTE id = item_id(lnk); BOOL r = FALSE; - if (id && SUCCEEDED(CoCreateInstance(&CLSID_TaskbandPin, NULL, CLSCTX_INPROC_SERVER, &IID_IPinnedList3, (void **)&pl))) { - r = SUCCEEDED(pl->lpVtbl->Modify(pl, NULL, id, 0x7fffffff)); - pl->lpVtbl->Release(pl); - } - CoTaskMemFree(id); - return r; -} - -// Pins DynaRun (the shortcut lnk, which starts the launcher) to the taskbar of the current user. TRUE: done, or -// on Windows 11 under way (*res = PIN_NEXT_SIGNIN: the policy set by the machine stage applies at the next sign-in). -static BOOL pin_taskbar(const WCHAR *lnk, int *res) -{ - static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; - os_ver(&ma, &mi, &b); - if (ma == 5 || (ma == 6 && !mi)) { // XP, Vista: the Quick Launch toolbar - if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; - CreateDirectoryW(d, NULL); - if (!save_link(lstrcatW(d, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; - if (!RegOpenKeyExW(HKEY_CURRENT_USER, cat3(sub, USERKEY, L"\\Shortcuts", NULL), 0, KEY_SET_VALUE, &k)) { - RegSetValueExW(k, d, 0, REG_BINARY, (const BYTE *)"", 0); // a shortcut we created: uninstall deletes it - RegCloseKey(k); - } - return TRUE; - } - if (ma >= 10 && b >= 22000) { // Windows 11: see taskbar_policy - if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) *res = PIN_NEXT_SIGNIN; - return *res == PIN_NEXT_SIGNIN; - } - if (pinned_ours()) return TRUE; - if (ma >= 10 && b >= 17763) pin_list(lnk); else pin_verb(lnk); - return wait_pinned(); -} - -static BOOL desktop_link(WCHAR *out, HKEY backup) // one of our shortcuts to pin: desktop, Start menu, else a new one -{ - WCHAR d[MAX_PATH]; - if (find_our_shortcut(out)) return TRUE; - if (!sf_path(SF_OWN, FALSE, d)) return FALSE; - CreateDirectoryW(d, NULL); - if (!save_link(cat3(out, d, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; - RegSetValueExW(backup, out, 0, REG_BINARY, (const BYTE *)"", 0); - return TRUE; -} - -// Opens file the way a double click does, but as the signed-in user's normal (non-elevated) process: the desktop's shell (Explorer, which runs -// with the normal token) is asked to do it - IShellWindows -> the desktop's shell view -> its Application (IShellDispatch2) -> ShellExecute. -// FALSE when any step fails (no Explorer, no desktop: e.g. a session without one). -static BOOL shell_execute_unelevated(const WCHAR *file, const WCHAR *dir) -{ - IShellWindows *sw = NULL; IDispatch *disp = NULL, *dv = NULL, *app = NULL; IServiceProvider *sp = NULL; IShellBrowser *sb = NULL; IShellView *view = NULL; - IShellFolderViewDual *fvd = NULL; IShellDispatch2 *sd = NULL; HRESULT hrc; VARIANT loc, root, a, d, o, s; LONG hwnd = 0; BSTR bf = NULL, bd = NULL; BOOL ok = FALSE; - hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // RPC_E_CHANGED_MODE: already initialised another way, usable - if (FAILED(hrc) && hrc != RPC_E_CHANGED_MODE) return FALSE; - VariantInit(&loc); VariantInit(&root); VariantInit(&a); VariantInit(&d); VariantInit(&o); VariantInit(&s); - loc.vt = VT_I4; loc.lVal = CSIDL_DESKTOP; - if (FAILED(CoCreateInstance(&CLSID_ShellWindows, NULL, CLSCTX_LOCAL_SERVER, &IID_IShellWindows, (void **)&sw))) goto done; - if (FAILED(IShellWindows_FindWindowSW(sw, &loc, &root, SWC_DESKTOP, &hwnd, SWFO_NEEDDISPATCH, &disp)) || !disp) goto done; - if (FAILED(IDispatch_QueryInterface(disp, &IID_IServiceProvider, (void **)&sp))) goto done; - if (FAILED(IServiceProvider_QueryService(sp, &SID_STopLevelBrowser, &IID_IShellBrowser, (void **)&sb))) goto done; - if (FAILED(IShellBrowser_QueryActiveShellView(sb, &view)) || !view) goto done; - if (FAILED(IShellView_GetItemObject(view, SVGIO_BACKGROUND, &IID_IDispatch, (void **)&dv)) || !dv) goto done; - if (FAILED(IDispatch_QueryInterface(dv, &IID_IShellFolderViewDual, (void **)&fvd))) goto done; - if (FAILED(IShellFolderViewDual_get_Application(fvd, &app)) || !app) goto done; - if (FAILED(IDispatch_QueryInterface(app, &IID_IShellDispatch2, (void **)&sd))) goto done; - if (!(bf = SysAllocString(file))) goto done; - if (dir && dir[0] && (bd = SysAllocString(dir))) { d.vt = VT_BSTR; d.bstrVal = bd; } - s.vt = VT_I4; s.lVal = SW_SHOWNORMAL; - ok = SUCCEEDED(IShellDispatch2_ShellExecute(sd, bf, a, d, o, s)); -done: - SysFreeString(bf); SysFreeString(bd); - if (sd) IShellDispatch2_Release(sd); - if (app) IDispatch_Release(app); - if (fvd) IShellFolderViewDual_Release(fvd); - if (dv) IDispatch_Release(dv); - if (view) IShellView_Release(view); - if (sb) IShellBrowser_Release(sb); - if (sp) IServiceProvider_Release(sp); - if (disp) IDispatch_Release(disp); - if (sw) IShellWindows_Release(sw); - if (SUCCEEDED(hrc)) CoUninitialize(); - return ok; -} - -// DynaRun is never started with more rights than a normal start from the desktop: an elevated wizard (setup run as administrator; the sign-in task -// of the driver installation, which runs with the user's full token) asks the shell to start it as the normal user. If that is impossible it is not -// started elevated: the user is told to use the desktop icon. Which way was used goes to the log. -static void launch_normal(const WCHAR *file, const WCHAR *dir) -{ - if (!is_admin()) { // already a normal process: a plain ShellExecute is the normal start - ShellExecuteW(g_hwnd, NULL, file, NULL, dir, SW_SHOWNORMAL); - run_log(L"start DynaRun: ShellExecute (this process is not elevated)"); - return; - } - if (shell_execute_unelevated(file, dir)) { run_log(L"start DynaRun: through the desktop shell, as the normal user (this process is elevated)"); return; } - run_log(L"start DynaRun: not started, this process is elevated and the desktop shell could not be asked"); - msg(T(L"DynaRun was not started from here. Please start it with the \"DynaRun V3\" icon on the desktop.", L"這裡沒有啟動 DynaRun,請改用桌面上的「DynaRun V3」圖示開啟。"), MB_ICONINFORMATION); -} - -// The wizard's "Start DynaRun": started the way a double click on the shortcut does it (the shortcut gives the -// process the AppUserModelID that groups it with the pinned icon, its working folder and show command), not by -// running the launcher directly. Falls back to the launcher with DynaRun's folder as working directory. -void start_dynarun(void) -{ - static WCHAR lnk[MAX_PATH], dir[MAX_PATH]; - WCHAR *q; BOOL have = find_our_shortcut(lnk); - if (!have && exists(cat3(lnk, g_dir, L"\\DynaRun V3.lnk", NULL))) have = TRUE; - if (have) { launch_normal(lnk, NULL); return; } - lstrcpyW(dir, g_exe); q = dir + lstrlenW(dir); while (q > dir && *q != '\\') q--; *q = 0; - launch_normal(g_launcher, dir[0] ? dir : NULL); -} - -BOOL enable_privilege(const WCHAR *name) -{ - HANDLE t; TOKEN_PRIVILEGES tp; BOOL r = FALSE; - if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &t)) return FALSE; - tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - if (LookupPrivilegeValueW(NULL, name, &tp.Privileges[0].Luid)) - r = AdjustTokenPrivileges(t, FALSE, &tp, 0, NULL, NULL) && GetLastError() == ERROR_SUCCESS; - CloseHandle(t); - return r; -} - -// One of the user's shell folders, from their registry (redirected or localized folders, e.g. XP's -// "「開始」功能表"); \ when it is not set. The values come from another user's hive: all copies are bounded. -// out holds cch characters; the result is shorter than cch or empty (then the caller skips the folder). -static void user_folder(HKEY hive, const WCHAR *profile, const WCHAR *name, const WCHAR *def, WCHAR *out, int cch) -{ - static WCHAR v[MAX_PATH], t[2 * MAX_PATH + 16]; DWORD n; - out[0] = 0; - if (hive && reg_str(hive, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", name, v, MAX_PATH, 0)) { - t[0] = 0; - if (!lstrcmpiW(lstrcpynW(t, v, 14), L"%USERPROFILE%")) { // that user's, not ours - if (lstrlenW(profile) + lstrlenW(v + 13) + 1 <= (int)(sizeof(t) / sizeof(t[0]))) cat3(t, profile, v + 13, NULL); else t[0] = 0; - } - else lstrcpyW(t, v); // v is at most MAX_PATH - if (t[0]) { - n = ExpandEnvironmentStringsW(t, out, cch); - if (n && n <= (DWORD)cch) return; - } - out[0] = 0; - } - if (lstrlenW(profile) + 1 + lstrlenW(def) < cch) cat3(out, profile, L"\\", def); -} - -#define HIVE_PREFIX L"DynaRunFix-uninstall" -#define FOLDER_CCH (2 * MAX_PATH) // buffer of a folder path; 64 characters of it stay free for what is appended - -// A hive that an earlier uninstall loaded and could not unload (it was killed) would keep that user's NTUSER.DAT locked. -static void unload_leftover_hives(void) -{ - static WCHAR names[16][80]; WCHAR name[260]; DWORD i, n; int k = 0; - for (i = 0; k < 16 && (n = 260, !RegEnumKeyExW(HKEY_USERS, i, name, &n, NULL, NULL, NULL, NULL)); i++) - if (n >= 20 && n < 80 && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, name, 20, HIVE_PREFIX, 20) == CSTR_EQUAL) lstrcpyW(names[k++], name); - while (k) RegUnLoadKeyW(HKEY_USERS, names[--k]); -} - -static void unretarget_user(const WCHAR *sid, const WCHAR *profile) -{ - static int seq; - static WCHAR p[FOLDER_CCH], sub[300], tmphive[80]; HKEY hive = NULL; BOOL loaded = FALSE; int id; - wsprintfW(tmphive, HIVE_PREFIX L"-%lu-%lu-%d", GetCurrentProcessId(), GetTickCount(), seq++); // unique: never a name another load uses - if (lstrlenW(profile) + 12 >= MAX_PATH) return; // (the caller checked; NTUSER.DAT must fit) - if (RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &hive)) { // not signed in: load their hive - hive = NULL; - if (!RegLoadKeyW(HKEY_USERS, tmphive, cat3(p, profile, L"\\NTUSER.DAT", NULL)) && - !RegOpenKeyExW(HKEY_USERS, tmphive, 0, KEY_READ | KEY_WRITE, &hive)) loaded = TRUE; - else hive = NULL; - } - for (id = 0; id < SF_COUNT; id++) { // the same folders as unretarget_shell, of their account - if (!(SF[id].uses & SFU_UNRET)) continue; - user_folder(hive, profile, SF[id].reg, SF[id].rel, p, FOLDER_CCH - 64); - if (!p[0]) continue; - if (SF[id].sub) lstrcatW(p, SF[id].sub); - unretarget(p, (SF[id].deep & SFU_UNRET) != 0); - } - if (hive) { - drop_run(hive, TRUE); drop_policy(hive); // start at sign-in, Windows 11 pin - // their backups are of no use any more - RegDeleteKeyW(hive, cat3(sub, USERKEY, L"\\Shortcuts", NULL)); - RegDeleteKeyW(hive, USERKEY); - RegCloseKey(hive); - } - if (loaded) RegUnLoadKeyW(HKEY_USERS, tmphive); -} - -// skip: the user who runs the uninstall; their own shortcuts, backups and Run value are put back by the user-side stage -static void unretarget_all_users(const WCHAR *skip) -{ - static const WCHAR LIST[] = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"; - static WCHAR sid[200], key[300], raw[MAX_PATH], profile[MAX_PATH]; HKEY k; DWORD i, n; - enable_privilege(L"SeRestorePrivilege"); enable_privilege(L"SeBackupPrivilege"); // to load the hives of users not signed in - unload_leftover_hives(); - if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, LIST, 0, KEY_READ | WOW, &k)) return; // the 64-bit view: where the system keeps the profiles - for (i = 0; n = 200, !RegEnumKeyExW(k, i, sid, &n, NULL, NULL, NULL, NULL); i++) { - // people (local, domain, Microsoft Entra), not service accounts - if (lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-5-21") && lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-12-1")) continue; - if (skip && skip[0] && !lstrcmpiW(sid, skip)) continue; - if (!reg_str(HKEY_LOCAL_MACHINE, cat3(key, LIST, L"\\", sid), L"ProfileImagePath", raw, MAX_PATH, WOW)) continue; - if (ExpandEnvironmentStringsW(raw, profile, MAX_PATH) - 1 >= MAX_PATH || !exists(profile)) continue; - unretarget_user(sid, profile); - } - RegCloseKey(k); -} - -/* ---------- stages ---------- */ - -// the command line of the elevated stage is not trusted: a user SID is a SID of a real account with a profile on this machine -BOOL valid_user_sid(const WCHAR *sid) -{ - static WCHAR key[300]; PSID s; BOOL ok = FALSE; - if (lstrlenW(sid) < 8 || lstrlenW(sid) > 120 || (lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-5-21") && lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-12-1"))) return FALSE; - if (!ConvertStringSidToSidW(sid, &s)) return FALSE; - LocalFree(s); - cat3(key, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\", sid, NULL); - { HKEY k; if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, key, 0, KEY_QUERY_VALUE | WOW, &k)) { RegCloseKey(k); ok = TRUE; } } - return ok; -} - -static int machine_install_locked(const install_opts *o, const WCHAR *sid) -{ - static WCHAR p[MAX_PATH + 16]; - HKEY k; trust_ctx t; - if (!valid_user_sid(sid)) { error2(o, T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } - zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); - if (still_running(o, L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since - if (!check_exe(&t)) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } - CreateDirectoryW(g_dir, NULL); - if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || - !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { - error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); - return RC_FAIL; - } - { // Locale Emulator, resources 10.. (see setup.rc) - static const WCHAR *le[] = { L"LEProc.exe", L"LoaderDll.dll", L"LocaleEmulator.dll", L"LECommonLibrary.dll", - L"LEConfig.xml", L"COPYING", L"COPYING.LESSER", L"THIRD-PARTY.txt" }; - int i; - CreateDirectoryW(cat3(p, g_dir, L"\\le", NULL), NULL); - for (i = 0; i < (int)(sizeof(le) / sizeof(le[0])); i++) - if (!extract(10 + i, cat3(p, g_dir, L"\\le\\", le[i]))) { - error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); - return RC_FAIL; - } - } - cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); - if (lstrcmpiW(p, g_self) && ((!CopyFileW(g_self, p, FALSE) && !(move_aside(p) && CopyFileW(g_self, p, FALSE))) || !same_file(g_self, p))) { - error2(o, T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); - return RC_FAIL; - } - driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) - if (!app_key_create(APP_MACHINE, &k)) return RC_FAIL; - set_str(k, L"InstallDir", g_dir); - if (t.exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use - else RegDeleteValueW(k, L"DynaRunExe"); - - touch_report(o, recover_exe_time()); // a time stamp an earlier run could not put back - // the per-user COM registrations are not copied to HKLM any more (records of older versions are left alone) - if (!t.exe_trusted) setup_log(L"DynaRun's folder or exe can be changed by ordinary users (or its path contains a link): no manifest was written"); - - // garbled text under "Beta: Use Unicode UTF-8": external manifest with activeCodePage=Legacy - if (GetACP() == CP_UTF8 && t.exe_trusted && !exists(cat3(p, g_exe, L".manifest", NULL)) && extract(3, p)) { - set_dword(k, L"ManifestInstalled", 1); - touch_report(o, touch(g_exe)); - } - - { HKEY b; if (!RegCreateKeyExW(k, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) { fix_shortcuts(TRUE, b); RegCloseKey(b); } } - taskbar_policy(o, sid, k); - RegCloseKey(k); - - if (!RegCreateKeyExW(HKEY_LOCAL_MACHINE, UNINSTKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { - set_str(k, L"DisplayName", L"DynaRunFix"); - set_str(k, L"DisplayVersion", WIDEN(DRF_DISPLAY)); - set_str(k, L"Publisher", L"DynaRunFix (github.com/timliudev/DynaRunFix)"); - set_str(k, L"URLInfoAbout", L"https://github.com/timliudev/DynaRunFix"); - set_str(k, L"InstallLocation", g_dir); - cat3(p, g_dir, L"\\DynaRunFix-Setup.exe,0", NULL); set_str(k, L"DisplayIcon", p); // our icon, not DynaRun's - cat3(p, L"\"", g_dir, L"\\DynaRunFix-Setup.exe\" /uninstall"); set_str(k, L"UninstallString", p); - set_dword(k, L"NoModify", 1); set_dword(k, L"NoRepair", 1); set_dword(k, L"EstimatedSize", 100); - RegCloseKey(k); - } - return RC_OK; -} - -static int machine_install(const install_opts *o, const WCHAR *sid) -{ - int rc; - if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } - rc = machine_install_locked(o, sid); - stage_unlock(); - return rc; -} - -// Text of the manifest that DynaRunFix 1.3.6 wrote (its embedded file had a different comment and the assembly version 3.26.0.0). -static const char MANIFEST_V136[] = - "\n" - "\n" - "\n" - " \n" - " \n" - " \n" - " Legacy\n" - " \n" - " \n" - "\n"; - -// The file on disk is d, line endings (CR) not counted: a checkout with CRLF and one with LF build the same manifest. -static BOOL same_text(const WCHAR *p, const void *d, DWORD n) -{ - DWORD got, i = 0, j = 0; BYTE *f = read_file(p, &got); const BYTE *x = d; BOOL r = f != NULL; - while (r) { - while (i < got && f[i] == '\r') i++; - while (j < n && x[j] == '\r') j++; - if (i >= got || j >= n) { r = i >= got && j >= n; break; } - r = f[i++] == x[j++]; - } - release(f); - return r; -} - -// skip_sid: the user who started the uninstall (their own part is done by the user-side stage afterwards) -// The manifest next to DynaRun V3.exe is deleted only when it is a file DynaRunFix wrote: the same text as the embedded one or, for -// files of earlier versions, the same with the older assembly identity version (3.26.0.0) or the text of release 1.3.6. -static BOOL manifest_is_ours(const WCHAR *p) -{ - static const char NEWV[] = "version=\"1.0.0.0\"", OLDV[] = "version=\"3.26.0.0\""; - HRSRC r = FindResourceW(NULL, MAKEINTRESOURCEW(3), (LPCWSTR)RT_RCDATA); const char *d; char *c; DWORD n, i, nl = sizeof(NEWV) - 1, ol = sizeof(OLDV) - 1, j; BOOL ok = FALSE; - if (!r) return FALSE; - d = LockResource(LoadResource(NULL, r)); n = SizeofResource(NULL, r); - if (!d || !n) return FALSE; - if (same_text(p, d, n)) return TRUE; - if (same_text(p, MANIFEST_V136, sizeof(MANIFEST_V136) - 1)) return TRUE; - for (i = 0; i + nl <= n; i++) { - for (j = 0; j < nl && d[i + j] == NEWV[j]; j++); - if (j < nl) continue; - if ((c = alloc(n + ol))) { - for (j = 0; j < i; j++) c[j] = d[j]; - for (j = 0; j < ol; j++) c[i + j] = OLDV[j]; - for (j = i + nl; j < n; j++) c[j - nl + ol] = d[j]; - ok = same_text(p, c, n - nl + ol); - release(c); - } - break; - } - return ok; -} - -// The user's Windows 11 taskbar policy (sid: the user's SID; their hive is loaded when they are signed in): removed when it is ours -static void drop_policy_of(const WCHAR *sid) -{ - HKEY k; - if (!sid || !sid[0] || !valid_user_sid(sid)) return; - if (!RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } -} - -static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) -{ - static WCHAR p[MAX_PATH + 16], polsid[200]; static const WCHAR KEEP[] = L"com-copied.txt\0com-backup\0com-skipped.txt\0setup.log\0"; // the record of the COM copies stays - HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; trust_ctx t; - if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } - zero(&t, sizeof(t)); - if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(t.user_sid, skip_sid, 200); else skip_sid = NULL; - if (o->close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open - if (!g_exe[0]) locate_dynarun(); // not recorded (an untrusted path is not): the default folders - check_exe(&t); // g_exe is from HKLM; t.exe_trusted says whether to run it - if (touch_report(o, recover_exe_time()) == TOUCH_STUCK) stuck = TRUE; - restore_shortcuts(HKEY_LOCAL_MACHINE, APPKEY); - unretarget_shell(TRUE); - // the taskbar policy (Windows 11) of the user who runs the uninstall and of the user the install recorded: read before the key goes - polsid[0] = 0; reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"TaskbarPolicySid", polsid, 200, 0); - drop_policy_of(skip_sid); drop_policy_of(polsid); - unretarget_all_users(skip_sid); - if (app_key_open(APP_MACHINE, KEY_READ, &k)) { RegQueryValueExW(k, L"ManifestInstalled", NULL, NULL, (BYTE *)&man, &n); RegCloseKey(k); } - if (man == 1 && g_exe[0] && !t.exe_trusted && exists(cat3(p, g_exe, L".manifest", NULL))) - setup_log(L"the manifest next to DynaRun V3.exe was left in place: DynaRun's folder or exe can be changed by ordinary users, so nothing is deleted or scheduled for deletion there"); - else if (man == 1 && g_exe[0] && exists(cat3(p, g_exe, L".manifest", NULL))) { - if (!manifest_is_ours(p)) setup_log(L"the manifest next to DynaRun V3.exe is not the file DynaRunFix wrote (it was changed): left in place"); - else if (DeleteFileW(p)) deleted = TRUE; - else if (move_aside(p)) { // in use: renamed at once (a reinstall before the restart writes its own file), the renamed one goes at the next restart - deleted = TRUE; - warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); - } - else { // cannot even be renamed: it goes at the next restart, and the record that it is ours stays - MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); keep = TRUE; - warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); - } - } - if (deleted) { if (t.exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } - // Before the key goes: the driver part ends a waiting task and resumes BitLocker if DynaRunFix suspended it (the flag is in the key). - // An installed driver stays. - driver_machine_uninstall(); - // stuck: the original time stamp of DynaRun V3.exe could not be put back; ExeOrigTimePath / ExeOrigTime stay for the next install or - // uninstall (touch_report says so) - the key is not deleted then - if (!keep && !stuck) RegDeleteKeyW(HKEY_LOCAL_MACHINE, APPKEY); - else if (app_key_open(APP_MACHINE, KEY_SET_VALUE, &k)) { // what is no longer needed; ManifestInstalled, DynaRunExe (and the time stamp record) stay - RegDeleteValueW(k, L"InstallDir"); RegDeleteValueW(k, L"TaskbarPolicy"); RegDeleteValueW(k, L"TaskbarPolicySid"); RegCloseKey(k); - } - RegDeleteKeyW(HKEY_LOCAL_MACHINE, UNINSTKEY); - delete_tree(g_dir, KEEP, 0); - stage_unlock(); - return RC_OK; -} - -static int memcmp_(const BYTE *x, const BYTE *y, DWORD n) { while (n--) if (*x++ != *y++) return 1; return 0; } - -// A copy of src as the new file dst (CREATE_NEW: nothing that exists is used), then held: the returned handle is opened for reading -// with FILE_SHARE_READ only, so as long as it is open nobody can write to, rename or delete the copy, and the content read through it -// is compared with src. INVALID_HANDLE_VALUE when any step fails (dst is deleted again). The copy can be run while the handle is open. -static HANDLE copy_and_hold(const WCHAR *src, const WCHAR *dst) -{ - static BYTE a[65536], b[65536]; - HANDLE s, d, h = INVALID_HANDLE_VALUE; DWORD na, nb; BOOL ok = TRUE; BY_HANDLE_FILE_INFORMATION bi; - s = CreateFileW(src, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (s == INVALID_HANDLE_VALUE) return h; - d = CreateFileW(dst, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); - if (d == INVALID_HANDLE_VALUE) { CloseHandle(s); return h; } - while (ok && ReadFile(s, a, sizeof(a), &na, NULL) && na) ok = WriteFile(d, a, na, &nb, NULL) && nb == na; - CloseHandle(d); - if (ok) { - h = CreateFileW(dst, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL); - if (h != INVALID_HANDLE_VALUE) { - if (!GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) ok = FALSE; - SetFilePointer(s, 0, NULL, FILE_BEGIN); - while (ok) { // the held copy has the content of the original - BOOL r1 = ReadFile(s, a, sizeof(a), &na, NULL), r2 = ReadFile(h, b, sizeof(b), &nb, NULL); - if (!r1 || !r2 || na != nb || memcmp_(a, b, na)) { ok = FALSE; break; } - if (!na) break; - } - if (!ok) { CloseHandle(h); h = INVALID_HANDLE_VALUE; } - } - } - CloseHandle(s); - if (h == INVALID_HANDLE_VALUE) DeleteFileW(dst); - return h; -} - -// Runs " " elevated (one UAC prompt) and returns its exit code; 2 = cancelled. exe: the program to run, NULL = this one. -int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o) -{ - SHELLEXECUTEINFOW se; DWORD rc = RC_FAIL, err, tn; static WCHAR a[1024], root[4], tmp[MAX_PATH]; const WCHAR *exe = exe_path ? exe_path : g_self; HANDLE hold = INVALID_HANDLE_VALUE; LARGE_INTEGER q; - lstrcpyW(a, args); if (o->quiet) lstrcatW(a, L" /quiet"); if (o->close) lstrcatW(a, L" /close"); - if (o->pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin - if (o->keep) lstrcatW(a, L" /keep"); - // Mapped network drives (e.g. a shared folder) do not exist for the elevated process: run a copy in %TEMP% - lstrcpynW(root, g_self, 4); - if (!exe_path && root[1] == ':' && GetDriveTypeW(root) == DRIVE_REMOTE && (tn = GetTempPathW(MAX_PATH - 60, tmp)) != 0 && tn < MAX_PATH - 60) { // room for the longest name below (17 + 3 x 10 digits + 2 + 4 + NUL) - q.QuadPart = 0; QueryPerformanceCounter(&q); - wsprintfW(tmp + lstrlenW(tmp), L"DynaRunFix-Setup-%lu-%lu-%lu.exe", GetCurrentProcessId(), GetTickCount(), q.LowPart); - // the copy is held open (nobody can change it) from here until the elevated process has ended: what the UAC prompt names is what runs - if ((hold = copy_and_hold(g_self, tmp)) != INVALID_HANDLE_VALUE) exe = tmp; - } - zero(&se, sizeof(se)); - se.cbSize = sizeof(se); se.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC; - se.hwnd = g_hwnd; se.lpVerb = L"runas"; se.lpFile = exe; se.lpParameters = a; se.nShow = SW_SHOWNORMAL; - if (!ShellExecuteExW(&se)) { - err = GetLastError(); - if (hold != INVALID_HANDLE_VALUE) { CloseHandle(hold); DeleteFileW(tmp); } - return err == ERROR_CANCELLED ? RC_CANCEL : RC_FAIL; - } - WaitForSingleObject(se.hProcess, INFINITE); - GetExitCodeProcess(se.hProcess, &rc); - CloseHandle(se.hProcess); - if (hold != INVALID_HANDLE_VALUE) { CloseHandle(hold); DeleteFileW(tmp); } - return (int)rc; -} - -static const WCHAR *no_admin(void) -{ return T(L"Administrator permission was not granted. Nothing was changed.", L"未取得系統管理員權限,沒有做任何變更。"); } - -// Elevated: install DynaRun from its MSI, then the machine stage. The MSI installs per user (as the -// original setup does), so it must run as the user who asked for it: with over-the-shoulder -// elevation (another account typed into the UAC prompt) return 3 and let the caller run it. -// -// The MSI comes from a folder the user can write (Downloads, %TEMP%), and msiexec runs it with the rights of this stage: -// the file is first copied into a new folder only administrators and SYSTEM can use (%WINDIR%\Temp\DynaRunFix-...; created -// here with that ACL, so nobody else could have prepared it), checked there again (it must be Dyna Pro's DynaRun setup) and -// run from there; the copy is deleted afterwards. Cabinet files the MSI names outside itself are copied along. -static BOOL make_admin_dir(const WCHAR *base, WCHAR *out) // base: the folder in which it is created (%WINDIR%\Temp) -{ - static const WCHAR sddl[] = L"D:P(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)"; // protected: Administrators and SYSTEM, full control - PSECURITY_DESCRIPTOR sd; SECURITY_ATTRIBUTES sa; int i; BOOL ok = FALSE; LARGE_INTEGER q; - if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl, SDDL_REVISION_1, &sd, NULL)) return FALSE; - sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = sd; sa.bInheritHandle = FALSE; - for (i = 0; i < 20 && !ok; i++) { // a name that exists already is refused by CreateDirectory: try another - q.QuadPart = 0; QueryPerformanceCounter(&q); - lstrcpynW(out, base, MAX_PATH - 60); - wsprintfW(out + lstrlenW(out), L"\\DynaRunFix-%lu-%lu-%lu", GetCurrentProcessId(), GetTickCount(), q.LowPart + i); - ok = CreateDirectoryW(out, &sa); - } - LocalFree(sd); - return ok; -} - -static BOOL copy_new(const WCHAR *from, const WCHAR *to) // to must not exist -{ - static BYTE buf[65536]; HANDLE in, out; DWORD n, w; BOOL ok = TRUE; - in = CreateFileW(from, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); - if (in == INVALID_HANDLE_VALUE) return FALSE; - out = CreateFileW(to, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); - if (out == INVALID_HANDLE_VALUE) { CloseHandle(in); return FALSE; } - while (ok && ReadFile(in, buf, sizeof(buf), &n, NULL) && n) ok = WriteFile(out, buf, n, &w, NULL) && w == n; - CloseHandle(in); - ok = ok && FlushFileBuffers(out); - CloseHandle(out); - if (!ok) DeleteFileW(to); - return ok; -} - -static void drop_admin_dir(const WCHAR *dir) { delete_tree(dir, NULL, 0); } - -static BOOL stage_msi(const WCHAR *msi, WCHAR *dir, WCHAR *staged) -{ - WCHAR *cabs, *c, src[MAX_PATH], dst[MAX_PATH], base[MAX_PATH], *e; int n; - GetWindowsDirectoryW(base, MAX_PATH - 20); lstrcatW(base, L"\\Temp"); - if (!make_admin_dir(base, dir)) return FALSE; - cat3(staged, dir, L"\\Setup.msi", NULL); - if (!copy_new(msi, staged)) { drop_admin_dir(dir); return FALSE; } - if ((cabs = msi_external_files(staged))) { // the names are read from the copy that is going to be run (and checked), not from the user's file - lstrcpynW(src, msi, MAX_PATH); for (e = src + lstrlenW(src); e > src && *e != '\\'; e--); n = (int)(e - src); - for (c = cabs; *c; c += lstrlenW(c) + 1) - if (n + 1 + lstrlenW(c) < MAX_PATH && dir[0] && lstrlenW(dir) + 1 + lstrlenW(c) < MAX_PATH) { - lstrcpynW(src + n + 1, c, MAX_PATH - n - 1); src[n] = '\\'; - copy_new(src, cat3(dst, dir, L"\\", c)); // a missing one is msiexec's error to report - } - release(cabs); - } - return TRUE; -} - -static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *msi) -{ - static WCHAR me[200], txt[300], dir[MAX_PATH], staged[MAX_PATH]; int rc; - if (!user_sid(me, 200) || lstrcmpiW(me, sid)) return RC_OVER_SHOULDER; - if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } - if (still_running(o, L"full install")) { stage_unlock(); return RC_RUNNING; } - if (!stage_msi(msi, dir, staged)) { - stage_unlock(); - msg(T(L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。"), MB_ICONERROR); - return RC_MSI_FAILED; - } - if (msi_check(staged, NULL, 0) != MSI_DYNARUN) { // the check of the copy that is going to be run - drop_admin_dir(dir); stage_unlock(); - msg(T(L"This is not the DynaRun V3 setup, so it will not be installed.", L"這不是 DynaRun V3 的安裝檔,所以不會安裝它。"), MB_ICONERROR); - return RC_MSI_FAILED; - } - rc = run_msiexec(staged); - drop_admin_dir(dir); - stage_unlock(); - if (rc != 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { - if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; - wsprintfW(txt, T(L"The DynaRun V3 setup failed (Windows Installer error %d).", L"DynaRun V3 安裝失敗(Windows Installer 錯誤 %d)。"), rc); - if (rc == ERROR_INSTALL_ALREADY_RUNNING) - lstrcatW(txt, T(L"\n\nAnother installation is running. Wait until it finishes and try again.", L"\n\n有其他程式正在安裝,請等它完成後再試一次。")); - msg(txt, MB_ICONERROR); - return RC_MSI_FAILED; - } - if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return RC_NO_DYNARUN; } - return machine_install(o, sid); -} - -// Not elevated: the current user's shortcuts; a desktop shortcut when there is none at all. -static void user_stage(const install_opts *o, install_result *res) -{ - static WCHAR p[MAX_PATH]; HKEY b, s; - res->pin = PIN_NOT_ASKED; - if (!app_key_create(APP_USER, &b)) return; - // another version is installed now: what update.c noted no longer applies; the first start of DynaRun looks again - RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); - if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { - int desk = fix_shortcuts(FALSE, s); - if (!desk && sf_path(SF_DESKTOP, TRUE, p)) - desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here - if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p) && - save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) - RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); - if (o->pin && !o->keep) { - res->pin = PIN_FAILED; - if (desktop_link(p, s) && pin_taskbar(p, &res->pin) && res->pin == PIN_FAILED) res->pin = PIN_DONE; - } - RegCloseKey(s); - } - RegCloseKey(b); - if (!o->keep && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { // start at sign-in - // with the driver step ahead the value is NOT written now: the sign-in after the driver restart would start DynaRun while the - // driver is being installed. It is only noted (AutostartPending) and registered by autostart_flush when the driver steps are over. - if (o->autostart && !o->autostart_later) set_str(s, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); - else RegDeleteValueW(s, L"DynaRunFix"); - RegCloseKey(s); - if (app_key_create(APP_USER, &b)) { - if (o->autostart && o->autostart_later) set_dword(b, L"AutostartPending", 1); - else RegDeleteValueW(b, L"AutostartPending"); - RegCloseKey(b); - } - } - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); -} - -// The end of the driver steps (installed, failed, cancelled, skipped, wizard closed): the sign-in start the user asked for is registered now, -// whatever the driver's outcome. Not called while a restart for the driver is pending (wizard.c). Uninstall removes the flag with HKCU\Software\DynaRunFix. -void autostart_flush(void) -{ - static WCHAR p[MAX_PATH]; HKEY k, r; DWORD v = 0, n = 4, t = 0; BOOL pending; - if (!app_key_open(APP_USER, KEY_ALL_ACCESS, &k)) return; - pending = RegQueryValueExW(k, L"AutostartPending", NULL, &t, (BYTE *)&v, &n) == ERROR_SUCCESS && t == REG_DWORD && v; - if (pending) RegDeleteValueW(k, L"AutostartPending"); - RegCloseKey(k); - if (pending && exists(g_launcher) && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &r, NULL)) { - set_str(r, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); - RegCloseKey(r); - } -} - -// The fix is installed and is this version: the uninstall entry names our version and the launcher and the dll are in the install folder. (A dev build -// shows its commit as the version, so a rebuilt one is "not current" until it has been installed once.) -BOOL fix_current(void) -{ - static WCHAR v[64], d[MAX_PATH]; - if (!reg_str(HKEY_LOCAL_MACHINE, UNINSTKEY, L"DisplayVersion", v, 64, 0) || lstrcmpW(v, WIDEN(DRF_DISPLAY))) return FALSE; - return exists(g_launcher) && exists(cat3(d, g_dir, L"\\dynafix.dll", NULL)); -} - -// The wizard's one elevated stage may do the driver's preparation too (zip != NULL; see driver_prep_stage): the fix and the driver then share ONE UAC prompt. -// The result: the fix's failure code, or, when the fix is installed (user stage done), RC_OK or the driver's code (DRV_E_*, DRV_W_BDE: >= DRV_E_FIRST). FIX_DONE (errors.h) tells which. - -static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) // the command line of the elevated stage -{ - if (!zip) return; - lstrcatW(args, L" /with-driver \""); lstrcatW(args, zip); lstrcatW(args, L"\""); - if (accept && accept[0]) { lstrcatW(args, L" /unverified "); lstrcatW(args, accept); } -} - -int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept) -{ - static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; - if (!user_sid(sid, 200)) return RC_FAIL; - if (running_early(o, L"install (before elevation)")) return RC_RUNNING; // before the UAC prompt; /quiet without /close ends here - CoInitialize(NULL); - if (is_admin()) { rc = machine_install(o, sid); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } - else { - wsprintfW(args, L"/machine %s \"%s\"", sid, g_exe); - add_driver_args(args, zip, accept); - if ((rc = run_elevated(args, NULL, o)) == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } - } - if (!FIX_DONE(rc)) return rc; - user_stage(o, res); - return rc; -} - -int install_fix(const install_opts *o, install_result *res) { return install_fix_driver(o, res, NULL, NULL); } - -int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) -{ - static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; - if (!user_sid(sid, 200)) return RC_FAIL; - if (running_early(o, L"full install (before elevation)")) return RC_RUNNING; - CoInitialize(NULL); - pkg_prepare_documents(msi); // as this user, before the SYSTEM-side install (error 1305 otherwise) - if (is_admin()) { rc = machine_full(o, sid, msi); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } - else { - wsprintfW(args, L"/full %s \"%s\"", sid, msi); - add_driver_args(args, zip, accept); - rc = run_elevated(args, NULL, o); - } - if (rc == RC_OVER_SHOULDER) { // over-the-shoulder elevation: setup as this user, then the fix (and the driver) - rc = run_msiexec(msi); - if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; - if (rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED) return RC_MSI_FAILED; - if (!locate_dynarun()) return RC_NO_DYNARUN; - return install_fix_driver(o, res, zip, accept); - } - if (rc == RC_CANCEL) return RC_CANCEL; - if (!FIX_DONE(rc)) return rc; - locate_dynarun(); - user_stage(o, res); - return rc; -} - -int install_all(const install_opts *o, install_result *res, const WCHAR *msi) { return install_all_driver(o, res, msi, NULL, NULL); } - -// The uninstall question's Yes / No buttons become "Fix only" / "Remove all". -static HHOOK g_cbt; -static LRESULT CALLBACK relabel(int code, WPARAM w, LPARAM l) -{ - HHOOK h = g_cbt; - if (code == HCBT_ACTIVATE) { - SetDlgItemTextW((HWND)w, IDYES, T(L"&Fix only", L"只移除修正(&F)")); - SetDlgItemTextW((HWND)w, IDNO, T(L"&Remove all", L"全部移除(&R)")); - UnhookWindowsHookEx(g_cbt); g_cbt = NULL; - } - return CallNextHookEx(h, code, w, l); -} - -// Copies of the uninstaller that earlier builds (1.4.0-rc1/rc2) left in %TEMP%; a running one stays -static const WCHAR TEMP_PREFIX[] = L"DynaRunFix-uninstall-"; -static void drop_old_temp_copies(void) -{ - WCHAR t[MAX_PATH], f[MAX_PATH]; WIN32_FIND_DATAW fd; HANDLE h; DWORD n = GetTempPathW(MAX_PATH - 60, t); - if (!n || n >= MAX_PATH - 60) return; - lstrcatW(t, TEMP_PREFIX); - h = FindFirstFileW(cat3(f, t, L"*.exe", NULL), &fd); - if (h == INVALID_HANDLE_VALUE) return; - do { - if ((fd.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || lstrlenW(t) + lstrlenW(fd.cFileName) >= MAX_PATH) continue; - GetTempPathW(MAX_PATH - 60, f); lstrcatW(f, fd.cFileName); - DeleteFileW(f); - } while (FindNextFileW(h, &fd)); - FindClose(h); -} - -static int uninstall(const install_opts *given) -{ - static WCHAR p[MAX_PATH + 160], prod[40], sid[200], args[260]; int rc; BOOL all = FALSE; - install_opts mine = *given, *o = &mine; // the "close DynaRun" consent asked below is this uninstall's own - const WCHAR *elev = NULL; - // The uninstaller runs from the installed program (no copy in %TEMP%): the elevated stage renames what it cannot delete (this very - // file, while the user-side stage still runs) and Windows deletes it at the next restart. - cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); - drop_old_temp_copies(); // copies that earlier builds left in %TEMP% - if (!exists(p) && !reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0)) { - info(o, T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); - return RC_OK; - } - if (!o->quiet && !dynarun_product(prod)) { - if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; - } else if (!o->quiet) { - g_cbt = SetWindowsHookExW(WH_CBT, relabel, NULL, GetCurrentThreadId()); - rc = msg(T(L"Uninstall DynaRunFix?\n\n" - L"Fix only: the DynaRun shortcuts start the original DynaRun again; DynaRun V3 stays installed.\n\n" - L"Remove all: DynaRun V3 is then uninstalled too, by Dyna Pro's own uninstaller.", - L"要解除安裝 DynaRunFix 嗎?\n\n" - L"只移除修正:DynaRun 的捷徑改回開啟原版,DynaRun V3 保留。\n\n" - L"全部移除:接著再用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3。"), MB_YESNOCANCEL | MB_ICONQUESTION); - if (g_cbt) { UnhookWindowsHookEx(g_cbt); g_cbt = NULL; } - if (rc == IDCANCEL) return RC_CANCEL; - all = rc == IDNO; - } - // Running DynaRun / launcher / LEProc hold files of the folder: ask first, the elevated stage then closes them. - // /quiet without /close goes on; what is still in use is deleted at the next restart. - if (!o->quiet && running_programs()) { - if (msg(T(L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" - L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", - L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" - L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; - o->close = TRUE; - } - reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); - if (!g_exe[0]) locate_dynarun(); - CoInitialize(NULL); - if (!user_sid(sid, 200)) sid[0] = 0; - // The elevated stage comes first: when the UAC prompt is cancelled (or fails to start) nothing has been changed yet and the - // user's shortcuts, Run value and pins still work with the installed launcher. - if (is_admin()) rc = machine_uninstall(o, sid); - else { - trust_ctx t; - // What runs elevated is the installed program in Program Files (administrators-only folder), never a copy in a folder the user can - // write. Only when there is no installed copy (a broken install) is this file itself run, if it is not one of our %TEMP% copies. - zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); - if (exists(p)) { - if (g_is_writable(&t, g_dir) || g_is_writable(&t, p)) { - error2(o, T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", - L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); - return RC_FAIL; - } - elev = p; - } - wsprintfW(args, L"/unmachine %s", sid); - rc = run_elevated(sid[0] ? args : L"/unmachine", elev, o); - } - if (rc == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } - if (rc) { // the elevated stage did not finish: nothing user-side is undone either - if (!o->quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } - return rc; - } - restore_shortcuts(HKEY_CURRENT_USER, USERKEY); - RegDeleteKeyW(HKEY_CURRENT_USER, USERKEY); - drop_run(HKEY_CURRENT_USER, FALSE); - drop_policy(HKEY_CURRENT_USER); // the Windows 11 taskbar policy of this user, when it is still ours - unretarget_shell(FALSE); // our shortcuts that no backup covered, and pins the user made - SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); - if (all) { - rc = remove_dynarun(); - if (rc == ERROR_INSTALL_USEREXIT) { info(o, T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } - if (rc > 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { - wsprintfW(p, T(L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", - L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。"), rc); - msg(p, MB_ICONERROR); - return RC_MSI_FAILED; - } - info(o, T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); - return RC_OK; - } - info(o, T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); - return RC_OK; -} - -// the command line after its first n arguments, as typed (the launcher's arguments for /update) -static const WCHAR *after_args(int n) -{ - const WCHAR *c = GetCommandLineW(); BOOL q = FALSE; - while (n--) { - while (*c == ' ' || *c == '\t') c++; - for (; *c && (q || (*c != ' ' && *c != '\t')); c++) if (*c == '"') q = !q; - } - while (*c == ' ' || *c == '\t') c++; - return c; -} - -// The /unverified rule of both stages: argv[i] is /unverified and a value of exactly 64 characters follows it, else NULL. The elevated install stage -// (/machine, /full) looks for it at every position from argv[4] on (cmd_parse), /driver-prep only at argv[4]: the two differ for a hand-typed -// "/driver-prep /quiet /unverified " (see cmd_parse). -static const WCHAR *unverified_value(int argc, WCHAR **argv, int i) -{ - return i + 1 < argc && !lstrcmpiW(argv[i], L"/unverified") && lstrlenW(argv[i + 1]) == 64 ? argv[i + 1] : NULL; -} - -// The switches of the command line, at any position (tests/test_hardening.c and test_driver.c lock the rules). c->opts is set to the defaults -// (install_opts_default), then quiet, close, keep are set and pin, autostart cleared by what the command line says; -// c->pin: /pin seen; c->dzip, c->daccept, c->daccept4: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). -// /with-driver and /unverified take the next argument whatever it is; an /unverified value that is not 64 characters long is dropped. daccept is the last -// valid /unverified at index >= 4 (the install stage's rule), daccept4 the one at index 4 (the driver prep's rule); they differ when -// - a valid /unverified comes later (argv[5] on, e.g. after /quiet): daccept has it, daccept4 is NULL; -// - two valid ones are given: daccept is the later one, daccept4 the one at argv[4]. -// Each verb keeps using the rule it always had (/machine, /full: daccept; /driver-prep: daccept4), so no command line behaves differently from before. -static void cmd_parse(cmdline *c) -{ - int i; const WCHAR *v; - install_opts_default(&c->opts); - c->pin = FALSE; c->dzip = c->daccept = c->daccept4 = NULL; - for (i = 1; i < c->argc; i++) { - WCHAR *a = c->argv[i]; - if (!lstrcmpiW(a, L"/quiet")) c->opts.quiet = TRUE; - else if (!lstrcmpiW(a, L"/close")) c->opts.close = TRUE; - else if (!lstrcmpiW(a, L"/notaskbar")) c->opts.pin = FALSE; - else if (!lstrcmpiW(a, L"/noautostart")) c->opts.autostart = FALSE; - else if (!lstrcmpiW(a, L"/pin")) c->pin = TRUE; - else if (!lstrcmpiW(a, L"/keep")) c->opts.keep = TRUE; - else if (i >= 4 && !lstrcmpiW(a, L"/with-driver") && i + 1 < c->argc) c->dzip = c->argv[++i]; // after the SID and the path: see install_fix_driver - else if (i >= 4 && !lstrcmpiW(a, L"/unverified") && i + 1 < c->argc) { - if ((v = unverified_value(c->argc, c->argv, i)) != NULL) { c->daccept = v; if (i == 4) c->daccept4 = v; } - i++; - } - } -} - -/* ---------- the verbs: argv[1] ---------- */ - -static int cmd_machine(cmdline *c) // /machine [/with-driver [/unverified ]]: the elevated install stage -{ - int rc; - lstrcpynW(g_exe, c->argv[3], MAX_PATH); - CoInitialize(NULL); - rc = machine_install(&c->opts, c->argv[2]); - if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); - return rc; -} - -static int cmd_full(cmdline *c) // /full [/with-driver ...]: the same, with the DynaRun MSI first -{ - int rc; - CoInitialize(NULL); - rc = machine_full(&c->opts, c->argv[2], c->argv[3]); - if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); - return rc; -} - -static int cmd_unmachine(cmdline *c) // /unmachine []: the elevated uninstall stage -{ - reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); - CoInitialize(NULL); - return machine_uninstall(&c->opts, c->argc >= 3 && c->argv[2][0] == 'S' && c->argv[2][1] == '-' ? c->argv[2] : NULL); -} - -static int cmd_uninstall(cmdline *c) { return uninstall(&c->opts); } -static int cmd_checkupdate(cmdline *c) { (void)c; return update_check(); } -static int cmd_update(cmdline *c) { (void)c; return update_run(after_args(2)); } -static int cmd_offer(cmdline *c) { (void)c; return update_offer(); } - -// min_args: the verb is only taken with that many arguments (the program counts); with fewer, the command line is no verb and the wizard (or /quiet install) runs. -// The elevated driver verbs take 2 and check their own arguments (a /driver-prep without them is an error exit, not the wizard). -static const verb VERBS[] = { - { L"/machine", 4, cmd_machine }, - { L"/full", 4, cmd_full }, - { L"/unmachine", 2, cmd_unmachine }, - { L"/driver", 2, cmd_driver }, // driver.c - { L"/driver-continue", 2, cmd_driver_continue }, - { L"/driver-prep", 2, cmd_driver_prep }, - { L"/driver-install", 2, cmd_driver_install }, - { L"/driver-cancel", 2, cmd_driver_cancel }, - { L"/uninstall", 2, cmd_uninstall }, - { L"/checkupdate", 2, cmd_checkupdate }, - { L"/update", 2, cmd_update }, - { L"/offer", 2, cmd_offer }, -}; - -static const verb *verb_find(int argc, WCHAR **argv) -{ - int i; - if (argc < 2) return NULL; - for (i = 0; i < (int)(sizeof(VERBS) / sizeof(VERBS[0])); i++) - if (argc >= VERBS[i].min_args && !lstrcmpiW(argv[1], VERBS[i].name)) return &VERBS[i]; - return NULL; -} - -// Runs the verb of the command line: its exit code, or DRV_NOVERB when argv[1] is none (the caller then installs or opens the wizard). -static int verb_run(cmdline *c) -{ - const verb *v = verb_find(c->argc, c->argv); - return v ? v->run(c) : DRV_NOVERB; -} +// The rest of this file is split by topic into src\setup\*.h. They are fragments of THIS translation unit (static functions and file-local +// state refer to each other), not headers and not separate .c files: build.cmd and the tests compile only setup.c. The order matters: +// a fragment uses only what the ones before it declare (the few forward declarations are inside the fragments). +#include "setup/defs.h" // constants, globals, trust_ctx +#include "setup/util.h" // system-folder DLLs, memory, files, registry, shared helpers (path_join, run_process, dir_walk, app key) +#include "setup/stage.h" // one elevated stage at a time, setup.log of the install folder +#include "setup/trust.h" // who may change a path; the exe path of the elevated stage +#include "setup/exetime.h" // the borrowed exe time stamp (manifest refresh) +#include "setup/procs.h" // paths, programs that hold the files, the user log, closing DynaRun +#include "setup/shortcuts.h" // shortcut folders, shortcut files, backups and restore +#include "setup/taskbar.h" // sign-in start, taskbar pin, unelevated start of DynaRun +#include "setup/users.h" // other accounts' hives (uninstall) +#include "setup/machine.h" // machine stage: install and uninstall +#include "setup/elevate.h" // held copy, run_elevated +#include "setup/msi_stage.h" // the vendor MSI in an administrators-only folder, the full stage +#include "setup/userstage.h" // user stage, the install entry points +#include "setup/uninstall.h" // the uninstall +#include "setup/cmdline.h" // command line parsing, verb table void WinMainCRTStartup(void) { diff --git a/src/setup/cmdline.h b/src/setup/cmdline.h new file mode 100644 index 0000000..0310287 --- /dev/null +++ b/src/setup/cmdline.h @@ -0,0 +1,116 @@ +// Part of setup.c (command line and verb table): included by it, in one compilation unit; never compiled on its own. +// the command line after its first n arguments, as typed (the launcher's arguments for /update) +static const WCHAR *after_args(int n) +{ + const WCHAR *c = GetCommandLineW(); BOOL q = FALSE; + while (n--) { + while (*c == ' ' || *c == '\t') c++; + for (; *c && (q || (*c != ' ' && *c != '\t')); c++) if (*c == '"') q = !q; + } + while (*c == ' ' || *c == '\t') c++; + return c; +} + +// The /unverified rule of both stages: argv[i] is /unverified and a value of exactly 64 characters follows it, else NULL. The elevated install stage +// (/machine, /full) looks for it at every position from argv[4] on (cmd_parse), /driver-prep only at argv[4]: the two differ for a hand-typed +// "/driver-prep /quiet /unverified " (see cmd_parse). +static const WCHAR *unverified_value(int argc, WCHAR **argv, int i) +{ + return i + 1 < argc && !lstrcmpiW(argv[i], L"/unverified") && lstrlenW(argv[i + 1]) == 64 ? argv[i + 1] : NULL; +} + +// The switches of the command line, at any position (tests/test_hardening.c and test_driver.c lock the rules). c->opts is set to the defaults +// (install_opts_default), then quiet, close, keep are set and pin, autostart cleared by what the command line says; +// c->pin: /pin seen; c->dzip, c->daccept, c->daccept4: the values of /with-driver and /unverified (only from argv[4] on, after the verb, the SID and the path). +// /with-driver and /unverified take the next argument whatever it is; an /unverified value that is not 64 characters long is dropped. daccept is the last +// valid /unverified at index >= 4 (the install stage's rule), daccept4 the one at index 4 (the driver prep's rule); they differ when +// - a valid /unverified comes later (argv[5] on, e.g. after /quiet): daccept has it, daccept4 is NULL; +// - two valid ones are given: daccept is the later one, daccept4 the one at argv[4]. +// Each verb keeps using the rule it always had (/machine, /full: daccept; /driver-prep: daccept4), so no command line behaves differently from before. +static void cmd_parse(cmdline *c) +{ + int i; const WCHAR *v; + install_opts_default(&c->opts); + c->pin = FALSE; c->dzip = c->daccept = c->daccept4 = NULL; + for (i = 1; i < c->argc; i++) { + WCHAR *a = c->argv[i]; + if (!lstrcmpiW(a, L"/quiet")) c->opts.quiet = TRUE; + else if (!lstrcmpiW(a, L"/close")) c->opts.close = TRUE; + else if (!lstrcmpiW(a, L"/notaskbar")) c->opts.pin = FALSE; + else if (!lstrcmpiW(a, L"/noautostart")) c->opts.autostart = FALSE; + else if (!lstrcmpiW(a, L"/pin")) c->pin = TRUE; + else if (!lstrcmpiW(a, L"/keep")) c->opts.keep = TRUE; + else if (i >= 4 && !lstrcmpiW(a, L"/with-driver") && i + 1 < c->argc) c->dzip = c->argv[++i]; // after the SID and the path: see install_fix_driver + else if (i >= 4 && !lstrcmpiW(a, L"/unverified") && i + 1 < c->argc) { + if ((v = unverified_value(c->argc, c->argv, i)) != NULL) { c->daccept = v; if (i == 4) c->daccept4 = v; } + i++; + } + } +} + +/* ---------- the verbs: argv[1] ---------- */ + +static int cmd_machine(cmdline *c) // /machine [/with-driver [/unverified ]]: the elevated install stage +{ + int rc; + lstrcpynW(g_exe, c->argv[3], MAX_PATH); + CoInitialize(NULL); + rc = machine_install(&c->opts, c->argv[2]); + if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); + return rc; +} + +static int cmd_full(cmdline *c) // /full [/with-driver ...]: the same, with the DynaRun MSI first +{ + int rc; + CoInitialize(NULL); + rc = machine_full(&c->opts, c->argv[2], c->argv[3]); + if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); + return rc; +} + +static int cmd_unmachine(cmdline *c) // /unmachine []: the elevated uninstall stage +{ + reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); + CoInitialize(NULL); + return machine_uninstall(&c->opts, c->argc >= 3 && c->argv[2][0] == 'S' && c->argv[2][1] == '-' ? c->argv[2] : NULL); +} + +static int cmd_uninstall(cmdline *c) { return uninstall(&c->opts); } +static int cmd_checkupdate(cmdline *c) { (void)c; return update_check(); } +static int cmd_update(cmdline *c) { (void)c; return update_run(after_args(2)); } +static int cmd_offer(cmdline *c) { (void)c; return update_offer(); } + +// min_args: the verb is only taken with that many arguments (the program counts); with fewer, the command line is no verb and the wizard (or /quiet install) runs. +// The elevated driver verbs take 2 and check their own arguments (a /driver-prep without them is an error exit, not the wizard). +static const verb VERBS[] = { + { L"/machine", 4, cmd_machine }, + { L"/full", 4, cmd_full }, + { L"/unmachine", 2, cmd_unmachine }, + { L"/driver", 2, cmd_driver }, // driver.c + { L"/driver-continue", 2, cmd_driver_continue }, + { L"/driver-prep", 2, cmd_driver_prep }, + { L"/driver-install", 2, cmd_driver_install }, + { L"/driver-cancel", 2, cmd_driver_cancel }, + { L"/uninstall", 2, cmd_uninstall }, + { L"/checkupdate", 2, cmd_checkupdate }, + { L"/update", 2, cmd_update }, + { L"/offer", 2, cmd_offer }, +}; + +static const verb *verb_find(int argc, WCHAR **argv) +{ + int i; + if (argc < 2) return NULL; + for (i = 0; i < (int)(sizeof(VERBS) / sizeof(VERBS[0])); i++) + if (argc >= VERBS[i].min_args && !lstrcmpiW(argv[1], VERBS[i].name)) return &VERBS[i]; + return NULL; +} + +// Runs the verb of the command line: its exit code, or DRV_NOVERB when argv[1] is none (the caller then installs or opens the wizard). +static int verb_run(cmdline *c) +{ + const verb *v = verb_find(c->argc, c->argv); + return v ? v->run(c) : DRV_NOVERB; +} + diff --git a/src/setup/defs.h b/src/setup/defs.h new file mode 100644 index 0000000..b0b2112 --- /dev/null +++ b/src/setup/defs.h @@ -0,0 +1,40 @@ +// Part of setup.c (shared constants, globals, trust_ctx): included by it, in one compilation unit; never compiled on its own. +#ifndef SLDF_RUNAS_USER +#define SLDF_RUNAS_USER 0x2000 +#endif +#define WOW KEY_WOW64_64KEY // DynaRun's COM keys: physical view, like 64-bit reg.exe sees them +#define WIDEN2(x) L##x +#define WIDEN(x) WIDEN2(x) + +static const WCHAR APPKEY[] = L"SOFTWARE\\DynaRunFix"; +static const WCHAR USERKEY[] = L"Software\\DynaRunFix"; +static const WCHAR RUNKEY[] = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; +static const WCHAR POLKEY[] = L"Software\\Policies\\Microsoft\\Windows\\Explorer"; +static const WCHAR APPID[] = L"DynaRunFix.DynaRunV3"; // AppUserModelID of the launcher shortcuts and of DynaRun (dynafix.c) +static const PROPERTYKEY PKEY_AUMID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 5 }; +// technique described by Gee Law (geelaw.blog/entries/msedge-pins/), also used by Firefox's PinToTaskbar plugin (MPL 2.0): calling the +// undocumented IPinnedList3::Modify of Explorer's taskband. The GUIDs and the vtable layout are facts of that interface; the code is +// our own (compared with Firefox's PinToTaskbar.cpp: different structure and wording, nothing copied). +static const GUID CLSID_TaskbandPin = { 0x90aa3a4e, 0x1cba, 0x4233, { 0xb8, 0xbb, 0x53, 0x57, 0x73, 0xd4, 0x84, 0x49 } }; +static const GUID IID_IPinnedList3 = { 0x0dd79ae2, 0xd156, 0x45d4, { 0x9e, 0xeb, 0x3b, 0x54, 0x97, 0x69, 0xe9, 0x40 } }; +typedef struct IPinnedList3 { const struct { + HRESULT (STDMETHODCALLTYPE *QueryInterface)(struct IPinnedList3 *, REFIID, void **); + ULONG (STDMETHODCALLTYPE *AddRef)(struct IPinnedList3 *); + ULONG (STDMETHODCALLTYPE *Release)(struct IPinnedList3 *); + void *others[13]; + HRESULT (STDMETHODCALLTYPE *Modify)(struct IPinnedList3 *, PCIDLIST_ABSOLUTE unpin, PCIDLIST_ABSOLUTE pin, int caller); +} *lpVtbl; } IPinnedList3; +static const WCHAR UNINSTKEY[] = L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\DynaRunFix"; +static const WCHAR DYNARUN_REL[] = L"\\Dyna Pro Dynamometers\\DynaRun V3.exe"; + +BOOL g_zh; +int g_drvmode; // DRV_MODE_*: the wizard starts at the driver pages (driver.c, wizard.c) +HWND g_hwnd; +WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; +// What the ACL decisions of a stage depend on, passed to the functions that decide (never kept in a global): each stage makes one. +typedef struct { + WCHAR user_sid[200]; // the user who started the setup (elevated stage: from its command line); "" = none known + BOOL exe_trusted; // set by check_exe: DynaRun's exe and folder cannot be changed by ordinary users +} trust_ctx; +#define TITLE T(L"DynaRunFix Setup", L"DynaRunFix 安裝程式") + diff --git a/src/setup/elevate.h b/src/setup/elevate.h new file mode 100644 index 0000000..85f72f9 --- /dev/null +++ b/src/setup/elevate.h @@ -0,0 +1,64 @@ +// Part of setup.c (elevation): included by it, in one compilation unit; never compiled on its own. +static int memcmp_(const BYTE *x, const BYTE *y, DWORD n) { while (n--) if (*x++ != *y++) return 1; return 0; } + +// A copy of src as the new file dst (CREATE_NEW: nothing that exists is used), then held: the returned handle is opened for reading +// with FILE_SHARE_READ only, so as long as it is open nobody can write to, rename or delete the copy, and the content read through it +// is compared with src. INVALID_HANDLE_VALUE when any step fails (dst is deleted again). The copy can be run while the handle is open. +static HANDLE copy_and_hold(const WCHAR *src, const WCHAR *dst) +{ + static BYTE a[65536], b[65536]; + HANDLE s, d, h = INVALID_HANDLE_VALUE; DWORD na, nb; BOOL ok = TRUE; BY_HANDLE_FILE_INFORMATION bi; + s = CreateFileW(src, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (s == INVALID_HANDLE_VALUE) return h; + d = CreateFileW(dst, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); + if (d == INVALID_HANDLE_VALUE) { CloseHandle(s); return h; } + while (ok && ReadFile(s, a, sizeof(a), &na, NULL) && na) ok = WriteFile(d, a, na, &nb, NULL) && nb == na; + CloseHandle(d); + if (ok) { + h = CreateFileW(dst, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (h != INVALID_HANDLE_VALUE) { + if (!GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) ok = FALSE; + SetFilePointer(s, 0, NULL, FILE_BEGIN); + while (ok) { // the held copy has the content of the original + BOOL r1 = ReadFile(s, a, sizeof(a), &na, NULL), r2 = ReadFile(h, b, sizeof(b), &nb, NULL); + if (!r1 || !r2 || na != nb || memcmp_(a, b, na)) { ok = FALSE; break; } + if (!na) break; + } + if (!ok) { CloseHandle(h); h = INVALID_HANDLE_VALUE; } + } + } + CloseHandle(s); + if (h == INVALID_HANDLE_VALUE) DeleteFileW(dst); + return h; +} + +// Runs " " elevated (one UAC prompt) and returns its exit code; 2 = cancelled. exe: the program to run, NULL = this one. +int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o) +{ + SHELLEXECUTEINFOW se; DWORD rc = RC_FAIL, err, tn; static WCHAR a[1024], root[4], tmp[MAX_PATH]; const WCHAR *exe = exe_path ? exe_path : g_self; HANDLE hold = INVALID_HANDLE_VALUE; LARGE_INTEGER q; + lstrcpyW(a, args); if (o->quiet) lstrcatW(a, L" /quiet"); if (o->close) lstrcatW(a, L" /close"); + if (o->pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin + if (o->keep) lstrcatW(a, L" /keep"); + // Mapped network drives (e.g. a shared folder) do not exist for the elevated process: run a copy in %TEMP% + lstrcpynW(root, g_self, 4); + if (!exe_path && root[1] == ':' && GetDriveTypeW(root) == DRIVE_REMOTE && (tn = GetTempPathW(MAX_PATH - 60, tmp)) != 0 && tn < MAX_PATH - 60) { // room for the longest name below (17 + 3 x 10 digits + 2 + 4 + NUL) + q.QuadPart = 0; QueryPerformanceCounter(&q); + wsprintfW(tmp + lstrlenW(tmp), L"DynaRunFix-Setup-%lu-%lu-%lu.exe", GetCurrentProcessId(), GetTickCount(), q.LowPart); + // the copy is held open (nobody can change it) from here until the elevated process has ended: what the UAC prompt names is what runs + if ((hold = copy_and_hold(g_self, tmp)) != INVALID_HANDLE_VALUE) exe = tmp; + } + zero(&se, sizeof(se)); + se.cbSize = sizeof(se); se.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI | SEE_MASK_NOASYNC; + se.hwnd = g_hwnd; se.lpVerb = L"runas"; se.lpFile = exe; se.lpParameters = a; se.nShow = SW_SHOWNORMAL; + if (!ShellExecuteExW(&se)) { + err = GetLastError(); + if (hold != INVALID_HANDLE_VALUE) { CloseHandle(hold); DeleteFileW(tmp); } + return err == ERROR_CANCELLED ? RC_CANCEL : RC_FAIL; + } + WaitForSingleObject(se.hProcess, INFINITE); + GetExitCodeProcess(se.hProcess, &rc); + CloseHandle(se.hProcess); + if (hold != INVALID_HANDLE_VALUE) { CloseHandle(hold); DeleteFileW(tmp); } + return (int)rc; +} + diff --git a/src/setup/exetime.h b/src/setup/exetime.h new file mode 100644 index 0000000..3f67a01 --- /dev/null +++ b/src/setup/exetime.h @@ -0,0 +1,102 @@ +// Part of setup.c (the borrowed exe time stamp): included by it, in one compilation unit; never compiled on its own. +// Observed in testing: Windows keeps using the manifest state it saw at the exe's last start until it sees a different LastWriteTime +// on a process creation. So: read the LastWriteTime, set another value, let Windows see it by creating the exe's process suspended +// and terminating it at once (none of the exe's code runs), then set the original value back. The exe's content and its +// timestamps end up exactly as they were. (Setting the value and restoring it without the process creation in between does not +// make Windows read the manifest.) +// The original time is written to HKLM\SOFTWARE\DynaRunFix (ExeOrigTimePath, ExeOrigTime) before it is changed and removed +// after it has been put back and read back; recover_exe_time puts back what a run that was killed or failed left. +enum { TOUCH_OK, TOUCH_NOTDONE, TOUCH_STUCK }; // NOTDONE: nothing was changed. STUCK: the original time could not be put back +static const WCHAR ORIGPATH[] = L"ExeOrigTimePath", ORIGTIME[] = L"ExeOrigTime"; +static HKEY g_app_root = HKEY_LOCAL_MACHINE; static const WCHAR *g_app_key = APPKEY; // where the record is kept (the tests use a key of their own) + +static BOOL same_time(const FILETIME *a, const FILETIME *b) { return a->dwLowDateTime == b->dwLowDateTime && a->dwHighDateTime == b->dwHighDateTime; } + +static BOOL time_set_verified(HANDLE h, const FILETIME *t) // set, read back, retry a few times +{ + FILETIME c; int i; + for (i = 0; i < 5; i++) { + if (SetFileTime(h, NULL, NULL, t) && GetFileTime(h, NULL, NULL, &c) && same_time(&c, t)) return TRUE; + Sleep(300); + } + return FALSE; +} + +static HANDLE open_times(const WCHAR *p) +{ return CreateFileW(p, FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); } + +static void forget_exe_time(HKEY k) { RegDeleteValueW(k, ORIGPATH); RegDeleteValueW(k, ORIGTIME); } + +static int recover_exe_time(void) +{ + HKEY k; static WCHAR path[MAX_PATH]; FILETIME saved, cur; DWORD n = sizeof(saved), type; HANDLE h; int rc = TOUCH_OK; + if (RegOpenKeyExW(g_app_root, g_app_key, 0, KEY_ALL_ACCESS, &k)) return TOUCH_OK; + if (reg_str(g_app_root, g_app_key, ORIGPATH, path, MAX_PATH, 0) && is_dynarun_name(path) && + !RegQueryValueExW(k, ORIGTIME, NULL, &type, (BYTE *)&saved, &n) && type == REG_BINARY && n == sizeof(saved)) { + if ((h = open_times(path)) == INVALID_HANDLE_VALUE) { if (!exists(path)) forget_exe_time(k); else rc = TOUCH_STUCK; } + else { + if (GetFileTime(h, NULL, NULL, &cur) && same_time(&cur, &saved)) forget_exe_time(k); + else if (time_set_verified(h, &saved)) { forget_exe_time(k); setup_log(L"restored the time stamp of DynaRun V3.exe that an earlier run had left changed"); } + else rc = TOUCH_STUCK; + CloseHandle(h); + } + } + RegCloseKey(k); + return rc; +} + +// The temporary time: now, but at least 4 s from the original (the file systems keep 2 s at the coarsest, FAT), on an even second. +// (After rounding down to an even second it is still more than 2 s away.) +static void pick_temp_time(const FILETIME *orig, FILETIME *out) +{ + ULARGE_INTEGER u, o; SYSTEMTIME st; + GetSystemTimeAsFileTime(out); + u.LowPart = out->dwLowDateTime; u.HighPart = out->dwHighDateTime; o.LowPart = orig->dwLowDateTime; o.HighPart = orig->dwHighDateTime; + if ((u.QuadPart > o.QuadPart ? u.QuadPart - o.QuadPart : o.QuadPart - u.QuadPart) < 40000000ULL) u.QuadPart = o.QuadPart + 100000000ULL; + out->dwLowDateTime = u.LowPart; out->dwHighDateTime = u.HighPart; + if (FileTimeToSystemTime(out, &st)) { st.wSecond &= ~1; st.wMilliseconds = 0; SystemTimeToFileTime(&st, out); } +} + +static int touch(const WCHAR *p) +{ + FILETIME orig, ft; HANDLE h; HKEY k; STARTUPINFOW si; PROCESS_INFORMATION pi; WCHAR *cmd; int rc = TOUCH_NOTDONE; + if (!p[0] || !is_dynarun_name(p) || !stage_lock()) return TOUCH_NOTDONE; + h = open_times(p); + if (h != INVALID_HANDLE_VALUE) { + if (GetFileTime(h, NULL, NULL, &orig) && !RegCreateKeyExW(g_app_root, g_app_key, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { + if (!RegSetValueExW(k, ORIGPATH, 0, REG_SZ, (const BYTE *)p, (lstrlenW(p) + 1) * sizeof(WCHAR)) && + !RegSetValueExW(k, ORIGTIME, 0, REG_BINARY, (const BYTE *)&orig, sizeof(orig))) { + pick_temp_time(&orig, &ft); + if (SetFileTime(h, NULL, NULL, &ft)) { + zero(&si, sizeof si); si.cb = sizeof si; + if ((cmd = alloc((MAX_PATH + 16) * sizeof(WCHAR)))) { + cat3(cmd, L"\"", p, L"\""); + if (CreateProcessW(p, cmd, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) { + TerminateProcess(pi.hProcess, 0); WaitForSingleObject(pi.hProcess, 5000); + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + } + release(cmd); + } + if (time_set_verified(h, &orig)) { forget_exe_time(k); rc = TOUCH_OK; } + else rc = TOUCH_STUCK; // the record stays: the next install or uninstall puts it back + } else forget_exe_time(k); // the time was not changed + } else forget_exe_time(k); + RegCloseKey(k); + } + CloseHandle(h); + } + stage_unlock(); + return rc; +} + +// what the caller does with touch's result +static int touch_report(const install_opts *o, int t) +{ + if (t == TOUCH_STUCK) + warn(o, T(L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", + L"DynaRun V3.exe 的修改時間無法還原。已記錄在登錄檔,下次安裝或解除安裝 DynaRunFix 時會再還原。")); + else if (t == TOUCH_NOTDONE) setup_log(L"the manifest was not refreshed: DynaRun V3.exe could not be touched"); + return t; +} + + diff --git a/src/setup/machine.h b/src/setup/machine.h new file mode 100644 index 0000000..b843b53 --- /dev/null +++ b/src/setup/machine.h @@ -0,0 +1,206 @@ +// Part of setup.c (machine stage): included by it, in one compilation unit; never compiled on its own. +/* ---------- stages ---------- */ + +// the command line of the elevated stage is not trusted: a user SID is a SID of a real account with a profile on this machine +BOOL valid_user_sid(const WCHAR *sid) +{ + static WCHAR key[300]; PSID s; BOOL ok = FALSE; + if (lstrlenW(sid) < 8 || lstrlenW(sid) > 120 || (lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-5-21") && lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-12-1"))) return FALSE; + if (!ConvertStringSidToSidW(sid, &s)) return FALSE; + LocalFree(s); + cat3(key, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\", sid, NULL); + { HKEY k; if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, key, 0, KEY_QUERY_VALUE | WOW, &k)) { RegCloseKey(k); ok = TRUE; } } + return ok; +} + +static int machine_install_locked(const install_opts *o, const WCHAR *sid) +{ + static WCHAR p[MAX_PATH + 16]; + HKEY k; trust_ctx t; + if (!valid_user_sid(sid)) { error2(o, T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } + zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); + if (still_running(o, L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since + if (!check_exe(&t)) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } + CreateDirectoryW(g_dir, NULL); + if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || + !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { + error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); + return RC_FAIL; + } + { // Locale Emulator, resources 10.. (see setup.rc) + static const WCHAR *le[] = { L"LEProc.exe", L"LoaderDll.dll", L"LocaleEmulator.dll", L"LECommonLibrary.dll", + L"LEConfig.xml", L"COPYING", L"COPYING.LESSER", L"THIRD-PARTY.txt" }; + int i; + CreateDirectoryW(cat3(p, g_dir, L"\\le", NULL), NULL); + for (i = 0; i < (int)(sizeof(le) / sizeof(le[0])); i++) + if (!extract(10 + i, cat3(p, g_dir, L"\\le\\", le[i]))) { + error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); + return RC_FAIL; + } + } + cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); + if (lstrcmpiW(p, g_self) && ((!CopyFileW(g_self, p, FALSE) && !(move_aside(p) && CopyFileW(g_self, p, FALSE))) || !same_file(g_self, p))) { + error2(o, T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); + return RC_FAIL; + } + driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) + if (!app_key_create(APP_MACHINE, &k)) return RC_FAIL; + set_str(k, L"InstallDir", g_dir); + if (t.exe_trusted) set_str(k, L"DynaRunExe", g_exe); // a path that ordinary users can change is not recorded for the elevated stages (uninstall) to use + else RegDeleteValueW(k, L"DynaRunExe"); + + touch_report(o, recover_exe_time()); // a time stamp an earlier run could not put back + // the per-user COM registrations are not copied to HKLM any more (records of older versions are left alone) + if (!t.exe_trusted) setup_log(L"DynaRun's folder or exe can be changed by ordinary users (or its path contains a link): no manifest was written"); + + // garbled text under "Beta: Use Unicode UTF-8": external manifest with activeCodePage=Legacy + if (GetACP() == CP_UTF8 && t.exe_trusted && !exists(cat3(p, g_exe, L".manifest", NULL)) && extract(3, p)) { + set_dword(k, L"ManifestInstalled", 1); + touch_report(o, touch(g_exe)); + } + + { HKEY b; if (!RegCreateKeyExW(k, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &b, NULL)) { fix_shortcuts(TRUE, b); RegCloseKey(b); } } + taskbar_policy(o, sid, k); + RegCloseKey(k); + + if (!RegCreateKeyExW(HKEY_LOCAL_MACHINE, UNINSTKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { + set_str(k, L"DisplayName", L"DynaRunFix"); + set_str(k, L"DisplayVersion", WIDEN(DRF_DISPLAY)); + set_str(k, L"Publisher", L"DynaRunFix (github.com/timliudev/DynaRunFix)"); + set_str(k, L"URLInfoAbout", L"https://github.com/timliudev/DynaRunFix"); + set_str(k, L"InstallLocation", g_dir); + cat3(p, g_dir, L"\\DynaRunFix-Setup.exe,0", NULL); set_str(k, L"DisplayIcon", p); // our icon, not DynaRun's + cat3(p, L"\"", g_dir, L"\\DynaRunFix-Setup.exe\" /uninstall"); set_str(k, L"UninstallString", p); + set_dword(k, L"NoModify", 1); set_dword(k, L"NoRepair", 1); set_dword(k, L"EstimatedSize", 100); + RegCloseKey(k); + } + return RC_OK; +} + +static int machine_install(const install_opts *o, const WCHAR *sid) +{ + int rc; + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } + rc = machine_install_locked(o, sid); + stage_unlock(); + return rc; +} + +// Text of the manifest that DynaRunFix 1.3.6 wrote (its embedded file had a different comment and the assembly version 3.26.0.0). +static const char MANIFEST_V136[] = + "\n" + "\n" + "\n" + " \n" + " \n" + " \n" + " Legacy\n" + " \n" + " \n" + "\n"; + +// The file on disk is d, line endings (CR) not counted: a checkout with CRLF and one with LF build the same manifest. +static BOOL same_text(const WCHAR *p, const void *d, DWORD n) +{ + DWORD got, i = 0, j = 0; BYTE *f = read_file(p, &got); const BYTE *x = d; BOOL r = f != NULL; + while (r) { + while (i < got && f[i] == '\r') i++; + while (j < n && x[j] == '\r') j++; + if (i >= got || j >= n) { r = i >= got && j >= n; break; } + r = f[i++] == x[j++]; + } + release(f); + return r; +} + +// skip_sid: the user who started the uninstall (their own part is done by the user-side stage afterwards) +// The manifest next to DynaRun V3.exe is deleted only when it is a file DynaRunFix wrote: the same text as the embedded one or, for +// files of earlier versions, the same with the older assembly identity version (3.26.0.0) or the text of release 1.3.6. +static BOOL manifest_is_ours(const WCHAR *p) +{ + static const char NEWV[] = "version=\"1.0.0.0\"", OLDV[] = "version=\"3.26.0.0\""; + HRSRC r = FindResourceW(NULL, MAKEINTRESOURCEW(3), (LPCWSTR)RT_RCDATA); const char *d; char *c; DWORD n, i, nl = sizeof(NEWV) - 1, ol = sizeof(OLDV) - 1, j; BOOL ok = FALSE; + if (!r) return FALSE; + d = LockResource(LoadResource(NULL, r)); n = SizeofResource(NULL, r); + if (!d || !n) return FALSE; + if (same_text(p, d, n)) return TRUE; + if (same_text(p, MANIFEST_V136, sizeof(MANIFEST_V136) - 1)) return TRUE; + for (i = 0; i + nl <= n; i++) { + for (j = 0; j < nl && d[i + j] == NEWV[j]; j++); + if (j < nl) continue; + if ((c = alloc(n + ol))) { + for (j = 0; j < i; j++) c[j] = d[j]; + for (j = 0; j < ol; j++) c[i + j] = OLDV[j]; + for (j = i + nl; j < n; j++) c[j - nl + ol] = d[j]; + ok = same_text(p, c, n - nl + ol); + release(c); + } + break; + } + return ok; +} + +// The user's Windows 11 taskbar policy (sid: the user's SID; their hive is loaded when they are signed in): removed when it is ours +static void drop_policy_of(const WCHAR *sid) +{ + HKEY k; + if (!sid || !sid[0] || !valid_user_sid(sid)) return; + if (!RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } +} + +static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) +{ + static WCHAR p[MAX_PATH + 16], polsid[200]; static const WCHAR KEEP[] = L"com-copied.txt\0com-backup\0com-skipped.txt\0setup.log\0"; // the record of the COM copies stays + HKEY k; DWORD man = 0, n = 4; BOOL keep = FALSE, deleted = FALSE, stuck = FALSE; trust_ctx t; + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } + zero(&t, sizeof(t)); + if (skip_sid && skip_sid[0] && valid_user_sid(skip_sid)) lstrcpynW(t.user_sid, skip_sid, 200); else skip_sid = NULL; + if (o->close) close_programs(); // the user agreed (or /close): DynaRun keeps files of the folder open + if (!g_exe[0]) locate_dynarun(); // not recorded (an untrusted path is not): the default folders + check_exe(&t); // g_exe is from HKLM; t.exe_trusted says whether to run it + if (touch_report(o, recover_exe_time()) == TOUCH_STUCK) stuck = TRUE; + restore_shortcuts(HKEY_LOCAL_MACHINE, APPKEY); + unretarget_shell(TRUE); + // the taskbar policy (Windows 11) of the user who runs the uninstall and of the user the install recorded: read before the key goes + polsid[0] = 0; reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"TaskbarPolicySid", polsid, 200, 0); + drop_policy_of(skip_sid); drop_policy_of(polsid); + unretarget_all_users(skip_sid); + if (app_key_open(APP_MACHINE, KEY_READ, &k)) { RegQueryValueExW(k, L"ManifestInstalled", NULL, NULL, (BYTE *)&man, &n); RegCloseKey(k); } + if (man == 1 && g_exe[0] && !t.exe_trusted && exists(cat3(p, g_exe, L".manifest", NULL))) + setup_log(L"the manifest next to DynaRun V3.exe was left in place: DynaRun's folder or exe can be changed by ordinary users, so nothing is deleted or scheduled for deletion there"); + else if (man == 1 && g_exe[0] && exists(cat3(p, g_exe, L".manifest", NULL))) { + if (!manifest_is_ours(p)) setup_log(L"the manifest next to DynaRun V3.exe is not the file DynaRunFix wrote (it was changed): left in place"); + else if (DeleteFileW(p)) deleted = TRUE; + else if (move_aside(p)) { // in use: renamed at once (a reinstall before the restart writes its own file), the renamed one goes at the next restart + deleted = TRUE; + warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + } + else { // cannot even be renamed: it goes at the next restart, and the record that it is ours stays + MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); keep = TRUE; + warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + } + } + if (deleted) { if (t.exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } + // Before the key goes: the driver part ends a waiting task and resumes BitLocker if DynaRunFix suspended it (the flag is in the key). + // An installed driver stays. + driver_machine_uninstall(); + // stuck: the original time stamp of DynaRun V3.exe could not be put back; ExeOrigTimePath / ExeOrigTime stay for the next install or + // uninstall (touch_report says so) - the key is not deleted then + if (!keep && !stuck) RegDeleteKeyW(HKEY_LOCAL_MACHINE, APPKEY); + else if (app_key_open(APP_MACHINE, KEY_SET_VALUE, &k)) { // what is no longer needed; ManifestInstalled, DynaRunExe (and the time stamp record) stay + RegDeleteValueW(k, L"InstallDir"); RegDeleteValueW(k, L"TaskbarPolicy"); RegDeleteValueW(k, L"TaskbarPolicySid"); RegCloseKey(k); + } + RegDeleteKeyW(HKEY_LOCAL_MACHINE, UNINSTKEY); + delete_tree(g_dir, KEEP, 0); + stage_unlock(); + return RC_OK; +} + diff --git a/src/setup/msi_stage.h b/src/setup/msi_stage.h new file mode 100644 index 0000000..e38fe19 --- /dev/null +++ b/src/setup/msi_stage.h @@ -0,0 +1,95 @@ +// Part of setup.c (vendor MSI staging and full stage): included by it, in one compilation unit; never compiled on its own. +static const WCHAR *no_admin(void) +{ return T(L"Administrator permission was not granted. Nothing was changed.", L"未取得系統管理員權限,沒有做任何變更。"); } + +// Elevated: install DynaRun from its MSI, then the machine stage. The MSI installs per user (as the +// original setup does), so it must run as the user who asked for it: with over-the-shoulder +// elevation (another account typed into the UAC prompt) return 3 and let the caller run it. +// +// The MSI comes from a folder the user can write (Downloads, %TEMP%), and msiexec runs it with the rights of this stage: +// the file is first copied into a new folder only administrators and SYSTEM can use (%WINDIR%\Temp\DynaRunFix-...; created +// here with that ACL, so nobody else could have prepared it), checked there again (it must be Dyna Pro's DynaRun setup) and +// run from there; the copy is deleted afterwards. Cabinet files the MSI names outside itself are copied along. +static BOOL make_admin_dir(const WCHAR *base, WCHAR *out) // base: the folder in which it is created (%WINDIR%\Temp) +{ + static const WCHAR sddl[] = L"D:P(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)"; // protected: Administrators and SYSTEM, full control + PSECURITY_DESCRIPTOR sd; SECURITY_ATTRIBUTES sa; int i; BOOL ok = FALSE; LARGE_INTEGER q; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl, SDDL_REVISION_1, &sd, NULL)) return FALSE; + sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = sd; sa.bInheritHandle = FALSE; + for (i = 0; i < 20 && !ok; i++) { // a name that exists already is refused by CreateDirectory: try another + q.QuadPart = 0; QueryPerformanceCounter(&q); + lstrcpynW(out, base, MAX_PATH - 60); + wsprintfW(out + lstrlenW(out), L"\\DynaRunFix-%lu-%lu-%lu", GetCurrentProcessId(), GetTickCount(), q.LowPart + i); + ok = CreateDirectoryW(out, &sa); + } + LocalFree(sd); + return ok; +} + +static BOOL copy_new(const WCHAR *from, const WCHAR *to) // to must not exist +{ + static BYTE buf[65536]; HANDLE in, out; DWORD n, w; BOOL ok = TRUE; + in = CreateFileW(from, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); + if (in == INVALID_HANDLE_VALUE) return FALSE; + out = CreateFileW(to, GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); + if (out == INVALID_HANDLE_VALUE) { CloseHandle(in); return FALSE; } + while (ok && ReadFile(in, buf, sizeof(buf), &n, NULL) && n) ok = WriteFile(out, buf, n, &w, NULL) && w == n; + CloseHandle(in); + ok = ok && FlushFileBuffers(out); + CloseHandle(out); + if (!ok) DeleteFileW(to); + return ok; +} + +static void drop_admin_dir(const WCHAR *dir) { delete_tree(dir, NULL, 0); } + +static BOOL stage_msi(const WCHAR *msi, WCHAR *dir, WCHAR *staged) +{ + WCHAR *cabs, *c, src[MAX_PATH], dst[MAX_PATH], base[MAX_PATH], *e; int n; + GetWindowsDirectoryW(base, MAX_PATH - 20); lstrcatW(base, L"\\Temp"); + if (!make_admin_dir(base, dir)) return FALSE; + cat3(staged, dir, L"\\Setup.msi", NULL); + if (!copy_new(msi, staged)) { drop_admin_dir(dir); return FALSE; } + if ((cabs = msi_external_files(staged))) { // the names are read from the copy that is going to be run (and checked), not from the user's file + lstrcpynW(src, msi, MAX_PATH); for (e = src + lstrlenW(src); e > src && *e != '\\'; e--); n = (int)(e - src); + for (c = cabs; *c; c += lstrlenW(c) + 1) + if (n + 1 + lstrlenW(c) < MAX_PATH && dir[0] && lstrlenW(dir) + 1 + lstrlenW(c) < MAX_PATH) { + lstrcpynW(src + n + 1, c, MAX_PATH - n - 1); src[n] = '\\'; + copy_new(src, cat3(dst, dir, L"\\", c)); // a missing one is msiexec's error to report + } + release(cabs); + } + return TRUE; +} + +static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *msi) +{ + static WCHAR me[200], txt[300], dir[MAX_PATH], staged[MAX_PATH]; int rc; + if (!user_sid(me, 200) || lstrcmpiW(me, sid)) return RC_OVER_SHOULDER; + if (!stage_lock()) { error2(o, busy_text(), NULL); return RC_FAIL; } + if (still_running(o, L"full install")) { stage_unlock(); return RC_RUNNING; } + if (!stage_msi(msi, dir, staged)) { + stage_unlock(); + msg(T(L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。"), MB_ICONERROR); + return RC_MSI_FAILED; + } + if (msi_check(staged, NULL, 0) != MSI_DYNARUN) { // the check of the copy that is going to be run + drop_admin_dir(dir); stage_unlock(); + msg(T(L"This is not the DynaRun V3 setup, so it will not be installed.", L"這不是 DynaRun V3 的安裝檔,所以不會安裝它。"), MB_ICONERROR); + return RC_MSI_FAILED; + } + rc = run_msiexec(staged); + drop_admin_dir(dir); + stage_unlock(); + if (rc != 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { + if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; + wsprintfW(txt, T(L"The DynaRun V3 setup failed (Windows Installer error %d).", L"DynaRun V3 安裝失敗(Windows Installer 錯誤 %d)。"), rc); + if (rc == ERROR_INSTALL_ALREADY_RUNNING) + lstrcatW(txt, T(L"\n\nAnother installation is running. Wait until it finishes and try again.", L"\n\n有其他程式正在安裝,請等它完成後再試一次。")); + msg(txt, MB_ICONERROR); + return RC_MSI_FAILED; + } + if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return RC_NO_DYNARUN; } + return machine_install(o, sid); +} + diff --git a/src/setup/procs.h b/src/setup/procs.h new file mode 100644 index 0000000..ecc6189 --- /dev/null +++ b/src/setup/procs.h @@ -0,0 +1,224 @@ +// Part of setup.c (paths, running programs, user log): included by it, in one compilation unit; never compiled on its own. +/* ---------- paths ---------- */ + +static void init_paths(void) +{ + GetModuleFileNameW(NULL, g_self, MAX_PATH); + if (!SHGetSpecialFolderPathW(NULL, g_dir, CSIDL_PROGRAM_FILES, FALSE)) lstrcpyW(g_dir, L"C:\\Program Files"); + lstrcatW(g_dir, L"\\DynaRunFix"); + cat3(g_launcher, g_dir, L"\\DynaRunFix.exe", NULL); +} + +BOOL locate_dynarun(void) +{ + static const int csidl[] = { CSIDL_PROGRAM_FILESX86, CSIDL_PROGRAM_FILES }; + WCHAR pf[MAX_PATH]; int i; + if (reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0) && exists(g_exe)) return TRUE; + // the program folders from the system (not from environment variables, which a process can set for the elevated stage) + for (i = 0; i < 2; i++) + if (SHGetSpecialFolderPathW(NULL, pf, csidl[i], FALSE) && lstrlenW(pf) + lstrlenW(DYNARUN_REL) < MAX_PATH && exists(cat3(g_exe, pf, DYNARUN_REL, NULL))) return TRUE; + g_exe[0] = 0; + return FALSE; +} + +/* ---------- programs that hold the files the setup replaces ---------- */ + +typedef BOOL (WINAPI *QueryFullProcessImageNameW_t)(HANDLE, DWORD, LPWSTR, PDWORD); + +static void process_path(DWORD pid, WCHAR *out, DWORD cch) // the exe of a process, or "" when it cannot be read +{ + static QueryFullProcessImageNameW_t q; static BOOL tried; HANDLE h; MODULEENTRY32W me; + out[0] = 0; + if (!tried) { tried = TRUE; q = (QueryFullProcessImageNameW_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "QueryFullProcessImageNameW"); } + // Vista and later: also for an elevated process when this one is not (PROCESS_QUERY_LIMITED_INFORMATION) + if (q && (h = OpenProcess(0x1000, FALSE, pid))) { + if (!q(h, 0, out, &cch)) out[0] = 0; + CloseHandle(h); + if (out[0]) return; + } + h = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, pid); // XP: the first module is the exe + if (h == INVALID_HANDLE_VALUE) return; + me.dwSize = sizeof(me); + if (Module32FirstW(h, &me)) lstrcpynW(out, me.szExePath, cch); + CloseHandle(h); +} + +// A running DynaRun V3 has dynafix.dll (and under Locale Emulator LoaderDll.dll / LocaleEmulator.dll) +// of the install folder loaded. Windows lets the setup rename such files and put new ones in place, +// but that DynaRun keeps the old code, and "Start DynaRun" would only find it already running, so +// DynaRun is closed first (the user agrees in the wizard, or passes /close). +// What holds up the install (judge_proc): the DynaRun V3.exe that is the known one (g_exe; a DynaRun of another folder does not use +// our files), and the launcher and LEProc when they run from the install folder. A program whose exe path cannot be read counts as +// ours (better than a half-updated install). The setup closes only what it can tell is ours and that runs in its own session; the +// rest still holds up the install, but "close" would not help, and the wizard and the log say so. +typedef struct { DWORD pid; int bit; DWORD sess; int why; WCHAR path[MAX_PATH]; } runproc; +enum { JUDGE_SKIP, JUDGE_CLOSE, JUDGE_OTHERSESSION, JUDGE_NOPATH, JUDGE_NOEXE }; // not in the way / can be closed / cannot: why + +static BOOL same_path(const WCHAR *a, const WCHAR *b) // case-insensitive; 8.3 and long names of one file match +{ + WCHAR x[MAX_PATH], y[MAX_PATH]; DWORD m, n; + if (!a[0] || !b[0]) return FALSE; + if (!lstrcmpiW(a, b)) return TRUE; + m = GetLongPathNameW(a, x, MAX_PATH); n = GetLongPathNameW(b, y, MAX_PATH); + return m && m < MAX_PATH && n && n < MAX_PATH && !lstrcmpiW(x, y); +} + +// bit: RUN_* of the program; path: its exe ("" when unreadable); exe: the known DynaRun V3.exe ("" when not known yet); +// dir: the install folder with a trailing backslash +static int judge_proc(int bit, BOOL same_session, const WCHAR *path, const WCHAR *exe, const WCHAR *dir) +{ + int len = lstrlenW(dir); + if (bit == RUN_DYNARUN) { + if (!path[0]) return JUDGE_NOPATH; + if (!exe[0]) return JUDGE_NOEXE; + if (!same_path(path, exe)) return JUDGE_SKIP; + } else { + if (!path[0]) return JUDGE_NOPATH; + if (!(lstrlenW(path) > len && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, len, dir, len) == CSTR_EQUAL)) return JUDGE_SKIP; + } + return same_session ? JUDGE_CLOSE : JUDGE_OTHERSESSION; +} + +// The programs in the way (up to max in list); the result has their RUN_* bits, with RUN_CANCLOSE / RUN_NOCLOSE for the two kinds. +static int scan_programs(runproc *list, int max, int *count) +{ + PROCESSENTRY32W pe; HANDLE s; WCHAR path[MAX_PATH], dir[MAX_PATH + 2]; int r = 0, bit, why; DWORD mysess = 0, sess; + *count = 0; + ProcessIdToSessionId(GetCurrentProcessId(), &mysess); + s = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (s == INVALID_HANDLE_VALUE) return 0; + cat3(dir, g_dir, L"\\", NULL); + pe.dwSize = sizeof(pe); + if (Process32FirstW(s, &pe)) do { + bit = !lstrcmpiW(pe.szExeFile, L"DynaRun V3.exe") ? RUN_DYNARUN : !lstrcmpiW(pe.szExeFile, L"DynaRunFix.exe") ? RUN_LAUNCHER : + !lstrcmpiW(pe.szExeFile, L"LEProc.exe") ? RUN_LEPROC : 0; + if (!bit) continue; + if (!ProcessIdToSessionId(pe.th32ProcessID, &sess)) sess = (DWORD)-1; + process_path(pe.th32ProcessID, path, MAX_PATH); + why = judge_proc(bit, sess == mysess, path, g_exe, dir); + if (why == JUDGE_SKIP) continue; + r |= bit | (why == JUDGE_CLOSE ? RUN_CANCLOSE : RUN_NOCLOSE); + if (list && *count < max) { + list[*count].pid = pe.th32ProcessID; list[*count].bit = bit; list[*count].sess = sess; list[*count].why = why; + lstrcpyW(list[*count].path, path); (*count)++; + } + } while (Process32NextW(s, &pe)); + CloseHandle(s); + return r; +} + +int running_programs(void) +{ int n; return scan_programs(NULL, 0, &n); } + +// Why the setup stops with RC_RUNNING (nothing is shown with /quiet): one line per program in a log. The elevated stage writes +// setup.log in the install folder; before that folder exists (or without administrator rights) %TEMP%\DynaRunFix-setup.log. +static void run_log(const WCHAR *line) +{ + static WCHAR p[MAX_PATH]; + if (is_admin() && g_dir[0] && exists(g_dir)) { setup_log(line); return; } + if (GetTempPathW(MAX_PATH - 24, p)) append_line(lstrcatW(p, L"DynaRunFix-setup.log"), line); +} + +// For the other files (the wizard side of the driver step): the line with a time stamp, so that durations can be read from the log. +// The wizard thread, its worker and the update-hint thread all log: one lock covers the formatting (static buffers) and the write. +static CRITICAL_SECTION g_ulog_cs; +static volatile LONG g_ulog_state; // 0 not set up, 1 being set up, 2 ready + +void user_log(const WCHAR *line) +{ + static WCHAR l[2 * MAX_PATH + 400]; SYSTEMTIME t; const WCHAR *p, *q; + if (g_ulog_state != 2) { + if (InterlockedCompareExchange(&g_ulog_state, 1, 0) == 0) { InitializeCriticalSection(&g_ulog_cs); g_ulog_state = 2; } + else while (g_ulog_state != 2) Sleep(1); + } + EnterCriticalSection(&g_ulog_cs); + GetLocalTime(&t); + for (p = line; *p; p = *q ? q + 1 : q) { // a text of several lines is several lines, each with its time; the log is UTF-8 (append_line) + int n, pre, cnt; + for (q = p; *q && *q != '\n'; q++); + n = (int)(q - p); + while (n > 0 && p[n - 1] == '\r') n--; + if (n <= 0) continue; + wsprintfW(l, L"[%02u:%02u:%02u.%03u] ", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); + pre = lstrlenW(l); cnt = n + 1; + if (cnt > (int)(sizeof(l) / sizeof(l[0])) - pre) cnt = (int)(sizeof(l) / sizeof(l[0])) - pre; + lstrcpynW(l + pre, p, cnt); + run_log(l); + } + LeaveCriticalSection(&g_ulog_cs); +} + +static void log_running(const WCHAR *stage, const runproc *l, int n, int r, BOOL close) +{ + static const WCHAR *const names[] = { L"DynaRun V3.exe", L"DynaRunFix.exe", L"LEProc.exe" }; + static const WCHAR *const reasons[] = { L"can be closed", L"cannot be closed: runs in another session", + L"cannot be closed: its exe path cannot be read", L"cannot be closed: the DynaRun folder is not known" }; + WCHAR t[900]; SYSTEMTIME st; int i, k; DWORD me = 0; + GetLocalTime(&st); ProcessIdToSessionId(GetCurrentProcessId(), &me); + wsprintfW(t, L"%04u-%02u-%02u %02u:%02u:%02u %s: stopped, exit code 6 (DynaRun is running), nothing was changed. %s", st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond, stage, + (r & RUN_NOCLOSE) ? (close ? L"/close was not used: a program that cannot be closed is running." : L"A program that cannot be closed is running.") + : (close ? L"It was still running after /close." : L"Run with /close to close it.")); + run_log(t); + for (i = 0; i < n; i++) { + k = l[i].bit == RUN_DYNARUN ? 0 : l[i].bit == RUN_LAUNCHER ? 1 : 2; + wsprintfW(t, L" %s pid %lu, session %lu (this setup: %lu), %s%s%s", names[k], l[i].pid, l[i].sess, me, reasons[l[i].why - JUDGE_CLOSE], + l[i].path[0] ? L", " : L"", l[i].path); + run_log(t); + } +} + +static BOOL CALLBACK close_window(HWND h, LPARAM pid) +{ + DWORD p; + GetWindowThreadProcessId(h, &p); + if (p == (DWORD)pid && IsWindowVisible(h)) PostMessageW(h, WM_CLOSE, 0, 0); + return TRUE; +} + +static void wait_gone(runproc *l, int n, int bits, DWORD ms) +{ + HANDLE h[16]; int i, k = 0; + for (i = 0; i < n; i++) if ((l[i].bit & bits) && l[i].why == JUDGE_CLOSE && (h[k] = OpenProcess(SYNCHRONIZE, FALSE, l[i].pid))) k++; + if (k) WaitForMultipleObjects(k, h, TRUE, ms); + while (k) CloseHandle(h[--k]); +} + +// Closes DynaRun as if its window were closed (WM_CLOSE); what is still running 10 s later (e.g. a +// question DynaRun asks on exit) is ended. Runs in the elevated stage: an elevated DynaRun accepts +// neither messages nor TerminateProcess from a process that is not elevated. +static void close_programs(void) +{ + runproc *l = alloc(16 * sizeof(runproc)); int n, i; HANDLE h; // on the heap: a stack of that size would need the C runtime's probe + if (!l || !(scan_programs(l, 16, &n) & RUN_CANCLOSE)) { release(l); return; } + for (i = 0; i < n; i++) if (l[i].bit == RUN_DYNARUN && l[i].why == JUDGE_CLOSE) EnumWindows(close_window, l[i].pid); + wait_gone(l, n, RUN_DYNARUN, 10000); + wait_gone(l, n, RUN_LAUNCHER | RUN_LEPROC, 2000); // LEProc waits for DynaRun, the launcher ends by itself + scan_programs(l, 16, &n); + for (i = 0; i < n; i++) + if (l[i].why == JUDGE_CLOSE && (h = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, l[i].pid))) { + TerminateProcess(h, 1); WaitForSingleObject(h, 5000); CloseHandle(h); + } + release(l); +} + +// The elevated install stages: closes DynaRun first when the user agreed (or /close), then TRUE (and the reason logged) when something +// still holds up the install. Nothing is closed when part of it cannot be closed: the install stops anyway, and a test run would end for nothing. +static BOOL still_running(const install_opts *o, const WCHAR *stage) +{ + runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); + if (o->close && r && !(r & RUN_NOCLOSE)) { close_programs(); r = scan_programs(l, l ? 16 : 0, &n); } + if (r && l) log_running(stage, l, n, r, o->close); + release(l); + return r != 0; +} + +// Before the UAC prompt: TRUE (logged) when /quiet without /close ends here. With /close the elevated stage decides: it can read more. +static BOOL running_early(const install_opts *o, const WCHAR *stage) +{ + runproc *l = alloc(16 * sizeof(runproc)); int n, r = scan_programs(l, l ? 16 : 0, &n); + if (r && !o->close && l) log_running(stage, l, n, r, o->close); + release(l); + return r && !o->close; +} + diff --git a/src/setup/shortcuts.h b/src/setup/shortcuts.h new file mode 100644 index 0000000..b9388a6 --- /dev/null +++ b/src/setup/shortcuts.h @@ -0,0 +1,418 @@ +// Part of setup.c (shortcut folders and shortcut files): included by it, in one compilation unit; never compiled on its own. +// The installer copies nothing to HKLM\SOFTWARE\Classes (promote_com_in lives in tests\promote_com.h). + +/* ---------- shortcuts ---------- */ + +typedef UINT (WINAPI *MsiGetShortcutTargetW_t)(LPCWSTR, LPWSTR, LPWSTR, LPWSTR); +typedef int (WINAPI *MsiGetComponentPathW_t)(LPCWSTR, LPCWSTR, LPWSTR, DWORD *); +static MsiGetShortcutTargetW_t pMsiGetShortcutTarget; +static MsiGetComponentPathW_t pMsiGetComponentPath; + +// Looks up the two MSI functions, once (more calls do nothing). msi.dll is loaded by its full path in the system folder (load_system_dll), never +// searched for. Both pointers stay NULL when msi.dll or a function is missing: classify then skips its check of advertised shortcuts. +static void msi_api_init(void) +{ + static BOOL done; HMODULE msi; + if (done) return; + done = TRUE; + if (!(msi = load_system_dll(L"msi.dll"))) return; + pMsiGetShortcutTarget = (MsiGetShortcutTargetW_t)GetProcAddress(msi, "MsiGetShortcutTargetW"); + pMsiGetComponentPath = (MsiGetComponentPathW_t)GetProcAddress(msi, "MsiGetComponentPathW"); + if (!pMsiGetComponentPath) pMsiGetShortcutTarget = NULL; +} + +static IShellLinkW *load_link(const WCHAR *p) +{ + IShellLinkW *sl; IPersistFile *pf; HRESULT hr = E_FAIL; + if (FAILED(CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **)&sl))) return NULL; + if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IPersistFile, (void **)&pf))) { hr = IPersistFile_Load(pf, p, STGM_READ); IPersistFile_Release(pf); } + if (SUCCEEDED(hr)) return sl; + IShellLinkW_Release(sl); + return NULL; +} + +enum { LNK_OTHER, LNK_DYNARUN, LNK_OURS }; +// A shortcut target that is the launcher of some DynaRunFix install: "...\DynaRunFix\DynaRunFix.exe". The path of this install +// (g_launcher) is compared first; this also catches the launcher as an older version or Windows (32-bit program on 64-bit Windows: +// "Program Files" vs "Program Files (x86)") recorded it, so such a shortcut is still taken for ours when it is restored or pointed back. +static BOOL launcher_path_like(const WCHAR *t) +{ + static const WCHAR tail[] = L"\\DynaRunFix\\DynaRunFix.exe"; int n = lstrlenW(t), m = (int)(sizeof(tail) / sizeof(WCHAR)) - 1; + return n > m && !lstrcmpiW(t + n - m, tail); +} + +static int classify(const WCHAR *p) +{ + WCHAR t[MAX_PATH], prod[40], feat[40], comp[40]; DWORD n = MAX_PATH; IShellLinkW *sl = load_link(p); int r = LNK_OTHER; + if (!sl) return r; + t[0] = 0; + IShellLinkW_GetPath(sl, t, MAX_PATH, NULL, 0); + IShellLinkW_Release(sl); + if (!lstrcmpiW(t, g_launcher) || launcher_path_like(t)) return LNK_OURS; + if (!lstrcmpiW(t, g_exe)) return LNK_DYNARUN; + // the original setup is an MSI: its shortcuts are "advertised" and point to the product, not the exe + msi_api_init(); + if (pMsiGetShortcutTarget && !pMsiGetShortcutTarget(p, prod, feat, comp) && + pMsiGetComponentPath(prod, comp, t, &n) >= 0 && !lstrcmpiW(t, g_exe)) r = LNK_DYNARUN; + return r; +} + +// Windows 7+: an explicit AppUserModelID on the launcher shortcuts, so that a running DynaRun (same ID, set by +// dynafix.dll) groups under the pinned icon; other targets get none. +static void set_appid(IShellLinkW *sl, const WCHAR *id) +{ + IPropertyStore *ps; PROPVARIANT pv; DWORD ma, mi, b; + os_ver(&ma, &mi, &b); + if (ma < 6 || (ma == 6 && mi < 1) || FAILED(IShellLinkW_QueryInterface(sl, &IID_IPropertyStore, (void **)&ps))) return; + zero(&pv, sizeof(pv)); + if (id) { pv.vt = VT_LPWSTR; pv.pwszVal = (LPWSTR)id; } // VT_EMPTY removes it + if (SUCCEEDED(IPropertyStore_SetValue(ps, &PKEY_AUMID, &pv))) IPropertyStore_Commit(ps); + IPropertyStore_Release(ps); +} + +// DynaRun's own icon. DynaRun V3.exe holds only the chart icon of its windows; the blue Dyna Run logo is in the icon +// file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries +// its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. +static BOOL g_icontried; static WCHAR g_icon[MAX_PATH]; static int g_iconidx; +static BOOL is_lnk(const WCHAR *p) { int n = lstrlenW(p); return n >= 4 && !lstrcmpiW(p + n - 4, L".lnk"); } + +// The shell folders that can hold a DynaRun shortcut: ONE list, for every walk and check that looks at them (the icon search, the fix, the +// uninstall's point-back, the search for our own shortcut, and the check of the paths a backup is put back to). A row says which of them +// include the folder (SFU_*) and which of those also look into its subfolders (deep); nothing else lists a shortcut folder, so a folder that +// is not here is neither scanned nor accepted by restore_shortcuts. +enum { SFU_FIX = 1, SFU_UNRET = 2, SFU_FIND = 4, SFU_ICON = 8, SFU_RESTORE = 16 }; +enum { SF_DESKTOP, SF_STARTMENU, SF_PROGRAMS, SF_QUICKLAUNCH, SF_OWN, SF_COUNT }; // the index in SF[]; the first two are searched first +enum { SF_USER = 1, SF_COMMON = 2, SF_BOTH = 3 }; // which of the two folders of a row (bit 0 the user's, bit 1 all users') +typedef struct { + int csidl[2]; // [0] the user's folder, [1] the all users' folder (0: there is none) + const WCHAR *sub; // below that folder + const WCHAR *reg, *rel; // the folder of another account (unretarget_user): its name in User Shell Folders, and the default below the profile + unsigned uses, deep; +} shortcut_folder; +static const WCHAR QUICKLAUNCH[] = L"\\Microsoft\\Internet Explorer\\Quick Launch"; // incl. pinned taskbar items +static const shortcut_folder SF[SF_COUNT] = { + { { CSIDL_DESKTOPDIRECTORY, CSIDL_COMMON_DESKTOPDIRECTORY }, NULL, L"Desktop", L"Desktop", + SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, 0 }, + // the Start menu root (not just Programs): some DynaRun installs put their shortcut there. The fix and the point-back look at the root + // alone and at Programs on its own; the two searches look below the whole Start menu. + { { CSIDL_STARTMENU, CSIDL_COMMON_STARTMENU }, NULL, L"Start Menu", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu", + SFU_FIX | SFU_UNRET | SFU_FIND | SFU_ICON | SFU_RESTORE, SFU_FIND | SFU_ICON }, + { { CSIDL_PROGRAMS, CSIDL_COMMON_PROGRAMS }, NULL, L"Programs", L"AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs", + SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, + { { CSIDL_APPDATA, 0 }, QUICKLAUNCH, L"AppData", L"AppData\\Roaming", + SFU_FIX | SFU_UNRET | SFU_RESTORE, SFU_FIX | SFU_UNRET }, + { { CSIDL_APPDATA, 0 }, L"\\DynaRunFix", NULL, NULL, SFU_RESTORE, 0 }, // desktop_link(): the shortcut it creates when there is none +}; + +// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none or the path is too long for what goes below it. +static BOOL sf_path(int id, BOOL common, WCHAR *out) +{ + const shortcut_folder *f = &SF[id]; int csidl = f->csidl[common ? 1 : 0]; + if (!csidl || !SHGetSpecialFolderPathW(NULL, out, csidl, FALSE)) return FALSE; + if (f->sub) { if (lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } + return TRUE; +} + +// fn(folder, row, common, deep, ctx) is called for each folder that the use (one SFU_*) lists, in the scope (SF_USER, SF_COMMON, SF_BOTH): a row's +// user folder before its all users' one. fn returns FALSE to stop; then this returns TRUE. +typedef BOOL (*sf_fn)(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx); +static BOOL shortcut_folders(unsigned use, int scope, sf_fn fn, void *ctx) +{ + WCHAR d[MAX_PATH]; int i, id, c; + for (i = 0; i < SF_COUNT; i++) { + id = use == SFU_ICON && i < 2 ? 1 - i : i; // the icon is looked for in the Start menu before the Desktop + if (!(SF[id].uses & use)) continue; + for (c = 0; c < 2; c++) + if ((scope & (1 << c)) && sf_path(id, c, d) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; + } + return FALSE; +} + +// DynaRun's own icon. DynaRun V3.exe holds only the chart icon of its windows; the blue Dyna Run logo is in the icon +// file Dyna Pro's MSI gives its shortcuts. Windows shows a running DynaRun with the icon of the shortcut that carries +// its AppUserModelID, so every shortcut of ours gets that logo, taken from a DynaRun shortcut that does not use the exe. +// The walks below (dir_walk) skip names that start with '.' and paths longer than their buffer; all but the fix do not follow links. +typedef struct { WCHAR p[MAX_PATH], ic[MAX_PATH], x[MAX_PATH]; BOOL recurse, found; } icon_walk; +static BOOL find_icon(const WCHAR *dir, BOOL recurse); +static BOOL icon_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + icon_walk *w = vc; IShellLinkW *sl; int idx; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_icon(p, TRUE); return !w->found; } + if (!is_lnk(p) || classify(p) == LNK_OTHER || !(sl = load_link(p))) return TRUE; + w->ic[0] = 0; idx = 0; + IShellLinkW_GetIconLocation(sl, w->ic, MAX_PATH, &idx); + IShellLinkW_Release(sl); + if (w->ic[0] && lstrcmpiW(w->ic, g_exe) && ExpandEnvironmentStringsW(w->ic, w->x, MAX_PATH) && exists(w->x)) { + lstrcpyW(g_icon, w->ic); g_iconidx = idx; w->found = TRUE; + } + return !w->found; +} + +static BOOL find_icon(const WCHAR *dir, BOOL recurse) +{ + icon_walk *w = alloc(sizeof(*w)); BOOL r; + if (!w) return FALSE; + w->recurse = recurse; + dir_walk(dir, w->p, MAX_PATH, MAX_PATH + 1, icon_entry, w); // paths of at most MAX_PATH characters (dir_walk's limit is "shorter than") + r = w->found; + release(w); + return r; +} + +static BOOL icon_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { return !find_icon(dir, deep); } + +static void dynarun_icon(void) +{ + if (g_icontried) return; + g_icontried = TRUE; + shortcut_folders(SFU_ICON, SF_BOTH, icon_folder, NULL); +} + +static BOOL save_link(const WCHAR *p, const WCHAR *target, const WCHAR *icon, int idx, WORD hotkey, int show, BOOL runas) +{ + IShellLinkW *sl; IPersistFile *pf; IShellLinkDataList *dl; WCHAR wd[MAX_PATH], *e; HRESULT hr = E_FAIL; DWORD fl; + if (FAILED(CoCreateInstance(&CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, &IID_IShellLinkW, (void **)&sl))) return FALSE; + lstrcpyW(wd, g_exe); for (e = wd + lstrlenW(wd); e > wd && *e != '\\'; e--); *e = 0; + IShellLinkW_SetPath(sl, target); + IShellLinkW_SetWorkingDirectory(sl, wd); + if (target == g_launcher && (!icon || !icon[0] || !lstrcmpiW(icon, g_exe))) { // the exe's chart icon: DynaRun's logo instead + dynarun_icon(); + if (g_icon[0]) { icon = g_icon; idx = g_iconidx; } + } + IShellLinkW_SetIconLocation(sl, icon, idx); + if (target == g_launcher) IShellLinkW_SetDescription(sl, L"DynaRun V3 (DynaRunFix)"); + set_appid(sl, target == g_launcher ? APPID : NULL); + if (hotkey) IShellLinkW_SetHotkey(sl, hotkey); + if (show) IShellLinkW_SetShowCmd(sl, show); + if (runas && SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IShellLinkDataList, (void **)&dl))) { + if (SUCCEEDED(IShellLinkDataList_GetFlags(dl, &fl))) IShellLinkDataList_SetFlags(dl, fl | SLDF_RUNAS_USER); + IShellLinkDataList_Release(dl); + } + if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IPersistFile, (void **)&pf))) { hr = IPersistFile_Save(pf, p, TRUE); IPersistFile_Release(pf); } + IShellLinkW_Release(sl); + return SUCCEEDED(hr); +} + +// Points shortcut p at target; keeps its name, icon and hotkey. Not the "Run as administrator" flag (the launcher never elevates DynaRun). +static BOOL relink(const WCHAR *p, const WCHAR *target) +{ + IShellLinkW *sl = load_link(p); IShellLinkDataList *dl; WCHAR icon[MAX_PATH]; int idx = 0, show = 0; + WORD hotkey = 0; DWORD fl = 0; + if (!sl) return FALSE; + icon[0] = 0; + IShellLinkW_GetIconLocation(sl, icon, MAX_PATH, &idx); + IShellLinkW_GetHotkey(sl, &hotkey); + IShellLinkW_GetShowCmd(sl, &show); + if (SUCCEEDED(IShellLinkW_QueryInterface(sl, &IID_IShellLinkDataList, (void **)&dl))) { IShellLinkDataList_GetFlags(dl, &fl); IShellLinkDataList_Release(dl); } + IShellLinkW_Release(sl); + if (!icon[0]) { lstrcpyW(icon, g_exe); idx = 0; } + (void)fl; // SLDF_RUNAS_USER is not carried over to the retargeted shortcut + return save_link(p, target, icon, idx, hotkey, show, FALSE); +} + +// Points an existing DynaRun shortcut at the launcher. The original file is saved in \Shortcuts. +static void retarget(const WCHAR *p, HKEY backup) +{ + DWORD n; void *orig = read_file(p, &n); + if (!orig) return; + if (!RegSetValueExW(backup, p, 0, REG_BINARY, orig, n)) relink(p, g_launcher); + release(orig); +} + +typedef struct { WCHAR p[2 * MAX_PATH]; HKEY backup; BOOL recurse; int found; } scan_walk; +static int scan(const WCHAR *dir, BOOL recurse, HKEY backup); +static BOOL scan_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) // (this walk follows links; the others do not) +{ + scan_walk *w = vc; int c; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found += scan(p, TRUE, w->backup); return TRUE; } + if (!is_lnk(p)) return TRUE; + if ((c = classify(p)) == LNK_DYNARUN) retarget(p, w->backup); + // ours from an older version (before v1.3.0 they had no AppUserModelID): written again with the ID, else the + // running DynaRun (ID set by dynafix.dll) matches no shortcut and Windows shows its window icon, not DynaRun's + else if (c == LNK_OURS) relink(p, g_launcher); + if (c != LNK_OTHER) w->found++; + return TRUE; +} + +static int scan(const WCHAR *dir, BOOL recurse, HKEY backup) // returns number of DynaRun shortcuts +{ + scan_walk *w = alloc(sizeof(*w)); int found; + if (!w) return 0; + w->backup = backup; w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, scan_lnk, w); + found = w->found; + release(w); + return found; +} + +typedef struct { WCHAR p[2 * MAX_PATH]; WCHAR *out; BOOL recurse, found; } ours_walk; +static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out); +static BOOL ours_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + ours_walk *w = vc; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) w->found = find_ours(p, TRUE, w->out); return !w->found; } + if (is_lnk(p) && classify(p) == LNK_OURS) { lstrcpyW(w->out, p); w->found = TRUE; } + return !w->found; +} + +static BOOL find_ours(const WCHAR *dir, BOOL recurse, WCHAR *out) // the first shortcut of ours below dir +{ + ours_walk *w = alloc(sizeof(*w)); BOOL r; + if (!w) return FALSE; + w->out = out; w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, ours_lnk, w); + r = w->found; + release(w); + return r; +} + +// The first shortcut of ours where a user sees it: the Desktop, then the Start menu (the user's folder before the all users' one). +static BOOL ours_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *out) { return !find_ours(dir, deep, out); } +static BOOL find_our_shortcut(WCHAR *out) { return shortcut_folders(SFU_FIND, SF_BOTH, ours_folder, out); } + +typedef struct { HKEY backup; int desk; } fix_ctx; +static BOOL fix_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) +{ + fix_ctx *c = vc; int n = scan(dir, deep, c->backup); + if (id == SF_DESKTOP) c->desk = n; + return TRUE; +} + +static int fix_shortcuts(BOOL common, HKEY backup) // returns the number of DynaRun shortcuts on the Desktop +{ + fix_ctx c; + c.backup = backup; c.desk = 0; + shortcut_folders(SFU_FIX, common ? SF_COMMON : SF_USER, fix_folder, &c); + return c.desk; +} + +// restore_shortcuts writes files at paths that come from the registry. A path is used only when it is a .lnk file below one of the +// shortcut folders the backups were made from (SF[]: the rows that list SFU_RESTORE; user's or all users', as the backup is), with no ".." +// component and no link (junction) as its folder. When this process is elevated and the backups are in +// HKCU, which a program of the same user can write, the file must also be below the user's profile folder. +static BOOL under_dir(const WCHAR *path, const WCHAR *dir) +{ + int n = lstrlenW(dir); + while (n > 0 && dir[n - 1] == '\\') n--; + return n > 0 && lstrlenW(path) > n + 1 && path[n] == '\\' && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, path, n, dir, n) == CSTR_EQUAL; +} + +static BOOL has_dotdot(const WCHAR *p) +{ + const WCHAR *s = p; + for (; *p; p++) if (p[0] == '.' && p[1] == '.' && (p == s || p[-1] == '\\') && (p[2] == 0 || p[2] == '\\')) return TRUE; + return FALSE; +} + +// The syntax part of lnk_path_ok, without any folder lookup (tested with Windows 7 style paths): an absolute drive or UNC path that ends in +// ".lnk", has no ".." component and lies below one of dirs[0..n) (case-insensitive; trailing backslashes of a folder do not matter). +static BOOL lnk_name_in_dirs(const WCHAR *name, const WCHAR *const *dirs, int n) +{ + int i, len = lstrlenW(name); + if (len < 8 || len >= MAX_PATH || lstrcmpiW(name + len - 4, L".lnk") || name[len - 5] == '\\' || has_dotdot(name)) return FALSE; // not a file called just ".lnk" + if (!(((name[0] >= 'A' && name[0] <= 'Z') || (name[0] >= 'a' && name[0] <= 'z')) && name[1] == ':' && name[2] == '\\') && + !(name[0] == '\\' && name[1] == '\\' && name[2] != '?' && name[2] != '.')) return FALSE; // an absolute drive or UNC path, no device path + for (i = 0; i < n; i++) if (dirs[i] && dirs[i][0] && under_dir(name, dirs[i])) return TRUE; + return FALSE; +} + +// d: the long form of a folder or file name (8.3 components expanded) when Windows can tell, else unchanged +static void long_form(WCHAR *d) +{ + WCHAR t[MAX_PATH]; DWORD n = GetLongPathNameW(d, t, MAX_PATH); + if (n && n < MAX_PATH) lstrcpyW(d, t); +} + +typedef struct { WCHAR dirs[10][MAX_PATH]; const WCHAR *list[10]; int n; } restore_dirs; +static BOOL restore_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *vc) +{ + restore_dirs *r = vc; + lstrcpyW(r->dirs[r->n], dir); r->list[r->n] = r->dirs[r->n]; r->n++; + if (id != SF_OWN) { // (the folder of desktop_link() is listed as the shell reports it) + lstrcpyW(r->dirs[r->n], dir); long_form(r->dirs[r->n]); // the backup may hold the long form of a folder that the shell reports in 8.3 form + r->list[r->n] = r->dirs[r->n]; r->n++; + } + return TRUE; +} + +static BOOL lnk_path_ok(const WCHAR *name, BOOL common) +{ + static restore_dirs rd; + WCHAR d[MAX_PATH], *e, par[MAX_PATH], lname[MAX_PATH]; int len = lstrlenW(name); DWORD a; + if (len < 8 || len >= MAX_PATH) return FALSE; + if (!common && is_admin()) { // elevated: the user's own registry is not trusted beyond their profile + if (!SHGetSpecialFolderPathW(NULL, d, CSIDL_PROFILE, FALSE)) return FALSE; + if (!under_dir(name, d)) { + lstrcpyW(lname, name); long_form(lname); long_form(d); // 8.3 form on one side only + if (!under_dir(lname, d)) return FALSE; + } + } + rd.n = 0; + shortcut_folders(SFU_RESTORE, common ? SF_COMMON : SF_USER, restore_folder, &rd); + lstrcpyW(par, name); for (e = par + len; e > par && *e != '\\'; e--); *e = 0; + if (!lnk_name_in_dirs(name, rd.list, rd.n)) { + lstrcpyW(lname, par); long_form(lname); // the same, with the folder of the file in long form + if (!path_join(lname, MAX_PATH, lname, e + 1)) return FALSE; + if (!lnk_name_in_dirs(lname, rd.list, rd.n)) return FALSE; + } + a = GetFileAttributesW(par); + if (a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_REPARSE_POINT)) return FALSE; + a = GetFileAttributesW(name); + return a == INVALID_FILE_ATTRIBUTES || !(a & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)); +} + +// The backup stays (key not deleted) when a shortcut could not be put back or was refused: nothing is lost, a later uninstall can retry. +static void restore_shortcuts(HKEY root, const WCHAR *key) +{ + HKEY k; DWORD i, nmax = 0, dmax = 0, n, dn, type; WCHAR *name; BYTE *data; WCHAR sub[64]; BOOL failed = FALSE, common = root == HKEY_LOCAL_MACHINE; + if (RegOpenKeyExW(root, cat3(sub, key, L"\\Shortcuts", NULL), 0, KEY_READ, &k)) return; + RegQueryInfoKeyW(k, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &nmax, &dmax, NULL, NULL); + name = alloc((nmax + 2) * sizeof(WCHAR)); data = alloc(dmax + 2); + if (!name || !data) failed = TRUE; + for (i = 0; name && data && (n = nmax + 1, dn = dmax, !RegEnumValueW(k, i, name, &n, NULL, &type, data, &dn)); i++) { + if (type != REG_BINARY || !lnk_path_ok(name, common)) { failed = TRUE; setup_log(L"a shortcut backup was not restored: its path is not a .lnk file in a shell folder"); continue; } + if (classify(name) != LNK_OURS && exists(name)) continue; // changed by the user since: leave it + if (!dn) { if (exists(name) && !DeleteFileW(name)) failed = TRUE; } // a shortcut we created + else if (dn < 76 || data[0] != 0x4C || data[1] || data[2] || data[3] || !write_file(name, data, dn)) failed = TRUE; // a .lnk starts with its 0x4C header size + } + release(name); release(data); + RegCloseKey(k); + if (!failed) RegDeleteKeyW(root, sub); +} + +// Uninstall, other accounts: their backups are in their own HKCU, so their shortcuts that still start +// the launcher (which is about to be deleted) are pointed back at DynaRun V3.exe instead. Only files that +// are our shortcuts are rewritten, so a folder read from another user's registry can do no harm; reparse +// points (junctions) are not followed. +typedef struct { WCHAR p[2 * MAX_PATH]; BOOL recurse; } unret_walk; +static void unretarget(const WCHAR *dir, BOOL recurse); +static BOOL unret_lnk(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) +{ + unret_walk *w = vc; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) return TRUE; + if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { if (w->recurse) unretarget(p, TRUE); return TRUE; } + if (is_lnk(p) && g_exe[0] && classify(p) == LNK_OURS) relink(p, g_exe); + return TRUE; +} + +static void unretarget(const WCHAR *dir, BOOL recurse) +{ + unret_walk *w = alloc(sizeof(*w)); + if (!w) return; + w->recurse = recurse; + dir_walk(dir, w->p, 2 * MAX_PATH, 2 * MAX_PATH + 1, unret_lnk, w); + release(w); +} + +// What is left of our shortcuts in the shell folders after the backups were put back (a backup that is missing, as when an older version did +// not record one, or was refused): they point to DynaRun V3.exe again instead of the launcher that is being removed. The folders include the +// pins the user made of the launcher (Quick Launch). +static BOOL unret_folder(const WCHAR *dir, int id, BOOL common, BOOL deep, void *ctx) { unretarget(dir, deep); return TRUE; } +static void unretarget_shell(BOOL common) +{ + if (!g_exe[0]) return; + shortcut_folders(SFU_UNRET, common ? SF_COMMON : SF_USER, unret_folder, NULL); +} + diff --git a/src/setup/stage.h b/src/setup/stage.h new file mode 100644 index 0000000..16b05be --- /dev/null +++ b/src/setup/stage.h @@ -0,0 +1,30 @@ +// Part of setup.c (stage lock and setup.log): included by it, in one compilation unit; never compiled on its own. +/* ---------- elevated stage: one at a time, trust checks, the borrowed time stamp ---------- */ + +// The elevated stages (install, uninstall, MSI) of two setups must not interleave. Recursive for the owning thread. +static HANDLE g_stage_mx; static int g_stage_depth; + +static BOOL stage_lock(void) +{ + DWORD r; + if (!g_stage_mx && !(g_stage_mx = CreateMutexW(NULL, FALSE, L"Global\\DynaRunFix-machine-stage"))) return FALSE; + r = WaitForSingleObject(g_stage_mx, 60000); + if (r != WAIT_OBJECT_0 && r != WAIT_ABANDONED) return FALSE; // an abandoned mutex (a setup that was killed) is ours now + g_stage_depth++; + return TRUE; +} + +static void stage_unlock(void) { if (g_stage_depth > 0) { g_stage_depth--; ReleaseMutex(g_stage_mx); } } + +static const WCHAR *busy_text(void) +{ return T(L"Another DynaRunFix setup is running. Wait until it finishes and try again.", L"另一個 DynaRunFix 安裝程式正在執行,請等它完成後再試一次。"); } + +// A line in \setup.log (only the elevated stage writes there; the folder is administrators-only) +static void setup_log(const WCHAR *line) +{ + static WCHAR p[MAX_PATH]; + if (g_dir[0] && exists(g_dir)) append_line(cat3(p, g_dir, L"\\setup.log", NULL), line); +} + +static void warn(const install_opts *o, const WCHAR *text) { setup_log(text); if (!o->quiet) msg(text, MB_ICONWARNING); } + diff --git a/src/setup/taskbar.h b/src/setup/taskbar.h new file mode 100644 index 0000000..9d68919 --- /dev/null +++ b/src/setup/taskbar.h @@ -0,0 +1,224 @@ +// Part of setup.c (sign-in start, taskbar pin, unelevated start): included by it, in one compilation unit; never compiled on its own. +/* ---------- taskbar pin and sign-in start ---------- */ + +static void drop_run(HKEY hive, BOOL only_ours) // the "Run" value; of other accounts only when it starts our launcher +{ + static WCHAR v[MAX_PATH]; HKEY k; + if (only_ours && !(reg_str(hive, RUNKEY, L"DynaRunFix", v, MAX_PATH, 0) && contains_i(v, g_launcher))) return; + if (!RegOpenKeyExW(hive, RUNKEY, 0, KEY_SET_VALUE, &k)) { RegDeleteValueW(k, L"DynaRunFix"); RegCloseKey(k); } +} + +static WCHAR *layout_xml(WCHAR *out) { return cat3(out, g_dir, L"\\TaskbarLayout.xml", NULL); } + +static void drop_policy(HKEY hive) // the Windows 11 taskbar layout policy, when it is still ours +{ + static WCHAR v[MAX_PATH], x[MAX_PATH]; HKEY k; + if (reg_str(hive, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(x)) && + !RegOpenKeyExW(hive, POLKEY, 0, KEY_SET_VALUE, &k)) { + RegDeleteValueW(k, L"StartLayoutFile"); RegDeleteValueW(k, L"LockedStartLayout"); RegCloseKey(k); + } +} + +static const WCHAR XML_HEAD[] = + L"\n" + L"\n" + L" \n \n \n" + L" keep) return; + if (!o->pin) { // reinstalled without the pin: take our policy back + if (reg_str(app, NULL, L"TaskbarPolicySid", v, MAX_PATH, 0) && !RegOpenKeyExW(HKEY_USERS, v, 0, KEY_READ | KEY_WRITE, &k)) { drop_policy(k); RegCloseKey(k); } + RegDeleteValueW(app, L"TaskbarPolicy"); RegDeleteValueW(app, L"TaskbarPolicySid"); + return; + } + if (ma < 10 || b < 22000) return; + cat3(path, sid, L"\\", POLKEY); + layout_xml(x); + if (reg_str(HKEY_USERS, path, L"StartLayoutFile", v, MAX_PATH, 0) && lstrcmpiW(v, x)) return; + if (!save_link(cat3(l, g_dir, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return; + if (!RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_QUERY_VALUE | WOW, &k)) { + RegQueryValueExW(k, L"UBR", NULL, NULL, (BYTE *)&ubr, &n); RegCloseKey(k); + } + // PinGeneration only on builds that know it: elsewhere it makes Windows ignore the pin + gen = b >= 26200 || (b == 26100 && ubr >= 4484) || (b == 22631 && ubr >= 5549); + lstrcpyW(xml, XML_HEAD); lstrcatW(xml, l); + lstrcatW(xml, gen ? L"\" PinGeneration=\"1\"/>\n" : L"\"/>\n"); + lstrcatW(xml, L" \n \n \n\n"); + n = WideCharToMultiByte(CP_UTF8, 0, xml, -1, u, sizeof(u), NULL, NULL); + if (!n || !write_file(x, u, n - 1)) return; + if (RegCreateKeyExW(HKEY_USERS, path, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) return; + RegSetValueExW(k, L"StartLayoutFile", 0, REG_EXPAND_SZ, (const BYTE *)x, (lstrlenW(x) + 1) * sizeof(WCHAR)); + set_dword(k, L"LockedStartLayout", 1); + RegCloseKey(k); + set_dword(app, L"TaskbarPolicy", 1); set_str(app, L"TaskbarPolicySid", sid); +} + +static BOOL pinned_ours(void) // a shortcut to the launcher among the taskbar's pinned items (Windows 7+) +{ + static WCHAR d[MAX_PATH], f[MAX_PATH]; + if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; + lstrcatW(d, L"\\User Pinned\\TaskBar"); + return find_ours(d, FALSE, f); +} + +static BOOL wait_pinned(void) // Explorer writes the shortcut a moment after the call returned +{ + int i; + for (i = 0; i < 30; i++) { if (pinned_ours()) return TRUE; Sleep(100); } + return FALSE; +} + +typedef HRESULT (WINAPI *SHParseDisplayName_t)(PCWSTR, IBindCtx *, PIDLIST_ABSOLUTE *, ULONG, ULONG *); +typedef HRESULT (WINAPI *SHBindToParent_t)(PCIDLIST_ABSOLUTE, REFIID, void **, PCUITEMID_CHILD *); + +static PIDLIST_ABSOLUTE item_id(const WCHAR *p) // not on XP's shell32: looked up +{ + SHParseDisplayName_t f = (SHParseDisplayName_t)GetProcAddress(GetModuleHandleW(L"shell32.dll"), "SHParseDisplayName"); PIDLIST_ABSOLUTE id = NULL; + if (!f || FAILED(f(p, NULL, &id, 0, NULL))) return NULL; + return id; +} + +static BOOL pin_verb(const WCHAR *lnk) // Windows 7 .. Windows 10 1803: the shell's "Pin to taskbar" verb +{ + SHBindToParent_t bind = (SHBindToParent_t)GetProcAddress(GetModuleHandleW(L"shell32.dll"), "SHBindToParent"); + PIDLIST_ABSOLUTE id = item_id(lnk); IShellFolder *sf; PCUITEMID_CHILD ch; IContextMenu *cm; HMENU m; CMINVOKECOMMANDINFO ci; BOOL r = FALSE; + if (id && bind && SUCCEEDED(bind(id, &IID_IShellFolder, (void **)&sf, &ch))) { + if (SUCCEEDED(IShellFolder_GetUIObjectOf(sf, NULL, 1, &ch, &IID_IContextMenu, NULL, (void **)&cm))) { + m = CreatePopupMenu(); + if (SUCCEEDED(IContextMenu_QueryContextMenu(cm, m, 0, 1, 0x7fff, CMF_NORMAL))) { + zero(&ci, sizeof(ci)); ci.cbSize = sizeof(ci); ci.lpVerb = "taskbarpin"; ci.nShow = SW_HIDE; + r = SUCCEEDED(IContextMenu_InvokeCommand(cm, &ci)); + } + DestroyMenu(m); IContextMenu_Release(cm); + } + IShellFolder_Release(sf); + } + CoTaskMemFree(id); + return r; +} + +static BOOL pin_list(const WCHAR *lnk) // Windows 10 1809 and later: Explorer's IPinnedList3 +{ + IPinnedList3 *pl; PIDLIST_ABSOLUTE id = item_id(lnk); BOOL r = FALSE; + if (id && SUCCEEDED(CoCreateInstance(&CLSID_TaskbandPin, NULL, CLSCTX_INPROC_SERVER, &IID_IPinnedList3, (void **)&pl))) { + r = SUCCEEDED(pl->lpVtbl->Modify(pl, NULL, id, 0x7fffffff)); + pl->lpVtbl->Release(pl); + } + CoTaskMemFree(id); + return r; +} + +// Pins DynaRun (the shortcut lnk, which starts the launcher) to the taskbar of the current user. TRUE: done, or +// on Windows 11 under way (*res = PIN_NEXT_SIGNIN: the policy set by the machine stage applies at the next sign-in). +static BOOL pin_taskbar(const WCHAR *lnk, int *res) +{ + static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; + os_ver(&ma, &mi, &b); + if (ma == 5 || (ma == 6 && !mi)) { // XP, Vista: the Quick Launch toolbar + if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; + CreateDirectoryW(d, NULL); + if (!save_link(lstrcatW(d, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; + if (!RegOpenKeyExW(HKEY_CURRENT_USER, cat3(sub, USERKEY, L"\\Shortcuts", NULL), 0, KEY_SET_VALUE, &k)) { + RegSetValueExW(k, d, 0, REG_BINARY, (const BYTE *)"", 0); // a shortcut we created: uninstall deletes it + RegCloseKey(k); + } + return TRUE; + } + if (ma >= 10 && b >= 22000) { // Windows 11: see taskbar_policy + if (reg_str(HKEY_CURRENT_USER, POLKEY, L"StartLayoutFile", v, MAX_PATH, 0) && !lstrcmpiW(v, layout_xml(d))) *res = PIN_NEXT_SIGNIN; + return *res == PIN_NEXT_SIGNIN; + } + if (pinned_ours()) return TRUE; + if (ma >= 10 && b >= 17763) pin_list(lnk); else pin_verb(lnk); + return wait_pinned(); +} + +static BOOL desktop_link(WCHAR *out, HKEY backup) // one of our shortcuts to pin: desktop, Start menu, else a new one +{ + WCHAR d[MAX_PATH]; + if (find_our_shortcut(out)) return TRUE; + if (!sf_path(SF_OWN, FALSE, d)) return FALSE; + CreateDirectoryW(d, NULL); + if (!save_link(cat3(out, d, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; + RegSetValueExW(backup, out, 0, REG_BINARY, (const BYTE *)"", 0); + return TRUE; +} + +// Opens file the way a double click does, but as the signed-in user's normal (non-elevated) process: the desktop's shell (Explorer, which runs +// with the normal token) is asked to do it - IShellWindows -> the desktop's shell view -> its Application (IShellDispatch2) -> ShellExecute. +// FALSE when any step fails (no Explorer, no desktop: e.g. a session without one). +static BOOL shell_execute_unelevated(const WCHAR *file, const WCHAR *dir) +{ + IShellWindows *sw = NULL; IDispatch *disp = NULL, *dv = NULL, *app = NULL; IServiceProvider *sp = NULL; IShellBrowser *sb = NULL; IShellView *view = NULL; + IShellFolderViewDual *fvd = NULL; IShellDispatch2 *sd = NULL; HRESULT hrc; VARIANT loc, root, a, d, o, s; LONG hwnd = 0; BSTR bf = NULL, bd = NULL; BOOL ok = FALSE; + hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // RPC_E_CHANGED_MODE: already initialised another way, usable + if (FAILED(hrc) && hrc != RPC_E_CHANGED_MODE) return FALSE; + VariantInit(&loc); VariantInit(&root); VariantInit(&a); VariantInit(&d); VariantInit(&o); VariantInit(&s); + loc.vt = VT_I4; loc.lVal = CSIDL_DESKTOP; + if (FAILED(CoCreateInstance(&CLSID_ShellWindows, NULL, CLSCTX_LOCAL_SERVER, &IID_IShellWindows, (void **)&sw))) goto done; + if (FAILED(IShellWindows_FindWindowSW(sw, &loc, &root, SWC_DESKTOP, &hwnd, SWFO_NEEDDISPATCH, &disp)) || !disp) goto done; + if (FAILED(IDispatch_QueryInterface(disp, &IID_IServiceProvider, (void **)&sp))) goto done; + if (FAILED(IServiceProvider_QueryService(sp, &SID_STopLevelBrowser, &IID_IShellBrowser, (void **)&sb))) goto done; + if (FAILED(IShellBrowser_QueryActiveShellView(sb, &view)) || !view) goto done; + if (FAILED(IShellView_GetItemObject(view, SVGIO_BACKGROUND, &IID_IDispatch, (void **)&dv)) || !dv) goto done; + if (FAILED(IDispatch_QueryInterface(dv, &IID_IShellFolderViewDual, (void **)&fvd))) goto done; + if (FAILED(IShellFolderViewDual_get_Application(fvd, &app)) || !app) goto done; + if (FAILED(IDispatch_QueryInterface(app, &IID_IShellDispatch2, (void **)&sd))) goto done; + if (!(bf = SysAllocString(file))) goto done; + if (dir && dir[0] && (bd = SysAllocString(dir))) { d.vt = VT_BSTR; d.bstrVal = bd; } + s.vt = VT_I4; s.lVal = SW_SHOWNORMAL; + ok = SUCCEEDED(IShellDispatch2_ShellExecute(sd, bf, a, d, o, s)); +done: + SysFreeString(bf); SysFreeString(bd); + if (sd) IShellDispatch2_Release(sd); + if (app) IDispatch_Release(app); + if (fvd) IShellFolderViewDual_Release(fvd); + if (dv) IDispatch_Release(dv); + if (view) IShellView_Release(view); + if (sb) IShellBrowser_Release(sb); + if (sp) IServiceProvider_Release(sp); + if (disp) IDispatch_Release(disp); + if (sw) IShellWindows_Release(sw); + if (SUCCEEDED(hrc)) CoUninitialize(); + return ok; +} + +// DynaRun is never started with more rights than a normal start from the desktop: an elevated wizard (setup run as administrator; the sign-in task +// of the driver installation, which runs with the user's full token) asks the shell to start it as the normal user. If that is impossible it is not +// started elevated: the user is told to use the desktop icon. Which way was used goes to the log. +static void launch_normal(const WCHAR *file, const WCHAR *dir) +{ + if (!is_admin()) { // already a normal process: a plain ShellExecute is the normal start + ShellExecuteW(g_hwnd, NULL, file, NULL, dir, SW_SHOWNORMAL); + run_log(L"start DynaRun: ShellExecute (this process is not elevated)"); + return; + } + if (shell_execute_unelevated(file, dir)) { run_log(L"start DynaRun: through the desktop shell, as the normal user (this process is elevated)"); return; } + run_log(L"start DynaRun: not started, this process is elevated and the desktop shell could not be asked"); + msg(T(L"DynaRun was not started from here. Please start it with the \"DynaRun V3\" icon on the desktop.", L"這裡沒有啟動 DynaRun,請改用桌面上的「DynaRun V3」圖示開啟。"), MB_ICONINFORMATION); +} + +// The wizard's "Start DynaRun": started the way a double click on the shortcut does it (the shortcut gives the +// process the AppUserModelID that groups it with the pinned icon, its working folder and show command), not by +// running the launcher directly. Falls back to the launcher with DynaRun's folder as working directory. +void start_dynarun(void) +{ + static WCHAR lnk[MAX_PATH], dir[MAX_PATH]; + WCHAR *q; BOOL have = find_our_shortcut(lnk); + if (!have && exists(cat3(lnk, g_dir, L"\\DynaRun V3.lnk", NULL))) have = TRUE; + if (have) { launch_normal(lnk, NULL); return; } + lstrcpyW(dir, g_exe); q = dir + lstrlenW(dir); while (q > dir && *q != '\\') q--; *q = 0; + launch_normal(g_launcher, dir[0] ? dir : NULL); +} + diff --git a/src/setup/trust.h b/src/setup/trust.h new file mode 100644 index 0000000..c9bedf4 --- /dev/null +++ b/src/setup/trust.h @@ -0,0 +1,80 @@ +// Part of setup.c (ACL and exe trust checks): included by it, in one compilation unit; never compiled on its own. +static BOOL sid_in(PSID s, const WCHAR *const *list) +{ + PSID t; BOOL r = FALSE; int i; + for (i = 0; !r && list[i]; i++) if (ConvertStringSidToSidW(list[i], &t)) { r = EqualSid(s, t); LocalFree(t); } + return r; +} + +#define WRITE_MASK (FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_DELETE_CHILD | DELETE | WRITE_DAC | WRITE_OWNER | GENERIC_WRITE | GENERIC_ALL) + +// TRUE when ordinary users could change what is at path (or when that cannot be told). Not user-writable: owned by SYSTEM, +// the Administrators or TrustedInstaller, and no ACE that applies to the object itself gives Everyone, Users, Authenticated Users, +// Interactive, Power Users or the user who started the setup a write, delete or permission right. +static BOOL user_writable(const trust_ctx *t, const WCHAR *path) +{ + static const WCHAR *const owners[] = { L"S-1-5-18", L"S-1-5-32-544", L"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464", NULL }; + static const WCHAR *const others[] = { L"S-1-1-0", L"S-1-5-32-545", L"S-1-5-11", L"S-1-5-4", L"S-1-5-32-547", NULL }; + const WCHAR *const me[2] = { t->user_sid, NULL }; + PSID owner = NULL; PACL dacl = NULL; PSECURITY_DESCRIPTOR sd = NULL; BOOL bad = TRUE; DWORD i; ACCESS_ALLOWED_ACE *a; + if (GetNamedSecurityInfoW((LPWSTR)path, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner, NULL, &dacl, NULL, &sd)) return TRUE; + if (owner && dacl && sid_in(owner, owners)) { // no DACL = full access for everyone: bad + bad = FALSE; + for (i = 0; i < dacl->AceCount && !bad; i++) { + if (!GetAce(dacl, i, (void **)&a)) { bad = TRUE; break; } + if (a->Header.AceType != ACCESS_ALLOWED_ACE_TYPE || (a->Header.AceFlags & INHERIT_ONLY_ACE) || !(a->Mask & WRITE_MASK)) continue; + if (sid_in((PSID)&a->SidStart, others) || (t->user_sid[0] && sid_in((PSID)&a->SidStart, me))) bad = TRUE; + } + } + LocalFree(sd); + return bad; +} + +static BOOL (*g_is_writable)(const trust_ctx *t, const WCHAR *path) = user_writable; // the tests replace this + +// driver.c: may ordinary users (and the user who started the setup, sid; NULL: no such user is known) change what is at path? +BOOL path_user_writable(const WCHAR *path, const WCHAR *sid) +{ + trust_ctx t; + zero(&t, sizeof(t)); + if (sid) lstrcpynW(t.user_sid, sid, 200); + return g_is_writable(&t, path); +} + +static BOOL is_dynarun_name(const WCHAR *p) +{ + const WCHAR *n = p + lstrlenW(p); + while (n > p && n[-1] != '\\') n--; + return !lstrcmpiW(n, L"DynaRun V3.exe"); +} + +typedef DWORD (WINAPI *GetFinalPathNameByHandleW_t)(HANDLE, LPWSTR, DWORD, DWORD); + +// The elevated stage does not trust the DynaRun path it was given on its command line. It must be an absolute path on a +// fixed drive, named DynaRun V3.exe, an existing file. t->exe_trusted says whether what the elevated stage writes there +// (manifest), starts (the borrowed time stamp) and registers (COM servers in that folder) is safe: no link in the path, and +// neither the folder nor the exe changeable by ordinary users. FALSE: the path is unusable; exe_trusted FALSE only: skip those steps. +static BOOL check_exe(trust_ctx *t) +{ + WCHAR root[4], fin[MAX_PATH + 8], dir[MAX_PATH], *e; HANDLE h; GetFinalPathNameByHandleW_t f; DWORD n; + t->exe_trusted = FALSE; + if (!g_exe[0] || lstrlenW(g_exe) >= MAX_PATH - 1 || !is_dynarun_name(g_exe)) return FALSE; + if (!((g_exe[0] >= 'A' && g_exe[0] <= 'Z') || (g_exe[0] >= 'a' && g_exe[0] <= 'z')) || g_exe[1] != ':' || g_exe[2] != '\\') return FALSE; + lstrcpynW(root, g_exe, 4); + if (GetDriveTypeW(root) != DRIVE_FIXED) return FALSE; + h = CreateFileW(g_exe, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, NULL); + if (h == INVALID_HANDLE_VALUE) return FALSE; + f = (GetFinalPathNameByHandleW_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "GetFinalPathNameByHandleW"); + if (GetFileType(h) != FILE_TYPE_DISK) { CloseHandle(h); return FALSE; } + n = f ? f(h, fin, MAX_PATH + 8, 0) : 0; + CloseHandle(h); + // Windows XP has no GetFinalPathNameByHandleW: a junction in the path to DynaRun V3.exe cannot be detected there, only the + // ACL checks below apply (limitation; later Windows versions check for links) + if (!f) setup_log(L"Windows XP: links (junctions) in the path to DynaRun V3.exe cannot be detected"); + // with GetFinalPathNameByHandleW (Vista and later) the final path must be the path given: no link anywhere in it + if (f && (!n || n >= MAX_PATH + 8 || fin[0] != '\\' || fin[1] != '\\' || fin[2] != '?' || fin[3] != '\\' || lstrcmpiW(fin + 4, g_exe))) return TRUE; + lstrcpyW(dir, g_exe); for (e = dir + lstrlenW(dir); e > dir && *e != '\\'; e--); *e = 0; + t->exe_trusted = !g_is_writable(t, dir) && !g_is_writable(t, g_exe); + return TRUE; +} + diff --git a/src/setup/uninstall.h b/src/setup/uninstall.h new file mode 100644 index 0000000..440b4bd --- /dev/null +++ b/src/setup/uninstall.h @@ -0,0 +1,117 @@ +// Part of setup.c (uninstall): included by it, in one compilation unit; never compiled on its own. +// The uninstall question's Yes / No buttons become "Fix only" / "Remove all". +static HHOOK g_cbt; +static LRESULT CALLBACK relabel(int code, WPARAM w, LPARAM l) +{ + HHOOK h = g_cbt; + if (code == HCBT_ACTIVATE) { + SetDlgItemTextW((HWND)w, IDYES, T(L"&Fix only", L"只移除修正(&F)")); + SetDlgItemTextW((HWND)w, IDNO, T(L"&Remove all", L"全部移除(&R)")); + UnhookWindowsHookEx(g_cbt); g_cbt = NULL; + } + return CallNextHookEx(h, code, w, l); +} + +// Copies of the uninstaller that earlier builds (1.4.0-rc1/rc2) left in %TEMP%; a running one stays +static const WCHAR TEMP_PREFIX[] = L"DynaRunFix-uninstall-"; +static void drop_old_temp_copies(void) +{ + WCHAR t[MAX_PATH], f[MAX_PATH]; WIN32_FIND_DATAW fd; HANDLE h; DWORD n = GetTempPathW(MAX_PATH - 60, t); + if (!n || n >= MAX_PATH - 60) return; + lstrcatW(t, TEMP_PREFIX); + h = FindFirstFileW(cat3(f, t, L"*.exe", NULL), &fd); + if (h == INVALID_HANDLE_VALUE) return; + do { + if ((fd.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || lstrlenW(t) + lstrlenW(fd.cFileName) >= MAX_PATH) continue; + GetTempPathW(MAX_PATH - 60, f); lstrcatW(f, fd.cFileName); + DeleteFileW(f); + } while (FindNextFileW(h, &fd)); + FindClose(h); +} + +static int uninstall(const install_opts *given) +{ + static WCHAR p[MAX_PATH + 160], prod[40], sid[200], args[260]; int rc; BOOL all = FALSE; + install_opts mine = *given, *o = &mine; // the "close DynaRun" consent asked below is this uninstall's own + const WCHAR *elev = NULL; + // The uninstaller runs from the installed program (no copy in %TEMP%): the elevated stage renames what it cannot delete (this very + // file, while the user-side stage still runs) and Windows deletes it at the next restart. + cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); + drop_old_temp_copies(); // copies that earlier builds left in %TEMP% + if (!exists(p) && !reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0)) { + info(o, T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); + return RC_OK; + } + if (!o->quiet && !dynarun_product(prod)) { + if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; + } else if (!o->quiet) { + g_cbt = SetWindowsHookExW(WH_CBT, relabel, NULL, GetCurrentThreadId()); + rc = msg(T(L"Uninstall DynaRunFix?\n\n" + L"Fix only: the DynaRun shortcuts start the original DynaRun again; DynaRun V3 stays installed.\n\n" + L"Remove all: DynaRun V3 is then uninstalled too, by Dyna Pro's own uninstaller.", + L"要解除安裝 DynaRunFix 嗎?\n\n" + L"只移除修正:DynaRun 的捷徑改回開啟原版,DynaRun V3 保留。\n\n" + L"全部移除:接著再用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3。"), MB_YESNOCANCEL | MB_ICONQUESTION); + if (g_cbt) { UnhookWindowsHookEx(g_cbt); g_cbt = NULL; } + if (rc == IDCANCEL) return RC_CANCEL; + all = rc == IDNO; + } + // Running DynaRun / launcher / LEProc hold files of the folder: ask first, the elevated stage then closes them. + // /quiet without /close goes on; what is still in use is deleted at the next restart. + if (!o->quiet && running_programs()) { + if (msg(T(L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" + L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", + L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" + L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; + o->close = TRUE; + } + reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); + if (!g_exe[0]) locate_dynarun(); + CoInitialize(NULL); + if (!user_sid(sid, 200)) sid[0] = 0; + // The elevated stage comes first: when the UAC prompt is cancelled (or fails to start) nothing has been changed yet and the + // user's shortcuts, Run value and pins still work with the installed launcher. + if (is_admin()) rc = machine_uninstall(o, sid); + else { + trust_ctx t; + // What runs elevated is the installed program in Program Files (administrators-only folder), never a copy in a folder the user can + // write. Only when there is no installed copy (a broken install) is this file itself run, if it is not one of our %TEMP% copies. + zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); + if (exists(p)) { + if (g_is_writable(&t, g_dir) || g_is_writable(&t, p)) { + error2(o, T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", + L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); + return RC_FAIL; + } + elev = p; + } + wsprintfW(args, L"/unmachine %s", sid); + rc = run_elevated(sid[0] ? args : L"/unmachine", elev, o); + } + if (rc == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } + if (rc) { // the elevated stage did not finish: nothing user-side is undone either + if (!o->quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } + return rc; + } + restore_shortcuts(HKEY_CURRENT_USER, USERKEY); + RegDeleteKeyW(HKEY_CURRENT_USER, USERKEY); + drop_run(HKEY_CURRENT_USER, FALSE); + drop_policy(HKEY_CURRENT_USER); // the Windows 11 taskbar policy of this user, when it is still ours + unretarget_shell(FALSE); // our shortcuts that no backup covered, and pins the user made + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); + if (all) { + rc = remove_dynarun(); + if (rc == ERROR_INSTALL_USEREXIT) { info(o, T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } + if (rc > 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { + wsprintfW(p, T(L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", + L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。"), rc); + msg(p, MB_ICONERROR); + return RC_MSI_FAILED; + } + info(o, T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); + return RC_OK; + } + info(o, T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); + return RC_OK; +} + diff --git a/src/setup/users.h b/src/setup/users.h new file mode 100644 index 0000000..1b32a77 --- /dev/null +++ b/src/setup/users.h @@ -0,0 +1,94 @@ +// Part of setup.c (other accounts' hives): included by it, in one compilation unit; never compiled on its own. +BOOL enable_privilege(const WCHAR *name) +{ + HANDLE t; TOKEN_PRIVILEGES tp; BOOL r = FALSE; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &t)) return FALSE; + tp.PrivilegeCount = 1; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + if (LookupPrivilegeValueW(NULL, name, &tp.Privileges[0].Luid)) + r = AdjustTokenPrivileges(t, FALSE, &tp, 0, NULL, NULL) && GetLastError() == ERROR_SUCCESS; + CloseHandle(t); + return r; +} + +// One of the user's shell folders, from their registry (redirected or localized folders, e.g. XP's +// "「開始」功能表"); \ when it is not set. The values come from another user's hive: all copies are bounded. +// out holds cch characters; the result is shorter than cch or empty (then the caller skips the folder). +static void user_folder(HKEY hive, const WCHAR *profile, const WCHAR *name, const WCHAR *def, WCHAR *out, int cch) +{ + static WCHAR v[MAX_PATH], t[2 * MAX_PATH + 16]; DWORD n; + out[0] = 0; + if (hive && reg_str(hive, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", name, v, MAX_PATH, 0)) { + t[0] = 0; + if (!lstrcmpiW(lstrcpynW(t, v, 14), L"%USERPROFILE%")) { // that user's, not ours + if (lstrlenW(profile) + lstrlenW(v + 13) + 1 <= (int)(sizeof(t) / sizeof(t[0]))) cat3(t, profile, v + 13, NULL); else t[0] = 0; + } + else lstrcpyW(t, v); // v is at most MAX_PATH + if (t[0]) { + n = ExpandEnvironmentStringsW(t, out, cch); + if (n && n <= (DWORD)cch) return; + } + out[0] = 0; + } + if (lstrlenW(profile) + 1 + lstrlenW(def) < cch) cat3(out, profile, L"\\", def); +} + +#define HIVE_PREFIX L"DynaRunFix-uninstall" +#define FOLDER_CCH (2 * MAX_PATH) // buffer of a folder path; 64 characters of it stay free for what is appended + +// A hive that an earlier uninstall loaded and could not unload (it was killed) would keep that user's NTUSER.DAT locked. +static void unload_leftover_hives(void) +{ + static WCHAR names[16][80]; WCHAR name[260]; DWORD i, n; int k = 0; + for (i = 0; k < 16 && (n = 260, !RegEnumKeyExW(HKEY_USERS, i, name, &n, NULL, NULL, NULL, NULL)); i++) + if (n >= 20 && n < 80 && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, name, 20, HIVE_PREFIX, 20) == CSTR_EQUAL) lstrcpyW(names[k++], name); + while (k) RegUnLoadKeyW(HKEY_USERS, names[--k]); +} + +static void unretarget_user(const WCHAR *sid, const WCHAR *profile) +{ + static int seq; + static WCHAR p[FOLDER_CCH], sub[300], tmphive[80]; HKEY hive = NULL; BOOL loaded = FALSE; int id; + wsprintfW(tmphive, HIVE_PREFIX L"-%lu-%lu-%d", GetCurrentProcessId(), GetTickCount(), seq++); // unique: never a name another load uses + if (lstrlenW(profile) + 12 >= MAX_PATH) return; // (the caller checked; NTUSER.DAT must fit) + if (RegOpenKeyExW(HKEY_USERS, sid, 0, KEY_READ | KEY_WRITE, &hive)) { // not signed in: load their hive + hive = NULL; + if (!RegLoadKeyW(HKEY_USERS, tmphive, cat3(p, profile, L"\\NTUSER.DAT", NULL)) && + !RegOpenKeyExW(HKEY_USERS, tmphive, 0, KEY_READ | KEY_WRITE, &hive)) loaded = TRUE; + else hive = NULL; + } + for (id = 0; id < SF_COUNT; id++) { // the same folders as unretarget_shell, of their account + if (!(SF[id].uses & SFU_UNRET)) continue; + user_folder(hive, profile, SF[id].reg, SF[id].rel, p, FOLDER_CCH - 64); + if (!p[0]) continue; + if (SF[id].sub) lstrcatW(p, SF[id].sub); + unretarget(p, (SF[id].deep & SFU_UNRET) != 0); + } + if (hive) { + drop_run(hive, TRUE); drop_policy(hive); // start at sign-in, Windows 11 pin + // their backups are of no use any more + RegDeleteKeyW(hive, cat3(sub, USERKEY, L"\\Shortcuts", NULL)); + RegDeleteKeyW(hive, USERKEY); + RegCloseKey(hive); + } + if (loaded) RegUnLoadKeyW(HKEY_USERS, tmphive); +} + +// skip: the user who runs the uninstall; their own shortcuts, backups and Run value are put back by the user-side stage +static void unretarget_all_users(const WCHAR *skip) +{ + static const WCHAR LIST[] = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList"; + static WCHAR sid[200], key[300], raw[MAX_PATH], profile[MAX_PATH]; HKEY k; DWORD i, n; + enable_privilege(L"SeRestorePrivilege"); enable_privilege(L"SeBackupPrivilege"); // to load the hives of users not signed in + unload_leftover_hives(); + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, LIST, 0, KEY_READ | WOW, &k)) return; // the 64-bit view: where the system keeps the profiles + for (i = 0; n = 200, !RegEnumKeyExW(k, i, sid, &n, NULL, NULL, NULL, NULL); i++) { + // people (local, domain, Microsoft Entra), not service accounts + if (lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-5-21") && lstrcmpiW(lstrcpynW(key, sid, 9), L"S-1-12-1")) continue; + if (skip && skip[0] && !lstrcmpiW(sid, skip)) continue; + if (!reg_str(HKEY_LOCAL_MACHINE, cat3(key, LIST, L"\\", sid), L"ProfileImagePath", raw, MAX_PATH, WOW)) continue; + if (ExpandEnvironmentStringsW(raw, profile, MAX_PATH) - 1 >= MAX_PATH || !exists(profile)) continue; + unretarget_user(sid, profile); + } + RegCloseKey(k); +} + diff --git a/src/setup/userstage.h b/src/setup/userstage.h new file mode 100644 index 0000000..83d6061 --- /dev/null +++ b/src/setup/userstage.h @@ -0,0 +1,120 @@ +// Part of setup.c (user stage and install entry points): included by it, in one compilation unit; never compiled on its own. +// Not elevated: the current user's shortcuts; a desktop shortcut when there is none at all. +static void user_stage(const install_opts *o, install_result *res) +{ + static WCHAR p[MAX_PATH]; HKEY b, s; + res->pin = PIN_NOT_ASKED; + if (!app_key_create(APP_USER, &b)) return; + // another version is installed now: what update.c noted no longer applies; the first start of DynaRun looks again + RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); + if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { + int desk = fix_shortcuts(FALSE, s); + if (!desk && sf_path(SF_DESKTOP, TRUE, p)) + desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here + if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p) && + save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) + RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); + if (o->pin && !o->keep) { + res->pin = PIN_FAILED; + if (desktop_link(p, s) && pin_taskbar(p, &res->pin) && res->pin == PIN_FAILED) res->pin = PIN_DONE; + } + RegCloseKey(s); + } + RegCloseKey(b); + if (!o->keep && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { // start at sign-in + // with the driver step ahead the value is NOT written now: the sign-in after the driver restart would start DynaRun while the + // driver is being installed. It is only noted (AutostartPending) and registered by autostart_flush when the driver steps are over. + if (o->autostart && !o->autostart_later) set_str(s, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); + else RegDeleteValueW(s, L"DynaRunFix"); + RegCloseKey(s); + if (app_key_create(APP_USER, &b)) { + if (o->autostart && o->autostart_later) set_dword(b, L"AutostartPending", 1); + else RegDeleteValueW(b, L"AutostartPending"); + RegCloseKey(b); + } + } + SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); +} + +// The end of the driver steps (installed, failed, cancelled, skipped, wizard closed): the sign-in start the user asked for is registered now, +// whatever the driver's outcome. Not called while a restart for the driver is pending (wizard.c). Uninstall removes the flag with HKCU\Software\DynaRunFix. +void autostart_flush(void) +{ + static WCHAR p[MAX_PATH]; HKEY k, r; DWORD v = 0, n = 4, t = 0; BOOL pending; + if (!app_key_open(APP_USER, KEY_ALL_ACCESS, &k)) return; + pending = RegQueryValueExW(k, L"AutostartPending", NULL, &t, (BYTE *)&v, &n) == ERROR_SUCCESS && t == REG_DWORD && v; + if (pending) RegDeleteValueW(k, L"AutostartPending"); + RegCloseKey(k); + if (pending && exists(g_launcher) && !RegCreateKeyExW(HKEY_CURRENT_USER, RUNKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &r, NULL)) { + set_str(r, L"DynaRunFix", cat3(p, L"\"", g_launcher, L"\" /autostart")); + RegCloseKey(r); + } +} + +// The fix is installed and is this version: the uninstall entry names our version and the launcher and the dll are in the install folder. (A dev build +// shows its commit as the version, so a rebuilt one is "not current" until it has been installed once.) +BOOL fix_current(void) +{ + static WCHAR v[64], d[MAX_PATH]; + if (!reg_str(HKEY_LOCAL_MACHINE, UNINSTKEY, L"DisplayVersion", v, 64, 0) || lstrcmpW(v, WIDEN(DRF_DISPLAY))) return FALSE; + return exists(g_launcher) && exists(cat3(d, g_dir, L"\\dynafix.dll", NULL)); +} + +// The wizard's one elevated stage may do the driver's preparation too (zip != NULL; see driver_prep_stage): the fix and the driver then share ONE UAC prompt. +// The result: the fix's failure code, or, when the fix is installed (user stage done), RC_OK or the driver's code (DRV_E_*, DRV_W_BDE: >= DRV_E_FIRST). FIX_DONE (errors.h) tells which. + +static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) // the command line of the elevated stage +{ + if (!zip) return; + lstrcatW(args, L" /with-driver \""); lstrcatW(args, zip); lstrcatW(args, L"\""); + if (accept && accept[0]) { lstrcatW(args, L" /unverified "); lstrcatW(args, accept); } +} + +int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept) +{ + static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; + if (!user_sid(sid, 200)) return RC_FAIL; + if (running_early(o, L"install (before elevation)")) return RC_RUNNING; // before the UAC prompt; /quiet without /close ends here + CoInitialize(NULL); + if (is_admin()) { rc = machine_install(o, sid); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } + else { + wsprintfW(args, L"/machine %s \"%s\"", sid, g_exe); + add_driver_args(args, zip, accept); + if ((rc = run_elevated(args, NULL, o)) == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } + } + if (!FIX_DONE(rc)) return rc; + user_stage(o, res); + return rc; +} + +int install_fix(const install_opts *o, install_result *res) { return install_fix_driver(o, res, NULL, NULL); } + +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept) +{ + static WCHAR sid[200], args[2 * MAX_PATH + 400]; int rc; + if (!user_sid(sid, 200)) return RC_FAIL; + if (running_early(o, L"full install (before elevation)")) return RC_RUNNING; + CoInitialize(NULL); + pkg_prepare_documents(msi); // as this user, before the SYSTEM-side install (error 1305 otherwise) + if (is_admin()) { rc = machine_full(o, sid, msi); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } + else { + wsprintfW(args, L"/full %s \"%s\"", sid, msi); + add_driver_args(args, zip, accept); + rc = run_elevated(args, NULL, o); + } + if (rc == RC_OVER_SHOULDER) { // over-the-shoulder elevation: setup as this user, then the fix (and the driver) + rc = run_msiexec(msi); + if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; + if (rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED) return RC_MSI_FAILED; + if (!locate_dynarun()) return RC_NO_DYNARUN; + return install_fix_driver(o, res, zip, accept); + } + if (rc == RC_CANCEL) return RC_CANCEL; + if (!FIX_DONE(rc)) return rc; + locate_dynarun(); + user_stage(o, res); + return rc; +} + +int install_all(const install_opts *o, install_result *res, const WCHAR *msi) { return install_all_driver(o, res, msi, NULL, NULL); } + diff --git a/src/setup/util.h b/src/setup/util.h new file mode 100644 index 0000000..e5adf4c --- /dev/null +++ b/src/setup/util.h @@ -0,0 +1,240 @@ +// Part of setup.c (DLL loading and small helpers): included by it, in one compilation unit; never compiled on its own. +/* ---------- DLLs: only from the system folder ---------- */ + +// msi.dll and wininet.dll are delay-loaded (build.cmd: /DELAYLOAD) and loaded here by their full path in the system folder, never +// searched for (a planted msi.dll / wininet.dll next to the setup, e.g. in Downloads, is not loaded). The linker flag +// /DEPENDENTLOADFLAG:0x800 and SetDefaultDllDirectories (WinMainCRTStartup) do the same for every other DLL on systems that have them +// (Windows 7 with KB2533623 and later); on older systems the delay-load hook and the full paths below are what protects. +static HMODULE load_system_dll(const WCHAR *name) +{ + WCHAR p[MAX_PATH]; UINT n = GetSystemDirectoryW(p, MAX_PATH - 40); + if (!n || n >= MAX_PATH - 40 || lstrlenW(name) > 30) return NULL; + lstrcatW(p, L"\\"); lstrcatW(p, name); + return LoadLibraryW(p); +} + +static FARPROC WINAPI dll_hook(unsigned notify, PDelayLoadInfo info) +{ + WCHAR n[40]; + if (notify != dliNotePreLoadLibrary || !info || !info->szDll) return NULL; + if (MultiByteToWideChar(CP_ACP, 0, info->szDll, -1, n, 40) <= 0) return NULL; + return (FARPROC)load_system_dll(n); // NULL: the delay-load helper then raises its usual error +} +const PfnDliHook __pfnDliNotifyHook2 = dll_hook; + +/* ---------- small helpers (no C runtime) ---------- */ + +void zero(void *p, SIZE_T n) { volatile char *z = (volatile char *)p; while (n--) *z++ = 0; } +void install_opts_default(install_opts *o) { zero(o, sizeof(*o)); o->pin = TRUE; o->autostart = TRUE; } +void *alloc(SIZE_T n) { return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, n); } +void release(void *p) { if (p) HeapFree(GetProcessHeap(), 0, p); } +static WCHAR *cat3(WCHAR *d, const WCHAR *a, const WCHAR *b, const WCHAR *c) +{ lstrcpyW(d, a); lstrcatW(d, b); if (c) lstrcatW(d, c); return d; } +BOOL exists(const WCHAR *p) { return GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES; } + +BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name) +{ + if (lstrlenW(dir) + lstrlenW(name) + 2 >= limit) return FALSE; + if (out != dir) lstrcpyW(out, dir); + lstrcatW(out, L"\\"); lstrcatW(out, name); + return TRUE; +} + +int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code) +{ + STARTUPINFOW si; PROCESS_INFORMATION pi; int r = PROC_EXITED; + zero(&si, sizeof(si)); si.cb = sizeof(si); + if (hide) { si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } + if (!CreateProcessW(exe, cmd, NULL, NULL, FALSE, flags, NULL, cwd, &si, &pi)) return PROC_NOSTART; + if (WaitForSingleObject(pi.hProcess, ms) == WAIT_OBJECT_0 || ms == INFINITE) GetExitCodeProcess(pi.hProcess, code); + else { TerminateProcess(pi.hProcess, 1); r = PROC_TIMEOUT; } + CloseHandle(pi.hThread); CloseHandle(pi.hProcess); + return r; +} + +int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx) +{ + WIN32_FIND_DATAW fd; HANDLE f; int r = WALK_DONE; + if (lstrlenW(dir) + 3 > bufcch) return WALK_NOOPEN; + lstrcpyW(buf, dir); lstrcatW(buf, L"\\*"); + if ((f = FindFirstFileW(buf, &fd)) == INVALID_HANDLE_VALUE) return WALK_NOOPEN; + do { + if (fd.cFileName[0] == '.' || !path_join(buf, limit, dir, fd.cFileName)) continue; + if (!fn(buf, &fd, ctx)) { r = WALK_STOPPED; break; } + } while (FindNextFileW(f, &fd)); + FindClose(f); + return r; +} + +BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k) +{ return !RegOpenKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, sam, k); } +BOOL app_key_create(BOOL machine, HKEY *k) +{ return !RegCreateKeyExW(machine ? HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER, machine ? APPKEY : USERKEY, 0, NULL, 0, KEY_ALL_ACCESS, NULL, k, NULL); } +int msg(const WCHAR *text, UINT flags) { return MessageBoxW(g_hwnd, text, TITLE, flags | MB_SETFOREGROUND); } +static void info(const install_opts *o, const WCHAR *text) { if (!o->quiet) msg(text, MB_ICONINFORMATION); } +static void error2(const install_opts *o, const WCHAR *text, const WCHAR *what) +{ static WCHAR b[1024]; if (o->quiet) return; lstrcpynW(b, text, 600); if (what) { lstrcatW(b, L"\n\n"); lstrcatW(b, what); } msg(b, MB_ICONERROR); } + +BOOL is_admin(void) +{ + SID_IDENTIFIER_AUTHORITY nt = SECURITY_NT_AUTHORITY; PSID sid; BOOL r = FALSE; + if (AllocateAndInitializeSid(&nt, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &sid)) { + if (!CheckTokenMembership(NULL, sid, &r)) r = FALSE; + FreeSid(sid); + } + return r; +} + +BOOL user_sid(WCHAR *out, int cch) +{ + HANDLE tok; static BYTE buf[256]; DWORD n; WCHAR *s; BOOL r = FALSE; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &tok)) return FALSE; + if (GetTokenInformation(tok, TokenUser, buf, sizeof(buf), &n) && ConvertSidToStringSidW(((TOKEN_USER *)buf)->User.Sid, &s)) { + lstrcpynW(out, s, cch); LocalFree(s); r = TRUE; + } + CloseHandle(tok); + return r; +} + +static void *read_file(const WCHAR *p, DWORD *n) +{ + HANDLE h = CreateFileW(p, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); void *d; + if (h == INVALID_HANDLE_VALUE) return NULL; + *n = GetFileSize(h, NULL); + d = alloc(*n + 1); + if (d && !ReadFile(h, d, *n, n, NULL)) { release(d); d = NULL; } + CloseHandle(h); + return d; +} + +// In-use files (dynafix.dll inside a running DynaRun) cannot be overwritten or deleted, but can be +// renamed; the renamed file goes away at the next restart. +static BOOL move_aside(const WCHAR *p) +{ + static WCHAR old[MAX_PATH + 32]; + wsprintfW(old, L"%s.%lu.old", p, GetTickCount()); + if (!MoveFileExW(p, old, MOVEFILE_REPLACE_EXISTING)) return FALSE; + // "delete at restart" is administrators-only: in the elevated stage the renamed file is registered for that; without + // the right, delete it right away if nothing holds it (a stale .old is all that can be left) + if (!MoveFileExW(old, NULL, MOVEFILE_DELAY_UNTIL_REBOOT)) DeleteFileW(old); + return TRUE; +} + +static BOOL write_file(const WCHAR *p, const void *d, DWORD n) +{ + HANDLE h = CreateFileW(p, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD w; BOOL ok; + if (h == INVALID_HANDLE_VALUE && exists(p) && move_aside(p)) + h = CreateFileW(p, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (h == INVALID_HANDLE_VALUE) return FALSE; + ok = WriteFile(h, d, n, &w, NULL) && w == n; + CloseHandle(h); + return ok; +} + +// Appends one line to a log. A link is never followed: a reparse point (symbolic link, junction) or a file with several hard links +// is refused, so the elevated stage cannot be made to write somewhere else through a file planted in %TEMP%. +static void append_line(const WCHAR *p, const WCHAR *line) +{ + static char u[4096]; int n = WideCharToMultiByte(CP_UTF8, 0, line, -1, u, sizeof(u) - 2, NULL, NULL); DWORD w; + BY_HANDLE_FILE_INFORMATION bi; + HANDLE h = CreateFileW(p, FILE_APPEND_DATA | FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, NULL); + if (h == INVALID_HANDLE_VALUE) return; + if (n <= 0 || !GetFileInformationByHandle(h, &bi) || (bi.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) || + bi.nNumberOfLinks > 1) { CloseHandle(h); return; } + u[n - 1] = '\r'; u[n] = '\n'; + WriteFile(h, u, n + 1, &w, NULL); + CloseHandle(h); +} + +static BOOL same_bytes(const WCHAR *p, const void *d, DWORD n) // the file on disk is exactly d +{ + DWORD got, i; BYTE *f = read_file(p, &got); BOOL r = f && got == n; + for (i = 0; r && i < n; i++) r = f[i] == ((const BYTE *)d)[i]; + release(f); + return r; +} + +BOOL same_file(const WCHAR *a, const WCHAR *b) +{ + DWORD n; void *d = read_file(a, &n); BOOL r = d && same_bytes(b, d, n); + release(d); + return r; +} + +static BOOL extract(int id, const WCHAR *p) // written and read back: the installed file is the payload +{ + HRSRC r = FindResourceW(NULL, MAKEINTRESOURCEW(id), (LPCWSTR)RT_RCDATA); const void *d; DWORD n; + if (!r) return FALSE; + d = LockResource(LoadResource(NULL, r)); n = SizeofResource(NULL, r); + return write_file(p, d, n) && same_bytes(p, d, n); +} + +static BOOL name_in(const WCHAR *list, const WCHAR *n) +{ + if (list) for (; *list; list += lstrlenW(list) + 1) if (!lstrcmpiW(list, n)) return TRUE; + return FALSE; +} + +// Deletes dir and everything in it, except the top-level names in keep (a multi-string; dir then stays). A link (junction, +// symbolic link) is removed itself and never followed. What is in use is renamed aside and goes at the next restart. +void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth) +{ + WCHAR *p; WIN32_FIND_DATAW fd; HANDLE f; DWORD a = GetFileAttributesW(dir); BOOL kept = FALSE; + if (a == INVALID_FILE_ATTRIBUTES) return; + if (a & FILE_ATTRIBUTE_REPARSE_POINT) { if (!RemoveDirectoryW(dir)) MoveFileExW(dir, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); return; } + if (depth > 40 || !(p = alloc(2 * MAX_PATH * sizeof(WCHAR)))) return; + if (lstrlenW(dir) + 3 < 2 * MAX_PATH) { + cat3(p, dir, L"\\*", NULL); + f = FindFirstFileW(p, &fd); + if (f != INVALID_HANDLE_VALUE) { + do { + if (!lstrcmpW(fd.cFileName, L".") || !lstrcmpW(fd.cFileName, L"..")) continue; + if ((depth == 0 && name_in(keep, fd.cFileName)) || !path_join(p, 2 * MAX_PATH, dir, fd.cFileName)) { kept = TRUE; continue; } // kept, or a path too long for the buffer + if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) { if (!RemoveDirectoryW(p)) MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); } + else delete_tree(p, NULL, depth + 1); + } + else if (!DeleteFileW(p) && !move_aside(p)) MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); // locked: gone at the next restart + } while (FindNextFileW(f, &fd)); + FindClose(f); + } + } + if (!kept && !RemoveDirectoryW(dir)) MoveFileExW(dir, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); + release(p); +} + +static DWORD reg_str(HKEY root, const WCHAR *path, const WCHAR *name, WCHAR *out, DWORD cch, REGSAM extra) +{ + HKEY k; DWORD type, n = (cch - 1) * sizeof(WCHAR); LONG r; + out[0] = 0; + if (RegOpenKeyExW(root, path, 0, KEY_QUERY_VALUE | extra, &k)) return 0; + r = RegQueryValueExW(k, name, NULL, &type, (BYTE *)out, &n); + RegCloseKey(k); + if (r || (type != REG_SZ && type != REG_EXPAND_SZ)) { out[0] = 0; return 0; } + out[n / sizeof(WCHAR)] = 0; + return lstrlenW(out); +} + +static void set_str(HKEY k, const WCHAR *name, const WCHAR *v) +{ RegSetValueExW(k, name, 0, REG_SZ, (const BYTE *)v, (lstrlenW(v) + 1) * sizeof(WCHAR)); } +static void set_dword(HKEY k, const WCHAR *name, DWORD v) { RegSetValueExW(k, name, 0, REG_DWORD, (const BYTE *)&v, 4); } + +static BOOL contains_i(const WCHAR *s, const WCHAR *sub) +{ + int n = lstrlenW(sub); + for (; lstrlenW(s) >= n; s++) if (CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, s, n, sub, n) == CSTR_EQUAL) return TRUE; + return FALSE; +} + +// The real Windows version (GetVersion lies to programs without a matching manifest). +static void os_ver(DWORD *major, DWORD *minor, DWORD *build) +{ + typedef LONG (WINAPI *RtlGetVersion_t)(OSVERSIONINFOW *); + RtlGetVersion_t f = (RtlGetVersion_t)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"); OSVERSIONINFOW v; + zero(&v, sizeof(v)); v.dwOSVersionInfoSize = sizeof(v); + if (f) f(&v); + *major = v.dwMajorVersion; *minor = v.dwMinorVersion; *build = v.dwBuildNumber; +} + +DWORD os_build(void) { DWORD a, b, c; os_ver(&a, &b, &c); return c; } + From 732c20a502cf75a344cbeb75c9013f80c300dcbd Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:28:21 +0800 Subject: [PATCH 19/41] Zip: one central directory reader zip_open and zip_walk each had their own copy of the end-of-central-directory scan, the central directory read and the entry field decoding. They now share zipcd_load (EOCD scan + directory read) and zipent_fill (entry fields + local header); the per-reader rules stay where they were: zip_walk still refuses Zip64 markers and any damaged entry, zip_open still takes the first .msi and ignores the entry count. crc_init is one idempotent entry point (done flag set after the table is complete) called by zipcd_load, zip_extract and zip_password_plausible. Co-Authored-By: Claude Opus 5.5 --- src/package.c | 85 +++++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/src/package.c b/src/package.c index d84a5df..a1ad34d 100644 --- a/src/package.c +++ b/src/package.c @@ -758,70 +758,81 @@ static void zk_init(zkeys *z, const char *pw) static BOOL read_at(HANDLE f, DWORD off, void *b, DWORD n) { DWORD r; return SetFilePointer(f, off, NULL, FILE_BEGIN) != INVALID_SET_FILE_POINTER && ReadFile(f, b, n, &r, NULL) && r == n; } -int zip_open(const WCHAR *zip, zipent *e) +// The one place that finds the central directory: the end-of-central-directory record (searched backwards in the last 64 KB), then the directory itself +// read into z->cd (cdsize + 1 bytes). z->zip64: one of the EOCD fields holds the Zip64 marker. The readers below add their own rules on top of this. +typedef struct { BYTE *cd; DWORD cdsize, count, adj; BOOL zip64; } zipcd; + +static BOOL zipcd_load(HANDLE f, zipcd *z) { - HANDLE f = CreateFileW(zip, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - DWORD size, tail, i, cdoff, cdsize, adj, lho, nl, xl; BYTE *t = NULL, *cd = NULL, *p, lh[30]; int rc = PK_FORMAT; - if (f == INVALID_HANDLE_VALUE) return PK_IO; + DWORD size, tail, i, cdoff; BYTE *t = NULL; BOOL ok = FALSE; crc_init(); - zero(e, sizeof(*e)); lstrcpynW(e->path, zip, MAX_PATH); + zero(z, sizeof(*z)); size = GetFileSize(f, NULL); tail = size < 65557 ? size : 65557; if (size < 22 || !(t = alloc(tail)) || !read_at(f, size - tail, t, tail)) goto out; for (i = tail - 22 + 1; i-- > 0;) if (t[i] == 'P' && t[i + 1] == 'K' && t[i + 2] == 5 && t[i + 3] == 6) break; if (i == (DWORD)-1) goto out; - cdsize = U32(t + i + 12); cdoff = U32(t + i + 16); + z->count = U16(t + i + 10); z->cdsize = U32(t + i + 12); cdoff = U32(t + i + 16); + z->zip64 = z->count == 0xFFFF || z->cdsize == 0xFFFFFFFF || cdoff == 0xFFFFFFFF; // archives that start with the "PK00" spanning marker store offsets without those 4 bytes - adj = (cdoff + cdsize + (tail - i) == size) ? 0 : 4; - if (cdsize > size || !(cd = alloc(cdsize)) || !read_at(f, cdoff + adj, cd, cdsize)) goto out; - for (p = cd; p + 46 <= cd + cdsize && U32(p) == 0x02014b50; p += 46 + nl + U16(p + 30) + U16(p + 32)) { + z->adj = (cdoff + z->cdsize + (tail - i) == size) ? 0 : 4; + if (z->cdsize > size || !(z->cd = alloc(z->cdsize + 1)) || !read_at(f, cdoff + z->adj, z->cd, z->cdsize)) goto out; + ok = TRUE; +out: + release(t); + if (!ok) { release(z->cd); z->cd = NULL; } + return ok; +} + +// The fields of the central directory entry at p, and where its data starts (from the local header, which must be there). +static BOOL zipent_fill(HANDLE f, const BYTE *p, DWORD adj, zipent *e) +{ + BYTE lh[30]; DWORD lho = U32(p + 42) + adj; + e->flags = U16(p + 8); e->method = U16(p + 10); e->dostime = U16(p + 12); + e->crc = U32(p + 16); e->csize = U32(p + 20); e->usize = U32(p + 24); + if (!read_at(f, lho, lh, 30) || U32(lh) != 0x04034b50) return FALSE; + e->data = lho + 30 + U16(lh + 26) + U16(lh + 28); + return TRUE; +} + +int zip_open(const WCHAR *zip, zipent *e) +{ + HANDLE f = CreateFileW(zip, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); + DWORD nl; zipcd z; BYTE *p; int rc = PK_FORMAT; + if (f == INVALID_HANDLE_VALUE) return PK_IO; + zero(e, sizeof(*e)); lstrcpynW(e->path, zip, MAX_PATH); + if (!zipcd_load(f, &z)) goto out; + for (p = z.cd; p + 46 <= z.cd + z.cdsize && U32(p) == 0x02014b50; p += 46 + nl + U16(p + 30) + U16(p + 32)) { nl = U16(p + 28); - if (nl < 4 || p + 46 + nl > cd + cdsize) break; + if (nl < 4 || p + 46 + nl > z.cd + z.cdsize) break; if (CompareStringA(LOCALE_INVARIANT, NORM_IGNORECASE, (char *)p + 46 + nl - 4, 4, ".msi", 4) != CSTR_EQUAL) continue; - e->flags = U16(p + 8); e->method = U16(p + 10); e->dostime = U16(p + 12); - e->crc = U32(p + 16); e->csize = U32(p + 20); e->usize = U32(p + 24); lho = U32(p + 42) + adj; - if (!read_at(f, lho, lh, 30) || U32(lh) != 0x04034b50) break; - nl = U16(lh + 26); xl = U16(lh + 28); - e->data = lho + 30 + nl + xl; + if (!zipent_fill(f, p, z.adj, e)) break; rc = (e->method == 0 || e->method == 8) ? PK_OK : PK_FORMAT; break; } out: - release(t); release(cd); CloseHandle(f); + release(z.cd); CloseHandle(f); return rc; } int zip_walk(const WCHAR *zip, zip_visit_fn fn, void *ctx) { HANDLE f = CreateFileW(zip, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); - DWORD size, tail, i, cdoff, cdsize, adj, lho, nl, count, seen = 0; BYTE *t = NULL, *cd = NULL, *p, lh[30]; int rc = PK_FORMAT; zipent e; + DWORD nl, seen = 0; zipcd z; BYTE *p; int rc = PK_FORMAT; zipent e; if (f == INVALID_HANDLE_VALUE) return PK_IO; - crc_init(); - size = GetFileSize(f, NULL); - tail = size < 65557 ? size : 65557; - if (size < 22 || !(t = alloc(tail)) || !read_at(f, size - tail, t, tail)) goto out; - for (i = tail - 22 + 1; i-- > 0;) if (t[i] == 'P' && t[i + 1] == 'K' && t[i + 2] == 5 && t[i + 3] == 6) break; - if (i == (DWORD)-1) goto out; - count = U16(t + i + 10); cdsize = U32(t + i + 12); cdoff = U32(t + i + 16); - if (count == 0xFFFF || cdsize == 0xFFFFFFFF || cdoff == 0xFFFFFFFF) goto out; // Zip64 - adj = (cdoff + cdsize + (tail - i) == size) ? 0 : 4; - if (cdsize > size || !(cd = alloc(cdsize + 1)) || !read_at(f, cdoff + adj, cd, cdsize)) goto out; - for (p = cd; seen < count; p += 46 + nl + U16(p + 30) + U16(p + 32), seen++) { - if (p + 46 > cd + cdsize || U32(p) != 0x02014b50) goto out; + if (!zipcd_load(f, &z) || z.zip64) goto out; // Zip64 is not read + for (p = z.cd; seen < z.count; p += 46 + nl + U16(p + 30) + U16(p + 32), seen++) { + if (p + 46 > z.cd + z.cdsize || U32(p) != 0x02014b50) goto out; nl = U16(p + 28); - if (p + 46 + nl + U16(p + 30) + U16(p + 32) > cd + cdsize) goto out; + if (p + 46 + nl + U16(p + 30) + U16(p + 32) > z.cd + z.cdsize) goto out; zero(&e, sizeof(e)); lstrcpynW(e.path, zip, MAX_PATH); - e.flags = U16(p + 8); e.method = U16(p + 10); e.dostime = U16(p + 12); - e.crc = U32(p + 16); e.csize = U32(p + 20); e.usize = U32(p + 24); lho = U32(p + 42); - if (e.csize == 0xFFFFFFFF || e.usize == 0xFFFFFFFF || lho == 0xFFFFFFFF) goto out; - lho += adj; - if (!read_at(f, lho, lh, 30) || U32(lh) != 0x04034b50) goto out; - e.data = lho + 30 + U16(lh + 26) + U16(lh + 28); + if (U32(p + 20) == 0xFFFFFFFF || U32(p + 24) == 0xFFFFFFFF || U32(p + 42) == 0xFFFFFFFF) goto out; + if (!zipent_fill(f, p, z.adj, &e)) goto out; if (!fn((const char *)p + 46, nl, &e, ctx)) { rc = PK_CANCEL; goto out; } } rc = PK_OK; out: - release(t); release(cd); CloseHandle(f); + release(z.cd); CloseHandle(f); return rc; } From 60fb2c7376a202c6b616251d427be3b2dc6c1dc1 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:31:15 +0800 Subject: [PATCH 20/41] Driver: pass tool and package-log settings as arguments run_tool_ok takes the tolerated exit code and its meaning as arguments (run_tool is the plain form), replacing g_tool_ok_code / g_tool_ok_text. The zip and folder searches take a pkg_search (accept callback with a context, and the log function), replacing g_pkg_log. The first folder with all 18 files of which some differ is now the folder search's own result (known_ctx.other, replacing g_other_dir); that needed one callback context for both candidate kinds, so zip_known_cb and dir_known_cb became one known_cb. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 91 ++++++++++++++++++++++++--------------------- src/package.c | 67 ++++++++++++++++----------------- src/setup.h | 12 +++--- tests/test_driver.c | 21 +++++++---- 4 files changed, 100 insertions(+), 91 deletions(-) diff --git a/src/driver.c b/src/driver.c index 96b8db5..42af406 100644 --- a/src/driver.c +++ b/src/driver.c @@ -450,23 +450,6 @@ static BOOL zip_scan(const WCHAR *zip, BOOL *known, BOOL *sane, WCHAR *rep, int // For the search of the user's folders: a zip whose pinned files are all there and right, whatever the zip is called or how it was packed. static BOOL zip_has_all(const WCHAR *zip) { return pkg_zip_has(zip, DRV_FILES, DRV_NFILES, DRV_MAX_FILE) == DRV_NFILES; } // cheap: only the central directory is read -static BOOL zip_known_cb(const WCHAR *zip) -{ - static WCHAR l[2 * MAX_PATH + 300], hex[65], rep[1500]; WIN32_FILE_ATTRIBUTE_DATA fa; ULONGLONG sz = 0; BOOL known = FALSE, sane = FALSE, ok; - if (GetFileAttributesExW(zip, GetFileExInfoStandard, &fa)) sz = ((ULONGLONG)fa.nFileSizeHigh << 32) | fa.nFileSizeLow; - wsprintfW(l, L"driver zip candidate %s (%lu bytes): ", zip, (DWORD)sz); - if (!zip_has_all(zip)) { lstrcatW(l, L"not a driver package (not all of the 18 installed files are in it)"); user_log(l); return FALSE; } - ok = zip_scan(zip, &known, &sane, rep, 1500); - if (!ok) lstrcatW(l, L"the files could not be unpacked for the check"); - else if (known) { - lstrcatW(l, L"all 18 installed files match the pinned ones"); - if (pkg_sha256(zip, hex, &sz)) { lstrcatW(l, L"; zip SHA-256 "); lstrcatW(l, hex); lstrcatW(l, lstrcmpiW(hex, DRIVER_ZIP_SHA256) ? L" (not the official zip as a whole)" : L" (the official zip)"); } - } else lstrcatW(l, sane ? L"all 18 files are there but some differ from the pinned ones" : L"files differ or are missing"); - user_log(l); - if (ok && !known && rep[0]) user_log(rep); - return ok && known; -} - /* ---------- an already unpacked driver folder ---------- */ static BOOL write_utf16_file(const WCHAR *path, const WCHAR *text, int chars); @@ -485,6 +468,8 @@ static BOOL dir_layout_ok(const WCHAR *dir) return TRUE; } +static BOOL dir_layout_cb(const WCHAR *dir, void *ctx) { (void)ctx; return dir_layout_ok(dir); } + static BOOL bus_inf_loose_at(const WCHAR *dir) { WCHAR p[MAX_PATH]; BYTE *d; DWORD n = 0; BOOL ok = FALSE; @@ -509,18 +494,36 @@ static BOOL digest_of(const drv_check *c, const WCHAR *tmp, WCHAR *hex) return ok; } -// For the search: a folder whose pinned files are all there and right (counts like a zip with the right content; no zip hash exists, only a log line). -static WCHAR g_other_dir[MAX_PATH]; // the first folder met on the way with all 18 files of which some differ (see driver_fetch: no second walk) +// What the search for a package on this computer carries along: whether the candidate is a folder (else a zip), and, for folders, the first one met +// on the way with all 18 files of which some differ (kept so driver_fetch needs no second walk). +typedef struct { BOOL isdir; WCHAR other[MAX_PATH]; } known_ctx; -static BOOL dir_known_cb(const WCHAR *dir) +// For the search of the user's folders: a zip, or an unpacked folder, whose pinned files are all there and right, whatever the zip is called or how it +// was packed (a folder counts like a zip with the right content; no zip hash exists for it, only a log line). +static BOOL known_cb(const WCHAR *path, void *vctx) { - static WCHAR l[2 * MAX_PATH + 200]; static drv_check c; - if (!dir_layout_ok(dir)) return FALSE; - check_files_at(dir, L"\\", NULL, &c); - wsprintfW(l, L"driver folder candidate %s: %d of 18 installed files match the pinned ones, %d differ, %d missing", dir, c.ok, c.differ, c.missing); + known_ctx *k = (known_ctx *)vctx; + static WCHAR l[2 * MAX_PATH + 300], hex[65], rep[1500]; static drv_check c; WIN32_FILE_ATTRIBUTE_DATA fa; ULONGLONG sz = 0; BOOL known = FALSE, sane = FALSE, ok; + if (k->isdir) { + if (!dir_layout_ok(path)) return FALSE; + check_files_at(path, L"\\", NULL, &c); + wsprintfW(l, L"driver folder candidate %s: %d of 18 installed files match the pinned ones, %d differ, %d missing", path, c.ok, c.differ, c.missing); + user_log(l); + if (c.ok != DRV_NFILES && !k->other[0] && c.missing == 0 && bus_inf_loose_at(path)) lstrcpyW(k->other, path); + return c.ok == DRV_NFILES; + } + if (GetFileAttributesExW(path, GetFileExInfoStandard, &fa)) sz = ((ULONGLONG)fa.nFileSizeHigh << 32) | fa.nFileSizeLow; + wsprintfW(l, L"driver zip candidate %s (%lu bytes): ", path, (DWORD)sz); + if (!zip_has_all(path)) { lstrcatW(l, L"not a driver package (not all of the 18 installed files are in it)"); user_log(l); return FALSE; } + ok = zip_scan(path, &known, &sane, rep, 1500); + if (!ok) lstrcatW(l, L"the files could not be unpacked for the check"); + else if (known) { + lstrcatW(l, L"all 18 installed files match the pinned ones"); + if (pkg_sha256(path, hex, &sz)) { lstrcatW(l, L"; zip SHA-256 "); lstrcatW(l, hex); lstrcatW(l, lstrcmpiW(hex, DRIVER_ZIP_SHA256) ? L" (not the official zip as a whole)" : L" (the official zip)"); } + } else lstrcatW(l, sane ? L"all 18 files are there but some differ from the pinned ones" : L"files differ or are missing"); user_log(l); - if (c.ok != DRV_NFILES && !g_other_dir[0] && c.missing == 0 && bus_inf_loose_at(dir)) lstrcpyW(g_other_dir, dir); - return c.ok == DRV_NFILES; + if (ok && !known && rep[0]) user_log(rep); + return ok && known; } // For the search when nothing matched: a folder with all 18 files where some differ and the bus INF names the Dyna Pro device id ("install anyway" may be offered). @@ -533,12 +536,12 @@ static BOOL dir_other_cb(const WCHAR *dir) } // No zip with the right content: the newest zip called "Dyna Pro USB*" (our own cache first). It goes the way of a download that differs. -static BOOL driver_local_other(WCHAR *zip) +static BOOL driver_local_other(WCHAR *zip, const pkg_search *s) { WCHAR own[MAX_PATH]; pkg_temp_dir(own); lstrcatW(own, L"\\" DRIVER_ZIP_NAME); if (exists(own)) { lstrcpyW(zip, own); return TRUE; } - return pkg_find_zip_named(L"Dyna Pro USB", zip); + return pkg_find_zip_named(L"Dyna Pro USB", s, zip); } // Our own cache file is not the user's: a download that was refused (or that failed the checks) must not be offered again as a "local copy". @@ -574,7 +577,8 @@ int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, if (a == INVALID_FILE_ATTRIBUTES) return DRV_E_HASH; if (a & FILE_ATTRIBUTE_DIRECTORY) { *how |= DRV_SRC_DIR; - if (!pkg_find_dir_in(path, 2, dir_layout_ok, d)) { zip_note(path, L"no ftdibus.inf with i386 and amd64 folders in it"); return DRV_E_HASH; } + static const pkg_search layout = { dir_layout_cb, NULL, NULL }; + if (!pkg_find_dir_in(path, 2, &layout, d)) { zip_note(path, L"no ftdibus.inf with i386 and amd64 folders in it"); return DRV_E_HASH; } lstrcpyW(path, d); check_files_at(path, L"\\", NULL, &c); if (c.ok == DRV_NFILES) return PK_OK; @@ -604,22 +608,25 @@ int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int driver_fetch(WCHAR *zip, progress_fn cb, volatile LONG *cancel, DWORD *err, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch) { int rc, hw; BOOL skip_other = g_skip_other; // kept for the whole run, so resume and retry do not offer the declined copy again + static known_ctx kz, kd; // the zip search and the folder search (kd.other: the walk's second result) + pkg_search sz, sd; *how = DRV_SRC_DOWNLOADED; hex[0] = 0; *size = 0; if (rep) rep[0] = 0; - g_pkg_log = user_log; // the folder search says which folders it looks in - g_other_dir[0] = 0; + zero(&kz, sizeof(kz)); zero(&kd, sizeof(kd)); kd.isdir = TRUE; + sz.accept = known_cb; sz.ctx = &kz; sz.log = user_log; // the searches say which folders they look in + sd.accept = known_cb; sd.ctx = &kd; sd.log = user_log; user_log(L"driver_fetch: looking for a copy of the driver on this computer"); if (cb) cb((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts // 1. a copy whose driver files are the pinned ones (a zip of any name or packing, or an unpacked folder): nothing to download pkg_temp_dir(zip); lstrcatW(zip, L"\\" DRIVER_ZIP_NAME); - if (((exists(zip) && zip_known_cb(zip)) || pkg_find_zip_if(DRV_MAX_ZIP, zip_known_cb, zip)) || - pkg_find_dirs(dir_known_cb, zip)) { + if (((exists(zip) && known_cb(zip, &kz)) || pkg_find_zip_if(DRV_MAX_ZIP, &sz, zip)) || + pkg_find_dirs(&sd, zip)) { hw = driver_check(zip, how, hex, size, rep, repcch); if (hw == PK_OK) { zip_note(zip, L"found on this computer, its driver files match the pinned ones: nothing is downloaded"); return PK_OK; } } // 2. a zip called "Dyna Pro USB*", or a folder with all 18 files, whose files differ goes the way of a download that differs: sanity checks, then the // user is asked. One that is not a usable driver package is skipped (logged) and the official site is tried. if (skip_other) user_log(L"the user declined a local copy that differs: not offered again, the official site is used"); - if (!skip_other && (driver_local_other(zip) || (g_other_dir[0] && lstrcpyW(zip, g_other_dir)))) { // (the folders were walked once, above) + if (!skip_other && (driver_local_other(zip, &sz) || (kd.other[0] && lstrcpyW(zip, kd.other)))) { // (the folders were walked once, above) hw = driver_check(zip, how, hex, size, rep, repcch); if (hw == PK_OK || hw == DRV_W_HASH) return hw; zip_note(zip, L"differs from the pinned files and is not a usable driver package: skipped"); @@ -672,25 +679,25 @@ static void drv_log2(const WCHAR *what, const WCHAR *arg, DWORD v) } // "tool args", hidden, waited for; its exit code, or (DWORD)-1 when it could not be started or did not finish in 3 minutes. -static DWORD g_tool_ok_code; // an exit code that is not an error for the next run_tool (and what it means), see cancel_locked -static const WCHAR *g_tool_ok_text; - -static DWORD run_tool(const WCHAR *tool, const WCHAR *args) +// ok_text (NULL: none): what the exit code ok_code means when it is not an error (only the log line differs), see cancel_locked. +static DWORD run_tool_ok(const WCHAR *tool, const WCHAR *args, DWORD ok_code, const WCHAR *ok_text) { static WCHAR win[MAX_PATH], exe[MAX_PATH]; WCHAR *cmd; DWORD rc = (DWORD)-1; if (!GetWindowsDirectoryW(win, MAX_PATH - 40) || !drv_sys_tool(win, is_wow64(), tool, exe, MAX_PATH) || !exists(exe)) { drv_log2(tool, L"not found", rc); return rc; } if (!(cmd = alloc((lstrlenW(exe) + lstrlenW(args) + 8) * sizeof(WCHAR)))) return rc; lstrcpyW(cmd, L"\""); lstrcatW(cmd, exe); lstrcatW(cmd, L"\" "); lstrcatW(cmd, args); run_process(exe, cmd, win, CREATE_NO_WINDOW, TRUE, 180000, &rc); // (not started, or no end in 3 minutes: rc stays -1) - if (g_tool_ok_text && rc == g_tool_ok_code) { + if (ok_text && rc == ok_code) { static WCHAR l[2 * MAX_PATH + 240]; - lstrcpyW(l, tool); lstrcatW(l, L" "); lstrcatW(l, args); lstrcatW(l, L" -> "); lstrcatW(l, g_tool_ok_text); + lstrcpyW(l, tool); lstrcatW(l, L" "); lstrcatW(l, args); lstrcatW(l, L" -> "); lstrcatW(l, ok_text); drv_log(l); } else drv_log2(tool, args, rc); release(cmd); return rc; } +static DWORD run_tool(const WCHAR *tool, const WCHAR *args) { return run_tool_ok(tool, args, 0, NULL); } + // Recovery passwords: 8 groups of 6 digits separated by '-'. Found by that pattern (any run of at least two such groups), not by a label, so it // works in every language; the digits become '*', the '-' stay. Key protector IDs (GUIDs) are not touched. static BOOL is_digit_byte(BYTE c) { return c >= '0' && c <= '9'; } @@ -1094,9 +1101,7 @@ static void bde_resume(void) // BitLocker protection on again static void cancel_locked(void) { run_tool(L"schtasks.exe", L"/delete /tn " TASK_NAME L" /f"); - g_tool_ok_code = 1; g_tool_ok_text = L"not present (1: the start-up menu had already used the one-time option up): nothing to remove"; - run_tool(L"bcdedit.exe", L"/deletevalue {current} onetimeadvancedoptions"); - g_tool_ok_text = NULL; + run_tool_ok(L"bcdedit.exe", L"/deletevalue {current} onetimeadvancedoptions", 1, L"not present (1: the start-up menu had already used the one-time option up): nothing to remove"); { WCHAR b[MAX_PATH], f[MAX_PATH]; base_dir(b); if (accept_path(f, b)) DeleteFileW(f); } // an "install anyway" acceptance ends with the driver step bde_resume(); } diff --git a/src/package.c b/src/package.c index a1ad34d..ba1a0c9 100644 --- a/src/package.c +++ b/src/package.c @@ -114,28 +114,25 @@ static void search_dirs(WCHAR dirs[4][MAX_PATH]) SHGetSpecialFolderPathW(NULL, dirs[3], CSIDL_PERSONAL, FALSE); } -typedef BOOL (*zip_accept_fn)(const WCHAR *path); - -// Where the search says what it looks at (the caller sets it; NULL = silent): one line per folder, and per zip that is skipped for its size. -void (*g_pkg_log)(const WCHAR *line); -static void plog(const WCHAR *a, const WCHAR *b) +// Where the search says what it looks at (s->log, NULL = silent): one line per folder, and per zip that is skipped for its size. +static void plog(const pkg_search *s, const WCHAR *a, const WCHAR *b) { static WCHAR l[2 * MAX_PATH + 80]; - if (!g_pkg_log || lstrlenW(a) + lstrlenW(b) + 1 >= (int)(sizeof(l) / sizeof(l[0]))) return; + if (!s || !s->log || lstrlenW(a) + lstrlenW(b) + 1 >= (int)(sizeof(l) / sizeof(l[0]))) return; lstrcpyW(l, a); lstrcatW(l, b); - g_pkg_log(l); + s->log(l); } // Two modes. accept given: the first zip (at most maxsize bytes) that accept() approves - by its content, whatever it is called. prefix given // instead: the newest zip whose name starts with prefix, whatever it contains (*bt: its time, in/out). -static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, zip_accept_fn accept, const WCHAR *prefix, FILETIME *bt, WCHAR *out); -typedef struct { BOOL subdirs; DWORD maxsize; zip_accept_fn accept; const WCHAR *prefix; FILETIME *bt; WCHAR *out; BOOL found; int plen; } zipfind_ctx; +static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, const pkg_search *s, const WCHAR *prefix, FILETIME *bt, WCHAR *out); +typedef struct { BOOL subdirs; DWORD maxsize; const pkg_search *s; const WCHAR *prefix; FILETIME *bt; WCHAR *out; BOOL found; int plen; } zipfind_ctx; static void zip_one(zipfind_ctx *c, const WCHAR *p, const WIN32_FIND_DATAW *fd) { int n; if (fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if (c->subdirs && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && find_zip_in(p, FALSE, c->maxsize, c->accept, c->prefix, c->bt, c->out)) c->found = TRUE; + if (c->subdirs && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && find_zip_in(p, FALSE, c->maxsize, c->s, c->prefix, c->bt, c->out)) c->found = TRUE; return; } if (fd->dwFileAttributes & (FILE_ATTRIBUTE_OFFLINE | FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS | FILE_ATTRIBUTE_RECALL_ON_OPEN)) return; // cloud-only: do not download it just to look @@ -147,8 +144,8 @@ static void zip_one(zipfind_ctx *c, const WCHAR *p, const WIN32_FIND_DATAW *fd) lstrcpyW(c->out, p); *c->bt = fd->ftLastWriteTime; c->found = TRUE; return; } - if (fd->nFileSizeHigh || fd->nFileSizeLow > c->maxsize) { plog(L" skipped (too big for a driver package): ", p); return; } // the driver package is a few MB - if (c->accept(p)) { lstrcpyW(c->out, p); c->found = TRUE; } + if (fd->nFileSizeHigh || fd->nFileSizeLow > c->maxsize) { plog(c->s, L" skipped (too big for a driver package): ", p); return; } // the driver package is a few MB + if (c->s->accept(p, c->s->ctx)) { lstrcpyW(c->out, p); c->found = TRUE; } } // (the walk goes on after every entry, except once a zip is found when not looking for the newest by name) @@ -159,80 +156,80 @@ static BOOL zip_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) return c->prefix || !c->found; } -static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, zip_accept_fn accept, const WCHAR *prefix, FILETIME *bt, WCHAR *out) +static BOOL find_zip_in(const WCHAR *dir, BOOL subdirs, DWORD maxsize, const pkg_search *s, const WCHAR *prefix, FILETIME *bt, WCHAR *out) { WCHAR p[MAX_PATH * 2]; zipfind_ctx c; - if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH) { plog(L"zip search: a folder with no usable path", L""); return FALSE; } - plog(prefix ? L"zip search by name in " : L"zip search by content in ", dir); - c.subdirs = subdirs; c.maxsize = maxsize; c.accept = accept; c.prefix = prefix; c.bt = bt; c.out = out; c.found = FALSE; c.plen = prefix ? lstrlenW(prefix) : 0; - if (dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, zip_entry, &c) == WALK_NOOPEN) { plog(L" the folder cannot be read or does not exist", L""); return FALSE; } + if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH) { plog(s, L"zip search: a folder with no usable path", L""); return FALSE; } + plog(s, prefix ? L"zip search by name in " : L"zip search by content in ", dir); + c.subdirs = subdirs; c.maxsize = maxsize; c.s = s; c.prefix = prefix; c.bt = bt; c.out = out; c.found = FALSE; c.plen = prefix ? lstrlenW(prefix) : 0; + if (dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, zip_entry, &c) == WALK_NOOPEN) { plog(s, L" the folder cannot be read or does not exist", L""); return FALSE; } return c.found; } -// The first zip (at most maxsize bytes) in the folders pkg_find_local looks in (and one level into Downloads) that accept() approves. -BOOL pkg_find_zip_if(DWORD maxsize, BOOL (*accept)(const WCHAR *path), WCHAR *out) +// The first zip (at most maxsize bytes) in the folders pkg_find_local looks in (and one level into Downloads) that s->accept() approves. +BOOL pkg_find_zip_if(DWORD maxsize, const pkg_search *s, WCHAR *out) { static WCHAR dirs[4][MAX_PATH]; int i; out[0] = 0; search_dirs(dirs); - for (i = 0; i < 4; i++) if (find_zip_in(dirs[i], i == 1, maxsize, accept, NULL, NULL, out)) return TRUE; + for (i = 0; i < 4; i++) if (find_zip_in(dirs[i], i == 1, maxsize, s, NULL, NULL, out)) return TRUE; return FALSE; } // The newest zip whose name starts with prefix in those folders (the candidate when no zip has the right content). -BOOL pkg_find_zip_named(const WCHAR *prefix, WCHAR *out) +BOOL pkg_find_zip_named(const WCHAR *prefix, const pkg_search *s, WCHAR *out) { static WCHAR dirs[4][MAX_PATH]; FILETIME bt; int i; BOOL found = FALSE; out[0] = 0; bt.dwLowDateTime = bt.dwHighDateTime = 0; search_dirs(dirs); - for (i = 0; i < 4; i++) if (find_zip_in(dirs[i], i == 1, 0, NULL, prefix, &bt, out)) found = TRUE; + for (i = 0; i < 4; i++) if (find_zip_in(dirs[i], i == 1, 0, s, prefix, &bt, out)) found = TRUE; return found; } -// Depth-limited walk: accept(dir) for dir itself, then for its sub-folders down to `depth` levels (never into a link). *budget: the most folders +// Depth-limited walk: s->accept(dir) for dir itself, then for its sub-folders down to `depth` levels (never into a link). *budget: the most folders // still to be looked at in all (so a big Documents folder cannot make it slow). -static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, BOOL (*accept)(const WCHAR *dir), WCHAR *out); -typedef struct { int depth, *budget; BOOL (*accept)(const WCHAR *dir); WCHAR *out; BOOL found; } dirfind_ctx; +static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, const pkg_search *s, WCHAR *out); +typedef struct { int depth, *budget; const pkg_search *s; WCHAR *out; BOOL found; } dirfind_ctx; static BOOL dir_entry(const WCHAR *p, const WIN32_FIND_DATAW *fd, void *vc) { dirfind_ctx *c = vc; if ((fd->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) && !(fd->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) - c->found = find_dir_in(p, c->depth - 1, c->budget, c->accept, c->out); + c->found = find_dir_in(p, c->depth - 1, c->budget, c->s, c->out); return !c->found && *c->budget >= 0; } -static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, BOOL (*accept)(const WCHAR *dir), WCHAR *out) +static BOOL find_dir_in(const WCHAR *dir, int depth, int *budget, const pkg_search *s, WCHAR *out) { WCHAR p[MAX_PATH * 2]; dirfind_ctx c; if (!dir[0] || lstrlenW(dir) + 4 >= MAX_PATH || --*budget < 0) return FALSE; - if (accept(dir)) { lstrcpyW(out, dir); return TRUE; } + if (s->accept(dir, s->ctx)) { lstrcpyW(out, dir); return TRUE; } if (depth <= 0) return FALSE; - c.depth = depth; c.budget = budget; c.accept = accept; c.out = out; c.found = FALSE; + c.depth = depth; c.budget = budget; c.s = s; c.out = out; c.found = FALSE; dir_walk(dir, p, MAX_PATH * 2, MAX_PATH, dir_entry, &c); return c.found; } -// The first folder that accept() approves, in the folders pkg_find_local looks in: the setup's folder, Desktop and Documents one level down, +// The first folder that s->accept() approves, in the folders pkg_find_local looks in: the setup's folder, Desktop and Documents one level down, // Downloads two levels down (Downloads\Dyna Pro USB\64-Bit Drivers), at most 600 folders in all. -BOOL pkg_find_dirs(BOOL (*accept)(const WCHAR *dir), WCHAR *out) +BOOL pkg_find_dirs(const pkg_search *s, WCHAR *out) { static WCHAR dirs[4][MAX_PATH]; int i, budget = 600; out[0] = 0; search_dirs(dirs); for (i = 0; i < 4; i++) { - plog(L"folder search in ", dirs[i]); - if (find_dir_in(dirs[i], i == 1 ? 2 : 1, &budget, accept, out)) return TRUE; + plog(s, L"folder search in ", dirs[i]); + if (find_dir_in(dirs[i], i == 1 ? 2 : 1, &budget, s, out)) return TRUE; } return FALSE; } // The same walk below one given folder (the folder the user picked). -BOOL pkg_find_dir_in(const WCHAR *root, int depth, BOOL (*accept)(const WCHAR *dir), WCHAR *out) +BOOL pkg_find_dir_in(const WCHAR *root, int depth, const pkg_search *s, WCHAR *out) { int budget = 600; out[0] = 0; - return find_dir_in(root, depth, &budget, accept, out); + return find_dir_in(root, depth, &budget, s, out); } BOOL pkg_find_local(WCHAR *out) diff --git a/src/setup.h b/src/setup.h index 78cbf42..3e7168d 100644 --- a/src/setup.h +++ b/src/setup.h @@ -114,11 +114,13 @@ typedef struct { } zipent; BOOL pkg_find_local(WCHAR *out); // a DynaRun MSI, or a zip with an .msi inside, in Downloads/Desktop/Documents or next to us -BOOL pkg_find_zip_if(DWORD maxsize, BOOL (*accept)(const WCHAR *path), WCHAR *out); // the first zip (at most maxsize bytes) in the same places that accept() approves -extern void (*g_pkg_log)(const WCHAR *line); // set to log the folders the zip searches look in (NULL: silent) -BOOL pkg_find_dirs(BOOL (*accept)(const WCHAR *dir), WCHAR *out); // the first folder accept() approves, in the same places (two levels down in Downloads) -BOOL pkg_find_dir_in(const WCHAR *root, int depth, BOOL (*accept)(const WCHAR *dir), WCHAR *out); // accept() for root and the folders up to `depth` levels below it -BOOL pkg_find_zip_named(const WCHAR *prefix, WCHAR *out); // the newest zip whose name starts with prefix in the same places +// What a search for a driver package looks at and tells: accept(path, ctx) approves a zip / folder (the walk stops at the first one), log (NULL: silent) +// gets one line per folder looked at. pkg_find_zip_named uses only log (s may be NULL there). +typedef struct { BOOL (*accept)(const WCHAR *path, void *ctx); void *ctx; void (*log)(const WCHAR *line); } pkg_search; +BOOL pkg_find_zip_if(DWORD maxsize, const pkg_search *s, WCHAR *out); // the first zip (at most maxsize bytes) in the same places that s->accept() approves +BOOL pkg_find_dirs(const pkg_search *s, WCHAR *out); // the first folder s->accept() approves, in the same places (two levels down in Downloads) +BOOL pkg_find_dir_in(const WCHAR *root, int depth, const pkg_search *s, WCHAR *out); // s->accept() for root and the folders up to `depth` levels below it +BOOL pkg_find_zip_named(const WCHAR *prefix, const pkg_search *s, WCHAR *out); // the newest zip whose name starts with prefix in the same places void pkg_reject(const WCHAR *path); // not the DynaRun setup after all: pkg_find_local skips it int pkg_download(WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); BOOL pkg_url_ok(const WCHAR *u, const WCHAR *hosts); // https and on one of the ';'-separated hosts (NULL = any) diff --git a/tests/test_driver.c b/tests/test_driver.c index 72f7dc9..de6ab81 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -13,6 +13,10 @@ #include "zip_extract_all.h" #include +// the search callback of driver_fetch, called on one zip / one folder (no walk, nothing recorded) +static BOOL zip_known_cb(const WCHAR *zip) { known_ctx k; zero(&k, sizeof(k)); return known_cb(zip, &k); } +static BOOL dir_known_cb(const WCHAR *dir) { known_ctx k; zero(&k, sizeof(k)); k.isdir = TRUE; return known_cb(dir, &k); } + // what setup.c calls elsewhere and the tests do not need int wizard(const install_opts *init) { (void)init; return 0; } int update_check(void) { return 0; } @@ -660,7 +664,8 @@ static void test_files(void) /* ---------- the folder walk, the layout test and driver_check (synthetic packages only) ---------- */ -static BOOL has_marker(const WCHAR *dir) { WCHAR p[MAX_PATH]; wsprintfW(p, L"%s\\marker.txt", dir); return exists(p); } +static BOOL has_marker(const WCHAR *dir, void *ctx) { WCHAR p[MAX_PATH]; (void)ctx; wsprintfW(p, L"%s\\marker.txt", dir); return exists(p); } +static const pkg_search s_marker = { has_marker, NULL, NULL }; static void touch_file(const WCHAR *path) { @@ -678,19 +683,19 @@ static void test_check(void) wsprintfW(b, L"%s\\b", a); CreateDirectoryW(b, NULL); wsprintfW(c, L"%s\\c", b); CreateDirectoryW(c, NULL); wsprintfW(p, L"%s\\marker.txt", c); touch_file(p); - CHECK(!pkg_find_dir_in(root, 0, has_marker, o) && !o[0], L"walk: depth 0 looks at the root only"); - CHECK(!pkg_find_dir_in(root, 2, has_marker, o) && !o[0], L"walk: a folder three levels down is out of reach for depth 2"); - CHECK(pkg_find_dir_in(root, 3, has_marker, o) && !lstrcmpiW(o, c), L"walk: depth 3 finds it and returns its path"); + CHECK(!pkg_find_dir_in(root, 0, &s_marker, o) && !o[0], L"walk: depth 0 looks at the root only"); + CHECK(!pkg_find_dir_in(root, 2, &s_marker, o) && !o[0], L"walk: a folder three levels down is out of reach for depth 2"); + CHECK(pkg_find_dir_in(root, 3, &s_marker, o) && !lstrcmpiW(o, c), L"walk: depth 3 finds it and returns its path"); wsprintfW(p, L"%s\\marker.txt", b); touch_file(p); - CHECK(pkg_find_dir_in(root, 3, has_marker, o) && !lstrcmpiW(o, b), L"walk: the nearest approved folder wins"); + CHECK(pkg_find_dir_in(root, 3, &s_marker, o) && !lstrcmpiW(o, b), L"walk: the nearest approved folder wins"); wsprintfW(p, L"%s\\marker.txt", root); touch_file(p); - CHECK(pkg_find_dir_in(root, 0, has_marker, o) && !lstrcmpiW(o, root), L"walk: the root itself can be approved"); + CHECK(pkg_find_dir_in(root, 0, &s_marker, o) && !lstrcmpiW(o, root), L"walk: the root itself can be approved"); wsprintfW(p, L"%s\\nothing", root); - CHECK(!pkg_find_dir_in(p, 2, has_marker, o) && !o[0], L"walk: a folder that does not exist finds nothing"); + CHECK(!pkg_find_dir_in(p, 2, &s_marker, o) && !o[0], L"walk: a folder that does not exist finds nothing"); { // a name that leaves no room for the walk is refused, not overrun WCHAR longp[MAX_PATH + 40]; int i; lstrcpyW(longp, root); for (i = lstrlenW(longp); i < MAX_PATH; i++) longp[i] = 'x'; longp[MAX_PATH] = 0; - CHECK(!pkg_find_dir_in(longp, 2, has_marker, o), L"walk: a path that is too long is refused"); + CHECK(!pkg_find_dir_in(longp, 2, &s_marker, o), L"walk: a path that is too long is refused"); } // driver_layout_ok: ftdibus.inf (a file) with i386 and amd64 folders beside it From 4f82272b526fea042931f727aba4fce7bd0b692b Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:32:22 +0800 Subject: [PATCH 21/41] Driver: prep_locked with one exit Every return after the argument checks is now 'set res, goto out'; the exit does the cleanup the former return sites did inline (remove base\driver, cancel_locked, bde_resume), so the leftovers are exactly the same as before. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/driver.c b/src/driver.c index 42af406..7d07fd0 100644 --- a/src/driver.c +++ b/src/driver.c @@ -1109,40 +1109,41 @@ static void cancel_locked(void) static int prep_locked(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { static WCHAR base[MAX_PATH], p[MAX_PATH + 40], z[MAX_PATH + 40], args[2 * MAX_PATH + 80], hex[65], xml[3000], dr[4]; - ULONGLONG size; volatile LONG nocancel = 0; int n, files, ps; BOOL bde_warn = FALSE, suspend, partly; bde_info bi; + ULONGLONG size; volatile LONG nocancel = 0; int n, files, ps, res = DRV_SUCCESS; BOOL bde_warn = FALSE, suspend, partly; bde_info bi; BOOL stage = lstrcmpW(zip, L"-") != 0, inside, fromdir = FALSE; - if (!valid_user_sid(sid)) return DRV_E_SID; + BOOL drop_staged = FALSE, undo = FALSE, resume = FALSE; // what the one exit has to do: remove base\driver, cancel_locked, bde_resume + if (!valid_user_sid(sid)) { res = DRV_E_SID; goto out; } if (stage) { DWORD fa = GetFileAttributesW(zip); fromdir = fa != INVALID_FILE_ATTRIBUTES && (fa & FILE_ATTRIBUTE_DIRECTORY) && !(fa & FILE_ATTRIBUTE_REPARSE_POINT); } base_dir(base); - if (lstrlenW(base) + 40 >= MAX_PATH) return DRV_E_DIR; + if (lstrlenW(base) + 40 >= MAX_PATH) { res = DRV_E_DIR; goto out; } lstrcpyW(p, base); lstrcatW(p, L"\\" SETUP_NAME); inside = !lstrcmpiW(g_self, p); if (stage) { static drv_check chk; - if (!make_base(base, inside, sid)) return DRV_E_DIR; + if (!make_base(base, inside, sid)) { res = DRV_E_DIR; goto out; } log_in_base(base, sid); drv_log(L"prep: staging"); lstrcpyW(args, base); lstrcatW(args, L"\\driver"); if (fromdir) { // an already unpacked folder: the pinned files are copied from it drv_log(L"prep: staging from an unpacked folder:"); drv_log(zip); - if (!CreateDirectoryW(args, NULL)) return DRV_E_EXTRACT; // make_base left the folder empty: this one is new + if (!CreateDirectoryW(args, NULL)) { res = DRV_E_EXTRACT; goto out; } // make_base left the folder empty: this one is new n = stage_from_dir(zip, args, &files); } else { lstrcpyW(z, base); lstrcatW(z, L"\\driver.zip"); - if (!CopyFileW(zip, z, TRUE)) return DRV_E_COPY; // our own copy, in the administrators-only folder: what is read is this copy - if (!pkg_sha256(z, hex, &size)) { DeleteFileW(z); return DRV_E_HASH; } + if (!CopyFileW(zip, z, TRUE)) { res = DRV_E_COPY; goto out; } // our own copy, in the administrators-only folder: what is read is this copy + if (!pkg_sha256(z, hex, &size)) { DeleteFileW(z); res = DRV_E_HASH; goto out; } drv_log2(L"prep: sha256 of the zip", hex, 0); - if (!CreateDirectoryW(args, NULL)) { DeleteFileW(z); return DRV_E_EXTRACT; } // make_base left the folder empty: this one is new + if (!CreateDirectoryW(args, NULL)) { DeleteFileW(z); res = DRV_E_EXTRACT; goto out; } // make_base left the folder empty: this one is new n = stage_files(z, args, &nocancel, &files); // only the pinned files, into folders made here; files: how many are missing DeleteFileW(z); } drv_log2(L"prep: pinned files missing in the source", NULL, files); - if (n != PK_OK || files) { delete_tree(args, NULL, 0); return DRV_E_EXTRACT; } + if (n != PK_OK || files) { drop_staged = TRUE; res = DRV_E_EXTRACT; goto out; } // what is hashed now are the staged copies, and those are what pnputil installs: there is no second extraction between the check and the install check_files(args, NULL, &chk); if (fromdir) { // no zip hash: the digest of the staged files' hashes is what "install anyway" is bound to lstrcpyW(z, base); lstrcatW(z, L"\\digest.tmp"); - if (!digest_of(&chk, z, hex)) { delete_tree(args, NULL, 0); return DRV_E_HASH; } + if (!digest_of(&chk, z, hex)) { drop_staged = TRUE; res = DRV_E_HASH; goto out; } } drv_log2(L"prep: staged files that are the pinned ones", NULL, chk.ok); if (chk.ok == DRV_NFILES) { @@ -1154,16 +1155,15 @@ static int prep_locked(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) WCHAR rep[1500]; drv_log(L"prep: the staged files are not the pinned ones and were not accepted:"); check_report(&chk, rep, 1500); drv_log(rep); - delete_tree(args, NULL, 0); - return DRV_E_HASH; + drop_staged = TRUE; res = DRV_E_HASH; goto out; } - if (!staged_ok(base)) return DRV_E_INF; + if (!staged_ok(base)) { res = DRV_E_INF; goto out; } } else { // a retry: the staged driver must still be ours - if (!exists(base) || path_user_writable(base, sid)) return DRV_E_DIR; + if (!exists(base) || path_user_writable(base, sid)) { res = DRV_E_DIR; goto out; } log_in_base(base, sid); - if (!staged_ok(base)) return DRV_E_INF; + if (!staged_ok(base)) { res = DRV_E_INF; goto out; } } - if (!inside && ((!CopyFileW(g_self, p, FALSE)) || !same_file(g_self, p))) return DRV_E_COPY; // the task starts this copy + if (!inside && ((!CopyFileW(g_self, p, FALSE)) || !same_file(g_self, p))) { res = DRV_E_COPY; goto out; } // the task starts this copy // BitLocker: the restart menu asks for the recovery key on an encrypted PC; suspend protection until the driver is installed (resumed by install/cancel). // -RebootCount 3 is the safety net: protection returns by itself after 3 restarts if the flow is abandoned; a retry suspends again and starts the count over. // "Not encrypted" is not an error. @@ -1206,16 +1206,21 @@ static int prep_locked(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) // Everything else is armed; the wizard lets the user decide (DRV_W_BDE). "Not readable / no BitLocker" with a failing manage-bde is no warning. if ((ps == BDE_PS_ON || ps == BDE_PS_UNKNOWN) && (rc != 0 || after == BDE_PS_ON || after == BDE_PS_UNKNOWN)) bde_warn = TRUE; } - if (run_tool(L"bcdedit.exe", L"/set {current} onetimeadvancedoptions on") != 0) { bde_resume(); return DRV_E_BCD; } + if (run_tool(L"bcdedit.exe", L"/set {current} onetimeadvancedoptions on") != 0) { resume = TRUE; res = DRV_E_BCD; goto out; } n = drv_task_xml(xml, 3000, sid, p); lstrcpyW(z, base); lstrcatW(z, L"\\task.xml"); - if (n < 0 || !write_utf16_file(z, xml, n)) { cancel_locked(); return DRV_E_TASK; } + if (n < 0 || !write_utf16_file(z, xml, n)) { undo = TRUE; res = DRV_E_TASK; goto out; } lstrcpyW(args, L"/create /tn " TASK_NAME L" /xml \""); lstrcatW(args, z); lstrcatW(args, L"\" /f"); n = (int)run_tool(L"schtasks.exe", args); DeleteFileW(z); - if (n != 0) { cancel_locked(); return DRV_E_TASK; } + if (n != 0) { undo = TRUE; res = DRV_E_TASK; goto out; } drv_log(bde_warn ? L"prep: done, but BitLocker could not be suspended" : L"prep: done"); - return bde_warn ? DRV_W_BDE : DRV_SUCCESS; + res = bde_warn ? DRV_W_BDE : DRV_SUCCESS; +out: + if (drop_staged) { lstrcpyW(args, base); lstrcatW(args, L"\\driver"); delete_tree(args, NULL, 0); } + if (undo) cancel_locked(); + if (resume) bde_resume(); + return res; } static int install_locked(void) From 948ee1f7e6a3f62e2868a8c38d24682af2a2ee5e Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:34:23 +0800 Subject: [PATCH 22/41] Driver: remove the staged files and accepted.txt when the preparation fails prep_locked, first attempt (a zip or folder is given, the staging folder was just made by make_base), now removes \DynaRunFix\driver\ and accepted.txt on these failures, which used to leave them behind: - staged_ok fails after staging (DRV_E_INF) - copying the setup exe into the folder fails (DRV_E_COPY) - bcdedit fails (DRV_E_BCD; accepted.txt was not removed there either) - the task XML cannot be written or schtasks fails (DRV_E_TASK; cancel_locked already removed accepted.txt, the staged tree stayed) Nothing is armed at those points and every later attempt stages again (make_base wipes the folder), so nothing can use the leftovers; the other staging failures (extract, hash, not accepted) already dropped the tree. Unchanged on purpose: a retry (zip "-", the staged files from the first attempt, task armed) keeps its staged files on DRV_E_INF and DRV_E_COPY; success and DRV_W_BDE keep everything. Co-Authored-By: Claude Opus 5.5 --- src/driver.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/driver.c b/src/driver.c index 7d07fd0..6551fbe 100644 --- a/src/driver.c +++ b/src/driver.c @@ -1157,13 +1157,13 @@ static int prep_locked(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) check_report(&chk, rep, 1500); drv_log(rep); drop_staged = TRUE; res = DRV_E_HASH; goto out; } - if (!staged_ok(base)) { res = DRV_E_INF; goto out; } + if (!staged_ok(base)) { drop_staged = TRUE; res = DRV_E_INF; goto out; } } else { // a retry: the staged driver must still be ours if (!exists(base) || path_user_writable(base, sid)) { res = DRV_E_DIR; goto out; } log_in_base(base, sid); if (!staged_ok(base)) { res = DRV_E_INF; goto out; } } - if (!inside && ((!CopyFileW(g_self, p, FALSE)) || !same_file(g_self, p))) { res = DRV_E_COPY; goto out; } // the task starts this copy + if (!inside && ((!CopyFileW(g_self, p, FALSE)) || !same_file(g_self, p))) { drop_staged = stage; res = DRV_E_COPY; goto out; } // the task starts this copy (a first attempt ends here: nothing is armed, the next one stages again; a retry keeps the staged files) // BitLocker: the restart menu asks for the recovery key on an encrypted PC; suspend protection until the driver is installed (resumed by install/cancel). // -RebootCount 3 is the safety net: protection returns by itself after 3 restarts if the flow is abandoned; a retry suspends again and starts the count over. // "Not encrypted" is not an error. @@ -1206,18 +1206,21 @@ static int prep_locked(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) // Everything else is armed; the wizard lets the user decide (DRV_W_BDE). "Not readable / no BitLocker" with a failing manage-bde is no warning. if ((ps == BDE_PS_ON || ps == BDE_PS_UNKNOWN) && (rc != 0 || after == BDE_PS_ON || after == BDE_PS_UNKNOWN)) bde_warn = TRUE; } - if (run_tool(L"bcdedit.exe", L"/set {current} onetimeadvancedoptions on") != 0) { resume = TRUE; res = DRV_E_BCD; goto out; } + if (run_tool(L"bcdedit.exe", L"/set {current} onetimeadvancedoptions on") != 0) { drop_staged = stage; resume = TRUE; res = DRV_E_BCD; goto out; } n = drv_task_xml(xml, 3000, sid, p); lstrcpyW(z, base); lstrcatW(z, L"\\task.xml"); - if (n < 0 || !write_utf16_file(z, xml, n)) { undo = TRUE; res = DRV_E_TASK; goto out; } + if (n < 0 || !write_utf16_file(z, xml, n)) { drop_staged = stage; undo = TRUE; res = DRV_E_TASK; goto out; } lstrcpyW(args, L"/create /tn " TASK_NAME L" /xml \""); lstrcatW(args, z); lstrcatW(args, L"\" /f"); n = (int)run_tool(L"schtasks.exe", args); DeleteFileW(z); - if (n != 0) { undo = TRUE; res = DRV_E_TASK; goto out; } + if (n != 0) { drop_staged = stage; undo = TRUE; res = DRV_E_TASK; goto out; } drv_log(bde_warn ? L"prep: done, but BitLocker could not be suspended" : L"prep: done"); res = bde_warn ? DRV_W_BDE : DRV_SUCCESS; out: - if (drop_staged) { lstrcpyW(args, base); lstrcatW(args, L"\\driver"); delete_tree(args, NULL, 0); } + if (drop_staged) { // a first attempt that failed: neither the staged files nor an "install anyway" acceptance outlive it + lstrcpyW(args, base); lstrcatW(args, L"\\driver"); delete_tree(args, NULL, 0); + if (accept_path(z, base)) DeleteFileW(z); + } if (undo) cancel_locked(); if (resume) bde_resume(); return res; From d103f7e4fed3906554cbaf61a6c8c082ec732c17 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:41:08 +0800 Subject: [PATCH 23/41] Split setup.h into module headers (setup.h stays as an umbrella) drf_app.h (language, paths, options, command line, basic helpers, install entry points), drf_util.h (path_join, run_process, dir_walk, app key, the elevated-stage helpers), drf_package.h, drf_driver.h, drf_update.h, drf_wizard.h. Declarations used only by the tests moved to drf_package_internal.h and drf_driver_internal.h. The drf_ prefix keeps the names apart from src\setup\util.h. setup.h includes the public headers so update.c is unchanged. Declarations only moved; the generated code is identical. Co-Authored-By: Claude Opus 5.5 --- src/drf_app.h | 64 +++++++++++ src/drf_driver.h | 39 +++++++ src/drf_driver_internal.h | 17 +++ src/drf_package.h | 66 +++++++++++ src/drf_package_internal.h | 16 +++ src/drf_update.h | 7 ++ src/drf_util.h | 31 +++++ src/drf_wizard.h | 5 + src/driver.c | 7 +- src/package.c | 5 +- src/setup.c | 7 +- src/setup.h | 230 ++----------------------------------- src/wizard.c | 4 +- tests/test_driver.c | 1 + tests/test_hardening.c | 1 + 15 files changed, 275 insertions(+), 225 deletions(-) create mode 100644 src/drf_app.h create mode 100644 src/drf_driver.h create mode 100644 src/drf_driver_internal.h create mode 100644 src/drf_package.h create mode 100644 src/drf_package_internal.h create mode 100644 src/drf_update.h create mode 100644 src/drf_util.h create mode 100644 src/drf_wizard.h diff --git a/src/drf_app.h b/src/drf_app.h new file mode 100644 index 0000000..6faa92a --- /dev/null +++ b/src/drf_app.h @@ -0,0 +1,64 @@ +// What every module of DynaRunFix-Setup.exe needs: the language switch T(), the paths, the install options, the command line, the basic helpers of +// setup.c and the install entry points. The other drf_*.h headers (util, package, driver, update, wizard) are included where they are used; setup.h includes +// them all (update.c includes only that). The drf_ prefix keeps these names apart from the src/setup/*.h fragments (util.h there). +#pragma once +#include +#include "errors.h" // RC_*, PK_*, PKC_*, DRV_E_* / DRV_W_*, FIX_DONE: the result codes + +extern BOOL g_zh; +// What the user asked for. Built from the command line (setup.c) or by the wizard's pages (wizard.c) and handed down explicitly: the install, the +// uninstall and run_elevated read nothing else. Only quiet, close, pin and keep reach the elevated stage (run_elevated forwards them as switches). +typedef struct { + BOOL quiet; // /quiet: no message boxes + BOOL close; // close a running DynaRun before installing (/close, or the wizard's consent for one install job) + BOOL pin; // pin DynaRun to the taskbar (default on; /notaskbar) + BOOL autostart; // start it at sign-in (default on; /noautostart) + BOOL autostart_later; // the driver follows: the sign-in start is registered once the driver steps are over (autostart_flush) + BOOL keep; // /keep: leave the pin, the sign-in start and the desktop shortcut as they are +} install_opts; +void install_opts_default(install_opts *o); // pin and autostart on, the rest off +// What the install reports besides its exit code (written by the user stage, read by the wizard). +typedef struct { + int pin; // PIN_*: 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin +} install_result; +// The command line, parsed once (setup.c, cmd_parse). The verb table (setup.c) hands it to the verb's function. +typedef struct { + int argc; WCHAR **argv; // as CommandLineToArgvW gave them; argv[0] is the program, argv[1] the verb + install_opts opts; // the switches that apply to every verb: /quiet /close /notaskbar /noautostart /keep + BOOL pin; // /pin seen (the elevated stage sets up the Windows 11 pin only when it is asked to) + const WCHAR *dzip; // /with-driver : only from argv[4] on (after the verb, the SID and the path), any value is taken + const WCHAR *daccept; // /unverified from argv[4] on, at any position (/machine, /full): the last one with 64 characters + const WCHAR *daccept4; // ... only as argv[4] argv[5] (/driver-prep): the same value, or NULL +} cmdline; +typedef int (*verb_fn)(cmdline *c); // returns the exit code of the process +typedef struct { const WCHAR *name; int min_args; verb_fn run; } verb; // argv[1] (any case) is name and argc >= min_args +extern HWND g_hwnd; // wizard window, owner of UAC prompts +extern WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; +#define T(en, zh) (g_zh ? (zh) : (en)) +enum { DRV_MODE_NONE, DRV_MODE_STANDALONE, DRV_MODE_CONTINUE }; +extern int g_drvmode; // the wizard starts at the driver pages: /driver, /driver-continue + +/* setup.c */ +void zero(void *p, SIZE_T n); +void *alloc(SIZE_T n); +void release(void *p); +BOOL exists(const WCHAR *p); +int msg(const WCHAR *text, UINT flags); +BOOL is_admin(void); +void user_log(const WCHAR *line); // one line in setup.log (administrator, install folder there) or %TEMP%\DynaRunFix-setup.log +DWORD os_build(void); // Windows build number (real, not the compatibility-shimmed one) +BOOL user_sid(WCHAR *out, int cch); +BOOL fix_current(void); // the fix is installed and is the version of this setup (uninstall entry, launcher and dll there) +BOOL locate_dynarun(void); // fills g_exe from the registry or the default folders; no UI +void start_dynarun(void); // the wizard's "Start DynaRun": through our shortcut, like a double click +int install_fix(const install_opts *o, install_result *res); // DynaRun is installed: elevated stage + user shortcuts. RC_OK, RC_CANCEL +int install_all(const install_opts *o, install_result *res, const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL +// The same, and the driver's preparation (driver_prep) in the SAME elevated stage: one UAC prompt for both. zip/accept as for driver_prep. +// RC_OK; RC_CANCEL; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). +int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept); +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); +enum { RUN_DYNARUN = 1, RUN_LAUNCHER = 2, RUN_LEPROC = 4, RUN_CANCLOSE = 8, RUN_NOCLOSE = 16 }; +int running_programs(void); // RUN_* bits of the programs that hold files the setup replaces. RUN_CANCLOSE: one of them can be closed + // by the setup; RUN_NOCLOSE: one cannot (another session, or its exe path cannot be told) + +void autostart_flush(void); // registers the sign-in start that user_stage deferred (HKCU AutostartPending); no-op otherwise diff --git a/src/drf_driver.h b/src/drf_driver.h new file mode 100644 index 0000000..51d600b --- /dev/null +++ b/src/drf_driver.h @@ -0,0 +1,39 @@ +// driver.c: the Dyna Pro USB driver step. +#pragma once +#include "drf_app.h" +#include "drf_package.h" + +/* driver.c: the Dyna Pro USB driver (FTDI 2.08.24 with the Dyna Pro device id; its INF files are not in the signed catalogs that ship with them, so + Windows installs it only with driver signature enforcement off for one start: see docs/DRIVER.md) */ +#define DYNAPRO_USB_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20USB.zip" +#define DRIVER_DOC_URL L"https://github.com/timliudev/DynaRunFix/blob/main/docs/DRIVER.md" +enum { DRV_CI_ENFORCED, DRV_CI_OFF, DRV_CI_UNKNOWN }; // driver signature enforcement now +BOOL driver_supported(void); // Windows 10 (build 10240) or later: older systems install the driver by hand +BOOL driver_installed(void); // a Dyna Pro ftdibus package is in the driver store (%windir%\INF\oem*.inf) +enum { DRV_SRC_DOWNLOADED = 0, DRV_SRC_LOCAL = 1, DRV_ZIPDIFF = 2, DRV_SRC_DIR = 4, DRV_SRC_PICKED = 8 }; // *how of driver_fetch: where the zip came from, and (flag) the zip as a whole is not the official one +// The zip: found on this computer (DRV_SRC_LOCAL; DRV_SRC_DIR: an already unpacked folder, `zip` is its path and hex a digest of the files' hashes) or downloaded. The check is of its CONTENT: the files Windows installs (driver.c, DRV_FILES). +// PK_OK: those files are the pinned ones (DRV_ZIPDIFF: although the zip as a whole differs, e.g. re-packed). DRV_W_HASH: some differ or are missing but +// the package passed the sanity checks - the user is asked ("install anyway"); hex / size: the SHA-256 and size of the zip, rep: the files that differ. +// DRV_E_HASH: not a usable driver package. Other PK_*: the download failed. cb((DWORD)-1, 1) tells the "checking" phase (after the download, not pausable). +int driver_fetch(WCHAR *zip, progress_fn cb, volatile LONG *cancel, DWORD *err, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); +void driver_skip_local_other(void); // the user declined a local copy that differs: the next driver_fetch skips such copies and downloads +BOOL driver_layout_ok(const WCHAR *dir); // the folder holds ftdibus.inf with i386 and amd64 folders (an unpacked driver) +int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); // a zip or an unpacked folder the user chose: the same check as for a copy that was found (PK_OK / DRV_W_HASH / DRV_E_HASH); path becomes the folder +const WCHAR *driver_pinned_sha256(void); // the SHA-256 of the zip this program knows +void driver_forget(const WCHAR *zip); // deletes the zip when it is our own cache file in %TEMP%\DynaRunFix (a refused download), never a file of the user's +BOOL driver_partial(DWORD *have); // a partial driver download that driver_fetch can continue; *have: bytes so far +int driver_prep(const install_opts *o, const WCHAR *zip, const WCHAR *accept); // elevated: stage the driver, arm the restart menu and the task (zip "-": the staged copy); 0 or DRV_E_*, 2 = UAC cancelled +int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept); // elevated, inside the install stage (setup.c): driver_prep for sid, no further elevation +int driver_bitlocker(void); // BitLocker protection of the system drive, not elevated: 1 on, 0 off / none, -1 cannot be told (needs administrator rights) +int driver_install(const install_opts *o); // elevated: pnputil, then clean up. 0 or DRV_E_*, 2 = UAC cancelled +int driver_cancel(const install_opts *o); // elevated: task, restart menu and BitLocker suspension back. 2 = UAC cancelled +BOOL driver_reboot(void); // restarts Windows (not elevated) +int driver_ci_state(void); // DRV_CI_* +// The driver's entries in the verb table (setup.c). The elevated three refuse a process that is not elevated (RC_FAIL). +int cmd_driver(cmdline *c); // /driver: the wizard at the driver pages +int cmd_driver_continue(cmdline *c); // /driver-continue: the same, after the restart +int cmd_driver_prep(cmdline *c); // /driver-prep [/unverified ] +int cmd_driver_install(cmdline *c); // /driver-install +int cmd_driver_cancel(cmdline *c); // /driver-cancel +void driver_machine_stage(const WCHAR *installed_setup); // elevated install: the Start menu shortcut "Install Dyna Pro USB driver" +void driver_machine_uninstall(void); // elevated uninstall: the shortcut, a waiting task, BitLocker resumed if we suspended it, the staged files (an installed driver is left); call before the DynaRunFix key is deleted diff --git a/src/drf_driver_internal.h b/src/drf_driver_internal.h new file mode 100644 index 0000000..5e821b3 --- /dev/null +++ b/src/drf_driver_internal.h @@ -0,0 +1,17 @@ +// driver.c declarations that only tests/test_driver.c uses besides driver.c itself (pure helpers and the pinned file table). +#pragma once +#include "drf_driver.h" + +int drv_file_count(void); // the files that get installed (pinned: name below "Dyna Pro USB\", size, SHA-256) +int drv_file_find(const WCHAR *rel); // index of that name (any case, / or \), or -1 +BOOL drv_file_info(int i, const WCHAR **rel, DWORD *size, const WCHAR **sha); +BOOL drv_file_match(int i, DWORD size, const WCHAR *sha); // is a file of this size and SHA-256 the pinned file i? +int drv_check_dir(const WCHAR *dir, int *differ, int *missing); // how many pinned files below dir\Dyna Pro USB\ are right; the others differ / are missing +BOOL drv_inf_matches(const BYTE *data, DWORD n); // pure helpers, unit-tested (tests\test_driver.c) +BOOL drv_ci_enforced(DWORD code_integrity_options); +BOOL drv_bde_suspend(int protection_status, int conversion_status, int key_protectors); // BDE_PS_* (driver.c), conversion status and protector count (-1 unknown): suspend BitLocker? A volume mid-conversion with protectors is suspended although it reads "off" +BOOL drv_bde_suspend_now(int protection_status, int conversion_status, int key_protectors, BOOL suspended_by_us); // ... at prep time: also when our own flag says we suspended it earlier (a retry) +int drv_task_xml(WCHAR *out, int cch, const WCHAR *sid, const WCHAR *exe); +BOOL drv_sys_tool(const WCHAR *windir, BOOL wow64, const WCHAR *tool, WCHAR *out, int cch); +BOOL drv_pnputil_ok(DWORD code); +BOOL drv_pnputil_args(WCHAR *out, int cch, const WCHAR *inf); diff --git a/src/drf_package.h b/src/drf_package.h new file mode 100644 index 0000000..89cd07a --- /dev/null +++ b/src/drf_package.h @@ -0,0 +1,66 @@ +// package.c: finding and downloading the DynaRun setup and the driver package, zip and MSI handling. +#pragma once +#include +#include +#include "errors.h" // PK_*, PKC_* + +#define DYNAPRO_PAGE L"https://dynapro.co.uk/Software_Release.htm" +#define DYNAPRO_HOST L"dynapro.co.uk" +// Hosts a download from GitHub may be served from (releases/download/... redirects to the asset CDN): every hop is checked against them. +#define GITHUB_HOSTS L"github.com;api.github.com;objects.githubusercontent.com;release-assets.githubusercontent.com" +#define DYNAPRO_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20Dynamometers.zip" +typedef void (*progress_fn)(DWORD done, DWORD total); + +typedef struct { + WCHAR path[MAX_PATH]; + DWORD data; // offset of the (encrypted) entry data + DWORD csize, usize, crc, flags, method, dostime; +} zipent; + +BOOL pkg_find_local(WCHAR *out); // a DynaRun MSI, or a zip with an .msi inside, in Downloads/Desktop/Documents or next to us +// What a search for a driver package looks at and tells: accept(path, ctx) approves a zip / folder (the walk stops at the first one), log (NULL: silent) +// gets one line per folder looked at. pkg_find_zip_named uses only log (s may be NULL there). +typedef struct { BOOL (*accept)(const WCHAR *path, void *ctx); void *ctx; void (*log)(const WCHAR *line); } pkg_search; +BOOL pkg_find_zip_if(DWORD maxsize, const pkg_search *s, WCHAR *out); // the first zip (at most maxsize bytes) in the same places that s->accept() approves +BOOL pkg_find_dirs(const pkg_search *s, WCHAR *out); // the first folder s->accept() approves, in the same places (two levels down in Downloads) +BOOL pkg_find_dir_in(const WCHAR *root, int depth, const pkg_search *s, WCHAR *out); // s->accept() for root and the folders up to `depth` levels below it +BOOL pkg_find_zip_named(const WCHAR *prefix, const pkg_search *s, WCHAR *out); // the newest zip whose name starts with prefix in the same places +void pkg_reject(const WCHAR *path); // not the DynaRun setup after all: pkg_find_local skips it +int pkg_download(WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); +HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, const WCHAR *hdr, DWORD *status, DWORD *err, BOOL *refused); // redirects followed by hand, each hop checked +BOOL pkg_partial(const WCHAR *name, const WCHAR *etagval, DWORD *have); // a partial download pkg_fetch can continue (.part and its ETag are there); *have: bytes so far +BOOL pkg_download_partial(DWORD *have); // ... of the DynaRun zip +enum { NET_UNKNOWN = 0, NET_NO = 1, NET_YES = 2 }; +HINTERNET pkg_net_open(DWORD connect_ms, DWORD receive_ms); // a session with these timeouts, TLS 1.2+ (NULL: none) +BOOL dynarun_version(WCHAR *out, int cch); // the installed DynaRun V3's version (Windows Installer record), FALSE when not installed that way +int pkg_update_hint(WCHAR *installed, WCHAR *latest, int cch, void (*log)(const WCHAR *line)); // 0 nothing, 1 the site names a newer version, 2 the site's zip differs from the last download +int pkg_net_state(void); // NET_NO: Windows says it is disconnected (the download is not even tried); NET_YES / NET_UNKNOWN: try +int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const WCHAR *host, WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); +int zip_open(const WCHAR *zip, zipent *e); // first *.msi entry +// The files of a package named in a pinned table (the driver's 18 files): safe unpacking / copying and checking (package.c). Used by the driver; the DynaRun setup +// uses pkg_prepare_target / pkg_extract_checked. Nothing a zip says about a path is used, destinations are built from the table; links are refused; sizes capped. +typedef struct { const WCHAR *rel; DWORD size; const WCHAR *sha; } pkg_file; // name below the package folder ('/' separators), size, SHA-256 (lower case) +#define PKG_MAXF 24 +typedef struct { int ok, differ, missing; BYTE st[PKG_MAXF]; WCHAR hex[PKG_MAXF][65]; } pkg_check; // st: 0 ok, 1 differs, 2 missing; hex: what the file hashes to +int pkg_entry_index(const pkg_file *t, int n, const char *name, DWORD nlen); // the table file a zip entry name is (after a folder boundary, any case), or -1 +BOOL pkg_is_plain_dir(const WCHAR *p); +BOOL pkg_file_path(WCHAR *out, const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int i); +BOOL pkg_dirs_plain(const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n); +int pkg_zip_has(const WCHAR *zip, const pkg_file *t, int n, DWORD maxfile); // how many table files the zip has (central directory only), -1: not a zip +int pkg_stage_files(const WCHAR *zip, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, volatile LONG *cancel, int *missing); +int pkg_stage_from_dir(const WCHAR *src, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, int *missing); +void pkg_check_files(const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int n, WCHAR (*expect)[65], pkg_check *c); +int pkg_extract_checked(const zipent *e, const char *pw, const WCHAR *out, DWORD maxsize, progress_fn cb, volatile LONG *cancel); // zip_extract with a size cap and a free target +BOOL zip_password_plausible(const zipent *e, const char *pw); +int zip_extract(const zipent *e, const char *pw, const WCHAR *out, progress_fn cb, volatile LONG *cancel); +BOOL pkg_sha256(const WCHAR *path, WCHAR *hex, ULONGLONG *size); // lower-case hex (65 WCHARs) and size of a file, read-only +void msi_info(const WCHAR *msi, WCHAR *version, WCHAR *manufacturer, int cch); // ProductVersion / Manufacturer (Windows Installer API, read-only; "" when absent) +char *msi_license_rtf(const WCHAR *msi); // RTF of the setup's license page (heap) or NULL +enum { MSI_NONE, MSI_OTHER, MSI_DYNARUN }; +int msi_check(const WCHAR *msi, WCHAR *version, int cch); // MSI_NONE: not an MSI at all. DynaRun: by UpgradeCode +WCHAR *msi_external_files(const WCHAR *msi); // multi-string (heap) of cabinet files kept outside the MSI, or NULL +int run_msiexec(const WCHAR *msi); // msiexec exit code +BOOL dynarun_product(WCHAR *out); // product code of the installed DynaRun V3 (39 chars) +int remove_dynarun(void); // Dyna Pro's uninstall of DynaRun V3; msiexec exit code, -1 none +void pkg_temp_dir(WCHAR *out); // %TEMP%\DynaRunFix (created) +int pkg_prepare_documents(const WCHAR *msi); // download OneDrive "online-only" copies the setup overwrites diff --git a/src/drf_package_internal.h b/src/drf_package_internal.h new file mode 100644 index 0000000..09ce88e --- /dev/null +++ b/src/drf_package_internal.h @@ -0,0 +1,16 @@ +// package.c declarations that only the tests (tests/test_driver.c, test_hardening.c, test_update.c) use besides package.c itself. +#pragma once +#include "drf_package.h" + +BOOL pkg_url_ok(const WCHAR *u, const WCHAR *hosts); // https and on one of the ';'-separated hosts (NULL = any) +BOOL pkg_redirect_target(const WCHAR *cur, const WCHAR *loc, const WCHAR *host, WCHAR *out, int cch); // where a redirect from cur to loc goes, if allowed +int ver_compare(const WCHAR *a, const WCHAR *b); // <0 / 0 / >0: a older / the same / newer than b (not versions: 0) +BOOL pkg_page_version(const char *html, DWORD n, WCHAR *out, int cch); // the DynaRun V3 version the Dyna Pro software page names +// Every entry of the central directory, in order: fn(name as stored (not 0-terminated), its length, entry, ctx). fn returns FALSE to stop. +// PK_OK: all visited; PK_CANCEL: fn stopped it; PK_FORMAT: not a (supported, no Zip64) zip; PK_IO. e->data is valid (local header read). +typedef BOOL (*zip_visit_fn)(const char *name, DWORD nlen, const zipent *e, void *ctx); +int zip_walk(const WCHAR *zip, zip_visit_fn fn, void *ctx); +// An entry name that may be created below a folder: relative, '/' separators, no "..", ".", empty, "x:" / backslash / control or +// reserved characters, no device names, no trailing dot or space. *isdir: the name ends in '/'. +BOOL zip_name_safe(const char *name, DWORD nlen, BOOL *isdir); +BOOL pkg_prepare_target(const WCHAR *path); // free a path that is going to be written (a plain file is removed, a link removed itself, a folder refused) diff --git a/src/drf_update.h b/src/drf_update.h new file mode 100644 index 0000000..96ac28c --- /dev/null +++ b/src/drf_update.h @@ -0,0 +1,7 @@ +// update.c: the update check and offer (run by the launcher). +#pragma once +#include "drf_app.h" + +int update_check(void); // /checkupdate: notes a newer release in HKCU\Software\DynaRunFix; no UI +int update_offer(void); // /offer: the launcher started DynaRun; asks beside it, installs once it is closed +int update_run(const WCHAR *args); // /update : asks, installs a noted release, starts the launcher diff --git a/src/drf_util.h b/src/drf_util.h new file mode 100644 index 0000000..9d15b73 --- /dev/null +++ b/src/drf_util.h @@ -0,0 +1,31 @@ +// Helpers of setup.c that package.c, driver.c and the fragments share: path join, process start, directory walk, the app key, the elevated stage. +#pragma once +#include "drf_app.h" +// shared helpers (setup.c): used by setup.c, package.c, driver.c +// out = "dir\name", when the result is short enough for a buffer of `limit` characters (it must be at least 2 characters shorter than limit: the old +// `lstrlenW(dir) + lstrlenW(name) + 2 >= limit` test); FALSE and out untouched otherwise. out may be dir itself (not name). +BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name); +// Starts exe with the command line cmd (a writable buffer; creation flags `flags`, working folder cwd or NULL, window hidden when `hide`) and waits for it. +// PROC_EXITED: it ended, *code is its exit code (left alone if the code cannot be read); PROC_TIMEOUT: still running after ms (INFINITE never), it was +// terminated (exit code 1), *code is left alone; PROC_NOSTART: CreateProcess failed, GetLastError() is its error. Handles are not inherited. +enum { PROC_NOSTART = 0, PROC_EXITED = 1, PROC_TIMEOUT = 2 }; +int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code); +// Calls fn(path, fd, ctx) for each entry of dir; names starting with '.' and entries that do not pass path_join(.., limit, ..) are skipped. +// path is "dir\name" in buf (the caller's buffer of bufcch characters, which first holds the pattern "dir\*"). fn may call dir_walk again for a subfolder, +// with a buffer of its own: that is how the walks recurse. fn returns FALSE to stop. WALK_DONE: all entries visited; WALK_STOPPED: fn stopped it; +// WALK_NOOPEN: the folder cannot be read (or the pattern does not fit in buf). +enum { WALK_DONE = 0, WALK_STOPPED = 1, WALK_NOOPEN = 2 }; +typedef BOOL (*walk_fn)(const WCHAR *path, const WIN32_FIND_DATAW *fd, void *ctx); +int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx); +// HKCU\Software\DynaRunFix (APP_USER) or HKLM\SOFTWARE\DynaRunFix (APP_MACHINE): TRUE and *k open (KEY_ALL_ACCESS when created), else FALSE. The caller closes *k. +#define APP_USER FALSE +#define APP_MACHINE TRUE +BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k); +BOOL app_key_create(BOOL machine, HKEY *k); +// shared with driver.c +void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth); // a link is removed itself, never followed; in-use files go at the next restart +BOOL same_file(const WCHAR *a, const WCHAR *b); +int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o); // " [ /quiet][ /close][ /pin][ /keep]" (from *o) with one UAC prompt; its exit code, RC_CANCEL = cancelled +BOOL valid_user_sid(const WCHAR *sid); // a SID of a real account with a profile here +BOOL path_user_writable(const WCHAR *path, const WCHAR *sid); // could ordinary users (or the user sid) change what is at path (or is that unknown) +BOOL enable_privilege(const WCHAR *name); diff --git a/src/drf_wizard.h b/src/drf_wizard.h new file mode 100644 index 0000000..f6043b3 --- /dev/null +++ b/src/drf_wizard.h @@ -0,0 +1,5 @@ +// wizard.c +#pragma once +#include "drf_app.h" + +int wizard(const install_opts *init); // init: the options the command line gave (the pages change them) diff --git a/src/driver.c b/src/driver.c index 6551fbe..d3d3184 100644 --- a/src/driver.c +++ b/src/driver.c @@ -30,7 +30,12 @@ #include #include #include -#include "setup.h" +#include "drf_app.h" +#include "drf_util.h" +#include "drf_package.h" +#include "drf_driver.h" +#include "drf_driver_internal.h" +#include "drf_wizard.h" // SHA-256 of "Dyna Pro USB.zip" as served by Dyna Pro on 2026-10-15 (2335114 bytes; Last-Modified 2026-05-09). A different file is never installed. #define DRIVER_ZIP_SHA256 L"c3ae487018956767d65a08e3d5cd8434da42cdce81988753515ee0f72c49e943" diff --git a/src/package.c b/src/package.c index ba1a0c9..1c8f6ef 100644 --- a/src/package.c +++ b/src/package.c @@ -9,7 +9,10 @@ #include #include #include -#include "setup.h" +#include "drf_app.h" +#include "drf_util.h" +#include "drf_package.h" +#include "drf_package_internal.h" #include "version.h" #include "miniz.h" // inflate only (third_party/miniz, built with the MINIZ_NO_* options in build.cmd) diff --git a/src/setup.c b/src/setup.c index 39b46cc..fd25ca7 100644 --- a/src/setup.c +++ b/src/setup.c @@ -52,7 +52,12 @@ #include #include #include -#include "setup.h" +#include "drf_app.h" +#include "drf_util.h" +#include "drf_package.h" +#include "drf_driver.h" +#include "drf_update.h" +#include "drf_wizard.h" #include "version.h" // The rest of this file is split by topic into src\setup\*.h. They are fragments of THIS translation unit (static functions and file-local diff --git a/src/setup.h b/src/setup.h index 3e7168d..20870b3 100644 --- a/src/setup.h +++ b/src/setup.h @@ -1,222 +1,10 @@ -// Shared declarations of DynaRunFix-Setup.exe (setup.c, wizard.c, package.c). +// Shared declarations of DynaRunFix-Setup.exe, as one umbrella over the module headers (kept so that update.c, which includes only this, is unchanged). +// New code includes the module header it needs: drf_app.h (every module), drf_util.h, drf_package.h, drf_driver.h, drf_update.h, drf_wizard.h. +// The *_internal.h headers (used by the tests) are not part of it. #pragma once -#include -#include -#include "errors.h" // RC_*, PK_*, PKC_*, DRV_E_* / DRV_W_*, FIX_DONE: the result codes - -extern BOOL g_zh; -// What the user asked for. Built from the command line (setup.c) or by the wizard's pages (wizard.c) and handed down explicitly: the install, the -// uninstall and run_elevated read nothing else. Only quiet, close, pin and keep reach the elevated stage (run_elevated forwards them as switches). -typedef struct { - BOOL quiet; // /quiet: no message boxes - BOOL close; // close a running DynaRun before installing (/close, or the wizard's consent for one install job) - BOOL pin; // pin DynaRun to the taskbar (default on; /notaskbar) - BOOL autostart; // start it at sign-in (default on; /noautostart) - BOOL autostart_later; // the driver follows: the sign-in start is registered once the driver steps are over (autostart_flush) - BOOL keep; // /keep: leave the pin, the sign-in start and the desktop shortcut as they are -} install_opts; -void install_opts_default(install_opts *o); // pin and autostart on, the rest off -// What the install reports besides its exit code (written by the user stage, read by the wizard). -typedef struct { - int pin; // PIN_*: 0 not asked, 1 pinned, 2 appears after the next sign-in, 3 could not pin -} install_result; -// The command line, parsed once (setup.c, cmd_parse). The verb table (setup.c) hands it to the verb's function. -typedef struct { - int argc; WCHAR **argv; // as CommandLineToArgvW gave them; argv[0] is the program, argv[1] the verb - install_opts opts; // the switches that apply to every verb: /quiet /close /notaskbar /noautostart /keep - BOOL pin; // /pin seen (the elevated stage sets up the Windows 11 pin only when it is asked to) - const WCHAR *dzip; // /with-driver : only from argv[4] on (after the verb, the SID and the path), any value is taken - const WCHAR *daccept; // /unverified from argv[4] on, at any position (/machine, /full): the last one with 64 characters - const WCHAR *daccept4; // ... only as argv[4] argv[5] (/driver-prep): the same value, or NULL -} cmdline; -typedef int (*verb_fn)(cmdline *c); // returns the exit code of the process -typedef struct { const WCHAR *name; int min_args; verb_fn run; } verb; // argv[1] (any case) is name and argc >= min_args -extern HWND g_hwnd; // wizard window, owner of UAC prompts -extern WCHAR g_self[MAX_PATH], g_dir[MAX_PATH], g_launcher[MAX_PATH], g_exe[MAX_PATH]; -#define T(en, zh) (g_zh ? (zh) : (en)) -enum { DRV_MODE_NONE, DRV_MODE_STANDALONE, DRV_MODE_CONTINUE }; -extern int g_drvmode; // the wizard starts at the driver pages: /driver, /driver-continue - -/* setup.c */ -void zero(void *p, SIZE_T n); -void *alloc(SIZE_T n); -void release(void *p); -BOOL exists(const WCHAR *p); -int msg(const WCHAR *text, UINT flags); -BOOL is_admin(void); -void user_log(const WCHAR *line); // one line in setup.log (administrator, install folder there) or %TEMP%\DynaRunFix-setup.log -DWORD os_build(void); // Windows build number (real, not the compatibility-shimmed one) -BOOL user_sid(WCHAR *out, int cch); -BOOL fix_current(void); // the fix is installed and is the version of this setup (uninstall entry, launcher and dll there) -BOOL locate_dynarun(void); // fills g_exe from the registry or the default folders; no UI -void start_dynarun(void); // the wizard's "Start DynaRun": through our shortcut, like a double click -int install_fix(const install_opts *o, install_result *res); // DynaRun is installed: elevated stage + user shortcuts. RC_OK, RC_CANCEL -int install_all(const install_opts *o, install_result *res, const WCHAR *msi); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL -// The same, and the driver's preparation (driver_prep) in the SAME elevated stage: one UAC prompt for both. zip/accept as for driver_prep. -// RC_OK; RC_CANCEL; another small code: the fix failed (the driver was not touched); >= DRV_E_HASH: the fix is installed, the driver step gave this code (DRV_E_*, DRV_W_BDE). -int install_fix_driver(const install_opts *o, install_result *res, const WCHAR *zip, const WCHAR *accept); -int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *zip, const WCHAR *accept); -// shared helpers (setup.c): used by setup.c, package.c, driver.c -// out = "dir\name", when the result is short enough for a buffer of `limit` characters (it must be at least 2 characters shorter than limit: the old -// `lstrlenW(dir) + lstrlenW(name) + 2 >= limit` test); FALSE and out untouched otherwise. out may be dir itself (not name). -BOOL path_join(WCHAR *out, int limit, const WCHAR *dir, const WCHAR *name); -// Starts exe with the command line cmd (a writable buffer; creation flags `flags`, working folder cwd or NULL, window hidden when `hide`) and waits for it. -// PROC_EXITED: it ended, *code is its exit code (left alone if the code cannot be read); PROC_TIMEOUT: still running after ms (INFINITE never), it was -// terminated (exit code 1), *code is left alone; PROC_NOSTART: CreateProcess failed, GetLastError() is its error. Handles are not inherited. -enum { PROC_NOSTART = 0, PROC_EXITED = 1, PROC_TIMEOUT = 2 }; -int run_process(const WCHAR *exe, WCHAR *cmd, const WCHAR *cwd, DWORD flags, BOOL hide, DWORD ms, DWORD *code); -// Calls fn(path, fd, ctx) for each entry of dir; names starting with '.' and entries that do not pass path_join(.., limit, ..) are skipped. -// path is "dir\name" in buf (the caller's buffer of bufcch characters, which first holds the pattern "dir\*"). fn may call dir_walk again for a subfolder, -// with a buffer of its own: that is how the walks recurse. fn returns FALSE to stop. WALK_DONE: all entries visited; WALK_STOPPED: fn stopped it; -// WALK_NOOPEN: the folder cannot be read (or the pattern does not fit in buf). -enum { WALK_DONE = 0, WALK_STOPPED = 1, WALK_NOOPEN = 2 }; -typedef BOOL (*walk_fn)(const WCHAR *path, const WIN32_FIND_DATAW *fd, void *ctx); -int dir_walk(const WCHAR *dir, WCHAR *buf, int bufcch, int limit, walk_fn fn, void *ctx); -// HKCU\Software\DynaRunFix (APP_USER) or HKLM\SOFTWARE\DynaRunFix (APP_MACHINE): TRUE and *k open (KEY_ALL_ACCESS when created), else FALSE. The caller closes *k. -#define APP_USER FALSE -#define APP_MACHINE TRUE -BOOL app_key_open(BOOL machine, REGSAM sam, HKEY *k); -BOOL app_key_create(BOOL machine, HKEY *k); -// shared with driver.c -void delete_tree(const WCHAR *dir, const WCHAR *keep, int depth); // a link is removed itself, never followed; in-use files go at the next restart -BOOL same_file(const WCHAR *a, const WCHAR *b); -int run_elevated(const WCHAR *args, const WCHAR *exe_path, const install_opts *o); // " [ /quiet][ /close][ /pin][ /keep]" (from *o) with one UAC prompt; its exit code, RC_CANCEL = cancelled -BOOL valid_user_sid(const WCHAR *sid); // a SID of a real account with a profile here -BOOL path_user_writable(const WCHAR *path, const WCHAR *sid); // could ordinary users (or the user sid) change what is at path (or is that unknown) -BOOL enable_privilege(const WCHAR *name); -enum { RUN_DYNARUN = 1, RUN_LAUNCHER = 2, RUN_LEPROC = 4, RUN_CANCLOSE = 8, RUN_NOCLOSE = 16 }; -int running_programs(void); // RUN_* bits of the programs that hold files the setup replaces. RUN_CANCLOSE: one of them can be closed - // by the setup; RUN_NOCLOSE: one cannot (another session, or its exe path cannot be told) - -/* update.c */ -int update_check(void); // /checkupdate: notes a newer release in HKCU\Software\DynaRunFix; no UI -int update_offer(void); // /offer: the launcher started DynaRun; asks beside it, installs once it is closed -int update_run(const WCHAR *args); // /update : asks, installs a noted release, starts the launcher - -void autostart_flush(void); // registers the sign-in start that user_stage deferred (HKCU AutostartPending); no-op otherwise - -/* wizard.c */ -int wizard(const install_opts *init); // init: the options the command line gave (the pages change them) - - -/* package.c */ -#define DYNAPRO_PAGE L"https://dynapro.co.uk/Software_Release.htm" -#define DYNAPRO_HOST L"dynapro.co.uk" -// Hosts a download from GitHub may be served from (releases/download/... redirects to the asset CDN): every hop is checked against them. -#define GITHUB_HOSTS L"github.com;api.github.com;objects.githubusercontent.com;release-assets.githubusercontent.com" -#define DYNAPRO_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20Dynamometers.zip" -typedef void (*progress_fn)(DWORD done, DWORD total); - -typedef struct { - WCHAR path[MAX_PATH]; - DWORD data; // offset of the (encrypted) entry data - DWORD csize, usize, crc, flags, method, dostime; -} zipent; - -BOOL pkg_find_local(WCHAR *out); // a DynaRun MSI, or a zip with an .msi inside, in Downloads/Desktop/Documents or next to us -// What a search for a driver package looks at and tells: accept(path, ctx) approves a zip / folder (the walk stops at the first one), log (NULL: silent) -// gets one line per folder looked at. pkg_find_zip_named uses only log (s may be NULL there). -typedef struct { BOOL (*accept)(const WCHAR *path, void *ctx); void *ctx; void (*log)(const WCHAR *line); } pkg_search; -BOOL pkg_find_zip_if(DWORD maxsize, const pkg_search *s, WCHAR *out); // the first zip (at most maxsize bytes) in the same places that s->accept() approves -BOOL pkg_find_dirs(const pkg_search *s, WCHAR *out); // the first folder s->accept() approves, in the same places (two levels down in Downloads) -BOOL pkg_find_dir_in(const WCHAR *root, int depth, const pkg_search *s, WCHAR *out); // s->accept() for root and the folders up to `depth` levels below it -BOOL pkg_find_zip_named(const WCHAR *prefix, const pkg_search *s, WCHAR *out); // the newest zip whose name starts with prefix in the same places -void pkg_reject(const WCHAR *path); // not the DynaRun setup after all: pkg_find_local skips it -int pkg_download(WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); -BOOL pkg_url_ok(const WCHAR *u, const WCHAR *hosts); // https and on one of the ';'-separated hosts (NULL = any) -HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, const WCHAR *hdr, DWORD *status, DWORD *err, BOOL *refused); // redirects followed by hand, each hop checked -BOOL pkg_redirect_target(const WCHAR *cur, const WCHAR *loc, const WCHAR *host, WCHAR *out, int cch); // where a redirect from cur to loc goes, if allowed -BOOL pkg_partial(const WCHAR *name, const WCHAR *etagval, DWORD *have); // a partial download pkg_fetch can continue (.part and its ETag are there); *have: bytes so far -BOOL pkg_download_partial(DWORD *have); // ... of the DynaRun zip -enum { NET_UNKNOWN = 0, NET_NO = 1, NET_YES = 2 }; -HINTERNET pkg_net_open(DWORD connect_ms, DWORD receive_ms); // a session with these timeouts, TLS 1.2+ (NULL: none) -int ver_compare(const WCHAR *a, const WCHAR *b); // <0 / 0 / >0: a older / the same / newer than b (not versions: 0) -BOOL pkg_page_version(const char *html, DWORD n, WCHAR *out, int cch); // the DynaRun V3 version the Dyna Pro software page names -BOOL dynarun_version(WCHAR *out, int cch); // the installed DynaRun V3's version (Windows Installer record), FALSE when not installed that way -int pkg_update_hint(WCHAR *installed, WCHAR *latest, int cch, void (*log)(const WCHAR *line)); // 0 nothing, 1 the site names a newer version, 2 the site's zip differs from the last download -int pkg_net_state(void); // NET_NO: Windows says it is disconnected (the download is not even tried); NET_YES / NET_UNKNOWN: try -int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const WCHAR *host, WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); -int zip_open(const WCHAR *zip, zipent *e); // first *.msi entry -// Every entry of the central directory, in order: fn(name as stored (not 0-terminated), its length, entry, ctx). fn returns FALSE to stop. -// PK_OK: all visited; PK_CANCEL: fn stopped it; PK_FORMAT: not a (supported, no Zip64) zip; PK_IO. e->data is valid (local header read). -typedef BOOL (*zip_visit_fn)(const char *name, DWORD nlen, const zipent *e, void *ctx); -int zip_walk(const WCHAR *zip, zip_visit_fn fn, void *ctx); -// An entry name that may be created below a folder: relative, '/' separators, no "..", ".", empty, "x:" / backslash / control or -// reserved characters, no device names, no trailing dot or space. *isdir: the name ends in '/'. -BOOL zip_name_safe(const char *name, DWORD nlen, BOOL *isdir); -// The files of a package named in a pinned table (the driver's 18 files): safe unpacking / copying and checking (package.c). Used by the driver; the DynaRun setup -// uses pkg_prepare_target / pkg_extract_checked. Nothing a zip says about a path is used, destinations are built from the table; links are refused; sizes capped. -typedef struct { const WCHAR *rel; DWORD size; const WCHAR *sha; } pkg_file; // name below the package folder ('/' separators), size, SHA-256 (lower case) -#define PKG_MAXF 24 -typedef struct { int ok, differ, missing; BYTE st[PKG_MAXF]; WCHAR hex[PKG_MAXF][65]; } pkg_check; // st: 0 ok, 1 differs, 2 missing; hex: what the file hashes to -int pkg_entry_index(const pkg_file *t, int n, const char *name, DWORD nlen); // the table file a zip entry name is (after a folder boundary, any case), or -1 -BOOL pkg_is_plain_dir(const WCHAR *p); -BOOL pkg_file_path(WCHAR *out, const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int i); -BOOL pkg_dirs_plain(const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n); -int pkg_zip_has(const WCHAR *zip, const pkg_file *t, int n, DWORD maxfile); // how many table files the zip has (central directory only), -1: not a zip -int pkg_stage_files(const WCHAR *zip, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, volatile LONG *cancel, int *missing); -int pkg_stage_from_dir(const WCHAR *src, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, int *missing); -void pkg_check_files(const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int n, WCHAR (*expect)[65], pkg_check *c); -BOOL pkg_prepare_target(const WCHAR *path); // free a path that is going to be written (a plain file is removed, a link removed itself, a folder refused) -int pkg_extract_checked(const zipent *e, const char *pw, const WCHAR *out, DWORD maxsize, progress_fn cb, volatile LONG *cancel); // zip_extract with a size cap and a free target -BOOL zip_password_plausible(const zipent *e, const char *pw); -int zip_extract(const zipent *e, const char *pw, const WCHAR *out, progress_fn cb, volatile LONG *cancel); -BOOL pkg_sha256(const WCHAR *path, WCHAR *hex, ULONGLONG *size); // lower-case hex (65 WCHARs) and size of a file, read-only -void msi_info(const WCHAR *msi, WCHAR *version, WCHAR *manufacturer, int cch); // ProductVersion / Manufacturer (Windows Installer API, read-only; "" when absent) -char *msi_license_rtf(const WCHAR *msi); // RTF of the setup's license page (heap) or NULL -enum { MSI_NONE, MSI_OTHER, MSI_DYNARUN }; -int msi_check(const WCHAR *msi, WCHAR *version, int cch); // MSI_NONE: not an MSI at all. DynaRun: by UpgradeCode -WCHAR *msi_external_files(const WCHAR *msi); // multi-string (heap) of cabinet files kept outside the MSI, or NULL -int run_msiexec(const WCHAR *msi); // msiexec exit code -BOOL dynarun_product(WCHAR *out); // product code of the installed DynaRun V3 (39 chars) -int remove_dynarun(void); // Dyna Pro's uninstall of DynaRun V3; msiexec exit code, -1 none -void pkg_temp_dir(WCHAR *out); // %TEMP%\DynaRunFix (created) -int pkg_prepare_documents(const WCHAR *msi); // download OneDrive "online-only" copies the setup overwrites - -/* driver.c: the Dyna Pro USB driver (FTDI 2.08.24 with the Dyna Pro device id; its INF files are not in the signed catalogs that ship with them, so - Windows installs it only with driver signature enforcement off for one start: see docs/DRIVER.md) */ -#define DYNAPRO_USB_ZIP L"https://dynapro.co.uk/Site/graphics/Technical_Info/Software/Dyna%20Pro%20USB.zip" -#define DRIVER_DOC_URL L"https://github.com/timliudev/DynaRunFix/blob/main/docs/DRIVER.md" -enum { DRV_CI_ENFORCED, DRV_CI_OFF, DRV_CI_UNKNOWN }; // driver signature enforcement now -BOOL driver_supported(void); // Windows 10 (build 10240) or later: older systems install the driver by hand -BOOL driver_installed(void); // a Dyna Pro ftdibus package is in the driver store (%windir%\INF\oem*.inf) -enum { DRV_SRC_DOWNLOADED = 0, DRV_SRC_LOCAL = 1, DRV_ZIPDIFF = 2, DRV_SRC_DIR = 4, DRV_SRC_PICKED = 8 }; // *how of driver_fetch: where the zip came from, and (flag) the zip as a whole is not the official one -// The zip: found on this computer (DRV_SRC_LOCAL; DRV_SRC_DIR: an already unpacked folder, `zip` is its path and hex a digest of the files' hashes) or downloaded. The check is of its CONTENT: the files Windows installs (driver.c, DRV_FILES). -// PK_OK: those files are the pinned ones (DRV_ZIPDIFF: although the zip as a whole differs, e.g. re-packed). DRV_W_HASH: some differ or are missing but -// the package passed the sanity checks - the user is asked ("install anyway"); hex / size: the SHA-256 and size of the zip, rep: the files that differ. -// DRV_E_HASH: not a usable driver package. Other PK_*: the download failed. cb((DWORD)-1, 1) tells the "checking" phase (after the download, not pausable). -int driver_fetch(WCHAR *zip, progress_fn cb, volatile LONG *cancel, DWORD *err, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); -void driver_skip_local_other(void); // the user declined a local copy that differs: the next driver_fetch skips such copies and downloads -BOOL driver_layout_ok(const WCHAR *dir); // the folder holds ftdibus.inf with i386 and amd64 folders (an unpacked driver) -int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); // a zip or an unpacked folder the user chose: the same check as for a copy that was found (PK_OK / DRV_W_HASH / DRV_E_HASH); path becomes the folder -const WCHAR *driver_pinned_sha256(void); // the SHA-256 of the zip this program knows -void driver_forget(const WCHAR *zip); // deletes the zip when it is our own cache file in %TEMP%\DynaRunFix (a refused download), never a file of the user's -BOOL driver_partial(DWORD *have); // a partial driver download that driver_fetch can continue; *have: bytes so far -int driver_prep(const install_opts *o, const WCHAR *zip, const WCHAR *accept); // elevated: stage the driver, arm the restart menu and the task (zip "-": the staged copy); 0 or DRV_E_*, 2 = UAC cancelled -int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept); // elevated, inside the install stage (setup.c): driver_prep for sid, no further elevation -int driver_bitlocker(void); // BitLocker protection of the system drive, not elevated: 1 on, 0 off / none, -1 cannot be told (needs administrator rights) -int driver_install(const install_opts *o); // elevated: pnputil, then clean up. 0 or DRV_E_*, 2 = UAC cancelled -int driver_cancel(const install_opts *o); // elevated: task, restart menu and BitLocker suspension back. 2 = UAC cancelled -BOOL driver_reboot(void); // restarts Windows (not elevated) -int driver_ci_state(void); // DRV_CI_* -// The driver's entries in the verb table (setup.c). The elevated three refuse a process that is not elevated (RC_FAIL). -int cmd_driver(cmdline *c); // /driver: the wizard at the driver pages -int cmd_driver_continue(cmdline *c); // /driver-continue: the same, after the restart -int cmd_driver_prep(cmdline *c); // /driver-prep [/unverified ] -int cmd_driver_install(cmdline *c); // /driver-install -int cmd_driver_cancel(cmdline *c); // /driver-cancel -void driver_machine_stage(const WCHAR *installed_setup); // elevated install: the Start menu shortcut "Install Dyna Pro USB driver" -int drv_file_count(void); // the files that get installed (pinned: name below "Dyna Pro USB\", size, SHA-256) -int drv_file_find(const WCHAR *rel); // index of that name (any case, / or \), or -1 -BOOL drv_file_info(int i, const WCHAR **rel, DWORD *size, const WCHAR **sha); -BOOL drv_file_match(int i, DWORD size, const WCHAR *sha); // is a file of this size and SHA-256 the pinned file i? -int drv_check_dir(const WCHAR *dir, int *differ, int *missing); // how many pinned files below dir\Dyna Pro USB\ are right; the others differ / are missing -BOOL drv_inf_matches(const BYTE *data, DWORD n); // pure helpers, unit-tested (tests\test_driver.c) -BOOL drv_ci_enforced(DWORD code_integrity_options); -BOOL drv_bde_suspend(int protection_status, int conversion_status, int key_protectors); // BDE_PS_* (driver.c), conversion status and protector count (-1 unknown): suspend BitLocker? A volume mid-conversion with protectors is suspended although it reads "off" -BOOL drv_bde_suspend_now(int protection_status, int conversion_status, int key_protectors, BOOL suspended_by_us); // ... at prep time: also when our own flag says we suspended it earlier (a retry) -int drv_task_xml(WCHAR *out, int cch, const WCHAR *sid, const WCHAR *exe); -BOOL drv_sys_tool(const WCHAR *windir, BOOL wow64, const WCHAR *tool, WCHAR *out, int cch); -BOOL drv_pnputil_ok(DWORD code); -BOOL drv_pnputil_args(WCHAR *out, int cch, const WCHAR *inf); -void driver_machine_uninstall(void); // elevated uninstall: the shortcut, a waiting task, BitLocker resumed if we suspended it, the staged files (an installed driver is left); call before the DynaRunFix key is deleted +#include "drf_app.h" +#include "drf_util.h" +#include "drf_package.h" +#include "drf_driver.h" +#include "drf_update.h" +#include "drf_wizard.h" diff --git a/src/wizard.c b/src/wizard.c index b938d44..f85289b 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -15,7 +15,9 @@ #include #include #include -#include "setup.h" +#include "drf_app.h" +#include "drf_package.h" +#include "drf_driver.h" #include "version.h" #define WIDEN2(x) L##x diff --git a/tests/test_driver.c b/tests/test_driver.c index de6ab81..ee6ed7e 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -10,6 +10,7 @@ #define WinMainCRTStartup setup_main_not_used_here #include "../src/setup.c" #include "../src/driver.c" +#include "../src/drf_package_internal.h" #include "zip_extract_all.h" #include diff --git a/tests/test_hardening.c b/tests/test_hardening.c index b8036d4..576ad62 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -20,6 +20,7 @@ static BOOL WINAPI test_ShellExecuteExW(SHELLEXECUTEINFOW *se) } #define ShellExecuteExW test_ShellExecuteExW #include "../src/setup.c" +#include "../src/drf_package_internal.h" static trust_ctx g_tc; // the user SID and the exe trust of the checks below (a stage of the installer makes its own) #include "promote_com.h" #include From 32c46455080c86736ad29a1826f21b6fab08e0a4 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:44:24 +0800 Subject: [PATCH 24/41] Wizard: job table instead of job switches JOBS[] holds, per job, the worker proc, the on_done handler and the two flags that used to be enum-order comparisons (keep the wizard behind the UAC/msiexec windows, bring it back to the front when done). job_proc and job_done no longer switch. test_job_table checks the table: a row with proc and on_done for every job, procs that return outside a comment, job_done still takes the close consent back. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 232 ++++++++++++++++++++++++++------------------ tests/test_driver.c | 51 +++++++--- 2 files changed, 175 insertions(+), 108 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index f85289b..f15a75b 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -26,7 +26,7 @@ enum { ID_TITLE = 100, ID_BODY, ID_EDIT, ID_RICH, ID_CHECK, ID_PROG, ID_STATUS, ID_EXTRA, ID_SECOND, ID_PRIMARY, ID_KEEPUP, ID_PIN, ID_AUTO, ID_DRV, ID_IMG, ID_INFO, ID_THIRD }; enum { P_HELLO, P_FETCH, P_FAIL, P_PASSWORD, P_EXTRACT, P_CHECK, P_LICENSE, P_RUNNING, P_INSTALL, P_DONE, P_ERROR, P_DRV_INTRO, P_DRV_PRESS7, P_DRV_HASH, P_DRV_BDE, P_DRV_PREP, P_DRV_REBOOT, P_DRV_RETRY, P_DRV_WARN, P_DRV_INSTALL, P_DRV_DONE, P_DRV_ERROR }; // driver.c -enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, JOB_DRV_INSTALL, JOB_DRV_CANCEL }; +enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, JOB_DRV_INSTALL, JOB_DRV_CANCEL, JOB_COUNT }; // the order is not used anywhere: JOBS[] says what each job is #define WM_PROGRESS (WM_APP + 1) #define WM_JOBDONE (WM_APP + 2) #define WM_HINT (WM_APP + 3) // the DynaRun update hint is known (wParam: 0 none, 1 newer version, 2 the site's file differs) @@ -723,35 +723,70 @@ static void on_progress(DWORD done, DWORD total) PostMessageW(g_hwnd, WM_PROGRESS, done, total); } -static DWORD WINAPI job_proc(LPVOID unused) +// What a job runs on the worker thread (job_proc), and what is done with its result on the UI thread (JOBS[].on_done, below). +static int jp_acquire(void) // the DynaRun setup or the driver: a copy on this computer first, else the download { - int rc = RC_FAIL; - switch (g_job) { - case JOB_ACQUIRE: // the DynaRun setup or the driver: a copy on this computer first, else the download - on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts - if (g_acq == &ACQ_DRIVER) { - rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); - if (rc == PK_OK || rc == DRV_W_HASH) { - on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked - g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds - } - } else { - g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); - if (g_pkg_found) rc = PK_OK; - else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } + int rc; + on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts + if (g_acq == &ACQ_DRIVER) { + rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + if (rc == PK_OK || rc == DRV_W_HASH) { + on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked + g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds } - break; - case JOB_EXTRACT: rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); SecureZeroMemory(g_pw, sizeof(g_pw)); break; // a size cap, and a free target (shared with the driver) // the password is not needed any more - case JOB_INSTALL_ALL: rc = g_pre ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); break; // g_pre: the driver's preparation in the same elevated stage - case JOB_INSTALL_FIX: rc = g_pre ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); break; - case JOB_DRV_PREP: - // "install anyway" accepts exactly this zip (its SHA-256), nothing else. (No comment after the call on the line of a break: it once swallowed the - // break, and the install followed the prep at once, without the restart.) - rc = driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); - break; - case JOB_DRV_INSTALL: rc = driver_install(&g_jobopts); break; - case JOB_DRV_CANCEL: rc = driver_cancel(&g_jobopts); break; + } else { + g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); + if (g_pkg_found) rc = PK_OK; + else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } } + return rc; +} +static int jp_extract(void) +{ + int rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); // a size cap, and a free target (shared with the driver) + SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more + return rc; +} +static int jp_install_all(void) { return g_pre ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); } // g_pre: the driver's preparation in the same elevated stage +static int jp_install_fix(void) { return g_pre ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); } +static int jp_drv_prep(void) +{ + // "install anyway" accepts exactly this zip (its SHA-256), nothing else. + return driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); +} +static int jp_drv_install(void) { return driver_install(&g_jobopts); } +static int jp_drv_cancel(void) { return driver_cancel(&g_jobopts); } + +static void jd_acquire(int job, int rc); +static void jd_extract(int job, int rc); +static void jd_install(int job, int rc); +static void jd_drv_prep(int job, int rc); +static void jd_drv_install(int job, int rc); +static void jd_drv_cancel(int job, int rc); + +// One row per job, indexed by the JOB_* value (nothing depends on the order of the values). +// proc runs on the worker thread; returns the job's result code +// on_done runs on the UI thread when the worker has ended (job_done), with the job and its result +// keep_up the elevated stage or a system tool works: the UAC prompt and msiexec's window take the foreground, so ID_KEEPUP keeps the wizard behind them +// bring_front the wizard comes back in front of other windows when the job is done (the elevated part allows it) +typedef struct { + int (*proc)(void); + void (*on_done)(int job, int rc); + BOOL keep_up, bring_front; +} job_desc; +static const job_desc JOBS[JOB_COUNT] = { + [JOB_ACQUIRE] = { jp_acquire, jd_acquire, FALSE, FALSE }, + [JOB_EXTRACT] = { jp_extract, jd_extract, FALSE, FALSE }, + [JOB_INSTALL_ALL] = { jp_install_all, jd_install, TRUE, TRUE }, + [JOB_INSTALL_FIX] = { jp_install_fix, jd_install, TRUE, TRUE }, + [JOB_DRV_PREP] = { jp_drv_prep, jd_drv_prep, TRUE, TRUE }, + [JOB_DRV_INSTALL] = { jp_drv_install, jd_drv_install, TRUE, TRUE }, + [JOB_DRV_CANCEL] = { jp_drv_cancel, jd_drv_cancel, TRUE, TRUE }, +}; + +static DWORD WINAPI job_proc(LPVOID unused) +{ + int rc = JOBS[g_job].proc(); PostMessageW(g_hwnd, WM_JOBDONE, g_job, rc); (void)unused; return 0; @@ -776,7 +811,7 @@ static void start_job(int job, int page) set_page(page); g_jobopts = g_opts; // the job runs with the options as they are now g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); - if (job == JOB_INSTALL_ALL || job == JOB_INSTALL_FIX || job >= JOB_DRV_PREP) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); + if (JOBS[job].keep_up) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); } /* ---------- steps ---------- */ @@ -1190,74 +1225,87 @@ static void choose_driver_source(void) acq_driver_result(rc); } +static void jd_acquire(int job, int rc) { (void)job; acq_done(rc); } + +static void jd_extract(int job, int rc) +{ + (void)job; + if (rc == PK_OK) { after_extract(); return; } + drop_msi(); // a failed or cancelled extraction leaves no decrypted copy + if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); return; } + if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page + WCHAR nm[100]; + lstrcpynW(nm, file_name(g_pkg), 100); + pkg_reject(g_pkg); + g_acq = &ACQ_SETUP; g_pkg_found = FALSE; + wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); + set_page(P_FAIL); + return; + } + if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } + lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); + set_page(P_ERROR); +} + +static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX +{ + if (g_pre && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) + cleanup_files(); + g_fix_done = TRUE; + if (rc == DRV_SUCCESS) drv_reboot_now(); + else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); + else drv_error(rc); + return; + } + if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } + if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile + if (rc == RC_CANCEL) { + lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); + set_page(g_pre ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // g_pre: the page with the button that started it + return; + } + wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", + L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); + set_page(P_ERROR); +} + +static void jd_drv_prep(int job, int rc) +{ + (void)job; + if (rc == DRV_SUCCESS) { drv_reboot_now(); return; } + if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides + if (rc == RC_CANCEL) { + lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); + set_page(g_drvmode == DRV_MODE_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); + return; + } + drv_error(rc); +} + +static void jd_drv_install(int job, int rc) +{ + (void)job; + if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); return; } + if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } + if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed + lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); + set_page(P_DRV_RETRY); return; + } + drv_error(rc); +} + +static void jd_drv_cancel(int job, int rc) +{ + (void)job; (void)rc; + DestroyWindow(g_hwnd); // cancelled (or not allowed to: nothing else can be done from here) +} + static void job_done(int job, int rc) { WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; if (g_close_asked) { g_opts.close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) if (g_closing) { DestroyWindow(g_hwnd); return; } - switch (job) { - case JOB_ACQUIRE: acq_done(rc); break; - case JOB_EXTRACT: - if (rc == PK_OK) { after_extract(); break; } - drop_msi(); // a failed or cancelled extraction leaves no decrypted copy - if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); break; } - if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page - WCHAR nm[100]; - lstrcpynW(nm, file_name(g_pkg), 100); - pkg_reject(g_pkg); - g_acq = &ACQ_SETUP; g_pkg_found = FALSE; - wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); - set_page(P_FAIL); - break; - } - if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); break; } - lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); - set_page(P_ERROR); - break; - case JOB_INSTALL_ALL: - case JOB_INSTALL_FIX: - if (g_pre && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) - cleanup_files(); - g_fix_done = TRUE; - if (rc == DRV_SUCCESS) drv_reboot_now(); - else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); - else drv_error(rc); - break; - } - if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); break; } - if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); break; } // DynaRun was started meanwhile - if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); - set_page(g_pre ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // g_pre: the page with the button that started it - break; - } - wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", - L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); - set_page(P_ERROR); - break; - case JOB_DRV_PREP: - if (rc == DRV_SUCCESS) { drv_reboot_now(); break; } - if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); break; } // armed, but BitLocker is still on: the user decides - if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); - set_page(g_drvmode == DRV_MODE_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); - break; - } - drv_error(rc); - break; - case JOB_DRV_INSTALL: - if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); break; } - if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); break; } - if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed - lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); - set_page(P_DRV_RETRY); break; - } - drv_error(rc); - break; - case JOB_DRV_CANCEL: - DestroyWindow(g_hwnd); // cancelled (or not allowed to: nothing else can be done from here) - break; - } + JOBS[job].on_done(job, rc); } static void on_primary(void) @@ -1639,7 +1687,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) case WM_JOBDONE: KillTimer(h, ID_KEEPUP); // after the elevated part (which allows it) the wizard comes back in front of other windows - if (w == JOB_INSTALL_ALL || w == JOB_INSTALL_FIX || w >= JOB_DRV_PREP) bring_front(h); + if (JOBS[w].bring_front) bring_front(h); job_done((int)w, (int)l); return 0; case WM_PAINT: { PAINTSTRUCT ps; RECT c; HDC dc = BeginPaint(h, &ps); HPEN pen, old; diff --git a/tests/test_driver.c b/tests/test_driver.c index ee6ed7e..f2150cd 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -765,31 +765,50 @@ static void test_check(void) // the prep (no restart in between). Every `case JOB_...:` of job_proc in src\wizard.c must have a break outside of comments before the next one. static void test_job_table(void) { - char *t, *a, *end, *c, *next; DWORD n = 0; BYTE *f = read_file(L"src\\wizard.c", &n); int labels = 0; + char *t, *a, *end, *c, *next; DWORD n = 0; BYTE *f = read_file(L"src\\wizard.c", &n); int jobs = 0, rows = 0; if (!f) { wprintf(L"info: src\\wizard.c not found from this folder: the job table is not checked\n"); return; } t = (char *)f; - a = strstr(t, "static DWORD WINAPI job_proc"); end = a ? strstr(a, "WM_JOBDONE") : NULL; - CHECK(a && end, L"job_proc is in src\\wizard.c"); - for (c = a ? strstr(a, "case JOB_") : NULL; c && c < end; c = next) { - char *p, *stop; BOOL has_break = FALSE; - next = strstr(c + 5, "case JOB_"); stop = next && next < end ? next : end; - for (p = c; p < stop && !has_break; p++) { + // every job has a row in JOBS[] with a proc (jp_*) and an on_done (jd_*); a job without a row would call a NULL pointer + a = strstr(t, "enum { JOB_ACQUIRE"); end = a ? strchr(a, '}') : NULL; + CHECK(a && end, L"the JOB_* enum is in src\\wizard.c"); + for (c = a; c && end && c < end; c++) if (!strncmp(c, "JOB_", 4) && strncmp(c, "JOB_COUNT", 9)) { jobs++; while (c < end && *c != ',' && *c != ' ') c++; } + CHECK(jobs >= 7, L"all seven jobs were looked at"); + a = strstr(t, "static const job_desc JOBS["); end = a ? strstr(a, "};") : NULL; + CHECK(a && end, L"the job table JOBS[] is in src\\wizard.c"); + for (c = a ? strstr(a, "= {") : NULL, c = c ? strstr(c, "[JOB_") : NULL; c && c < end; c = next) { + char *row_end; BOOL has_proc, has_done; + next = strstr(c + 5, "[JOB_"); if (next && next >= end) next = NULL; + row_end = next ? next : end; + has_proc = FALSE; has_done = FALSE; + for (a = c; a < row_end; a++) { if (!strncmp(a, "jp_", 3)) has_proc = TRUE; if (!strncmp(a, "jd_", 3)) has_done = TRUE; } + CHECK(has_proc && has_done, L"every row of JOBS[] names a proc (jp_*) and an on_done (jd_*)"); + rows++; + if (!next) break; + } + CHECK(rows == jobs, L"JOBS[] has exactly one row per job"); + // each proc ends with a return that is not inside a comment (a comment once swallowed the break that ended a job's case) + for (c = t, rows = 0; (c = strstr(c, "static int jp_")) != NULL; c += 14) { + char *p, *stop; BOOL has_return = FALSE; + stop = strstr(c, "\nstatic "); if (!stop) stop = t + n; + for (p = c; p < stop && !has_return; p++) { if (p[0] == '/' && p[1] == '/') { while (p < stop && *p != '\n') p++; continue; } // a comment runs to the end of its line - if (!strncmp(p, "break;", 6)) has_break = TRUE; + if (!strncmp(p, "return ", 7)) has_return = TRUE; } - CHECK(has_break, L"every case of job_proc ends with a break that is not inside a comment"); - labels++; - if (!(next && next < end)) break; + CHECK(has_return, L"every job proc returns its result code outside of a comment"); + rows++; } - CHECK(labels >= 7, L"all seven jobs were looked at"); + CHECK(rows == jobs, L"every job has its own proc function"); + // job_proc and job_done go through the table + a = strstr(t, "static DWORD WINAPI job_proc"); end = a ? strstr(a, "WM_JOBDONE") : NULL; + CHECK(a && end && strstr(a, "JOBS[g_job].proc()") && strstr(a, "JOBS[g_job].proc()") < end, L"job_proc runs the proc of the job's row"); // g_opts.close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it // records that (g_close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since - a = strstr(t, "static void job_done"); end = a ? strstr(a, "switch (job)") : NULL; - CHECK(a && end, L"job_done is in src\\wizard.c"); + a = strstr(t, "static void job_done"); end = a ? strstr(a, "JOBS[job].on_done") : NULL; + CHECK(a && end, L"job_done is in src\\wizard.c and hands the result to the job's on_done"); for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_opts.close = FALSE", 20)) break; CHECK(a && end && c < end, L"job_done takes back the \"close DynaRun\" consent of the job that has ended"); - for (c = t, labels = 0; (c = strstr(c, "g_opts.close = TRUE")) != NULL; c += 19) labels++; - CHECK(labels == 1 && strstr(t, "g_opts.close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_opts.close only together with g_close_asked"); + for (c = t, jobs = 0; (c = strstr(c, "g_opts.close = TRUE")) != NULL; c += 19) jobs++; + CHECK(jobs == 1 && strstr(t, "g_opts.close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_opts.close only together with g_close_asked"); release(f); } From 3bbb0386c685016367724726f9485acdbc8d01f3 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:46:07 +0800 Subject: [PATCH 25/41] Wizard: one flow value instead of the g_pre/g_driver_only/g_drvmode combination g_flow (FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE) replaces g_pre, g_driver_only and the later reads of g_drvmode, which is now read once at the start of wizard(). g_fix_done stays: it is progress, not flow. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 60 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index f15a75b..1ba8905 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -50,10 +50,19 @@ static DWORD g_neterr, g_total; static BOOL g_paused, g_pause_req; // download paused (worker ended, .part kept) / pause asked, worker still stopping static int g_hint; // DynaRun update hint (pkg_update_hint): 0 none, 1 a newer version is published, 2 the site's zip differs from the last download static WCHAR g_hint_inst[40], g_hint_new[40]; // the installed and the published version (hint 1) -static BOOL g_pre; // the driver is ticked in the main flow: its package is fetched and checked BEFORE the install, and the one elevated stage installs the fix and prepares the driver (one UAC prompt) -static int g_inst_job; // the install job (JOB_INSTALL_ALL / JOB_INSTALL_FIX) that waits for the driver package while g_pre -static BOOL g_fix_done; // g_pre: the fix is installed (the driver step after it may still have failed) -static BOOL g_driver_only; // DynaRun and its fix are installed (this version) and only the driver is missing: the first page offers just that +// Which way through the pages this run takes. One value; it replaces the three flags (the driver ticked on the first page, the driver-only first page, the +// /driver and /driver-continue command line) whose combinations were never all possible. It changes only in start (wizard), go_install, skip_driver and the +// "Reinstall the fix..." link. +// FLOW_MAIN the fix (and, when DynaRun is not installed, DynaRun itself): the first page, no driver in it. The default. +// FLOW_MAIN_WITH_DRIVER the driver is ticked on the first page: its package is fetched and checked BEFORE the install, and the one elevated stage installs the +// fix and prepares the driver (one UAC prompt). The user can still skip it (back to FLOW_MAIN). +// FLOW_DRIVER_ONLY only the driver is the job: /driver (straight to the driver pages), or DynaRun and its fix are installed (this version) and only the +// driver is missing (the first page offers just that, and "Reinstall the fix..." turns it into FLOW_MAIN). +// FLOW_DRIVER_CONTINUE /driver-continue, the sign-in task after the restart: the driver pages that follow the restart. +enum { FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE }; +static int g_flow = FLOW_MAIN; +static int g_inst_job; // the install job (JOB_INSTALL_ALL / JOB_INSTALL_FIX) that waits for the driver package in FLOW_MAIN_WITH_DRIVER +static BOOL g_fix_done; // progress, not flow: FLOW_MAIN_WITH_DRIVER, the fix is installed (the driver step after it may still have failed) static BOOL g_checking, g_marquee, g_drv_allow; // the non-pausable "checking" phase / marquee bar (size unknown) / the user chose "install anyway" for a zip with another hash static int g_drv_how; // DRV_SRC_*: the driver zip was downloaded, or found on this computer (nothing downloaded) static WCHAR g_drv_hex[65]; // SHA-256 and size of a driver zip that is not the pinned file (the warning page shows them) @@ -319,7 +328,7 @@ static int place_image(int x, int y, int w, int ah) // The "Start DynaRun" button is offered only when DynaRun is installed here and our launcher is in place (g_exe comes from locate_dynarun, which // the wizard runs at start in every mode, so this also holds in the process the sign-in task starts after the restart; g_launcher from init_paths). // The driver step may be skipped (the fix is installed without it) only in the main install flow, not when it is the whole job (the shortcut, /driver, or the driver-only first page). -static BOOL can_skip_driver(void) { return g_drvmode == DRV_MODE_NONE && !g_driver_only; } +static BOOL can_skip_driver(void) { return g_flow == FLOW_MAIN || g_flow == FLOW_MAIN_WITH_DRIVER; } // The line about a newer DynaRun on the first page, and its button. A hint only: nothing happens unless the button is clicked. static void hint_line(WCHAR *b) @@ -373,7 +382,7 @@ static void set_page(int p) vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); switch (p) { case P_HELLO: - if (g_driver_only) { + if (g_flow == FLOW_DRIVER_ONLY) { wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", @@ -544,7 +553,7 @@ static void set_page(int p) L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" L"下次重新開機會自動恢復。\n\n" L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - if (g_pre) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); text(ID_BODY, b); buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); @@ -563,7 +572,7 @@ static void set_page(int p) lstrcatW(b, t); } if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); - if (g_pre) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); @@ -574,7 +583,7 @@ static void set_page(int p) text(ID_BODY, b); } show_image(0); - if (g_pre) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet + if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); if (g_note[0]) status(g_note, TRUE); break; @@ -747,8 +756,8 @@ static int jp_extract(void) SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more return rc; } -static int jp_install_all(void) { return g_pre ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); } // g_pre: the driver's preparation in the same elevated stage -static int jp_install_fix(void) { return g_pre ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); } +static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage +static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); } static int jp_drv_prep(void) { // "install anyway" accepts exactly this zip (its SHA-256), nothing else. @@ -836,10 +845,10 @@ static void install(int job) } // The install of the fix (job: JOB_INSTALL_ALL / JOB_INSTALL_FIX). With the driver ticked its package is fetched and checked first (the driver pages), and the -// one elevated stage then does both: the UAC prompt comes once, before the restart. The driver-only first page (g_driver_only) has no install: it starts the prep itself. +// one elevated stage then does both: the UAC prompt comes once, before the restart. The driver-only first page (FLOW_DRIVER_ONLY) has no install: it starts the prep itself. static void go_install(int job) { - if (g_drv && !g_driver_only && g_drvmode == DRV_MODE_NONE && !g_pre) { g_pre = TRUE; g_inst_job = job; g_note[0] = 0; set_page(P_DRV_INTRO); } + if (g_drv && g_flow == FLOW_MAIN) { g_flow = FLOW_MAIN_WITH_DRIVER; g_inst_job = job; g_note[0] = 0; set_page(P_DRV_INTRO); } else install(job); } @@ -847,7 +856,7 @@ static void go_install(int job) static void skip_driver(void) { user_log(L"driver: skipped, the fix is installed without it"); - g_drv = FALSE; g_opts.autostart_later = FALSE; g_pre = FALSE; + g_drv = FALSE; g_opts.autostart_later = FALSE; g_flow = FLOW_MAIN; install(g_inst_job); } @@ -1059,7 +1068,7 @@ static void drv_start(void) return; } g_drv_have = driver_installed(); - if (g_drvmode == DRV_MODE_CONTINUE) { + if (g_flow == FLOW_DRIVER_CONTINUE) { if (g_drv_have) { if (is_admin()) driver_cancel(&g_opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes else set_page(driver_ci_state() == DRV_CI_ENFORCED ? P_DRV_RETRY : P_DRV_WARN); } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); @@ -1249,7 +1258,7 @@ static void jd_extract(int job, int rc) static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX { - if (g_pre && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) + if (g_flow == FLOW_MAIN_WITH_DRIVER && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) cleanup_files(); g_fix_done = TRUE; if (rc == DRV_SUCCESS) drv_reboot_now(); @@ -1261,7 +1270,7 @@ static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INS if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); - set_page(g_pre ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // g_pre: the page with the button that started it + set_page(g_flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it return; } wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", @@ -1276,7 +1285,7 @@ static void jd_drv_prep(int job, int rc) if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); - set_page(g_drvmode == DRV_MODE_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); + set_page(g_flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); return; } drv_error(rc); @@ -1314,7 +1323,7 @@ static void on_primary(void) case P_HELLO: g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; - if (g_driver_only) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver + if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } @@ -1340,7 +1349,7 @@ static void on_primary(void) break; case P_ERROR: DestroyWindow(g_hwnd); break; case P_DRV_INTRO: acq_start(&ACQ_DRIVER); break; - case P_DRV_PRESS7: if (g_pre) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); break; // g_pre: the fix and the driver preparation in one elevated stage + case P_DRV_PRESS7: if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); break; // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage case P_DRV_HASH: hash_choice(FALSE); break; // "Cancel" is the default button case P_DRV_RETRY: g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); break; // the staged driver is still there case P_DRV_WARN: start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); break; @@ -1366,7 +1375,7 @@ static void on_close(void) if (g_page == P_INSTALL || g_page == P_DRV_PREP || g_page == P_DRV_INSTALL) return; // msiexec / registry / system tool work must not be interrupted if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } // after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) - if (g_drvmode == DRV_MODE_CONTINUE && (g_page == P_DRV_RETRY || g_page == P_DRV_WARN) && is_admin()) driver_cancel(&g_opts); + if (g_flow == FLOW_DRIVER_CONTINUE && (g_page == P_DRV_RETRY || g_page == P_DRV_WARN) && is_admin()) driver_cancel(&g_opts); if (g_page == P_DRV_BDE) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); return; } // the task, the menu flag and the suspension were armed: take them back first DestroyWindow(g_hwnd); } @@ -1392,7 +1401,7 @@ static void on_third(void) else if (g_page == P_HELLO && g_hint) { // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_drv = !g_driver_only && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; g_opts.autostart_later = g_drv; user_log(L"DynaRun update hint: the user chose to download the new version"); g_acq_fresh = TRUE; // an older package found on this computer would not be the new version @@ -1627,7 +1636,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) if (g_page == P_FAIL || g_page == P_FETCH || g_page == P_CHECK || g_page == P_LICENSE) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); else if (g_page == P_DRV_BDE) ShellExecuteW(h, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); else if (g_page == P_PASSWORD) choose_file(); - else if (g_page == P_HELLO) { if (g_driver_only) { g_driver_only = FALSE; set_page(P_HELLO); } else choose_exe(); } // "Reinstall the fix..." / "Installed elsewhere..." + else if (g_page == P_HELLO) { if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } else choose_exe(); } // "Reinstall the fix..." / "Installed elsewhere..." else if (g_page == P_DRV_ERROR) ShellExecuteW(h, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); else if (g_page == P_DRV_HASH) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); // after a failed driver download break; @@ -1744,7 +1753,10 @@ int wizard(const install_opts *init) create_controls(); g_installed = locate_dynarun(); g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); - g_driver_only = g_installed && g_drv_avail && fix_current(); // the fix is there and current: only the driver is missing + // g_drvmode (set by cmd_driver / cmd_driver_continue before the wizard starts) is read here and nowhere else + if (g_drvmode == DRV_MODE_STANDALONE) g_flow = FLOW_DRIVER_ONLY; + else if (g_drvmode == DRV_MODE_CONTINUE) g_flow = FLOW_DRIVER_CONTINUE; + else g_flow = g_installed && g_drv_avail && fix_current() ? FLOW_DRIVER_ONLY : FLOW_MAIN; // the fix is there and current: only the driver is missing if (g_drvmode != DRV_MODE_NONE) drv_start(); else { DWORD tid; From 0a83fb0d6adff44650238f3f7fa67b97564c3267 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:51:51 +0800 Subject: [PATCH 26/41] Wizard: page table, one enter function per page set_page (a 350-line switch) is split into one enter_ function per page, and PAGES[] has one row per page: enter, primary, second, third, extra, the close hook, the page's Alt+key shortcuts and PF_NOCLOSE. on_primary, on_second, on_third, ID_EXTRA, on_close and the Alt+key handling in the message loop go through the row. A new page is a P_* value, an enter_* function and a row. Behaviour is unchanged: every page keeps the same buttons, texts, shortcuts and close behaviour. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 1003 ++++++++++++++++++++++++++++---------------------- 1 file changed, 570 insertions(+), 433 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index 1ba8905..5514155 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -25,7 +25,8 @@ enum { ID_TITLE = 100, ID_BODY, ID_EDIT, ID_RICH, ID_CHECK, ID_PROG, ID_STATUS, ID_EXTRA, ID_SECOND, ID_PRIMARY, ID_KEEPUP, ID_PIN, ID_AUTO, ID_DRV, ID_IMG, ID_INFO, ID_THIRD }; enum { P_HELLO, P_FETCH, P_FAIL, P_PASSWORD, P_EXTRACT, P_CHECK, P_LICENSE, P_RUNNING, P_INSTALL, P_DONE, P_ERROR, - P_DRV_INTRO, P_DRV_PRESS7, P_DRV_HASH, P_DRV_BDE, P_DRV_PREP, P_DRV_REBOOT, P_DRV_RETRY, P_DRV_WARN, P_DRV_INSTALL, P_DRV_DONE, P_DRV_ERROR }; // driver.c + P_DRV_INTRO, P_DRV_PRESS7, P_DRV_HASH, P_DRV_BDE, P_DRV_PREP, P_DRV_REBOOT, P_DRV_RETRY, P_DRV_WARN, P_DRV_INSTALL, P_DRV_DONE, P_DRV_ERROR, // driver.c + P_COUNT }; enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, JOB_DRV_INSTALL, JOB_DRV_CANCEL, JOB_COUNT }; // the order is not used anywhere: JOBS[] says what each job is #define WM_PROGRESS (WM_APP + 1) #define WM_JOBDONE (WM_APP + 2) @@ -93,7 +94,22 @@ static BOOL g_drv_avail, g_drv, g_drv_have, g_drv_noreboot; // g_drv_have: alr static WCHAR g_drvzip[MAX_PATH]; // the checked zip ("" = the staged copy, after a restart) static HBITMAP g_bmp[2], g_shown; // the two screenshots (decoded once) and the scaled one in the picture control static int g_bw[2], g_bh[2], g_img, g_shown_img = -1, g_shown_w, g_shown_h; +// What one page is (PAGES[], below): its builder, what its buttons do, what closing the window does. +typedef struct { UINT key; int id; BOOL (*when)(void); } hotkey; // Alt+key clicks the control id (when is NULL, or says the shortcut is on) +enum { PF_NOCLOSE = 1 }; +typedef struct { + void (*enter)(void); + void (*primary)(void); + void (*second)(BOOL button); + void (*third)(void); + void (*extra)(void); + void (*close)(void); + const hotkey *keys; + DWORD flags; +} page_desc; +static const page_desc PAGES[P_COUNT]; static void layout(void); +static void set_page(int p); static void place(int id, int x, int y, int w, int h); #define S(x) MulDiv((x), g_dpi, 96) #define H(id) GetDlgItem(g_hwnd, (id)) @@ -102,6 +118,7 @@ static void place(int id, int x, int y, int w, int h); static void vis(int id, BOOL on) { ShowWindow(H(id), on ? SW_SHOW : SW_HIDE); } static void text(int id, const WCHAR *t) { SetWindowTextW(H(id), t ? t : L""); vis(id, t != NULL); } +static WCHAR pgbuf[2600]; // the text of the page being built (one page at a time, on the UI thread) static int g_sth = -1; // room the layout reserved for the status line (its measured height, 0 when hidden) static BOOL g_building; // set_page is rebuilding the page: it lays out once at its end static int status_need(void); @@ -371,356 +388,392 @@ static void acp_warning(void) L"請到「設定 → 時間與語言 → 語言與地區 → 系統管理語言設定 → 變更系統地區設定」選「中文(繁體,台灣)」,再重新開機。", TRUE); } -static void set_page(int p) +static void enter_hello(void) { - static WCHAR b[2600]; - g_page = p; - g_paused = g_pause_req = FALSE; - g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress - g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does - KillTimer(g_hwnd, TIMER_RUNNING); - vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); - switch (p) { - case P_HELLO: - if (g_flow == FLOW_DRIVER_ONLY) { - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " - L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" - L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", - L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" - L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" - L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); - hint_line(b); - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); - text(ID_BODY, b); - buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); - if (g_note[0]) status(g_note, TRUE); - break; - } - if (g_installed) { - text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " - L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" - L"When Windows asks whether to allow changes, click \"Yes\".%s", - L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" - L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); - hint_line(b); - text(ID_BODY, b); - buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); - } else { - text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); - wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" - L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" - L" 2. install DynaRun V3\n" - L" 3. add the fixes for Windows 10/11\n\n" - L"You need the setup password that Dyna Pro gave you.%s", - L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" - L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" - L" 2. 安裝 DynaRun V3\n" - L" 3. 加上 Windows 10/11 的相容性修正\n\n" - L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s"), disclaimer()); - text(ID_BODY, b); - buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); - } - vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); - if (g_note[0]) status(g_note, TRUE); - break; - case P_FETCH: // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver - wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); - text(ID_TITLE, b); - fetch_body(0); - progress(FALSE); status(L" ", FALSE); - buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); - break; - case P_FAIL: // the one error page of the acquire flow - if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); - else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); - if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence - text(ID_TITLE, b); - { - BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) - BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); - g_badfile = FALSE; - wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" - L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", - L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" - L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); - text(ID_BODY, b); - buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), - T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); - } - break; - case P_PASSWORD: - text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); - wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", - L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); + WCHAR *b = pgbuf; + if (g_flow == FLOW_DRIVER_ONLY) { + wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " + L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" + L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", + L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" + L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" + L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); + hint_line(b); + text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); text(ID_BODY, b); - vis(ID_EDIT, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); + buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); if (g_note[0]) status(g_note, TRUE); - SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); - break; - case P_EXTRACT: - text(ID_TITLE, T(L"Opening the setup file", L"正在打開安裝檔")); - text(ID_BODY, T(L"Please wait...", L"請稍候…")); - progress(FALSE); - buttons(NULL, T(L"Cancel", L"取消"), NULL); - break; - case P_CHECK: // everything we know about the setup file, to compare with what Dyna Pro gives - text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); - text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", - L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); - SetWindowTextW(H(ID_INFO), g_info); - vis(ID_INFO, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); - break; - case P_LICENSE: - text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); - lstrcpyW(b, g_nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", - L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") - : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); + return; + } + if (g_installed) { + text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); + wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " + L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" + L"When Windows asks whether to allow changes, click \"Yes\".%s", + L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" + L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); + hint_line(b); text(ID_BODY, b); - SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") - : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); - vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); - EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); - SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus - if (g_note[0]) status(g_note, TRUE); - break; - case P_RUNNING: { - WCHAR names[200]; - running_names(g_lastrun = running_programs(), names); - text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); - if (g_lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help - wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" - L"Ask that user to close DynaRun; this page then offers \"Install\".", - L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" - L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。"), names); - text(ID_BODY, b); - buttons(NULL, T(L"Cancel", L"取消"), NULL); - } else { - wsprintfW(b, T(L"DynaRun must be closed to install:\n%s\n" - L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", - L"安裝前必須關閉 DynaRun:\n%s\n" - L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。"), names); - text(ID_BODY, b); - buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); - } - if (g_note[0]) status(g_note, TRUE); - SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); - break; + buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); + } else { + text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); + wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" + L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" + L" 2. install DynaRun V3\n" + L" 3. add the fixes for Windows 10/11\n\n" + L"You need the setup password that Dyna Pro gave you.%s", + L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" + L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" + L" 2. 安裝 DynaRun V3\n" + L" 3. 加上 Windows 10/11 的相容性修正\n\n" + L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s"), disclaimer()); + text(ID_BODY, b); + buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); } - case P_INSTALL: - text(ID_TITLE, T(L"Installing", L"安裝中")); - text(ID_BODY, T(L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); - break; - case P_DONE: - text(ID_TITLE, T(L"Done", L"完成")); - lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" - L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", - L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" - L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); - pin_hint(b); - if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); - lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); + vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_fetch(void) // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver +{ + WCHAR *b = pgbuf; + wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); + text(ID_TITLE, b); + fetch_body(0); + progress(FALSE); status(L" ", FALSE); + buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); +} + +static void enter_fail(void) // the one error page of the acquire flow +{ + WCHAR *b = pgbuf; + if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); + else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); + if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence + text(ID_TITLE, b); + { + BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) + BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); + g_badfile = FALSE; + wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" + L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", + L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" + L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); text(ID_BODY, b); - buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); - acp_warning(); - break; - case P_ERROR: - text(ID_TITLE, T(L"Something went wrong", L"發生問題")); - text(ID_BODY, g_note); - buttons(T(L"Close", L"關閉"), NULL, NULL); - break; - case P_DRV_INTRO: - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); - lstrcpyW(b, T(L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" - L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" - L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" - L" 3. come back by itself after you sign in and finish the installation\n\n" - L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " - L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" - L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " - L"It resumes by itself, after at most 3 restarts.", - L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" - L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" - L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" - L" 3. 登入後本程式會自動回來,完成安裝\n\n" - L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" - L"下次重新開機會自動恢復。\n\n" - L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", - L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); + buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), + T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); + } +} + +static void enter_password(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); + wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", + L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); + text(ID_BODY, b); + vis(ID_EDIT, TRUE); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); + if (g_note[0]) status(g_note, TRUE); + SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); +} + +static void enter_extract(void) +{ + text(ID_TITLE, T(L"Opening the setup file", L"正在打開安裝檔")); + text(ID_BODY, T(L"Please wait...", L"請稍候…")); + progress(FALSE); + buttons(NULL, T(L"Cancel", L"取消"), NULL); +} + +static void enter_check(void) // everything we know about the setup file, to compare with what Dyna Pro gives +{ + text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); + text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", + L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); + SetWindowTextW(H(ID_INFO), g_info); + vis(ID_INFO, TRUE); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_license(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); + lstrcpyW(b, g_nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", + L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") + : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); + text(ID_BODY, b); + SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") + : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); + vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); + EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); + SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_running(void) +{ + WCHAR *b = pgbuf; + WCHAR names[200]; + running_names(g_lastrun = running_programs(), names); + text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); + if (g_lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help + wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" + L"Ask that user to close DynaRun; this page then offers \"Install\".", + L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" + L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。"), names); text(ID_BODY, b); - buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); - break; - case P_DRV_PRESS7: - text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); - { - int bl = g_drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) - b[0] = 0; - if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); - else if (g_drv_how & DRV_SRC_LOCAL) { - WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm - if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); - else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); - lstrcatW(b, t); - } - if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", - L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); - lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", - L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); - if (bl > 0) lstrcatW(b, T(L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); - else if (bl < 0) lstrcatW(b, T(L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); - text(ID_BODY, b); - } - show_image(0); - if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet - else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); - if (g_note[0]) status(g_note, TRUE); - break; - case P_DRV_HASH: { // a driver zip that is not the file this program knows: the user decides - static WCHAR src[2 * MAX_PATH + 40]; - if (g_drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (g_drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, g_drvzip); } - else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); - text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); - // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to - // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost + buttons(NULL, T(L"Cancel", L"取消"), NULL); + } else { + wsprintfW(b, T(L"DynaRun must be closed to install:\n%s\n" + L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", + L"安裝前必須關閉 DynaRun:\n%s\n" + L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。"), names); + text(ID_BODY, b); + buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); + } + if (g_note[0]) status(g_note, TRUE); + SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); +} + +static void enter_install(void) +{ + text(ID_TITLE, T(L"Installing", L"安裝中")); + text(ID_BODY, T(L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_done(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Done", L"完成")); + lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" + L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", + L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" + L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); + pin_hint(b); + if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); + lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); + text(ID_BODY, b); + buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); + acp_warning(); +} + +static void enter_error(void) +{ + text(ID_TITLE, T(L"Something went wrong", L"發生問題")); + text(ID_BODY, g_note); + buttons(T(L"Close", L"關閉"), NULL, NULL); +} + +static void enter_drv_intro(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); + lstrcpyW(b, T(L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" + L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" + L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" + L" 3. come back by itself after you sign in and finish the installation\n\n" + L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " + L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" + L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " + L"It resumes by itself, after at most 3 restarts.", + L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" + L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" + L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" + L" 3. 登入後本程式會自動回來,完成安裝\n\n" + L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" + L"下次重新開機會自動恢復。\n\n" + L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", + L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); + text(ID_BODY, b); + buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_press7(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); + { + int bl = g_drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) b[0] = 0; - if (g_drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of - cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" - L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); - cat_to(b, 2600, src); - cat_to(b, 2600, L"\n\n"); - } else { - WCHAR sz[24]; - wsprintfW(sz, L"%lu", (DWORD)g_drv_size); - cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" - L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); - cat_to(b, 2600, src); - cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); - cat_to(b, 2600, sz); - cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); - cat_to(b, 2600, g_drv_hex); - cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); - cat_to(b, 2600, driver_pinned_sha256()); - cat_to(b, 2600, L"\n\n"); - } - cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); - { - int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line - const WCHAR *p = g_drv_rep; - int used = 0, more = 0; - while (*p) { - const WCHAR *e = p; int n; - while (*e && *e != L'\n') e++; - n = (int)(e - p) + (*e ? 1 : 0); // the line with its line feed - if (!more && used + n <= room) { lstrcpynW(b + lstrlenW(b), p, n + 1); used += n; } - else more++; - p += n; - } - if (more) { - WCHAR t[48]; - wsprintfW(t, T(L" (and %d more)", L" (另外還有 %d 個)"), more); - cat_to(b, 2600, t); - } + if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); + else if (g_drv_how & DRV_SRC_LOCAL) { + WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm + if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); + else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); + lstrcatW(b, t); } + if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", + L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); + lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", + L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); + if (bl > 0) lstrcatW(b, T(L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); + else if (bl < 0) lstrcatW(b, T(L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); text(ID_BODY, b); - // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left - buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); - break; } - case P_DRV_BDE: - text(ID_TITLE, T(L"BitLocker could not be suspended", L"BitLocker 沒有暫停成功")); - text(ID_BODY, T(L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " - L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " - L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", - L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" - L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。")); - buttons(T(L"Restart anyway", L"仍要重新開機"), T(L"Later", L"稍後"), T(L"Find the recovery key", L"查詢修復金鑰")); - break; - case P_DRV_PREP: - text(ID_TITLE, T(L"Please wait", L"請稍候")); - text(ID_BODY, T(L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); - break; - case P_DRV_REBOOT: - text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); - lstrcpyW(b, g_drv_noreboot - ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " - L"When the menu appears, press 7. After you sign in, this program continues by itself.", - L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") - : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" - L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", - L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); - if (g_fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped - text(ID_BODY, b); - buttons(T(L"Close", L"關閉"), NULL, NULL); - if (g_fix_done) acp_warning(); - break; - case P_DRV_RETRY: - text(ID_TITLE, T(L"7 was not pressed", L"剛才沒有按到 7")); - text(ID_BODY, T(L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" - L"Try again? The computer restarts once more. When the menu appears, press 7.", - L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" - L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); - buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); - break; - case P_DRV_WARN: - text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); - text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", - L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); - show_image(1); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); - break; - case P_DRV_INSTALL: - text(ID_TITLE, T(L"Installing the driver", L"正在安裝驅動程式")); - text(ID_BODY, T(L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); - break; - case P_DRV_DONE: - text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); - text(ID_BODY, g_drv_have - ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", - L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") - : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" - L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", - L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" - L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。")); - if (can_start_dynarun()) buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); // only after a success (this page); a failure shows P_DRV_ERROR - else buttons(T(L"Close", L"關閉"), NULL, NULL); - break; - case P_DRV_ERROR: - text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); - text(ID_BODY, g_note); - buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); - break; + show_image(0); + if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet + else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_hash(void) // a driver zip that is not the file this program knows: the user decides +{ + WCHAR *b = pgbuf; + static WCHAR src[2 * MAX_PATH + 40]; + if (g_drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (g_drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, g_drvzip); } + else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); + text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); + // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to + // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost + b[0] = 0; + if (g_drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of + cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" + L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, src); + cat_to(b, 2600, L"\n\n"); + } else { + WCHAR sz[24]; + wsprintfW(sz, L"%lu", (DWORD)g_drv_size); + cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" + L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, src); + cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); + cat_to(b, 2600, sz); + cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); + cat_to(b, 2600, g_drv_hex); + cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); + cat_to(b, 2600, driver_pinned_sha256()); + cat_to(b, 2600, L"\n\n"); } - g_note[0] = 0; - g_building = FALSE; - layout(); - if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) - SetFocus(GetNextDlgTabItem(g_hwnd, NULL, FALSE)); // something must hold the keyboard focus + cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); + { + int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line + const WCHAR *p = g_drv_rep; + int used = 0, more = 0; + while (*p) { + const WCHAR *e = p; int n; + while (*e && *e != L'\n') e++; + n = (int)(e - p) + (*e ? 1 : 0); // the line with its line feed + if (!more && used + n <= room) { lstrcpynW(b + lstrlenW(b), p, n + 1); used += n; } + else more++; + p += n; + } + if (more) { + WCHAR t[48]; + wsprintfW(t, T(L" (and %d more)", L" (另外還有 %d 個)"), more); + cat_to(b, 2600, t); + } + } + text(ID_BODY, b); + // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left + buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_bde(void) +{ + text(ID_TITLE, T(L"BitLocker could not be suspended", L"BitLocker 沒有暫停成功")); + text(ID_BODY, T(L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " + L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " + L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", + L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" + L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。")); + buttons(T(L"Restart anyway", L"仍要重新開機"), T(L"Later", L"稍後"), T(L"Find the recovery key", L"查詢修復金鑰")); +} + +static void enter_drv_prep(void) +{ + text(ID_TITLE, T(L"Please wait", L"請稍候")); + text(ID_BODY, T(L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_drv_reboot(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); + lstrcpyW(b, g_drv_noreboot + ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " + L"When the menu appears, press 7. After you sign in, this program continues by itself.", + L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") + : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" + L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", + L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); + if (g_fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped + text(ID_BODY, b); + buttons(T(L"Close", L"關閉"), NULL, NULL); + if (g_fix_done) acp_warning(); +} + +static void enter_drv_retry(void) +{ + text(ID_TITLE, T(L"7 was not pressed", L"剛才沒有按到 7")); + text(ID_BODY, T(L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" + L"Try again? The computer restarts once more. When the menu appears, press 7.", + L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" + L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); + buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_warn(void) +{ + text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); + text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", + L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); + show_image(1); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); } +static void enter_drv_install(void) +{ + text(ID_TITLE, T(L"Installing the driver", L"正在安裝驅動程式")); + text(ID_BODY, T(L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_drv_done(void) +{ + text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); + text(ID_BODY, g_drv_have + ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", + L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") + : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" + L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", + L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" + L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。")); + if (can_start_dynarun()) buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); // only after a success (this page); a failure shows P_DRV_ERROR + else buttons(T(L"Close", L"關閉"), NULL, NULL); +} + +static void enter_drv_error(void) +{ + text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); + text(ID_BODY, g_note); + buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); +} + + /* ---------- worker thread ---------- */ static void on_progress(DWORD done, DWORD total) @@ -1317,96 +1370,195 @@ static void job_done(int job, int rc) JOBS[job].on_done(job, rc); } -static void on_primary(void) +/* ---------- the pages: what a click, Esc or the close button does on each ---------- */ + +// The first page's options are read when it is left, whichever button leaves it. +static void take_options(void) { - switch (g_page) { - case P_HELLO: - g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; - if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver - g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it - if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } - break; - case P_FAIL: acq_start(g_acq); break; // Retry / Continue download - case P_PASSWORD: - if (!take_password()) { - status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", - L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。"), TRUE); - SetFocus(H(ID_EDIT)); break; - } - start_job(JOB_EXTRACT, P_EXTRACT); - break; - case P_CHECK: set_page(P_LICENSE); break; - case P_LICENSE: go_install(JOB_INSTALL_ALL); break; - case P_RUNNING: - if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only - start_job(g_pending, P_INSTALL); - break; - case P_DONE: - start_dynarun(); - DestroyWindow(g_hwnd); - break; - case P_ERROR: DestroyWindow(g_hwnd); break; - case P_DRV_INTRO: acq_start(&ACQ_DRIVER); break; - case P_DRV_PRESS7: if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); break; // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage - case P_DRV_HASH: hash_choice(FALSE); break; // "Cancel" is the default button - case P_DRV_RETRY: g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); break; // the staged driver is still there - case P_DRV_WARN: start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); break; - case P_DRV_BDE: drv_reboot_now(); break; // "Restart anyway" - case P_DRV_DONE: - if (can_start_dynarun()) start_dynarun(); // "Start DynaRun" (otherwise the button reads "Close") - DestroyWindow(g_hwnd); - break; - case P_FETCH: // "Pause" / "Resume" - if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part - else if (g_busy && !g_pause_req) { - g_pause_req = TRUE; - InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag - text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); - } - break; - case P_DRV_REBOOT: case P_DRV_ERROR: DestroyWindow(g_hwnd); break; + g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; +} + +static void end_wizard(void) { DestroyWindow(g_hwnd); } + +// The primary button (right-most, Enter), page by page. +static void pri_hello(void) +{ + take_options(); + if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); return; } // the fix is current: straight to the driver + g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it + if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } +} +static void pri_fail(void) { acq_start(g_acq); } // Retry / Continue download +static void pri_password(void) +{ + if (!take_password()) { + status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", + L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。"), TRUE); + SetFocus(H(ID_EDIT)); + return; + } + start_job(JOB_EXTRACT, P_EXTRACT); +} +static void pri_check(void) { set_page(P_LICENSE); } +static void pri_license(void) { go_install(JOB_INSTALL_ALL); } +static void pri_running(void) +{ + if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only + start_job(g_pending, P_INSTALL); +} +static void pri_done(void) +{ + start_dynarun(); + end_wizard(); +} +static void pri_drv_intro(void) { acq_start(&ACQ_DRIVER); } +static void pri_drv_press7(void) { if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); } // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage +static void pri_drv_hash(void) { hash_choice(FALSE); } // "Cancel" is the default button +static void pri_drv_retry(void) { g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); } // the staged driver is still there +static void pri_drv_warn(void) { start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); } +static void pri_drv_bde(void) { drv_reboot_now(); } // "Restart anyway" +static void pri_drv_done(void) +{ + if (can_start_dynarun()) start_dynarun(); // "Start DynaRun" (otherwise the button reads "Close") + end_wizard(); +} +static void pri_fetch(void) // "Pause" / "Resume" +{ + if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part + else if (g_busy && !g_pause_req) { + g_pause_req = TRUE; + InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag + text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); } } +// Closing the window (the X, Alt+F4, and "Cancel" / "Close" where that is what leaving means). A page can refuse it (PF_NOCLOSE), or do something first +// (its close hook, which then also ends the wizard if that is what closing means there). static void on_close(void) { - if (g_page == P_INSTALL || g_page == P_DRV_PREP || g_page == P_DRV_INSTALL) return; // msiexec / registry / system tool work must not be interrupted + const page_desc *d = &PAGES[g_page]; + if (d->flags & PF_NOCLOSE) return; // msiexec / registry / system tool work must not be interrupted if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } - // after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) - if (g_flow == FLOW_DRIVER_CONTINUE && (g_page == P_DRV_RETRY || g_page == P_DRV_WARN) && is_admin()) driver_cancel(&g_opts); - if (g_page == P_DRV_BDE) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); return; } // the task, the menu flag and the suspension were armed: take them back first - DestroyWindow(g_hwnd); + if (d->close) d->close(); else end_wizard(); +} +// after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) +static void cls_drv_task(void) +{ + if (g_flow == FLOW_DRIVER_CONTINUE && is_admin()) driver_cancel(&g_opts); + end_wizard(); } +static void cls_drv_bde(void) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and the suspension were armed: take them back first -// The second button (and Esc): Cancel / Skip / Later / Close -static void on_second(BOOL button) +// The second button (and Esc): Cancel / Skip / Later / Close. A page without a handler closes. button FALSE: Esc. +static void sec_none(BOOL button) { (void)button; } // the page "Installing": no way out +static void sec_fail(BOOL button) { - if (g_page == P_DRV_HASH) hash_choice(button); // the button is "Install anyway"; Esc is a cancel - else if (g_page == P_FAIL && button && g_acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver - else if (g_page == P_FETCH && button) { - if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once - else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) - } - else if (g_page == P_DRV_INTRO && button && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver - else if (g_page == P_DRV_RETRY || g_page == P_DRV_WARN || g_page == P_DRV_BDE) start_job(JOB_DRV_CANCEL, P_DRV_PREP); // the task, the menu flag and BitLocker back - else if (g_page != P_INSTALL) on_close(); + if (button && g_acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver + else on_close(); } +static void sec_fetch(BOOL button) +{ + if (!button) { on_close(); return; } + if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once + else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) +} +static void sec_drv_intro(BOOL button) +{ + if (button && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver + else on_close(); +} +static void sec_drv_hash(BOOL button) { hash_choice(button); } // the button is "Install anyway"; Esc is a cancel +static void sec_drv_cancel(BOOL button) { (void)button; start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and BitLocker back // The third button (left of the second one): another way on. On the error page: choose the file (or folder) yourself. -static void on_third(void) -{ - if (g_page == P_FAIL) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } - else if (g_page == P_HELLO && g_hint) { // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) - g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; - user_log(L"DynaRun update hint: the user chose to download the new version"); - g_acq_fresh = TRUE; // an older package found on this computer would not be the new version - acq_start(&ACQ_SETUP); - } +static void thr_fail(void) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } +static void thr_hello(void) +{ + if (!g_hint) return; // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) + take_options(); + g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart_later = g_drv; + user_log(L"DynaRun update hint: the user chose to download the new version"); + g_acq_fresh = TRUE; // an older package found on this computer would not be the new version + acq_start(&ACQ_SETUP); +} + +// The extra button (bottom left, link style). +static void ext_dynapro(void) { ShellExecuteW(g_hwnd, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); } // also after a failed driver download (P_DRV_HASH) +static void ext_recovery_key(void) { ShellExecuteW(g_hwnd, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); } +static void ext_manual(void) { ShellExecuteW(g_hwnd, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); } +static void ext_hello(void) // "Reinstall the fix..." / "Installed elsewhere..." +{ + if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } + else choose_exe(); +} + +// Alt+key shortcuts that exist on one page only (the controls' own & mnemonics do not work while the focus is in the edit or the rich text). +static BOOL when_drv_avail(void) { return g_drv_avail; } +static const hotkey KEYS_HELLO[] = { { 'T', ID_PIN, NULL }, { 'S', ID_AUTO, NULL }, { 'D', ID_DRV, when_drv_avail }, { 0 } }; +static const hotkey KEYS_LICENSE[] = { { 'A', ID_CHECK, NULL }, { 0 } }; + +// One row per page, indexed by the P_* value. A NULL handler means: nothing happens (second: the page closes). +// enter builds the page (the controls, texts and buttons it shows); set_page has reset everything before it +// primary second third extra what the four buttons do (ID_PRIMARY / Enter, ID_SECOND / Esc, ID_THIRD, ID_EXTRA) +// close what closing the window does on this page, after the common refusals; NULL: ends the wizard +// keys the page's Alt+key shortcuts +// flags PF_NOCLOSE: closing the window is refused +// A new page: a P_* value, an enter_* function, a row here. +static const page_desc PAGES[P_COUNT] = { + [P_HELLO] = { enter_hello, pri_hello, NULL, thr_hello, ext_hello, NULL, KEYS_HELLO, 0 }, + [P_FETCH] = { enter_fetch, pri_fetch, sec_fetch, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_FAIL] = { enter_fail, pri_fail, sec_fail, thr_fail, ext_dynapro, NULL, NULL, 0 }, + [P_PASSWORD] = { enter_password, pri_password, NULL, NULL, choose_file, NULL, NULL, 0 }, + [P_EXTRACT] = { enter_extract, NULL, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_CHECK] = { enter_check, pri_check, NULL, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_LICENSE] = { enter_license, pri_license, NULL, NULL, ext_dynapro, NULL, KEYS_LICENSE, 0 }, + [P_RUNNING] = { enter_running, pri_running, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_INSTALL] = { enter_install, NULL, sec_none, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DONE] = { enter_done, pri_done, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_ERROR] = { enter_error, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_INTRO] = { enter_drv_intro, pri_drv_intro, sec_drv_intro, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_PRESS7] = { enter_drv_press7, pri_drv_press7, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_HASH] = { enter_drv_hash, pri_drv_hash, sec_drv_hash, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_DRV_BDE] = { enter_drv_bde, pri_drv_bde, sec_drv_cancel, NULL, ext_recovery_key, cls_drv_bde, NULL, 0 }, + [P_DRV_PREP] = { enter_drv_prep, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DRV_REBOOT] = { enter_drv_reboot, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_RETRY] = { enter_drv_retry, pri_drv_retry, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, + [P_DRV_WARN] = { enter_drv_warn, pri_drv_warn, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, + [P_DRV_INSTALL] = { enter_drv_install, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DRV_DONE] = { enter_drv_done, pri_drv_done, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_ERROR] = { enter_drv_error, end_wizard, NULL, NULL, ext_manual, NULL, NULL, 0 }, +}; + +static void set_page(int p) +{ + g_page = p; + g_paused = g_pause_req = FALSE; + g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress + g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does + KillTimer(g_hwnd, TIMER_RUNNING); + vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); + PAGES[p].enter(); + g_note[0] = 0; + g_building = FALSE; + layout(); + if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) + SetFocus(GetNextDlgTabItem(g_hwnd, NULL, FALSE)); // something must hold the keyboard focus +} + +// The window asks the table (the controls are the same on every page). +static void on_primary(void) { if (PAGES[g_page].primary) PAGES[g_page].primary(); } +static void on_second(BOOL button) { if (PAGES[g_page].second) PAGES[g_page].second(button); else on_close(); } +static void on_third(void) { if (PAGES[g_page].third) PAGES[g_page].third(); } +static void on_extra(void) { if (PAGES[g_page].extra) PAGES[g_page].extra(); } +// The control id of the Alt+key the current page has a shortcut for; 0 when there is none. +static int page_hotkey(WPARAM key) +{ + const hotkey *k = PAGES[g_page].keys; + for (; k && k->key; k++) if (k->key == key && (!k->when || k->when())) return k->id; + return 0; } /* ---------- window ---------- */ @@ -1632,14 +1784,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) case IDOK: case ID_PRIMARY: if (IsWindowVisible(H(ID_PRIMARY)) && IsWindowEnabled(H(ID_PRIMARY))) on_primary(); break; case IDCANCEL: case ID_SECOND: on_second(LOWORD(w) == ID_SECOND); break; case ID_THIRD: on_third(); break; - case ID_EXTRA: - if (g_page == P_FAIL || g_page == P_FETCH || g_page == P_CHECK || g_page == P_LICENSE) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); - else if (g_page == P_DRV_BDE) ShellExecuteW(h, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); - else if (g_page == P_PASSWORD) choose_file(); - else if (g_page == P_HELLO) { if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } else choose_exe(); } // "Reinstall the fix..." / "Installed elsewhere..." - else if (g_page == P_DRV_ERROR) ShellExecuteW(h, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); - else if (g_page == P_DRV_HASH) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); // after a failed driver download - break; + case ID_EXTRA: on_extra(); break; case ID_CHECK: EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); break; } return 0; @@ -1722,7 +1867,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) int wizard(const install_opts *init) { - INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; + INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; int key; static WCHAR title[100]; g_opts = *init; // the command line's options; the pages change this copy icc.dwSize = sizeof(icc); icc.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; @@ -1771,16 +1916,8 @@ int wizard(const install_opts *init) if (n) SetFocus(n); continue; } - if (msg.message == WM_SYSKEYDOWN && msg.wParam == 'A' && g_page == P_LICENSE) { // Alt+A: "I accept" - SendMessageW(H(ID_CHECK), BM_CLICK, 0, 0); - continue; - } - if (msg.message == WM_SYSKEYDOWN && (msg.wParam == 'T' || msg.wParam == 'S') && g_page == P_HELLO) { // Alt+T / Alt+S - SendMessageW(H(msg.wParam == 'T' ? ID_PIN : ID_AUTO), BM_CLICK, 0, 0); - continue; - } - if (msg.message == WM_SYSKEYDOWN && msg.wParam == 'D' && g_page == P_HELLO && g_drv_avail) { // Alt+D: the driver - SendMessageW(H(ID_DRV), BM_CLICK, 0, 0); + if (msg.message == WM_SYSKEYDOWN && (key = page_hotkey(msg.wParam)) != 0) { // Alt+key shortcuts of the page + SendMessageW(H(key), BM_CLICK, 0, 0); continue; } if (IsDialogMessageW(g_hwnd, &msg)) continue; From 9d899bf8b868f170f22d24c2722f67804facdd64 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:52:48 +0800 Subject: [PATCH 27/41] Wizard: explicit result instead of deriving the exit code from the page g_result is the process exit code of wizard(). It is RC_CANCEL until a page decides otherwise: the success pages (Done, driver installed, restart for the driver) set RC_OK, the error pages (acquire error, "Something went wrong", "The driver was not installed") set RC_FAIL; set_page takes it back to RC_CANCEL first. wizard() returns it instead of testing g_page. Every exit code is the one the old page mapping gave. The install_result that held the name is now g_instres. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index 5514155..f59555a 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -37,7 +37,12 @@ static int g_page, g_job, g_dpi, g_lastpct = -1, g_pending, g_lastrun; // g_pe static volatile LONG g_cancel; static install_opts g_opts; // what the pages ask for (starts as the command line gave it); only the UI thread changes it static install_opts g_jobopts; // the copy a job runs with (start_job): the worker never reads g_opts while the pages may change it -static install_result g_result; // what the install job reports besides its code (the pin hint); written by the worker, read once the job is done +static install_result g_instres; // what the install job reports besides its code (the pin hint); written by the worker, read once the job is done +// The process exit code of wizard(): what the user's run came to. It is RC_CANCEL while nothing is decided (the window is closed before the end), and set +// where the outcome is decided: a page that says "it worked" (Done, the driver is installed, the restart for the driver) makes it RC_OK, an error page +// (the acquire flow's, "Something went wrong", "The driver was not installed") RC_FAIL. set_page takes it back to RC_CANCEL first, so it always belongs to +// the page on screen; wherever the window ends (a button, Esc, Alt+F4, a job that ends while the window is closing) wizard() returns this value. +static int g_result = RC_CANCEL; static BOOL g_close_asked; // g_opts.close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) static BOOL g_busy, g_closing, g_installed, g_pkg_ours, g_pkg_found, g_status_err, g_badfile, g_nolicense; // g_nolicense: the setup's license text could not be read static int g_nfound; // packages pkg_find_local has handed us so far @@ -370,12 +375,12 @@ static void cat_to(WCHAR *d, int cap, const WCHAR *s) if (n < cap - 1) lstrcpynW(d + n, s, cap - n); } -// What is said about the taskbar pin (g_result.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with +// What is said about the taskbar pin (g_instres.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with // the install (g_fix_done: the page "Done" is skipped), on the restart page. static void pin_hint(WCHAR *b) { - if (g_result.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); - if (g_result.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 + if (g_instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (g_instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", @@ -446,6 +451,7 @@ static void enter_fetch(void) // looking for a copy here, downloading, checkin static void enter_fail(void) // the one error page of the acquire flow { + g_result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) WCHAR *b = pgbuf; if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); @@ -550,6 +556,7 @@ static void enter_install(void) static void enter_done(void) { + g_result = RC_OK; // everything is installed WCHAR *b = pgbuf; text(ID_TITLE, T(L"Done", L"完成")); lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" @@ -566,6 +573,7 @@ static void enter_done(void) static void enter_error(void) { + g_result = RC_FAIL; // the install failed text(ID_TITLE, T(L"Something went wrong", L"發生問題")); text(ID_BODY, g_note); buttons(T(L"Close", L"關閉"), NULL, NULL); @@ -707,6 +715,7 @@ static void enter_drv_prep(void) static void enter_drv_reboot(void) { + g_result = RC_OK; // the driver step is prepared; the restart continues it WCHAR *b = pgbuf; text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); lstrcpyW(b, g_drv_noreboot @@ -754,6 +763,7 @@ static void enter_drv_install(void) static void enter_drv_done(void) { + g_result = RC_OK; // the driver is installed (or was already) text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); text(ID_BODY, g_drv_have ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", @@ -768,6 +778,7 @@ static void enter_drv_done(void) static void enter_drv_error(void) { + g_result = RC_FAIL; // the driver step failed text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); text(ID_BODY, g_note); buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); @@ -809,8 +820,8 @@ static int jp_extract(void) SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more return rc; } -static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_result, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_result, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage -static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_result, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_result); } +static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_instres, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_instres, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage +static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_instres, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_instres); } static int jp_drv_prep(void) { // "install anyway" accepts exactly this zip (its SHA-256), nothing else. @@ -1535,6 +1546,7 @@ static const page_desc PAGES[P_COUNT] = { static void set_page(int p) { g_page = p; + g_result = RC_CANCEL; // undecided until a page below says otherwise g_paused = g_pause_req = FALSE; g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does @@ -1894,7 +1906,7 @@ int wizard(const install_opts *init) g_hwnd = CreateWindowExW(0, wc.lpszClassName, title, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN, (GetSystemMetrics(SM_CXSCREEN) - (r.right - r.left)) / 2, (GetSystemMetrics(SM_CYSCREEN) - (r.bottom - r.top)) / 2, r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); - if (!g_hwnd) return 1; + if (!g_hwnd) return RC_FAIL; create_controls(); g_installed = locate_dynarun(); g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); @@ -1925,7 +1937,5 @@ int wizard(const install_opts *init) } g_hwnd = NULL; if (g_page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it - if (g_page == P_DONE || g_page == P_DRV_DONE || g_page == P_DRV_REBOOT) return RC_OK; // ended on a success page - if (g_page == P_FAIL || g_page == P_ERROR || g_page == P_DRV_ERROR) return RC_FAIL; // ended on an error page - return RC_CANCEL; // closed before the end + return g_result; } From a6f5243f0bb9fdc84cf98b8e5cab6f23ab1ed9b8 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:57:22 +0800 Subject: [PATCH 28/41] Split wizard.c into flow files included by wizard.c (one compilation unit) wizard.c keeps the includes, the ids, the enums and the state; the code is in src\wizard\layout.h (UI helpers, screenshots, controls, layout), acquire.h (getting the DynaRun setup and the driver package: download, password, check, license), driver.h (the driver pages and jobs) and main.h (the main flow, JOBS[], PAGES[], the window procedure, wizard()). They are fragments of the same translation unit, like src\setup\*.h; build.cmd is unchanged. Only the order of the functions and a few forward declarations in wizard.c differ: every function compiles to the same code. tests: test_job_table() reads wizard.c and the four fragments as one text. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 1841 +---------------------------------------- src/wizard/acquire.h | 423 ++++++++++ src/wizard/driver.h | 456 ++++++++++ src/wizard/layout.h | 392 +++++++++ src/wizard/main.h | 566 +++++++++++++ tests/README.md | 2 +- tests/VM_CHECKLIST.md | 2 +- tests/test_driver.c | 29 +- 8 files changed, 1883 insertions(+), 1828 deletions(-) create mode 100644 src/wizard/acquire.h create mode 100644 src/wizard/driver.h create mode 100644 src/wizard/layout.h create mode 100644 src/wizard/main.h diff --git a/src/wizard.c b/src/wizard.c index f59555a..5ebadca 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -99,6 +99,7 @@ static BOOL g_drv_avail, g_drv, g_drv_have, g_drv_noreboot; // g_drv_have: alr static WCHAR g_drvzip[MAX_PATH]; // the checked zip ("" = the staged copy, after a restart) static HBITMAP g_bmp[2], g_shown; // the two screenshots (decoded once) and the scaled one in the picture control static int g_bw[2], g_bh[2], g_img, g_shown_img = -1, g_shown_w, g_shown_h; +static WCHAR pgbuf[2600]; // the text of the page being built (one page at a time, on the UI thread) // What one page is (PAGES[], below): its builder, what its buttons do, what closing the window does. typedef struct { UINT key; int id; BOOL (*when)(void); } hotkey; // Alt+key clicks the control id (when is NULL, or says the shortcut is on) enum { PF_NOCLOSE = 1 }; @@ -119,1823 +120,23 @@ static void place(int id, int x, int y, int w, int h); #define S(x) MulDiv((x), g_dpi, 96) #define H(id) GetDlgItem(g_hwnd, (id)) -/* ---------- small UI helpers ---------- */ - -static void vis(int id, BOOL on) { ShowWindow(H(id), on ? SW_SHOW : SW_HIDE); } -static void text(int id, const WCHAR *t) { SetWindowTextW(H(id), t ? t : L""); vis(id, t != NULL); } -static WCHAR pgbuf[2600]; // the text of the page being built (one page at a time, on the UI thread) -static int g_sth = -1; // room the layout reserved for the status line (its measured height, 0 when hidden) -static BOOL g_building; // set_page is rebuilding the page: it lays out once at its end -static int status_need(void); -// Sets the status line. When the room it needs differs from what the layout reserved (shown, hidden, another number of lines), the page is laid out -// again, so the line is never cut off by the button bar and the flexible parts (license text, picture, info box) give way. -static void status(const WCHAR *t, BOOL err) -{ - g_status_err = err; text(ID_STATUS, t); - if (g_hwnd && !g_building && status_need() != g_sth) layout(); -} - -// The button bar of every page: primary = the right-most one and the default (Enter); second = the way out, directly left of it (Cancel, or Skip / Later / -// Close where that is what leaving means); third = another way on, left of the second one; extra = the one link-style action at the bottom left. -static void buttons4(const WCHAR *primary, const WCHAR *second, const WCHAR *third, const WCHAR *extra) -{ - text(ID_PRIMARY, primary); text(ID_SECOND, second); text(ID_THIRD, third); text(ID_EXTRA, extra); - EnableWindow(H(ID_PRIMARY), TRUE); - if (primary) SetFocus(H(ID_PRIMARY)); -} -static void buttons(const WCHAR *primary, const WCHAR *second, const WCHAR *extra) { buttons4(primary, second, NULL, extra); } - -static void progress(BOOL marquee) -{ - HWND p = H(ID_PROG); - LONG st = GetWindowLongW(p, GWL_STYLE); - SetWindowLongW(p, GWL_STYLE, marquee ? st | PBS_MARQUEE : st & ~PBS_MARQUEE); - SendMessageW(p, PBM_SETMARQUEE, marquee, 40); - SendMessageW(p, PBM_SETRANGE32, 0, 1000); - SendMessageW(p, PBM_SETPOS, 0, 0); - g_lastpct = -1; - vis(ID_PROG, TRUE); -} - -static void fmt_mb(WCHAR *o, DWORD n) { wsprintfW(o, L"%lu.%lu", n / 1048576, n % 1048576 / 104858); } - -// A download that can be continued where it stopped (its .part and ETag are there): appends how much is already received to g_note and returns TRUE. -// The button then reads "Continue download" instead of "Retry". -static BOOL resume_note(void) -{ - DWORD have = 0; WCHAR a[24], t[24], line[200]; - if (!g_acq->partial(&have)) return FALSE; - fmt_mb(a, have); - if (g_dltotal > have) { - fmt_mb(t, g_dltotal); - wsprintfW(line, T(L"\n\n%s of %s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s / %s MB,按「繼續下載」會從中斷的地方接續。"), a, t); - } else - wsprintfW(line, T(L"\n\n%s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s MB,按「繼續下載」會從中斷的地方接續。"), a); - lstrcatW(g_note, line); - return TRUE; -} - -static UINT StrToUint(const WCHAR *s) { UINT v = 0; while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0'); return v; } - -// ANSI code page of the system locale, also when "Beta: UTF-8" makes the active one 65001 -static UINT legacy_acp(void) -{ - WCHAR b[8]; - return GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTANSICODEPAGE, b, 8) ? StrToUint(b) : GetACP(); -} - -static const WCHAR *file_name(const WCHAR *path) -{ - const WCHAR *n = path; - for (; *path; path++) if (*path == '\\' || *path == '/') n = path + 1; - return n; -} - -// "DynaRun V3", "DynaRunFix.exe" ... for the running-programs page -static void running_names(int run, WCHAR *out) -{ - out[0] = 0; - if (run & RUN_DYNARUN) lstrcatW(out, L" DynaRun V3\n"); - if (run & RUN_LAUNCHER) lstrcatW(out, T(L" DynaRunFix.exe (starting DynaRun)\n", L" DynaRunFix.exe(正在啟動 DynaRun)\n")); - if (run & RUN_LEPROC) lstrcatW(out, L" LEProc.exe (Locale Emulator)\n"); -} - -// The size is the server's (Content-Length), shown once the download has started. -static void fetch_body(DWORD total) -{ - WCHAR size[40], b[500]; - size[0] = 0; - if (total) wsprintfW(size, T(L" (%lu.%lu MB)", L"(%lu.%lu MB)"), total / 1048576, total % 1048576 / 104858); - wsprintfW(b, T(L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" - L"If it does not work, click \"Open website\" to get it from the website yourself.", - L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" - L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(g_acq), size); - g_total = total; - text(ID_BODY, b); -} - -// Last paragraph of the first page. -static const WCHAR *disclaimer(void) -{ - return T(L"\n\nDynaRunFix is an independent open-source tool, not affiliated with or endorsed by Dyna Pro Dynamometers. " - L"It is provided as is, without warranty (MIT license). DynaRun V3 is licensed to you separately by Dyna Pro.", - L"\n\nDynaRunFix 是獨立的開源工具,與 Dyna Pro Dynamometers 無關,也未經其認可;依 MIT 授權按現狀提供,不提供任何擔保。" - L"DynaRun V3 由 Dyna Pro 另行授權給你。"); -} - -/* ---------- screenshots of the driver steps (PNG resources, decoded with GDI+ looked up at run time: nothing is linked to it) ---------- */ - -typedef struct { UINT32 version; void *callback; BOOL no_thread, no_codecs; } gdip_input; - -// The PNG resource id as a bitmap (alpha on white), or NULL when GDI+ is not there or the picture cannot be read. -static HBITMAP load_png(int id, int *w, int *h) -{ - typedef int (WINAPI *startup_t)(ULONG_PTR *, const gdip_input *, void *); - typedef int (WINAPI *from_stream_t)(IStream *, void **); - typedef int (WINAPI *to_hbitmap_t)(void *, HBITMAP *, DWORD); - typedef int (WINAPI *dim_t)(void *, UINT *); - typedef int (WINAPI *dispose_t)(void *); - static HMODULE lib; static ULONG_PTR token; static BOOL tried; - static startup_t startup; static from_stream_t from_stream; static to_hbitmap_t to_hbitmap; static dim_t width, height; static dispose_t dispose; - WCHAR path[MAX_PATH]; gdip_input in; HRSRC r; const void *data; DWORD n; HGLOBAL mem; void *dst; IStream *st = NULL; void *bmp = NULL; HBITMAP hb = NULL; UINT bw = 0, bh = 0; - if (!tried) { - tried = TRUE; - GetSystemDirectoryW(path, MAX_PATH - 20); lstrcatW(path, L"\\gdiplus.dll"); // by full system path, never searched for - if ((lib = LoadLibraryW(path)) != NULL) { - startup = (startup_t)GetProcAddress(lib, "GdiplusStartup"); - from_stream = (from_stream_t)GetProcAddress(lib, "GdipCreateBitmapFromStream"); - to_hbitmap = (to_hbitmap_t)GetProcAddress(lib, "GdipCreateHBITMAPFromBitmap"); - width = (dim_t)GetProcAddress(lib, "GdipGetImageWidth"); height = (dim_t)GetProcAddress(lib, "GdipGetImageHeight"); - dispose = (dispose_t)GetProcAddress(lib, "GdipDisposeImage"); - zero(&in, sizeof(in)); in.version = 1; - if (!startup || !from_stream || !to_hbitmap || !width || !height || !dispose || startup(&token, &in, NULL)) startup = NULL; - } - } - if (!lib || !startup) return NULL; - if (!(r = FindResourceW(NULL, MAKEINTRESOURCEW(id), (LPCWSTR)RT_RCDATA)) || !(data = LockResource(LoadResource(NULL, r)))) return NULL; - n = SizeofResource(NULL, r); - if (!(mem = GlobalAlloc(GMEM_MOVEABLE, n)) || !(dst = GlobalLock(mem))) { if (mem) GlobalFree(mem); return NULL; } - CopyMemory(dst, data, n); GlobalUnlock(mem); - if (SUCCEEDED(CreateStreamOnHGlobal(mem, TRUE, &st))) { // the stream owns mem now - if (from_stream(st, &bmp) == 0 && bmp) { - width(bmp, &bw); height(bmp, &bh); - if (to_hbitmap(bmp, &hb, 0xFFFFFFFF) != 0) hb = NULL; // transparent parts on white - dispose(bmp); - } - IStream_Release(st); - } else GlobalFree(mem); - if (hb) { *w = (int)bw; *h = (int)bh; } - return hb; -} - -// Highlight of the "7) Disable driver signature enforcement" line of the restart menu picture (driver-press-7.png, 683 x 468): the line's -// rectangle in picture pixels. Drawn on the scaled copy, so the outline stays sharp at any size and DPI. -// Line 7 of the picture (683 x 468) has its text in rows 266..278 and columns 121..267; line 6 ends at row 258, line 8 starts at row 286. The outline runs 4 rows -// away from the text on each side, in the middle of the gaps, and is 2 pixels thick. -#define HL_X0 116 -#define HL_Y0 262 -#define HL_X1 272 -#define HL_Y1 282 - -static void draw_highlight(HDC dc, int sw, int sh, int tw, int th) -{ - int x0 = MulDiv(HL_X0, tw, sw), y0 = MulDiv(HL_Y0, th, sh), x1 = MulDiv(HL_X1, tw, sw), y1 = MulDiv(HL_Y1, th, sh); - int lw = S(2) < 2 ? 2 : S(2), rh = y1 - y0, rad = rh / 2 + 2, lx0 = x1 + lw + rh, lx1; - HPEN pen = CreatePen(PS_SOLID, lw, RGB(255, 153, 0)), op; - HBRUSH red = CreateSolidBrush(RGB(224, 52, 0)), ob = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH)); - LOGFONTW lf; HFONT f, of; const WCHAR *t = T(L"Press 7", L"按 7"); SIZE sz; POINT tri[3]; RECT tr; - op = (HPEN)SelectObject(dc, pen); - RoundRect(dc, x0, y0, x1, y1, rad, rad); // orange outline around the line (the pen is centred on the edge) - // callout to the right of the line: red label "Press 7" with a small arrow pointing at it - GetObjectW(g_font, sizeof(lf), &lf); lf.lfHeight = -(rh * 7 / 10 < 10 ? 10 : rh * 7 / 10); lf.lfWeight = FW_BOLD; lf.lfQuality = CLEARTYPE_QUALITY; - f = CreateFontIndirectW(&lf); of = (HFONT)SelectObject(dc, f); - GetTextExtentPoint32W(dc, t, lstrlenW(t), &sz); - lx1 = lx0 + sz.cx + rh; - if (lx1 > tw - 2) { lx0 -= lx1 - (tw - 2); lx1 = tw - 2; } // keep it inside the picture - SelectObject(dc, GetStockObject(NULL_PEN)); SelectObject(dc, red); - RoundRect(dc, lx0, y0, lx1, y1, rad, rad); - tri[0].x = x1 + lw + 3; tri[0].y = (y0 + y1) / 2; // tip, next to the outline - tri[1].x = lx0 + 1; tri[1].y = y0; tri[2].x = lx0 + 1; tri[2].y = y1; - Polygon(dc, tri, 3); - SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(255, 255, 255)); - tr.left = lx0; tr.right = lx1; tr.top = y0; tr.bottom = y1; - DrawTextW(dc, t, -1, &tr, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); - SelectObject(dc, of); DeleteObject(f); - SelectObject(dc, op); SelectObject(dc, ob); DeleteObject(pen); DeleteObject(red); -} - -static HBITMAP scale_bitmap(HBITMAP src, int sw, int sh, int tw, int th, BOOL highlight) -{ - HDC scr = GetDC(NULL), a = CreateCompatibleDC(scr), b = CreateCompatibleDC(scr); HBITMAP dst = CreateCompatibleBitmap(scr, tw, th), oa, ob; - oa = SelectObject(a, src); ob = SelectObject(b, dst); - SetStretchBltMode(b, HALFTONE); SetBrushOrgEx(b, 0, 0, NULL); - StretchBlt(b, 0, 0, tw, th, a, 0, 0, sw, sh, SRCCOPY); - if (highlight) draw_highlight(b, sw, sh, tw, th); - SelectObject(a, oa); SelectObject(b, ob); DeleteDC(a); DeleteDC(b); ReleaseDC(NULL, scr); - return dst; -} - -static void show_image(int which) // 0: press 7 menu, 1: "install anyway" -{ - g_img = which; - if (!g_bmp[which]) g_bmp[which] = load_png(18 + which, &g_bw[which], &g_bh[which]); - vis(ID_IMG, g_bmp[which] != NULL); -} - -// Size of the picture of the current page fitted into w x ah (not larger than at 96 dpi scaled to this screen). -static void fit_image(int w, int ah, int *tw, int *th) -{ - int ow = g_bw[g_img], oh = g_bh[g_img]; - if (ah < S(60)) ah = S(60); - *tw = w; *th = MulDiv(oh, *tw, ow); - if (*th > ah) { *th = ah; *tw = MulDiv(ow, *th, oh); } - if (*tw > MulDiv(ow, g_dpi, 96)) { *tw = MulDiv(ow, g_dpi, 96); *th = MulDiv(oh, *tw, ow); } -} - -// Fits the picture of the current page into w x ah; returns the height used. -static int place_image(int x, int y, int w, int ah) -{ - int ow = g_bw[g_img], oh = g_bh[g_img], tw, th; HBITMAP old; - fit_image(w, ah, &tw, &th); - if (g_shown_img != g_img || g_shown_w != tw || g_shown_h != th) { - HBITMAP s = scale_bitmap(g_bmp[g_img], ow, oh, tw, th, g_img == 0); - old = (HBITMAP)SendMessageW(H(ID_IMG), STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)s); - if (old) DeleteObject(old); - g_shown = s; g_shown_img = g_img; g_shown_w = tw; g_shown_h = th; - } - place(ID_IMG, x, y, tw, th); - return th; -} - -// The "Start DynaRun" button is offered only when DynaRun is installed here and our launcher is in place (g_exe comes from locate_dynarun, which -// the wizard runs at start in every mode, so this also holds in the process the sign-in task starts after the restart; g_launcher from init_paths). -// The driver step may be skipped (the fix is installed without it) only in the main install flow, not when it is the whole job (the shortcut, /driver, or the driver-only first page). -static BOOL can_skip_driver(void) { return g_flow == FLOW_MAIN || g_flow == FLOW_MAIN_WITH_DRIVER; } - -// The line about a newer DynaRun on the first page, and its button. A hint only: nothing happens unless the button is clicked. -static void hint_line(WCHAR *b) -{ - WCHAR t[400]; - if (g_hint == HINT_NEWER) { - wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", - L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), g_hint_new, g_hint_inst); - lstrcatW(b, t); - } else if (g_hint == HINT_ZIPDIFF) - lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", - L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); -} -static const WCHAR *hint_btn(void) { return g_hint ? T(L"Download new version", L"下載新版") : NULL; } - -static BOOL can_start_dynarun(void) { return g_installed && g_exe[0] && exists(g_launcher); } - -// Appends s to d (a buffer of cap characters), cutting it where it no longer fits; d always stays terminated. -static void cat_to(WCHAR *d, int cap, const WCHAR *s) -{ - int n = lstrlenW(d); - if (n < cap - 1) lstrcpynW(d + n, s, cap - n); -} - -// What is said about the taskbar pin (g_instres.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with -// the install (g_fix_done: the page "Done" is skipped), on the restart page. -static void pin_hint(WCHAR *b) -{ - if (g_instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); - if (g_instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 - ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", - L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") - : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", - L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「釘選到工作列」。")); -} -static void acp_warning(void) -{ - if (g_zh && legacy_acp() != 950) // DynaRun's Chinese is Big5: only a zh-TW system locale shows it everywhere - status(L"注意:這台電腦的「非 Unicode 程式的語言」不是中文(台灣),DynaRun 的中文可能會變成亂碼。\n" - L"請到「設定 → 時間與語言 → 語言與地區 → 系統管理語言設定 → 變更系統地區設定」選「中文(繁體,台灣)」,再重新開機。", TRUE); -} - -static void enter_hello(void) -{ - WCHAR *b = pgbuf; - if (g_flow == FLOW_DRIVER_ONLY) { - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " - L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" - L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", - L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" - L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" - L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); - hint_line(b); - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); - text(ID_BODY, b); - buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); - if (g_note[0]) status(g_note, TRUE); - return; - } - if (g_installed) { - text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " - L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" - L"When Windows asks whether to allow changes, click \"Yes\".%s", - L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" - L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); - hint_line(b); - text(ID_BODY, b); - buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); - } else { - text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); - wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" - L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" - L" 2. install DynaRun V3\n" - L" 3. add the fixes for Windows 10/11\n\n" - L"You need the setup password that Dyna Pro gave you.%s", - L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" - L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" - L" 2. 安裝 DynaRun V3\n" - L" 3. 加上 Windows 10/11 的相容性修正\n\n" - L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s"), disclaimer()); - text(ID_BODY, b); - buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); - } - vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_fetch(void) // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver -{ - WCHAR *b = pgbuf; - wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); - text(ID_TITLE, b); - fetch_body(0); - progress(FALSE); status(L" ", FALSE); - buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); -} - -static void enter_fail(void) // the one error page of the acquire flow -{ - g_result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) - WCHAR *b = pgbuf; - if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); - else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); - if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence - text(ID_TITLE, b); - { - BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) - BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); - g_badfile = FALSE; - wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" - L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", - L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" - L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); - text(ID_BODY, b); - buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), - T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); - } -} - -static void enter_password(void) -{ - WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); - wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", - L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); - text(ID_BODY, b); - vis(ID_EDIT, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); - if (g_note[0]) status(g_note, TRUE); - SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); -} - -static void enter_extract(void) -{ - text(ID_TITLE, T(L"Opening the setup file", L"正在打開安裝檔")); - text(ID_BODY, T(L"Please wait...", L"請稍候…")); - progress(FALSE); - buttons(NULL, T(L"Cancel", L"取消"), NULL); -} - -static void enter_check(void) // everything we know about the setup file, to compare with what Dyna Pro gives -{ - text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); - text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", - L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); - SetWindowTextW(H(ID_INFO), g_info); - vis(ID_INFO, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_license(void) -{ - WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); - lstrcpyW(b, g_nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", - L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") - : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); - text(ID_BODY, b); - SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") - : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); - vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); - EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); - SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_running(void) -{ - WCHAR *b = pgbuf; - WCHAR names[200]; - running_names(g_lastrun = running_programs(), names); - text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); - if (g_lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help - wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" - L"Ask that user to close DynaRun; this page then offers \"Install\".", - L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" - L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。"), names); - text(ID_BODY, b); - buttons(NULL, T(L"Cancel", L"取消"), NULL); - } else { - wsprintfW(b, T(L"DynaRun must be closed to install:\n%s\n" - L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", - L"安裝前必須關閉 DynaRun:\n%s\n" - L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。"), names); - text(ID_BODY, b); - buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); - } - if (g_note[0]) status(g_note, TRUE); - SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); -} - -static void enter_install(void) -{ - text(ID_TITLE, T(L"Installing", L"安裝中")); - text(ID_BODY, T(L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); -} - -static void enter_done(void) -{ - g_result = RC_OK; // everything is installed - WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Done", L"完成")); - lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" - L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", - L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" - L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); - pin_hint(b); - if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); - lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); - text(ID_BODY, b); - buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); - acp_warning(); -} - -static void enter_error(void) -{ - g_result = RC_FAIL; // the install failed - text(ID_TITLE, T(L"Something went wrong", L"發生問題")); - text(ID_BODY, g_note); - buttons(T(L"Close", L"關閉"), NULL, NULL); -} - -static void enter_drv_intro(void) -{ - WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); - lstrcpyW(b, T(L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" - L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" - L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" - L" 3. come back by itself after you sign in and finish the installation\n\n" - L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " - L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" - L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " - L"It resumes by itself, after at most 3 restarts.", - L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" - L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" - L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" - L" 3. 登入後本程式會自動回來,完成安裝\n\n" - L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" - L"下次重新開機會自動恢復。\n\n" - L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", - L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); - text(ID_BODY, b); - buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_drv_press7(void) -{ - WCHAR *b = pgbuf; - text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); - { - int bl = g_drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) - b[0] = 0; - if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); - else if (g_drv_how & DRV_SRC_LOCAL) { - WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm - if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); - else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); - lstrcatW(b, t); - } - if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", - L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); - lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", - L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); - if (bl > 0) lstrcatW(b, T(L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); - else if (bl < 0) lstrcatW(b, T(L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); - text(ID_BODY, b); - } - show_image(0); - if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet - else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_drv_hash(void) // a driver zip that is not the file this program knows: the user decides -{ - WCHAR *b = pgbuf; - static WCHAR src[2 * MAX_PATH + 40]; - if (g_drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (g_drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, g_drvzip); } - else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); - text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); - // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to - // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost - b[0] = 0; - if (g_drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of - cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" - L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); - cat_to(b, 2600, src); - cat_to(b, 2600, L"\n\n"); - } else { - WCHAR sz[24]; - wsprintfW(sz, L"%lu", (DWORD)g_drv_size); - cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" - L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); - cat_to(b, 2600, src); - cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); - cat_to(b, 2600, sz); - cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); - cat_to(b, 2600, g_drv_hex); - cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); - cat_to(b, 2600, driver_pinned_sha256()); - cat_to(b, 2600, L"\n\n"); - } - cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); - { - int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line - const WCHAR *p = g_drv_rep; - int used = 0, more = 0; - while (*p) { - const WCHAR *e = p; int n; - while (*e && *e != L'\n') e++; - n = (int)(e - p) + (*e ? 1 : 0); // the line with its line feed - if (!more && used + n <= room) { lstrcpynW(b + lstrlenW(b), p, n + 1); used += n; } - else more++; - p += n; - } - if (more) { - WCHAR t[48]; - wsprintfW(t, T(L" (and %d more)", L" (另外還有 %d 個)"), more); - cat_to(b, 2600, t); - } - } - text(ID_BODY, b); - // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left - buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_drv_bde(void) -{ - text(ID_TITLE, T(L"BitLocker could not be suspended", L"BitLocker 沒有暫停成功")); - text(ID_BODY, T(L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " - L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " - L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", - L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" - L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。")); - buttons(T(L"Restart anyway", L"仍要重新開機"), T(L"Later", L"稍後"), T(L"Find the recovery key", L"查詢修復金鑰")); -} - -static void enter_drv_prep(void) -{ - text(ID_TITLE, T(L"Please wait", L"請稍候")); - text(ID_BODY, T(L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); -} - -static void enter_drv_reboot(void) -{ - g_result = RC_OK; // the driver step is prepared; the restart continues it - WCHAR *b = pgbuf; - text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); - lstrcpyW(b, g_drv_noreboot - ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " - L"When the menu appears, press 7. After you sign in, this program continues by itself.", - L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") - : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" - L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", - L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); - if (g_fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped - text(ID_BODY, b); - buttons(T(L"Close", L"關閉"), NULL, NULL); - if (g_fix_done) acp_warning(); -} - -static void enter_drv_retry(void) -{ - text(ID_TITLE, T(L"7 was not pressed", L"剛才沒有按到 7")); - text(ID_BODY, T(L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" - L"Try again? The computer restarts once more. When the menu appears, press 7.", - L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" - L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); - buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_drv_warn(void) -{ - text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); - text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", - L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); - show_image(1); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); -} - -static void enter_drv_install(void) -{ - text(ID_TITLE, T(L"Installing the driver", L"正在安裝驅動程式")); - text(ID_BODY, T(L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。")); - progress(TRUE); - buttons(NULL, NULL, NULL); -} - -static void enter_drv_done(void) -{ - g_result = RC_OK; // the driver is installed (or was already) - text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); - text(ID_BODY, g_drv_have - ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", - L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") - : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" - L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", - L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" - L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。")); - if (can_start_dynarun()) buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); // only after a success (this page); a failure shows P_DRV_ERROR - else buttons(T(L"Close", L"關閉"), NULL, NULL); -} - -static void enter_drv_error(void) -{ - g_result = RC_FAIL; // the driver step failed - text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); - text(ID_BODY, g_note); - buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); -} - - -/* ---------- worker thread ---------- */ - -static void on_progress(DWORD done, DWORD total) -{ - int pct = total ? MulDiv(done, 1000, total) : 0; - if (done == (DWORD)-1) { PostMessageW(g_hwnd, WM_PROGRESS, done, total); return; } // the "checking" phase (driver_fetch) - if (pct == g_lastpct) return; - g_lastpct = pct; - PostMessageW(g_hwnd, WM_PROGRESS, done, total); -} - -// What a job runs on the worker thread (job_proc), and what is done with its result on the UI thread (JOBS[].on_done, below). -static int jp_acquire(void) // the DynaRun setup or the driver: a copy on this computer first, else the download -{ - int rc; - on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts - if (g_acq == &ACQ_DRIVER) { - rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); - if (rc == PK_OK || rc == DRV_W_HASH) { - on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked - g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds - } - } else { - g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); - if (g_pkg_found) rc = PK_OK; - else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } - } - return rc; -} -static int jp_extract(void) -{ - int rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); // a size cap, and a free target (shared with the driver) - SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more - return rc; -} -static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_instres, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_instres, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage -static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_instres, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_instres); } -static int jp_drv_prep(void) -{ - // "install anyway" accepts exactly this zip (its SHA-256), nothing else. - return driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); -} -static int jp_drv_install(void) { return driver_install(&g_jobopts); } -static int jp_drv_cancel(void) { return driver_cancel(&g_jobopts); } - -static void jd_acquire(int job, int rc); -static void jd_extract(int job, int rc); -static void jd_install(int job, int rc); -static void jd_drv_prep(int job, int rc); -static void jd_drv_install(int job, int rc); -static void jd_drv_cancel(int job, int rc); - -// One row per job, indexed by the JOB_* value (nothing depends on the order of the values). -// proc runs on the worker thread; returns the job's result code -// on_done runs on the UI thread when the worker has ended (job_done), with the job and its result -// keep_up the elevated stage or a system tool works: the UAC prompt and msiexec's window take the foreground, so ID_KEEPUP keeps the wizard behind them -// bring_front the wizard comes back in front of other windows when the job is done (the elevated part allows it) -typedef struct { - int (*proc)(void); - void (*on_done)(int job, int rc); - BOOL keep_up, bring_front; -} job_desc; -static const job_desc JOBS[JOB_COUNT] = { - [JOB_ACQUIRE] = { jp_acquire, jd_acquire, FALSE, FALSE }, - [JOB_EXTRACT] = { jp_extract, jd_extract, FALSE, FALSE }, - [JOB_INSTALL_ALL] = { jp_install_all, jd_install, TRUE, TRUE }, - [JOB_INSTALL_FIX] = { jp_install_fix, jd_install, TRUE, TRUE }, - [JOB_DRV_PREP] = { jp_drv_prep, jd_drv_prep, TRUE, TRUE }, - [JOB_DRV_INSTALL] = { jp_drv_install, jd_drv_install, TRUE, TRUE }, - [JOB_DRV_CANCEL] = { jp_drv_cancel, jd_drv_cancel, TRUE, TRUE }, -}; - -static DWORD WINAPI job_proc(LPVOID unused) -{ - int rc = JOBS[g_job].proc(); - PostMessageW(g_hwnd, WM_JOBDONE, g_job, rc); - (void)unused; - return 0; -} - -// The update hint is asked in the background when the first page shows an installed DynaRun; the answer comes as WM_HINT. Never waited for. -static DWORD WINAPI hint_proc(LPVOID unused) -{ - static WCHAR i[40], n[40]; int r; - r = pkg_update_hint(i, n, 40, user_log); - if (r) { lstrcpyW(g_hint_inst, i); lstrcpyW(g_hint_new, n); } - if (g_hwnd) PostMessageW(g_hwnd, WM_HINT, (WPARAM)r, 0); - (void)unused; - return 0; -} - -static void start_job(int job, int page) -{ - DWORD tid; - g_job = job; g_cancel = 0; g_busy = TRUE; - if (job == JOB_ACQUIRE && g_dlacq != g_acq) { g_dlacq = g_acq; g_dltotal = g_dlhave = 0; } // a resume of the same download keeps what is known - set_page(page); - g_jobopts = g_opts; // the job runs with the options as they are now - g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); - if (JOBS[job].keep_up) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); -} - -/* ---------- steps ---------- */ - -static void open_package(void); - -typedef struct { const char *p; LONG left; } rtfsrc; -static DWORD CALLBACK rtf_in(DWORD_PTR c, LPBYTE b, LONG n, LONG *got) -{ - rtfsrc *s = (rtfsrc *)c; LONG i, k = n < s->left ? n : s->left; - for (i = 0; i < k; i++) b[i] = (BYTE)s->p[i]; - s->p += k; s->left -= k; *got = k; - return 0; -} - -// The install jobs start only once DynaRun is closed (see running_programs in setup.c). -static void install(int job) -{ - g_pending = job; - if (running_programs()) set_page(P_RUNNING); - else start_job(job, P_INSTALL); -} - -// The install of the fix (job: JOB_INSTALL_ALL / JOB_INSTALL_FIX). With the driver ticked its package is fetched and checked first (the driver pages), and the -// one elevated stage then does both: the UAC prompt comes once, before the restart. The driver-only first page (FLOW_DRIVER_ONLY) has no install: it starts the prep itself. -static void go_install(int job) -{ - if (g_drv && g_flow == FLOW_MAIN) { g_flow = FLOW_MAIN_WITH_DRIVER; g_inst_job = job; g_note[0] = 0; set_page(P_DRV_INTRO); } - else install(job); -} - -// "Skip" on the driver pages of the main flow: the fix is installed without the driver (the sign-in start is then registered by the install itself). -static void skip_driver(void) -{ - user_log(L"driver: skipped, the fix is installed without it"); - g_drv = FALSE; g_opts.autostart_later = FALSE; g_flow = FLOW_MAIN; - install(g_inst_job); -} - -// Starts the acquire flow for the DynaRun setup or the driver (the same pages; a network check comes first inside the download). -static void acq_start(const acq_desc *d) -{ - g_acq = d; - if (d == &ACQ_SETUP) { g_pkg_ours = g_pkg_found = FALSE; g_acq_local_ok = !g_acq_fresh && g_nfound < 8; } // the cap: pkg_reject remembers 8 files - else { g_drvzip[0] = 0; g_drv_allow = FALSE; } - start_job(JOB_ACQUIRE, P_FETCH); -} - -// Whatever the file is called: only the real DynaRun setup is ever installed. -static void not_dynarun(void) -{ - WCHAR m[MAX_PATH], nm[100]; - g_acq = &ACQ_SETUP; - lstrcpynW(nm, file_name(g_pkg), 100); // wsprintfW does not check the size of g_note: a long file name must not overflow it - wsprintfW(g_note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", - L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。"), nm); - pkg_reject(g_pkg); - pkg_temp_dir(m); lstrcatW(m, L"\\Setup.msi"); - if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); // our extracted copy - if (g_pkg_ours) DeleteFileW(g_pkg); - if (g_pkg_found) { acq_start(&ACQ_SETUP); return; } // we picked it, not the user: quietly try the next one or download - g_badfile = TRUE; - set_page(P_FAIL); -} - -static void fmt_size(WCHAR *o, ULONGLONG n) -{ - DWORD v = (DWORD)n; - wsprintfW(o, T(L"%lu bytes (%lu.%lu MB)", L"%lu 位元組(%lu.%lu MB)"), v, v / 1048576, v % 1048576 / 104858); -} - -// Shown on the license page so the user can compare with what Dyna Pro provides. Display only: nothing here blocks the install. -static void build_info(void) -{ - WCHAR hex[65], hex2[65], ver[64], mfr[128], sz[80]; ULONGLONG size = 0, size2 = 0; BOOL same = !lstrcmpiW(g_pkg, g_msi), okp, okm = FALSE; - HCURSOR old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); - okp = pkg_sha256(g_pkg, hex, &size); - if (!same) okm = pkg_sha256(g_msi, hex2, &size2); - msi_info(g_msi, ver, mfr, 64); - SetCursor(old); - g_info[0] = 0; - lstrcatW(g_info, T(L"Source: ", L"來源:")); - lstrcatW(g_info, g_pkg_ours ? T(L"downloaded now from dynapro.co.uk", L"剛從 dynapro.co.uk 下載") - : g_pkg_found ? T(L"found on this computer, not downloaded by DynaRunFix", L"在這台電腦上找到,不是 DynaRunFix 下載的") - : T(L"chosen by you", L"你選擇的檔案")); - lstrcatW(g_info, T(L"\r\n\r\nFile:\r\n", L"\r\n\r\n檔案路徑:\r\n")); lstrcatW(g_info, g_pkg); - fmt_size(sz, size); - lstrcatW(g_info, T(L"\r\n\r\nSize: ", L"\r\n\r\n大小:")); lstrcatW(g_info, sz); - lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okp ? hex : L"?"); - if (!same) { - fmt_size(sz, size2); - lstrcatW(g_info, T(L"\r\n\r\nSetup.msi inside the file, size: ", L"\r\n\r\n檔案裡的 Setup.msi,大小:")); lstrcatW(g_info, sz); - lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okm ? hex2 : L"?"); - } - lstrcatW(g_info, L"\r\n\r\nMSI ProductVersion: "); lstrcatW(g_info, ver[0] ? ver : L"?"); - lstrcatW(g_info, L"\r\nManufacturer: "); lstrcatW(g_info, mfr[0] ? mfr : L"?"); -} - -static void after_extract(void) -{ - if (msi_check(g_msi, g_ver, 64) != MSI_DYNARUN) { not_dynarun(); return; } - release(g_rtf); - g_rtf = msi_license_rtf(g_msi); - g_nolicense = !g_rtf; - build_info(); - if (g_rtf) { - rtfsrc src; EDITSTREAM es; - src.p = g_rtf; src.left = lstrlenA(g_rtf); - es.dwCookie = (DWORD_PTR)&src; es.dwError = 0; es.pfnCallback = rtf_in; - SendMessageW(H(ID_RICH), EM_STREAMIN, SF_RTF, (LPARAM)&es); - } else // never install without the user accepting Dyna Pro's terms - SetWindowTextW(H(ID_RICH), T(L"DynaRun V3 is licensed to you by Dyna Pro Dynamometers Ltd under their end-user license agreement " - L"and their terms and conditions of sale. The agreement is part of Dyna Pro's setup, but this " - L"installer could not read it from this setup file.\r\n\r\n" - L"To read it, contact Dyna Pro (support@dynapro.co.uk) or see their website " - L"(\"Open website\" below).\r\n\r\n" - L"Install only if you accept Dyna Pro's license terms.", - L"DynaRun V3 是 Dyna Pro Dynamometers Ltd 依其使用者授權合約及銷售條款授權給你使用的軟體。" - L"授權合約包含在 Dyna Pro 的安裝檔中,但本安裝程式無法從這個安裝檔讀出合約內容。\r\n\r\n" - L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟官網」)。\r\n\r\n" - L"請在同意 Dyna Pro 的授權條款後再安裝。")); - SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); - set_page(P_CHECK); -} - -static void open_package(void) -{ - if (msi_check(g_pkg, NULL, 0) != MSI_NONE) { lstrcpyW(g_msi, g_pkg); after_extract(); return; } // by content, not by extension - if (zip_open(g_pkg, &g_zip) != PK_OK) { g_msi[0] = 0; not_dynarun(); return; } - pkg_temp_dir(g_msi); lstrcatW(g_msi, L"\\Setup.msi"); - if (g_zip.flags & 1) set_page(P_PASSWORD); - else { g_pw[0] = 0; start_job(JOB_EXTRACT, P_EXTRACT); } -} - -// The ZIP password is bytes; try the usual encodings of what was typed. -static BOOL take_password(void) -{ - static const UINT cps[] = { CP_OEMCP, CP_ACP, CP_UTF8 }; - WCHAR w[128]; int i; BOOL ok = FALSE; - GetWindowTextW(H(ID_EDIT), w, 128); - if (!w[0]) return FALSE; - for (i = 0; i < 3 && !ok; i++) - ok = WideCharToMultiByte(cps[i], 0, w, -1, g_pw, sizeof(g_pw), NULL, NULL) && zip_password_plausible(&g_zip, g_pw); - SecureZeroMemory(w, sizeof(w)); - if (!ok) SecureZeroMemory(g_pw, sizeof(g_pw)); - else SetWindowTextW(H(ID_EDIT), L""); // the typed password does not stay in the edit control (g_pw is wiped after the extraction) - return ok; -} - -static void choose_file(void) -{ - OPENFILENAMEW of; WCHAR f[MAX_PATH]; - zero(&of, sizeof(of)); f[0] = 0; - of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; - of.lpstrFilter = T(L"DynaRun V3 setup (*.zip;*.msi)\0*.zip;*.msi\0All files\0*.*\0", L"DynaRun V3 安裝檔 (*.zip;*.msi)\0*.zip;*.msi\0所有檔案\0*.*\0"); - of.lpstrFile = f; of.nMaxFile = MAX_PATH; - of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - if (!GetOpenFileNameW(&of)) return; - if (g_pkg_ours && lstrcmpiW(g_pkg, f)) DeleteFileW(g_pkg); // the download that turned out not to be wanted - lstrcpyW(g_pkg, f); g_pkg_ours = g_pkg_found = FALSE; - open_package(); -} - -static void choose_exe(void) -{ - OPENFILENAMEW of; WCHAR f[MAX_PATH]; - zero(&of, sizeof(of)); f[0] = 0; - of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; - of.lpstrFilter = L"DynaRun V3.exe\0DynaRun V3.exe\0*.exe\0*.exe\0"; - of.lpstrFile = f; of.nMaxFile = MAX_PATH; - of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - if (!GetOpenFileNameW(&of)) return; - lstrcpyW(g_exe, f); g_installed = TRUE; - set_page(P_HELLO); -} - -static void drop_msi(void) // the decrypted Setup.msi in our temp folder (the user's own MSI is never touched) -{ - WCHAR d[MAX_PATH], m[MAX_PATH]; - pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); - if (g_msi[0] && !lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); - RemoveDirectoryW(d); // only when empty -} - -static void cleanup_files(void) -{ - WCHAR d[MAX_PATH], m[MAX_PATH]; - pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); - if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); - if (g_pkg_ours) DeleteFileW(g_pkg); - RemoveDirectoryW(d); -} - -/* ---------- the driver (driver.c) ---------- */ - -// The text of a failed driver step (rc: PK_* of the download, DRV_E_* of driver.c) and the error page. -static void drv_error(int rc) -{ - switch (rc) { - case DRV_E_HASH: - lstrcpyW(g_note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", - L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。")); - break; - case DRV_E_BCD: lstrcpyW(g_note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; - case DRV_E_TASK: lstrcpyW(g_note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; - case DRV_E_DIR: lstrcpyW(g_note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", - L"無法把工作資料夾 %ProgramData%\\DynaRunFix 設為安全狀態(舊資料夾仍在使用中,或其他使用者可以修改它)。沒有安裝任何東西。請關閉其他程式、重新啟動電腦後再試一次。")); break; - case DRV_E_PNP: lstrcpyW(g_note, T(L"Windows did not accept the driver. Driver signature checking may still have been on.", L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。")); break; - default: wsprintfW(g_note, T(L"The driver could not be prepared (code %d).", L"無法準備驅動程式(代碼 %d)。"), rc); break; - } - lstrcatW(g_note, T(L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。")); - if (g_fix_done) lstrcatW(g_note, T(L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", - L"\n\nDynaRun 的修正本身已經安裝好了。之後想再試一次驅動程式,可以用開始功能表裡的「安裝 Dyna Pro USB 驅動程式」。")); - set_page(P_DRV_ERROR); -} - -static void drv_reboot_now(void) -{ - g_drv_noreboot = !driver_reboot(); - set_page(P_DRV_REBOOT); -} - -// /driver and /driver-continue: the first page -static void drv_start(void) -{ - if (!driver_supported()) { - DWORD b = os_build(); // 9200+: Windows 8 / 8.1, 6000+: Vista / 7, below: XP - lstrcpyW(g_note, T(L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", - L"這個 Windows 版本太舊,無法自動安裝(需要 Windows 10 或更新版本)。請手動安裝:\n\n")); - if (b >= 9200) - lstrcatW(g_note, T(L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " - L"Then install the driver in Device Manager (Update driver > Browse my computer) and choose \"Install this driver software anyway\" on the red warning.", - L"Windows 8 / 8.1:按住 Shift 再按「重新啟動」,依序選 疑難排解 > 進階選項 > 啟動設定 > 重新啟動,然後按 7(停用驅動程式強制簽章;32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" - L"接著在裝置管理員安裝驅動程式(更新驅動程式 > 瀏覽電腦上的驅動程式),並在紅色警告選「仍然安裝此驅動程式軟體」。")); - else if (b >= 6000) - lstrcatW(g_note, T(L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " - L"(32-bit editions do not enforce driver signing and only show the warning, so they can skip this). Then in Device Manager use Update Driver > Browse my computer > Have Disk, and choose \"Install this driver software anyway\" on the red warning.", - L"Windows Vista / 7:重新開機,在出現 Windows 標誌之前連續按 F8,在「進階開機選項」選「停用驅動程式強制簽章」(32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" - L"接著在裝置管理員用 更新驅動程式 > 瀏覽電腦 > 從磁片安裝,並在紅色警告選「仍然安裝此驅動程式軟體」。")); - else - lstrcatW(g_note, T(L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", - L"Windows XP 不強制驅動程式簽章。請在裝置管理員安裝驅動程式;Windows 說軟體未通過 Windows 標誌測試時,按「仍然繼續」。")); - lstrcatW(g_note, T(L"\n\nThe manual steps have the details: click \"Open the manual steps\".", L"\n\n詳細說明請按「開啟手動安裝說明」。")); - set_page(P_DRV_ERROR); - return; - } - g_drv_have = driver_installed(); - if (g_flow == FLOW_DRIVER_CONTINUE) { - if (g_drv_have) { if (is_admin()) driver_cancel(&g_opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes - else set_page(driver_ci_state() == DRV_CI_ENFORCED ? P_DRV_RETRY : P_DRV_WARN); - } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); -} - -// The one text for every way the acquire flow can end badly (the DynaRun setup and the driver alike): g_note. -static void acq_note(int rc) -{ - switch (rc) { - case PK_CANCEL: lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); break; - case PK_HTTP: wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); break; - case PK_CERT: wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", - L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), g_neterr); break; - case PK_URL: lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", - L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); break; - case PK_IO: wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); break; - case DRV_E_HASH: lstrcpyW(g_note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", - L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。")); break; - default: // PK_NET and anything unforeseen: the first real error of the attempts - if (g_neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(g_note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", - L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。")); - else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), g_neterr); - break; - } -} - -// The one error page of the acquire flow. -static void acq_fail(int rc) { acq_note(rc); set_page(P_FAIL); } - -// The download worker has ended because "Pause" was clicked: the .part and its ETag are kept, the page stays, the button becomes "Resume". -static void fetch_paused(void) -{ - WCHAR a[24], t[24], s[160]; - g_paused = TRUE; g_pause_req = FALSE; - fmt_mb(a, g_dlhave); - if (g_dltotal) { fmt_mb(t, g_dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } - else wsprintfW(s, T(L"Paused: %s MB downloaded.", L"已暫停:已下載 %s MB。"), a); - buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); - status(s, FALSE); - layout(); -} - -// "Cancel" while paused (no worker is running any more): the same result as a cancel during the transfer. -static void fetch_stopped(void) { g_paused = FALSE; acq_fail(PK_CANCEL); } - -// The choice on the warning page: install the zip anyway (the elevated step is told, see driver_prep) or cancel. Both are logged. -static void hash_choice(BOOL install) -{ - static WCHAR l[2 * MAX_PATH + 120]; - lstrcpyW(l, install ? L"driver zip with another hash: the user chose to install it anyway: " : L"driver zip with another hash: the user cancelled: "); - lstrcatW(l, g_drvzip); user_log(l); - if (install) { g_drv_allow = TRUE; set_page(P_DRV_PRESS7); return; } - if (!(g_drv_how & DRV_SRC_LOCAL)) driver_forget(g_drvzip); // our own refused download is not offered again; a file of the user's stays - if ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download - else if (g_drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); - g_drvzip[0] = 0; g_drv_allow = FALSE; - lstrcpyW(g_note, ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") - : T(L"Cancelled: the driver was not installed.", L"已取消,沒有安裝驅動程式。")); - set_page(P_DRV_INTRO); -} - -// What the driver check says (from the search, the download or a file the user chose): on to the restart page, to the warning page, or the error page. -static void acq_driver_result(int rc) -{ - if (rc == PK_OK) { - g_drv_allow = FALSE; - if (!g_bmp[0]) g_bmp[0] = load_png(18, &g_bw[0], &g_bh[0]); // the picture is decoded before the page changes, so the new page is built at once - set_page(P_DRV_PRESS7); - } else if (rc == DRV_W_HASH) { - WCHAR m[2 * MAX_PATH + 100]; - g_drv_allow = FALSE; - lstrcpyW(m, L"driver package with other files: the warning page is shown for "); lstrcatW(m, g_drvzip); user_log(m); - set_page(P_DRV_HASH); - } else acq_fail(rc); -} - -// The end of the acquire job. The DynaRun setup goes on to its password / check / license pages (open_package), the driver to its restart or warning page. -static void acq_done(int rc) -{ - BOOL drv = g_acq == &ACQ_DRIVER; - if ((rc == PK_OK || (drv && rc == DRV_W_HASH)) && g_cancel == 1) { // "Cancel" while the file was being checked - if (drv && !(g_drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(g_drvzip); - acq_fail(PK_CANCEL); - return; - } - if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); return; } - if (drv) { acq_driver_result(rc); return; } - if (rc == PK_OK) { if (g_pkg_found) g_nfound++; open_package(); return; } - acq_fail(rc); -} - -// A file dialog for the driver: one dialog, filter "Dyna Pro USB driver (*.zip;*.inf)". IFileOpenDialog where there is one (Vista and later), else the classic dialog. -// The path of the chosen file in p (MAX_PATH); FALSE when the user cancelled. -static BOOL pick_driver_file(WCHAR *p) -{ - static const GUID clsid = { 0xDC1C5A9C, 0xE88A, 0x4DDE, { 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7 } }; // CLSID_FileOpenDialog - static const GUID iid = { 0xD57C7288, 0xD4AD, 0x4768, { 0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60 } }; // IID_IFileOpenDialog - IFileOpenDialog *fd = NULL; HRESULT hr, hrc; BOOL ok = FALSE; - p[0] = 0; - hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); - if ((SUCCEEDED(hrc) || hrc == RPC_E_CHANGED_MODE) && SUCCEEDED(CoCreateInstance(&clsid, NULL, CLSCTX_INPROC_SERVER, &iid, (void **)&fd))) { - COMDLG_FILTERSPEC spec[2]; DWORD opt = 0; - spec[0].pszName = T(L"Dyna Pro USB driver (*.zip;*.inf)", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)"); spec[0].pszSpec = L"*.zip;*.inf"; - spec[1].pszName = T(L"All files", L"所有檔案"); spec[1].pszSpec = L"*.*"; - fd->lpVtbl->SetFileTypes(fd, 2, spec); - fd->lpVtbl->SetTitle(fd, T(L"Choose the Dyna Pro USB driver: Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", - L"選擇 Dyna Pro USB 驅動程式:Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf")); - fd->lpVtbl->SetOkButtonLabel(fd, T(L"Use this", L"使用這個")); - if (SUCCEEDED(fd->lpVtbl->GetOptions(fd, &opt))) fd->lpVtbl->SetOptions(fd, opt | FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST | FOS_PATHMUSTEXIST); - hr = fd->lpVtbl->Show(fd, g_hwnd); - if (SUCCEEDED(hr)) { - IShellItem *si = NULL; - if (SUCCEEDED(fd->lpVtbl->GetResult(fd, &si)) && si) { - WCHAR *name = NULL; - if (SUCCEEDED(si->lpVtbl->GetDisplayName(si, SIGDN_FILESYSPATH, &name)) && name) { - if (lstrlenW(name) < MAX_PATH) { lstrcpyW(p, name); ok = TRUE; } - CoTaskMemFree(name); - } - si->lpVtbl->Release(si); - } - } - fd->lpVtbl->Release(fd); - } else { - OPENFILENAMEW of; - zero(&of, sizeof(of)); - of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; - of.lpstrFilter = T(L"Dyna Pro USB driver (*.zip;*.inf)\0*.zip;*.inf\0All files\0*.*\0", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)\0*.zip;*.inf\0所有檔案\0*.*\0"); - of.lpstrTitle = T(L"Choose Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"選擇 Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf"); - of.lpstrFile = p; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; - ok = GetOpenFileNameW(&of); - } - if (SUCCEEDED(hrc)) CoUninitialize(); - return ok && p[0]; -} - -// "Choose file..." for the driver: a zip, or an .inf of the folder it is unpacked in (the folder that holds the expected layout is looked for from there, up to -// two levels up); the same check as for a copy found by the search, then the same pages. -static void choose_driver_source(void) -{ - WCHAR p[MAX_PATH], d[MAX_PATH]; int rc, k, n; HCURSOR old; - if (!pick_driver_file(p)) return; - n = lstrlenW(p); - if (n > 4 && !lstrcmpiW(p + n - 4, L".inf")) { // an .inf: use the folder it is in, or the first of its parents with the layout (ftdibus.inf, i386, amd64) - WCHAR *e; - for (e = p + n; e > p && *e != '\\'; e--); - *e = 0; // the file's folder - lstrcpyW(d, p); - for (k = 0; k < 3; k++) { - if (driver_layout_ok(d)) { lstrcpyW(p, d); break; } - for (e = d + lstrlenW(d); e > d && *e != '\\'; e--); - if (e == d) break; - *e = 0; - } - } - { WCHAR l[MAX_PATH + 60]; lstrcpyW(l, L"driver: the user chose "); lstrcatW(l, p); user_log(l); } - g_acq = &ACQ_DRIVER; lstrcpyW(g_drvzip, p); - old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); - rc = driver_check(g_drvzip, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); - g_drv_how |= DRV_SRC_PICKED; // chosen by the user: always checked; declining its warning does not touch the memory of declined auto-found copies - user_log(L"driver: a file the user chose is checked as it is (the skip of declined auto-found copies does not apply to it)"); - g_drv_bl = driver_bitlocker(); - SetCursor(old); - acq_driver_result(rc); -} - -static void jd_acquire(int job, int rc) { (void)job; acq_done(rc); } - -static void jd_extract(int job, int rc) -{ - (void)job; - if (rc == PK_OK) { after_extract(); return; } - drop_msi(); // a failed or cancelled extraction leaves no decrypted copy - if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); return; } - if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page - WCHAR nm[100]; - lstrcpynW(nm, file_name(g_pkg), 100); - pkg_reject(g_pkg); - g_acq = &ACQ_SETUP; g_pkg_found = FALSE; - wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); - set_page(P_FAIL); - return; - } - if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } - lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); - set_page(P_ERROR); -} - -static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX -{ - if (g_flow == FLOW_MAIN_WITH_DRIVER && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) - cleanup_files(); - g_fix_done = TRUE; - if (rc == DRV_SUCCESS) drv_reboot_now(); - else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); - else drv_error(rc); - return; - } - if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } - if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile - if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); - set_page(g_flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it - return; - } - wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", - L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); - set_page(P_ERROR); -} - -static void jd_drv_prep(int job, int rc) -{ - (void)job; - if (rc == DRV_SUCCESS) { drv_reboot_now(); return; } - if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides - if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); - set_page(g_flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); - return; - } - drv_error(rc); -} - -static void jd_drv_install(int job, int rc) -{ - (void)job; - if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); return; } - if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } - if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed - lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); - set_page(P_DRV_RETRY); return; - } - drv_error(rc); -} - -static void jd_drv_cancel(int job, int rc) -{ - (void)job; (void)rc; - DestroyWindow(g_hwnd); // cancelled (or not allowed to: nothing else can be done from here) -} - -static void job_done(int job, int rc) -{ - WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; - if (g_close_asked) { g_opts.close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) - if (g_closing) { DestroyWindow(g_hwnd); return; } - JOBS[job].on_done(job, rc); -} - -/* ---------- the pages: what a click, Esc or the close button does on each ---------- */ - -// The first page's options are read when it is left, whichever button leaves it. -static void take_options(void) -{ - g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; -} - -static void end_wizard(void) { DestroyWindow(g_hwnd); } - -// The primary button (right-most, Enter), page by page. -static void pri_hello(void) -{ - take_options(); - if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); return; } // the fix is current: straight to the driver - g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it - if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } -} -static void pri_fail(void) { acq_start(g_acq); } // Retry / Continue download -static void pri_password(void) -{ - if (!take_password()) { - status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", - L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。"), TRUE); - SetFocus(H(ID_EDIT)); - return; - } - start_job(JOB_EXTRACT, P_EXTRACT); -} -static void pri_check(void) { set_page(P_LICENSE); } -static void pri_license(void) { go_install(JOB_INSTALL_ALL); } -static void pri_running(void) -{ - if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only - start_job(g_pending, P_INSTALL); -} -static void pri_done(void) -{ - start_dynarun(); - end_wizard(); -} -static void pri_drv_intro(void) { acq_start(&ACQ_DRIVER); } -static void pri_drv_press7(void) { if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); } // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage -static void pri_drv_hash(void) { hash_choice(FALSE); } // "Cancel" is the default button -static void pri_drv_retry(void) { g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); } // the staged driver is still there -static void pri_drv_warn(void) { start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); } -static void pri_drv_bde(void) { drv_reboot_now(); } // "Restart anyway" -static void pri_drv_done(void) -{ - if (can_start_dynarun()) start_dynarun(); // "Start DynaRun" (otherwise the button reads "Close") - end_wizard(); -} -static void pri_fetch(void) // "Pause" / "Resume" -{ - if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part - else if (g_busy && !g_pause_req) { - g_pause_req = TRUE; - InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag - text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); - } -} - -// Closing the window (the X, Alt+F4, and "Cancel" / "Close" where that is what leaving means). A page can refuse it (PF_NOCLOSE), or do something first -// (its close hook, which then also ends the wizard if that is what closing means there). -static void on_close(void) -{ - const page_desc *d = &PAGES[g_page]; - if (d->flags & PF_NOCLOSE) return; // msiexec / registry / system tool work must not be interrupted - if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } - if (d->close) d->close(); else end_wizard(); -} -// after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) -static void cls_drv_task(void) -{ - if (g_flow == FLOW_DRIVER_CONTINUE && is_admin()) driver_cancel(&g_opts); - end_wizard(); -} -static void cls_drv_bde(void) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and the suspension were armed: take them back first - -// The second button (and Esc): Cancel / Skip / Later / Close. A page without a handler closes. button FALSE: Esc. -static void sec_none(BOOL button) { (void)button; } // the page "Installing": no way out -static void sec_fail(BOOL button) -{ - if (button && g_acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver - else on_close(); -} -static void sec_fetch(BOOL button) -{ - if (!button) { on_close(); return; } - if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once - else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) -} -static void sec_drv_intro(BOOL button) -{ - if (button && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver - else on_close(); -} -static void sec_drv_hash(BOOL button) { hash_choice(button); } // the button is "Install anyway"; Esc is a cancel -static void sec_drv_cancel(BOOL button) { (void)button; start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and BitLocker back - -// The third button (left of the second one): another way on. On the error page: choose the file (or folder) yourself. -static void thr_fail(void) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } -static void thr_hello(void) -{ - if (!g_hint) return; // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) - take_options(); - g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; - user_log(L"DynaRun update hint: the user chose to download the new version"); - g_acq_fresh = TRUE; // an older package found on this computer would not be the new version - acq_start(&ACQ_SETUP); -} - -// The extra button (bottom left, link style). -static void ext_dynapro(void) { ShellExecuteW(g_hwnd, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); } // also after a failed driver download (P_DRV_HASH) -static void ext_recovery_key(void) { ShellExecuteW(g_hwnd, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); } -static void ext_manual(void) { ShellExecuteW(g_hwnd, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); } -static void ext_hello(void) // "Reinstall the fix..." / "Installed elsewhere..." -{ - if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } - else choose_exe(); -} - -// Alt+key shortcuts that exist on one page only (the controls' own & mnemonics do not work while the focus is in the edit or the rich text). -static BOOL when_drv_avail(void) { return g_drv_avail; } -static const hotkey KEYS_HELLO[] = { { 'T', ID_PIN, NULL }, { 'S', ID_AUTO, NULL }, { 'D', ID_DRV, when_drv_avail }, { 0 } }; -static const hotkey KEYS_LICENSE[] = { { 'A', ID_CHECK, NULL }, { 0 } }; - -// One row per page, indexed by the P_* value. A NULL handler means: nothing happens (second: the page closes). -// enter builds the page (the controls, texts and buttons it shows); set_page has reset everything before it -// primary second third extra what the four buttons do (ID_PRIMARY / Enter, ID_SECOND / Esc, ID_THIRD, ID_EXTRA) -// close what closing the window does on this page, after the common refusals; NULL: ends the wizard -// keys the page's Alt+key shortcuts -// flags PF_NOCLOSE: closing the window is refused -// A new page: a P_* value, an enter_* function, a row here. -static const page_desc PAGES[P_COUNT] = { - [P_HELLO] = { enter_hello, pri_hello, NULL, thr_hello, ext_hello, NULL, KEYS_HELLO, 0 }, - [P_FETCH] = { enter_fetch, pri_fetch, sec_fetch, NULL, ext_dynapro, NULL, NULL, 0 }, - [P_FAIL] = { enter_fail, pri_fail, sec_fail, thr_fail, ext_dynapro, NULL, NULL, 0 }, - [P_PASSWORD] = { enter_password, pri_password, NULL, NULL, choose_file, NULL, NULL, 0 }, - [P_EXTRACT] = { enter_extract, NULL, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_CHECK] = { enter_check, pri_check, NULL, NULL, ext_dynapro, NULL, NULL, 0 }, - [P_LICENSE] = { enter_license, pri_license, NULL, NULL, ext_dynapro, NULL, KEYS_LICENSE, 0 }, - [P_RUNNING] = { enter_running, pri_running, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_INSTALL] = { enter_install, NULL, sec_none, NULL, NULL, NULL, NULL, PF_NOCLOSE }, - [P_DONE] = { enter_done, pri_done, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_ERROR] = { enter_error, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_DRV_INTRO] = { enter_drv_intro, pri_drv_intro, sec_drv_intro, NULL, NULL, NULL, NULL, 0 }, - [P_DRV_PRESS7] = { enter_drv_press7, pri_drv_press7, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_DRV_HASH] = { enter_drv_hash, pri_drv_hash, sec_drv_hash, NULL, ext_dynapro, NULL, NULL, 0 }, - [P_DRV_BDE] = { enter_drv_bde, pri_drv_bde, sec_drv_cancel, NULL, ext_recovery_key, cls_drv_bde, NULL, 0 }, - [P_DRV_PREP] = { enter_drv_prep, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, - [P_DRV_REBOOT] = { enter_drv_reboot, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_DRV_RETRY] = { enter_drv_retry, pri_drv_retry, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, - [P_DRV_WARN] = { enter_drv_warn, pri_drv_warn, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, - [P_DRV_INSTALL] = { enter_drv_install, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, - [P_DRV_DONE] = { enter_drv_done, pri_drv_done, NULL, NULL, NULL, NULL, NULL, 0 }, - [P_DRV_ERROR] = { enter_drv_error, end_wizard, NULL, NULL, ext_manual, NULL, NULL, 0 }, -}; - -static void set_page(int p) -{ - g_page = p; - g_result = RC_CANCEL; // undecided until a page below says otherwise - g_paused = g_pause_req = FALSE; - g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress - g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does - KillTimer(g_hwnd, TIMER_RUNNING); - vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); - PAGES[p].enter(); - g_note[0] = 0; - g_building = FALSE; - layout(); - if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) - SetFocus(GetNextDlgTabItem(g_hwnd, NULL, FALSE)); // something must hold the keyboard focus -} - -// The window asks the table (the controls are the same on every page). -static void on_primary(void) { if (PAGES[g_page].primary) PAGES[g_page].primary(); } -static void on_second(BOOL button) { if (PAGES[g_page].second) PAGES[g_page].second(button); else on_close(); } -static void on_third(void) { if (PAGES[g_page].third) PAGES[g_page].third(); } -static void on_extra(void) { if (PAGES[g_page].extra) PAGES[g_page].extra(); } -// The control id of the Alt+key the current page has a shortcut for; 0 when there is none. -static int page_hotkey(WPARAM key) -{ - const hotkey *k = PAGES[g_page].keys; - for (; k && k->key; k++) if (k->key == key && (!k->when || k->when())) return k->id; - return 0; -} - -/* ---------- window ---------- */ - -#define FOOTER 76 // height of the grey button band, at 96 dpi - -static HWND add(const WCHAR *cls, DWORD style, int id) -{ - DWORD ex = (id == ID_EDIT || id == ID_RICH || id == ID_INFO) ? WS_EX_CLIENTEDGE : 0; // themed border like other Windows text boxes - HWND c = CreateWindowExW(ex, cls, L"", WS_CHILD | WS_CLIPSIBLINGS | style, 0, 0, 0, 0, g_hwnd, (HMENU)(INT_PTR)id, NULL, NULL); - SendMessageW(c, WM_SETFONT, (WPARAM)(id == ID_TITLE ? g_big : g_font), FALSE); - return c; -} - -static void create_controls(void) -{ - WCHAR lib[MAX_PATH]; const WCHAR *rich; - // by full system path, never searched for (a planted msftedit.dll in the current or the exe's folder is not loaded) - GetSystemDirectoryW(lib, MAX_PATH - 20); lstrcatW(lib, L"\\msftedit.dll"); - if (LoadLibraryW(lib)) rich = L"RICHEDIT50W"; - else { GetSystemDirectoryW(lib, MAX_PATH - 20); lstrcatW(lib, L"\\riched20.dll"); LoadLibraryW(lib); rich = L"RichEdit20W"; } - add(L"STATIC", WS_VISIBLE | SS_NOPREFIX, ID_TITLE); - add(L"STATIC", WS_VISIBLE | SS_NOPREFIX, ID_BODY); - add(L"EDIT", WS_TABSTOP | ES_PASSWORD | ES_AUTOHSCROLL, ID_EDIT); - add(rich, WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY, ID_RICH); - add(L"EDIT", WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY, ID_INFO); - add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_CHECK); - add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_PIN); - add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_AUTO); - add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX | BS_MULTILINE, ID_DRV); - add(L"STATIC", SS_BITMAP | SS_NOTIFY, ID_IMG); - SetWindowTextW(H(ID_DRV), T(L"Also install the Dyna Pro USB &driver (one restart needed)", L"同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)(&D)")); - SendMessageW(H(ID_DRV), BM_SETCHECK, BST_CHECKED, 0); - SetWindowTextW(H(ID_PIN), T(L"Pin DynaRun to the &taskbar", L"釘選到工作列(&T)")); - SetWindowTextW(H(ID_AUTO), T(L"Start DynaRun when Windows &starts", L"開機時自動啟動 DynaRun(&S)")); - SendMessageW(H(ID_PIN), BM_SETCHECK, g_opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); - SendMessageW(H(ID_AUTO), BM_SETCHECK, g_opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); - SetWindowTextW(H(ID_CHECK), T(L"I &accept the license agreement", L"我接受授權合約(&A)")); - add(PROGRESS_CLASSW, 0, ID_PROG); - add(L"STATIC", SS_NOPREFIX, ID_STATUS); - add(L"BUTTON", WS_TABSTOP, ID_EXTRA); - add(L"BUTTON", WS_TABSTOP, ID_THIRD); - add(L"BUTTON", WS_TABSTOP, ID_SECOND); - add(L"BUTTON", WS_TABSTOP | BS_DEFPUSHBUTTON, ID_PRIMARY); -} - -// Height of a control's text wrapped to width w, or its width when w is 0. (The buffer holds the longest text of any page, the license page's -// info block, with room to spare; a cut-off text would measure too short and the controls below it would overlap the last lines.) -static int measure(int id, int w) -{ - static WCHAR t[4096]; RECT r; HDC dc = GetDC(g_hwnd); - HGDIOBJ old = SelectObject(dc, (HGDIOBJ)SendMessageW(H(id), WM_GETFONT, 0, 0)); - GetWindowTextW(H(id), t, 4096); - r.left = r.top = 0; r.right = w ? w : 10000; r.bottom = 0; - DrawTextW(dc, t, -1, &r, DT_CALCRECT | DT_NOPREFIX | DT_EXPANDTABS | (w ? DT_WORDBREAK : DT_SINGLELINE)); - SelectObject(dc, old); ReleaseDC(g_hwnd, dc); - return w ? r.bottom : r.right; -} - -static BOOL shown(int id) { return (GetWindowLongW(H(id), GWL_STYLE) & WS_VISIBLE) != 0; } // also before the window is shown - -static int status_need(void) -{ - RECT c; - if (!shown(ID_STATUS)) return 0; - GetClientRect(g_hwnd, &c); - return measure(ID_STATUS, c.right - 2 * S(32)) + S(8); -} -static void place(int id, int x, int y, int w, int h) { SetWindowPos(H(id), NULL, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE); } - -// One top-down pass over the visible controls from their measured sizes. apply FALSE: nothing is moved, it returns the client height the page needs -// (the picture and the license text at the size they want); TRUE: places the controls so that the flexible ones (picture, license text, note) use -// what is left above the button band at `bottom`. -static int stack(BOOL apply, int w, int bottom) -{ - int m = S(32), y, h, sh = shown(ID_STATUS) ? measure(ID_STATUS, w) + S(8) : 0; // sh: room the status line needs at this width and DPI (it may wrap) - if (apply) g_sth = sh; - y = S(shown(ID_PIN) ? 18 : 24); // tighter on the first page: its text and the options must fit - h = measure(ID_TITLE, w); if (apply) place(ID_TITLE, m, y, w, h); y += h + S(shown(ID_PIN) ? 10 : 16); - h = measure(ID_BODY, w) + S(6); if (apply) place(ID_BODY, m, y, w, h); y += h + S(shown(ID_PIN) ? 6 : 20); - if (shown(ID_PIN)) { // the two options: side by side when they fit, else stacked - int a = measure(ID_PIN, 0) + S(24), b2 = measure(ID_AUTO, 0) + S(24), side = a + b2 + S(16) <= w; - if (apply) { place(ID_PIN, m, y, a, S(24)); place(ID_AUTO, side ? m + a + S(16) : m, side ? y : y + S(24), b2, S(24)); } - y += side ? S(24) : S(48); - } - if (shown(ID_DRV)) { h = measure(ID_DRV, w - S(24)) + S(8); if (apply) place(ID_DRV, m, y + S(2), w, h); y += h + S(4); } - if (shown(ID_IMG)) { - if (apply) y += place_image(m, y, w, bottom - y - S(10) - sh) + S(8); // the status line below keeps its room - else { int tw, th; fit_image(w, 100000, &tw, &th); y += th + S(8); } - } - if (shown(ID_EDIT)) { if (apply) place(ID_EDIT, m, y, w < S(360) ? w : S(360), S(34)); y += S(34) + S(12); } - if (shown(ID_PROG)) { if (apply) place(ID_PROG, m, y, w, S(20)); y += S(20) + S(10); } - if (shown(ID_RICH)) { - int cy = apply ? bottom - (sh ? sh + S(6) : S(16)) - S(28) : y + S(200) + S(10); // the license text: at least ~200 px high; it gives way to the status line - if (apply) { place(ID_CHECK, m, cy, w, S(28)); place(ID_RICH, m, y, w, cy - S(10) - y); } - y = cy + S(28) + (apply && sh ? S(4) : 0); - } - if (shown(ID_INFO)) { // the details of the setup file: a read-only text box (selectable, copyable) with the rest of the page - int bh2 = apply ? bottom - S(16) - y - sh : S(200); - if (apply) place(ID_INFO, m, y, w, bh2 > S(60) ? bh2 : S(60)); - y += bh2 + S(16); - } - if (shown(ID_STATUS)) { - if (apply) place(ID_STATUS, m, y, w, bottom - y > 0 ? bottom - y : 0); - else y += measure(ID_STATUS, w) + S(8); - } - return apply ? y : y + S(FOOTER); -} - -// Sets the client height of the window to `need` (at least the usual one, at most what the screen's work area allows), keeping it centred. -static void fit_window(int need) -{ - RECT wr, cr, work; int target = need > S(580) ? need : S(580), nc, nh, y; - if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0)) return; - GetWindowRect(g_hwnd, &wr); GetClientRect(g_hwnd, &cr); - nc = (wr.bottom - wr.top) - cr.bottom; - if (target + nc > work.bottom - work.top) target = work.bottom - work.top - nc; - if (target == cr.bottom) return; - nh = target + nc; - y = wr.top - (nh - (wr.bottom - wr.top)) / 2; - if (y + nh > work.bottom) y = work.bottom - nh; - if (y < work.top) y = work.top; - SetWindowPos(g_hwnd, NULL, wr.left, y, wr.right - wr.left, nh, SWP_NOZORDER | SWP_NOACTIVATE); -} - -// Lays the visible controls out top-down from the measured text heights (the window grows when a page needs more room); buttons right-aligned in the footer. -static void layout(void) -{ - static const int btn[] = { ID_PRIMARY, ID_SECOND, ID_THIRD }; - RECT c; int m = S(32), w, x, i, bw, bh = S(36), by, bottom; - GetClientRect(g_hwnd, &c); - w = c.right - 2 * m; - fit_window(stack(FALSE, w, 0)); - GetClientRect(g_hwnd, &c); - bottom = c.bottom - S(FOOTER); - stack(TRUE, w, bottom); - by = bottom + (S(FOOTER) - bh) / 2; - for (x = c.right - m, i = 0; i < 3; i++) { - if (!shown(btn[i])) continue; - bw = measure(btn[i], 0) + S(48); if (bw < S(112)) bw = S(112); - x -= bw; place(btn[i], x, by, bw, bh); x -= S(10); - } - bw = measure(ID_EXTRA, 0) + S(40); if (bw < S(112)) bw = S(112); - place(ID_EXTRA, m, by, bw, bh); - InvalidateRect(g_hwnd, NULL, TRUE); -} - -// The installed UI font for the language (font linking would mix two fonts in Chinese text). -static int CALLBACK font_found(const LOGFONTW *lf, const TEXTMETRICW *tm, DWORD type, LPARAM found) -{ *(BOOL *)found = TRUE; (void)lf; (void)tm; (void)type; return 0; } -static void pick_face(LOGFONTW *lf) -{ - static const WCHAR *zh[] = { L"Microsoft JhengHei UI", L"Microsoft JhengHei", L"PMingLiU", NULL }, - *en[] = { L"Segoe UI", L"Tahoma", NULL }; - const WCHAR **f; LOGFONTW q; HDC dc = GetDC(NULL); BOOL found; - for (f = g_zh ? zh : en; *f; f++) { - zero(&q, sizeof(q)); q.lfCharSet = DEFAULT_CHARSET; lstrcpyW(q.lfFaceName, *f); found = FALSE; - EnumFontFamiliesExW(dc, &q, font_found, (LPARAM)&found, 0); - if (found) { lstrcpyW(lf->lfFaceName, *f); lf->lfCharSet = DEFAULT_CHARSET; break; } - } - ReleaseDC(NULL, dc); -} - -// Brings the wizard back in front after the elevated part. SetForegroundWindow alone is refused -// when another process (msiexec's progress window, "Windows configures ...") had the foreground: -// raise it to the top of the z-order anyway, then borrow the foreground thread's input to activate it. -static void bring_front(HWND h) -{ - HWND fg = GetForegroundWindow(); DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0; - if (IsIconic(h)) ShowWindow(h, SW_RESTORE); - SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); - SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); - if (SetForegroundWindow(h) || !other || other == me) return; - if (AttachThreadInput(me, other, TRUE)) { - BringWindowToTop(h); SetForegroundWindow(h); SetFocus(h); - AttachThreadInput(me, other, FALSE); - } - if (GetForegroundWindow() != h) { // still refused: at least flash the taskbar button - FLASHWINFO f; f.cbSize = sizeof(f); f.hwnd = h; f.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG; f.uCount = 3; f.dwTimeout = 0; - FlashWindowEx(&f); - } -} - -// Image name (no path) of the process that owns window w; "" when it cannot be read. -static const WCHAR *owner_name(HWND w, WCHAR *buf, DWORD n) -{ - typedef BOOL (WINAPI *qfpin_t)(HANDLE, DWORD, LPWSTR, PDWORD); - static qfpin_t qfpin; DWORD pid = 0; HANDLE p; const WCHAR *name = buf, *c; - buf[0] = 0; - if (!qfpin) qfpin = (qfpin_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "QueryFullProcessImageNameW"); - if (!qfpin || !GetWindowThreadProcessId(w, &pid)) return buf; - if ((p = OpenProcess(0x1000 /* PROCESS_QUERY_LIMITED_INFORMATION */, FALSE, pid)) != NULL) { - if (!qfpin(p, 0, buf, &n)) buf[0] = 0; - CloseHandle(p); - } - for (c = buf; *c; c++) if (*c == '\\') name = c + 1; - return name; -} - -// While the elevated part installs: the UAC prompt and msiexec's progress window take the foreground, -// and afterwards Windows may activate Explorer, which then covers the wizard. Keep the wizard just -// behind msiexec's window, and above Explorer; other programs the user switches to are left alone. -static void keep_up(HWND h) -{ - static WCHAR b[MAX_PATH]; const WCHAR *n; HWND fg = GetForegroundWindow(); DWORD pid = 0; - if (!fg || fg == h || IsIconic(h)) return; - GetWindowThreadProcessId(fg, &pid); - if (pid == GetCurrentProcessId()) return; - n = owner_name(fg, b, MAX_PATH); - if (!lstrcmpiW(n, L"msiexec.exe") || !lstrcmpiW(n, L"consent.exe")) - SetWindowPos(h, fg, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - else if (!lstrcmpiW(n, L"explorer.exe")) { - SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); - } -} - -static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) -{ - switch (m) { - case WM_COMMAND: - switch (LOWORD(w)) { - case IDOK: case ID_PRIMARY: if (IsWindowVisible(H(ID_PRIMARY)) && IsWindowEnabled(H(ID_PRIMARY))) on_primary(); break; - case IDCANCEL: case ID_SECOND: on_second(LOWORD(w) == ID_SECOND); break; - case ID_THIRD: on_third(); break; - case ID_EXTRA: on_extra(); break; - case ID_CHECK: EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); break; - } - return 0; - case WM_PROGRESS: { - static WCHAR b[100]; - DWORD done = (DWORD)w, total = (DWORD)l; - if (done == (DWORD)-1) { // download over, the file is being checked: not pausable, bar and text say so - if (g_page == P_FETCH && !g_paused) { - g_marquee = TRUE; - progress(TRUE); - if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(g_acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves - else { // the file is being checked: not pausable - g_checking = TRUE; - text(ID_TITLE, T(L"Checking the driver", L"正在檢查驅動程式")); - status(T(L"Checking the driver...", L"正在檢查驅動程式…"), FALSE); - EnableWindow(H(ID_PRIMARY), FALSE); - } - layout(); - } - return 0; - } - if (g_checking) return 0; - if (total && g_marquee) { g_marquee = FALSE; progress(FALSE); } // the size became known - else if (!total && !g_marquee && g_page == P_FETCH) { g_marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received - if (!g_marquee) SendMessageW(H(ID_PROG), PBM_SETPOS, total ? MulDiv(done, 1000, total) : 0, 0); - g_dlhave = done; if (total) g_dltotal = total; - if (g_page == P_FETCH) { - if (total != g_total) { fetch_body(total); layout(); } - wsprintfW(b, total ? L"%lu.%lu / %lu.%lu MB" : L"%lu.%lu MB", done / 1048576, done % 1048576 / 104858, - total / 1048576, total % 1048576 / 104858); - status(b, FALSE); - } - return 0; - } - case WM_TIMER: // the page follows DynaRun being closed (or started again) - if (w == TIMER_RUNNING && g_page == P_RUNNING) { - int run = running_programs(); - if (run != g_lastrun) { - g_lastrun = run; - if (run) set_page(P_RUNNING); - else { // closed by the user meanwhile: plain "Install" - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); - status(T(L"DynaRun is closed.", L"DynaRun 已關閉。"), FALSE); - layout(); - } - } - } - if (w == ID_KEEPUP) keep_up(h); - return 0; - case WM_HINT: - g_hint = (int)w; - if (g_hint && g_page == P_HELLO && !g_busy) set_page(P_HELLO); - return 0; - case WM_JOBDONE: - KillTimer(h, ID_KEEPUP); - // after the elevated part (which allows it) the wizard comes back in front of other windows - if (JOBS[w].bring_front) bring_front(h); - job_done((int)w, (int)l); return 0; - case WM_PAINT: { - PAINTSTRUCT ps; RECT c; HDC dc = BeginPaint(h, &ps); HPEN pen, old; - GetClientRect(h, &c); c.top = c.bottom - S(FOOTER); - FillRect(dc, &c, g_grey); - pen = CreatePen(PS_SOLID, 1, RGB(223, 223, 223)); old = SelectObject(dc, pen); - MoveToEx(dc, 0, c.top, NULL); LineTo(dc, c.right, c.top); - SelectObject(dc, old); DeleteObject(pen); - EndPaint(h, &ps); - return 0; - } - case WM_CTLCOLORBTN: - return (LRESULT)g_grey; // push buttons sit on the grey footer - case WM_CTLCOLORSTATIC: - SetBkColor((HDC)w, RGB(255, 255, 255)); - SetTextColor((HDC)w, (HWND)l == H(ID_STATUS) && g_status_err ? RGB(192, 0, 0) : GetSysColor(COLOR_WINDOWTEXT)); - return (LRESULT)g_white; - case WM_CLOSE: on_close(); return 0; - case WM_DESTROY: SecureZeroMemory(g_pw, sizeof(g_pw)); SetWindowTextW(H(ID_EDIT), L""); drop_msi(); PostQuitMessage(0); return 0; - } - return DefWindowProcW(h, m, w, l); -} - -int wizard(const install_opts *init) -{ - INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; int key; - static WCHAR title[100]; - g_opts = *init; // the command line's options; the pages change this copy - icc.dwSize = sizeof(icc); icc.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; - InitCommonControlsEx(&icc); - dc = GetDC(NULL); g_dpi = GetDeviceCaps(dc, LOGPIXELSY); ReleaseDC(NULL, dc); - zero(&nm, sizeof(nm)); -#pragma warning(suppress: 4996) - nm.cbSize = LOBYTE(GetVersion()) >= 6 ? sizeof(nm) : sizeof(nm) - sizeof(int); // XP: no iPaddedBorderWidth - SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, nm.cbSize, &nm, 0); - lf = nm.lfMessageFont; pick_face(&lf); lf.lfQuality = CLEARTYPE_QUALITY; - lf.lfHeight = -S(17); lf.lfWeight = FW_NORMAL; g_font = CreateFontIndirectW(&lf); // larger than usual: easy to read - lf.lfHeight = -S(28); lf.lfWeight = FW_BOLD; g_big = CreateFontIndirectW(&lf); - g_white = CreateSolidBrush(RGB(255, 255, 255)); - g_grey = CreateSolidBrush(RGB(243, 243, 243)); - - zero(&wc, sizeof(wc)); - wc.lpfnWndProc = wndproc; wc.hInstance = GetModuleHandleW(NULL); wc.lpszClassName = L"DynaRunFixSetup"; - wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); wc.hbrBackground = g_white; - wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(10)); - RegisterClassW(&wc); - r.left = r.top = 0; r.right = S(620); r.bottom = S(580); // the first page: text, disclaimer, the two options and a one-line note - AdjustWindowRect(&r, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); - wsprintfW(title, T(L"DynaRunFix Setup %s", L"DynaRunFix 安裝程式 %s"), WIDEN(DRF_DISPLAY)); - g_hwnd = CreateWindowExW(0, wc.lpszClassName, title, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN, - (GetSystemMetrics(SM_CXSCREEN) - (r.right - r.left)) / 2, (GetSystemMetrics(SM_CYSCREEN) - (r.bottom - r.top)) / 2, - r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); - if (!g_hwnd) return RC_FAIL; - create_controls(); - g_installed = locate_dynarun(); - g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); - // g_drvmode (set by cmd_driver / cmd_driver_continue before the wizard starts) is read here and nowhere else - if (g_drvmode == DRV_MODE_STANDALONE) g_flow = FLOW_DRIVER_ONLY; - else if (g_drvmode == DRV_MODE_CONTINUE) g_flow = FLOW_DRIVER_CONTINUE; - else g_flow = g_installed && g_drv_avail && fix_current() ? FLOW_DRIVER_ONLY : FLOW_MAIN; // the fix is there and current: only the driver is missing - if (g_drvmode != DRV_MODE_NONE) drv_start(); - else { - DWORD tid; - set_page(P_HELLO); - if (g_installed) CloseHandle(CreateThread(NULL, 0, hint_proc, NULL, 0, &tid)); // is there a newer DynaRun? (a hint; the page does not wait) - } - ShowWindow(g_hwnd, SW_SHOWNORMAL); - SetForegroundWindow(g_hwnd); - while (GetMessageW(&msg, NULL, 0, 0) > 0) { - if (msg.message == WM_KEYDOWN && msg.wParam == VK_TAB) { // Tab between the visible controls - HWND n = GetNextDlgTabItem(g_hwnd, GetFocus(), GetKeyState(VK_SHIFT) < 0); - if (n) SetFocus(n); - continue; - } - if (msg.message == WM_SYSKEYDOWN && (key = page_hotkey(msg.wParam)) != 0) { // Alt+key shortcuts of the page - SendMessageW(H(key), BM_CLICK, 0, 0); - continue; - } - if (IsDialogMessageW(g_hwnd, &msg)) continue; - TranslateMessage(&msg); DispatchMessageW(&msg); - } - g_hwnd = NULL; - if (g_page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it - return g_result; -} +// The rest of this file is split by flow into src\wizard\*.h. They are fragments of THIS translation unit (static functions and file-local state refer to +// each other), not headers and not separate .c files: build.cmd and the tests compile only wizard.c. A fragment uses what the ones before it define; what a +// flow calls in a later one is declared here. +static BOOL can_skip_driver(void); // driver.h +static void skip_driver(void); +static void acq_driver_result(int rc); +static void choose_driver_source(void); +static void pin_hint(WCHAR *b); // main.h +static void acp_warning(void); +static BOOL can_start_dynarun(void); +static void start_job(int job, int page); +static void install(int job); +static void go_install(int job); +static void on_close(void); +static void end_wizard(void); + +#include "wizard/layout.h" // the window: UI helpers, screenshots, controls, layout +#include "wizard/acquire.h" // getting the DynaRun setup (and the driver package): download, password, check, license +#include "wizard/driver.h" // the driver pages and jobs +#include "wizard/main.h" // the main flow, the jobs, the page table, the window procedure, wizard() diff --git a/src/wizard/acquire.h b/src/wizard/acquire.h new file mode 100644 index 0000000..9968178 --- /dev/null +++ b/src/wizard/acquire.h @@ -0,0 +1,423 @@ +// Getting the DynaRun V3 setup (and, through the same pages, the Dyna Pro USB driver): looking for a copy on this computer, the download with pause / resume, +// the one error page, the password, the check of the file and the license pages. Its jobs: JOB_ACQUIRE, JOB_EXTRACT. + +// A download that can be continued where it stopped (its .part and ETag are there): appends how much is already received to g_note and returns TRUE. +// The button then reads "Continue download" instead of "Retry". +static BOOL resume_note(void) +{ + DWORD have = 0; WCHAR a[24], t[24], line[200]; + if (!g_acq->partial(&have)) return FALSE; + fmt_mb(a, have); + if (g_dltotal > have) { + fmt_mb(t, g_dltotal); + wsprintfW(line, T(L"\n\n%s of %s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s / %s MB,按「繼續下載」會從中斷的地方接續。"), a, t); + } else + wsprintfW(line, T(L"\n\n%s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s MB,按「繼續下載」會從中斷的地方接續。"), a); + lstrcatW(g_note, line); + return TRUE; +} + +// The size is the server's (Content-Length), shown once the download has started. +static void fetch_body(DWORD total) +{ + WCHAR size[40], b[500]; + size[0] = 0; + if (total) wsprintfW(size, T(L" (%lu.%lu MB)", L"(%lu.%lu MB)"), total / 1048576, total % 1048576 / 104858); + wsprintfW(b, T(L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" + L"If it does not work, click \"Open website\" to get it from the website yourself.", + L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" + L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(g_acq), size); + g_total = total; + text(ID_BODY, b); +} + +static void enter_fetch(void) // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver +{ + WCHAR *b = pgbuf; + wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); + text(ID_TITLE, b); + fetch_body(0); + progress(FALSE); status(L" ", FALSE); + buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); +} + +static void enter_fail(void) // the one error page of the acquire flow +{ + g_result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) + WCHAR *b = pgbuf; + if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); + else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); + if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence + text(ID_TITLE, b); + { + BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) + BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); + g_badfile = FALSE; + wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" + L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", + L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" + L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); + text(ID_BODY, b); + buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), + T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); + } +} + +static void enter_password(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); + wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", + L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); + text(ID_BODY, b); + vis(ID_EDIT, TRUE); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); + if (g_note[0]) status(g_note, TRUE); + SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); +} + +static void enter_extract(void) +{ + text(ID_TITLE, T(L"Opening the setup file", L"正在打開安裝檔")); + text(ID_BODY, T(L"Please wait...", L"請稍候…")); + progress(FALSE); + buttons(NULL, T(L"Cancel", L"取消"), NULL); +} + +static void enter_check(void) // everything we know about the setup file, to compare with what Dyna Pro gives +{ + text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); + text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", + L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); + SetWindowTextW(H(ID_INFO), g_info); + vis(ID_INFO, TRUE); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_license(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); + lstrcpyW(b, g_nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", + L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") + : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); + text(ID_BODY, b); + SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") + : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); + vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); + EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); + SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus + if (g_note[0]) status(g_note, TRUE); +} + +static void on_progress(DWORD done, DWORD total) +{ + int pct = total ? MulDiv(done, 1000, total) : 0; + if (done == (DWORD)-1) { PostMessageW(g_hwnd, WM_PROGRESS, done, total); return; } // the "checking" phase (driver_fetch) + if (pct == g_lastpct) return; + g_lastpct = pct; + PostMessageW(g_hwnd, WM_PROGRESS, done, total); +} + +// What a job runs on the worker thread (job_proc), and what is done with its result on the UI thread (JOBS[].on_done, below). +static int jp_acquire(void) // the DynaRun setup or the driver: a copy on this computer first, else the download +{ + int rc; + on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts + if (g_acq == &ACQ_DRIVER) { + rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + if (rc == PK_OK || rc == DRV_W_HASH) { + on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked + g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds + } + } else { + g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); + if (g_pkg_found) rc = PK_OK; + else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } + } + return rc; +} + +static int jp_extract(void) +{ + int rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); // a size cap, and a free target (shared with the driver) + SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more + return rc; +} + +static void open_package(void); + +typedef struct { const char *p; LONG left; } rtfsrc; + +static DWORD CALLBACK rtf_in(DWORD_PTR c, LPBYTE b, LONG n, LONG *got) +{ + rtfsrc *s = (rtfsrc *)c; LONG i, k = n < s->left ? n : s->left; + for (i = 0; i < k; i++) b[i] = (BYTE)s->p[i]; + s->p += k; s->left -= k; *got = k; + return 0; +} + +// Starts the acquire flow for the DynaRun setup or the driver (the same pages; a network check comes first inside the download). +static void acq_start(const acq_desc *d) +{ + g_acq = d; + if (d == &ACQ_SETUP) { g_pkg_ours = g_pkg_found = FALSE; g_acq_local_ok = !g_acq_fresh && g_nfound < 8; } // the cap: pkg_reject remembers 8 files + else { g_drvzip[0] = 0; g_drv_allow = FALSE; } + start_job(JOB_ACQUIRE, P_FETCH); +} + +// Whatever the file is called: only the real DynaRun setup is ever installed. +static void not_dynarun(void) +{ + WCHAR m[MAX_PATH], nm[100]; + g_acq = &ACQ_SETUP; + lstrcpynW(nm, file_name(g_pkg), 100); // wsprintfW does not check the size of g_note: a long file name must not overflow it + wsprintfW(g_note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", + L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。"), nm); + pkg_reject(g_pkg); + pkg_temp_dir(m); lstrcatW(m, L"\\Setup.msi"); + if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); // our extracted copy + if (g_pkg_ours) DeleteFileW(g_pkg); + if (g_pkg_found) { acq_start(&ACQ_SETUP); return; } // we picked it, not the user: quietly try the next one or download + g_badfile = TRUE; + set_page(P_FAIL); +} + +static void fmt_size(WCHAR *o, ULONGLONG n) +{ + DWORD v = (DWORD)n; + wsprintfW(o, T(L"%lu bytes (%lu.%lu MB)", L"%lu 位元組(%lu.%lu MB)"), v, v / 1048576, v % 1048576 / 104858); +} + +// Shown on the license page so the user can compare with what Dyna Pro provides. Display only: nothing here blocks the install. +static void build_info(void) +{ + WCHAR hex[65], hex2[65], ver[64], mfr[128], sz[80]; ULONGLONG size = 0, size2 = 0; BOOL same = !lstrcmpiW(g_pkg, g_msi), okp, okm = FALSE; + HCURSOR old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); + okp = pkg_sha256(g_pkg, hex, &size); + if (!same) okm = pkg_sha256(g_msi, hex2, &size2); + msi_info(g_msi, ver, mfr, 64); + SetCursor(old); + g_info[0] = 0; + lstrcatW(g_info, T(L"Source: ", L"來源:")); + lstrcatW(g_info, g_pkg_ours ? T(L"downloaded now from dynapro.co.uk", L"剛從 dynapro.co.uk 下載") + : g_pkg_found ? T(L"found on this computer, not downloaded by DynaRunFix", L"在這台電腦上找到,不是 DynaRunFix 下載的") + : T(L"chosen by you", L"你選擇的檔案")); + lstrcatW(g_info, T(L"\r\n\r\nFile:\r\n", L"\r\n\r\n檔案路徑:\r\n")); lstrcatW(g_info, g_pkg); + fmt_size(sz, size); + lstrcatW(g_info, T(L"\r\n\r\nSize: ", L"\r\n\r\n大小:")); lstrcatW(g_info, sz); + lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okp ? hex : L"?"); + if (!same) { + fmt_size(sz, size2); + lstrcatW(g_info, T(L"\r\n\r\nSetup.msi inside the file, size: ", L"\r\n\r\n檔案裡的 Setup.msi,大小:")); lstrcatW(g_info, sz); + lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okm ? hex2 : L"?"); + } + lstrcatW(g_info, L"\r\n\r\nMSI ProductVersion: "); lstrcatW(g_info, ver[0] ? ver : L"?"); + lstrcatW(g_info, L"\r\nManufacturer: "); lstrcatW(g_info, mfr[0] ? mfr : L"?"); +} + +static void after_extract(void) +{ + if (msi_check(g_msi, g_ver, 64) != MSI_DYNARUN) { not_dynarun(); return; } + release(g_rtf); + g_rtf = msi_license_rtf(g_msi); + g_nolicense = !g_rtf; + build_info(); + if (g_rtf) { + rtfsrc src; EDITSTREAM es; + src.p = g_rtf; src.left = lstrlenA(g_rtf); + es.dwCookie = (DWORD_PTR)&src; es.dwError = 0; es.pfnCallback = rtf_in; + SendMessageW(H(ID_RICH), EM_STREAMIN, SF_RTF, (LPARAM)&es); + } else // never install without the user accepting Dyna Pro's terms + SetWindowTextW(H(ID_RICH), T(L"DynaRun V3 is licensed to you by Dyna Pro Dynamometers Ltd under their end-user license agreement " + L"and their terms and conditions of sale. The agreement is part of Dyna Pro's setup, but this " + L"installer could not read it from this setup file.\r\n\r\n" + L"To read it, contact Dyna Pro (support@dynapro.co.uk) or see their website " + L"(\"Open website\" below).\r\n\r\n" + L"Install only if you accept Dyna Pro's license terms.", + L"DynaRun V3 是 Dyna Pro Dynamometers Ltd 依其使用者授權合約及銷售條款授權給你使用的軟體。" + L"授權合約包含在 Dyna Pro 的安裝檔中,但本安裝程式無法從這個安裝檔讀出合約內容。\r\n\r\n" + L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟官網」)。\r\n\r\n" + L"請在同意 Dyna Pro 的授權條款後再安裝。")); + SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); + set_page(P_CHECK); +} + +static void open_package(void) +{ + if (msi_check(g_pkg, NULL, 0) != MSI_NONE) { lstrcpyW(g_msi, g_pkg); after_extract(); return; } // by content, not by extension + if (zip_open(g_pkg, &g_zip) != PK_OK) { g_msi[0] = 0; not_dynarun(); return; } + pkg_temp_dir(g_msi); lstrcatW(g_msi, L"\\Setup.msi"); + if (g_zip.flags & 1) set_page(P_PASSWORD); + else { g_pw[0] = 0; start_job(JOB_EXTRACT, P_EXTRACT); } +} + +// The ZIP password is bytes; try the usual encodings of what was typed. +static BOOL take_password(void) +{ + static const UINT cps[] = { CP_OEMCP, CP_ACP, CP_UTF8 }; + WCHAR w[128]; int i; BOOL ok = FALSE; + GetWindowTextW(H(ID_EDIT), w, 128); + if (!w[0]) return FALSE; + for (i = 0; i < 3 && !ok; i++) + ok = WideCharToMultiByte(cps[i], 0, w, -1, g_pw, sizeof(g_pw), NULL, NULL) && zip_password_plausible(&g_zip, g_pw); + SecureZeroMemory(w, sizeof(w)); + if (!ok) SecureZeroMemory(g_pw, sizeof(g_pw)); + else SetWindowTextW(H(ID_EDIT), L""); // the typed password does not stay in the edit control (g_pw is wiped after the extraction) + return ok; +} + +static void choose_file(void) +{ + OPENFILENAMEW of; WCHAR f[MAX_PATH]; + zero(&of, sizeof(of)); f[0] = 0; + of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; + of.lpstrFilter = T(L"DynaRun V3 setup (*.zip;*.msi)\0*.zip;*.msi\0All files\0*.*\0", L"DynaRun V3 安裝檔 (*.zip;*.msi)\0*.zip;*.msi\0所有檔案\0*.*\0"); + of.lpstrFile = f; of.nMaxFile = MAX_PATH; + of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + if (!GetOpenFileNameW(&of)) return; + if (g_pkg_ours && lstrcmpiW(g_pkg, f)) DeleteFileW(g_pkg); // the download that turned out not to be wanted + lstrcpyW(g_pkg, f); g_pkg_ours = g_pkg_found = FALSE; + open_package(); +} + +static void drop_msi(void) // the decrypted Setup.msi in our temp folder (the user's own MSI is never touched) +{ + WCHAR d[MAX_PATH], m[MAX_PATH]; + pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); + if (g_msi[0] && !lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); + RemoveDirectoryW(d); // only when empty +} + +static void cleanup_files(void) +{ + WCHAR d[MAX_PATH], m[MAX_PATH]; + pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); + if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); + if (g_pkg_ours) DeleteFileW(g_pkg); + RemoveDirectoryW(d); +} + +// The one text for every way the acquire flow can end badly (the DynaRun setup and the driver alike): g_note. +static void acq_note(int rc) +{ + switch (rc) { + case PK_CANCEL: lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); break; + case PK_HTTP: wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); break; + case PK_CERT: wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", + L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), g_neterr); break; + case PK_URL: lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", + L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); break; + case PK_IO: wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); break; + case DRV_E_HASH: lstrcpyW(g_note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", + L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。")); break; + default: // PK_NET and anything unforeseen: the first real error of the attempts + if (g_neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(g_note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", + L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。")); + else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), g_neterr); + break; + } +} + +// The one error page of the acquire flow. +static void acq_fail(int rc) { acq_note(rc); set_page(P_FAIL); } + +// The download worker has ended because "Pause" was clicked: the .part and its ETag are kept, the page stays, the button becomes "Resume". +static void fetch_paused(void) +{ + WCHAR a[24], t[24], s[160]; + g_paused = TRUE; g_pause_req = FALSE; + fmt_mb(a, g_dlhave); + if (g_dltotal) { fmt_mb(t, g_dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } + else wsprintfW(s, T(L"Paused: %s MB downloaded.", L"已暫停:已下載 %s MB。"), a); + buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + status(s, FALSE); + layout(); +} + +// "Cancel" while paused (no worker is running any more): the same result as a cancel during the transfer. +static void fetch_stopped(void) { g_paused = FALSE; acq_fail(PK_CANCEL); } + +// The end of the acquire job. The DynaRun setup goes on to its password / check / license pages (open_package), the driver to its restart or warning page. +static void acq_done(int rc) +{ + BOOL drv = g_acq == &ACQ_DRIVER; + if ((rc == PK_OK || (drv && rc == DRV_W_HASH)) && g_cancel == 1) { // "Cancel" while the file was being checked + if (drv && !(g_drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(g_drvzip); + acq_fail(PK_CANCEL); + return; + } + if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); return; } + if (drv) { acq_driver_result(rc); return; } + if (rc == PK_OK) { if (g_pkg_found) g_nfound++; open_package(); return; } + acq_fail(rc); +} + +static void jd_acquire(int job, int rc) { (void)job; acq_done(rc); } + +static void jd_extract(int job, int rc) +{ + (void)job; + if (rc == PK_OK) { after_extract(); return; } + drop_msi(); // a failed or cancelled extraction leaves no decrypted copy + if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); return; } + if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page + WCHAR nm[100]; + lstrcpynW(nm, file_name(g_pkg), 100); + pkg_reject(g_pkg); + g_acq = &ACQ_SETUP; g_pkg_found = FALSE; + wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); + set_page(P_FAIL); + return; + } + if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } + lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); + set_page(P_ERROR); +} + +static void pri_fail(void) { acq_start(g_acq); } // Retry / Continue download + +static void pri_password(void) +{ + if (!take_password()) { + status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", + L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。"), TRUE); + SetFocus(H(ID_EDIT)); + return; + } + start_job(JOB_EXTRACT, P_EXTRACT); +} + +static void pri_check(void) { set_page(P_LICENSE); } +static void pri_license(void) { go_install(JOB_INSTALL_ALL); } + +static void pri_fetch(void) // "Pause" / "Resume" +{ + if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part + else if (g_busy && !g_pause_req) { + g_pause_req = TRUE; + InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag + text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); + } +} + +static void sec_fail(BOOL button) +{ + if (button && g_acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver + else on_close(); +} + +static void sec_fetch(BOOL button) +{ + if (!button) { on_close(); return; } + if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once + else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) +} + +// The third button (left of the second one): another way on. On the error page: choose the file (or folder) yourself. +static void thr_fail(void) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } + +// The extra button (bottom left, link style). +static void ext_dynapro(void) { ShellExecuteW(g_hwnd, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); } // also after a failed driver download (P_DRV_HASH) diff --git a/src/wizard/driver.h b/src/wizard/driver.h new file mode 100644 index 0000000..69da72d --- /dev/null +++ b/src/wizard/driver.h @@ -0,0 +1,456 @@ +// The Dyna Pro USB driver pages (driver.c does the work): the introduction, the restart menu, the warning for another zip, BitLocker, the restart, the +// pages after it, and the driver jobs (JOB_DRV_PREP / JOB_DRV_INSTALL / JOB_DRV_CANCEL). + +// The "Start DynaRun" button is offered only when DynaRun is installed here and our launcher is in place (g_exe comes from locate_dynarun, which +// the wizard runs at start in every mode, so this also holds in the process the sign-in task starts after the restart; g_launcher from init_paths). +// The driver step may be skipped (the fix is installed without it) only in the main install flow, not when it is the whole job (the shortcut, /driver, or the driver-only first page). +static BOOL can_skip_driver(void) { return g_flow == FLOW_MAIN || g_flow == FLOW_MAIN_WITH_DRIVER; } + +static void enter_drv_intro(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); + lstrcpyW(b, T(L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" + L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" + L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" + L" 3. come back by itself after you sign in and finish the installation\n\n" + L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " + L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" + L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " + L"It resumes by itself, after at most 3 restarts.", + L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" + L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" + L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" + L" 3. 登入後本程式會自動回來,完成安裝\n\n" + L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" + L"下次重新開機會自動恢復。\n\n" + L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", + L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); + text(ID_BODY, b); + buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_press7(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); + { + int bl = g_drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) + b[0] = 0; + if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); + else if (g_drv_how & DRV_SRC_LOCAL) { + WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm + if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); + else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); + lstrcatW(b, t); + } + if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); + if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", + L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); + lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", + L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); + if (bl > 0) lstrcatW(b, T(L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); + else if (bl < 0) lstrcatW(b, T(L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); + text(ID_BODY, b); + } + show_image(0); + if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet + else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_hash(void) // a driver zip that is not the file this program knows: the user decides +{ + WCHAR *b = pgbuf; + static WCHAR src[2 * MAX_PATH + 40]; + if (g_drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (g_drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, g_drvzip); } + else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); + text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); + // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to + // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost + b[0] = 0; + if (g_drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of + cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" + L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, src); + cat_to(b, 2600, L"\n\n"); + } else { + WCHAR sz[24]; + wsprintfW(sz, L"%lu", (DWORD)g_drv_size); + cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" + L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, src); + cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); + cat_to(b, 2600, sz); + cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); + cat_to(b, 2600, g_drv_hex); + cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); + cat_to(b, 2600, driver_pinned_sha256()); + cat_to(b, 2600, L"\n\n"); + } + cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); + { + int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line + const WCHAR *p = g_drv_rep; + int used = 0, more = 0; + while (*p) { + const WCHAR *e = p; int n; + while (*e && *e != L'\n') e++; + n = (int)(e - p) + (*e ? 1 : 0); // the line with its line feed + if (!more && used + n <= room) { lstrcpynW(b + lstrlenW(b), p, n + 1); used += n; } + else more++; + p += n; + } + if (more) { + WCHAR t[48]; + wsprintfW(t, T(L" (and %d more)", L" (另外還有 %d 個)"), more); + cat_to(b, 2600, t); + } + } + text(ID_BODY, b); + // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left + buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_bde(void) +{ + text(ID_TITLE, T(L"BitLocker could not be suspended", L"BitLocker 沒有暫停成功")); + text(ID_BODY, T(L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " + L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " + L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", + L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" + L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。")); + buttons(T(L"Restart anyway", L"仍要重新開機"), T(L"Later", L"稍後"), T(L"Find the recovery key", L"查詢修復金鑰")); +} + +static void enter_drv_prep(void) +{ + text(ID_TITLE, T(L"Please wait", L"請稍候")); + text(ID_BODY, T(L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_drv_reboot(void) +{ + g_result = RC_OK; // the driver step is prepared; the restart continues it + WCHAR *b = pgbuf; + text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); + lstrcpyW(b, g_drv_noreboot + ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " + L"When the menu appears, press 7. After you sign in, this program continues by itself.", + L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") + : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" + L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", + L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); + if (g_fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped + text(ID_BODY, b); + buttons(T(L"Close", L"關閉"), NULL, NULL); + if (g_fix_done) acp_warning(); +} + +static void enter_drv_retry(void) +{ + text(ID_TITLE, T(L"7 was not pressed", L"剛才沒有按到 7")); + text(ID_BODY, T(L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" + L"Try again? The computer restarts once more. When the menu appears, press 7.", + L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" + L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); + buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_warn(void) +{ + text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); + text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", + L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); + show_image(1); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_drv_install(void) +{ + text(ID_TITLE, T(L"Installing the driver", L"正在安裝驅動程式")); + text(ID_BODY, T(L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_drv_done(void) +{ + g_result = RC_OK; // the driver is installed (or was already) + text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); + text(ID_BODY, g_drv_have + ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", + L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") + : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" + L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", + L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" + L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。")); + if (can_start_dynarun()) buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); // only after a success (this page); a failure shows P_DRV_ERROR + else buttons(T(L"Close", L"關閉"), NULL, NULL); +} + +static void enter_drv_error(void) +{ + g_result = RC_FAIL; // the driver step failed + text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); + text(ID_BODY, g_note); + buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); +} + +static int jp_drv_prep(void) +{ + // "install anyway" accepts exactly this zip (its SHA-256), nothing else. + return driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); +} + +static int jp_drv_install(void) { return driver_install(&g_jobopts); } +static int jp_drv_cancel(void) { return driver_cancel(&g_jobopts); } + +// "Skip" on the driver pages of the main flow: the fix is installed without the driver (the sign-in start is then registered by the install itself). +static void skip_driver(void) +{ + user_log(L"driver: skipped, the fix is installed without it"); + g_drv = FALSE; g_opts.autostart_later = FALSE; g_flow = FLOW_MAIN; + install(g_inst_job); +} + +// The text of a failed driver step (rc: PK_* of the download, DRV_E_* of driver.c) and the error page. +static void drv_error(int rc) +{ + switch (rc) { + case DRV_E_HASH: + lstrcpyW(g_note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", + L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。")); + break; + case DRV_E_BCD: lstrcpyW(g_note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; + case DRV_E_TASK: lstrcpyW(g_note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; + case DRV_E_DIR: lstrcpyW(g_note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", + L"無法把工作資料夾 %ProgramData%\\DynaRunFix 設為安全狀態(舊資料夾仍在使用中,或其他使用者可以修改它)。沒有安裝任何東西。請關閉其他程式、重新啟動電腦後再試一次。")); break; + case DRV_E_PNP: lstrcpyW(g_note, T(L"Windows did not accept the driver. Driver signature checking may still have been on.", L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。")); break; + default: wsprintfW(g_note, T(L"The driver could not be prepared (code %d).", L"無法準備驅動程式(代碼 %d)。"), rc); break; + } + lstrcatW(g_note, T(L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。")); + if (g_fix_done) lstrcatW(g_note, T(L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", + L"\n\nDynaRun 的修正本身已經安裝好了。之後想再試一次驅動程式,可以用開始功能表裡的「安裝 Dyna Pro USB 驅動程式」。")); + set_page(P_DRV_ERROR); +} + +static void drv_reboot_now(void) +{ + g_drv_noreboot = !driver_reboot(); + set_page(P_DRV_REBOOT); +} + +// /driver and /driver-continue: the first page +static void drv_start(void) +{ + if (!driver_supported()) { + DWORD b = os_build(); // 9200+: Windows 8 / 8.1, 6000+: Vista / 7, below: XP + lstrcpyW(g_note, T(L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", + L"這個 Windows 版本太舊,無法自動安裝(需要 Windows 10 或更新版本)。請手動安裝:\n\n")); + if (b >= 9200) + lstrcatW(g_note, T(L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " + L"Then install the driver in Device Manager (Update driver > Browse my computer) and choose \"Install this driver software anyway\" on the red warning.", + L"Windows 8 / 8.1:按住 Shift 再按「重新啟動」,依序選 疑難排解 > 進階選項 > 啟動設定 > 重新啟動,然後按 7(停用驅動程式強制簽章;32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" + L"接著在裝置管理員安裝驅動程式(更新驅動程式 > 瀏覽電腦上的驅動程式),並在紅色警告選「仍然安裝此驅動程式軟體」。")); + else if (b >= 6000) + lstrcatW(g_note, T(L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " + L"(32-bit editions do not enforce driver signing and only show the warning, so they can skip this). Then in Device Manager use Update Driver > Browse my computer > Have Disk, and choose \"Install this driver software anyway\" on the red warning.", + L"Windows Vista / 7:重新開機,在出現 Windows 標誌之前連續按 F8,在「進階開機選項」選「停用驅動程式強制簽章」(32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" + L"接著在裝置管理員用 更新驅動程式 > 瀏覽電腦 > 從磁片安裝,並在紅色警告選「仍然安裝此驅動程式軟體」。")); + else + lstrcatW(g_note, T(L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", + L"Windows XP 不強制驅動程式簽章。請在裝置管理員安裝驅動程式;Windows 說軟體未通過 Windows 標誌測試時,按「仍然繼續」。")); + lstrcatW(g_note, T(L"\n\nThe manual steps have the details: click \"Open the manual steps\".", L"\n\n詳細說明請按「開啟手動安裝說明」。")); + set_page(P_DRV_ERROR); + return; + } + g_drv_have = driver_installed(); + if (g_flow == FLOW_DRIVER_CONTINUE) { + if (g_drv_have) { if (is_admin()) driver_cancel(&g_opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes + else set_page(driver_ci_state() == DRV_CI_ENFORCED ? P_DRV_RETRY : P_DRV_WARN); + } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); +} + +// The choice on the warning page: install the zip anyway (the elevated step is told, see driver_prep) or cancel. Both are logged. +static void hash_choice(BOOL install) +{ + static WCHAR l[2 * MAX_PATH + 120]; + lstrcpyW(l, install ? L"driver zip with another hash: the user chose to install it anyway: " : L"driver zip with another hash: the user cancelled: "); + lstrcatW(l, g_drvzip); user_log(l); + if (install) { g_drv_allow = TRUE; set_page(P_DRV_PRESS7); return; } + if (!(g_drv_how & DRV_SRC_LOCAL)) driver_forget(g_drvzip); // our own refused download is not offered again; a file of the user's stays + if ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download + else if (g_drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); + g_drvzip[0] = 0; g_drv_allow = FALSE; + lstrcpyW(g_note, ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") + : T(L"Cancelled: the driver was not installed.", L"已取消,沒有安裝驅動程式。")); + set_page(P_DRV_INTRO); +} + +// What the driver check says (from the search, the download or a file the user chose): on to the restart page, to the warning page, or the error page. +static void acq_driver_result(int rc) +{ + if (rc == PK_OK) { + g_drv_allow = FALSE; + if (!g_bmp[0]) g_bmp[0] = load_png(18, &g_bw[0], &g_bh[0]); // the picture is decoded before the page changes, so the new page is built at once + set_page(P_DRV_PRESS7); + } else if (rc == DRV_W_HASH) { + WCHAR m[2 * MAX_PATH + 100]; + g_drv_allow = FALSE; + lstrcpyW(m, L"driver package with other files: the warning page is shown for "); lstrcatW(m, g_drvzip); user_log(m); + set_page(P_DRV_HASH); + } else acq_fail(rc); +} + +// A file dialog for the driver: one dialog, filter "Dyna Pro USB driver (*.zip;*.inf)". IFileOpenDialog where there is one (Vista and later), else the classic dialog. +// The path of the chosen file in p (MAX_PATH); FALSE when the user cancelled. +static BOOL pick_driver_file(WCHAR *p) +{ + static const GUID clsid = { 0xDC1C5A9C, 0xE88A, 0x4DDE, { 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7 } }; // CLSID_FileOpenDialog + static const GUID iid = { 0xD57C7288, 0xD4AD, 0x4768, { 0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60 } }; // IID_IFileOpenDialog + IFileOpenDialog *fd = NULL; HRESULT hr, hrc; BOOL ok = FALSE; + p[0] = 0; + hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if ((SUCCEEDED(hrc) || hrc == RPC_E_CHANGED_MODE) && SUCCEEDED(CoCreateInstance(&clsid, NULL, CLSCTX_INPROC_SERVER, &iid, (void **)&fd))) { + COMDLG_FILTERSPEC spec[2]; DWORD opt = 0; + spec[0].pszName = T(L"Dyna Pro USB driver (*.zip;*.inf)", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)"); spec[0].pszSpec = L"*.zip;*.inf"; + spec[1].pszName = T(L"All files", L"所有檔案"); spec[1].pszSpec = L"*.*"; + fd->lpVtbl->SetFileTypes(fd, 2, spec); + fd->lpVtbl->SetTitle(fd, T(L"Choose the Dyna Pro USB driver: Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", + L"選擇 Dyna Pro USB 驅動程式:Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf")); + fd->lpVtbl->SetOkButtonLabel(fd, T(L"Use this", L"使用這個")); + if (SUCCEEDED(fd->lpVtbl->GetOptions(fd, &opt))) fd->lpVtbl->SetOptions(fd, opt | FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST | FOS_PATHMUSTEXIST); + hr = fd->lpVtbl->Show(fd, g_hwnd); + if (SUCCEEDED(hr)) { + IShellItem *si = NULL; + if (SUCCEEDED(fd->lpVtbl->GetResult(fd, &si)) && si) { + WCHAR *name = NULL; + if (SUCCEEDED(si->lpVtbl->GetDisplayName(si, SIGDN_FILESYSPATH, &name)) && name) { + if (lstrlenW(name) < MAX_PATH) { lstrcpyW(p, name); ok = TRUE; } + CoTaskMemFree(name); + } + si->lpVtbl->Release(si); + } + } + fd->lpVtbl->Release(fd); + } else { + OPENFILENAMEW of; + zero(&of, sizeof(of)); + of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; + of.lpstrFilter = T(L"Dyna Pro USB driver (*.zip;*.inf)\0*.zip;*.inf\0All files\0*.*\0", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)\0*.zip;*.inf\0所有檔案\0*.*\0"); + of.lpstrTitle = T(L"Choose Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"選擇 Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf"); + of.lpstrFile = p; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + ok = GetOpenFileNameW(&of); + } + if (SUCCEEDED(hrc)) CoUninitialize(); + return ok && p[0]; +} + +// "Choose file..." for the driver: a zip, or an .inf of the folder it is unpacked in (the folder that holds the expected layout is looked for from there, up to +// two levels up); the same check as for a copy found by the search, then the same pages. +static void choose_driver_source(void) +{ + WCHAR p[MAX_PATH], d[MAX_PATH]; int rc, k, n; HCURSOR old; + if (!pick_driver_file(p)) return; + n = lstrlenW(p); + if (n > 4 && !lstrcmpiW(p + n - 4, L".inf")) { // an .inf: use the folder it is in, or the first of its parents with the layout (ftdibus.inf, i386, amd64) + WCHAR *e; + for (e = p + n; e > p && *e != '\\'; e--); + *e = 0; // the file's folder + lstrcpyW(d, p); + for (k = 0; k < 3; k++) { + if (driver_layout_ok(d)) { lstrcpyW(p, d); break; } + for (e = d + lstrlenW(d); e > d && *e != '\\'; e--); + if (e == d) break; + *e = 0; + } + } + { WCHAR l[MAX_PATH + 60]; lstrcpyW(l, L"driver: the user chose "); lstrcatW(l, p); user_log(l); } + g_acq = &ACQ_DRIVER; lstrcpyW(g_drvzip, p); + old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); + rc = driver_check(g_drvzip, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + g_drv_how |= DRV_SRC_PICKED; // chosen by the user: always checked; declining its warning does not touch the memory of declined auto-found copies + user_log(L"driver: a file the user chose is checked as it is (the skip of declined auto-found copies does not apply to it)"); + g_drv_bl = driver_bitlocker(); + SetCursor(old); + acq_driver_result(rc); +} + +static void jd_drv_prep(int job, int rc) +{ + (void)job; + if (rc == DRV_SUCCESS) { drv_reboot_now(); return; } + if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides + if (rc == RC_CANCEL) { + lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); + set_page(g_flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); + return; + } + drv_error(rc); +} + +static void jd_drv_install(int job, int rc) +{ + (void)job; + if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); return; } + if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } + if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed + lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); + set_page(P_DRV_RETRY); return; + } + drv_error(rc); +} + +static void jd_drv_cancel(int job, int rc) +{ + (void)job; (void)rc; + DestroyWindow(g_hwnd); // cancelled (or not allowed to: nothing else can be done from here) +} + +static void pri_drv_intro(void) { acq_start(&ACQ_DRIVER); } +static void pri_drv_press7(void) { if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); } // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage +static void pri_drv_hash(void) { hash_choice(FALSE); } // "Cancel" is the default button +static void pri_drv_retry(void) { g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); } // the staged driver is still there +static void pri_drv_warn(void) { start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); } +static void pri_drv_bde(void) { drv_reboot_now(); } // "Restart anyway" + +static void pri_drv_done(void) +{ + if (can_start_dynarun()) start_dynarun(); // "Start DynaRun" (otherwise the button reads "Close") + end_wizard(); +} + +// after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) +static void cls_drv_task(void) +{ + if (g_flow == FLOW_DRIVER_CONTINUE && is_admin()) driver_cancel(&g_opts); + end_wizard(); +} + +static void cls_drv_bde(void) { start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and the suspension were armed: take them back first + +static void sec_drv_intro(BOOL button) +{ + if (button && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver + else on_close(); +} + +static void sec_drv_hash(BOOL button) { hash_choice(button); } // the button is "Install anyway"; Esc is a cancel +static void sec_drv_cancel(BOOL button) { (void)button; start_job(JOB_DRV_CANCEL, P_DRV_PREP); } // the task, the menu flag and BitLocker back +static void ext_recovery_key(void) { ShellExecuteW(g_hwnd, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); } +static void ext_manual(void) { ShellExecuteW(g_hwnd, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); } diff --git a/src/wizard/layout.h b/src/wizard/layout.h new file mode 100644 index 0000000..52ad53a --- /dev/null +++ b/src/wizard/layout.h @@ -0,0 +1,392 @@ +// The wizard window: the small UI helpers (texts, buttons, status line, progress bar), the screenshots of the driver steps, the controls, the layout +// of the visible ones top-down, bringing the window to the front while the elevated part works. + +static void vis(int id, BOOL on) { ShowWindow(H(id), on ? SW_SHOW : SW_HIDE); } +static void text(int id, const WCHAR *t) { SetWindowTextW(H(id), t ? t : L""); vis(id, t != NULL); } +static int g_sth = -1; // room the layout reserved for the status line (its measured height, 0 when hidden) +static BOOL g_building; // set_page is rebuilding the page: it lays out once at its end +static int status_need(void); + +// Sets the status line. When the room it needs differs from what the layout reserved (shown, hidden, another number of lines), the page is laid out +// again, so the line is never cut off by the button bar and the flexible parts (license text, picture, info box) give way. +static void status(const WCHAR *t, BOOL err) +{ + g_status_err = err; text(ID_STATUS, t); + if (g_hwnd && !g_building && status_need() != g_sth) layout(); +} + +// The button bar of every page: primary = the right-most one and the default (Enter); second = the way out, directly left of it (Cancel, or Skip / Later / +// Close where that is what leaving means); third = another way on, left of the second one; extra = the one link-style action at the bottom left. +static void buttons4(const WCHAR *primary, const WCHAR *second, const WCHAR *third, const WCHAR *extra) +{ + text(ID_PRIMARY, primary); text(ID_SECOND, second); text(ID_THIRD, third); text(ID_EXTRA, extra); + EnableWindow(H(ID_PRIMARY), TRUE); + if (primary) SetFocus(H(ID_PRIMARY)); +} + +static void buttons(const WCHAR *primary, const WCHAR *second, const WCHAR *extra) { buttons4(primary, second, NULL, extra); } + +static void progress(BOOL marquee) +{ + HWND p = H(ID_PROG); + LONG st = GetWindowLongW(p, GWL_STYLE); + SetWindowLongW(p, GWL_STYLE, marquee ? st | PBS_MARQUEE : st & ~PBS_MARQUEE); + SendMessageW(p, PBM_SETMARQUEE, marquee, 40); + SendMessageW(p, PBM_SETRANGE32, 0, 1000); + SendMessageW(p, PBM_SETPOS, 0, 0); + g_lastpct = -1; + vis(ID_PROG, TRUE); +} + +static void fmt_mb(WCHAR *o, DWORD n) { wsprintfW(o, L"%lu.%lu", n / 1048576, n % 1048576 / 104858); } + +static const WCHAR *file_name(const WCHAR *path) +{ + const WCHAR *n = path; + for (; *path; path++) if (*path == '\\' || *path == '/') n = path + 1; + return n; +} + +typedef struct { UINT32 version; void *callback; BOOL no_thread, no_codecs; } gdip_input; + +// The PNG resource id as a bitmap (alpha on white), or NULL when GDI+ is not there or the picture cannot be read. +static HBITMAP load_png(int id, int *w, int *h) +{ + typedef int (WINAPI *startup_t)(ULONG_PTR *, const gdip_input *, void *); + typedef int (WINAPI *from_stream_t)(IStream *, void **); + typedef int (WINAPI *to_hbitmap_t)(void *, HBITMAP *, DWORD); + typedef int (WINAPI *dim_t)(void *, UINT *); + typedef int (WINAPI *dispose_t)(void *); + static HMODULE lib; static ULONG_PTR token; static BOOL tried; + static startup_t startup; static from_stream_t from_stream; static to_hbitmap_t to_hbitmap; static dim_t width, height; static dispose_t dispose; + WCHAR path[MAX_PATH]; gdip_input in; HRSRC r; const void *data; DWORD n; HGLOBAL mem; void *dst; IStream *st = NULL; void *bmp = NULL; HBITMAP hb = NULL; UINT bw = 0, bh = 0; + if (!tried) { + tried = TRUE; + GetSystemDirectoryW(path, MAX_PATH - 20); lstrcatW(path, L"\\gdiplus.dll"); // by full system path, never searched for + if ((lib = LoadLibraryW(path)) != NULL) { + startup = (startup_t)GetProcAddress(lib, "GdiplusStartup"); + from_stream = (from_stream_t)GetProcAddress(lib, "GdipCreateBitmapFromStream"); + to_hbitmap = (to_hbitmap_t)GetProcAddress(lib, "GdipCreateHBITMAPFromBitmap"); + width = (dim_t)GetProcAddress(lib, "GdipGetImageWidth"); height = (dim_t)GetProcAddress(lib, "GdipGetImageHeight"); + dispose = (dispose_t)GetProcAddress(lib, "GdipDisposeImage"); + zero(&in, sizeof(in)); in.version = 1; + if (!startup || !from_stream || !to_hbitmap || !width || !height || !dispose || startup(&token, &in, NULL)) startup = NULL; + } + } + if (!lib || !startup) return NULL; + if (!(r = FindResourceW(NULL, MAKEINTRESOURCEW(id), (LPCWSTR)RT_RCDATA)) || !(data = LockResource(LoadResource(NULL, r)))) return NULL; + n = SizeofResource(NULL, r); + if (!(mem = GlobalAlloc(GMEM_MOVEABLE, n)) || !(dst = GlobalLock(mem))) { if (mem) GlobalFree(mem); return NULL; } + CopyMemory(dst, data, n); GlobalUnlock(mem); + if (SUCCEEDED(CreateStreamOnHGlobal(mem, TRUE, &st))) { // the stream owns mem now + if (from_stream(st, &bmp) == 0 && bmp) { + width(bmp, &bw); height(bmp, &bh); + if (to_hbitmap(bmp, &hb, 0xFFFFFFFF) != 0) hb = NULL; // transparent parts on white + dispose(bmp); + } + IStream_Release(st); + } else GlobalFree(mem); + if (hb) { *w = (int)bw; *h = (int)bh; } + return hb; +} + +// Highlight of the "7) Disable driver signature enforcement" line of the restart menu picture (driver-press-7.png, 683 x 468): the line's +// rectangle in picture pixels. Drawn on the scaled copy, so the outline stays sharp at any size and DPI. +// Line 7 of the picture (683 x 468) has its text in rows 266..278 and columns 121..267; line 6 ends at row 258, line 8 starts at row 286. The outline runs 4 rows +// away from the text on each side, in the middle of the gaps, and is 2 pixels thick. +#define HL_X0 116 +#define HL_Y0 262 +#define HL_X1 272 +#define HL_Y1 282 + +static void draw_highlight(HDC dc, int sw, int sh, int tw, int th) +{ + int x0 = MulDiv(HL_X0, tw, sw), y0 = MulDiv(HL_Y0, th, sh), x1 = MulDiv(HL_X1, tw, sw), y1 = MulDiv(HL_Y1, th, sh); + int lw = S(2) < 2 ? 2 : S(2), rh = y1 - y0, rad = rh / 2 + 2, lx0 = x1 + lw + rh, lx1; + HPEN pen = CreatePen(PS_SOLID, lw, RGB(255, 153, 0)), op; + HBRUSH red = CreateSolidBrush(RGB(224, 52, 0)), ob = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH)); + LOGFONTW lf; HFONT f, of; const WCHAR *t = T(L"Press 7", L"按 7"); SIZE sz; POINT tri[3]; RECT tr; + op = (HPEN)SelectObject(dc, pen); + RoundRect(dc, x0, y0, x1, y1, rad, rad); // orange outline around the line (the pen is centred on the edge) + // callout to the right of the line: red label "Press 7" with a small arrow pointing at it + GetObjectW(g_font, sizeof(lf), &lf); lf.lfHeight = -(rh * 7 / 10 < 10 ? 10 : rh * 7 / 10); lf.lfWeight = FW_BOLD; lf.lfQuality = CLEARTYPE_QUALITY; + f = CreateFontIndirectW(&lf); of = (HFONT)SelectObject(dc, f); + GetTextExtentPoint32W(dc, t, lstrlenW(t), &sz); + lx1 = lx0 + sz.cx + rh; + if (lx1 > tw - 2) { lx0 -= lx1 - (tw - 2); lx1 = tw - 2; } // keep it inside the picture + SelectObject(dc, GetStockObject(NULL_PEN)); SelectObject(dc, red); + RoundRect(dc, lx0, y0, lx1, y1, rad, rad); + tri[0].x = x1 + lw + 3; tri[0].y = (y0 + y1) / 2; // tip, next to the outline + tri[1].x = lx0 + 1; tri[1].y = y0; tri[2].x = lx0 + 1; tri[2].y = y1; + Polygon(dc, tri, 3); + SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(255, 255, 255)); + tr.left = lx0; tr.right = lx1; tr.top = y0; tr.bottom = y1; + DrawTextW(dc, t, -1, &tr, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); + SelectObject(dc, of); DeleteObject(f); + SelectObject(dc, op); SelectObject(dc, ob); DeleteObject(pen); DeleteObject(red); +} + +static HBITMAP scale_bitmap(HBITMAP src, int sw, int sh, int tw, int th, BOOL highlight) +{ + HDC scr = GetDC(NULL), a = CreateCompatibleDC(scr), b = CreateCompatibleDC(scr); HBITMAP dst = CreateCompatibleBitmap(scr, tw, th), oa, ob; + oa = SelectObject(a, src); ob = SelectObject(b, dst); + SetStretchBltMode(b, HALFTONE); SetBrushOrgEx(b, 0, 0, NULL); + StretchBlt(b, 0, 0, tw, th, a, 0, 0, sw, sh, SRCCOPY); + if (highlight) draw_highlight(b, sw, sh, tw, th); + SelectObject(a, oa); SelectObject(b, ob); DeleteDC(a); DeleteDC(b); ReleaseDC(NULL, scr); + return dst; +} + +static void show_image(int which) // 0: press 7 menu, 1: "install anyway" +{ + g_img = which; + if (!g_bmp[which]) g_bmp[which] = load_png(18 + which, &g_bw[which], &g_bh[which]); + vis(ID_IMG, g_bmp[which] != NULL); +} + +// Size of the picture of the current page fitted into w x ah (not larger than at 96 dpi scaled to this screen). +static void fit_image(int w, int ah, int *tw, int *th) +{ + int ow = g_bw[g_img], oh = g_bh[g_img]; + if (ah < S(60)) ah = S(60); + *tw = w; *th = MulDiv(oh, *tw, ow); + if (*th > ah) { *th = ah; *tw = MulDiv(ow, *th, oh); } + if (*tw > MulDiv(ow, g_dpi, 96)) { *tw = MulDiv(ow, g_dpi, 96); *th = MulDiv(oh, *tw, ow); } +} + +// Fits the picture of the current page into w x ah; returns the height used. +static int place_image(int x, int y, int w, int ah) +{ + int ow = g_bw[g_img], oh = g_bh[g_img], tw, th; HBITMAP old; + fit_image(w, ah, &tw, &th); + if (g_shown_img != g_img || g_shown_w != tw || g_shown_h != th) { + HBITMAP s = scale_bitmap(g_bmp[g_img], ow, oh, tw, th, g_img == 0); + old = (HBITMAP)SendMessageW(H(ID_IMG), STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)s); + if (old) DeleteObject(old); + g_shown = s; g_shown_img = g_img; g_shown_w = tw; g_shown_h = th; + } + place(ID_IMG, x, y, tw, th); + return th; +} + +// Appends s to d (a buffer of cap characters), cutting it where it no longer fits; d always stays terminated. +static void cat_to(WCHAR *d, int cap, const WCHAR *s) +{ + int n = lstrlenW(d); + if (n < cap - 1) lstrcpynW(d + n, s, cap - n); +} + +#define FOOTER 76 // height of the grey button band, at 96 dpi + +static HWND add(const WCHAR *cls, DWORD style, int id) +{ + DWORD ex = (id == ID_EDIT || id == ID_RICH || id == ID_INFO) ? WS_EX_CLIENTEDGE : 0; // themed border like other Windows text boxes + HWND c = CreateWindowExW(ex, cls, L"", WS_CHILD | WS_CLIPSIBLINGS | style, 0, 0, 0, 0, g_hwnd, (HMENU)(INT_PTR)id, NULL, NULL); + SendMessageW(c, WM_SETFONT, (WPARAM)(id == ID_TITLE ? g_big : g_font), FALSE); + return c; +} + +static void create_controls(void) +{ + WCHAR lib[MAX_PATH]; const WCHAR *rich; + // by full system path, never searched for (a planted msftedit.dll in the current or the exe's folder is not loaded) + GetSystemDirectoryW(lib, MAX_PATH - 20); lstrcatW(lib, L"\\msftedit.dll"); + if (LoadLibraryW(lib)) rich = L"RICHEDIT50W"; + else { GetSystemDirectoryW(lib, MAX_PATH - 20); lstrcatW(lib, L"\\riched20.dll"); LoadLibraryW(lib); rich = L"RichEdit20W"; } + add(L"STATIC", WS_VISIBLE | SS_NOPREFIX, ID_TITLE); + add(L"STATIC", WS_VISIBLE | SS_NOPREFIX, ID_BODY); + add(L"EDIT", WS_TABSTOP | ES_PASSWORD | ES_AUTOHSCROLL, ID_EDIT); + add(rich, WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY, ID_RICH); + add(L"EDIT", WS_TABSTOP | WS_VSCROLL | ES_MULTILINE | ES_READONLY, ID_INFO); + add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_CHECK); + add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_PIN); + add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_AUTO); + add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX | BS_MULTILINE, ID_DRV); + add(L"STATIC", SS_BITMAP | SS_NOTIFY, ID_IMG); + SetWindowTextW(H(ID_DRV), T(L"Also install the Dyna Pro USB &driver (one restart needed)", L"同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)(&D)")); + SendMessageW(H(ID_DRV), BM_SETCHECK, BST_CHECKED, 0); + SetWindowTextW(H(ID_PIN), T(L"Pin DynaRun to the &taskbar", L"釘選到工作列(&T)")); + SetWindowTextW(H(ID_AUTO), T(L"Start DynaRun when Windows &starts", L"開機時自動啟動 DynaRun(&S)")); + SendMessageW(H(ID_PIN), BM_SETCHECK, g_opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); + SendMessageW(H(ID_AUTO), BM_SETCHECK, g_opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); + SetWindowTextW(H(ID_CHECK), T(L"I &accept the license agreement", L"我接受授權合約(&A)")); + add(PROGRESS_CLASSW, 0, ID_PROG); + add(L"STATIC", SS_NOPREFIX, ID_STATUS); + add(L"BUTTON", WS_TABSTOP, ID_EXTRA); + add(L"BUTTON", WS_TABSTOP, ID_THIRD); + add(L"BUTTON", WS_TABSTOP, ID_SECOND); + add(L"BUTTON", WS_TABSTOP | BS_DEFPUSHBUTTON, ID_PRIMARY); +} + +// Height of a control's text wrapped to width w, or its width when w is 0. (The buffer holds the longest text of any page, the license page's +// info block, with room to spare; a cut-off text would measure too short and the controls below it would overlap the last lines.) +static int measure(int id, int w) +{ + static WCHAR t[4096]; RECT r; HDC dc = GetDC(g_hwnd); + HGDIOBJ old = SelectObject(dc, (HGDIOBJ)SendMessageW(H(id), WM_GETFONT, 0, 0)); + GetWindowTextW(H(id), t, 4096); + r.left = r.top = 0; r.right = w ? w : 10000; r.bottom = 0; + DrawTextW(dc, t, -1, &r, DT_CALCRECT | DT_NOPREFIX | DT_EXPANDTABS | (w ? DT_WORDBREAK : DT_SINGLELINE)); + SelectObject(dc, old); ReleaseDC(g_hwnd, dc); + return w ? r.bottom : r.right; +} + +static BOOL shown(int id) { return (GetWindowLongW(H(id), GWL_STYLE) & WS_VISIBLE) != 0; } // also before the window is shown + +static int status_need(void) +{ + RECT c; + if (!shown(ID_STATUS)) return 0; + GetClientRect(g_hwnd, &c); + return measure(ID_STATUS, c.right - 2 * S(32)) + S(8); +} + +static void place(int id, int x, int y, int w, int h) { SetWindowPos(H(id), NULL, x, y, w, h, SWP_NOZORDER | SWP_NOACTIVATE); } + +// One top-down pass over the visible controls from their measured sizes. apply FALSE: nothing is moved, it returns the client height the page needs +// (the picture and the license text at the size they want); TRUE: places the controls so that the flexible ones (picture, license text, note) use +// what is left above the button band at `bottom`. +static int stack(BOOL apply, int w, int bottom) +{ + int m = S(32), y, h, sh = shown(ID_STATUS) ? measure(ID_STATUS, w) + S(8) : 0; // sh: room the status line needs at this width and DPI (it may wrap) + if (apply) g_sth = sh; + y = S(shown(ID_PIN) ? 18 : 24); // tighter on the first page: its text and the options must fit + h = measure(ID_TITLE, w); if (apply) place(ID_TITLE, m, y, w, h); y += h + S(shown(ID_PIN) ? 10 : 16); + h = measure(ID_BODY, w) + S(6); if (apply) place(ID_BODY, m, y, w, h); y += h + S(shown(ID_PIN) ? 6 : 20); + if (shown(ID_PIN)) { // the two options: side by side when they fit, else stacked + int a = measure(ID_PIN, 0) + S(24), b2 = measure(ID_AUTO, 0) + S(24), side = a + b2 + S(16) <= w; + if (apply) { place(ID_PIN, m, y, a, S(24)); place(ID_AUTO, side ? m + a + S(16) : m, side ? y : y + S(24), b2, S(24)); } + y += side ? S(24) : S(48); + } + if (shown(ID_DRV)) { h = measure(ID_DRV, w - S(24)) + S(8); if (apply) place(ID_DRV, m, y + S(2), w, h); y += h + S(4); } + if (shown(ID_IMG)) { + if (apply) y += place_image(m, y, w, bottom - y - S(10) - sh) + S(8); // the status line below keeps its room + else { int tw, th; fit_image(w, 100000, &tw, &th); y += th + S(8); } + } + if (shown(ID_EDIT)) { if (apply) place(ID_EDIT, m, y, w < S(360) ? w : S(360), S(34)); y += S(34) + S(12); } + if (shown(ID_PROG)) { if (apply) place(ID_PROG, m, y, w, S(20)); y += S(20) + S(10); } + if (shown(ID_RICH)) { + int cy = apply ? bottom - (sh ? sh + S(6) : S(16)) - S(28) : y + S(200) + S(10); // the license text: at least ~200 px high; it gives way to the status line + if (apply) { place(ID_CHECK, m, cy, w, S(28)); place(ID_RICH, m, y, w, cy - S(10) - y); } + y = cy + S(28) + (apply && sh ? S(4) : 0); + } + if (shown(ID_INFO)) { // the details of the setup file: a read-only text box (selectable, copyable) with the rest of the page + int bh2 = apply ? bottom - S(16) - y - sh : S(200); + if (apply) place(ID_INFO, m, y, w, bh2 > S(60) ? bh2 : S(60)); + y += bh2 + S(16); + } + if (shown(ID_STATUS)) { + if (apply) place(ID_STATUS, m, y, w, bottom - y > 0 ? bottom - y : 0); + else y += measure(ID_STATUS, w) + S(8); + } + return apply ? y : y + S(FOOTER); +} + +// Sets the client height of the window to `need` (at least the usual one, at most what the screen's work area allows), keeping it centred. +static void fit_window(int need) +{ + RECT wr, cr, work; int target = need > S(580) ? need : S(580), nc, nh, y; + if (!SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0)) return; + GetWindowRect(g_hwnd, &wr); GetClientRect(g_hwnd, &cr); + nc = (wr.bottom - wr.top) - cr.bottom; + if (target + nc > work.bottom - work.top) target = work.bottom - work.top - nc; + if (target == cr.bottom) return; + nh = target + nc; + y = wr.top - (nh - (wr.bottom - wr.top)) / 2; + if (y + nh > work.bottom) y = work.bottom - nh; + if (y < work.top) y = work.top; + SetWindowPos(g_hwnd, NULL, wr.left, y, wr.right - wr.left, nh, SWP_NOZORDER | SWP_NOACTIVATE); +} + +// Lays the visible controls out top-down from the measured text heights (the window grows when a page needs more room); buttons right-aligned in the footer. +static void layout(void) +{ + static const int btn[] = { ID_PRIMARY, ID_SECOND, ID_THIRD }; + RECT c; int m = S(32), w, x, i, bw, bh = S(36), by, bottom; + GetClientRect(g_hwnd, &c); + w = c.right - 2 * m; + fit_window(stack(FALSE, w, 0)); + GetClientRect(g_hwnd, &c); + bottom = c.bottom - S(FOOTER); + stack(TRUE, w, bottom); + by = bottom + (S(FOOTER) - bh) / 2; + for (x = c.right - m, i = 0; i < 3; i++) { + if (!shown(btn[i])) continue; + bw = measure(btn[i], 0) + S(48); if (bw < S(112)) bw = S(112); + x -= bw; place(btn[i], x, by, bw, bh); x -= S(10); + } + bw = measure(ID_EXTRA, 0) + S(40); if (bw < S(112)) bw = S(112); + place(ID_EXTRA, m, by, bw, bh); + InvalidateRect(g_hwnd, NULL, TRUE); +} + +// The installed UI font for the language (font linking would mix two fonts in Chinese text). +static int CALLBACK font_found(const LOGFONTW *lf, const TEXTMETRICW *tm, DWORD type, LPARAM found) +{ *(BOOL *)found = TRUE; (void)lf; (void)tm; (void)type; return 0; } + +static void pick_face(LOGFONTW *lf) +{ + static const WCHAR *zh[] = { L"Microsoft JhengHei UI", L"Microsoft JhengHei", L"PMingLiU", NULL }, + *en[] = { L"Segoe UI", L"Tahoma", NULL }; + const WCHAR **f; LOGFONTW q; HDC dc = GetDC(NULL); BOOL found; + for (f = g_zh ? zh : en; *f; f++) { + zero(&q, sizeof(q)); q.lfCharSet = DEFAULT_CHARSET; lstrcpyW(q.lfFaceName, *f); found = FALSE; + EnumFontFamiliesExW(dc, &q, font_found, (LPARAM)&found, 0); + if (found) { lstrcpyW(lf->lfFaceName, *f); lf->lfCharSet = DEFAULT_CHARSET; break; } + } + ReleaseDC(NULL, dc); +} + +// Brings the wizard back in front after the elevated part. SetForegroundWindow alone is refused +// when another process (msiexec's progress window, "Windows configures ...") had the foreground: +// raise it to the top of the z-order anyway, then borrow the foreground thread's input to activate it. +static void bring_front(HWND h) +{ + HWND fg = GetForegroundWindow(); DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0; + if (IsIconic(h)) ShowWindow(h, SW_RESTORE); + SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + if (SetForegroundWindow(h) || !other || other == me) return; + if (AttachThreadInput(me, other, TRUE)) { + BringWindowToTop(h); SetForegroundWindow(h); SetFocus(h); + AttachThreadInput(me, other, FALSE); + } + if (GetForegroundWindow() != h) { // still refused: at least flash the taskbar button + FLASHWINFO f; f.cbSize = sizeof(f); f.hwnd = h; f.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG; f.uCount = 3; f.dwTimeout = 0; + FlashWindowEx(&f); + } +} + +// Image name (no path) of the process that owns window w; "" when it cannot be read. +static const WCHAR *owner_name(HWND w, WCHAR *buf, DWORD n) +{ + typedef BOOL (WINAPI *qfpin_t)(HANDLE, DWORD, LPWSTR, PDWORD); + static qfpin_t qfpin; DWORD pid = 0; HANDLE p; const WCHAR *name = buf, *c; + buf[0] = 0; + if (!qfpin) qfpin = (qfpin_t)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "QueryFullProcessImageNameW"); + if (!qfpin || !GetWindowThreadProcessId(w, &pid)) return buf; + if ((p = OpenProcess(0x1000 /* PROCESS_QUERY_LIMITED_INFORMATION */, FALSE, pid)) != NULL) { + if (!qfpin(p, 0, buf, &n)) buf[0] = 0; + CloseHandle(p); + } + for (c = buf; *c; c++) if (*c == '\\') name = c + 1; + return name; +} + +// While the elevated part installs: the UAC prompt and msiexec's progress window take the foreground, +// and afterwards Windows may activate Explorer, which then covers the wizard. Keep the wizard just +// behind msiexec's window, and above Explorer; other programs the user switches to are left alone. +static void keep_up(HWND h) +{ + static WCHAR b[MAX_PATH]; const WCHAR *n; HWND fg = GetForegroundWindow(); DWORD pid = 0; + if (!fg || fg == h || IsIconic(h)) return; + GetWindowThreadProcessId(fg, &pid); + if (pid == GetCurrentProcessId()) return; + n = owner_name(fg, b, MAX_PATH); + if (!lstrcmpiW(n, L"msiexec.exe") || !lstrcmpiW(n, L"consent.exe")) + SetWindowPos(h, fg, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + else if (!lstrcmpiW(n, L"explorer.exe")) { + SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } +} diff --git a/src/wizard/main.h b/src/wizard/main.h new file mode 100644 index 0000000..0efadf8 --- /dev/null +++ b/src/wizard/main.h @@ -0,0 +1,566 @@ +// The main flow (the first page, the running-programs page, the install, Done, the error page), the job machinery (JOBS[], start_job, job_done), the page +// table PAGES[] with the window procedure, and wizard() itself. + +static UINT StrToUint(const WCHAR *s) { UINT v = 0; while (*s >= '0' && *s <= '9') v = v * 10 + (*s++ - '0'); return v; } + +// ANSI code page of the system locale, also when "Beta: UTF-8" makes the active one 65001 +static UINT legacy_acp(void) +{ + WCHAR b[8]; + return GetLocaleInfoW(LOCALE_SYSTEM_DEFAULT, LOCALE_IDEFAULTANSICODEPAGE, b, 8) ? StrToUint(b) : GetACP(); +} + +// "DynaRun V3", "DynaRunFix.exe" ... for the running-programs page +static void running_names(int run, WCHAR *out) +{ + out[0] = 0; + if (run & RUN_DYNARUN) lstrcatW(out, L" DynaRun V3\n"); + if (run & RUN_LAUNCHER) lstrcatW(out, T(L" DynaRunFix.exe (starting DynaRun)\n", L" DynaRunFix.exe(正在啟動 DynaRun)\n")); + if (run & RUN_LEPROC) lstrcatW(out, L" LEProc.exe (Locale Emulator)\n"); +} + +// Last paragraph of the first page. +static const WCHAR *disclaimer(void) +{ + return T(L"\n\nDynaRunFix is an independent open-source tool, not affiliated with or endorsed by Dyna Pro Dynamometers. " + L"It is provided as is, without warranty (MIT license). DynaRun V3 is licensed to you separately by Dyna Pro.", + L"\n\nDynaRunFix 是獨立的開源工具,與 Dyna Pro Dynamometers 無關,也未經其認可;依 MIT 授權按現狀提供,不提供任何擔保。" + L"DynaRun V3 由 Dyna Pro 另行授權給你。"); +} + +// The line about a newer DynaRun on the first page, and its button. A hint only: nothing happens unless the button is clicked. +static void hint_line(WCHAR *b) +{ + WCHAR t[400]; + if (g_hint == HINT_NEWER) { + wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), g_hint_new, g_hint_inst); + lstrcatW(b, t); + } else if (g_hint == HINT_ZIPDIFF) + lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); +} + +static const WCHAR *hint_btn(void) { return g_hint ? T(L"Download new version", L"下載新版") : NULL; } + +static BOOL can_start_dynarun(void) { return g_installed && g_exe[0] && exists(g_launcher); } + +// What is said about the taskbar pin (g_instres.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with +// the install (g_fix_done: the page "Done" is skipped), on the restart page. +static void pin_hint(WCHAR *b) +{ + if (g_instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (g_instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 + ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", + L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") + : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", + L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「釘選到工作列」。")); +} + +static void acp_warning(void) +{ + if (g_zh && legacy_acp() != 950) // DynaRun's Chinese is Big5: only a zh-TW system locale shows it everywhere + status(L"注意:這台電腦的「非 Unicode 程式的語言」不是中文(台灣),DynaRun 的中文可能會變成亂碼。\n" + L"請到「設定 → 時間與語言 → 語言與地區 → 系統管理語言設定 → 變更系統地區設定」選「中文(繁體,台灣)」,再重新開機。", TRUE); +} + +static void enter_hello(void) +{ + WCHAR *b = pgbuf; + if (g_flow == FLOW_DRIVER_ONLY) { + wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " + L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" + L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", + L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" + L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" + L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); + hint_line(b); + text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); + text(ID_BODY, b); + buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); + if (g_note[0]) status(g_note, TRUE); + return; + } + if (g_installed) { + text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); + wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " + L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" + L"When Windows asks whether to allow changes, click \"Yes\".%s", + L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" + L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); + hint_line(b); + text(ID_BODY, b); + buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); + } else { + text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); + wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" + L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" + L" 2. install DynaRun V3\n" + L" 3. add the fixes for Windows 10/11\n\n" + L"You need the setup password that Dyna Pro gave you.%s", + L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" + L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" + L" 2. 安裝 DynaRun V3\n" + L" 3. 加上 Windows 10/11 的相容性修正\n\n" + L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s"), disclaimer()); + text(ID_BODY, b); + buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); + } + vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); + if (g_note[0]) status(g_note, TRUE); +} + +static void enter_running(void) +{ + WCHAR *b = pgbuf; + WCHAR names[200]; + running_names(g_lastrun = running_programs(), names); + text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); + if (g_lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help + wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" + L"Ask that user to close DynaRun; this page then offers \"Install\".", + L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" + L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。"), names); + text(ID_BODY, b); + buttons(NULL, T(L"Cancel", L"取消"), NULL); + } else { + wsprintfW(b, T(L"DynaRun must be closed to install:\n%s\n" + L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", + L"安裝前必須關閉 DynaRun:\n%s\n" + L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。"), names); + text(ID_BODY, b); + buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); + } + if (g_note[0]) status(g_note, TRUE); + SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); +} + +static void enter_install(void) +{ + text(ID_TITLE, T(L"Installing", L"安裝中")); + text(ID_BODY, T(L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + progress(TRUE); + buttons(NULL, NULL, NULL); +} + +static void enter_done(void) +{ + g_result = RC_OK; // everything is installed + WCHAR *b = pgbuf; + text(ID_TITLE, T(L"Done", L"完成")); + lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" + L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", + L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" + L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); + pin_hint(b); + if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); + lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); + text(ID_BODY, b); + buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); + acp_warning(); +} + +static void enter_error(void) +{ + g_result = RC_FAIL; // the install failed + text(ID_TITLE, T(L"Something went wrong", L"發生問題")); + text(ID_BODY, g_note); + buttons(T(L"Close", L"關閉"), NULL, NULL); +} + +static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_instres, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_instres, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage +static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_instres, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_instres); } + +static void jd_install(int job, int rc); + +// One row per job, indexed by the JOB_* value (nothing depends on the order of the values). +// proc runs on the worker thread; returns the job's result code +// on_done runs on the UI thread when the worker has ended (job_done), with the job and its result +// keep_up the elevated stage or a system tool works: the UAC prompt and msiexec's window take the foreground, so ID_KEEPUP keeps the wizard behind them +// bring_front the wizard comes back in front of other windows when the job is done (the elevated part allows it) +typedef struct { + int (*proc)(void); + void (*on_done)(int job, int rc); + BOOL keep_up, bring_front; +} job_desc; + +static const job_desc JOBS[JOB_COUNT] = { + [JOB_ACQUIRE] = { jp_acquire, jd_acquire, FALSE, FALSE }, + [JOB_EXTRACT] = { jp_extract, jd_extract, FALSE, FALSE }, + [JOB_INSTALL_ALL] = { jp_install_all, jd_install, TRUE, TRUE }, + [JOB_INSTALL_FIX] = { jp_install_fix, jd_install, TRUE, TRUE }, + [JOB_DRV_PREP] = { jp_drv_prep, jd_drv_prep, TRUE, TRUE }, + [JOB_DRV_INSTALL] = { jp_drv_install, jd_drv_install, TRUE, TRUE }, + [JOB_DRV_CANCEL] = { jp_drv_cancel, jd_drv_cancel, TRUE, TRUE }, +}; + +static DWORD WINAPI job_proc(LPVOID unused) +{ + int rc = JOBS[g_job].proc(); + PostMessageW(g_hwnd, WM_JOBDONE, g_job, rc); + (void)unused; + return 0; +} + +// The update hint is asked in the background when the first page shows an installed DynaRun; the answer comes as WM_HINT. Never waited for. +static DWORD WINAPI hint_proc(LPVOID unused) +{ + static WCHAR i[40], n[40]; int r; + r = pkg_update_hint(i, n, 40, user_log); + if (r) { lstrcpyW(g_hint_inst, i); lstrcpyW(g_hint_new, n); } + if (g_hwnd) PostMessageW(g_hwnd, WM_HINT, (WPARAM)r, 0); + (void)unused; + return 0; +} + +static void start_job(int job, int page) +{ + DWORD tid; + g_job = job; g_cancel = 0; g_busy = TRUE; + if (job == JOB_ACQUIRE && g_dlacq != g_acq) { g_dlacq = g_acq; g_dltotal = g_dlhave = 0; } // a resume of the same download keeps what is known + set_page(page); + g_jobopts = g_opts; // the job runs with the options as they are now + g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); + if (JOBS[job].keep_up) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); +} + +// The install jobs start only once DynaRun is closed (see running_programs in setup.c). +static void install(int job) +{ + g_pending = job; + if (running_programs()) set_page(P_RUNNING); + else start_job(job, P_INSTALL); +} + +// The install of the fix (job: JOB_INSTALL_ALL / JOB_INSTALL_FIX). With the driver ticked its package is fetched and checked first (the driver pages), and the +// one elevated stage then does both: the UAC prompt comes once, before the restart. The driver-only first page (FLOW_DRIVER_ONLY) has no install: it starts the prep itself. +static void go_install(int job) +{ + if (g_drv && g_flow == FLOW_MAIN) { g_flow = FLOW_MAIN_WITH_DRIVER; g_inst_job = job; g_note[0] = 0; set_page(P_DRV_INTRO); } + else install(job); +} + +static void choose_exe(void) +{ + OPENFILENAMEW of; WCHAR f[MAX_PATH]; + zero(&of, sizeof(of)); f[0] = 0; + of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; + of.lpstrFilter = L"DynaRun V3.exe\0DynaRun V3.exe\0*.exe\0*.exe\0"; + of.lpstrFile = f; of.nMaxFile = MAX_PATH; + of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + if (!GetOpenFileNameW(&of)) return; + lstrcpyW(g_exe, f); g_installed = TRUE; + set_page(P_HELLO); +} + +static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX +{ + if (g_flow == FLOW_MAIN_WITH_DRIVER && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) + cleanup_files(); + g_fix_done = TRUE; + if (rc == DRV_SUCCESS) drv_reboot_now(); + else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); + else drv_error(rc); + return; + } + if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } + if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile + if (rc == RC_CANCEL) { + lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); + set_page(g_flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it + return; + } + wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", + L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); + set_page(P_ERROR); +} + +static void job_done(int job, int rc) +{ + WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; + if (g_close_asked) { g_opts.close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) + if (g_closing) { DestroyWindow(g_hwnd); return; } + JOBS[job].on_done(job, rc); +} + +// The first page's options are read when it is left, whichever button leaves it. +static void take_options(void) +{ + g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; +} + +static void end_wizard(void) { DestroyWindow(g_hwnd); } + +// The primary button (right-most, Enter), page by page. +static void pri_hello(void) +{ + take_options(); + if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); return; } // the fix is current: straight to the driver + g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it + if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } +} + +static void pri_running(void) +{ + if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only + start_job(g_pending, P_INSTALL); +} + +static void pri_done(void) +{ + start_dynarun(); + end_wizard(); +} + +// Closing the window (the X, Alt+F4, and "Cancel" / "Close" where that is what leaving means). A page can refuse it (PF_NOCLOSE), or do something first +// (its close hook, which then also ends the wizard if that is what closing means there). +static void on_close(void) +{ + const page_desc *d = &PAGES[g_page]; + if (d->flags & PF_NOCLOSE) return; // msiexec / registry / system tool work must not be interrupted + if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } + if (d->close) d->close(); else end_wizard(); +} + +// The second button (and Esc): Cancel / Skip / Later / Close. A page without a handler closes. button FALSE: Esc. +static void sec_none(BOOL button) { (void)button; } // the page "Installing": no way out + +static void thr_hello(void) +{ + if (!g_hint) return; // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) + take_options(); + g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_opts.autostart_later = g_drv; + user_log(L"DynaRun update hint: the user chose to download the new version"); + g_acq_fresh = TRUE; // an older package found on this computer would not be the new version + acq_start(&ACQ_SETUP); +} + +static void ext_hello(void) // "Reinstall the fix..." / "Installed elsewhere..." +{ + if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } + else choose_exe(); +} + +// Alt+key shortcuts that exist on one page only (the controls' own & mnemonics do not work while the focus is in the edit or the rich text). +static BOOL when_drv_avail(void) { return g_drv_avail; } +static const hotkey KEYS_HELLO[] = { { 'T', ID_PIN, NULL }, { 'S', ID_AUTO, NULL }, { 'D', ID_DRV, when_drv_avail }, { 0 } }; +static const hotkey KEYS_LICENSE[] = { { 'A', ID_CHECK, NULL }, { 0 } }; + +// One row per page, indexed by the P_* value. A NULL handler means: nothing happens (second: the page closes). +// enter builds the page (the controls, texts and buttons it shows); set_page has reset everything before it +// primary second third extra what the four buttons do (ID_PRIMARY / Enter, ID_SECOND / Esc, ID_THIRD, ID_EXTRA) +// close what closing the window does on this page, after the common refusals; NULL: ends the wizard +// keys the page's Alt+key shortcuts +// flags PF_NOCLOSE: closing the window is refused +// A new page: a P_* value, an enter_* function, a row here. +static const page_desc PAGES[P_COUNT] = { + [P_HELLO] = { enter_hello, pri_hello, NULL, thr_hello, ext_hello, NULL, KEYS_HELLO, 0 }, + [P_FETCH] = { enter_fetch, pri_fetch, sec_fetch, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_FAIL] = { enter_fail, pri_fail, sec_fail, thr_fail, ext_dynapro, NULL, NULL, 0 }, + [P_PASSWORD] = { enter_password, pri_password, NULL, NULL, choose_file, NULL, NULL, 0 }, + [P_EXTRACT] = { enter_extract, NULL, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_CHECK] = { enter_check, pri_check, NULL, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_LICENSE] = { enter_license, pri_license, NULL, NULL, ext_dynapro, NULL, KEYS_LICENSE, 0 }, + [P_RUNNING] = { enter_running, pri_running, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_INSTALL] = { enter_install, NULL, sec_none, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DONE] = { enter_done, pri_done, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_ERROR] = { enter_error, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_INTRO] = { enter_drv_intro, pri_drv_intro, sec_drv_intro, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_PRESS7] = { enter_drv_press7, pri_drv_press7, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_HASH] = { enter_drv_hash, pri_drv_hash, sec_drv_hash, NULL, ext_dynapro, NULL, NULL, 0 }, + [P_DRV_BDE] = { enter_drv_bde, pri_drv_bde, sec_drv_cancel, NULL, ext_recovery_key, cls_drv_bde, NULL, 0 }, + [P_DRV_PREP] = { enter_drv_prep, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DRV_REBOOT] = { enter_drv_reboot, end_wizard, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_RETRY] = { enter_drv_retry, pri_drv_retry, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, + [P_DRV_WARN] = { enter_drv_warn, pri_drv_warn, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, 0 }, + [P_DRV_INSTALL] = { enter_drv_install, NULL, NULL, NULL, NULL, NULL, NULL, PF_NOCLOSE }, + [P_DRV_DONE] = { enter_drv_done, pri_drv_done, NULL, NULL, NULL, NULL, NULL, 0 }, + [P_DRV_ERROR] = { enter_drv_error, end_wizard, NULL, NULL, ext_manual, NULL, NULL, 0 }, +}; + +static void set_page(int p) +{ + g_page = p; + g_result = RC_CANCEL; // undecided until a page below says otherwise + g_paused = g_pause_req = FALSE; + g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress + g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does + KillTimer(g_hwnd, TIMER_RUNNING); + vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); + PAGES[p].enter(); + g_note[0] = 0; + g_building = FALSE; + layout(); + if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) + SetFocus(GetNextDlgTabItem(g_hwnd, NULL, FALSE)); // something must hold the keyboard focus +} + +// The window asks the table (the controls are the same on every page). +static void on_primary(void) { if (PAGES[g_page].primary) PAGES[g_page].primary(); } +static void on_second(BOOL button) { if (PAGES[g_page].second) PAGES[g_page].second(button); else on_close(); } +static void on_third(void) { if (PAGES[g_page].third) PAGES[g_page].third(); } +static void on_extra(void) { if (PAGES[g_page].extra) PAGES[g_page].extra(); } + +// The control id of the Alt+key the current page has a shortcut for; 0 when there is none. +static int page_hotkey(WPARAM key) +{ + const hotkey *k = PAGES[g_page].keys; + for (; k && k->key; k++) if (k->key == key && (!k->when || k->when())) return k->id; + return 0; +} + +static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + switch (m) { + case WM_COMMAND: + switch (LOWORD(w)) { + case IDOK: case ID_PRIMARY: if (IsWindowVisible(H(ID_PRIMARY)) && IsWindowEnabled(H(ID_PRIMARY))) on_primary(); break; + case IDCANCEL: case ID_SECOND: on_second(LOWORD(w) == ID_SECOND); break; + case ID_THIRD: on_third(); break; + case ID_EXTRA: on_extra(); break; + case ID_CHECK: EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); break; + } + return 0; + case WM_PROGRESS: { + static WCHAR b[100]; + DWORD done = (DWORD)w, total = (DWORD)l; + if (done == (DWORD)-1) { // download over, the file is being checked: not pausable, bar and text say so + if (g_page == P_FETCH && !g_paused) { + g_marquee = TRUE; + progress(TRUE); + if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(g_acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves + else { // the file is being checked: not pausable + g_checking = TRUE; + text(ID_TITLE, T(L"Checking the driver", L"正在檢查驅動程式")); + status(T(L"Checking the driver...", L"正在檢查驅動程式…"), FALSE); + EnableWindow(H(ID_PRIMARY), FALSE); + } + layout(); + } + return 0; + } + if (g_checking) return 0; + if (total && g_marquee) { g_marquee = FALSE; progress(FALSE); } // the size became known + else if (!total && !g_marquee && g_page == P_FETCH) { g_marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received + if (!g_marquee) SendMessageW(H(ID_PROG), PBM_SETPOS, total ? MulDiv(done, 1000, total) : 0, 0); + g_dlhave = done; if (total) g_dltotal = total; + if (g_page == P_FETCH) { + if (total != g_total) { fetch_body(total); layout(); } + wsprintfW(b, total ? L"%lu.%lu / %lu.%lu MB" : L"%lu.%lu MB", done / 1048576, done % 1048576 / 104858, + total / 1048576, total % 1048576 / 104858); + status(b, FALSE); + } + return 0; + } + case WM_TIMER: // the page follows DynaRun being closed (or started again) + if (w == TIMER_RUNNING && g_page == P_RUNNING) { + int run = running_programs(); + if (run != g_lastrun) { + g_lastrun = run; + if (run) set_page(P_RUNNING); + else { // closed by the user meanwhile: plain "Install" + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); + status(T(L"DynaRun is closed.", L"DynaRun 已關閉。"), FALSE); + layout(); + } + } + } + if (w == ID_KEEPUP) keep_up(h); + return 0; + case WM_HINT: + g_hint = (int)w; + if (g_hint && g_page == P_HELLO && !g_busy) set_page(P_HELLO); + return 0; + case WM_JOBDONE: + KillTimer(h, ID_KEEPUP); + // after the elevated part (which allows it) the wizard comes back in front of other windows + if (JOBS[w].bring_front) bring_front(h); + job_done((int)w, (int)l); return 0; + case WM_PAINT: { + PAINTSTRUCT ps; RECT c; HDC dc = BeginPaint(h, &ps); HPEN pen, old; + GetClientRect(h, &c); c.top = c.bottom - S(FOOTER); + FillRect(dc, &c, g_grey); + pen = CreatePen(PS_SOLID, 1, RGB(223, 223, 223)); old = SelectObject(dc, pen); + MoveToEx(dc, 0, c.top, NULL); LineTo(dc, c.right, c.top); + SelectObject(dc, old); DeleteObject(pen); + EndPaint(h, &ps); + return 0; + } + case WM_CTLCOLORBTN: + return (LRESULT)g_grey; // push buttons sit on the grey footer + case WM_CTLCOLORSTATIC: + SetBkColor((HDC)w, RGB(255, 255, 255)); + SetTextColor((HDC)w, (HWND)l == H(ID_STATUS) && g_status_err ? RGB(192, 0, 0) : GetSysColor(COLOR_WINDOWTEXT)); + return (LRESULT)g_white; + case WM_CLOSE: on_close(); return 0; + case WM_DESTROY: SecureZeroMemory(g_pw, sizeof(g_pw)); SetWindowTextW(H(ID_EDIT), L""); drop_msi(); PostQuitMessage(0); return 0; + } + return DefWindowProcW(h, m, w, l); +} + +int wizard(const install_opts *init) +{ + INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; int key; + static WCHAR title[100]; + g_opts = *init; // the command line's options; the pages change this copy + icc.dwSize = sizeof(icc); icc.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; + InitCommonControlsEx(&icc); + dc = GetDC(NULL); g_dpi = GetDeviceCaps(dc, LOGPIXELSY); ReleaseDC(NULL, dc); + zero(&nm, sizeof(nm)); +#pragma warning(suppress: 4996) + nm.cbSize = LOBYTE(GetVersion()) >= 6 ? sizeof(nm) : sizeof(nm) - sizeof(int); // XP: no iPaddedBorderWidth + SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, nm.cbSize, &nm, 0); + lf = nm.lfMessageFont; pick_face(&lf); lf.lfQuality = CLEARTYPE_QUALITY; + lf.lfHeight = -S(17); lf.lfWeight = FW_NORMAL; g_font = CreateFontIndirectW(&lf); // larger than usual: easy to read + lf.lfHeight = -S(28); lf.lfWeight = FW_BOLD; g_big = CreateFontIndirectW(&lf); + g_white = CreateSolidBrush(RGB(255, 255, 255)); + g_grey = CreateSolidBrush(RGB(243, 243, 243)); + + zero(&wc, sizeof(wc)); + wc.lpfnWndProc = wndproc; wc.hInstance = GetModuleHandleW(NULL); wc.lpszClassName = L"DynaRunFixSetup"; + wc.hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); wc.hbrBackground = g_white; + wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(10)); + RegisterClassW(&wc); + r.left = r.top = 0; r.right = S(620); r.bottom = S(580); // the first page: text, disclaimer, the two options and a one-line note + AdjustWindowRect(&r, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); + wsprintfW(title, T(L"DynaRunFix Setup %s", L"DynaRunFix 安裝程式 %s"), WIDEN(DRF_DISPLAY)); + g_hwnd = CreateWindowExW(0, wc.lpszClassName, title, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN, + (GetSystemMetrics(SM_CXSCREEN) - (r.right - r.left)) / 2, (GetSystemMetrics(SM_CYSCREEN) - (r.bottom - r.top)) / 2, + r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); + if (!g_hwnd) return RC_FAIL; + create_controls(); + g_installed = locate_dynarun(); + g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); + // g_drvmode (set by cmd_driver / cmd_driver_continue before the wizard starts) is read here and nowhere else + if (g_drvmode == DRV_MODE_STANDALONE) g_flow = FLOW_DRIVER_ONLY; + else if (g_drvmode == DRV_MODE_CONTINUE) g_flow = FLOW_DRIVER_CONTINUE; + else g_flow = g_installed && g_drv_avail && fix_current() ? FLOW_DRIVER_ONLY : FLOW_MAIN; // the fix is there and current: only the driver is missing + if (g_drvmode != DRV_MODE_NONE) drv_start(); + else { + DWORD tid; + set_page(P_HELLO); + if (g_installed) CloseHandle(CreateThread(NULL, 0, hint_proc, NULL, 0, &tid)); // is there a newer DynaRun? (a hint; the page does not wait) + } + ShowWindow(g_hwnd, SW_SHOWNORMAL); + SetForegroundWindow(g_hwnd); + while (GetMessageW(&msg, NULL, 0, 0) > 0) { + if (msg.message == WM_KEYDOWN && msg.wParam == VK_TAB) { // Tab between the visible controls + HWND n = GetNextDlgTabItem(g_hwnd, GetFocus(), GetKeyState(VK_SHIFT) < 0); + if (n) SetFocus(n); + continue; + } + if (msg.message == WM_SYSKEYDOWN && (key = page_hotkey(msg.wParam)) != 0) { // Alt+key shortcuts of the page + SendMessageW(H(key), BM_CLICK, 0, 0); + continue; + } + if (IsDialogMessageW(g_hwnd, &msg)) continue; + TranslateMessage(&msg); DispatchMessageW(&msg); + } + g_hwnd = NULL; + if (g_page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it + return g_result; +} diff --git a/tests/README.md b/tests/README.md index f59daeb..94d7db2 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,6 +40,6 @@ copies COM registrations, that code is only kept under test. Exit code 0 = all p # Driver tests -`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. +`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` and its fragments `src\wizard\*.h` (one text, in the order they are included) relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. The CI workflow (`.github/workflows/build.yml`) runs `build-tests.cmd`, `build-shexec-tests.cmd`, `build-hardening-tests.cmd` and `build-driver-tests.cmd` in the `build` job (read-only token); the release is made by a separate job. The scripts use a release Visual Studio; set `DRF_ALLOW_PRERELEASE=1` locally to allow a pre-release one. diff --git a/tests/VM_CHECKLIST.md b/tests/VM_CHECKLIST.md index 9106f8a..f33bf51 100644 --- a/tests/VM_CHECKLIST.md +++ b/tests/VM_CHECKLIST.md @@ -2,7 +2,7 @@ Manual checks for a build of `DynaRunFix-Setup.exe` that the automated tests cannot cover (UAC, the wizard, the restart for the driver, the network). Run them in a throw-away VM (Windows 10 or 11, snapshot before each section). Do not run them on the development host. -The texts in quotes are the English ones from the source (`src\wizard.c`, `src\setup.c`, `src\update.c`); a Traditional Chinese Windows shows the Chinese pair. +The texts in quotes are the English ones from the source (`src\wizard.c` and `src\wizard\*.h`, `src\setup.c`, `src\update.c`); a Traditional Chinese Windows shows the Chinese pair. Build first with `build.cmd` (the setup is `build\DynaRunFix-Setup.exe`). A dev build is version `dev` and never takes part in the update check. diff --git a/tests/test_driver.c b/tests/test_driver.c index f2150cd..8d99e29 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -762,19 +762,36 @@ static void test_check(void) /* ---------- a regression guard on the wizard's job table ---------- */ // Once a comment after the call in `case JOB_DRV_PREP: rc = driver_prep(...); // ... break;` swallowed the break, and the install ran right after -// the prep (no restart in between). Every `case JOB_...:` of job_proc in src\wizard.c must have a break outside of comments before the next one. +// the prep (no restart in between). Every `case JOB_...:` of job_proc in the wizard's source must have a break outside of comments before the next one. +// The wizard is src\wizard.c with its flow fragments src\wizard\*.h (one translation unit): the checks read them as one text, in the order they are included. +static void *read_wizard(DWORD *n) +{ + static const WCHAR *const parts[] = { L"src\\wizard.c", L"src\\wizard\\layout.h", L"src\\wizard\\acquire.h", L"src\\wizard\\driver.h", L"src\\wizard\\main.h" }; + void *piece[5]; DWORD len[5], total = 0; char *all, *d; int i; + for (i = 0; i < 5; i++) { + piece[i] = read_file(parts[i], &len[i]); + if (!piece[i]) { while (i-- > 0) release(piece[i]); return NULL; } + total += len[i] + 1; + } + all = (char *)alloc(total + 1); + for (i = 0, d = all; all && i < 5; i++) { CopyMemory(d, piece[i], len[i]); d += len[i]; *d++ = '\n'; } + for (i = 0; i < 5; i++) release(piece[i]); + *n = all ? total : 0; + return all; +} + static void test_job_table(void) { - char *t, *a, *end, *c, *next; DWORD n = 0; BYTE *f = read_file(L"src\\wizard.c", &n); int jobs = 0, rows = 0; - if (!f) { wprintf(L"info: src\\wizard.c not found from this folder: the job table is not checked\n"); return; } + char *t, *a, *end, *c, *next; DWORD n = 0; BYTE *f = (BYTE *)read_wizard(&n); int jobs = 0, rows = 0; + if (!f) { wprintf(L"info: src\\wizard.c and src\\wizard\\*.h not found from this folder: the job table is not checked\n"); return; } t = (char *)f; // every job has a row in JOBS[] with a proc (jp_*) and an on_done (jd_*); a job without a row would call a NULL pointer a = strstr(t, "enum { JOB_ACQUIRE"); end = a ? strchr(a, '}') : NULL; - CHECK(a && end, L"the JOB_* enum is in src\\wizard.c"); + CHECK(a && end, L"the JOB_* enum is in the wizard source"); for (c = a; c && end && c < end; c++) if (!strncmp(c, "JOB_", 4) && strncmp(c, "JOB_COUNT", 9)) { jobs++; while (c < end && *c != ',' && *c != ' ') c++; } CHECK(jobs >= 7, L"all seven jobs were looked at"); a = strstr(t, "static const job_desc JOBS["); end = a ? strstr(a, "};") : NULL; - CHECK(a && end, L"the job table JOBS[] is in src\\wizard.c"); + CHECK(a && end, L"the job table JOBS[] is in the wizard source"); for (c = a ? strstr(a, "= {") : NULL, c = c ? strstr(c, "[JOB_") : NULL; c && c < end; c = next) { char *row_end; BOOL has_proc, has_done; next = strstr(c + 5, "[JOB_"); if (next && next >= end) next = NULL; @@ -804,7 +821,7 @@ static void test_job_table(void) // g_opts.close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it // records that (g_close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since a = strstr(t, "static void job_done"); end = a ? strstr(a, "JOBS[job].on_done") : NULL; - CHECK(a && end, L"job_done is in src\\wizard.c and hands the result to the job's on_done"); + CHECK(a && end, L"job_done is in the wizard source and hands the result to the job's on_done"); for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_opts.close = FALSE", 20)) break; CHECK(a && end && c < end, L"job_done takes back the \"close DynaRun\" consent of the job that has ended"); for (c = t, jobs = 0; (c = strstr(c, "g_opts.close = TRUE")) != NULL; c += 19) jobs++; From 7410430a60f89faf91403a045cc7e0fde62ff403 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:05:02 +0800 Subject: [PATCH 29/41] Wizard: wizard_state, worker input and output handed over in structs The wizard's state is one UI-owned wizard_state (wz). A job gets a job_in copied by start_job (like the old g_jobopts) and writes only a job_out; job_done merges the part the job produced (JOBS[].merge) after the join. The hint thread writes its own hint_out before it posts WM_HINT. test_job_table follows the new names and checks that no job proc touches wz. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 133 ++++++++++++++--------- src/wizard/acquire.h | 244 +++++++++++++++++++++++-------------------- src/wizard/driver.h | 150 +++++++++++++------------- src/wizard/layout.h | 8 +- src/wizard/main.h | 234 ++++++++++++++++++++++------------------- tests/test_driver.c | 15 +-- 6 files changed, 433 insertions(+), 351 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index 5ebadca..12a00fe 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -33,48 +33,6 @@ enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, #define WM_HINT (WM_APP + 3) // the DynaRun update hint is known (wParam: 0 none, 1 newer version, 2 the site's file differs) #define TIMER_RUNNING 1 -static int g_page, g_job, g_dpi, g_lastpct = -1, g_pending, g_lastrun; // g_pending: install job waiting for DynaRun to close -static volatile LONG g_cancel; -static install_opts g_opts; // what the pages ask for (starts as the command line gave it); only the UI thread changes it -static install_opts g_jobopts; // the copy a job runs with (start_job): the worker never reads g_opts while the pages may change it -static install_result g_instres; // what the install job reports besides its code (the pin hint); written by the worker, read once the job is done -// The process exit code of wizard(): what the user's run came to. It is RC_CANCEL while nothing is decided (the window is closed before the end), and set -// where the outcome is decided: a page that says "it worked" (Done, the driver is installed, the restart for the driver) makes it RC_OK, an error page -// (the acquire flow's, "Something went wrong", "The driver was not installed") RC_FAIL. set_page takes it back to RC_CANCEL first, so it always belongs to -// the page on screen; wherever the window ends (a button, Esc, Alt+F4, a job that ends while the window is closing) wizard() returns this value. -static int g_result = RC_CANCEL; -static BOOL g_close_asked; // g_opts.close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) -static BOOL g_busy, g_closing, g_installed, g_pkg_ours, g_pkg_found, g_status_err, g_badfile, g_nolicense; // g_nolicense: the setup's license text could not be read -static int g_nfound; // packages pkg_find_local has handed us so far -static HANDLE g_thread; -static WCHAR g_pkg[MAX_PATH], g_msi[MAX_PATH], g_ver[64], g_note[1100]; -static zipent g_zip; -static char g_pw[256]; -static char *g_rtf; -static WCHAR g_info[2200]; // what the license page shows about the package (path, size, SHA-256, MSI version/manufacturer) -static DWORD g_neterr, g_total; -static BOOL g_paused, g_pause_req; // download paused (worker ended, .part kept) / pause asked, worker still stopping -static int g_hint; // DynaRun update hint (pkg_update_hint): 0 none, 1 a newer version is published, 2 the site's zip differs from the last download -static WCHAR g_hint_inst[40], g_hint_new[40]; // the installed and the published version (hint 1) -// Which way through the pages this run takes. One value; it replaces the three flags (the driver ticked on the first page, the driver-only first page, the -// /driver and /driver-continue command line) whose combinations were never all possible. It changes only in start (wizard), go_install, skip_driver and the -// "Reinstall the fix..." link. -// FLOW_MAIN the fix (and, when DynaRun is not installed, DynaRun itself): the first page, no driver in it. The default. -// FLOW_MAIN_WITH_DRIVER the driver is ticked on the first page: its package is fetched and checked BEFORE the install, and the one elevated stage installs the -// fix and prepares the driver (one UAC prompt). The user can still skip it (back to FLOW_MAIN). -// FLOW_DRIVER_ONLY only the driver is the job: /driver (straight to the driver pages), or DynaRun and its fix are installed (this version) and only the -// driver is missing (the first page offers just that, and "Reinstall the fix..." turns it into FLOW_MAIN). -// FLOW_DRIVER_CONTINUE /driver-continue, the sign-in task after the restart: the driver pages that follow the restart. -enum { FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE }; -static int g_flow = FLOW_MAIN; -static int g_inst_job; // the install job (JOB_INSTALL_ALL / JOB_INSTALL_FIX) that waits for the driver package in FLOW_MAIN_WITH_DRIVER -static BOOL g_fix_done; // progress, not flow: FLOW_MAIN_WITH_DRIVER, the fix is installed (the driver step after it may still have failed) -static BOOL g_checking, g_marquee, g_drv_allow; // the non-pausable "checking" phase / marquee bar (size unknown) / the user chose "install anyway" for a zip with another hash -static int g_drv_how; // DRV_SRC_*: the driver zip was downloaded, or found on this computer (nothing downloaded) -static WCHAR g_drv_hex[65]; // SHA-256 and size of a driver zip that is not the pinned file (the warning page shows them) -static ULONGLONG g_drv_size; -static WCHAR g_drv_rep[1500]; // the driver files that differ from the pinned ones, one per line (the warning page lists them) -static int g_drv_bl = BL_UNKNOWN; // BitLocker hint for the press-7 page, asked in the worker (WMI can take seconds): 1 on, 0 off, -1 cannot be told // What the acquire flow needs to know about the thing it gets: the DynaRun V3 setup and the Dyna Pro USB driver go through the same pages (looking for // a copy on this computer, download with pause / resume, one error page); only these texts, the "partial download" test, the search and the check differ. typedef struct { @@ -87,16 +45,93 @@ static const acq_desc ACQ_SETUP = { L"the DynaRun V3 setup", L"DynaRun V3 安裝 L"the zip (or the MSI) you downloaded", L"你下載的 zip(或 MSI)", pkg_download_partial }; static const acq_desc ACQ_DRIVER = { L"the Dyna Pro USB driver", L"驅動程式", L"\"Dyna Pro USB Drivers\"", L"「Dyna Pro USB Drivers」", L"Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf", driver_partial }; -static const acq_desc *g_acq = &ACQ_SETUP, *g_dlacq; // the thing being got now; the one g_dltotal belongs to -static BOOL g_acq_fresh; // "Download new version" (update hint): no package already on this computer is taken, it is downloaded -static BOOL g_acq_local_ok; // the DynaRun search may hand out another local package (pkg_find_local remembers 8 files) #define ACQ_NAME(d) T((d)->name_en, (d)->name_zh) -static DWORD g_dltotal, g_dlhave; // size of the file being downloaded (server's, 0 = not known) and bytes received so far + +// Which way through the pages this run takes. One value; it replaces the three flags (the driver ticked on the first page, the driver-only first page, the +// /driver and /driver-continue command line) whose combinations were never all possible. It changes only in start (wizard), go_install, skip_driver and the +// "Reinstall the fix..." link. +// FLOW_MAIN the fix (and, when DynaRun is not installed, DynaRun itself): the first page, no driver in it. The default. +// FLOW_MAIN_WITH_DRIVER the driver is ticked on the first page: its package is fetched and checked BEFORE the install, and the one elevated stage installs the +// fix and prepares the driver (one UAC prompt). The user can still skip it (back to FLOW_MAIN). +// FLOW_DRIVER_ONLY only the driver is the job: /driver (straight to the driver pages), or DynaRun and its fix are installed (this version) and only the +// driver is missing (the first page offers just that, and "Reinstall the fix..." turns it into FLOW_MAIN). +// FLOW_DRIVER_CONTINUE /driver-continue, the sign-in task after the restart: the driver pages that follow the restart. +enum { FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE }; + +// Everything the wizard knows about the run, in one place. OWNED BY THE UI THREAD: only the window procedure and what it calls read or write it. +// A worker thread never touches it: what a job needs is copied into g_in by start_job, what it produces is written to g_out, and job_done merges +// g_out into this struct after the thread has been joined (see JOBS[].merge in main.h). The hint thread does the same with g_hint_out and WM_HINT. +typedef struct { + int page, pending, lastrun; // pending: install job waiting for DynaRun to close + install_opts opts; // what the pages ask for (starts as the command line gave it); only the UI thread changes it + install_result instres; // what the install job reports besides its code (the pin hint); merged from g_out when the job is done + // The process exit code of wizard(): what the user's run came to. It is RC_CANCEL while nothing is decided (the window is closed before the end), and set + // where the outcome is decided: a page that says "it worked" (Done, the driver is installed, the restart for the driver) makes it RC_OK, an error page + // (the acquire flow's, "Something went wrong", "The driver was not installed") RC_FAIL. set_page takes it back to RC_CANCEL first, so it always belongs to + // the page on screen; wherever the window ends (a button, Esc, Alt+F4, a job that ends while the window is closing) wizard() returns this value. + int result; + BOOL close_asked; // opts.close was set by the "Close DynaRun and install" button: it covers that one install job only (job_done takes it back) + BOOL busy, closing, installed, pkg_ours, pkg_found, status_err, badfile, nolicense; // nolicense: the setup's license text could not be read + int nfound; // packages pkg_find_local has handed us so far + HANDLE thread; + WCHAR pkg[MAX_PATH], msi[MAX_PATH], ver[64], note[1100]; + zipent zip; + char pw[256]; + char *rtf; + WCHAR info[2200]; // what the license page shows about the package (path, size, SHA-256, MSI version/manufacturer) + DWORD neterr, total; + BOOL paused, pause_req; // download paused (worker ended, .part kept) / pause asked, worker still stopping + int hint; // DynaRun update hint (pkg_update_hint): 0 none, 1 a newer version is published, 2 the site's zip differs from the last download + WCHAR hint_inst[40], hint_new[40]; // the installed and the published version (hint 1) + int flow; // FLOW_* + int inst_job; // the install job (JOB_INSTALL_ALL / JOB_INSTALL_FIX) that waits for the driver package in FLOW_MAIN_WITH_DRIVER + BOOL fix_done; // progress, not flow: FLOW_MAIN_WITH_DRIVER, the fix is installed (the driver step after it may still have failed) + BOOL checking, marquee, drv_allow; // the non-pausable "checking" phase / marquee bar (size unknown) / the user chose "install anyway" for a zip with another hash + int drv_how; // DRV_SRC_*: the driver zip was downloaded, or found on this computer (nothing downloaded) + WCHAR drv_hex[65]; // SHA-256 and size of a driver zip that is not the pinned file (the warning page shows them) + ULONGLONG drv_size; + WCHAR drv_rep[1500]; // the driver files that differ from the pinned ones, one per line (the warning page lists them) + int drv_bl; // BitLocker hint for the press-7 page, asked in the worker (WMI can take seconds): 1 on, 0 off, -1 cannot be told + const acq_desc *acq, *dlacq; // the thing being got now; the one dltotal belongs to + BOOL acq_fresh; // "Download new version" (update hint): no package already on this computer is taken, it is downloaded + BOOL acq_local_ok; // the DynaRun search may hand out another local package (pkg_find_local remembers 8 files) + DWORD dltotal, dlhave; // size of the file being downloaded (server's, 0 = not known) and bytes received so far + // the Dyna Pro USB driver (driver.c): drv_avail: the check box on the first page is offered (Windows 10+, driver not there yet); drv: ticked + BOOL drv_avail, drv, drv_have, drv_noreboot; // drv_have: already installed; drv_noreboot: Windows would not restart by itself + WCHAR drvzip[MAX_PATH]; // the checked zip ("" = the staged copy, after a restart) +} wizard_state; +static wizard_state wz; // zero at start (BSS: no bytes in the exe); wizard() sets the few non-zero starting values + +// What a job's worker reads: copied from wz by start_job, so the worker never looks at state the pages go on changing. +typedef struct { + int job; + install_opts opts; // the options the job runs with + int flow; + const acq_desc *acq; BOOL acq_local_ok; + WCHAR msi[MAX_PATH]; + zipent zip; + char pw[256]; // the worker wipes it after the extraction (the UI's copy is wiped by start_job) + WCHAR drvzip[MAX_PATH]; BOOL drv_allow; WCHAR drv_hex[65]; // the driver zip, and the SHA-256 the user accepted for it +} job_in; +// What a job's worker produces: written only by the worker, read by the UI only after the join in job_done, which merges it into wz (JOBS[].merge). +// start_job starts it as a copy of the matching fields of wz, so a field the job does not write keeps its value. +typedef struct { + int lastpct; // on_progress: the last percentage posted (also reset by the UI's progress()) + DWORD neterr; + WCHAR pkg[MAX_PATH]; BOOL pkg_found, pkg_ours; // JOB_ACQUIRE for the DynaRun setup + WCHAR drvzip[MAX_PATH]; int drv_how; WCHAR drv_hex[65]; ULONGLONG drv_size; WCHAR drv_rep[1500]; int drv_bl; // JOB_ACQUIRE for the driver + install_result instres; // JOB_INSTALL_* +} job_out; +static job_in g_in; +static job_out g_out; +// The update hint's answer: written by the hint thread before it posts WM_HINT, read by the UI when the message arrives. +typedef struct { WCHAR inst[40], nw[40]; } hint_out; +static hint_out g_hint_out; + +static volatile LONG g_cancel; // the one value both sides use while a job runs: the UI sets 1 (cancel) / 2 (pause), the worker polls it +static int g_dpi; // the window's own resources and layout numbers (UI thread only, not part of the run's state) static HFONT g_font, g_big; static HBRUSH g_white, g_grey; -// the Dyna Pro USB driver (driver.c): g_drv_avail: the check box on the first page is offered (Windows 10+, driver not there yet); g_drv: ticked -static BOOL g_drv_avail, g_drv, g_drv_have, g_drv_noreboot; // g_drv_have: already installed; g_drv_noreboot: Windows would not restart by itself -static WCHAR g_drvzip[MAX_PATH]; // the checked zip ("" = the staged copy, after a restart) static HBITMAP g_bmp[2], g_shown; // the two screenshots (decoded once) and the scaled one in the picture control static int g_bw[2], g_bh[2], g_img, g_shown_img = -1, g_shown_w, g_shown_h; static WCHAR pgbuf[2600]; // the text of the page being built (one page at a time, on the UI thread) diff --git a/src/wizard/acquire.h b/src/wizard/acquire.h index 9968178..af95b08 100644 --- a/src/wizard/acquire.h +++ b/src/wizard/acquire.h @@ -1,19 +1,19 @@ // Getting the DynaRun V3 setup (and, through the same pages, the Dyna Pro USB driver): looking for a copy on this computer, the download with pause / resume, // the one error page, the password, the check of the file and the license pages. Its jobs: JOB_ACQUIRE, JOB_EXTRACT. -// A download that can be continued where it stopped (its .part and ETag are there): appends how much is already received to g_note and returns TRUE. +// A download that can be continued where it stopped (its .part and ETag are there): appends how much is already received to wz.note and returns TRUE. // The button then reads "Continue download" instead of "Retry". static BOOL resume_note(void) { DWORD have = 0; WCHAR a[24], t[24], line[200]; - if (!g_acq->partial(&have)) return FALSE; + if (!wz.acq->partial(&have)) return FALSE; fmt_mb(a, have); - if (g_dltotal > have) { - fmt_mb(t, g_dltotal); + if (wz.dltotal > have) { + fmt_mb(t, wz.dltotal); wsprintfW(line, T(L"\n\n%s of %s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s / %s MB,按「繼續下載」會從中斷的地方接續。"), a, t); } else wsprintfW(line, T(L"\n\n%s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s MB,按「繼續下載」會從中斷的地方接續。"), a); - lstrcatW(g_note, line); + lstrcatW(wz.note, line); return TRUE; } @@ -26,15 +26,15 @@ static void fetch_body(DWORD total) wsprintfW(b, T(L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" L"If it does not work, click \"Open website\" to get it from the website yourself.", L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" - L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(g_acq), size); - g_total = total; + L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(wz.acq), size); + wz.total = total; text(ID_BODY, b); } static void enter_fetch(void) // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver { WCHAR *b = pgbuf; - wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); + wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(wz.acq)); text(ID_TITLE, b); fetch_body(0); progress(FALSE); status(L" ", FALSE); @@ -43,20 +43,20 @@ static void enter_fetch(void) // looking for a copy here, downloading, checkin static void enter_fail(void) // the one error page of the acquire flow { - g_result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) + wz.result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) WCHAR *b = pgbuf; - if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); - else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); - if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence + if (wz.badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); + else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(wz.acq)); + if (!g_zh && !wz.badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence text(ID_TITLE, b); { - BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) - BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); - g_badfile = FALSE; + BOOL resume = !wz.badfile && resume_note(); // a .part to carry on from (appends its size to the note) + BOOL skip = wz.acq == &ACQ_DRIVER && can_skip_driver(); + wz.badfile = FALSE; wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" - L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); + L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), wz.note, T(wz.acq->get_en, wz.acq->get_zh), T(wz.acq->pick_en, wz.acq->pick_zh)); text(ID_BODY, b); buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); @@ -68,11 +68,11 @@ static void enter_password(void) WCHAR *b = pgbuf; text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", - L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); + L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(wz.pkg)); text(ID_BODY, b); vis(ID_EDIT, TRUE); buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); } @@ -89,61 +89,81 @@ static void enter_check(void) // everything we know about the setup file, to c text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); - SetWindowTextW(H(ID_INFO), g_info); + SetWindowTextW(H(ID_INFO), wz.info); vis(ID_INFO, TRUE); buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_license(void) { WCHAR *b = pgbuf; text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); - lstrcpyW(b, g_nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", + lstrcpyW(b, wz.nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); text(ID_BODY, b); - SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") + SetWindowTextW(H(ID_CHECK), wz.nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), wz.nolicense ? T(L"Open website", L"開啟官網") : NULL); EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } +// The progress callback of the worker (the downloads, the extraction, the driver check). It only posts; the percentage it compares with is g_out.lastpct +// (set to -1 by start_job, and again by the UI's progress()). static void on_progress(DWORD done, DWORD total) { int pct = total ? MulDiv(done, 1000, total) : 0; if (done == (DWORD)-1) { PostMessageW(g_hwnd, WM_PROGRESS, done, total); return; } // the "checking" phase (driver_fetch) - if (pct == g_lastpct) return; - g_lastpct = pct; + if (pct == g_out.lastpct) return; + g_out.lastpct = pct; PostMessageW(g_hwnd, WM_PROGRESS, done, total); } -// What a job runs on the worker thread (job_proc), and what is done with its result on the UI thread (JOBS[].on_done, below). +// What a job runs on the worker thread (job_proc), and what is done with its result on the UI thread (JOBS[].merge, then JOBS[].on_done, below). +// A jp_ function reads g_in and writes g_out, nothing else of the wizard's state. static int jp_acquire(void) // the DynaRun setup or the driver: a copy on this computer first, else the download { int rc; on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts - if (g_acq == &ACQ_DRIVER) { - rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + if (g_in.acq == &ACQ_DRIVER) { + rc = driver_fetch(g_out.drvzip, on_progress, &g_cancel, &g_out.neterr, &g_out.drv_how, g_out.drv_hex, &g_out.drv_size, g_out.drv_rep, 1500); if (rc == PK_OK || rc == DRV_W_HASH) { on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked - g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds + g_out.drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds } } else { - g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); - if (g_pkg_found) rc = PK_OK; - else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } + g_out.pkg_found = g_in.acq_local_ok && pkg_find_local(g_out.pkg); + if (g_out.pkg_found) rc = PK_OK; + else { g_out.pkg_ours = TRUE; rc = pkg_download(g_out.pkg, on_progress, &g_cancel, &g_out.neterr); } } return rc; } +// The UI takes what jp_acquire produced (after the join): for the driver its zip and check result, else the DynaRun setup's file. +static void mo_acquire(void) +{ + wz.neterr = g_out.neterr; + if (g_in.acq == &ACQ_DRIVER) { + CopyMemory(wz.drvzip, g_out.drvzip, sizeof(wz.drvzip)); + wz.drv_how = g_out.drv_how; + CopyMemory(wz.drv_hex, g_out.drv_hex, sizeof(wz.drv_hex)); + wz.drv_size = g_out.drv_size; + CopyMemory(wz.drv_rep, g_out.drv_rep, sizeof(wz.drv_rep)); + wz.drv_bl = g_out.drv_bl; + } else { + CopyMemory(wz.pkg, g_out.pkg, sizeof(wz.pkg)); + wz.pkg_found = g_out.pkg_found; wz.pkg_ours = g_out.pkg_ours; + } +} + static int jp_extract(void) { - int rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); // a size cap, and a free target (shared with the driver) - SecureZeroMemory(g_pw, sizeof(g_pw)); // the password is not needed any more + int rc = pkg_extract_checked(&g_in.zip, g_in.pw, g_in.msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); // a size cap, and a free target (shared with the driver) + SecureZeroMemory(g_in.pw, sizeof(g_in.pw)); // the password is not needed any more return rc; } @@ -162,9 +182,9 @@ static DWORD CALLBACK rtf_in(DWORD_PTR c, LPBYTE b, LONG n, LONG *got) // Starts the acquire flow for the DynaRun setup or the driver (the same pages; a network check comes first inside the download). static void acq_start(const acq_desc *d) { - g_acq = d; - if (d == &ACQ_SETUP) { g_pkg_ours = g_pkg_found = FALSE; g_acq_local_ok = !g_acq_fresh && g_nfound < 8; } // the cap: pkg_reject remembers 8 files - else { g_drvzip[0] = 0; g_drv_allow = FALSE; } + wz.acq = d; + if (d == &ACQ_SETUP) { wz.pkg_ours = wz.pkg_found = FALSE; wz.acq_local_ok = !wz.acq_fresh && wz.nfound < 8; } // the cap: pkg_reject remembers 8 files + else { wz.drvzip[0] = 0; wz.drv_allow = FALSE; } start_job(JOB_ACQUIRE, P_FETCH); } @@ -172,16 +192,16 @@ static void acq_start(const acq_desc *d) static void not_dynarun(void) { WCHAR m[MAX_PATH], nm[100]; - g_acq = &ACQ_SETUP; - lstrcpynW(nm, file_name(g_pkg), 100); // wsprintfW does not check the size of g_note: a long file name must not overflow it - wsprintfW(g_note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", + wz.acq = &ACQ_SETUP; + lstrcpynW(nm, file_name(wz.pkg), 100); // wsprintfW does not check the size of wz.note: a long file name must not overflow it + wsprintfW(wz.note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。"), nm); - pkg_reject(g_pkg); + pkg_reject(wz.pkg); pkg_temp_dir(m); lstrcatW(m, L"\\Setup.msi"); - if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); // our extracted copy - if (g_pkg_ours) DeleteFileW(g_pkg); - if (g_pkg_found) { acq_start(&ACQ_SETUP); return; } // we picked it, not the user: quietly try the next one or download - g_badfile = TRUE; + if (!lstrcmpiW(wz.msi, m)) DeleteFileW(wz.msi); // our extracted copy + if (wz.pkg_ours) DeleteFileW(wz.pkg); + if (wz.pkg_found) { acq_start(&ACQ_SETUP); return; } // we picked it, not the user: quietly try the next one or download + wz.badfile = TRUE; set_page(P_FAIL); } @@ -194,40 +214,40 @@ static void fmt_size(WCHAR *o, ULONGLONG n) // Shown on the license page so the user can compare with what Dyna Pro provides. Display only: nothing here blocks the install. static void build_info(void) { - WCHAR hex[65], hex2[65], ver[64], mfr[128], sz[80]; ULONGLONG size = 0, size2 = 0; BOOL same = !lstrcmpiW(g_pkg, g_msi), okp, okm = FALSE; + WCHAR hex[65], hex2[65], ver[64], mfr[128], sz[80]; ULONGLONG size = 0, size2 = 0; BOOL same = !lstrcmpiW(wz.pkg, wz.msi), okp, okm = FALSE; HCURSOR old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); - okp = pkg_sha256(g_pkg, hex, &size); - if (!same) okm = pkg_sha256(g_msi, hex2, &size2); - msi_info(g_msi, ver, mfr, 64); + okp = pkg_sha256(wz.pkg, hex, &size); + if (!same) okm = pkg_sha256(wz.msi, hex2, &size2); + msi_info(wz.msi, ver, mfr, 64); SetCursor(old); - g_info[0] = 0; - lstrcatW(g_info, T(L"Source: ", L"來源:")); - lstrcatW(g_info, g_pkg_ours ? T(L"downloaded now from dynapro.co.uk", L"剛從 dynapro.co.uk 下載") - : g_pkg_found ? T(L"found on this computer, not downloaded by DynaRunFix", L"在這台電腦上找到,不是 DynaRunFix 下載的") + wz.info[0] = 0; + lstrcatW(wz.info, T(L"Source: ", L"來源:")); + lstrcatW(wz.info, wz.pkg_ours ? T(L"downloaded now from dynapro.co.uk", L"剛從 dynapro.co.uk 下載") + : wz.pkg_found ? T(L"found on this computer, not downloaded by DynaRunFix", L"在這台電腦上找到,不是 DynaRunFix 下載的") : T(L"chosen by you", L"你選擇的檔案")); - lstrcatW(g_info, T(L"\r\n\r\nFile:\r\n", L"\r\n\r\n檔案路徑:\r\n")); lstrcatW(g_info, g_pkg); + lstrcatW(wz.info, T(L"\r\n\r\nFile:\r\n", L"\r\n\r\n檔案路徑:\r\n")); lstrcatW(wz.info, wz.pkg); fmt_size(sz, size); - lstrcatW(g_info, T(L"\r\n\r\nSize: ", L"\r\n\r\n大小:")); lstrcatW(g_info, sz); - lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okp ? hex : L"?"); + lstrcatW(wz.info, T(L"\r\n\r\nSize: ", L"\r\n\r\n大小:")); lstrcatW(wz.info, sz); + lstrcatW(wz.info, L"\r\nSHA-256:\r\n"); lstrcatW(wz.info, okp ? hex : L"?"); if (!same) { fmt_size(sz, size2); - lstrcatW(g_info, T(L"\r\n\r\nSetup.msi inside the file, size: ", L"\r\n\r\n檔案裡的 Setup.msi,大小:")); lstrcatW(g_info, sz); - lstrcatW(g_info, L"\r\nSHA-256:\r\n"); lstrcatW(g_info, okm ? hex2 : L"?"); + lstrcatW(wz.info, T(L"\r\n\r\nSetup.msi inside the file, size: ", L"\r\n\r\n檔案裡的 Setup.msi,大小:")); lstrcatW(wz.info, sz); + lstrcatW(wz.info, L"\r\nSHA-256:\r\n"); lstrcatW(wz.info, okm ? hex2 : L"?"); } - lstrcatW(g_info, L"\r\n\r\nMSI ProductVersion: "); lstrcatW(g_info, ver[0] ? ver : L"?"); - lstrcatW(g_info, L"\r\nManufacturer: "); lstrcatW(g_info, mfr[0] ? mfr : L"?"); + lstrcatW(wz.info, L"\r\n\r\nMSI ProductVersion: "); lstrcatW(wz.info, ver[0] ? ver : L"?"); + lstrcatW(wz.info, L"\r\nManufacturer: "); lstrcatW(wz.info, mfr[0] ? mfr : L"?"); } static void after_extract(void) { - if (msi_check(g_msi, g_ver, 64) != MSI_DYNARUN) { not_dynarun(); return; } - release(g_rtf); - g_rtf = msi_license_rtf(g_msi); - g_nolicense = !g_rtf; + if (msi_check(wz.msi, wz.ver, 64) != MSI_DYNARUN) { not_dynarun(); return; } + release(wz.rtf); + wz.rtf = msi_license_rtf(wz.msi); + wz.nolicense = !wz.rtf; build_info(); - if (g_rtf) { + if (wz.rtf) { rtfsrc src; EDITSTREAM es; - src.p = g_rtf; src.left = lstrlenA(g_rtf); + src.p = wz.rtf; src.left = lstrlenA(wz.rtf); es.dwCookie = (DWORD_PTR)&src; es.dwError = 0; es.pfnCallback = rtf_in; SendMessageW(H(ID_RICH), EM_STREAMIN, SF_RTF, (LPARAM)&es); } else // never install without the user accepting Dyna Pro's terms @@ -247,11 +267,11 @@ static void after_extract(void) static void open_package(void) { - if (msi_check(g_pkg, NULL, 0) != MSI_NONE) { lstrcpyW(g_msi, g_pkg); after_extract(); return; } // by content, not by extension - if (zip_open(g_pkg, &g_zip) != PK_OK) { g_msi[0] = 0; not_dynarun(); return; } - pkg_temp_dir(g_msi); lstrcatW(g_msi, L"\\Setup.msi"); - if (g_zip.flags & 1) set_page(P_PASSWORD); - else { g_pw[0] = 0; start_job(JOB_EXTRACT, P_EXTRACT); } + if (msi_check(wz.pkg, NULL, 0) != MSI_NONE) { lstrcpyW(wz.msi, wz.pkg); after_extract(); return; } // by content, not by extension + if (zip_open(wz.pkg, &wz.zip) != PK_OK) { wz.msi[0] = 0; not_dynarun(); return; } + pkg_temp_dir(wz.msi); lstrcatW(wz.msi, L"\\Setup.msi"); + if (wz.zip.flags & 1) set_page(P_PASSWORD); + else { wz.pw[0] = 0; start_job(JOB_EXTRACT, P_EXTRACT); } } // The ZIP password is bytes; try the usual encodings of what was typed. @@ -262,10 +282,10 @@ static BOOL take_password(void) GetWindowTextW(H(ID_EDIT), w, 128); if (!w[0]) return FALSE; for (i = 0; i < 3 && !ok; i++) - ok = WideCharToMultiByte(cps[i], 0, w, -1, g_pw, sizeof(g_pw), NULL, NULL) && zip_password_plausible(&g_zip, g_pw); + ok = WideCharToMultiByte(cps[i], 0, w, -1, wz.pw, sizeof(wz.pw), NULL, NULL) && zip_password_plausible(&wz.zip, wz.pw); SecureZeroMemory(w, sizeof(w)); - if (!ok) SecureZeroMemory(g_pw, sizeof(g_pw)); - else SetWindowTextW(H(ID_EDIT), L""); // the typed password does not stay in the edit control (g_pw is wiped after the extraction) + if (!ok) SecureZeroMemory(wz.pw, sizeof(wz.pw)); + else SetWindowTextW(H(ID_EDIT), L""); // the typed password does not stay in the edit control (wz.pw is wiped after the extraction) return ok; } @@ -278,8 +298,8 @@ static void choose_file(void) of.lpstrFile = f; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; if (!GetOpenFileNameW(&of)) return; - if (g_pkg_ours && lstrcmpiW(g_pkg, f)) DeleteFileW(g_pkg); // the download that turned out not to be wanted - lstrcpyW(g_pkg, f); g_pkg_ours = g_pkg_found = FALSE; + if (wz.pkg_ours && lstrcmpiW(wz.pkg, f)) DeleteFileW(wz.pkg); // the download that turned out not to be wanted + lstrcpyW(wz.pkg, f); wz.pkg_ours = wz.pkg_found = FALSE; open_package(); } @@ -287,7 +307,7 @@ static void drop_msi(void) // the decrypted Setup.msi { WCHAR d[MAX_PATH], m[MAX_PATH]; pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); - if (g_msi[0] && !lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); + if (wz.msi[0] && !lstrcmpiW(wz.msi, m)) DeleteFileW(wz.msi); RemoveDirectoryW(d); // only when empty } @@ -295,28 +315,28 @@ static void cleanup_files(void) { WCHAR d[MAX_PATH], m[MAX_PATH]; pkg_temp_dir(d); lstrcpyW(m, d); lstrcatW(m, L"\\Setup.msi"); - if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); - if (g_pkg_ours) DeleteFileW(g_pkg); + if (!lstrcmpiW(wz.msi, m)) DeleteFileW(wz.msi); + if (wz.pkg_ours) DeleteFileW(wz.pkg); RemoveDirectoryW(d); } -// The one text for every way the acquire flow can end badly (the DynaRun setup and the driver alike): g_note. +// The one text for every way the acquire flow can end badly (the DynaRun setup and the driver alike): wz.note. static void acq_note(int rc) { switch (rc) { - case PK_CANCEL: lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); break; - case PK_HTTP: wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); break; - case PK_CERT: wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", - L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), g_neterr); break; - case PK_URL: lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", + case PK_CANCEL: lstrcpyW(wz.note, T(L"The download was stopped.", L"已停止下載。")); break; + case PK_HTTP: wsprintfW(wz.note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), wz.neterr); break; + case PK_CERT: wsprintfW(wz.note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", + L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), wz.neterr); break; + case PK_URL: lstrcpyW(wz.note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); break; - case PK_IO: wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); break; - case DRV_E_HASH: lstrcpyW(g_note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", + case PK_IO: wsprintfW(wz.note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), wz.neterr); break; + case DRV_E_HASH: lstrcpyW(wz.note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。")); break; default: // PK_NET and anything unforeseen: the first real error of the attempts - if (g_neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(g_note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", + if (wz.neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(wz.note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。")); - else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), g_neterr); + else wsprintfW(wz.note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), wz.neterr); break; } } @@ -328,9 +348,9 @@ static void acq_fail(int rc) { acq_note(rc); set_page(P_FAIL); } static void fetch_paused(void) { WCHAR a[24], t[24], s[160]; - g_paused = TRUE; g_pause_req = FALSE; - fmt_mb(a, g_dlhave); - if (g_dltotal) { fmt_mb(t, g_dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } + wz.paused = TRUE; wz.pause_req = FALSE; + fmt_mb(a, wz.dlhave); + if (wz.dltotal) { fmt_mb(t, wz.dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } else wsprintfW(s, T(L"Paused: %s MB downloaded.", L"已暫停:已下載 %s MB。"), a); buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); status(s, FALSE); @@ -338,20 +358,20 @@ static void fetch_paused(void) } // "Cancel" while paused (no worker is running any more): the same result as a cancel during the transfer. -static void fetch_stopped(void) { g_paused = FALSE; acq_fail(PK_CANCEL); } +static void fetch_stopped(void) { wz.paused = FALSE; acq_fail(PK_CANCEL); } // The end of the acquire job. The DynaRun setup goes on to its password / check / license pages (open_package), the driver to its restart or warning page. static void acq_done(int rc) { - BOOL drv = g_acq == &ACQ_DRIVER; + BOOL drv = wz.acq == &ACQ_DRIVER; if ((rc == PK_OK || (drv && rc == DRV_W_HASH)) && g_cancel == 1) { // "Cancel" while the file was being checked - if (drv && !(g_drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(g_drvzip); + if (drv && !(wz.drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(wz.drvzip); acq_fail(PK_CANCEL); return; } - if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); return; } + if (rc == PK_CANCEL && wz.pause_req && g_cancel == 2) { fetch_paused(); return; } if (drv) { acq_driver_result(rc); return; } - if (rc == PK_OK) { if (g_pkg_found) g_nfound++; open_package(); return; } + if (rc == PK_OK) { if (wz.pkg_found) wz.nfound++; open_package(); return; } acq_fail(rc); } @@ -363,21 +383,21 @@ static void jd_extract(int job, int rc) if (rc == PK_OK) { after_extract(); return; } drop_msi(); // a failed or cancelled extraction leaves no decrypted copy if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); return; } - if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page + if (wz.pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page WCHAR nm[100]; - lstrcpynW(nm, file_name(g_pkg), 100); - pkg_reject(g_pkg); - g_acq = &ACQ_SETUP; g_pkg_found = FALSE; - wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); + lstrcpynW(nm, file_name(wz.pkg), 100); + pkg_reject(wz.pkg); + wz.acq = &ACQ_SETUP; wz.pkg_found = FALSE; + wsprintfW(wz.note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); set_page(P_FAIL); return; } - if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } - lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); + if (rc == PK_PASSWORD) { lstrcpyW(wz.note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } + lstrcpyW(wz.note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); set_page(P_ERROR); } -static void pri_fail(void) { acq_start(g_acq); } // Retry / Continue download +static void pri_fail(void) { acq_start(wz.acq); } // Retry / Continue download static void pri_password(void) { @@ -395,9 +415,9 @@ static void pri_license(void) { go_install(JOB_INSTALL_ALL); } static void pri_fetch(void) // "Pause" / "Resume" { - if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part - else if (g_busy && !g_pause_req) { - g_pause_req = TRUE; + if (wz.paused) { wz.paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part + else if (wz.busy && !wz.pause_req) { + wz.pause_req = TRUE; InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); } @@ -405,19 +425,19 @@ static void pri_fetch(void) static void sec_fail(BOOL button) { - if (button && g_acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver + if (button && wz.acq == &ACQ_DRIVER && can_skip_driver()) skip_driver(); // "Skip": the fix without the driver else on_close(); } static void sec_fetch(BOOL button) { if (!button) { on_close(); return; } - if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once + if (wz.paused) fetch_stopped(); // paused: no worker is running, finish at once else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) } // The third button (left of the second one): another way on. On the error page: choose the file (or folder) yourself. -static void thr_fail(void) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } +static void thr_fail(void) { if (wz.acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } // The extra button (bottom left, link style). static void ext_dynapro(void) { ShellExecuteW(g_hwnd, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); } // also after a failed driver download (P_DRV_HASH) diff --git a/src/wizard/driver.h b/src/wizard/driver.h index 69da72d..16cbaf2 100644 --- a/src/wizard/driver.h +++ b/src/wizard/driver.h @@ -4,7 +4,7 @@ // The "Start DynaRun" button is offered only when DynaRun is installed here and our launcher is in place (g_exe comes from locate_dynarun, which // the wizard runs at start in every mode, so this also holds in the process the sign-in task starts after the restart; g_launcher from init_paths). // The driver step may be skipped (the fix is installed without it) only in the main install flow, not when it is the whole job (the shortcut, /driver, or the driver-only first page). -static BOOL can_skip_driver(void) { return g_flow == FLOW_MAIN || g_flow == FLOW_MAIN_WITH_DRIVER; } +static BOOL can_skip_driver(void) { return wz.flow == FLOW_MAIN || wz.flow == FLOW_MAIN_WITH_DRIVER; } static void enter_drv_intro(void) { @@ -25,11 +25,11 @@ static void enter_drv_intro(void) L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" L"下次重新開機會自動恢復。\n\n" L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", + if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); text(ID_BODY, b); buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_press7(void) @@ -37,17 +37,17 @@ static void enter_drv_press7(void) WCHAR *b = pgbuf; text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); { - int bl = g_drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) + int bl = wz.drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) b[0] = 0; - if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); - else if (g_drv_how & DRV_SRC_LOCAL) { - WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm - if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); + if (wz.drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); + else if (wz.drv_how & DRV_SRC_LOCAL) { + WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(wz.drvzip), 100); // the longest text: ~95 chars + 99 of nm + if (wz.drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); lstrcatW(b, t); } - if (!g_drv_allow && (g_drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); - if (g_flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", + if (!wz.drv_allow && (wz.drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); + if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); @@ -58,22 +58,22 @@ static void enter_drv_press7(void) text(ID_BODY, b); } show_image(0); - if (g_flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet + if (wz.flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_hash(void) // a driver zip that is not the file this program knows: the user decides { WCHAR *b = pgbuf; static WCHAR src[2 * MAX_PATH + 40]; - if (g_drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (g_drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, g_drvzip); } + if (wz.drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (wz.drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, wz.drvzip); } else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost b[0] = 0; - if (g_drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of + if (wz.drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" @@ -82,7 +82,7 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro cat_to(b, 2600, L"\n\n"); } else { WCHAR sz[24]; - wsprintfW(sz, L"%lu", (DWORD)g_drv_size); + wsprintfW(sz, L"%lu", (DWORD)wz.drv_size); cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" @@ -91,7 +91,7 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); cat_to(b, 2600, sz); cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); - cat_to(b, 2600, g_drv_hex); + cat_to(b, 2600, wz.drv_hex); cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); cat_to(b, 2600, driver_pinned_sha256()); cat_to(b, 2600, L"\n\n"); @@ -99,7 +99,7 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); { int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line - const WCHAR *p = g_drv_rep; + const WCHAR *p = wz.drv_rep; int used = 0, more = 0; while (*p) { const WCHAR *e = p; int n; @@ -118,7 +118,7 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro text(ID_BODY, b); // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_bde(void) @@ -142,20 +142,20 @@ static void enter_drv_prep(void) static void enter_drv_reboot(void) { - g_result = RC_OK; // the driver step is prepared; the restart continues it + wz.result = RC_OK; // the driver step is prepared; the restart continues it WCHAR *b = pgbuf; - text(ID_TITLE, g_drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); - lstrcpyW(b, g_drv_noreboot + text(ID_TITLE, wz.drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); + lstrcpyW(b, wz.drv_noreboot ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " L"When the menu appears, press 7. After you sign in, this program continues by itself.", L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); - if (g_fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped + if (wz.fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped text(ID_BODY, b); buttons(T(L"Close", L"關閉"), NULL, NULL); - if (g_fix_done) acp_warning(); + if (wz.fix_done) acp_warning(); } static void enter_drv_retry(void) @@ -166,7 +166,7 @@ static void enter_drv_retry(void) L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_warn(void) @@ -176,7 +176,7 @@ static void enter_drv_warn(void) L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); show_image(1); buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_install(void) @@ -190,9 +190,9 @@ static void enter_drv_install(void) static void enter_drv_done(void) { - g_result = RC_OK; // the driver is installed (or was already) - text(ID_TITLE, g_drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); - text(ID_BODY, g_drv_have + wz.result = RC_OK; // the driver is installed (or was already) + text(ID_TITLE, wz.drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); + text(ID_BODY, wz.drv_have ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" @@ -205,27 +205,27 @@ static void enter_drv_done(void) static void enter_drv_error(void) { - g_result = RC_FAIL; // the driver step failed + wz.result = RC_FAIL; // the driver step failed text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); - text(ID_BODY, g_note); + text(ID_BODY, wz.note); buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); } static int jp_drv_prep(void) { // "install anyway" accepts exactly this zip (its SHA-256), nothing else. - return driver_prep(&g_jobopts, g_drvzip[0] ? g_drvzip : L"-", g_drv_allow ? g_drv_hex : NULL); + return driver_prep(&g_in.opts, g_in.drvzip[0] ? g_in.drvzip : L"-", g_in.drv_allow ? g_in.drv_hex : NULL); } -static int jp_drv_install(void) { return driver_install(&g_jobopts); } -static int jp_drv_cancel(void) { return driver_cancel(&g_jobopts); } +static int jp_drv_install(void) { return driver_install(&g_in.opts); } +static int jp_drv_cancel(void) { return driver_cancel(&g_in.opts); } // "Skip" on the driver pages of the main flow: the fix is installed without the driver (the sign-in start is then registered by the install itself). static void skip_driver(void) { user_log(L"driver: skipped, the fix is installed without it"); - g_drv = FALSE; g_opts.autostart_later = FALSE; g_flow = FLOW_MAIN; - install(g_inst_job); + wz.drv = FALSE; wz.opts.autostart_later = FALSE; wz.flow = FLOW_MAIN; + install(wz.inst_job); } // The text of a failed driver step (rc: PK_* of the download, DRV_E_* of driver.c) and the error page. @@ -233,25 +233,25 @@ static void drv_error(int rc) { switch (rc) { case DRV_E_HASH: - lstrcpyW(g_note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", + lstrcpyW(wz.note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。")); break; - case DRV_E_BCD: lstrcpyW(g_note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; - case DRV_E_TASK: lstrcpyW(g_note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; - case DRV_E_DIR: lstrcpyW(g_note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", + case DRV_E_BCD: lstrcpyW(wz.note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; + case DRV_E_TASK: lstrcpyW(wz.note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; + case DRV_E_DIR: lstrcpyW(wz.note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", L"無法把工作資料夾 %ProgramData%\\DynaRunFix 設為安全狀態(舊資料夾仍在使用中,或其他使用者可以修改它)。沒有安裝任何東西。請關閉其他程式、重新啟動電腦後再試一次。")); break; - case DRV_E_PNP: lstrcpyW(g_note, T(L"Windows did not accept the driver. Driver signature checking may still have been on.", L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。")); break; - default: wsprintfW(g_note, T(L"The driver could not be prepared (code %d).", L"無法準備驅動程式(代碼 %d)。"), rc); break; + case DRV_E_PNP: lstrcpyW(wz.note, T(L"Windows did not accept the driver. Driver signature checking may still have been on.", L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。")); break; + default: wsprintfW(wz.note, T(L"The driver could not be prepared (code %d).", L"無法準備驅動程式(代碼 %d)。"), rc); break; } - lstrcatW(g_note, T(L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。")); - if (g_fix_done) lstrcatW(g_note, T(L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", + lstrcatW(wz.note, T(L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。")); + if (wz.fix_done) lstrcatW(wz.note, T(L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", L"\n\nDynaRun 的修正本身已經安裝好了。之後想再試一次驅動程式,可以用開始功能表裡的「安裝 Dyna Pro USB 驅動程式」。")); set_page(P_DRV_ERROR); } static void drv_reboot_now(void) { - g_drv_noreboot = !driver_reboot(); + wz.drv_noreboot = !driver_reboot(); set_page(P_DRV_REBOOT); } @@ -260,30 +260,30 @@ static void drv_start(void) { if (!driver_supported()) { DWORD b = os_build(); // 9200+: Windows 8 / 8.1, 6000+: Vista / 7, below: XP - lstrcpyW(g_note, T(L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", + lstrcpyW(wz.note, T(L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", L"這個 Windows 版本太舊,無法自動安裝(需要 Windows 10 或更新版本)。請手動安裝:\n\n")); if (b >= 9200) - lstrcatW(g_note, T(L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " + lstrcatW(wz.note, T(L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " L"Then install the driver in Device Manager (Update driver > Browse my computer) and choose \"Install this driver software anyway\" on the red warning.", L"Windows 8 / 8.1:按住 Shift 再按「重新啟動」,依序選 疑難排解 > 進階選項 > 啟動設定 > 重新啟動,然後按 7(停用驅動程式強制簽章;32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" L"接著在裝置管理員安裝驅動程式(更新驅動程式 > 瀏覽電腦上的驅動程式),並在紅色警告選「仍然安裝此驅動程式軟體」。")); else if (b >= 6000) - lstrcatW(g_note, T(L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " + lstrcatW(wz.note, T(L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " L"(32-bit editions do not enforce driver signing and only show the warning, so they can skip this). Then in Device Manager use Update Driver > Browse my computer > Have Disk, and choose \"Install this driver software anyway\" on the red warning.", L"Windows Vista / 7:重新開機,在出現 Windows 標誌之前連續按 F8,在「進階開機選項」選「停用驅動程式強制簽章」(32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" L"接著在裝置管理員用 更新驅動程式 > 瀏覽電腦 > 從磁片安裝,並在紅色警告選「仍然安裝此驅動程式軟體」。")); else - lstrcatW(g_note, T(L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", + lstrcatW(wz.note, T(L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", L"Windows XP 不強制驅動程式簽章。請在裝置管理員安裝驅動程式;Windows 說軟體未通過 Windows 標誌測試時,按「仍然繼續」。")); - lstrcatW(g_note, T(L"\n\nThe manual steps have the details: click \"Open the manual steps\".", L"\n\n詳細說明請按「開啟手動安裝說明」。")); + lstrcatW(wz.note, T(L"\n\nThe manual steps have the details: click \"Open the manual steps\".", L"\n\n詳細說明請按「開啟手動安裝說明」。")); set_page(P_DRV_ERROR); return; } - g_drv_have = driver_installed(); - if (g_flow == FLOW_DRIVER_CONTINUE) { - if (g_drv_have) { if (is_admin()) driver_cancel(&g_opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes + wz.drv_have = driver_installed(); + if (wz.flow == FLOW_DRIVER_CONTINUE) { + if (wz.drv_have) { if (is_admin()) driver_cancel(&wz.opts); set_page(P_DRV_DONE); } // installed meanwhile: the waiting task goes else set_page(driver_ci_state() == DRV_CI_ENFORCED ? P_DRV_RETRY : P_DRV_WARN); - } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); + } else set_page(wz.drv_have ? P_DRV_DONE : P_DRV_INTRO); } // The choice on the warning page: install the zip anyway (the elevated step is told, see driver_prep) or cancel. Both are logged. @@ -291,13 +291,13 @@ static void hash_choice(BOOL install) { static WCHAR l[2 * MAX_PATH + 120]; lstrcpyW(l, install ? L"driver zip with another hash: the user chose to install it anyway: " : L"driver zip with another hash: the user cancelled: "); - lstrcatW(l, g_drvzip); user_log(l); - if (install) { g_drv_allow = TRUE; set_page(P_DRV_PRESS7); return; } - if (!(g_drv_how & DRV_SRC_LOCAL)) driver_forget(g_drvzip); // our own refused download is not offered again; a file of the user's stays - if ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download - else if (g_drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); - g_drvzip[0] = 0; g_drv_allow = FALSE; - lstrcpyW(g_note, ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") + lstrcatW(l, wz.drvzip); user_log(l); + if (install) { wz.drv_allow = TRUE; set_page(P_DRV_PRESS7); return; } + if (!(wz.drv_how & DRV_SRC_LOCAL)) driver_forget(wz.drvzip); // our own refused download is not offered again; a file of the user's stays + if ((wz.drv_how & DRV_SRC_LOCAL) && !(wz.drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download + else if (wz.drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); + wz.drvzip[0] = 0; wz.drv_allow = FALSE; + lstrcpyW(wz.note, ((wz.drv_how & DRV_SRC_LOCAL) && !(wz.drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") : T(L"Cancelled: the driver was not installed.", L"已取消,沒有安裝驅動程式。")); set_page(P_DRV_INTRO); } @@ -306,13 +306,13 @@ static void hash_choice(BOOL install) static void acq_driver_result(int rc) { if (rc == PK_OK) { - g_drv_allow = FALSE; + wz.drv_allow = FALSE; if (!g_bmp[0]) g_bmp[0] = load_png(18, &g_bw[0], &g_bh[0]); // the picture is decoded before the page changes, so the new page is built at once set_page(P_DRV_PRESS7); } else if (rc == DRV_W_HASH) { WCHAR m[2 * MAX_PATH + 100]; - g_drv_allow = FALSE; - lstrcpyW(m, L"driver package with other files: the warning page is shown for "); lstrcatW(m, g_drvzip); user_log(m); + wz.drv_allow = FALSE; + lstrcpyW(m, L"driver package with other files: the warning page is shown for "); lstrcatW(m, wz.drvzip); user_log(m); set_page(P_DRV_HASH); } else acq_fail(rc); } @@ -381,12 +381,12 @@ static void choose_driver_source(void) } } { WCHAR l[MAX_PATH + 60]; lstrcpyW(l, L"driver: the user chose "); lstrcatW(l, p); user_log(l); } - g_acq = &ACQ_DRIVER; lstrcpyW(g_drvzip, p); + wz.acq = &ACQ_DRIVER; lstrcpyW(wz.drvzip, p); old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); - rc = driver_check(g_drvzip, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); - g_drv_how |= DRV_SRC_PICKED; // chosen by the user: always checked; declining its warning does not touch the memory of declined auto-found copies + rc = driver_check(wz.drvzip, &wz.drv_how, wz.drv_hex, &wz.drv_size, wz.drv_rep, 1500); + wz.drv_how |= DRV_SRC_PICKED; // chosen by the user: always checked; declining its warning does not touch the memory of declined auto-found copies user_log(L"driver: a file the user chose is checked as it is (the skip of declined auto-found copies does not apply to it)"); - g_drv_bl = driver_bitlocker(); + wz.drv_bl = driver_bitlocker(); SetCursor(old); acq_driver_result(rc); } @@ -397,8 +397,8 @@ static void jd_drv_prep(int job, int rc) if (rc == DRV_SUCCESS) { drv_reboot_now(); return; } if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); - set_page(g_flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); + lstrcpyW(wz.note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); + set_page(wz.flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); return; } drv_error(rc); @@ -407,10 +407,10 @@ static void jd_drv_prep(int job, int rc) static void jd_drv_install(int job, int rc) { (void)job; - if (rc == DRV_SUCCESS) { g_drv_have = FALSE; set_page(P_DRV_DONE); return; } - if (rc == RC_CANCEL) { lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } + if (rc == DRV_SUCCESS) { wz.drv_have = FALSE; set_page(P_DRV_DONE); return; } + if (rc == RC_CANCEL) { lstrcpyW(wz.note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed - lstrcpyW(g_note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); + lstrcpyW(wz.note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); set_page(P_DRV_RETRY); return; } drv_error(rc); @@ -423,9 +423,9 @@ static void jd_drv_cancel(int job, int rc) } static void pri_drv_intro(void) { acq_start(&ACQ_DRIVER); } -static void pri_drv_press7(void) { if (g_flow == FLOW_MAIN_WITH_DRIVER) install(g_inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); } // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage +static void pri_drv_press7(void) { if (wz.flow == FLOW_MAIN_WITH_DRIVER) install(wz.inst_job); else start_job(JOB_DRV_PREP, P_DRV_PREP); } // FLOW_MAIN_WITH_DRIVER: the fix and the driver preparation in one elevated stage static void pri_drv_hash(void) { hash_choice(FALSE); } // "Cancel" is the default button -static void pri_drv_retry(void) { g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); } // the staged driver is still there +static void pri_drv_retry(void) { wz.drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); } // the staged driver is still there static void pri_drv_warn(void) { start_job(JOB_DRV_INSTALL, P_DRV_INSTALL); } static void pri_drv_bde(void) { drv_reboot_now(); } // "Restart anyway" @@ -438,7 +438,7 @@ static void pri_drv_done(void) // after the restart: closing the window ends the installation like "Cancel" (done at once when this runs elevated, as the task does) static void cls_drv_task(void) { - if (g_flow == FLOW_DRIVER_CONTINUE && is_admin()) driver_cancel(&g_opts); + if (wz.flow == FLOW_DRIVER_CONTINUE && is_admin()) driver_cancel(&wz.opts); end_wizard(); } diff --git a/src/wizard/layout.h b/src/wizard/layout.h index 52ad53a..ba07f0d 100644 --- a/src/wizard/layout.h +++ b/src/wizard/layout.h @@ -11,7 +11,7 @@ static int status_need(void); // again, so the line is never cut off by the button bar and the flexible parts (license text, picture, info box) give way. static void status(const WCHAR *t, BOOL err) { - g_status_err = err; text(ID_STATUS, t); + wz.status_err = err; text(ID_STATUS, t); if (g_hwnd && !g_building && status_need() != g_sth) layout(); } @@ -34,7 +34,7 @@ static void progress(BOOL marquee) SendMessageW(p, PBM_SETMARQUEE, marquee, 40); SendMessageW(p, PBM_SETRANGE32, 0, 1000); SendMessageW(p, PBM_SETPOS, 0, 0); - g_lastpct = -1; + g_out.lastpct = -1; vis(ID_PROG, TRUE); } @@ -207,8 +207,8 @@ static void create_controls(void) SendMessageW(H(ID_DRV), BM_SETCHECK, BST_CHECKED, 0); SetWindowTextW(H(ID_PIN), T(L"Pin DynaRun to the &taskbar", L"釘選到工作列(&T)")); SetWindowTextW(H(ID_AUTO), T(L"Start DynaRun when Windows &starts", L"開機時自動啟動 DynaRun(&S)")); - SendMessageW(H(ID_PIN), BM_SETCHECK, g_opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); - SendMessageW(H(ID_AUTO), BM_SETCHECK, g_opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); + SendMessageW(H(ID_PIN), BM_SETCHECK, wz.opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); + SendMessageW(H(ID_AUTO), BM_SETCHECK, wz.opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); SetWindowTextW(H(ID_CHECK), T(L"I &accept the license agreement", L"我接受授權合約(&A)")); add(PROGRESS_CLASSW, 0, ID_PROG); add(L"STATIC", SS_NOPREFIX, ID_STATUS); diff --git a/src/wizard/main.h b/src/wizard/main.h index 0efadf8..f444683 100644 --- a/src/wizard/main.h +++ b/src/wizard/main.h @@ -32,25 +32,25 @@ static const WCHAR *disclaimer(void) static void hint_line(WCHAR *b) { WCHAR t[400]; - if (g_hint == HINT_NEWER) { + if (wz.hint == HINT_NEWER) { wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", - L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), g_hint_new, g_hint_inst); + L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), wz.hint_new, wz.hint_inst); lstrcatW(b, t); - } else if (g_hint == HINT_ZIPDIFF) + } else if (wz.hint == HINT_ZIPDIFF) lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); } -static const WCHAR *hint_btn(void) { return g_hint ? T(L"Download new version", L"下載新版") : NULL; } +static const WCHAR *hint_btn(void) { return wz.hint ? T(L"Download new version", L"下載新版") : NULL; } -static BOOL can_start_dynarun(void) { return g_installed && g_exe[0] && exists(g_launcher); } +static BOOL can_start_dynarun(void) { return wz.installed && g_exe[0] && exists(g_launcher); } -// What is said about the taskbar pin (g_instres.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with -// the install (g_fix_done: the page "Done" is skipped), on the restart page. +// What is said about the taskbar pin (wz.instres.pin, set by the user stage) and about DynaRun's Chinese text: on the page "Done" and, when the driver goes with +// the install (wz.fix_done: the page "Done" is skipped), on the restart page. static void pin_hint(WCHAR *b) { - if (g_instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); - if (g_instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 + if (wz.instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (wz.instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", @@ -67,7 +67,7 @@ static void acp_warning(void) static void enter_hello(void) { WCHAR *b = pgbuf; - if (g_flow == FLOW_DRIVER_ONLY) { + if (wz.flow == FLOW_DRIVER_ONLY) { wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", @@ -78,10 +78,10 @@ static void enter_hello(void) text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); text(ID_BODY, b); buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); return; } - if (g_installed) { + if (wz.installed) { text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" @@ -106,17 +106,17 @@ static void enter_hello(void) text(ID_BODY, b); buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); } - vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); - if (g_note[0]) status(g_note, TRUE); + vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, wz.drv_avail); + if (wz.note[0]) status(wz.note, TRUE); } static void enter_running(void) { WCHAR *b = pgbuf; WCHAR names[200]; - running_names(g_lastrun = running_programs(), names); + running_names(wz.lastrun = running_programs(), names); text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); - if (g_lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help + if (wz.lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" L"Ask that user to close DynaRun; this page then offers \"Install\".", L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" @@ -131,7 +131,7 @@ static void enter_running(void) text(ID_BODY, b); buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); } - if (g_note[0]) status(g_note, TRUE); + if (wz.note[0]) status(wz.note, TRUE); SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); } @@ -146,7 +146,7 @@ static void enter_install(void) static void enter_done(void) { - g_result = RC_OK; // everything is installed + wz.result = RC_OK; // everything is installed WCHAR *b = pgbuf; text(ID_TITLE, T(L"Done", L"完成")); lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" @@ -154,7 +154,7 @@ static void enter_done(void) L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); pin_hint(b); - if (g_opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); + if (wz.opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); text(ID_BODY, b); buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); @@ -163,72 +163,93 @@ static void enter_done(void) static void enter_error(void) { - g_result = RC_FAIL; // the install failed + wz.result = RC_FAIL; // the install failed text(ID_TITLE, T(L"Something went wrong", L"發生問題")); - text(ID_BODY, g_note); + text(ID_BODY, wz.note); buttons(T(L"Close", L"關閉"), NULL, NULL); } -static int jp_install_all(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_jobopts, &g_instres, g_msi, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_all(&g_jobopts, &g_instres, g_msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage -static int jp_install_fix(void) { return g_flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_jobopts, &g_instres, g_drvzip, g_drv_allow ? g_drv_hex : NULL) : install_fix(&g_jobopts, &g_instres); } +static int jp_install_all(void) { return g_in.flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_in.opts, &g_out.instres, g_in.msi, g_in.drvzip, g_in.drv_allow ? g_in.drv_hex : NULL) : install_all(&g_in.opts, &g_out.instres, g_in.msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage +static int jp_install_fix(void) { return g_in.flow == FLOW_MAIN_WITH_DRIVER ? install_fix_driver(&g_in.opts, &g_out.instres, g_in.drvzip, g_in.drv_allow ? g_in.drv_hex : NULL) : install_fix(&g_in.opts, &g_out.instres); } +static void mo_install(void) { wz.instres = g_out.instres; } // the pin hint static void jd_install(int job, int rc); // One row per job, indexed by the JOB_* value (nothing depends on the order of the values). -// proc runs on the worker thread; returns the job's result code +// proc runs on the worker thread; returns the job's result code. Reads g_in, writes g_out, touches nothing else of the wizard's state +// merge runs on the UI thread after the worker has been joined, before on_done: takes the part of g_out the job produced into wz (NULL: none) // on_done runs on the UI thread when the worker has ended (job_done), with the job and its result // keep_up the elevated stage or a system tool works: the UAC prompt and msiexec's window take the foreground, so ID_KEEPUP keeps the wizard behind them // bring_front the wizard comes back in front of other windows when the job is done (the elevated part allows it) typedef struct { int (*proc)(void); + void (*merge)(void); void (*on_done)(int job, int rc); BOOL keep_up, bring_front; } job_desc; static const job_desc JOBS[JOB_COUNT] = { - [JOB_ACQUIRE] = { jp_acquire, jd_acquire, FALSE, FALSE }, - [JOB_EXTRACT] = { jp_extract, jd_extract, FALSE, FALSE }, - [JOB_INSTALL_ALL] = { jp_install_all, jd_install, TRUE, TRUE }, - [JOB_INSTALL_FIX] = { jp_install_fix, jd_install, TRUE, TRUE }, - [JOB_DRV_PREP] = { jp_drv_prep, jd_drv_prep, TRUE, TRUE }, - [JOB_DRV_INSTALL] = { jp_drv_install, jd_drv_install, TRUE, TRUE }, - [JOB_DRV_CANCEL] = { jp_drv_cancel, jd_drv_cancel, TRUE, TRUE }, + [JOB_ACQUIRE] = { jp_acquire, mo_acquire, jd_acquire, FALSE, FALSE }, + [JOB_EXTRACT] = { jp_extract, NULL, jd_extract, FALSE, FALSE }, + [JOB_INSTALL_ALL] = { jp_install_all, mo_install, jd_install, TRUE, TRUE }, + [JOB_INSTALL_FIX] = { jp_install_fix, mo_install, jd_install, TRUE, TRUE }, + [JOB_DRV_PREP] = { jp_drv_prep, NULL, jd_drv_prep, TRUE, TRUE }, + [JOB_DRV_INSTALL] = { jp_drv_install, NULL, jd_drv_install, TRUE, TRUE }, + [JOB_DRV_CANCEL] = { jp_drv_cancel, NULL, jd_drv_cancel, TRUE, TRUE }, }; static DWORD WINAPI job_proc(LPVOID unused) { - int rc = JOBS[g_job].proc(); - PostMessageW(g_hwnd, WM_JOBDONE, g_job, rc); + int rc = JOBS[g_in.job].proc(); + PostMessageW(g_hwnd, WM_JOBDONE, g_in.job, rc); (void)unused; return 0; } // The update hint is asked in the background when the first page shows an installed DynaRun; the answer comes as WM_HINT. Never waited for. +// The thread writes g_hint_out and then posts WM_HINT; the UI reads g_hint_out only when that message arrives. static DWORD WINAPI hint_proc(LPVOID unused) { - static WCHAR i[40], n[40]; int r; - r = pkg_update_hint(i, n, 40, user_log); - if (r) { lstrcpyW(g_hint_inst, i); lstrcpyW(g_hint_new, n); } + int r = pkg_update_hint(g_hint_out.inst, g_hint_out.nw, 40, user_log); if (g_hwnd) PostMessageW(g_hwnd, WM_HINT, (WPARAM)r, 0); (void)unused; return 0; } +// Hands the job what it needs (g_in) and starts its output (g_out) as a copy of the UI's values, on the UI thread, before the worker exists. The password +// leaves the UI's copy here: the worker wipes the one it was given. +static void job_handover(int job) +{ + g_in.job = job; g_in.opts = wz.opts; g_in.flow = wz.flow; + g_in.acq = wz.acq; g_in.acq_local_ok = wz.acq_local_ok; + CopyMemory(g_in.msi, wz.msi, sizeof(g_in.msi)); + CopyMemory(&g_in.zip, &wz.zip, sizeof(g_in.zip)); + CopyMemory(g_in.pw, wz.pw, sizeof(g_in.pw)); SecureZeroMemory(wz.pw, sizeof(wz.pw)); + CopyMemory(g_in.drvzip, wz.drvzip, sizeof(g_in.drvzip)); g_in.drv_allow = wz.drv_allow; + CopyMemory(g_in.drv_hex, wz.drv_hex, sizeof(g_in.drv_hex)); + g_out.lastpct = -1; g_out.neterr = wz.neterr; + CopyMemory(g_out.pkg, wz.pkg, sizeof(g_out.pkg)); g_out.pkg_found = wz.pkg_found; g_out.pkg_ours = wz.pkg_ours; + CopyMemory(g_out.drvzip, wz.drvzip, sizeof(g_out.drvzip)); g_out.drv_how = wz.drv_how; + CopyMemory(g_out.drv_hex, wz.drv_hex, sizeof(g_out.drv_hex)); g_out.drv_size = wz.drv_size; + CopyMemory(g_out.drv_rep, wz.drv_rep, sizeof(g_out.drv_rep)); g_out.drv_bl = wz.drv_bl; + g_out.instres = wz.instres; +} + static void start_job(int job, int page) { DWORD tid; - g_job = job; g_cancel = 0; g_busy = TRUE; - if (job == JOB_ACQUIRE && g_dlacq != g_acq) { g_dlacq = g_acq; g_dltotal = g_dlhave = 0; } // a resume of the same download keeps what is known + g_cancel = 0; wz.busy = TRUE; + if (job == JOB_ACQUIRE && wz.dlacq != wz.acq) { wz.dlacq = wz.acq; wz.dltotal = wz.dlhave = 0; } // a resume of the same download keeps what is known set_page(page); - g_jobopts = g_opts; // the job runs with the options as they are now - g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); + job_handover(job); // the job runs with the options and the state as they are now + wz.thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); if (JOBS[job].keep_up) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); } // The install jobs start only once DynaRun is closed (see running_programs in setup.c). static void install(int job) { - g_pending = job; + wz.pending = job; if (running_programs()) set_page(P_RUNNING); else start_job(job, P_INSTALL); } @@ -237,7 +258,7 @@ static void install(int job) // one elevated stage then does both: the UAC prompt comes once, before the restart. The driver-only first page (FLOW_DRIVER_ONLY) has no install: it starts the prep itself. static void go_install(int job) { - if (g_drv && g_flow == FLOW_MAIN) { g_flow = FLOW_MAIN_WITH_DRIVER; g_inst_job = job; g_note[0] = 0; set_page(P_DRV_INTRO); } + if (wz.drv && wz.flow == FLOW_MAIN) { wz.flow = FLOW_MAIN_WITH_DRIVER; wz.inst_job = job; wz.note[0] = 0; set_page(P_DRV_INTRO); } else install(job); } @@ -250,45 +271,46 @@ static void choose_exe(void) of.lpstrFile = f; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; if (!GetOpenFileNameW(&of)) return; - lstrcpyW(g_exe, f); g_installed = TRUE; + lstrcpyW(g_exe, f); wz.installed = TRUE; set_page(P_HELLO); } static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX { - if (g_flow == FLOW_MAIN_WITH_DRIVER && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) + if (wz.flow == FLOW_MAIN_WITH_DRIVER && FIX_DONE(rc)) { // the fix is installed, and the driver step ran in the same elevated stage: its result (as for JOB_DRV_PREP) cleanup_files(); - g_fix_done = TRUE; + wz.fix_done = TRUE; if (rc == DRV_SUCCESS) drv_reboot_now(); else if (rc == DRV_W_BDE) set_page(P_DRV_BDE); else drv_error(rc); return; } if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } - if (rc == RC_RUNNING) { g_pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile + if (rc == RC_RUNNING) { wz.pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile if (rc == RC_CANCEL) { - lstrcpyW(g_note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); - set_page(g_flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it + lstrcpyW(wz.note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); + set_page(wz.flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it return; } - wsprintfW(g_note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", + wsprintfW(wz.note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); set_page(P_ERROR); } static void job_done(int job, int rc) { - WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; - if (g_close_asked) { g_opts.close = FALSE; g_close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) - if (g_closing) { DestroyWindow(g_hwnd); return; } + WaitForSingleObject(wz.thread, INFINITE); CloseHandle(wz.thread); wz.thread = NULL; wz.busy = FALSE; // the join: what the worker wrote is ours now + if (JOBS[job].merge) JOBS[job].merge(); + if (wz.close_asked) { wz.opts.close = FALSE; wz.close_asked = FALSE; } // the consent was for the job that has just ended: a later stage must not close a DynaRun started since without asking (/close from the command line stays) + if (wz.closing) { DestroyWindow(g_hwnd); return; } JOBS[job].on_done(job, rc); } // The first page's options are read when it is left, whichever button leaves it. static void take_options(void) { - g_opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; + wz.opts.pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + wz.opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; } static void end_wizard(void) { DestroyWindow(g_hwnd); } @@ -297,16 +319,16 @@ static void end_wizard(void) { DestroyWindow(g_hwnd); } static void pri_hello(void) { take_options(); - if (g_flow == FLOW_DRIVER_ONLY) { g_drv = TRUE; g_opts.autostart_later = FALSE; set_page(P_DRV_INTRO); return; } // the fix is current: straight to the driver - g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it - if (g_installed) go_install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } + if (wz.flow == FLOW_DRIVER_ONLY) { wz.drv = TRUE; wz.opts.autostart_later = FALSE; set_page(P_DRV_INTRO); return; } // the fix is current: straight to the driver + wz.drv = wz.drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + wz.opts.autostart_later = wz.drv; // the driver restart comes first: the sign-in start is registered after it + if (wz.installed) go_install(JOB_INSTALL_FIX); else { wz.acq_fresh = FALSE; acq_start(&ACQ_SETUP); } } static void pri_running(void) { - if (!g_opts.close) { g_opts.close = TRUE; g_close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only - start_job(g_pending, P_INSTALL); + if (!wz.opts.close) { wz.opts.close = TRUE; wz.close_asked = TRUE; } // the elevated stage closes DynaRun (see close_programs in setup.c), for this job only + start_job(wz.pending, P_INSTALL); } static void pri_done(void) @@ -319,9 +341,9 @@ static void pri_done(void) // (its close hook, which then also ends the wizard if that is what closing means there). static void on_close(void) { - const page_desc *d = &PAGES[g_page]; + const page_desc *d = &PAGES[wz.page]; if (d->flags & PF_NOCLOSE) return; // msiexec / registry / system tool work must not be interrupted - if (g_busy) { g_closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } + if (wz.busy) { wz.closing = TRUE; InterlockedExchange(&g_cancel, 1); return; } if (d->close) d->close(); else end_wizard(); } @@ -330,23 +352,23 @@ static void sec_none(BOOL button) { (void)button; } static void thr_hello(void) { - if (!g_hint) return; // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) + if (!wz.hint) return; // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) take_options(); - g_drv = g_flow != FLOW_DRIVER_ONLY && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; - g_opts.autostart_later = g_drv; + wz.drv = wz.flow != FLOW_DRIVER_ONLY && wz.drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + wz.opts.autostart_later = wz.drv; user_log(L"DynaRun update hint: the user chose to download the new version"); - g_acq_fresh = TRUE; // an older package found on this computer would not be the new version + wz.acq_fresh = TRUE; // an older package found on this computer would not be the new version acq_start(&ACQ_SETUP); } static void ext_hello(void) // "Reinstall the fix..." / "Installed elsewhere..." { - if (g_flow == FLOW_DRIVER_ONLY) { g_flow = FLOW_MAIN; set_page(P_HELLO); } + if (wz.flow == FLOW_DRIVER_ONLY) { wz.flow = FLOW_MAIN; set_page(P_HELLO); } else choose_exe(); } // Alt+key shortcuts that exist on one page only (the controls' own & mnemonics do not work while the focus is in the edit or the rich text). -static BOOL when_drv_avail(void) { return g_drv_avail; } +static BOOL when_drv_avail(void) { return wz.drv_avail; } static const hotkey KEYS_HELLO[] = { { 'T', ID_PIN, NULL }, { 'S', ID_AUTO, NULL }, { 'D', ID_DRV, when_drv_avail }, { 0 } }; static const hotkey KEYS_LICENSE[] = { { 'A', ID_CHECK, NULL }, { 0 } }; @@ -384,15 +406,15 @@ static const page_desc PAGES[P_COUNT] = { static void set_page(int p) { - g_page = p; - g_result = RC_CANCEL; // undecided until a page below says otherwise - g_paused = g_pause_req = FALSE; - g_checking = g_marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress + wz.page = p; + wz.result = RC_CANCEL; // undecided until a page below says otherwise + wz.paused = wz.pause_req = FALSE; + wz.checking = wz.marquee = FALSE; // a page starts with a plain bar; a download started again (also after the checking phase) shows its progress g_building = TRUE; // status() does not lay out while the page is being rebuilt; the layout at the end does KillTimer(g_hwnd, TIMER_RUNNING); vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); PAGES[p].enter(); - g_note[0] = 0; + wz.note[0] = 0; g_building = FALSE; layout(); if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) @@ -400,15 +422,15 @@ static void set_page(int p) } // The window asks the table (the controls are the same on every page). -static void on_primary(void) { if (PAGES[g_page].primary) PAGES[g_page].primary(); } -static void on_second(BOOL button) { if (PAGES[g_page].second) PAGES[g_page].second(button); else on_close(); } -static void on_third(void) { if (PAGES[g_page].third) PAGES[g_page].third(); } -static void on_extra(void) { if (PAGES[g_page].extra) PAGES[g_page].extra(); } +static void on_primary(void) { if (PAGES[wz.page].primary) PAGES[wz.page].primary(); } +static void on_second(BOOL button) { if (PAGES[wz.page].second) PAGES[wz.page].second(button); else on_close(); } +static void on_third(void) { if (PAGES[wz.page].third) PAGES[wz.page].third(); } +static void on_extra(void) { if (PAGES[wz.page].extra) PAGES[wz.page].extra(); } // The control id of the Alt+key the current page has a shortcut for; 0 when there is none. static int page_hotkey(WPARAM key) { - const hotkey *k = PAGES[g_page].keys; + const hotkey *k = PAGES[wz.page].keys; for (; k && k->key; k++) if (k->key == key && (!k->when || k->when())) return k->id; return 0; } @@ -429,12 +451,12 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) static WCHAR b[100]; DWORD done = (DWORD)w, total = (DWORD)l; if (done == (DWORD)-1) { // download over, the file is being checked: not pausable, bar and text say so - if (g_page == P_FETCH && !g_paused) { - g_marquee = TRUE; + if (wz.page == P_FETCH && !wz.paused) { + wz.marquee = TRUE; progress(TRUE); - if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(g_acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves + if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(wz.acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves else { // the file is being checked: not pausable - g_checking = TRUE; + wz.checking = TRUE; text(ID_TITLE, T(L"Checking the driver", L"正在檢查驅動程式")); status(T(L"Checking the driver...", L"正在檢查驅動程式…"), FALSE); EnableWindow(H(ID_PRIMARY), FALSE); @@ -443,13 +465,13 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) } return 0; } - if (g_checking) return 0; - if (total && g_marquee) { g_marquee = FALSE; progress(FALSE); } // the size became known - else if (!total && !g_marquee && g_page == P_FETCH) { g_marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received - if (!g_marquee) SendMessageW(H(ID_PROG), PBM_SETPOS, total ? MulDiv(done, 1000, total) : 0, 0); - g_dlhave = done; if (total) g_dltotal = total; - if (g_page == P_FETCH) { - if (total != g_total) { fetch_body(total); layout(); } + if (wz.checking) return 0; + if (total && wz.marquee) { wz.marquee = FALSE; progress(FALSE); } // the size became known + else if (!total && !wz.marquee && wz.page == P_FETCH) { wz.marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received + if (!wz.marquee) SendMessageW(H(ID_PROG), PBM_SETPOS, total ? MulDiv(done, 1000, total) : 0, 0); + wz.dlhave = done; if (total) wz.dltotal = total; + if (wz.page == P_FETCH) { + if (total != wz.total) { fetch_body(total); layout(); } wsprintfW(b, total ? L"%lu.%lu / %lu.%lu MB" : L"%lu.%lu MB", done / 1048576, done % 1048576 / 104858, total / 1048576, total % 1048576 / 104858); status(b, FALSE); @@ -457,10 +479,10 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) return 0; } case WM_TIMER: // the page follows DynaRun being closed (or started again) - if (w == TIMER_RUNNING && g_page == P_RUNNING) { + if (w == TIMER_RUNNING && wz.page == P_RUNNING) { int run = running_programs(); - if (run != g_lastrun) { - g_lastrun = run; + if (run != wz.lastrun) { + wz.lastrun = run; if (run) set_page(P_RUNNING); else { // closed by the user meanwhile: plain "Install" buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); @@ -472,8 +494,9 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) if (w == ID_KEEPUP) keep_up(h); return 0; case WM_HINT: - g_hint = (int)w; - if (g_hint && g_page == P_HELLO && !g_busy) set_page(P_HELLO); + wz.hint = (int)w; + if (wz.hint) { lstrcpyW(wz.hint_inst, g_hint_out.inst); lstrcpyW(wz.hint_new, g_hint_out.nw); } + if (wz.hint && wz.page == P_HELLO && !wz.busy) set_page(P_HELLO); return 0; case WM_JOBDONE: KillTimer(h, ID_KEEPUP); @@ -494,10 +517,10 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) return (LRESULT)g_grey; // push buttons sit on the grey footer case WM_CTLCOLORSTATIC: SetBkColor((HDC)w, RGB(255, 255, 255)); - SetTextColor((HDC)w, (HWND)l == H(ID_STATUS) && g_status_err ? RGB(192, 0, 0) : GetSysColor(COLOR_WINDOWTEXT)); + SetTextColor((HDC)w, (HWND)l == H(ID_STATUS) && wz.status_err ? RGB(192, 0, 0) : GetSysColor(COLOR_WINDOWTEXT)); return (LRESULT)g_white; case WM_CLOSE: on_close(); return 0; - case WM_DESTROY: SecureZeroMemory(g_pw, sizeof(g_pw)); SetWindowTextW(H(ID_EDIT), L""); drop_msi(); PostQuitMessage(0); return 0; + case WM_DESTROY: SecureZeroMemory(wz.pw, sizeof(wz.pw)); SecureZeroMemory(g_in.pw, sizeof(g_in.pw)); SetWindowTextW(H(ID_EDIT), L""); drop_msi(); PostQuitMessage(0); return 0; } return DefWindowProcW(h, m, w, l); } @@ -506,7 +529,8 @@ int wizard(const install_opts *init) { INITCOMMONCONTROLSEX icc; WNDCLASSW wc; NONCLIENTMETRICSW nm; RECT r; MSG msg; HDC dc; LOGFONTW lf; int key; static WCHAR title[100]; - g_opts = *init; // the command line's options; the pages change this copy + wz.result = RC_CANCEL; wz.flow = FLOW_MAIN; wz.drv_bl = BL_UNKNOWN; wz.acq = &ACQ_SETUP; // the starting values of the state that are not zero + wz.opts = *init; // the command line's options; the pages change this copy icc.dwSize = sizeof(icc); icc.dwICC = ICC_PROGRESS_CLASS | ICC_STANDARD_CLASSES; InitCommonControlsEx(&icc); dc = GetDC(NULL); g_dpi = GetDeviceCaps(dc, LOGPIXELSY); ReleaseDC(NULL, dc); @@ -533,17 +557,17 @@ int wizard(const install_opts *init) r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); if (!g_hwnd) return RC_FAIL; create_controls(); - g_installed = locate_dynarun(); - g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); + wz.installed = locate_dynarun(); + wz.drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); // g_drvmode (set by cmd_driver / cmd_driver_continue before the wizard starts) is read here and nowhere else - if (g_drvmode == DRV_MODE_STANDALONE) g_flow = FLOW_DRIVER_ONLY; - else if (g_drvmode == DRV_MODE_CONTINUE) g_flow = FLOW_DRIVER_CONTINUE; - else g_flow = g_installed && g_drv_avail && fix_current() ? FLOW_DRIVER_ONLY : FLOW_MAIN; // the fix is there and current: only the driver is missing + if (g_drvmode == DRV_MODE_STANDALONE) wz.flow = FLOW_DRIVER_ONLY; + else if (g_drvmode == DRV_MODE_CONTINUE) wz.flow = FLOW_DRIVER_CONTINUE; + else wz.flow = wz.installed && wz.drv_avail && fix_current() ? FLOW_DRIVER_ONLY : FLOW_MAIN; // the fix is there and current: only the driver is missing if (g_drvmode != DRV_MODE_NONE) drv_start(); else { DWORD tid; set_page(P_HELLO); - if (g_installed) CloseHandle(CreateThread(NULL, 0, hint_proc, NULL, 0, &tid)); // is there a newer DynaRun? (a hint; the page does not wait) + if (wz.installed) CloseHandle(CreateThread(NULL, 0, hint_proc, NULL, 0, &tid)); // is there a newer DynaRun? (a hint; the page does not wait) } ShowWindow(g_hwnd, SW_SHOWNORMAL); SetForegroundWindow(g_hwnd); @@ -561,6 +585,6 @@ int wizard(const install_opts *init) TranslateMessage(&msg); DispatchMessageW(&msg); } g_hwnd = NULL; - if (g_page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it - return g_result; + if (wz.page != P_DRV_REBOOT) autostart_flush(); // unless the restart for the driver is still to come: then the continuation does it + return wz.result; } diff --git a/tests/test_driver.c b/tests/test_driver.c index 8d99e29..29d9ff9 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -812,20 +812,23 @@ static void test_job_table(void) if (!strncmp(p, "return ", 7)) has_return = TRUE; } CHECK(has_return, L"every job proc returns its result code outside of a comment"); + // the worker reads g_in and writes g_out: the wizard_state (wz, owned by the UI thread) does not appear in a job proc + for (p = c; p < stop && strncmp(p, "wz.", 3); p++); + CHECK(p >= stop, L"no job proc touches the UI's wizard_state"); rows++; } CHECK(rows == jobs, L"every job has its own proc function"); // job_proc and job_done go through the table a = strstr(t, "static DWORD WINAPI job_proc"); end = a ? strstr(a, "WM_JOBDONE") : NULL; - CHECK(a && end && strstr(a, "JOBS[g_job].proc()") && strstr(a, "JOBS[g_job].proc()") < end, L"job_proc runs the proc of the job's row"); - // g_opts.close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it - // records that (g_close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since + CHECK(a && end && strstr(a, "JOBS[g_in.job].proc()") && strstr(a, "JOBS[g_in.job].proc()") < end, L"job_proc runs the proc of the job's row"); + // wz.opts.close (forwarded to the elevated stage as /close) is the user's consent for one install job: the button that sets it + // records that (wz.close_asked) and job_done takes it back, so a later stage does not close a DynaRun started since a = strstr(t, "static void job_done"); end = a ? strstr(a, "JOBS[job].on_done") : NULL; CHECK(a && end, L"job_done is in the wizard source and hands the result to the job's on_done"); - for (c = a; c && end && c < end; c++) if (!strncmp(c, "g_opts.close = FALSE", 20)) break; + for (c = a; c && end && c < end; c++) if (!strncmp(c, "wz.opts.close = FALSE", 21)) break; CHECK(a && end && c < end, L"job_done takes back the \"close DynaRun\" consent of the job that has ended"); - for (c = t, jobs = 0; (c = strstr(c, "g_opts.close = TRUE")) != NULL; c += 19) jobs++; - CHECK(jobs == 1 && strstr(t, "g_opts.close = TRUE; g_close_asked = TRUE"), L"the wizard sets g_opts.close only together with g_close_asked"); + for (c = t, jobs = 0; (c = strstr(c, "wz.opts.close = TRUE")) != NULL; c += 20) jobs++; + CHECK(jobs == 1 && strstr(t, "wz.opts.close = TRUE; wz.close_asked = TRUE"), L"the wizard sets wz.opts.close only together with wz.close_asked"); release(f); } From 1c8498e8c97e3d3193f3193d16a9b392f39ba906 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:06:30 +0800 Subject: [PATCH 30/41] Wizard: only the worker writes the last progress percentage on_progress remembers the last percentage it posted (lastpct) so that it posts only when the value changes. The UI's progress() used to reset that value too, from the UI thread while the worker could be inside on_progress. Now start_job sets it before the worker exists and only the worker changes it afterwards. The race was benign (an int store that at worst made one more or one fewer identical update get through), but the visible difference is that with an unknown download size (percentage 0 throughout) the UI's reset on a page or marquee change let a second or third update through; now the progress bar gets exactly one update per value. The bar position is unchanged. Co-Authored-By: Claude Opus 5.5 --- src/wizard.c | 2 +- src/wizard/acquire.h | 4 ++-- src/wizard/layout.h | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/wizard.c b/src/wizard.c index 12a00fe..e3b6475 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -116,7 +116,7 @@ typedef struct { // What a job's worker produces: written only by the worker, read by the UI only after the join in job_done, which merges it into wz (JOBS[].merge). // start_job starts it as a copy of the matching fields of wz, so a field the job does not write keeps its value. typedef struct { - int lastpct; // on_progress: the last percentage posted (also reset by the UI's progress()) + int lastpct; // on_progress: the last percentage posted (the worker's own: nobody else reads or resets it) DWORD neterr; WCHAR pkg[MAX_PATH]; BOOL pkg_found, pkg_ours; // JOB_ACQUIRE for the DynaRun setup WCHAR drvzip[MAX_PATH]; int drv_how; WCHAR drv_hex[65]; ULONGLONG drv_size; WCHAR drv_rep[1500]; int drv_bl; // JOB_ACQUIRE for the driver diff --git a/src/wizard/acquire.h b/src/wizard/acquire.h index af95b08..5e8dd28 100644 --- a/src/wizard/acquire.h +++ b/src/wizard/acquire.h @@ -112,8 +112,8 @@ static void enter_license(void) if (wz.note[0]) status(wz.note, TRUE); } -// The progress callback of the worker (the downloads, the extraction, the driver check). It only posts; the percentage it compares with is g_out.lastpct -// (set to -1 by start_job, and again by the UI's progress()). +// The progress callback of the worker (the downloads, the extraction, the driver check). It only posts; the percentage it compares with is the worker's own +// (g_out.lastpct, set to -1 by start_job): the UI never touches it. static void on_progress(DWORD done, DWORD total) { int pct = total ? MulDiv(done, 1000, total) : 0; diff --git a/src/wizard/layout.h b/src/wizard/layout.h index ba07f0d..66a9365 100644 --- a/src/wizard/layout.h +++ b/src/wizard/layout.h @@ -34,7 +34,6 @@ static void progress(BOOL marquee) SendMessageW(p, PBM_SETMARQUEE, marquee, 40); SendMessageW(p, PBM_SETRANGE32, 0, 1000); SendMessageW(p, PBM_SETPOS, 0, 0); - g_out.lastpct = -1; vis(ID_PROG, TRUE); } From c860db58336cb260fdd2248a2556bbdc721a09f0 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:10:38 +0800 Subject: [PATCH 31/41] Wizard: en/zh strings in one table The wizard's 208 T(en, zh) calls (169 different pairs) become TX(S_...) over one table, UI_STR in src/wizard/strings.h; the setup's 29 calls (27 pairs) become TS(ST_...) over SETUP_STR in src/setup/strings.h. Exact duplicates (Cancel, Close, Open website ...) are in a table once. The texts are moved unchanged: the sequence of pairs used and the strings in wizard.obj / setup.obj are identical to before. setup.rc is untouched. Co-Authored-By: Claude Opus 5.5 --- src/setup.c | 1 + src/setup/defs.h | 2 +- src/setup/exetime.h | 3 +- src/setup/machine.h | 14 +- src/setup/msi_stage.h | 12 +- src/setup/stage.h | 2 +- src/setup/strings.h | 84 ++++++++ src/setup/taskbar.h | 2 +- src/setup/uninstall.h | 34 ++-- src/wizard.c | 1 + src/wizard/acquire.h | 130 +++++------- src/wizard/driver.h | 202 +++++++------------ src/wizard/layout.h | 10 +- src/wizard/main.h | 114 ++++------- src/wizard/strings.h | 452 ++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 738 insertions(+), 325 deletions(-) create mode 100644 src/setup/strings.h create mode 100644 src/wizard/strings.h diff --git a/src/setup.c b/src/setup.c index fd25ca7..028c726 100644 --- a/src/setup.c +++ b/src/setup.c @@ -63,6 +63,7 @@ // The rest of this file is split by topic into src\setup\*.h. They are fragments of THIS translation unit (static functions and file-local // state refer to each other), not headers and not separate .c files: build.cmd and the tests compile only setup.c. The order matters: // a fragment uses only what the ones before it declare (the few forward declarations are inside the fragments). +#include "setup/strings.h" // the English / Chinese texts: TS(ST_...) #include "setup/defs.h" // constants, globals, trust_ctx #include "setup/util.h" // system-folder DLLs, memory, files, registry, shared helpers (path_join, run_process, dir_walk, app key) #include "setup/stage.h" // one elevated stage at a time, setup.log of the install folder diff --git a/src/setup/defs.h b/src/setup/defs.h index b0b2112..6da517e 100644 --- a/src/setup/defs.h +++ b/src/setup/defs.h @@ -36,5 +36,5 @@ typedef struct { WCHAR user_sid[200]; // the user who started the setup (elevated stage: from its command line); "" = none known BOOL exe_trusted; // set by check_exe: DynaRun's exe and folder cannot be changed by ordinary users } trust_ctx; -#define TITLE T(L"DynaRunFix Setup", L"DynaRunFix 安裝程式") +#define TITLE TS(ST_DYNARUNFIX_SETUP) diff --git a/src/setup/exetime.h b/src/setup/exetime.h index 3f67a01..68dd2b0 100644 --- a/src/setup/exetime.h +++ b/src/setup/exetime.h @@ -93,8 +93,7 @@ static int touch(const WCHAR *p) static int touch_report(const install_opts *o, int t) { if (t == TOUCH_STUCK) - warn(o, T(L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", - L"DynaRun V3.exe 的修改時間無法還原。已記錄在登錄檔,下次安裝或解除安裝 DynaRunFix 時會再還原。")); + warn(o, TS(ST_THE_MODIFIED_TIME_OF_DYNARUN)); else if (t == TOUCH_NOTDONE) setup_log(L"the manifest was not refreshed: DynaRun V3.exe could not be touched"); return t; } diff --git a/src/setup/machine.h b/src/setup/machine.h index b843b53..003abe9 100644 --- a/src/setup/machine.h +++ b/src/setup/machine.h @@ -17,14 +17,14 @@ static int machine_install_locked(const install_opts *o, const WCHAR *sid) { static WCHAR p[MAX_PATH + 16]; HKEY k; trust_ctx t; - if (!valid_user_sid(sid)) { error2(o, T(L"The user account is not valid.", L"使用者帳戶無效。"), sid); return RC_FAIL; } + if (!valid_user_sid(sid)) { error2(o, TS(ST_THE_USER_ACCOUNT_IS_NOT), sid); return RC_FAIL; } zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); if (still_running(o, L"install")) return RC_RUNNING; // checked again here: DynaRun may have been started since - if (!check_exe(&t)) { error2(o, T(L"DynaRun V3.exe was not found at the given path.", L"在指定的路徑找不到 DynaRun V3.exe。"), g_exe); return RC_FAIL; } + if (!check_exe(&t)) { error2(o, TS(ST_DYNARUN_V3_EXE_WAS_NOT), g_exe); return RC_FAIL; } CreateDirectoryW(g_dir, NULL); if (!extract(1, g_launcher) || !extract(2, cat3(p, g_dir, L"\\dynafix.dll", NULL)) || !extract(4, cat3(p, g_dir, L"\\LICENSE-miniz.txt", NULL)) || !extract(5, cat3(p, g_dir, L"\\LICENSE.txt", NULL))) { - error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), g_dir); + error2(o, TS(ST_CANNOT_WRITE_THE_PROGRAM_FILES), g_dir); return RC_FAIL; } { // Locale Emulator, resources 10.. (see setup.rc) @@ -34,13 +34,13 @@ static int machine_install_locked(const install_opts *o, const WCHAR *sid) CreateDirectoryW(cat3(p, g_dir, L"\\le", NULL), NULL); for (i = 0; i < (int)(sizeof(le) / sizeof(le[0])); i++) if (!extract(10 + i, cat3(p, g_dir, L"\\le\\", le[i]))) { - error2(o, T(L"Cannot write the program files. Close DynaRun and try again.", L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。"), p); + error2(o, TS(ST_CANNOT_WRITE_THE_PROGRAM_FILES), p); return RC_FAIL; } } cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); if (lstrcmpiW(p, g_self) && ((!CopyFileW(g_self, p, FALSE) && !(move_aside(p) && CopyFileW(g_self, p, FALSE))) || !same_file(g_self, p))) { - error2(o, T(L"Cannot copy the uninstaller.", L"無法複製解除安裝程式。"), p); + error2(o, TS(ST_CANNOT_COPY_THE_UNINSTALLER), p); return RC_FAIL; } driver_machine_stage(p); // the Start menu shortcut to the driver installation (p: the installed setup) @@ -181,11 +181,11 @@ static int machine_uninstall(const install_opts *o, const WCHAR *skip_sid) else if (DeleteFileW(p)) deleted = TRUE; else if (move_aside(p)) { // in use: renamed at once (a reinstall before the restart writes its own file), the renamed one goes at the next restart deleted = TRUE; - warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + warn(o, TS(ST_THE_MANIFEST_FILE_NEXT_TO)); } else { // cannot even be renamed: it goes at the next restart, and the record that it is ours stays MoveFileExW(p, NULL, MOVEFILE_DELAY_UNTIL_REBOOT); keep = TRUE; - warn(o, T(L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。")); + warn(o, TS(ST_THE_MANIFEST_FILE_NEXT_TO)); } } if (deleted) { if (t.exe_trusted) { if (touch_report(o, touch(g_exe)) == TOUCH_STUCK) stuck = TRUE; } else setup_log(L"the manifest state was not refreshed: DynaRun's folder or exe can be changed by ordinary users"); } diff --git a/src/setup/msi_stage.h b/src/setup/msi_stage.h index e38fe19..2dd9e62 100644 --- a/src/setup/msi_stage.h +++ b/src/setup/msi_stage.h @@ -1,6 +1,6 @@ // Part of setup.c (vendor MSI staging and full stage): included by it, in one compilation unit; never compiled on its own. static const WCHAR *no_admin(void) -{ return T(L"Administrator permission was not granted. Nothing was changed.", L"未取得系統管理員權限,沒有做任何變更。"); } +{ return TS(ST_ADMINISTRATOR_PERMISSION_WAS_NOT_GRANTED); } // Elevated: install DynaRun from its MSI, then the machine stage. The MSI installs per user (as the // original setup does), so it must run as the user who asked for it: with over-the-shoulder @@ -70,12 +70,12 @@ static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *ms if (still_running(o, L"full install")) { stage_unlock(); return RC_RUNNING; } if (!stage_msi(msi, dir, staged)) { stage_unlock(); - msg(T(L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。"), MB_ICONERROR); + msg(TS(ST_THE_DYNARUN_V3_SETUP_COULD), MB_ICONERROR); return RC_MSI_FAILED; } if (msi_check(staged, NULL, 0) != MSI_DYNARUN) { // the check of the copy that is going to be run drop_admin_dir(dir); stage_unlock(); - msg(T(L"This is not the DynaRun V3 setup, so it will not be installed.", L"這不是 DynaRun V3 的安裝檔,所以不會安裝它。"), MB_ICONERROR); + msg(TS(ST_THIS_IS_NOT_THE_DYNARUN), MB_ICONERROR); return RC_MSI_FAILED; } rc = run_msiexec(staged); @@ -83,13 +83,13 @@ static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *ms stage_unlock(); if (rc != 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; - wsprintfW(txt, T(L"The DynaRun V3 setup failed (Windows Installer error %d).", L"DynaRun V3 安裝失敗(Windows Installer 錯誤 %d)。"), rc); + wsprintfW(txt, TS(ST_THE_DYNARUN_V3_SETUP_FAILED), rc); if (rc == ERROR_INSTALL_ALREADY_RUNNING) - lstrcatW(txt, T(L"\n\nAnother installation is running. Wait until it finishes and try again.", L"\n\n有其他程式正在安裝,請等它完成後再試一次。")); + lstrcatW(txt, TS(ST_ANOTHER_INSTALLATION_IS_RUNNING_WAIT)); msg(txt, MB_ICONERROR); return RC_MSI_FAILED; } - if (!locate_dynarun()) { msg(T(L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。"), MB_ICONERROR); return RC_NO_DYNARUN; } + if (!locate_dynarun()) { msg(TS(ST_DYNARUN_V3_WAS_INSTALLED_BUT), MB_ICONERROR); return RC_NO_DYNARUN; } return machine_install(o, sid); } diff --git a/src/setup/stage.h b/src/setup/stage.h index 16b05be..1560ce2 100644 --- a/src/setup/stage.h +++ b/src/setup/stage.h @@ -17,7 +17,7 @@ static BOOL stage_lock(void) static void stage_unlock(void) { if (g_stage_depth > 0) { g_stage_depth--; ReleaseMutex(g_stage_mx); } } static const WCHAR *busy_text(void) -{ return T(L"Another DynaRunFix setup is running. Wait until it finishes and try again.", L"另一個 DynaRunFix 安裝程式正在執行,請等它完成後再試一次。"); } +{ return TS(ST_ANOTHER_DYNARUNFIX_SETUP_IS_RUNNING); } // A line in \setup.log (only the elevated stage writes there; the folder is administrators-only) static void setup_log(const WCHAR *line) diff --git a/src/setup/strings.h b/src/setup/strings.h new file mode 100644 index 0000000..09c02e7 --- /dev/null +++ b/src/setup/strings.h @@ -0,0 +1,84 @@ +// Every text the setup (not the wizard: src\wizard\strings.h) shows in English and Chinese, in one table. The code says TS(ST_...) where it used to say +// T(L"English", L"中文"). The same pair is in the table once. A new text: a name in the enum, a row in SETUP_STR, TS(name) where it is shown. + +enum { + ST_DYNARUNFIX_SETUP, ST_ANOTHER_DYNARUNFIX_SETUP_IS_RUNNING, ST_THE_MODIFIED_TIME_OF_DYNARUN, ST_DYNARUN_WAS_NOT_STARTED_FROM, + ST_THE_USER_ACCOUNT_IS_NOT, ST_DYNARUN_V3_EXE_WAS_NOT, ST_CANNOT_WRITE_THE_PROGRAM_FILES, ST_CANNOT_COPY_THE_UNINSTALLER, + ST_THE_MANIFEST_FILE_NEXT_TO, ST_ADMINISTRATOR_PERMISSION_WAS_NOT_GRANTED, ST_THE_DYNARUN_V3_SETUP_COULD, ST_THIS_IS_NOT_THE_DYNARUN, + ST_THE_DYNARUN_V3_SETUP_FAILED, ST_ANOTHER_INSTALLATION_IS_RUNNING_WAIT, ST_DYNARUN_V3_WAS_INSTALLED_BUT, ST_FIX_ONLY, ST_REMOVE_ALL, + ST_DYNARUNFIX_IS_NOT_INSTALLED, ST_UNINSTALL_DYNARUNFIX, ST_UNINSTALL_DYNARUNFIX_FIX_ONLY_THE, ST_DYNARUN_IS_RUNNING_AND_MUST, + ST_THE_DYNARUNFIX_FOLDER_CAN_BE, ST_THE_UNINSTALL_DID_NOT_FINISH, ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_DYNARUN, + ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_BUT, ST_DYNARUNFIX_AND_DYNARUN_V3_HAVE, ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED, + ST_COUNT }; + +typedef struct { const WCHAR *en, *zh; } ui_str; +static const ui_str SETUP_STR[ST_COUNT] = { + // defs.h + [ST_DYNARUNFIX_SETUP] = { L"DynaRunFix Setup", + L"DynaRunFix 安裝程式" }, + // stage.h + [ST_ANOTHER_DYNARUNFIX_SETUP_IS_RUNNING] = { L"Another DynaRunFix setup is running. Wait until it finishes and try again.", + L"另一個 DynaRunFix 安裝程式正在執行,請等它完成後再試一次。" }, + // exetime.h + [ST_THE_MODIFIED_TIME_OF_DYNARUN] = { L"The modified time of DynaRun V3.exe could not be put back as it was. It is kept in the registry and put back the next time DynaRunFix is installed or uninstalled.", + L"DynaRun V3.exe 的修改時間無法還原。已記錄在登錄檔,下次安裝或解除安裝 DynaRunFix 時會再還原。" }, + // taskbar.h + [ST_DYNARUN_WAS_NOT_STARTED_FROM] = { L"DynaRun was not started from here. Please start it with the \"DynaRun V3\" icon on the desktop.", + L"這裡沒有啟動 DynaRun,請改用桌面上的「DynaRun V3」圖示開啟。" }, + // machine.h + [ST_THE_USER_ACCOUNT_IS_NOT] = { L"The user account is not valid.", + L"使用者帳戶無效。" }, + [ST_DYNARUN_V3_EXE_WAS_NOT] = { L"DynaRun V3.exe was not found at the given path.", + L"在指定的路徑找不到 DynaRun V3.exe。" }, + [ST_CANNOT_WRITE_THE_PROGRAM_FILES] = { L"Cannot write the program files. Close DynaRun and try again.", + L"無法寫入程式檔案。請關閉 DynaRun 後再試一次。" }, + [ST_CANNOT_COPY_THE_UNINSTALLER] = { L"Cannot copy the uninstaller.", + L"無法複製解除安裝程式。" }, + [ST_THE_MANIFEST_FILE_NEXT_TO] = { L"The manifest file next to DynaRun V3.exe is in use and will be deleted at the next restart.", + L"DynaRun V3.exe 旁的 manifest 檔正在使用中,將在下次重新啟動時刪除。" }, + // msi_stage.h + [ST_ADMINISTRATOR_PERMISSION_WAS_NOT_GRANTED] = { L"Administrator permission was not granted. Nothing was changed.", + L"未取得系統管理員權限,沒有做任何變更。" }, + [ST_THE_DYNARUN_V3_SETUP_COULD] = { L"The DynaRun V3 setup could not be copied for installation.", + L"無法複製 DynaRun V3 安裝檔以進行安裝。" }, + [ST_THIS_IS_NOT_THE_DYNARUN] = { L"This is not the DynaRun V3 setup, so it will not be installed.", + L"這不是 DynaRun V3 的安裝檔,所以不會安裝它。" }, + [ST_THE_DYNARUN_V3_SETUP_FAILED] = { L"The DynaRun V3 setup failed (Windows Installer error %d).", + L"DynaRun V3 安裝失敗(Windows Installer 錯誤 %d)。" }, + [ST_ANOTHER_INSTALLATION_IS_RUNNING_WAIT] = { L"\n\nAnother installation is running. Wait until it finishes and try again.", + L"\n\n有其他程式正在安裝,請等它完成後再試一次。" }, + [ST_DYNARUN_V3_WAS_INSTALLED_BUT] = { L"DynaRun V3 was installed, but DynaRun V3.exe was not found.", + L"DynaRun V3 已安裝,但找不到 DynaRun V3.exe。" }, + // uninstall.h + [ST_FIX_ONLY] = { L"&Fix only", + L"只移除修正(&F)" }, + [ST_REMOVE_ALL] = { L"&Remove all", + L"全部移除(&R)" }, + [ST_DYNARUNFIX_IS_NOT_INSTALLED] = { L"DynaRunFix is not installed.", + L"DynaRunFix 尚未安裝。" }, + [ST_UNINSTALL_DYNARUNFIX] = { L"Uninstall DynaRunFix?", + L"要解除安裝 DynaRunFix 嗎?" }, + [ST_UNINSTALL_DYNARUNFIX_FIX_ONLY_THE] = { L"Uninstall DynaRunFix?\n\n" + L"Fix only: the DynaRun shortcuts start the original DynaRun again; DynaRun V3 stays installed.\n\n" + L"Remove all: DynaRun V3 is then uninstalled too, by Dyna Pro's own uninstaller.", + L"要解除安裝 DynaRunFix 嗎?\n\n" + L"只移除修正:DynaRun 的捷徑改回開啟原版,DynaRun V3 保留。\n\n" + L"全部移除:接著再用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3。" }, + [ST_DYNARUN_IS_RUNNING_AND_MUST] = { L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" + L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", + L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" + L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。" }, + [ST_THE_DYNARUNFIX_FOLDER_CAN_BE] = { L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", + L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。" }, + [ST_THE_UNINSTALL_DID_NOT_FINISH] = { L"The uninstall did not finish (code %d). Nothing else was changed.", + L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。" }, + [ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_DYNARUN] = { L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", + L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。" }, + [ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_BUT] = { L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", + L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。" }, + [ST_DYNARUNFIX_AND_DYNARUN_V3_HAVE] = { L"DynaRunFix and DynaRun V3 have been uninstalled.", + L"DynaRunFix 和 DynaRun V3 都已解除安裝。" }, + [ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED] = { L"DynaRunFix has been uninstalled.", + L"DynaRunFix 已解除安裝。" }, +}; +#define TS(id) T(SETUP_STR[id].en, SETUP_STR[id].zh) diff --git a/src/setup/taskbar.h b/src/setup/taskbar.h index 9d68919..33f0013 100644 --- a/src/setup/taskbar.h +++ b/src/setup/taskbar.h @@ -206,7 +206,7 @@ static void launch_normal(const WCHAR *file, const WCHAR *dir) } if (shell_execute_unelevated(file, dir)) { run_log(L"start DynaRun: through the desktop shell, as the normal user (this process is elevated)"); return; } run_log(L"start DynaRun: not started, this process is elevated and the desktop shell could not be asked"); - msg(T(L"DynaRun was not started from here. Please start it with the \"DynaRun V3\" icon on the desktop.", L"這裡沒有啟動 DynaRun,請改用桌面上的「DynaRun V3」圖示開啟。"), MB_ICONINFORMATION); + msg(TS(ST_DYNARUN_WAS_NOT_STARTED_FROM), MB_ICONINFORMATION); } // The wizard's "Start DynaRun": started the way a double click on the shortcut does it (the shortcut gives the diff --git a/src/setup/uninstall.h b/src/setup/uninstall.h index 440b4bd..7fc09dd 100644 --- a/src/setup/uninstall.h +++ b/src/setup/uninstall.h @@ -5,8 +5,8 @@ static LRESULT CALLBACK relabel(int code, WPARAM w, LPARAM l) { HHOOK h = g_cbt; if (code == HCBT_ACTIVATE) { - SetDlgItemTextW((HWND)w, IDYES, T(L"&Fix only", L"只移除修正(&F)")); - SetDlgItemTextW((HWND)w, IDNO, T(L"&Remove all", L"全部移除(&R)")); + SetDlgItemTextW((HWND)w, IDYES, TS(ST_FIX_ONLY)); + SetDlgItemTextW((HWND)w, IDNO, TS(ST_REMOVE_ALL)); UnhookWindowsHookEx(g_cbt); g_cbt = NULL; } return CallNextHookEx(h, code, w, l); @@ -39,19 +39,14 @@ static int uninstall(const install_opts *given) cat3(p, g_dir, L"\\DynaRunFix-Setup.exe", NULL); drop_old_temp_copies(); // copies that earlier builds left in %TEMP% if (!exists(p) && !reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0)) { - info(o, T(L"DynaRunFix is not installed.", L"DynaRunFix 尚未安裝。")); + info(o, TS(ST_DYNARUNFIX_IS_NOT_INSTALLED)); return RC_OK; } if (!o->quiet && !dynarun_product(prod)) { - if (msg(T(L"Uninstall DynaRunFix?", L"要解除安裝 DynaRunFix 嗎?"), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; + if (msg(TS(ST_UNINSTALL_DYNARUNFIX), MB_YESNO | MB_ICONQUESTION) != IDYES) return RC_CANCEL; } else if (!o->quiet) { g_cbt = SetWindowsHookExW(WH_CBT, relabel, NULL, GetCurrentThreadId()); - rc = msg(T(L"Uninstall DynaRunFix?\n\n" - L"Fix only: the DynaRun shortcuts start the original DynaRun again; DynaRun V3 stays installed.\n\n" - L"Remove all: DynaRun V3 is then uninstalled too, by Dyna Pro's own uninstaller.", - L"要解除安裝 DynaRunFix 嗎?\n\n" - L"只移除修正:DynaRun 的捷徑改回開啟原版,DynaRun V3 保留。\n\n" - L"全部移除:接著再用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3。"), MB_YESNOCANCEL | MB_ICONQUESTION); + rc = msg(TS(ST_UNINSTALL_DYNARUNFIX_FIX_ONLY_THE), MB_YESNOCANCEL | MB_ICONQUESTION); if (g_cbt) { UnhookWindowsHookEx(g_cbt); g_cbt = NULL; } if (rc == IDCANCEL) return RC_CANCEL; all = rc == IDNO; @@ -59,10 +54,7 @@ static int uninstall(const install_opts *given) // Running DynaRun / launcher / LEProc hold files of the folder: ask first, the elevated stage then closes them. // /quiet without /close goes on; what is still in use is deleted at the next restart. if (!o->quiet && running_programs()) { - if (msg(T(L"DynaRun is running and must be closed to uninstall DynaRunFix.\n\n" - L"OK closes DynaRun now: a test run in progress stops, unsaved data is lost.", - L"解除安裝 DynaRunFix 前必須關閉 DynaRun。\n\n" - L"按「確定」會立即關閉 DynaRun,正在進行的測試會中斷,沒存檔的資料會遺失。"), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; + if (msg(TS(ST_DYNARUN_IS_RUNNING_AND_MUST), MB_OKCANCEL | MB_ICONWARNING) != IDOK) return RC_CANCEL; o->close = TRUE; } reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); @@ -79,8 +71,7 @@ static int uninstall(const install_opts *given) zero(&t, sizeof(t)); lstrcpynW(t.user_sid, sid, 200); if (exists(p)) { if (g_is_writable(&t, g_dir) || g_is_writable(&t, p)) { - error2(o, T(L"The DynaRunFix folder can be changed by ordinary users, so its uninstaller is not run with administrator rights. Remove DynaRunFix as an administrator.", - L"DynaRunFix 資料夾可被一般使用者修改,因此不會以系統管理員權限執行其中的解除安裝程式。請以系統管理員身分移除 DynaRunFix。"), g_dir); + error2(o, TS(ST_THE_DYNARUNFIX_FOLDER_CAN_BE), g_dir); return RC_FAIL; } elev = p; @@ -90,7 +81,7 @@ static int uninstall(const install_opts *given) } if (rc == RC_CANCEL) { msg(no_admin(), MB_ICONWARNING); return RC_CANCEL; } if (rc) { // the elevated stage did not finish: nothing user-side is undone either - if (!o->quiet) { wsprintfW(p, T(L"The uninstall did not finish (code %d). Nothing else was changed.", L"解除安裝沒有完成(代碼 %d)。沒有做其他變更。"), rc); msg(p, MB_ICONERROR); } + if (!o->quiet) { wsprintfW(p, TS(ST_THE_UNINSTALL_DID_NOT_FINISH), rc); msg(p, MB_ICONERROR); } return rc; } restore_shortcuts(HKEY_CURRENT_USER, USERKEY); @@ -101,17 +92,16 @@ static int uninstall(const install_opts *given) SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL); if (all) { rc = remove_dynarun(); - if (rc == ERROR_INSTALL_USEREXIT) { info(o, T(L"DynaRunFix has been uninstalled. DynaRun V3 was kept (its uninstall was cancelled).", L"DynaRunFix 已解除安裝。DynaRun V3 的解除安裝已取消,DynaRun V3 保留。")); return RC_CANCEL; } + if (rc == ERROR_INSTALL_USEREXIT) { info(o, TS(ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_DYNARUN)); return RC_CANCEL; } if (rc > 0 && rc != ERROR_SUCCESS_REBOOT_REQUIRED) { - wsprintfW(p, T(L"DynaRunFix has been uninstalled, but uninstalling DynaRun V3 failed (Windows Installer error %d).", - L"DynaRunFix 已解除安裝,但 DynaRun V3 解除安裝失敗(Windows Installer 錯誤 %d)。"), rc); + wsprintfW(p, TS(ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED_BUT), rc); msg(p, MB_ICONERROR); return RC_MSI_FAILED; } - info(o, T(L"DynaRunFix and DynaRun V3 have been uninstalled.", L"DynaRunFix 和 DynaRun V3 都已解除安裝。")); + info(o, TS(ST_DYNARUNFIX_AND_DYNARUN_V3_HAVE)); return RC_OK; } - info(o, T(L"DynaRunFix has been uninstalled.", L"DynaRunFix 已解除安裝。")); + info(o, TS(ST_DYNARUNFIX_HAS_BEEN_UNINSTALLED)); return RC_OK; } diff --git a/src/wizard.c b/src/wizard.c index e3b6475..99f396f 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -171,6 +171,7 @@ static void go_install(int job); static void on_close(void); static void end_wizard(void); +#include "wizard/strings.h" // the English / Chinese texts: TX(S_...) #include "wizard/layout.h" // the window: UI helpers, screenshots, controls, layout #include "wizard/acquire.h" // getting the DynaRun setup (and the driver package): download, password, check, license #include "wizard/driver.h" // the driver pages and jobs diff --git a/src/wizard/acquire.h b/src/wizard/acquire.h index 5e8dd28..cfc1458 100644 --- a/src/wizard/acquire.h +++ b/src/wizard/acquire.h @@ -10,9 +10,9 @@ static BOOL resume_note(void) fmt_mb(a, have); if (wz.dltotal > have) { fmt_mb(t, wz.dltotal); - wsprintfW(line, T(L"\n\n%s of %s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s / %s MB,按「繼續下載」會從中斷的地方接續。"), a, t); + wsprintfW(line, TX(S_OF_MB_ARE_DOWNLOADED_CONTINUE), a, t); } else - wsprintfW(line, T(L"\n\n%s MB are downloaded. \"Continue download\" carries on from there.", L"\n\n已下載 %s MB,按「繼續下載」會從中斷的地方接續。"), a); + wsprintfW(line, TX(S_MB_ARE_DOWNLOADED_CONTINUE_DOWNLOAD), a); lstrcatW(wz.note, line); return TRUE; } @@ -22,11 +22,8 @@ static void fetch_body(DWORD total) { WCHAR size[40], b[500]; size[0] = 0; - if (total) wsprintfW(size, T(L" (%lu.%lu MB)", L"(%lu.%lu MB)"), total / 1048576, total % 1048576 / 104858); - wsprintfW(b, T(L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" - L"If it does not work, click \"Open website\" to get it from the website yourself.", - L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" - L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(wz.acq), size); + if (total) wsprintfW(size, TX(S_MB), total / 1048576, total % 1048576 / 104858); + wsprintfW(b, TX(S_GETTING_A_COPY_ON_THIS), ACQ_NAME(wz.acq), size); wz.total = total; text(ID_BODY, b); } @@ -34,79 +31,73 @@ static void fetch_body(DWORD total) static void enter_fetch(void) // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver { WCHAR *b = pgbuf; - wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(wz.acq)); + wsprintfW(b, TX(S_GETTING), ACQ_NAME(wz.acq)); text(ID_TITLE, b); fetch_body(0); progress(FALSE); status(L" ", FALSE); - buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + buttons(TX(S_PAUSE), TX(S_CANCEL), TX(S_OPEN_WEBSITE)); } static void enter_fail(void) // the one error page of the acquire flow { wz.result = RC_FAIL; // the acquire flow ended on its error page (Retry and the other ways on leave it) WCHAR *b = pgbuf; - if (wz.badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); - else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(wz.acq)); + if (wz.badfile) lstrcpyW(b, TX(S_THIS_IS_NOT_THE_DYNARUN)); + else wsprintfW(b, TX(S_COULD_NOT_BE_DOWNLOADED), ACQ_NAME(wz.acq)); if (!g_zh && !wz.badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence text(ID_TITLE, b); { BOOL resume = !wz.badfile && resume_note(); // a .part to carry on from (appends its size to the note) BOOL skip = wz.acq == &ACQ_DRIVER && can_skip_driver(); wz.badfile = FALSE; - wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" - L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", - L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" - L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), wz.note, T(wz.acq->get_en, wz.acq->get_zh), T(wz.acq->pick_en, wz.acq->pick_zh)); + wsprintfW(b, TX(S_GET_IT_YOURSELF_1_CLICK), wz.note, T(wz.acq->get_en, wz.acq->get_zh), T(wz.acq->pick_en, wz.acq->pick_zh)); text(ID_BODY, b); - buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), - T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); + buttons4(resume ? TX(S_CONTINUE_DOWNLOAD) : TX(S_RETRY), skip ? TX(S_SKIP) : TX(S_CANCEL), + TX(S_CHOOSE_FILE), TX(S_OPEN_WEBSITE)); } } static void enter_password(void) { WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Enter the setup password", L"輸入安裝密碼")); - wsprintfW(b, T(L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", - L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(wz.pkg)); + text(ID_TITLE, TX(S_ENTER_THE_SETUP_PASSWORD)); + wsprintfW(b, TX(S_TYPE_THE_PASSWORD_THAT_DYNA), file_name(wz.pkg)); text(ID_BODY, b); vis(ID_EDIT, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); + buttons(TX(S_NEXT), TX(S_CANCEL), TX(S_CHOOSE_FILE)); if (wz.note[0]) status(wz.note, TRUE); SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); } static void enter_extract(void) { - text(ID_TITLE, T(L"Opening the setup file", L"正在打開安裝檔")); - text(ID_BODY, T(L"Please wait...", L"請稍候…")); + text(ID_TITLE, TX(S_OPENING_THE_SETUP_FILE)); + text(ID_BODY, TX(S_PLEASE_WAIT)); progress(FALSE); - buttons(NULL, T(L"Cancel", L"取消"), NULL); + buttons(NULL, TX(S_CANCEL), NULL); } static void enter_check(void) // everything we know about the setup file, to compare with what Dyna Pro gives { - text(ID_TITLE, T(L"Check the installer", L"確認安裝檔")); - text(ID_BODY, T(L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", - L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); + text(ID_TITLE, TX(S_CHECK_THE_INSTALLER)); + text(ID_BODY, TX(S_COMPARE_THIS_WITH_WHAT_DYNA)); SetWindowTextW(H(ID_INFO), wz.info); vis(ID_INFO, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + buttons(TX(S_NEXT), TX(S_CANCEL), TX(S_OPEN_WEBSITE)); if (wz.note[0]) status(wz.note, TRUE); } static void enter_license(void) { WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Dyna Pro license agreement", L"Dyna Pro 授權合約")); - lstrcpyW(b, wz.nolicense ? T(L"The license agreement in this DynaRun V3 setup could not be shown.", - L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。") - : T(L"Please read Dyna Pro's license agreement for DynaRun V3.", L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。")); + text(ID_TITLE, TX(S_DYNA_PRO_LICENSE_AGREEMENT)); + lstrcpyW(b, wz.nolicense ? TX(S_THE_LICENSE_AGREEMENT_IN_THIS) + : TX(S_PLEASE_READ_DYNA_PRO_S)); text(ID_BODY, b); - SetWindowTextW(H(ID_CHECK), wz.nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") - : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); + SetWindowTextW(H(ID_CHECK), wz.nolicense ? TX(S_I_UNDERSTAND_AND_ACCEPT_DYNA) + : TX(S_I_ACCEPT_THE_LICENSE_AGREEMENT)); vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), wz.nolicense ? T(L"Open website", L"開啟官網") : NULL); + buttons(TX(S_INSTALL), TX(S_CANCEL), wz.nolicense ? TX(S_OPEN_WEBSITE) : NULL); EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus if (wz.note[0]) status(wz.note, TRUE); @@ -194,8 +185,7 @@ static void not_dynarun(void) WCHAR m[MAX_PATH], nm[100]; wz.acq = &ACQ_SETUP; lstrcpynW(nm, file_name(wz.pkg), 100); // wsprintfW does not check the size of wz.note: a long file name must not overflow it - wsprintfW(wz.note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", - L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。"), nm); + wsprintfW(wz.note, TX(S_IS_NOT_THE_DYNARUN_V3), nm); pkg_reject(wz.pkg); pkg_temp_dir(m); lstrcatW(m, L"\\Setup.msi"); if (!lstrcmpiW(wz.msi, m)) DeleteFileW(wz.msi); // our extracted copy @@ -208,7 +198,7 @@ static void not_dynarun(void) static void fmt_size(WCHAR *o, ULONGLONG n) { DWORD v = (DWORD)n; - wsprintfW(o, T(L"%lu bytes (%lu.%lu MB)", L"%lu 位元組(%lu.%lu MB)"), v, v / 1048576, v % 1048576 / 104858); + wsprintfW(o, TX(S_BYTES_MB), v, v / 1048576, v % 1048576 / 104858); } // Shown on the license page so the user can compare with what Dyna Pro provides. Display only: nothing here blocks the install. @@ -221,17 +211,17 @@ static void build_info(void) msi_info(wz.msi, ver, mfr, 64); SetCursor(old); wz.info[0] = 0; - lstrcatW(wz.info, T(L"Source: ", L"來源:")); - lstrcatW(wz.info, wz.pkg_ours ? T(L"downloaded now from dynapro.co.uk", L"剛從 dynapro.co.uk 下載") - : wz.pkg_found ? T(L"found on this computer, not downloaded by DynaRunFix", L"在這台電腦上找到,不是 DynaRunFix 下載的") - : T(L"chosen by you", L"你選擇的檔案")); - lstrcatW(wz.info, T(L"\r\n\r\nFile:\r\n", L"\r\n\r\n檔案路徑:\r\n")); lstrcatW(wz.info, wz.pkg); + lstrcatW(wz.info, TX(S_SOURCE)); + lstrcatW(wz.info, wz.pkg_ours ? TX(S_DOWNLOADED_NOW_FROM_DYNAPRO_CO) + : wz.pkg_found ? TX(S_FOUND_ON_THIS_COMPUTER_NOT) + : TX(S_CHOSEN_BY_YOU)); + lstrcatW(wz.info, TX(S_FILE)); lstrcatW(wz.info, wz.pkg); fmt_size(sz, size); - lstrcatW(wz.info, T(L"\r\n\r\nSize: ", L"\r\n\r\n大小:")); lstrcatW(wz.info, sz); + lstrcatW(wz.info, TX(S_SIZE)); lstrcatW(wz.info, sz); lstrcatW(wz.info, L"\r\nSHA-256:\r\n"); lstrcatW(wz.info, okp ? hex : L"?"); if (!same) { fmt_size(sz, size2); - lstrcatW(wz.info, T(L"\r\n\r\nSetup.msi inside the file, size: ", L"\r\n\r\n檔案裡的 Setup.msi,大小:")); lstrcatW(wz.info, sz); + lstrcatW(wz.info, TX(S_SETUP_MSI_INSIDE_THE_FILE)); lstrcatW(wz.info, sz); lstrcatW(wz.info, L"\r\nSHA-256:\r\n"); lstrcatW(wz.info, okm ? hex2 : L"?"); } lstrcatW(wz.info, L"\r\n\r\nMSI ProductVersion: "); lstrcatW(wz.info, ver[0] ? ver : L"?"); @@ -251,16 +241,7 @@ static void after_extract(void) es.dwCookie = (DWORD_PTR)&src; es.dwError = 0; es.pfnCallback = rtf_in; SendMessageW(H(ID_RICH), EM_STREAMIN, SF_RTF, (LPARAM)&es); } else // never install without the user accepting Dyna Pro's terms - SetWindowTextW(H(ID_RICH), T(L"DynaRun V3 is licensed to you by Dyna Pro Dynamometers Ltd under their end-user license agreement " - L"and their terms and conditions of sale. The agreement is part of Dyna Pro's setup, but this " - L"installer could not read it from this setup file.\r\n\r\n" - L"To read it, contact Dyna Pro (support@dynapro.co.uk) or see their website " - L"(\"Open website\" below).\r\n\r\n" - L"Install only if you accept Dyna Pro's license terms.", - L"DynaRun V3 是 Dyna Pro Dynamometers Ltd 依其使用者授權合約及銷售條款授權給你使用的軟體。" - L"授權合約包含在 Dyna Pro 的安裝檔中,但本安裝程式無法從這個安裝檔讀出合約內容。\r\n\r\n" - L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟官網」)。\r\n\r\n" - L"請在同意 Dyna Pro 的授權條款後再安裝。")); + SetWindowTextW(H(ID_RICH), TX(S_DYNARUN_V3_IS_LICENSED_TO)); SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); set_page(P_CHECK); } @@ -294,7 +275,7 @@ static void choose_file(void) OPENFILENAMEW of; WCHAR f[MAX_PATH]; zero(&of, sizeof(of)); f[0] = 0; of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; - of.lpstrFilter = T(L"DynaRun V3 setup (*.zip;*.msi)\0*.zip;*.msi\0All files\0*.*\0", L"DynaRun V3 安裝檔 (*.zip;*.msi)\0*.zip;*.msi\0所有檔案\0*.*\0"); + of.lpstrFilter = TX(S_DYNARUN_V3_SETUP_ZIP_MSI); of.lpstrFile = f; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; if (!GetOpenFileNameW(&of)) return; @@ -324,19 +305,15 @@ static void cleanup_files(void) static void acq_note(int rc) { switch (rc) { - case PK_CANCEL: lstrcpyW(wz.note, T(L"The download was stopped.", L"已停止下載。")); break; - case PK_HTTP: wsprintfW(wz.note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), wz.neterr); break; - case PK_CERT: wsprintfW(wz.note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", - L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), wz.neterr); break; - case PK_URL: lstrcpyW(wz.note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", - L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); break; - case PK_IO: wsprintfW(wz.note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), wz.neterr); break; - case DRV_E_HASH: lstrcpyW(wz.note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", - L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。")); break; + case PK_CANCEL: lstrcpyW(wz.note, TX(S_THE_DOWNLOAD_WAS_STOPPED)); break; + case PK_HTTP: wsprintfW(wz.note, TX(S_THE_DYNA_PRO_WEBSITE_ANSWERED), wz.neterr); break; + case PK_CERT: wsprintfW(wz.note, TX(S_THE_SECURITY_CERTIFICATE_OF_THE), wz.neterr); break; + case PK_URL: lstrcpyW(wz.note, TX(S_THE_DOWNLOAD_WAS_REFUSED_THE)); break; + case PK_IO: wsprintfW(wz.note, TX(S_THE_FILE_COULD_NOT_BE), wz.neterr); break; + case DRV_E_HASH: lstrcpyW(wz.note, TX(S_THAT_FILE_IS_NOT_A)); break; default: // PK_NET and anything unforeseen: the first real error of the attempts - if (wz.neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(wz.note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", - L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。")); - else wsprintfW(wz.note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), wz.neterr); + if (wz.neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(wz.note, TX(S_THIS_COMPUTER_IS_NOT_CONNECTED)); + else wsprintfW(wz.note, TX(S_NO_CONNECTION_TO_THE_DYNA), wz.neterr); break; } } @@ -350,9 +327,9 @@ static void fetch_paused(void) WCHAR a[24], t[24], s[160]; wz.paused = TRUE; wz.pause_req = FALSE; fmt_mb(a, wz.dlhave); - if (wz.dltotal) { fmt_mb(t, wz.dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } - else wsprintfW(s, T(L"Paused: %s MB downloaded.", L"已暫停:已下載 %s MB。"), a); - buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); + if (wz.dltotal) { fmt_mb(t, wz.dltotal); wsprintfW(s, TX(S_PAUSED_OF_MB_DOWNLOADED), a, t); } + else wsprintfW(s, TX(S_PAUSED_MB_DOWNLOADED), a); + buttons(TX(S_RESUME), TX(S_CANCEL), TX(S_OPEN_WEBSITE)); status(s, FALSE); layout(); } @@ -388,12 +365,12 @@ static void jd_extract(int job, int rc) lstrcpynW(nm, file_name(wz.pkg), 100); pkg_reject(wz.pkg); wz.acq = &ACQ_SETUP; wz.pkg_found = FALSE; - wsprintfW(wz.note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); + wsprintfW(wz.note, TX(S_THE_FILE_FOUND_ON_THIS), nm); set_page(P_FAIL); return; } - if (rc == PK_PASSWORD) { lstrcpyW(wz.note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); return; } - lstrcpyW(wz.note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); + if (rc == PK_PASSWORD) { lstrcpyW(wz.note, TX(S_WRONG_PASSWORD_OR_THE_FILE)); set_page(P_PASSWORD); return; } + lstrcpyW(wz.note, TX(S_THE_SETUP_COULD_NOT_BE)); set_page(P_ERROR); } @@ -402,8 +379,7 @@ static void pri_fail(void) { acq_start(wz.acq); } static void pri_password(void) { if (!take_password()) { - status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", - L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。"), TRUE); + status(TX(S_WRONG_PASSWORD_PLEASE_CHECK_IT), TRUE); SetFocus(H(ID_EDIT)); return; } @@ -419,7 +395,7 @@ static void pri_fetch(void) else if (wz.busy && !wz.pause_req) { wz.pause_req = TRUE; InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag - text(ID_PRIMARY, T(L"Pausing...", L"暫停中…")); EnableWindow(H(ID_PRIMARY), FALSE); layout(); + text(ID_PRIMARY, TX(S_PAUSING)); EnableWindow(H(ID_PRIMARY), FALSE); layout(); } } diff --git a/src/wizard/driver.h b/src/wizard/driver.h index 16cbaf2..fa934c9 100644 --- a/src/wizard/driver.h +++ b/src/wizard/driver.h @@ -9,57 +9,38 @@ static BOOL can_skip_driver(void) { return wz.flow == FLOW_MAIN || wz.flow == FL static void enter_drv_intro(void) { WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); - lstrcpyW(b, T(L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" - L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" - L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" - L" 3. come back by itself after you sign in and finish the installation\n\n" - L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " - L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" - L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " - L"It resumes by itself, after at most 3 restarts.", - L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" - L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" - L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" - L" 3. 登入後本程式會自動回來,完成安裝\n\n" - L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" - L"下次重新開機會自動恢復。\n\n" - L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", - L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。")); + text(ID_TITLE, TX(S_INSTALL_THE_DYNA_PRO_USB)); + lstrcpyW(b, TX(S_WINDOWS_NEEDS_DYNA_PRO_S)); + if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, TX(S_DYNARUN_S_FIX_IS_INSTALLED)); text(ID_BODY, b); - buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); + buttons(TX(S_NEXT), can_skip_driver() ? TX(S_SKIP) : TX(S_CANCEL), NULL); if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_press7(void) { WCHAR *b = pgbuf; - text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); + text(ID_TITLE, TX(S_AFTER_THE_RESTART_YOU_WILL)); { int bl = wz.drv_bl; // asked in the worker (not elevated: -1 when WMI will not tell) b[0] = 0; - if (wz.drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); + if (wz.drv_allow) lstrcatW(b, TX(S_NOTE_THIS_DRIVER_FILE_IS)); else if (wz.drv_how & DRV_SRC_LOCAL) { WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(wz.drvzip), 100); // the longest text: ~95 chars + 99 of nm - if (wz.drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); - else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); + if (wz.drv_how & DRV_SRC_DIR) wsprintfW(t, TX(S_THE_UNPACKED_DRIVER_FOLDER_WAS), nm); + else wsprintfW(t, TX(S_THE_DRIVER_WAS_FOUND_ON), nm); lstrcatW(b, t); } - if (!wz.drv_allow && (wz.drv_how & DRV_ZIPDIFF)) lstrcatW(b, T(L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n")); - if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, T(L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", - L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n")); - lstrcatW(b, T(L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", - L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。")); - if (bl > 0) lstrcatW(b, T(L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); - else if (bl < 0) lstrcatW(b, T(L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", - L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。")); + if (!wz.drv_allow && (wz.drv_how & DRV_ZIPDIFF)) lstrcatW(b, TX(S_THE_ZIP_DIFFERS_FROM_THE)); + if (wz.flow == FLOW_MAIN_WITH_DRIVER) lstrcatW(b, TX(S_CLICK_INSTALL_AND_RESTART_DYNARUN)); + lstrcatW(b, TX(S_TIP_TAKE_A_PHOTO_OF)); + if (bl > 0) lstrcatW(b, TX(S_THIS_COMPUTER_USES_BITLOCKER_PROTECTION)); + else if (bl < 0) lstrcatW(b, TX(S_IF_THIS_COMPUTER_USES_BITLOCKER)); text(ID_BODY, b); } show_image(0); - if (wz.flow == FLOW_MAIN_WITH_DRIVER) buttons(T(L"Install and restart", L"安裝並重新開機"), T(L"Cancel", L"取消"), NULL); // nothing is installed yet - else buttons(T(L"Restart now", L"重新開機"), T(L"Later", L"稍後"), NULL); + if (wz.flow == FLOW_MAIN_WITH_DRIVER) buttons(TX(S_INSTALL_AND_RESTART), TX(S_CANCEL), NULL); // nothing is installed yet + else buttons(TX(S_RESTART_NOW), TX(S_LATER), NULL); if (wz.note[0]) status(wz.note, TRUE); } @@ -67,36 +48,30 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro { WCHAR *b = pgbuf; static WCHAR src[2 * MAX_PATH + 40]; - if (wz.drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (wz.drv_how & DRV_SRC_DIR) ? T(L"found on this computer (unpacked folder): ", L"在這台電腦上找到(已解開的資料夾):") : T(L"found on this computer: ", L"在這台電腦上找到:")); lstrcatW(src, wz.drvzip); } - else lstrcpyW(src, T(L"downloaded from the Dyna Pro website (dynapro.co.uk)", L"從 Dyna Pro 官網下載")); - text(ID_TITLE, T(L"The driver differs from the known version", L"驅動程式和已知版本不同")); + if (wz.drv_how & DRV_SRC_LOCAL) { lstrcpyW(src, (wz.drv_how & DRV_SRC_DIR) ? TX(S_FOUND_ON_THIS_COMPUTER_UNPACKED) : TX(S_FOUND_ON_THIS_COMPUTER)); lstrcatW(src, wz.drvzip); } + else lstrcpyW(src, TX(S_DOWNLOADED_FROM_THE_DYNA_PRO)); + text(ID_TITLE, TX(S_THE_DRIVER_DIFFERS_FROM_THE)); // wsprintfW cuts its output at 1024 characters, so the text is put together with bounded appends (b holds 2600): the advice to // cancel comes before the details, and the list of files goes last so that only the end of the list can be lost b[0] = 0; if (wz.drv_how & DRV_SRC_DIR) { // an unpacked folder: there is no zip to show a size or hash of - cat_to(b, 2600, T(L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" - L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, TX(S_THE_DRIVER_FILES_IN_THIS)); cat_to(b, 2600, src); cat_to(b, 2600, L"\n\n"); } else { WCHAR sz[24]; wsprintfW(sz, L"%lu", (DWORD)wz.drv_size); - cat_to(b, 2600, T(L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" - L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", - L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" - L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:")); + cat_to(b, 2600, TX(S_THE_DRIVER_FILES_IN_THIS_2)); cat_to(b, 2600, src); - cat_to(b, 2600, T(L"\nZip size: ", L"\n壓縮檔大小:")); + cat_to(b, 2600, TX(S_ZIP_SIZE)); cat_to(b, 2600, sz); - cat_to(b, 2600, T(L" bytes\nZip SHA-256: ", L" 位元組\n壓縮檔 SHA-256:")); + cat_to(b, 2600, TX(S_BYTES_ZIP_SHA_256)); cat_to(b, 2600, wz.drv_hex); - cat_to(b, 2600, T(L"\nKnown zip SHA-256: ", L"\n已知的壓縮檔 SHA-256:")); + cat_to(b, 2600, TX(S_KNOWN_ZIP_SHA_256)); cat_to(b, 2600, driver_pinned_sha256()); cat_to(b, 2600, L"\n\n"); } - cat_to(b, 2600, T(L"Files that differ (actual / expected):\n", L"不同的檔案(實際 / 已知):\n")); + cat_to(b, 2600, TX(S_FILES_THAT_DIFFER_ACTUAL_EXPECTED)); { int room = 2600 - lstrlenW(b) - 48; // keep room for the "and N more" line const WCHAR *p = wz.drv_rep; @@ -111,31 +86,27 @@ static void enter_drv_hash(void) // a driver zip that is not the file this pro } if (more) { WCHAR t[48]; - wsprintfW(t, T(L" (and %d more)", L" (另外還有 %d 個)"), more); + wsprintfW(t, TX(S_AND_MORE), more); cat_to(b, 2600, t); } } text(ID_BODY, b); // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left - buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); + buttons(TX(S_CANCEL), TX(S_INSTALL_ANYWAY), TX(S_OPEN_WEBSITE)); if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_bde(void) { - text(ID_TITLE, T(L"BitLocker could not be suspended", L"BitLocker 沒有暫停成功")); - text(ID_BODY, T(L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " - L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " - L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", - L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" - L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。")); - buttons(T(L"Restart anyway", L"仍要重新開機"), T(L"Later", L"稍後"), T(L"Find the recovery key", L"查詢修復金鑰")); + text(ID_TITLE, TX(S_BITLOCKER_COULD_NOT_BE_SUSPENDED)); + text(ID_BODY, TX(S_THIS_COMPUTER_USES_BITLOCKER_AND)); + buttons(TX(S_RESTART_ANYWAY), TX(S_LATER), TX(S_FIND_THE_RECOVERY_KEY)); } static void enter_drv_prep(void) { - text(ID_TITLE, T(L"Please wait", L"請稍候")); - text(ID_BODY, T(L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + text(ID_TITLE, TX(S_PLEASE_WAIT_2)); + text(ID_BODY, TX(S_GETTING_READY_WHEN_WINDOWS_ASKS)); progress(TRUE); buttons(NULL, NULL, NULL); } @@ -144,46 +115,37 @@ static void enter_drv_reboot(void) { wz.result = RC_OK; // the driver step is prepared; the restart continues it WCHAR *b = pgbuf; - text(ID_TITLE, wz.drv_noreboot ? T(L"Please restart the computer", L"請重新開機") : T(L"Restarting", L"即將重新開機")); + text(ID_TITLE, wz.drv_noreboot ? TX(S_PLEASE_RESTART_THE_COMPUTER) : TX(S_RESTARTING)); lstrcpyW(b, wz.drv_noreboot - ? T(L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " - L"When the menu appears, press 7. After you sign in, this program continues by itself.", - L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。") - : T(L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" - L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", - L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。")); + ? TX(S_EVERYTHING_IS_READY_BUT_WINDOWS) + : TX(S_THE_COMPUTER_RESTARTS_NOW_WHEN)); if (wz.fix_done) pin_hint(b); // the fix was installed with the driver: the hints of the page "Done", which is skipped text(ID_BODY, b); - buttons(T(L"Close", L"關閉"), NULL, NULL); + buttons(TX(S_CLOSE), NULL, NULL); if (wz.fix_done) acp_warning(); } static void enter_drv_retry(void) { - text(ID_TITLE, T(L"7 was not pressed", L"剛才沒有按到 7")); - text(ID_BODY, T(L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" - L"Try again? The computer restarts once more. When the menu appears, press 7.", - L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" - L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); - buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); + text(ID_TITLE, TX(S_7_WAS_NOT_PRESSED)); + text(ID_BODY, TX(S_THE_RESTART_MENU_WAS_SKIPPED)); + buttons(TX(S_RETRY), TX(S_CANCEL), NULL); if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_warn(void) { - text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); - text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", - L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); + text(ID_TITLE, TX(S_LAST_STEP_INSTALL_THE_DRIVER)); + text(ID_BODY, TX(S_DRIVER_SIGNATURE_CHECKING_IS_OFF)); show_image(1); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); + buttons(TX(S_INSTALL), TX(S_CANCEL), NULL); if (wz.note[0]) status(wz.note, TRUE); } static void enter_drv_install(void) { - text(ID_TITLE, T(L"Installing the driver", L"正在安裝驅動程式")); - text(ID_BODY, T(L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。")); + text(ID_TITLE, TX(S_INSTALLING_THE_DRIVER)); + text(ID_BODY, TX(S_INSTALLING_PLEASE_WAIT_IF_WINDOWS)); progress(TRUE); buttons(NULL, NULL, NULL); } @@ -191,24 +153,20 @@ static void enter_drv_install(void) static void enter_drv_done(void) { wz.result = RC_OK; // the driver is installed (or was already) - text(ID_TITLE, wz.drv_have ? T(L"The driver is already installed", L"驅動程式已安裝") : T(L"The driver is installed", L"驅動程式安裝完成")); + text(ID_TITLE, wz.drv_have ? TX(S_THE_DRIVER_IS_ALREADY_INSTALLED) : TX(S_THE_DRIVER_IS_INSTALLED)); text(ID_BODY, wz.drv_have - ? T(L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", - L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。") - : T(L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" - L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", - L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" - L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。")); - if (can_start_dynarun()) buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); // only after a success (this page); a failure shows P_DRV_ERROR - else buttons(T(L"Close", L"關閉"), NULL, NULL); + ? TX(S_THE_DYNA_PRO_USB_DRIVER) + : TX(S_CONNECT_THE_DYNA_PRO_DYNAMOMETER)); + if (can_start_dynarun()) buttons(TX(S_START_DYNARUN), TX(S_CLOSE), NULL); // only after a success (this page); a failure shows P_DRV_ERROR + else buttons(TX(S_CLOSE), NULL, NULL); } static void enter_drv_error(void) { wz.result = RC_FAIL; // the driver step failed - text(ID_TITLE, T(L"The driver was not installed", L"驅動程式沒有安裝")); + text(ID_TITLE, TX(S_THE_DRIVER_WAS_NOT_INSTALLED)); text(ID_BODY, wz.note); - buttons(T(L"Close", L"關閉"), NULL, T(L"Open the manual steps", L"開啟手動安裝說明")); + buttons(TX(S_CLOSE), NULL, TX(S_OPEN_THE_MANUAL_STEPS)); } static int jp_drv_prep(void) @@ -233,19 +191,16 @@ static void drv_error(int rc) { switch (rc) { case DRV_E_HASH: - lstrcpyW(wz.note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", - L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。")); + lstrcpyW(wz.note, TX(S_THE_DOWNLOADED_FILE_IS_NOT)); break; - case DRV_E_BCD: lstrcpyW(wz.note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; - case DRV_E_TASK: lstrcpyW(wz.note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; - case DRV_E_DIR: lstrcpyW(wz.note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", - L"無法把工作資料夾 %ProgramData%\\DynaRunFix 設為安全狀態(舊資料夾仍在使用中,或其他使用者可以修改它)。沒有安裝任何東西。請關閉其他程式、重新啟動電腦後再試一次。")); break; - case DRV_E_PNP: lstrcpyW(wz.note, T(L"Windows did not accept the driver. Driver signature checking may still have been on.", L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。")); break; - default: wsprintfW(wz.note, T(L"The driver could not be prepared (code %d).", L"無法準備驅動程式(代碼 %d)。"), rc); break; + case DRV_E_BCD: lstrcpyW(wz.note, TX(S_WINDOWS_COULD_NOT_BE_TOLD)); break; + case DRV_E_TASK: lstrcpyW(wz.note, TX(S_THE_TASK_THAT_CONTINUES_THE)); break; + case DRV_E_DIR: lstrcpyW(wz.note, TX(S_THE_WORKING_FOLDER_PROGRAMDATA_DYNARUNFIX)); break; + case DRV_E_PNP: lstrcpyW(wz.note, TX(S_WINDOWS_DID_NOT_ACCEPT_THE)); break; + default: wsprintfW(wz.note, TX(S_THE_DRIVER_COULD_NOT_BE), rc); break; } - lstrcatW(wz.note, T(L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。")); - if (wz.fix_done) lstrcatW(wz.note, T(L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", - L"\n\nDynaRun 的修正本身已經安裝好了。之後想再試一次驅動程式,可以用開始功能表裡的「安裝 Dyna Pro USB 驅動程式」。")); + lstrcatW(wz.note, TX(S_YOU_CAN_INSTALL_THE_DRIVER)); + if (wz.fix_done) lstrcatW(wz.note, TX(S_DYNARUN_S_FIX_ITSELF_IS)); set_page(P_DRV_ERROR); } @@ -260,22 +215,14 @@ static void drv_start(void) { if (!driver_supported()) { DWORD b = os_build(); // 9200+: Windows 8 / 8.1, 6000+: Vista / 7, below: XP - lstrcpyW(wz.note, T(L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", - L"這個 Windows 版本太舊,無法自動安裝(需要 Windows 10 或更新版本)。請手動安裝:\n\n")); + lstrcpyW(wz.note, TX(S_THIS_VERSION_OF_WINDOWS_IS)); if (b >= 9200) - lstrcatW(wz.note, T(L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " - L"Then install the driver in Device Manager (Update driver > Browse my computer) and choose \"Install this driver software anyway\" on the red warning.", - L"Windows 8 / 8.1:按住 Shift 再按「重新啟動」,依序選 疑難排解 > 進階選項 > 啟動設定 > 重新啟動,然後按 7(停用驅動程式強制簽章;32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" - L"接著在裝置管理員安裝驅動程式(更新驅動程式 > 瀏覽電腦上的驅動程式),並在紅色警告選「仍然安裝此驅動程式軟體」。")); + lstrcatW(wz.note, TX(S_WINDOWS_8_8_1_HOLD)); else if (b >= 6000) - lstrcatW(wz.note, T(L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " - L"(32-bit editions do not enforce driver signing and only show the warning, so they can skip this). Then in Device Manager use Update Driver > Browse my computer > Have Disk, and choose \"Install this driver software anyway\" on the red warning.", - L"Windows Vista / 7:重新開機,在出現 Windows 標誌之前連續按 F8,在「進階開機選項」選「停用驅動程式強制簽章」(32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" - L"接著在裝置管理員用 更新驅動程式 > 瀏覽電腦 > 從磁片安裝,並在紅色警告選「仍然安裝此驅動程式軟體」。")); + lstrcatW(wz.note, TX(S_WINDOWS_VISTA_7_RESTART_AND)); else - lstrcatW(wz.note, T(L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", - L"Windows XP 不強制驅動程式簽章。請在裝置管理員安裝驅動程式;Windows 說軟體未通過 Windows 標誌測試時,按「仍然繼續」。")); - lstrcatW(wz.note, T(L"\n\nThe manual steps have the details: click \"Open the manual steps\".", L"\n\n詳細說明請按「開啟手動安裝說明」。")); + lstrcatW(wz.note, TX(S_WINDOWS_XP_DOES_NOT_ENFORCE)); + lstrcatW(wz.note, TX(S_THE_MANUAL_STEPS_HAVE_THE)); set_page(P_DRV_ERROR); return; } @@ -297,8 +244,8 @@ static void hash_choice(BOOL install) if ((wz.drv_how & DRV_SRC_LOCAL) && !(wz.drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download else if (wz.drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); wz.drvzip[0] = 0; wz.drv_allow = FALSE; - lstrcpyW(wz.note, ((wz.drv_how & DRV_SRC_LOCAL) && !(wz.drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") - : T(L"Cancelled: the driver was not installed.", L"已取消,沒有安裝驅動程式。")); + lstrcpyW(wz.note, ((wz.drv_how & DRV_SRC_LOCAL) && !(wz.drv_how & DRV_SRC_PICKED)) ? TX(S_CANCELLED_THE_DRIVER_WAS_NOT) + : TX(S_CANCELLED_THE_DRIVER_WAS_NOT_2)); set_page(P_DRV_INTRO); } @@ -328,12 +275,11 @@ static BOOL pick_driver_file(WCHAR *p) hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); if ((SUCCEEDED(hrc) || hrc == RPC_E_CHANGED_MODE) && SUCCEEDED(CoCreateInstance(&clsid, NULL, CLSCTX_INPROC_SERVER, &iid, (void **)&fd))) { COMDLG_FILTERSPEC spec[2]; DWORD opt = 0; - spec[0].pszName = T(L"Dyna Pro USB driver (*.zip;*.inf)", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)"); spec[0].pszSpec = L"*.zip;*.inf"; - spec[1].pszName = T(L"All files", L"所有檔案"); spec[1].pszSpec = L"*.*"; + spec[0].pszName = TX(S_DYNA_PRO_USB_DRIVER_ZIP); spec[0].pszSpec = L"*.zip;*.inf"; + spec[1].pszName = TX(S_ALL_FILES); spec[1].pszSpec = L"*.*"; fd->lpVtbl->SetFileTypes(fd, 2, spec); - fd->lpVtbl->SetTitle(fd, T(L"Choose the Dyna Pro USB driver: Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", - L"選擇 Dyna Pro USB 驅動程式:Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf")); - fd->lpVtbl->SetOkButtonLabel(fd, T(L"Use this", L"使用這個")); + fd->lpVtbl->SetTitle(fd, TX(S_CHOOSE_THE_DYNA_PRO_USB)); + fd->lpVtbl->SetOkButtonLabel(fd, TX(S_USE_THIS)); if (SUCCEEDED(fd->lpVtbl->GetOptions(fd, &opt))) fd->lpVtbl->SetOptions(fd, opt | FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST | FOS_PATHMUSTEXIST); hr = fd->lpVtbl->Show(fd, g_hwnd); if (SUCCEEDED(hr)) { @@ -352,8 +298,8 @@ static BOOL pick_driver_file(WCHAR *p) OPENFILENAMEW of; zero(&of, sizeof(of)); of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; - of.lpstrFilter = T(L"Dyna Pro USB driver (*.zip;*.inf)\0*.zip;*.inf\0All files\0*.*\0", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)\0*.zip;*.inf\0所有檔案\0*.*\0"); - of.lpstrTitle = T(L"Choose Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"選擇 Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf"); + of.lpstrFilter = TX(S_DYNA_PRO_USB_DRIVER_ZIP_2); + of.lpstrTitle = TX(S_CHOOSE_DYNA_PRO_USB_ZIP); of.lpstrFile = p; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; ok = GetOpenFileNameW(&of); } @@ -397,7 +343,7 @@ static void jd_drv_prep(int job, int rc) if (rc == DRV_SUCCESS) { drv_reboot_now(); return; } if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); return; } // armed, but BitLocker is still on: the user decides if (rc == RC_CANCEL) { - lstrcpyW(wz.note, T(L"Nothing was changed: Windows needs your \"Yes\" first.", L"沒有做任何變更:需要在 Windows 詢問時按「是」。")); + lstrcpyW(wz.note, TX(S_NOTHING_WAS_CHANGED_WINDOWS_NEEDS)); set_page(wz.flow == FLOW_DRIVER_CONTINUE ? P_DRV_RETRY : P_DRV_PRESS7); return; } @@ -408,9 +354,9 @@ static void jd_drv_install(int job, int rc) { (void)job; if (rc == DRV_SUCCESS) { wz.drv_have = FALSE; set_page(P_DRV_DONE); return; } - if (rc == RC_CANCEL) { lstrcpyW(wz.note, T(L"Nothing was installed: Windows needs your \"Yes\" first.", L"沒有安裝:需要在 Windows 詢問時按「是」。")); set_page(P_DRV_WARN); return; } + if (rc == RC_CANCEL) { lstrcpyW(wz.note, TX(S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS)); set_page(P_DRV_WARN); return; } if (rc == DRV_E_ENFORCED) { // this start still checks signatures: nothing was installed, the restart stays armed - lstrcpyW(wz.note, T(L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。")); + lstrcpyW(wz.note, TX(S_DRIVER_SIGNATURE_CHECKING_IS_STILL)); set_page(P_DRV_RETRY); return; } drv_error(rc); diff --git a/src/wizard/layout.h b/src/wizard/layout.h index 66a9365..8100d24 100644 --- a/src/wizard/layout.h +++ b/src/wizard/layout.h @@ -104,7 +104,7 @@ static void draw_highlight(HDC dc, int sw, int sh, int tw, int th) int lw = S(2) < 2 ? 2 : S(2), rh = y1 - y0, rad = rh / 2 + 2, lx0 = x1 + lw + rh, lx1; HPEN pen = CreatePen(PS_SOLID, lw, RGB(255, 153, 0)), op; HBRUSH red = CreateSolidBrush(RGB(224, 52, 0)), ob = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH)); - LOGFONTW lf; HFONT f, of; const WCHAR *t = T(L"Press 7", L"按 7"); SIZE sz; POINT tri[3]; RECT tr; + LOGFONTW lf; HFONT f, of; const WCHAR *t = TX(S_PRESS_7); SIZE sz; POINT tri[3]; RECT tr; op = (HPEN)SelectObject(dc, pen); RoundRect(dc, x0, y0, x1, y1, rad, rad); // orange outline around the line (the pen is centred on the edge) // callout to the right of the line: red label "Press 7" with a small arrow pointing at it @@ -202,13 +202,13 @@ static void create_controls(void) add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX, ID_AUTO); add(L"BUTTON", WS_TABSTOP | BS_AUTOCHECKBOX | BS_MULTILINE, ID_DRV); add(L"STATIC", SS_BITMAP | SS_NOTIFY, ID_IMG); - SetWindowTextW(H(ID_DRV), T(L"Also install the Dyna Pro USB &driver (one restart needed)", L"同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)(&D)")); + SetWindowTextW(H(ID_DRV), TX(S_ALSO_INSTALL_THE_DYNA_PRO)); SendMessageW(H(ID_DRV), BM_SETCHECK, BST_CHECKED, 0); - SetWindowTextW(H(ID_PIN), T(L"Pin DynaRun to the &taskbar", L"釘選到工作列(&T)")); - SetWindowTextW(H(ID_AUTO), T(L"Start DynaRun when Windows &starts", L"開機時自動啟動 DynaRun(&S)")); + SetWindowTextW(H(ID_PIN), TX(S_PIN_DYNARUN_TO_THE_TASKBAR)); + SetWindowTextW(H(ID_AUTO), TX(S_START_DYNARUN_WHEN_WINDOWS_STARTS)); SendMessageW(H(ID_PIN), BM_SETCHECK, wz.opts.pin ? BST_CHECKED : BST_UNCHECKED, 0); SendMessageW(H(ID_AUTO), BM_SETCHECK, wz.opts.autostart ? BST_CHECKED : BST_UNCHECKED, 0); - SetWindowTextW(H(ID_CHECK), T(L"I &accept the license agreement", L"我接受授權合約(&A)")); + SetWindowTextW(H(ID_CHECK), TX(S_I_ACCEPT_THE_LICENSE_AGREEMENT)); add(PROGRESS_CLASSW, 0, ID_PROG); add(L"STATIC", SS_NOPREFIX, ID_STATUS); add(L"BUTTON", WS_TABSTOP, ID_EXTRA); diff --git a/src/wizard/main.h b/src/wizard/main.h index f444683..3f34a37 100644 --- a/src/wizard/main.h +++ b/src/wizard/main.h @@ -15,17 +15,14 @@ static void running_names(int run, WCHAR *out) { out[0] = 0; if (run & RUN_DYNARUN) lstrcatW(out, L" DynaRun V3\n"); - if (run & RUN_LAUNCHER) lstrcatW(out, T(L" DynaRunFix.exe (starting DynaRun)\n", L" DynaRunFix.exe(正在啟動 DynaRun)\n")); + if (run & RUN_LAUNCHER) lstrcatW(out, TX(S_DYNARUNFIX_EXE_STARTING_DYNARUN)); if (run & RUN_LEPROC) lstrcatW(out, L" LEProc.exe (Locale Emulator)\n"); } // Last paragraph of the first page. static const WCHAR *disclaimer(void) { - return T(L"\n\nDynaRunFix is an independent open-source tool, not affiliated with or endorsed by Dyna Pro Dynamometers. " - L"It is provided as is, without warranty (MIT license). DynaRun V3 is licensed to you separately by Dyna Pro.", - L"\n\nDynaRunFix 是獨立的開源工具,與 Dyna Pro Dynamometers 無關,也未經其認可;依 MIT 授權按現狀提供,不提供任何擔保。" - L"DynaRun V3 由 Dyna Pro 另行授權給你。"); + return TX(S_DYNARUNFIX_IS_AN_INDEPENDENT_OPEN); } // The line about a newer DynaRun on the first page, and its button. A hint only: nothing happens unless the button is clicked. @@ -33,15 +30,13 @@ static void hint_line(WCHAR *b) { WCHAR t[400]; if (wz.hint == HINT_NEWER) { - wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", - L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), wz.hint_new, wz.hint_inst); + wsprintfW(t, TX(S_A_NEWER_DYNARUN_V3_IS), wz.hint_new, wz.hint_inst); lstrcatW(b, t); } else if (wz.hint == HINT_ZIPDIFF) - lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", - L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); + lstrcatW(b, TX(S_DYNA_PRO_S_WEBSITE_MAY)); } -static const WCHAR *hint_btn(void) { return wz.hint ? T(L"Download new version", L"下載新版") : NULL; } +static const WCHAR *hint_btn(void) { return wz.hint ? TX(S_DOWNLOAD_NEW_VERSION) : NULL; } static BOOL can_start_dynarun(void) { return wz.installed && g_exe[0] && exists(g_launcher); } @@ -49,12 +44,10 @@ static BOOL can_start_dynarun(void) { return wz.installed && g_exe[0] && exists( // the install (wz.fix_done: the page "Done" is skipped), on the restart page. static void pin_hint(WCHAR *b) { - if (wz.instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, T(L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", L"\n\n登出再登入後,DynaRun 會出現在工作列上。")); + if (wz.instres.pin == PIN_NEXT_SIGNIN) lstrcatW(b, TX(S_DYNARUN_WILL_APPEAR_ON_THE)); if (wz.instres.pin == PIN_FAILED) lstrcatW(b, os_build() >= 22000 - ? T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", - L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。") - : T(L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", - L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「釘選到工作列」。")); + ? TX(S_TO_PIN_IT_TO_THE) + : TX(S_TO_PIN_IT_TO_THE_2)); } static void acp_warning(void) @@ -68,43 +61,25 @@ static void enter_hello(void) { WCHAR *b = pgbuf; if (wz.flow == FLOW_DRIVER_ONLY) { - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " - L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" - L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", - L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" - L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" - L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); + wsprintfW(b, TX(S_DYNARUN_V3_WAS_FOUND_THE), g_exe, WIDEN(DRF_DISPLAY)); hint_line(b); - text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); + text(ID_TITLE, TX(S_INSTALL_THE_DYNA_PRO_USB)); text(ID_BODY, b); - buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); + buttons4(TX(S_INSTALL_THE_DRIVER), TX(S_CANCEL), hint_btn(), TX(S_REINSTALL_THE_FIX)); if (wz.note[0]) status(wz.note, TRUE); return; } if (wz.installed) { - text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); - wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " - L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" - L"When Windows asks whether to allow changes, click \"Yes\".%s", - L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" - L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); + text(ID_TITLE, TX(S_FIX_DYNARUN_V3_FOR_THIS)); + wsprintfW(b, TX(S_DYNARUN_V3_WAS_FOUND_THIS), g_exe, disclaimer()); hint_line(b); text(ID_BODY, b); - buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); + buttons4(TX(S_INSTALL), TX(S_CANCEL), hint_btn(), NULL); } else { - text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); - wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" - L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" - L" 2. install DynaRun V3\n" - L" 3. add the fixes for Windows 10/11\n\n" - L"You need the setup password that Dyna Pro gave you.%s", - L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" - L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" - L" 2. 安裝 DynaRun V3\n" - L" 3. 加上 Windows 10/11 的相容性修正\n\n" - L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s"), disclaimer()); + text(ID_TITLE, TX(S_INSTALL_DYNARUN_V3)); + wsprintfW(b, TX(S_DYNARUN_V3_IS_NOT_INSTALLED), disclaimer()); text(ID_BODY, b); - buttons(T(L"Start", L"開始"), T(L"Cancel", L"取消"), T(L"Installed elsewhere...", L"已裝在其他位置…")); + buttons(TX(S_START), TX(S_CANCEL), TX(S_INSTALLED_ELSEWHERE)); } vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, wz.drv_avail); if (wz.note[0]) status(wz.note, TRUE); @@ -115,21 +90,15 @@ static void enter_running(void) WCHAR *b = pgbuf; WCHAR names[200]; running_names(wz.lastrun = running_programs(), names); - text(ID_TITLE, T(L"DynaRun is running", L"DynaRun 正在執行")); + text(ID_TITLE, TX(S_DYNARUN_IS_RUNNING)); if (wz.lastrun & RUN_NOCLOSE) { // another user's, or one that cannot be identified: closing from here would not help - wsprintfW(b, T(L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" - L"Ask that user to close DynaRun; this page then offers \"Install\".", - L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" - L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。"), names); + wsprintfW(b, TX(S_DYNARUN_IS_RUNNING_FOR_ANOTHER), names); text(ID_BODY, b); - buttons(NULL, T(L"Cancel", L"取消"), NULL); + buttons(NULL, TX(S_CANCEL), NULL); } else { - wsprintfW(b, T(L"DynaRun must be closed to install:\n%s\n" - L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", - L"安裝前必須關閉 DynaRun:\n%s\n" - L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。"), names); + wsprintfW(b, TX(S_DYNARUN_MUST_BE_CLOSED_TO), names); text(ID_BODY, b); - buttons(T(L"Close DynaRun and install", L"關閉 DynaRun 並安裝"), T(L"Cancel", L"取消"), NULL); + buttons(TX(S_CLOSE_DYNARUN_AND_INSTALL), TX(S_CANCEL), NULL); } if (wz.note[0]) status(wz.note, TRUE); SetTimer(g_hwnd, TIMER_RUNNING, 1000, NULL); @@ -137,9 +106,8 @@ static void enter_running(void) static void enter_install(void) { - text(ID_TITLE, T(L"Installing", L"安裝中")); - text(ID_BODY, T(L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", - L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。")); + text(ID_TITLE, TX(S_INSTALLING)); + text(ID_BODY, TX(S_INSTALLING_PLEASE_WAIT_THIS_TAKES)); progress(TRUE); buttons(NULL, NULL, NULL); } @@ -148,25 +116,22 @@ static void enter_done(void) { wz.result = RC_OK; // everything is installed WCHAR *b = pgbuf; - text(ID_TITLE, T(L"Done", L"完成")); - lstrcpyW(b, T(L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" - L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", - L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" - L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。")); + text(ID_TITLE, TX(S_DONE)); + lstrcpyW(b, TX(S_EVERYTHING_IS_INSTALLED_FROM_NOW)); pin_hint(b); - if (wz.opts.autostart) lstrcatW(b, T(L"\n\nDynaRun will start automatically when you sign in.", L"\n\n以後開機登入後會自動啟動 DynaRun。")); - lstrcatW(b, T(L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。")); + if (wz.opts.autostart) lstrcatW(b, TX(S_DYNARUN_WILL_START_AUTOMATICALLY_WHEN)); + lstrcatW(b, TX(S_IF_DYNARUN_S_SCREEN_EVER)); text(ID_BODY, b); - buttons(T(L"Start DynaRun", L"開始使用 DynaRun"), T(L"Close", L"關閉"), NULL); + buttons(TX(S_START_DYNARUN), TX(S_CLOSE), NULL); acp_warning(); } static void enter_error(void) { wz.result = RC_FAIL; // the install failed - text(ID_TITLE, T(L"Something went wrong", L"發生問題")); + text(ID_TITLE, TX(S_SOMETHING_WENT_WRONG)); text(ID_BODY, wz.note); - buttons(T(L"Close", L"關閉"), NULL, NULL); + buttons(TX(S_CLOSE), NULL, NULL); } static int jp_install_all(void) { return g_in.flow == FLOW_MAIN_WITH_DRIVER ? install_all_driver(&g_in.opts, &g_out.instres, g_in.msi, g_in.drvzip, g_in.drv_allow ? g_in.drv_hex : NULL) : install_all(&g_in.opts, &g_out.instres, g_in.msi); } // FLOW_MAIN_WITH_DRIVER: the driver's preparation in the same elevated stage @@ -288,12 +253,11 @@ static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INS if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } if (rc == RC_RUNNING) { wz.pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile if (rc == RC_CANCEL) { - lstrcpyW(wz.note, T(L"Nothing was installed: Windows needs your \"Yes\" to install.", L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。")); + lstrcpyW(wz.note, TX(S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS_2)); set_page(wz.flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : job == JOB_INSTALL_ALL ? P_LICENSE : P_HELLO); // FLOW_MAIN_WITH_DRIVER: the page with the button that started it return; } - wsprintfW(wz.note, T(L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", - L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); + wsprintfW(wz.note, TX(S_THE_INSTALLATION_DID_NOT_FINISH), rc); set_page(P_ERROR); } @@ -454,11 +418,11 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) if (wz.page == P_FETCH && !wz.paused) { wz.marquee = TRUE; progress(TRUE); - if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(wz.acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves + if (total == 2) { WCHAR s[160]; wsprintfW(s, TX(S_LOOKING_FOR_ON_THIS_COMPUTER), ACQ_NAME(wz.acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves else { // the file is being checked: not pausable wz.checking = TRUE; - text(ID_TITLE, T(L"Checking the driver", L"正在檢查驅動程式")); - status(T(L"Checking the driver...", L"正在檢查驅動程式…"), FALSE); + text(ID_TITLE, TX(S_CHECKING_THE_DRIVER)); + status(TX(S_CHECKING_THE_DRIVER_2), FALSE); EnableWindow(H(ID_PRIMARY), FALSE); } layout(); @@ -485,8 +449,8 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) wz.lastrun = run; if (run) set_page(P_RUNNING); else { // closed by the user meanwhile: plain "Install" - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); - status(T(L"DynaRun is closed.", L"DynaRun 已關閉。"), FALSE); + buttons(TX(S_INSTALL), TX(S_CANCEL), NULL); + status(TX(S_DYNARUN_IS_CLOSED), FALSE); layout(); } } @@ -551,7 +515,7 @@ int wizard(const install_opts *init) RegisterClassW(&wc); r.left = r.top = 0; r.right = S(620); r.bottom = S(580); // the first page: text, disclaimer, the two options and a one-line note AdjustWindowRect(&r, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE); - wsprintfW(title, T(L"DynaRunFix Setup %s", L"DynaRunFix 安裝程式 %s"), WIDEN(DRF_DISPLAY)); + wsprintfW(title, TX(S_DYNARUNFIX_SETUP), WIDEN(DRF_DISPLAY)); g_hwnd = CreateWindowExW(0, wc.lpszClassName, title, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN, (GetSystemMetrics(SM_CXSCREEN) - (r.right - r.left)) / 2, (GetSystemMetrics(SM_CYSCREEN) - (r.bottom - r.top)) / 2, r.right - r.left, r.bottom - r.top, NULL, NULL, wc.hInstance, NULL); diff --git a/src/wizard/strings.h b/src/wizard/strings.h new file mode 100644 index 0000000..24003df --- /dev/null +++ b/src/wizard/strings.h @@ -0,0 +1,452 @@ +// Every text the wizard shows in English and Chinese, in one table. The code says TX(S_...) where it used to say T(L"English", L"中文"). +// The same pair is in the table once, however many pages use it ("Cancel", "Close", "Open website" ...). A text that is put together at run time +// (wsprintfW, lstrcat) keeps its %s / %lu here. Only the pairs of the acquire flow's descriptions (ACQ_SETUP / ACQ_DRIVER in wizard.c) and the +// Chinese-only code page warning (acp_warning) are not in it. A new text: a name in the enum, a row in UI_STR, TX(name) where it is shown. + +enum { + S_PRESS_7, S_ALSO_INSTALL_THE_DYNA_PRO, S_PIN_DYNARUN_TO_THE_TASKBAR, S_START_DYNARUN_WHEN_WINDOWS_STARTS, S_I_ACCEPT_THE_LICENSE_AGREEMENT, + S_OF_MB_ARE_DOWNLOADED_CONTINUE, S_MB_ARE_DOWNLOADED_CONTINUE_DOWNLOAD, S_MB, S_GETTING_A_COPY_ON_THIS, S_GETTING, S_PAUSE, S_CANCEL, + S_OPEN_WEBSITE, S_THIS_IS_NOT_THE_DYNARUN, S_COULD_NOT_BE_DOWNLOADED, S_GET_IT_YOURSELF_1_CLICK, S_CONTINUE_DOWNLOAD, S_RETRY, S_SKIP, + S_CHOOSE_FILE, S_ENTER_THE_SETUP_PASSWORD, S_TYPE_THE_PASSWORD_THAT_DYNA, S_NEXT, S_OPENING_THE_SETUP_FILE, S_PLEASE_WAIT, S_CHECK_THE_INSTALLER, + S_COMPARE_THIS_WITH_WHAT_DYNA, S_DYNA_PRO_LICENSE_AGREEMENT, S_THE_LICENSE_AGREEMENT_IN_THIS, S_PLEASE_READ_DYNA_PRO_S, + S_I_UNDERSTAND_AND_ACCEPT_DYNA, S_INSTALL, S_IS_NOT_THE_DYNARUN_V3, S_BYTES_MB, S_SOURCE, S_DOWNLOADED_NOW_FROM_DYNAPRO_CO, + S_FOUND_ON_THIS_COMPUTER_NOT, S_CHOSEN_BY_YOU, S_FILE, S_SIZE, S_SETUP_MSI_INSIDE_THE_FILE, S_DYNARUN_V3_IS_LICENSED_TO, + S_DYNARUN_V3_SETUP_ZIP_MSI, S_THE_DOWNLOAD_WAS_STOPPED, S_THE_DYNA_PRO_WEBSITE_ANSWERED, S_THE_SECURITY_CERTIFICATE_OF_THE, + S_THE_DOWNLOAD_WAS_REFUSED_THE, S_THE_FILE_COULD_NOT_BE, S_THAT_FILE_IS_NOT_A, S_THIS_COMPUTER_IS_NOT_CONNECTED, S_NO_CONNECTION_TO_THE_DYNA, + S_PAUSED_OF_MB_DOWNLOADED, S_PAUSED_MB_DOWNLOADED, S_RESUME, S_THE_FILE_FOUND_ON_THIS, S_WRONG_PASSWORD_OR_THE_FILE, S_THE_SETUP_COULD_NOT_BE, + S_WRONG_PASSWORD_PLEASE_CHECK_IT, S_PAUSING, S_INSTALL_THE_DYNA_PRO_USB, S_WINDOWS_NEEDS_DYNA_PRO_S, S_DYNARUN_S_FIX_IS_INSTALLED, + S_AFTER_THE_RESTART_YOU_WILL, S_NOTE_THIS_DRIVER_FILE_IS, S_THE_UNPACKED_DRIVER_FOLDER_WAS, S_THE_DRIVER_WAS_FOUND_ON, S_THE_ZIP_DIFFERS_FROM_THE, + S_CLICK_INSTALL_AND_RESTART_DYNARUN, S_TIP_TAKE_A_PHOTO_OF, S_THIS_COMPUTER_USES_BITLOCKER_PROTECTION, S_IF_THIS_COMPUTER_USES_BITLOCKER, + S_INSTALL_AND_RESTART, S_RESTART_NOW, S_LATER, S_FOUND_ON_THIS_COMPUTER_UNPACKED, S_FOUND_ON_THIS_COMPUTER, S_DOWNLOADED_FROM_THE_DYNA_PRO, + S_THE_DRIVER_DIFFERS_FROM_THE, S_THE_DRIVER_FILES_IN_THIS, S_THE_DRIVER_FILES_IN_THIS_2, S_ZIP_SIZE, S_BYTES_ZIP_SHA_256, S_KNOWN_ZIP_SHA_256, + S_FILES_THAT_DIFFER_ACTUAL_EXPECTED, S_AND_MORE, S_INSTALL_ANYWAY, S_BITLOCKER_COULD_NOT_BE_SUSPENDED, S_THIS_COMPUTER_USES_BITLOCKER_AND, + S_RESTART_ANYWAY, S_FIND_THE_RECOVERY_KEY, S_PLEASE_WAIT_2, S_GETTING_READY_WHEN_WINDOWS_ASKS, S_PLEASE_RESTART_THE_COMPUTER, S_RESTARTING, + S_EVERYTHING_IS_READY_BUT_WINDOWS, S_THE_COMPUTER_RESTARTS_NOW_WHEN, S_CLOSE, S_7_WAS_NOT_PRESSED, S_THE_RESTART_MENU_WAS_SKIPPED, + S_LAST_STEP_INSTALL_THE_DRIVER, S_DRIVER_SIGNATURE_CHECKING_IS_OFF, S_INSTALLING_THE_DRIVER, S_INSTALLING_PLEASE_WAIT_IF_WINDOWS, + S_THE_DRIVER_IS_ALREADY_INSTALLED, S_THE_DRIVER_IS_INSTALLED, S_THE_DYNA_PRO_USB_DRIVER, S_CONNECT_THE_DYNA_PRO_DYNAMOMETER, S_START_DYNARUN, + S_THE_DRIVER_WAS_NOT_INSTALLED, S_OPEN_THE_MANUAL_STEPS, S_THE_DOWNLOADED_FILE_IS_NOT, S_WINDOWS_COULD_NOT_BE_TOLD, S_THE_TASK_THAT_CONTINUES_THE, + S_THE_WORKING_FOLDER_PROGRAMDATA_DYNARUNFIX, S_WINDOWS_DID_NOT_ACCEPT_THE, S_THE_DRIVER_COULD_NOT_BE, S_YOU_CAN_INSTALL_THE_DRIVER, + S_DYNARUN_S_FIX_ITSELF_IS, S_THIS_VERSION_OF_WINDOWS_IS, S_WINDOWS_8_8_1_HOLD, S_WINDOWS_VISTA_7_RESTART_AND, S_WINDOWS_XP_DOES_NOT_ENFORCE, + S_THE_MANUAL_STEPS_HAVE_THE, S_CANCELLED_THE_DRIVER_WAS_NOT, S_CANCELLED_THE_DRIVER_WAS_NOT_2, S_DYNA_PRO_USB_DRIVER_ZIP, S_ALL_FILES, + S_CHOOSE_THE_DYNA_PRO_USB, S_USE_THIS, S_DYNA_PRO_USB_DRIVER_ZIP_2, S_CHOOSE_DYNA_PRO_USB_ZIP, S_NOTHING_WAS_CHANGED_WINDOWS_NEEDS, + S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS, S_DRIVER_SIGNATURE_CHECKING_IS_STILL, S_DYNARUNFIX_EXE_STARTING_DYNARUN, + S_DYNARUNFIX_IS_AN_INDEPENDENT_OPEN, S_A_NEWER_DYNARUN_V3_IS, S_DYNA_PRO_S_WEBSITE_MAY, S_DOWNLOAD_NEW_VERSION, S_DYNARUN_WILL_APPEAR_ON_THE, + S_TO_PIN_IT_TO_THE, S_TO_PIN_IT_TO_THE_2, S_DYNARUN_V3_WAS_FOUND_THE, S_INSTALL_THE_DRIVER, S_REINSTALL_THE_FIX, S_FIX_DYNARUN_V3_FOR_THIS, + S_DYNARUN_V3_WAS_FOUND_THIS, S_INSTALL_DYNARUN_V3, S_DYNARUN_V3_IS_NOT_INSTALLED, S_START, S_INSTALLED_ELSEWHERE, S_DYNARUN_IS_RUNNING, + S_DYNARUN_IS_RUNNING_FOR_ANOTHER, S_DYNARUN_MUST_BE_CLOSED_TO, S_CLOSE_DYNARUN_AND_INSTALL, S_INSTALLING, S_INSTALLING_PLEASE_WAIT_THIS_TAKES, + S_DONE, S_EVERYTHING_IS_INSTALLED_FROM_NOW, S_DYNARUN_WILL_START_AUTOMATICALLY_WHEN, S_IF_DYNARUN_S_SCREEN_EVER, S_SOMETHING_WENT_WRONG, + S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS_2, S_THE_INSTALLATION_DID_NOT_FINISH, S_LOOKING_FOR_ON_THIS_COMPUTER, S_CHECKING_THE_DRIVER, + S_CHECKING_THE_DRIVER_2, S_DYNARUN_IS_CLOSED, S_DYNARUNFIX_SETUP, + S_COUNT }; + +typedef struct { const WCHAR *en, *zh; } ui_str; +static const ui_str UI_STR[S_COUNT] = { + // layout.h + [S_PRESS_7] = { L"Press 7", + L"按 7" }, + [S_ALSO_INSTALL_THE_DYNA_PRO] = { L"Also install the Dyna Pro USB &driver (one restart needed)", + L"同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)(&D)" }, + [S_PIN_DYNARUN_TO_THE_TASKBAR] = { L"Pin DynaRun to the &taskbar", + L"釘選到工作列(&T)" }, + [S_START_DYNARUN_WHEN_WINDOWS_STARTS] = { L"Start DynaRun when Windows &starts", + L"開機時自動啟動 DynaRun(&S)" }, + [S_I_ACCEPT_THE_LICENSE_AGREEMENT] = { L"I &accept the license agreement", + L"我接受授權合約(&A)" }, + // acquire.h + [S_OF_MB_ARE_DOWNLOADED_CONTINUE] = { L"\n\n%s of %s MB are downloaded. \"Continue download\" carries on from there.", + L"\n\n已下載 %s / %s MB,按「繼續下載」會從中斷的地方接續。" }, + [S_MB_ARE_DOWNLOADED_CONTINUE_DOWNLOAD] = { L"\n\n%s MB are downloaded. \"Continue download\" carries on from there.", + L"\n\n已下載 %s MB,按「繼續下載」會從中斷的地方接續。" }, + [S_MB] = { L" (%lu.%lu MB)", + L"(%lu.%lu MB)" }, + [S_GETTING_A_COPY_ON_THIS] = { L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" + L"If it does not work, click \"Open website\" to get it from the website yourself.", + L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" + L"如果下載不順利,可以按「開啟官網」,自己從官網下載。" }, + [S_GETTING] = { L"Getting %s", + L"正在取得%s" }, + [S_PAUSE] = { L"Pause", + L"暫停" }, + [S_CANCEL] = { L"Cancel", + L"取消" }, + [S_OPEN_WEBSITE] = { L"Open website", + L"開啟官網" }, + [S_THIS_IS_NOT_THE_DYNARUN] = { L"This is not the DynaRun V3 setup", + L"這不是 DynaRun V3 安裝檔" }, + [S_COULD_NOT_BE_DOWNLOADED] = { L"%s could not be downloaded", + L"無法自動下載%s" }, + [S_GET_IT_YOURSELF_1_CLICK] = { L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" + L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", + L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" + L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s" }, + [S_CONTINUE_DOWNLOAD] = { L"Continue download", + L"繼續下載" }, + [S_RETRY] = { L"Retry", + L"重試" }, + [S_SKIP] = { L"Skip", + L"略過" }, + [S_CHOOSE_FILE] = { L"Choose file...", + L"選擇檔案…" }, + [S_ENTER_THE_SETUP_PASSWORD] = { L"Enter the setup password", + L"輸入安裝密碼" }, + [S_TYPE_THE_PASSWORD_THAT_DYNA] = { L"Type the password that Dyna Pro gave you for the DynaRun V3 setup.\n(It is used only to open the setup file and is not saved.)\n\nSetup file: %s", + L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s" }, + [S_NEXT] = { L"Next", + L"下一步" }, + [S_OPENING_THE_SETUP_FILE] = { L"Opening the setup file", + L"正在打開安裝檔" }, + [S_PLEASE_WAIT] = { L"Please wait...", + L"請稍候…" }, + [S_CHECK_THE_INSTALLER] = { L"Check the installer", + L"確認安裝檔" }, + [S_COMPARE_THIS_WITH_WHAT_DYNA] = { L"Compare this with what Dyna Pro gives you. Dyna Pro does not sign its setup, so DynaRunFix cannot prove that it is exactly what Dyna Pro released: install only a file you got from Dyna Pro.", + L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。" }, + [S_DYNA_PRO_LICENSE_AGREEMENT] = { L"Dyna Pro license agreement", + L"Dyna Pro 授權合約" }, + [S_THE_LICENSE_AGREEMENT_IN_THIS] = { L"The license agreement in this DynaRun V3 setup could not be shown.", + L"無法顯示這個 DynaRun V3 安裝檔裡的授權合約。" }, + [S_PLEASE_READ_DYNA_PRO_S] = { L"Please read Dyna Pro's license agreement for DynaRun V3.", + L"請閱讀 Dyna Pro 的 DynaRun V3 授權合約。" }, + [S_I_UNDERSTAND_AND_ACCEPT_DYNA] = { L"I understand and &accept Dyna Pro's license terms", + L"我了解並同意 Dyna Pro 的授權條款(&A)" }, + [S_INSTALL] = { L"Install", + L"安裝" }, + [S_IS_NOT_THE_DYNARUN_V3] = { L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", + L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。" }, + [S_BYTES_MB] = { L"%lu bytes (%lu.%lu MB)", + L"%lu 位元組(%lu.%lu MB)" }, + [S_SOURCE] = { L"Source: ", + L"來源:" }, + [S_DOWNLOADED_NOW_FROM_DYNAPRO_CO] = { L"downloaded now from dynapro.co.uk", + L"剛從 dynapro.co.uk 下載" }, + [S_FOUND_ON_THIS_COMPUTER_NOT] = { L"found on this computer, not downloaded by DynaRunFix", + L"在這台電腦上找到,不是 DynaRunFix 下載的" }, + [S_CHOSEN_BY_YOU] = { L"chosen by you", + L"你選擇的檔案" }, + [S_FILE] = { L"\r\n\r\nFile:\r\n", + L"\r\n\r\n檔案路徑:\r\n" }, + [S_SIZE] = { L"\r\n\r\nSize: ", + L"\r\n\r\n大小:" }, + [S_SETUP_MSI_INSIDE_THE_FILE] = { L"\r\n\r\nSetup.msi inside the file, size: ", + L"\r\n\r\n檔案裡的 Setup.msi,大小:" }, + [S_DYNARUN_V3_IS_LICENSED_TO] = { L"DynaRun V3 is licensed to you by Dyna Pro Dynamometers Ltd under their end-user license agreement " + L"and their terms and conditions of sale. The agreement is part of Dyna Pro's setup, but this " + L"installer could not read it from this setup file.\r\n\r\n" + L"To read it, contact Dyna Pro (support@dynapro.co.uk) or see their website " + L"(\"Open website\" below).\r\n\r\n" + L"Install only if you accept Dyna Pro's license terms.", + L"DynaRun V3 是 Dyna Pro Dynamometers Ltd 依其使用者授權合約及銷售條款授權給你使用的軟體。" + L"授權合約包含在 Dyna Pro 的安裝檔中,但本安裝程式無法從這個安裝檔讀出合約內容。\r\n\r\n" + L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟官網」)。\r\n\r\n" + L"請在同意 Dyna Pro 的授權條款後再安裝。" }, + [S_DYNARUN_V3_SETUP_ZIP_MSI] = { L"DynaRun V3 setup (*.zip;*.msi)\0*.zip;*.msi\0All files\0*.*\0", + L"DynaRun V3 安裝檔 (*.zip;*.msi)\0*.zip;*.msi\0所有檔案\0*.*\0" }, + [S_THE_DOWNLOAD_WAS_STOPPED] = { L"The download was stopped.", + L"已停止下載。" }, + [S_THE_DYNA_PRO_WEBSITE_ANSWERED] = { L"The Dyna Pro website answered with error %lu.", + L"Dyna Pro 官網回應錯誤 %lu。" }, + [S_THE_SECURITY_CERTIFICATE_OF_THE] = { L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", + L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。" }, + [S_THE_DOWNLOAD_WAS_REFUSED_THE] = { L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", + L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。" }, + [S_THE_FILE_COULD_NOT_BE] = { L"The file could not be saved (error %lu).", + L"無法儲存檔案(錯誤 %lu)。" }, + [S_THAT_FILE_IS_NOT_A] = { L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", + L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。" }, + [S_THIS_COMPUTER_IS_NOT_CONNECTED] = { L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", + L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。" }, + [S_NO_CONNECTION_TO_THE_DYNA] = { L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", + L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。" }, + [S_PAUSED_OF_MB_DOWNLOADED] = { L"Paused: %s of %s MB downloaded.", + L"已暫停:已下載 %s / %s MB。" }, + [S_PAUSED_MB_DOWNLOADED] = { L"Paused: %s MB downloaded.", + L"已暫停:已下載 %s MB。" }, + [S_RESUME] = { L"Resume", + L"繼續" }, + [S_THE_FILE_FOUND_ON_THIS] = { L"The file found on this computer (%s) is damaged, so it will not be used.", + L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。" }, + [S_WRONG_PASSWORD_OR_THE_FILE] = { L"Wrong password, or the file is damaged. Please check the password.", + L"密碼不正確,或檔案已損毀。請再確認密碼。" }, + [S_THE_SETUP_COULD_NOT_BE] = { L"The setup could not be unpacked. Is the disk full?", + L"無法解開安裝檔,硬碟空間是否不足?" }, + [S_WRONG_PASSWORD_PLEASE_CHECK_IT] = { L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", + L"密碼不正確,請再確認(大小寫有差別)。\n如果上面的安裝檔不是 DynaRun 的,請按「選擇其他檔案…」。" }, + [S_PAUSING] = { L"Pausing...", + L"暫停中…" }, + // driver.h + [S_INSTALL_THE_DYNA_PRO_USB] = { L"Install the Dyna Pro USB driver", + L"安裝 Dyna Pro USB 驅動程式" }, + [S_WINDOWS_NEEDS_DYNA_PRO_S] = { L"Windows needs Dyna Pro's USB driver to talk to the dynamometer.\n\nThis program will:\n" + L" 1. download the driver from the Dyna Pro website (dynapro.co.uk, about 2 MB)\n" + L" 2. restart the computer once: a menu appears, press 7 on the keyboard\n" + L" 3. come back by itself after you sign in and finish the installation\n\n" + L"The driver's setting files (INF) in the vendor's package are not in the Microsoft-signed catalog files that ship with them, so Windows cannot validate them and only accepts the driver while " + L"driver signature checking is off for that one start. It is back on by itself at the next restart.\n\n" + L"If this PC is protected by BitLocker, the protection is suspended until the driver is installed. " + L"It resumes by itself, after at most 3 restarts.", + L"要讓電腦和馬力機連線,需要安裝 Dyna Pro 提供的 USB 驅動程式。\n\n接下來會:\n" + L" 1. 從 Dyna Pro 官網下載驅動程式(dynapro.co.uk,約 2 MB)\n" + L" 2. 請你重新開機一次,開機時會出現一個選單,請按鍵盤上的 7\n" + L" 3. 登入後本程式會自動回來,完成安裝\n\n" + L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" + L"下次重新開機會自動恢復。\n\n" + L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。" }, + [S_DYNARUN_S_FIX_IS_INSTALLED] = { L"\n\nDynaRun's fix is installed in the same step, just before the restart, so Windows asks for permission only once.", + L"\n\nDynaRun 的修正會在同一步、重新開機之前一起安裝,所以 Windows 只會詢問一次是否允許變更。" }, + [S_AFTER_THE_RESTART_YOU_WILL] = { L"After the restart you will see this: press 7", + L"重新開機後會看到這個畫面,請按 7" }, + [S_NOTE_THIS_DRIVER_FILE_IS] = { L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", + L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n" }, + [S_THE_UNPACKED_DRIVER_FOLDER_WAS] = { L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", + L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n" }, + [S_THE_DRIVER_WAS_FOUND_ON] = { L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", + L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n" }, + [S_THE_ZIP_DIFFERS_FROM_THE] = { L"The zip differs from the known version, but the driver files are identical: it can be installed.\n\n", + L"壓縮檔和已知版本不同,但驅動程式檔案相同,可以安裝。\n\n" }, + [S_CLICK_INSTALL_AND_RESTART_DYNARUN] = { L"Click \"Install and restart\": DynaRun's fix is installed and the driver is prepared in one step (Windows asks for permission once), then the computer restarts.\n\n", + L"按「安裝並重新開機」後,會一次完成 DynaRun 修正的安裝和驅動程式的準備(Windows 只會詢問一次是否允許變更),接著電腦重新開機。\n\n" }, + [S_TIP_TAKE_A_PHOTO_OF] = { L"Tip: take a photo of this page with your phone.\nSave your work in other programs before you restart.", + L"建議用手機拍下這一頁。\n按「重新開機」前請先存好其他程式的資料。" }, + [S_THIS_COMPUTER_USES_BITLOCKER_PROTECTION] = { L"\n\nThis computer uses BitLocker: protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n這台電腦開了 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。" }, + [S_IF_THIS_COMPUTER_USES_BITLOCKER] = { L"\n\nIf this computer uses BitLocker, protection is suspended for the restart and turned on again by itself after the driver is installed.", + L"\n\n如果這台電腦有開 BitLocker,重新開機前會暫時停用保護,驅動裝好後自動恢復。" }, + [S_INSTALL_AND_RESTART] = { L"Install and restart", + L"安裝並重新開機" }, + [S_RESTART_NOW] = { L"Restart now", + L"重新開機" }, + [S_LATER] = { L"Later", + L"稍後" }, + [S_FOUND_ON_THIS_COMPUTER_UNPACKED] = { L"found on this computer (unpacked folder): ", + L"在這台電腦上找到(已解開的資料夾):" }, + [S_FOUND_ON_THIS_COMPUTER] = { L"found on this computer: ", + L"在這台電腦上找到:" }, + [S_DOWNLOADED_FROM_THE_DYNA_PRO] = { L"downloaded from the Dyna Pro website (dynapro.co.uk)", + L"從 Dyna Pro 官網下載" }, + [S_THE_DRIVER_DIFFERS_FROM_THE] = { L"The driver differs from the known version", + L"驅動程式和已知版本不同" }, + [S_THE_DRIVER_FILES_IN_THIS] = { L"The driver files in this folder are not the version DynaRunFix knows. Dyna Pro may have updated them, or the files may be damaged or tampered with.\n\n" + L"Install them only if you are sure that they come from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個資料夾裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:" }, + [S_THE_DRIVER_FILES_IN_THIS_2] = { L"The driver files in this package are not the version DynaRunFix knows. Dyna Pro may have updated them, or the file may be damaged or tampered with.\n\n" + L"Install it only if you are sure that it comes from Dyna Pro. If in doubt, click \"Cancel\".\n\nSource: ", + L"這個驅動程式包裡的驅動程式檔和 DynaRunFix 已知的版本不同。可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改。\n\n" + L"確定檔案來自 Dyna Pro 才按「仍要安裝」;不確定請按「取消」。\n\n來源:" }, + [S_ZIP_SIZE] = { L"\nZip size: ", + L"\n壓縮檔大小:" }, + [S_BYTES_ZIP_SHA_256] = { L" bytes\nZip SHA-256: ", + L" 位元組\n壓縮檔 SHA-256:" }, + [S_KNOWN_ZIP_SHA_256] = { L"\nKnown zip SHA-256: ", + L"\n已知的壓縮檔 SHA-256:" }, + [S_FILES_THAT_DIFFER_ACTUAL_EXPECTED] = { L"Files that differ (actual / expected):\n", + L"不同的檔案(實際 / 已知):\n" }, + [S_AND_MORE] = { L" (and %d more)", + L" (另外還有 %d 個)" }, + [S_INSTALL_ANYWAY] = { L"Install anyway", + L"仍要安裝" }, + [S_BITLOCKER_COULD_NOT_BE_SUSPENDED] = { L"BitLocker could not be suspended", + L"BitLocker 沒有暫停成功" }, + [S_THIS_COMPUTER_USES_BITLOCKER_AND] = { L"This computer uses BitLocker, and its protection could not be suspended for the restart. After the restart Windows will probably ask for the " + L"BitLocker recovery key before it shows the start-up menu.\n\nYou can look the key up at https://aka.ms/myrecoverykey (Microsoft account), or ask your administrator. " + L"Without the key, choose \"Later\", suspend BitLocker yourself (Settings > Privacy & security > Device encryption, or Control Panel > BitLocker) and try again.", + L"這台電腦有開 BitLocker,但重新開機前無法暫停它的保護。重新開機後 Windows 很可能會先要求輸入 BitLocker 修復金鑰(「您必須輸入修復金鑰以存取啟動設定」),才會顯示選單。\n\n" + L"修復金鑰可到 https://aka.ms/myrecoverykey 用微軟帳戶查詢,或詢問系統管理員。沒有金鑰的話,請按「稍後」,自己先暫停 BitLocker(設定 → 隱私權與安全性 → 裝置加密,或控制台 → BitLocker)再試一次。" }, + [S_RESTART_ANYWAY] = { L"Restart anyway", + L"仍要重新開機" }, + [S_FIND_THE_RECOVERY_KEY] = { L"Find the recovery key", + L"查詢修復金鑰" }, + [S_PLEASE_WAIT_2] = { L"Please wait", + L"請稍候" }, + [S_GETTING_READY_WHEN_WINDOWS_ASKS] = { L"Getting ready...\n\nWhen Windows asks whether to allow changes, click \"Yes\".", + L"正在準備…\n\nWindows 詢問「是否允許變更」時,請按「是」。" }, + [S_PLEASE_RESTART_THE_COMPUTER] = { L"Please restart the computer", + L"請重新開機" }, + [S_RESTARTING] = { L"Restarting", + L"即將重新開機" }, + [S_EVERYTHING_IS_READY_BUT_WINDOWS] = { L"Everything is ready, but Windows would not restart by itself.\n\nPlease restart the computer yourself (Start, the power button, Restart). " + L"When the menu appears, press 7. After you sign in, this program continues by itself.", + L"都準備好了,但 Windows 沒有自動重新開機。\n\n請自己重新開機(開始 → 電源 → 重新啟動)。出現選單時請按 7;登入後本程式會自動繼續。" }, + [S_THE_COMPUTER_RESTARTS_NOW_WHEN] = { L"The computer restarts now.\n\nWhen the menu appears, press 7. After you sign in, this program continues by itself.\n\n" + L"If the computer does not restart (a program is waiting for you), save your work and restart it yourself.", + L"電腦即將重新開機。\n\n出現選單時請按 7;登入後本程式會自動繼續。\n\n如果電腦沒有重新開機(有程式在等你回應),請存好資料後自己重新開機。" }, + [S_CLOSE] = { L"Close", + L"關閉" }, + [S_7_WAS_NOT_PRESSED] = { L"7 was not pressed", + L"剛才沒有按到 7" }, + [S_THE_RESTART_MENU_WAS_SKIPPED] = { L"The restart menu was skipped, so Windows still checks driver signatures and the driver cannot be installed yet.\n\n" + L"Try again? The computer restarts once more. When the menu appears, press 7.", + L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" + L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。" }, + [S_LAST_STEP_INSTALL_THE_DRIVER] = { L"Last step: install the driver", + L"最後一步:安裝驅動程式" }, + [S_DRIVER_SIGNATURE_CHECKING_IS_OFF] = { L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", + L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。" }, + [S_INSTALLING_THE_DRIVER] = { L"Installing the driver", + L"正在安裝驅動程式" }, + [S_INSTALLING_PLEASE_WAIT_IF_WINDOWS] = { L"Installing, please wait.\n\nIf Windows shows a red warning, choose \"Install this driver software anyway\".\nIf Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候。\n\n如果 Windows 跳出紅色警告,請選「仍然安裝此驅動程式軟體」。\nWindows 詢問「是否允許變更」時,請按「是」。" }, + [S_THE_DRIVER_IS_ALREADY_INSTALLED] = { L"The driver is already installed", + L"驅動程式已安裝" }, + [S_THE_DRIVER_IS_INSTALLED] = { L"The driver is installed", + L"驅動程式安裝完成" }, + [S_THE_DYNA_PRO_USB_DRIVER] = { L"The Dyna Pro USB driver is already installed on this computer. Nothing more to do.", + L"這台電腦已經安裝過 Dyna Pro USB 驅動程式,不需要再裝。" }, + [S_CONNECT_THE_DYNA_PRO_DYNAMOMETER] = { L"Connect the Dyna Pro dynamometer by USB and switch it on: Windows recognizes it by itself.\n\n" + L"Driver signature checking comes back by itself the next time you restart the computer normally. Nothing else to set.", + L"請用 USB 接上 Dyna Pro 馬力機並開機,Windows 會自動認得它。\n\n" + L"驅動程式簽章檢查會在下次正常重新開機後自動恢復,不需要再做任何設定。" }, + [S_START_DYNARUN] = { L"Start DynaRun", + L"開始使用 DynaRun" }, + [S_THE_DRIVER_WAS_NOT_INSTALLED] = { L"The driver was not installed", + L"驅動程式沒有安裝" }, + [S_OPEN_THE_MANUAL_STEPS] = { L"Open the manual steps", + L"開啟手動安裝說明" }, + [S_THE_DOWNLOADED_FILE_IS_NOT] = { L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", + L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。" }, + [S_WINDOWS_COULD_NOT_BE_TOLD] = { L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", + L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。" }, + [S_THE_TASK_THAT_CONTINUES_THE] = { L"The task that continues the installation after the restart could not be created.", + L"無法建立重新開機後繼續安裝的排程工作。" }, + [S_THE_WORKING_FOLDER_PROGRAMDATA_DYNARUNFIX] = { L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", + L"無法把工作資料夾 %ProgramData%\\DynaRunFix 設為安全狀態(舊資料夾仍在使用中,或其他使用者可以修改它)。沒有安裝任何東西。請關閉其他程式、重新啟動電腦後再試一次。" }, + [S_WINDOWS_DID_NOT_ACCEPT_THE] = { L"Windows did not accept the driver. Driver signature checking may still have been on.", + L"Windows 沒有接受這個驅動程式,可能是簽章檢查還開著。" }, + [S_THE_DRIVER_COULD_NOT_BE] = { L"The driver could not be prepared (code %d).", + L"無法準備驅動程式(代碼 %d)。" }, + [S_YOU_CAN_INSTALL_THE_DRIVER] = { L"\n\nYou can install the driver by hand instead: click \"Open the manual steps\".", + L"\n\n也可以改用手動方式安裝,請按「開啟手動安裝說明」。" }, + [S_DYNARUN_S_FIX_ITSELF_IS] = { L"\n\nDynaRun's fix itself is installed. To try the driver again later, use \"Install Dyna Pro USB driver\" in the Start menu.", + L"\n\nDynaRun 的修正本身已經安裝好了。之後想再試一次驅動程式,可以用開始功能表裡的「安裝 Dyna Pro USB 驅動程式」。" }, + [S_THIS_VERSION_OF_WINDOWS_IS] = { L"This version of Windows is too old for the automatic installation (it needs Windows 10 or later). Install the driver by hand:\n\n", + L"這個 Windows 版本太舊,無法自動安裝(需要 Windows 10 或更新版本)。請手動安裝:\n\n" }, + [S_WINDOWS_8_8_1_HOLD] = { L"Windows 8 / 8.1: hold Shift and click Restart, then Troubleshoot > Advanced options > Startup Settings > Restart, and press 7 (Disable driver signature enforcement; 32-bit editions do not enforce driver signing and only show the warning, so they can skip this). " + L"Then install the driver in Device Manager (Update driver > Browse my computer) and choose \"Install this driver software anyway\" on the red warning.", + L"Windows 8 / 8.1:按住 Shift 再按「重新啟動」,依序選 疑難排解 > 進階選項 > 啟動設定 > 重新啟動,然後按 7(停用驅動程式強制簽章;32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" + L"接著在裝置管理員安裝驅動程式(更新驅動程式 > 瀏覽電腦上的驅動程式),並在紅色警告選「仍然安裝此驅動程式軟體」。" }, + [S_WINDOWS_VISTA_7_RESTART_AND] = { L"Windows Vista / 7: restart and press F8 repeatedly before the Windows logo appears, choose \"Disable Driver Signature Enforcement\" in Advanced Boot Options " + L"(32-bit editions do not enforce driver signing and only show the warning, so they can skip this). Then in Device Manager use Update Driver > Browse my computer > Have Disk, and choose \"Install this driver software anyway\" on the red warning.", + L"Windows Vista / 7:重新開機,在出現 Windows 標誌之前連續按 F8,在「進階開機選項」選「停用驅動程式強制簽章」(32 位元版不強制驅動程式簽章、只會顯示警告,可略過這一步)。" + L"接著在裝置管理員用 更新驅動程式 > 瀏覽電腦 > 從磁片安裝,並在紅色警告選「仍然安裝此驅動程式軟體」。" }, + [S_WINDOWS_XP_DOES_NOT_ENFORCE] = { L"Windows XP does not enforce driver signatures. Install the driver in Device Manager; when Windows says the software has not passed Windows Logo testing, click \"Continue Anyway\".", + L"Windows XP 不強制驅動程式簽章。請在裝置管理員安裝驅動程式;Windows 說軟體未通過 Windows 標誌測試時,按「仍然繼續」。" }, + [S_THE_MANUAL_STEPS_HAVE_THE] = { L"\n\nThe manual steps have the details: click \"Open the manual steps\".", + L"\n\n詳細說明請按「開啟手動安裝說明」。" }, + [S_CANCELLED_THE_DRIVER_WAS_NOT] = { L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", + L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。" }, + [S_CANCELLED_THE_DRIVER_WAS_NOT_2] = { L"Cancelled: the driver was not installed.", + L"已取消,沒有安裝驅動程式。" }, + [S_DYNA_PRO_USB_DRIVER_ZIP] = { L"Dyna Pro USB driver (*.zip;*.inf)", + L"Dyna Pro USB 驅動程式 (*.zip;*.inf)" }, + [S_ALL_FILES] = { L"All files", + L"所有檔案" }, + [S_CHOOSE_THE_DYNA_PRO_USB] = { L"Choose the Dyna Pro USB driver: Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", + L"選擇 Dyna Pro USB 驅動程式:Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf" }, + [S_USE_THIS] = { L"Use this", + L"使用這個" }, + [S_DYNA_PRO_USB_DRIVER_ZIP_2] = { L"Dyna Pro USB driver (*.zip;*.inf)\0*.zip;*.inf\0All files\0*.*\0", + L"Dyna Pro USB 驅動程式 (*.zip;*.inf)\0*.zip;*.inf\0所有檔案\0*.*\0" }, + [S_CHOOSE_DYNA_PRO_USB_ZIP] = { L"Choose Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", + L"選擇 Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf" }, + [S_NOTHING_WAS_CHANGED_WINDOWS_NEEDS] = { L"Nothing was changed: Windows needs your \"Yes\" first.", + L"沒有做任何變更:需要在 Windows 詢問時按「是」。" }, + [S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS] = { L"Nothing was installed: Windows needs your \"Yes\" first.", + L"沒有安裝:需要在 Windows 詢問時按「是」。" }, + [S_DRIVER_SIGNATURE_CHECKING_IS_STILL] = { L"Driver signature checking is still on in this Windows session, so nothing was installed. The restart with key 7 is needed first.", + L"這次 Windows 啟動時簽章檢查還開著,所以沒有安裝任何東西。需要先重新開機並按 7。" }, + // main.h + [S_DYNARUNFIX_EXE_STARTING_DYNARUN] = { L" DynaRunFix.exe (starting DynaRun)\n", + L" DynaRunFix.exe(正在啟動 DynaRun)\n" }, + [S_DYNARUNFIX_IS_AN_INDEPENDENT_OPEN] = { L"\n\nDynaRunFix is an independent open-source tool, not affiliated with or endorsed by Dyna Pro Dynamometers. " + L"It is provided as is, without warranty (MIT license). DynaRun V3 is licensed to you separately by Dyna Pro.", + L"\n\nDynaRunFix 是獨立的開源工具,與 Dyna Pro Dynamometers 無關,也未經其認可;依 MIT 授權按現狀提供,不提供任何擔保。" + L"DynaRun V3 由 Dyna Pro 另行授權給你。" }, + [S_A_NEWER_DYNARUN_V3_IS] = { L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。" }, + [S_DYNA_PRO_S_WEBSITE_MAY] = { L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。" }, + [S_DOWNLOAD_NEW_VERSION] = { L"Download new version", + L"下載新版" }, + [S_DYNARUN_WILL_APPEAR_ON_THE] = { L"\n\nDynaRun will appear on the taskbar after you sign out and back in.", + L"\n\n登出再登入後,DynaRun 會出現在工作列上。" }, + [S_TO_PIN_IT_TO_THE] = { L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Show more options → Pin to taskbar.", + L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「顯示其他選項」→「釘選到工作列」。" }, + [S_TO_PIN_IT_TO_THE_2] = { L"\n\nTo pin it to the taskbar: right-click the \"DynaRun V3\" icon on the desktop → Pin to taskbar.", + L"\n\n要釘選到工作列:在桌面「DynaRun V3」圖示上按右鍵 →「釘選到工作列」。" }, + [S_DYNARUN_V3_WAS_FOUND_THE] = { L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " + L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" + L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", + L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" + L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" + L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。" }, + [S_INSTALL_THE_DRIVER] = { L"Install the driver", + L"安裝驅動程式" }, + [S_REINSTALL_THE_FIX] = { L"Reinstall the fix...", + L"重新安裝修正…" }, + [S_FIX_DYNARUN_V3_FOR_THIS] = { L"Fix DynaRun V3 for this Windows", + L"讓 DynaRun V3 在這台電腦正常運作" }, + [S_DYNARUN_V3_WAS_FOUND_THIS] = { L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " + L"in OneDrive that do not open, garbled text, \"Run as administrator\").\n\n" + L"When Windows asks whether to allow changes, click \"Yes\".%s", + L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" + L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s" }, + [S_INSTALL_DYNARUN_V3] = { L"Install DynaRun V3", + L"安裝 DynaRun V3" }, + [S_DYNARUN_V3_IS_NOT_INSTALLED] = { L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" + L" 1. download the DynaRun V3 setup from the Dyna Pro website\n" + L" 2. install DynaRun V3\n" + L" 3. add the fixes for Windows 10/11\n\n" + L"You need the setup password that Dyna Pro gave you.%s", + L"這台電腦還沒有安裝 DynaRun V3。\n\n本程式會自動:\n" + L" 1. 從 Dyna Pro 官網下載 DynaRun V3 安裝檔\n" + L" 2. 安裝 DynaRun V3\n" + L" 3. 加上 Windows 10/11 的相容性修正\n\n" + L"過程中需要輸入 Dyna Pro 給你的安裝密碼。%s" }, + [S_START] = { L"Start", + L"開始" }, + [S_INSTALLED_ELSEWHERE] = { L"Installed elsewhere...", + L"已裝在其他位置…" }, + [S_DYNARUN_IS_RUNNING] = { L"DynaRun is running", + L"DynaRun 正在執行" }, + [S_DYNARUN_IS_RUNNING_FOR_ANOTHER] = { L"DynaRun is running for another user, or as a program that cannot be identified, so it cannot be closed from here:\n%s\n" + L"Ask that user to close DynaRun; this page then offers \"Install\".", + L"另一位使用者(或無法辨識的程式)正在執行 DynaRun,無法從這裡關閉:\n%s\n" + L"請該使用者先關閉 DynaRun,關閉後這個頁面會出現「安裝」按鈕。" }, + [S_DYNARUN_MUST_BE_CLOSED_TO] = { L"DynaRun must be closed to install:\n%s\n" + L"\"Close DynaRun and install\" closes it now.\nA test run in progress stops, unsaved data is lost.", + L"安裝前必須關閉 DynaRun:\n%s\n" + L"按「關閉 DynaRun 並安裝」會立即關閉它。\n正在進行的測試會中斷,沒存檔的資料會遺失。" }, + [S_CLOSE_DYNARUN_AND_INSTALL] = { L"Close DynaRun and install", + L"關閉 DynaRun 並安裝" }, + [S_INSTALLING] = { L"Installing", + L"安裝中" }, + [S_INSTALLING_PLEASE_WAIT_THIS_TAKES] = { L"Installing, please wait. This takes a minute or two.\n\nWhen Windows asks whether to allow changes, click \"Yes\".", + L"正在安裝,請稍候,大約需要一兩分鐘。\n\nWindows 詢問「是否允許變更」時,請按「是」。" }, + [S_DONE] = { L"Done", + L"完成" }, + [S_EVERYTHING_IS_INSTALLED_FROM_NOW] = { L"Everything is installed.\n\nFrom now on, start DynaRun with the \"DynaRun V3\" icon on the desktop as usual.\n\n" + L"The first start asks for your dynamometer model. If Windows asks whether to allow changes, click \"Yes\".", + L"全部安裝完成。\n\n以後照常點桌面上的「DynaRun V3」圖示啟動即可。\n\n" + L"第一次啟動時會要你選擇馬力機型號;如果 Windows 詢問「是否允許變更」,請按「是」。" }, + [S_DYNARUN_WILL_START_AUTOMATICALLY_WHEN] = { L"\n\nDynaRun will start automatically when you sign in.", + L"\n\n以後開機登入後會自動啟動 DynaRun。" }, + [S_IF_DYNARUN_S_SCREEN_EVER] = { L"\n\nIf DynaRun's screen ever looks wrong, close DynaRun and start it again.", + L"\n\n如果 DynaRun 的畫面顯示不正常,關掉 DynaRun 再開一次即可。" }, + [S_SOMETHING_WENT_WRONG] = { L"Something went wrong", + L"發生問題" }, + [S_NOTHING_WAS_INSTALLED_WINDOWS_NEEDS_2] = { L"Nothing was installed: Windows needs your \"Yes\" to install.", + L"沒有安裝:需要在 Windows 詢問時按「是」才能安裝。" }, + [S_THE_INSTALLATION_DID_NOT_FINISH] = { L"The installation did not finish (code %d).\n\nRestart the computer and run this program again.", + L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。" }, + [S_LOOKING_FOR_ON_THIS_COMPUTER] = { L"Looking for %s on this computer...", + L"正在尋找電腦上現有的%s…" }, + [S_CHECKING_THE_DRIVER] = { L"Checking the driver", + L"正在檢查驅動程式" }, + [S_CHECKING_THE_DRIVER_2] = { L"Checking the driver...", + L"正在檢查驅動程式…" }, + [S_DYNARUN_IS_CLOSED] = { L"DynaRun is closed.", + L"DynaRun 已關閉。" }, + [S_DYNARUNFIX_SETUP] = { L"DynaRunFix Setup %s", + L"DynaRunFix 安裝程式 %s" }, +}; +#define TX(id) T(UI_STR[id].en, UI_STR[id].zh) From 125d521d3000eb354f51d68835594aa77e2f90ee Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:12:31 +0800 Subject: [PATCH 32/41] Tests: drop duplicate driver_prep_stage stub after merge Co-Authored-By: Claude Opus 5.5 --- tests/test_hardening.c | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_hardening.c b/tests/test_hardening.c index 14f31b0..576ad62 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -39,7 +39,6 @@ int cmd_driver_cancel(cmdline *c) { (void)c; return 0; } int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { (void)sid; (void)zip; (void)accept; return 0; } // called by install_fix_driver / install_all_driver (not run here) void driver_machine_stage(const WCHAR *p) { (void)p; } void driver_machine_uninstall(void) { } -int driver_prep_stage(const WCHAR *sid, const WCHAR *zip, const WCHAR *accept) { (void)sid; (void)zip; (void)accept; return 0; } static int g_fail, g_pass; #define CHECK(c, what) do { if (c) { g_pass++; } else { g_fail++; wprintf(L"FAIL line %d: %s\n", __LINE__, what); } } while (0) From 3923d63ad3e80e8d57907a8693bcc4231c252333 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 01:14:13 +0800 Subject: [PATCH 33/41] Docs: align with the refactor; CHANGELOG entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ tests/README.md | 9 +++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdcb04c..9fda0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ [繁體中文在下方](#更新紀錄) +## Unreleased + +Internal refactor (no behaviour change): `setup.c` is split into topic files in `src\setup\` and `wizard.c` into flow files in `src\wizard\` (each still one compilation unit), `setup.h` is now an umbrella over the `drf_*.h` module headers, the result codes have names in `src\errors.h` (values unchanged), the install options travel in `install_opts` instead of globals, the setup and driver stages share one command-line verb table, the wizard uses a page table and a job table, the launcher and the update check share `src\applog.h` (log lock, clock, value names, verbs), and `dynafix.c` is split into `dynafix_*.inc.c`. Test-only code (COM promotion, whole-zip extraction) moved out of the shipped sources into `tests\`. + +Fixed: + +- **"Close DynaRun and install" now applies to one install only.** The consent was kept for later attempts: after a cancelled UAC prompt or a failed install, a DynaRun started in the meantime could be ended without asking. The question is now asked again. +- **A failed driver preparation no longer leaves files behind.** The staged driver files and `accepted.txt` in `%ProgramData%\DynaRunFix\driver` are removed when the check, the copy, the boot option or the scheduled task step fails. +- **A very long `%APPDATA%` path no longer overflows a buffer** in the shortcut and taskbar pin code; the folder that does not fit is skipped. +- **The download progress bar** is updated by the download thread only (exactly one update per value when the size is unknown). +- **dynafix.dll:** a race between the loader callback and the periodic check that could patch the same hook twice on two threads is fixed. + ## v1.7.1 — 2026-10-17 Fixed: @@ -496,6 +508,18 @@ Changed and fixed (after a review of v1.4.0): ## 更新紀錄 +### 未發行 + +內部重構(行為不變):`setup.c` 依主題拆成 `src\setup\` 內的檔案、`wizard.c` 拆成 `src\wizard\` 內的流程檔(仍是單一編譯單元);`setup.h` 改為 `drf_*.h` 模組標頭的總入口;結果碼在 `src\errors.h` 命名(數值不變);安裝選項以 `install_opts` 傳遞而非全域變數;安裝程式與驅動程式階段共用一張命令列動詞表;精靈改用頁面表與工作表;啟動器與更新檢查共用 `src\applog.h`(log 鎖、時鐘、值名稱、動詞);`dynafix.c` 拆成 `dynafix_*.inc.c`。僅供測試的程式碼(COM 登錄提升、整包 zip 解壓)移出正式原始碼,放到 `tests\`。 + +修正: + +- **「關閉 DynaRun 並安裝」只對一次安裝有效。** 先前同意會留給之後的重試:UAC 被取消或安裝失敗後,期間才啟動的 DynaRun 可能未經詢問就被結束。現在會重新詢問。 +- **驅動程式準備失敗時不再留下檔案。** 檢查、複製、開機選項或排程工作任一步失敗時,會移除 `%ProgramData%\DynaRunFix\driver` 內已放置的驅動程式檔與 `accepted.txt`。 +- **%APPDATA% 路徑非常長時不再造成緩衝區溢位**(捷徑與工作列釘選程式碼);放不下的資料夾會被略過。 +- **下載進度條**只由下載執行緒更新(大小未知時每個數值只更新一次)。 +- **dynafix.dll:** 修正載入回呼與定期檢查之間的競爭,兩個執行緒可能同時修改同一個 hook。 + ### v1.7.1 — 2026-10-17 修正: diff --git a/tests/README.md b/tests/README.md index 94d7db2..619949f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -29,6 +29,7 @@ To run the tests against another revision of the sources: `tests\build-tests.cmd `tests\build-hardening-tests.cmd` (after `build.cmd`) builds and runs two programs that compile the shipped sources into themselves: `test_hardening.c` (`src\setup.c`, `src\package.c`) and `test_update.c` (`src\update.c`, with its own `version.h` in `tests\fakever`). +`src\setup.c` is one compilation unit: it `#include`s the topic files in `src\setup\*.h`, so a test that includes `setup.c` gets all of them. What a test does not compile in (the wizard, the update check, `driver.c` in `test_hardening.c`) is stubbed in the test itself. They create only `%TEMP%\drf-sec-`, `%TEMP%\drf-upd-` and `HKCU\Software\DynaRunFixTest-` and delete them again; HKLM and the real DynaRun and DynaRunFix installs are not touched (the only look at the machine is read-only: the ACL of an installed DynaRun folder is printed). The held-open case in `test_driver.c` keeps a file in an old folder open without sharing, so `make_base` cannot remove it; as in normal use, `delete_tree` then asks Windows to delete that locked file at the next restart (a harmless pending-delete entry for a file in the test folder). The test closes its handle and removes the folder itself. @@ -36,10 +37,14 @@ The held-open case in `test_driver.c` keeps a file in an old folder open without with junctions, the administrators-only staging folder, the SID check, the re-check of the noted release, the log lock, that a file held open for hashing cannot be changed, renamed or deleted but still starts, and the COM registration path rules and copy logic between two temporary keys (junctions, relative/UNC paths, dot-dot, writable folders, `.ocx` only in system folders, AppID/TreatAs/ProgID verbs): the installer no longer -copies COM registrations, that code is only kept under test. Exit code 0 = all passed. +copies COM registrations, that code is only kept under test (`tests\promote_com.h`, included by `test_hardening.c` after `src\setup.c`). Exit code 0 = all passed. # Driver tests -`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` and its fragments `src\wizard\*.h` (one text, in the order they are included) relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. +`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (`zip_extract_all` in `tests\zip_extract_all.h`, test-only: nothing shipped unpacks a whole zip; zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` and its fragments `src\wizard\*.h` (one text, in the order they are included) relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. + +Result codes (`RC_*`, `PK_*`, `DRV_E_*`) are named in `src\errors.h` (the tests lock their values); the module headers are `src\drf_*.h` (`setup.h` includes them all); `src\applog.h` is shared by the launcher and `update.c`. + +`tests\VM_CHECKLIST.md` lists the manual checks these tests cannot reach (UAC, the wizard, the restart for the driver, the network). Run it in a throw-away VM, never on the development host. The CI workflow (`.github/workflows/build.yml`) runs `build-tests.cmd`, `build-shexec-tests.cmd`, `build-hardening-tests.cmd` and `build-driver-tests.cmd` in the `build` job (read-only token); the release is made by a separate job. The scripts use a release Visual Studio; set `DRF_ALLOW_PRERELEASE=1` locally to allow a pre-release one. From fd9acc67c0aa005f43740561378771a1455fa85a Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:20:07 +0800 Subject: [PATCH 34/41] Docs: refactor record (completed) Co-Authored-By: Claude Opus 5.5 --- docs/REFACTOR_PLAN.md | 64 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/REFACTOR_PLAN.md diff --git a/docs/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md new file mode 100644 index 0000000..0172ca2 --- /dev/null +++ b/docs/REFACTOR_PLAN.md @@ -0,0 +1,64 @@ +# 重構紀錄(已完成,2026-10-18) + +依據:2026-10-17 逐行讀完 `src/` 全部模組、`tests/`、`build.cmd` 後擬定的計畫;本文件記錄實際結果。分支 `refactor/solid`,僅在本機,Tim 審完 git tree 才上遠端。 + +## 原則(執行時維持) + +- **行為不變**:每一步都是純重構;疑似 bug 另案驗證、另開 commit。 +- **小步、可驗證**:每步單獨編譯、在 VM 跑測試與回歸清單、單獨 commit。主機只編輯、編譯、commit。 +- **跨行程 ABI 不動**:elevated stage 的命令列動詞與參數位置、行程結束碼的數值、log mutex 名稱與格式、`CwpProc` 匯出、`setup.rc` 資源 ID、`DRF_VERSION` 巨集全部維持原樣,只加命名常數。 +- **測試仍可 include**:測試靠 `#include "../src/setup.c"`、`driver.c` 取用 static,所以拆檔後各 `.c` 仍是單一編譯單元。 + +## 決定 + +Tim 的決定: + +1. 範圍:P0–P6 全部做,並行進行。 +2. 結果碼:只加名稱,數值不變(跨行程 ABI 不變)。 +3. 疑似 bug:先驗證,確認有問題的另開 commit 修,不混在重構 commit 裡。 +4. 死碼:移到 tests,不留在正式程式碼。 + +## 根本問題(「改 A 動 B」的來源) + +1. 流程靠全域旗標傳遞(`g_close`、`g_pin`、`g_keep`、`g_autostart(_later)`、`g_quiet`),`run_elevated` 暗中依它們附加參數。 +2. 隱藏狀態:`g_user_sid`、`g_exe_trusted`、`pMsi*`、`g_tool_ok_code`、`g_other_dir` 等側通道。 +3. 結果碼互相重疊(PK_*、RC_*、DRV_E_*、裸數字)。 +4. 分派散落:精靈 22 頁、7 個 job,新增一頁要改約 9 處;命令列在 setup.c 與 driver.c 各分派一次。 +5. 模式旗標 `g_pre`、`g_driver_only`、`g_fix_done`、`g_drvmode` 交叉決定頁面。 +6. 重複邏輯要手動同步(捷徑目錄清單、目錄走訪、啟動行程並等待、log mutex SDDL、zip EOCD 掃描、`update_run`/`update_offer`)。 +7. `setup.c` 混了約 8 個領域,`setup.h` 讓所有模組看到全部宣告。 + +## 實際結果(模組配置) + +- **P0 安全網**:`tests/VM_CHECKLIST.md`(VM 回歸清單)、特性測試鎖住命令列解析、`run_elevated` 參數字串與結果碼判斷。 +- **P1 基礎層**:`src/errors.h`(結果碼命名,數值不變);`src/setup/util.h`(路徑組合、`run_process`、`dir_walk`、app key 開啟);`src/applog.h`(launcher 與 update 共用 log mutex、SDDL、時鐘與 `Update*` 登錄值名稱)。 +- **P2 setup**:`install_opts` 取代全域旗標;`cmdline.h` 動詞表(併入 driver 的動詞);`shortcuts.h` 單一捷徑目錄清單;`trust_ctx` 與 `msi_api_init`。`src/setup.c` 保留入口並 include `src/setup/` 下分檔:`defs.h`、`strings.h`、`util.h`、`trust.h`、`exetime.h`、`procs.h`、`shortcuts.h`、`taskbar.h`、`elevate.h`、`machine.h`、`msi_stage.h`、`userstage.h`、`users.h`、`stage.h`、`uninstall.h`、`cmdline.h`。 +- **P3 package / driver**:`package.c` 單一 central directory 讀取器;`driver.c` 側通道改成參數、`prep_locked` 單一出口。`setup.h` 拆成 `drf_app.h`、`drf_util.h`、`drf_package.h`、`drf_driver.h`、`drf_update.h`、`drf_wizard.h`,測試專用宣告在 `drf_package_internal.h`、`drf_driver_internal.h`;`setup.h` 保留為總括 header,`setup_match.h` 為比對輔助。 +- **P4 wizard**:job 表、單一 flow 值、頁面表(每頁一個 `enter_*`)、明確的 `wz.result`(結果碼)、`wizard_state` 與 worker 輸入輸出結構、en/zh 字串集中。`src/wizard.c` include `src/wizard/` 下的 `main.h`、`acquire.h`、`driver.h`、`layout.h`、`strings.h`。 +- **P5 update**:`update_prompt(policy)` 取代 `update_run`/`update_offer`;launcher 入口拆成 `parse_args`、`elevation_gate`、`start_le`、`start_plain`、`hook_and_finish`。 +- **P6 dynafix**:資料表、提示 UI、選項、畫面、PEGRC 移到 `src/dynafix_tables.inc.c`、`dynafix_hint.inc.c`、`dynafix_options.inc.c`、`dynafix_screen.inc.c`、`dynafix_pegrc.inc.c`,由 `src/dynafix.c` include。hook 引擎、`seh_run`、`patch_slot`、`ldr_note`、USBFIX 狀態機、`cwp_work` 順序未動。 +- **死碼移入 tests**:`promote_com_in` 等測試專用 COM 輔助(0609115、e66d23d)移到 `tests/promote_com.h`,`zip_extract_all` 移到 `tests/zip_extract_all.h`。 + +### 與計畫的差異 + +- P1.3 `applog` 為純 header(`src/applog.h`),`build.cmd` 不需改。 +- dynafix 維持單一編譯單元(`.inc.c`),`logtest.c` 與 `build.cmd` 不動;dynafix 的 log 未併入 `applog.h`。 +- `setup.c` 仍是單一編譯單元,include 分檔,測試照舊 include 它。 + +## 疑似 bug:結論 + +1. `zip_extract` 未呼叫 `crc_init`:**不是 bug**。`zipent` 只來自 `zip_open`/`zip_walk`,它們已呼叫 `crc_init`;仍把 `crc_init` 改成可重複呼叫並由 `zip_extract` 呼叫作為防禦(P3.1)。 +2. `g_pkg_log` 常開:**不是 bug**。只有 `driver_fetch` 路徑會寫入;已改成參數(P3.2)。 +3. `classify` 的 `pMsi*` 為 NULL:**不是 bug**。`restore`/`unretarget` 只用 `LNK_OURS` 判斷,在 MSI 查詢之前就決定;仍新增 `msi_api_init`(P2.4,由 `classify` 呼叫,只執行一次),所以各路徑(含 uninstall)都會載入 msi.dll。 +4. `prep_locked` 提早 return 未清理:**已修**,948ee1f(準備失敗時清除已暫存檔案與 `accepted.txt`)。 +5. `peg_ldr_note` 在 loader lock 下呼叫 `logf`/`patch_base`:依 Tim 決定**不修**。沒有死鎖(具名 mutex 最多等 2 秒+檔案 I/O,不呼叫 LoadLibrary),且 patch 不能延後。但發現相關競態並修正,cb1aa28(loader callback 以非阻塞方式取得 `peg_check` 的 busy 旗標,拿不到就延後)。 +6. worker 無鎖寫入 `g_drv_*`、`g_neterr`、`g_hint_*`:**不是 bug**,`job_done` 在 UI 讀取前已 join worker;唯一的良性競態是 `g_lastpct`,改成只有 worker 寫入,1c8498e。 +7. `g_close` 設成 TRUE 後不再恢復:**已修**,2a1faa5(「關閉 DynaRun 並安裝」的同意只涵蓋一次安裝 job)。 +8. 死碼:移到 tests,0609115(另 e66d23d 移走其餘測試專用 COM 輔助)。 +- 額外修正:0791e0e,APPDATA 路徑過長時捷徑/釘選程式碼會溢位,改為略過該資料夾。 + +## 分支與驗證 + +- 分支:`claude/refactor-main-ab9bcd`(c860db5)、`claude/refactor-dynafix-65b202`(cb1aa28)、`claude/refactor-update-7ac5e6`(b4a59b6),皆從 main 912e3d4 分出,依序合併進 `refactor/solid`(merge commit ad34711、117eb7b、0582a1a);125d521 移除合併後重複的 `driver_prep_stage` stub;3923d63 文件對齊。 +- 最終 VM 驗證(3923d63,w11zhtw):單元測試 9/9 PASS;VM_CHECKLIST A 30 PASS/2 SKIP,B 23 PASS/13 SKIP;dynafix 8/8。證據:`C:\Data\legal\DynaRunFix\2026-10-18-solid\`。 +- 未驗證:標準使用者章節(5 項,需手動)、7.5 redirect(需受信任的自簽憑證)、BitLocker、Win8/7/XP、合併後版本的更新檢查(§8;已在 update 分支以 A–I 驗證)。 From ba51eb8b676ddcff1fd4cfe41c35f3db059420ad Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:36:46 +0800 Subject: [PATCH 35/41] Docs: remove private details from the refactor record Co-Authored-By: Claude Opus 5.5 --- docs/REFACTOR_PLAN.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md index 0172ca2..93b09f9 100644 --- a/docs/REFACTOR_PLAN.md +++ b/docs/REFACTOR_PLAN.md @@ -1,17 +1,17 @@ # 重構紀錄(已完成,2026-10-18) -依據:2026-10-17 逐行讀完 `src/` 全部模組、`tests/`、`build.cmd` 後擬定的計畫;本文件記錄實際結果。分支 `refactor/solid`,僅在本機,Tim 審完 git tree 才上遠端。 +依據:2026-10-17 逐行讀完 `src/` 全部模組、`tests/`、`build.cmd` 後擬定的計畫;本文件記錄實際結果。已合併進 `develop`。 ## 原則(執行時維持) - **行為不變**:每一步都是純重構;疑似 bug 另案驗證、另開 commit。 -- **小步、可驗證**:每步單獨編譯、在 VM 跑測試與回歸清單、單獨 commit。主機只編輯、編譯、commit。 +- **小步、可驗證**:每步單獨編譯、跑單元測試與回歸清單(`tests/VM_CHECKLIST.md`)、單獨 commit。 - **跨行程 ABI 不動**:elevated stage 的命令列動詞與參數位置、行程結束碼的數值、log mutex 名稱與格式、`CwpProc` 匯出、`setup.rc` 資源 ID、`DRF_VERSION` 巨集全部維持原樣,只加命名常數。 - **測試仍可 include**:測試靠 `#include "../src/setup.c"`、`driver.c` 取用 static,所以拆檔後各 `.c` 仍是單一編譯單元。 ## 決定 -Tim 的決定: +維護者的決定: 1. 範圍:P0–P6 全部做,並行進行。 2. 結果碼:只加名稱,數值不變(跨行程 ABI 不變)。 @@ -51,14 +51,14 @@ Tim 的決定: 2. `g_pkg_log` 常開:**不是 bug**。只有 `driver_fetch` 路徑會寫入;已改成參數(P3.2)。 3. `classify` 的 `pMsi*` 為 NULL:**不是 bug**。`restore`/`unretarget` 只用 `LNK_OURS` 判斷,在 MSI 查詢之前就決定;仍新增 `msi_api_init`(P2.4,由 `classify` 呼叫,只執行一次),所以各路徑(含 uninstall)都會載入 msi.dll。 4. `prep_locked` 提早 return 未清理:**已修**,948ee1f(準備失敗時清除已暫存檔案與 `accepted.txt`)。 -5. `peg_ldr_note` 在 loader lock 下呼叫 `logf`/`patch_base`:依 Tim 決定**不修**。沒有死鎖(具名 mutex 最多等 2 秒+檔案 I/O,不呼叫 LoadLibrary),且 patch 不能延後。但發現相關競態並修正,cb1aa28(loader callback 以非阻塞方式取得 `peg_check` 的 busy 旗標,拿不到就延後)。 +5. `peg_ldr_note` 在 loader lock 下呼叫 `logf`/`patch_base`:依維護者決定**不修**。沒有死鎖(具名 mutex 最多等 2 秒+檔案 I/O,不呼叫 LoadLibrary),且 patch 不能延後。但發現相關競態並修正,cb1aa28(loader callback 以非阻塞方式取得 `peg_check` 的 busy 旗標,拿不到就延後)。 6. worker 無鎖寫入 `g_drv_*`、`g_neterr`、`g_hint_*`:**不是 bug**,`job_done` 在 UI 讀取前已 join worker;唯一的良性競態是 `g_lastpct`,改成只有 worker 寫入,1c8498e。 7. `g_close` 設成 TRUE 後不再恢復:**已修**,2a1faa5(「關閉 DynaRun 並安裝」的同意只涵蓋一次安裝 job)。 8. 死碼:移到 tests,0609115(另 e66d23d 移走其餘測試專用 COM 輔助)。 - 額外修正:0791e0e,APPDATA 路徑過長時捷徑/釘選程式碼會溢位,改為略過該資料夾。 -## 分支與驗證 +## 合併與驗證 -- 分支:`claude/refactor-main-ab9bcd`(c860db5)、`claude/refactor-dynafix-65b202`(cb1aa28)、`claude/refactor-update-7ac5e6`(b4a59b6),皆從 main 912e3d4 分出,依序合併進 `refactor/solid`(merge commit ad34711、117eb7b、0582a1a);125d521 移除合併後重複的 `driver_prep_stage` stub;3923d63 文件對齊。 -- 最終 VM 驗證(3923d63,w11zhtw):單元測試 9/9 PASS;VM_CHECKLIST A 30 PASS/2 SKIP,B 23 PASS/13 SKIP;dynafix 8/8。證據:`C:\Data\legal\DynaRunFix\2026-10-18-solid\`。 -- 未驗證:標準使用者章節(5 項,需手動)、7.5 redirect(需受信任的自簽憑證)、BitLocker、Win8/7/XP、合併後版本的更新檢查(§8;已在 update 分支以 A–I 驗證)。 +- 重構分三條並行線(主程式/setup 與 wizard、dynafix、update),皆從 main 912e3d4 分出,依序合併進 `refactor/solid`(merge commit ad34711、117eb7b、0582a1a);各線主要 commit 為 c860db5、cb1aa28、b4a59b6。125d521 移除合併後重複的 `driver_prep_stage` stub;3923d63 文件對齊。 +- 最終驗證(3923d63):單元測試與 dynafix 測試通過,`tests/VM_CHECKLIST.md` 回歸清單通過;其中需手動或需特殊環境的項目未執行(見下)。 +- 未驗證:標準使用者相關的手動章節、7.5 redirect(需受信任的自簽憑證)、BitLocker、Win8/7/XP、合併後版本的更新檢查(§8;已在 update 分支驗證)。 From 8977f01795fb935555f36840f68d7b823dade21f Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:19 +0800 Subject: [PATCH 36/41] Shortcuts: bound-check everything appended after the shell folder path sf_path() now takes the number of characters the caller appends afterwards (the pinned-items folder, the shortcut file name) and skips the folder when the result would not fit in MAX_PATH. The check is split into sf_fit() so that test_hardening can exercise it with any path length. Co-Authored-By: Claude Opus 5.5 --- src/setup/shortcuts.h | 25 ++++++++++++++++++++----- src/setup/taskbar.h | 6 +++--- src/setup/userstage.h | 4 ++-- tests/test_hardening.c | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/src/setup/shortcuts.h b/src/setup/shortcuts.h index b9388a6..3cb517c 100644 --- a/src/setup/shortcuts.h +++ b/src/setup/shortcuts.h @@ -104,13 +104,28 @@ static const shortcut_folder SF[SF_COUNT] = { { { CSIDL_APPDATA, 0 }, L"\\DynaRunFix", NULL, NULL, SFU_RESTORE, 0 }, // desktop_link(): the shortcut it creates when there is none }; -// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none or the path is too long for what goes below it. -static BOOL sf_path(int id, BOOL common, WCHAR *out) +// What callers of sf_path append to the folder themselves afterwards (their `extra`): the shortcut name, and below the Quick Launch folder the pinned items' folder. +#define SF_LNK_LEN 15 // the length of: \DynaRun V3.lnk +#define SF_PINNED_LEN 20 // the length of: \User Pinned\TaskBar + +// Appends sub to out (a folder path in a MAX_PATH buffer) when the result, `extra` more characters and the terminator still fit; a row with a sub folder keeps +// 50 characters free for the names below it. FALSE (out unchanged) when it does not fit. Separate from sf_path so that a test can give it any length. +static BOOL sf_fit(WCHAR *out, const WCHAR *sub, int extra) +{ + int need = extra; + if (sub) { need += lstrlenW(sub); if (need < 50) need = 50; } + if (lstrlenW(out) + need >= MAX_PATH) return FALSE; + if (sub) lstrcatW(out, sub); + return TRUE; +} + +// The folder of row id (common: the all users' one) in out (MAX_PATH characters); FALSE when the shell has none or the path is too long for what goes below +// it, including the `extra` characters the caller appends itself. +static BOOL sf_path(int id, BOOL common, WCHAR *out, int extra) { const shortcut_folder *f = &SF[id]; int csidl = f->csidl[common ? 1 : 0]; if (!csidl || !SHGetSpecialFolderPathW(NULL, out, csidl, FALSE)) return FALSE; - if (f->sub) { if (lstrlenW(out) + 50 >= MAX_PATH) return FALSE; lstrcatW(out, f->sub); } - return TRUE; + return sf_fit(out, f->sub, extra); } // fn(folder, row, common, deep, ctx) is called for each folder that the use (one SFU_*) lists, in the scope (SF_USER, SF_COMMON, SF_BOTH): a row's @@ -123,7 +138,7 @@ static BOOL shortcut_folders(unsigned use, int scope, sf_fn fn, void *ctx) id = use == SFU_ICON && i < 2 ? 1 - i : i; // the icon is looked for in the Start menu before the Desktop if (!(SF[id].uses & use)) continue; for (c = 0; c < 2; c++) - if ((scope & (1 << c)) && sf_path(id, c, d) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; + if ((scope & (1 << c)) && sf_path(id, c, d, 0) && !fn(d, id, c, (SF[id].deep & use) != 0, ctx)) return TRUE; } return FALSE; } diff --git a/src/setup/taskbar.h b/src/setup/taskbar.h index 33f0013..d10e314 100644 --- a/src/setup/taskbar.h +++ b/src/setup/taskbar.h @@ -67,7 +67,7 @@ static void taskbar_policy(const install_opts *o, const WCHAR *sid, HKEY app) static BOOL pinned_ours(void) // a shortcut to the launcher among the taskbar's pinned items (Windows 7+) { static WCHAR d[MAX_PATH], f[MAX_PATH]; - if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; + if (!sf_path(SF_QUICKLAUNCH, FALSE, d, SF_PINNED_LEN)) return FALSE; lstrcatW(d, L"\\User Pinned\\TaskBar"); return find_ours(d, FALSE, f); } @@ -126,7 +126,7 @@ static BOOL pin_taskbar(const WCHAR *lnk, int *res) static WCHAR d[MAX_PATH], v[MAX_PATH], sub[64]; DWORD ma, mi, b; HKEY k; os_ver(&ma, &mi, &b); if (ma == 5 || (ma == 6 && !mi)) { // XP, Vista: the Quick Launch toolbar - if (!sf_path(SF_QUICKLAUNCH, FALSE, d)) return FALSE; + if (!sf_path(SF_QUICKLAUNCH, FALSE, d, SF_LNK_LEN)) return FALSE; CreateDirectoryW(d, NULL); if (!save_link(lstrcatW(d, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; if (!RegOpenKeyExW(HKEY_CURRENT_USER, cat3(sub, USERKEY, L"\\Shortcuts", NULL), 0, KEY_SET_VALUE, &k)) { @@ -148,7 +148,7 @@ static BOOL desktop_link(WCHAR *out, HKEY backup) // one of our shortcuts to p { WCHAR d[MAX_PATH]; if (find_our_shortcut(out)) return TRUE; - if (!sf_path(SF_OWN, FALSE, d)) return FALSE; + if (!sf_path(SF_OWN, FALSE, d, SF_LNK_LEN)) return FALSE; CreateDirectoryW(d, NULL); if (!save_link(cat3(out, d, L"\\DynaRun V3.lnk", NULL), g_launcher, g_exe, 0, 0, 0, FALSE)) return FALSE; RegSetValueExW(backup, out, 0, REG_BINARY, (const BYTE *)"", 0); diff --git a/src/setup/userstage.h b/src/setup/userstage.h index 83d6061..803445e 100644 --- a/src/setup/userstage.h +++ b/src/setup/userstage.h @@ -9,9 +9,9 @@ static void user_stage(const install_opts *o, install_result *res) RegDeleteValueW(b, L"UpdateChecked"); RegDeleteValueW(b, L"UpdateVersion"); RegDeleteValueW(b, L"UpdateSha256"); RegDeleteValueW(b, L"UpdateAsk"); RegDeleteValueW(b, L"UpdateSkip"); if (!RegCreateKeyExW(b, L"Shortcuts", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &s, NULL)) { int desk = fix_shortcuts(FALSE, s); - if (!desk && sf_path(SF_DESKTOP, TRUE, p)) + if (!desk && sf_path(SF_DESKTOP, TRUE, p, 0)) desk = scan(p, FALSE, s); // already handled by the machine stage; only counted here - if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p) && + if (!desk && !o->keep && sf_path(SF_DESKTOP, FALSE, p, SF_LNK_LEN) && save_link(lstrcatW(p, L"\\DynaRun V3.lnk"), g_launcher, g_exe, 0, 0, 0, FALSE)) RegSetValueExW(s, p, 0, REG_BINARY, (const BYTE *)"", 0); if (o->pin && !o->keep) { diff --git a/tests/test_hardening.c b/tests/test_hardening.c index 576ad62..2ca6c41 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -952,6 +952,39 @@ static void test_result_codes(void) // handling (install_all_driver), 4 = the DynaRun MSI failed, 5 = DynaRun not found after the MSI. } +// sf_fit: the length check behind sf_path (the shell's folder path itself cannot be chosen here, so the check takes the path as given). A path that does not fit +// leaves out unchanged and the folder is skipped; one that fits ends, with everything the callers append after it, within MAX_PATH including the terminator. +static void test_sf_fit(void) +{ + static const struct { int row, extra, max_len; const WCHAR *what; } t[] = { + { SF_QUICKLAUNCH, SF_PINNED_LEN, 198, L"Quick Launch + \\User Pinned\\TaskBar (pinned_ours)" }, + { SF_QUICKLAUNCH, SF_LNK_LEN, 203, L"Quick Launch + \\DynaRun V3.lnk (XP/Vista pin)" }, + { SF_OWN, SF_LNK_LEN, 209, L"\\DynaRunFix + \\DynaRun V3.lnk (desktop_link)" }, + { SF_DESKTOP, SF_LNK_LEN, 244, L"Desktop + \\DynaRun V3.lnk (user stage)" }, + { SF_DESKTOP, 0, 259, L"Desktop alone" }, + }; + int i, j, k, n; WCHAR out[MAX_PATH + 32], w[200]; BOOL ok; + CHECK(SF_LNK_LEN == lstrlenW(L"\\DynaRun V3.lnk") && SF_PINNED_LEN == lstrlenW(L"\\User Pinned\\TaskBar"), L"sf_fit: the suffix lengths match the strings the callers append"); + for (i = 0; i < (int)(sizeof t / sizeof t[0]); i++) { + const WCHAR *sub = SF[t[i].row].sub; int sublen = sub ? lstrlenW(sub) : 0; + for (k = -1; k <= 1; k++) { // just under, at, and just over the longest folder path that still fits + n = t[i].max_len + k; + for (j = 0; j < n; j++) out[j] = L'x'; + out[n] = 0; + ok = sf_fit(out, sub, t[i].extra); + wsprintfW(w, L"sf_fit: %s, folder path of %d characters: %s", t[i].what, n, k <= 0 ? L"kept" : L"skipped"); + CHECK(ok == (k <= 0), w); + if (ok) { + wsprintfW(w, L"sf_fit: %s, %d characters: sub folder appended, the whole path fits in MAX_PATH", t[i].what, n); + CHECK(lstrlenW(out) == n + sublen && lstrlenW(out) + t[i].extra < MAX_PATH, w); + } else { + wsprintfW(w, L"sf_fit: %s, %d characters: out unchanged when skipped", t[i].what, n); + CHECK(lstrlenW(out) == n, w); + } + } + } +} + int wmain(void) { HKEY k; @@ -977,6 +1010,7 @@ int wmain(void) wprintf(L"[run_elevated arguments]\n"); test_run_elevated_args(); wprintf(L"[command line switches]\n"); test_parse_switches(); wprintf(L"[verb table]\n"); test_verb_table(); + wprintf(L"[shortcut folder lengths]\n"); test_sf_fit(); wprintf(L"[result codes]\n"); test_result_codes(); { // clean up WCHAR c[1000]; From 92bedd200f2603aba549ce64030aa897bc193d0b Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:20 +0800 Subject: [PATCH 37/41] Wizard: ignore a WM_JOBDONE that does not name a running job The message parameter was used as an index into JOBS[] unchecked. Co-Authored-By: Claude Opus 5.5 --- src/wizard/main.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wizard/main.h b/src/wizard/main.h index 3f34a37..90dfc36 100644 --- a/src/wizard/main.h +++ b/src/wizard/main.h @@ -463,6 +463,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) if (wz.hint && wz.page == P_HELLO && !wz.busy) set_page(P_HELLO); return 0; case WM_JOBDONE: + if (!wz.busy || w >= JOB_COUNT) return 0; // not from our worker (nothing running, or a job number that is none): ignore, JOBS[] is not indexed KillTimer(h, ID_KEEPUP); // after the elevated part (which allows it) the wizard comes back in front of other windows if (JOBS[w].bring_front) bring_front(h); From b09b00a046ca87eb16b6093fa372adb8b510d23a Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:20 +0800 Subject: [PATCH 38/41] dynafix: state the bounded wait of the loader notification truthfully Comment only. The branch that wins the busy flag calls logf (log mutex, at most 2 s) and patch_base; only the flag itself is never waited for. Co-Authored-By: Claude Opus 5.5 --- src/dynafix_pegrc.inc.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dynafix_pegrc.inc.c b/src/dynafix_pegrc.inc.c index 3b8a605..283f0c9 100644 --- a/src/dynafix_pegrc.inc.c +++ b/src/dynafix_pegrc.inc.c @@ -181,7 +181,8 @@ static VOID NTAPI peg_ldr_note(ULONG reason, const ldrnote_t *d, PVOID ctx) for (j = 0; g_pegmods[k][j]; j++) w[j] = (WCHAR)g_pegmods[k][j]; w[j] = 0; if (!lstrcmpiW(n, w)) { - // under the loader lock: never wait; when peg_check is patching (on any thread) it is left to peg_check, no log here + // under the loader lock: the busy flag is never waited for - when peg_check is patching (on any thread) it is left to peg_check, no log here. + // The branch that wins the flag does wait, but only a bounded time: logf takes the log mutex (at most 2 s) and patch_base reads and patches memory. if (InterlockedCompareExchange(&g_pegbusy, 1, 0)) { InterlockedExchange(&g_pegdefer, 1); continue; } logf("PEGRC32D: loader notification for %s %u %u\r\n", (DWORD)(UINT_PTR)g_pegmods[k], 0, 0); patch_base((BYTE *)d->DllBase, g_pegmods[k], "peg-note", g_peg_hooks); From 1a8ba031577d528a6a8d3395607e5c1edf869fe1 Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:20 +0800 Subject: [PATCH 39/41] Docs: generic wording for the virtual machine tools Co-Authored-By: Claude Opus 5.5 --- docs/FIRST_START.md | 6 +++--- docs/ROOT_CAUSE.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/FIRST_START.md b/docs/FIRST_START.md index d890c6a..9092115 100644 --- a/docs/FIRST_START.md +++ b/docs/FIRST_START.md @@ -117,9 +117,9 @@ text in ANSI-charset fonts is drawn as Latin letters. ## How this was tested -* VirtualBox VMs: Windows 11 (locale changed to zh-TW), Windows 11 installed in zh-TW, Windows 7, Windows XP +* Virtual machines: Windows 11 (locale changed to zh-TW), Windows 11 installed in zh-TW, Windows 7, Windows XP (fresh install with the official DynaRun installer). - The VMs were driven through `VBoxManage` keyboard/mouse input and screenshots. + The VMs were driven through the virtual machine tools' keyboard/mouse input and screenshots. * Process Monitor captures filtered to `DynaPro` / `Setup_` events, registry dumps of `HKCU\Software\DynaPro` before and after the helper. @@ -202,5 +202,5 @@ DynaRun 在 ANSI 字元集字型裡的 Big5 文字會變成拉丁字母。 ### 測試方法 -VirtualBox 虛擬機(Win11 改繁中地區、原生繁中 Win11、Win7、全新 XP),透過 `VBoxManage` 鍵盤/滑鼠操作與截圖; +虛擬機(Win11 改繁中地區、原生繁中 Win11、Win7、全新 XP),透過虛擬機工具的鍵盤/滑鼠操作與截圖; Process Monitor 只篩選 `DynaPro`/`Setup_` 事件;設定程式執行前後匯出 `HKCU\Software\DynaPro`。 diff --git a/docs/ROOT_CAUSE.md b/docs/ROOT_CAUSE.md index 75619b7..d96e577 100644 --- a/docs/ROOT_CAUSE.md +++ b/docs/ROOT_CAUSE.md @@ -2,7 +2,7 @@ Environment where this was analysed: -| | Windows 11 (native) | Windows 7 SP1 (VirtualBox VM) | +| | Windows 11 (native) | Windows 7 SP1 (virtual machine) | |---|---|---| | DynaRun V3 | 3.26 | 3.26 (same installer) | | Screen | 2048x1280 logical, 2 monitors | 2560x1443, 1 monitor | From 948ff15135ef84f87290d55ac57351e58cf005ac Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:20 +0800 Subject: [PATCH 40/41] Docs/tests: small stale-reference fixes Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 2 +- tests/README.md | 4 ++-- tests/VM_CHECKLIST.md | 2 +- tests/build-tests.cmd | 2 +- tests/logtest.c | 2 +- tests/zip_extract_all.h | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8eec50e..48a8407 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: shell: cmd run: call tests\build-hardening-tests.cmd || exit /b 1 - # Driver tests (src\driver.c, the zip extraction of src\package.c); they use only %TEMP%\drf-drv-, no system tool is started. + # Driver tests (src\driver.c, the zip reading and staging of src\package.c; the whole-zip extraction lives only in tests\zip_extract_all.h); they use only %TEMP%\drf-drv-, no system tool is started. - name: Driver tests shell: cmd run: call tests\build-driver-tests.cmd || exit /b 1 diff --git a/tests/README.md b/tests/README.md index 619949f..ebfad74 100644 --- a/tests/README.md +++ b/tests/README.md @@ -19,7 +19,7 @@ powershell -File tests\run-tests.ps1 (needs python; writes only to a temp - **System DLL loading:** `logtest.exe loadsys` checks that `load_sys` (`src\dynafix.c`) loads a system DLL by name and refuses a missing one. To run the tests against another revision of the sources: `tests\build-tests.cmd [/DOLD_SRC]` -(`/DOLD_SRC` for a revision without `w2u`), then `run-tests.ps1 -Bin [-Mode old]`. +(`/DOLD_SRC` for a revision without `w2u`; the copy must be complete: `dynafix.c` includes its `dynafix_*.inc.c` siblings from its own folder and `launcher.c` includes `applog.h`), then `run-tests.ps1 -Bin [-Mode old]`. # First-run helper path tests @@ -41,7 +41,7 @@ copies COM registrations, that code is only kept under test (`tests\promote_com. # Driver tests -`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (`zip_extract_all` in `tests\zip_extract_all.h`, test-only: nothing shipped unpacks a whole zip; zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` and its fragments `src\wizard\*.h` (one text, in the order they are included) relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. +`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (`zip_extract_all` in `tests\zip_extract_all.h`, test-only: nothing shipped unpacks a whole zip; zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` and its fragments `src\wizard\*.h` (one text, in the order they are included; `strings.h` is not part of it) relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. Result codes (`RC_*`, `PK_*`, `DRV_E_*`) are named in `src\errors.h` (the tests lock their values); the module headers are `src\drf_*.h` (`setup.h` includes them all); `src\applog.h` is shared by the launcher and `update.c`. diff --git a/tests/VM_CHECKLIST.md b/tests/VM_CHECKLIST.md index f33bf51..e5be09e 100644 --- a/tests/VM_CHECKLIST.md +++ b/tests/VM_CHECKLIST.md @@ -2,7 +2,7 @@ Manual checks for a build of `DynaRunFix-Setup.exe` that the automated tests cannot cover (UAC, the wizard, the restart for the driver, the network). Run them in a throw-away VM (Windows 10 or 11, snapshot before each section). Do not run them on the development host. -The texts in quotes are the English ones from the source (`src\wizard.c` and `src\wizard\*.h`, `src\setup.c`, `src\update.c`); a Traditional Chinese Windows shows the Chinese pair. +The texts in quotes are the English ones from the source (`src\wizard.c` and `src\wizard\*.h` (strings in `src\wizard\strings.h`), `src\setup.c` and `src\setup\*.h` (strings in `src\setup\strings.h`), `src\update.c`); a Traditional Chinese Windows shows the Chinese pair. Build first with `build.cmd` (the setup is `build\DynaRunFix-Setup.exe`). A dev build is version `dev` and never takes part in the update check. diff --git a/tests/build-tests.cmd b/tests/build-tests.cmd index 58f0c75..3a59fe9 100644 --- a/tests/build-tests.cmd +++ b/tests/build-tests.cmd @@ -1,7 +1,7 @@ @echo off rem Builds the log test harness (tests\logtest.c). Not part of build.cmd / the release. rem Usage: tests\build-tests.cmd [outdir] [path\to\dynafix.c] [path\to\launcher.c] [extra cl options, e.g. /DOLD_SRC for a revision without w2u] -rem outdir defaults to build\tests; the two sources default to ..\src (give another copy to test an older revision). +rem outdir defaults to build\tests; the two sources default to ..\src (give another copy to test an older revision; dynafix_*.inc.c and applog.h must sit beside it as in src). rem Needs build\version.h, i.e. run build.cmd once first. The harness uses the normal CRT with /GS and /RTC1 rem (stack cookies and run-time checks), unlike the release build, so an overflow in logf/llog is caught. setlocal diff --git a/tests/logtest.c b/tests/logtest.c index 6517992..3ab6a52 100644 --- a/tests/logtest.c +++ b/tests/logtest.c @@ -2,7 +2,7 @@ // It #includes the real source, so what is tested is the shipped code: // logtest.exe -> src\dynafix.c (logf) // logtest_l.exe -> src\launcher.c (llog; built with -DTEST_LAUNCHER) -// SRC (default ..\src\dynafix.c / ..\src\launcher.c) can point at another copy, e.g. an older revision. +// SRC (default ..\src\dynafix.c / ..\src\launcher.c) can point at another copy, e.g. an older revision (with its dynafix_*.inc.c and applog.h beside it, as in src). // The log is %TEMP%\dynafix.log, so run it with TEMP pointing at a scratch directory. #include #include diff --git a/tests/zip_extract_all.h b/tests/zip_extract_all.h index 12ffa85..3ba58e4 100644 --- a/tests/zip_extract_all.h +++ b/tests/zip_extract_all.h @@ -1,6 +1,6 @@ // zip_extract_all: extracts every entry (stored or deflate, not encrypted) of a zip below dest (which must exist); an unsafe name, a damaged entry or a zip // larger than the limits fails the whole call (PK_FORMAT). *nfiles: files written. Nothing in the shipped code unpacks a whole zip (the driver and the -// DynaRun setup unpack only the entries a pinned table names), so it is not part of src\package.c; it is kept here, compiled by tests est_driver.c +// DynaRun setup unpack only the entries a pinned table names), so it is not part of src\package.c; it is kept here, compiled by tests\test_driver.c // (include it after src\setup.c), as a user of zip_walk / zip_extract / zip_name_safe and for the extraction tests. #define ZIP_MAX_FILES 500 From a176d7a28998b59ea1472a77474f9548842535db Mon Sep 17 00:00:00 2001 From: TimLiu <27921307+timliudev@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:41:21 +0800 Subject: [PATCH 41/41] Release v1.7.2: CHANGELOG and manual Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 14 ++++++++------ docs/MANUAL.md | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fda0c9..ba516ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,16 @@ [繁體中文在下方](#更新紀錄) -## Unreleased +## v1.7.2 — 2026-10-18 -Internal refactor (no behaviour change): `setup.c` is split into topic files in `src\setup\` and `wizard.c` into flow files in `src\wizard\` (each still one compilation unit), `setup.h` is now an umbrella over the `drf_*.h` module headers, the result codes have names in `src\errors.h` (values unchanged), the install options travel in `install_opts` instead of globals, the setup and driver stages share one command-line verb table, the wizard uses a page table and a job table, the launcher and the update check share `src\applog.h` (log lock, clock, value names, verbs), and `dynafix.c` is split into `dynafix_*.inc.c`. Test-only code (COM promotion, whole-zip extraction) moved out of the shipped sources into `tests\`. +Internal refactor (no intended behaviour change; see Fixed): `setup.c` is split into topic files in `src\setup\` and `wizard.c` into flow files in `src\wizard\` (each still one compilation unit), `setup.h` is now an umbrella over the `drf_*.h` module headers, the result codes have names in `src\errors.h` (values unchanged), the install options travel in `install_opts` instead of globals, the setup and driver stages share one command-line verb table, the wizard uses a page table and a job table, the launcher and the update check share `src\applog.h` (log lock, clock, value names, verbs), and `dynafix.c` is split into `dynafix_*.inc.c`. Test-only code (COM promotion, whole-zip extraction) moved out of the shipped sources into `tests\`. Fixed: - **"Close DynaRun and install" now applies to one install only.** The consent was kept for later attempts: after a cancelled UAC prompt or a failed install, a DynaRun started in the meantime could be ended without asking. The question is now asked again. - **A failed driver preparation no longer leaves files behind.** The staged driver files and `accepted.txt` in `%ProgramData%\DynaRunFix\driver` are removed when the check, the copy, the boot option or the scheduled task step fails. -- **A very long `%APPDATA%` path no longer overflows a buffer** in the shortcut and taskbar pin code; the folder that does not fit is skipped. +- **A very long `%APPDATA%` path no longer overflows a buffer** in the shortcut and taskbar pin code; the folder that does not fit is skipped. The check now also counts what is added after the folder (the taskbar's pinned-items folder and the shortcut file name), which could still overflow for paths of about 200 characters or more; the same check covers the desktop shortcut. +- **The setup wizard ignores a "job finished" window message that its own worker thread did not send.** The message number was used as an index into an internal table without a check, so a stray message could crash the wizard. - **The download progress bar** is updated by the download thread only (exactly one update per value when the size is unknown). - **dynafix.dll:** a race between the loader callback and the periodic check that could patch the same hook twice on two threads is fixed. @@ -508,15 +509,16 @@ Changed and fixed (after a review of v1.4.0): ## 更新紀錄 -### 未發行 +### v1.7.2 — 2026-10-18 -內部重構(行為不變):`setup.c` 依主題拆成 `src\setup\` 內的檔案、`wizard.c` 拆成 `src\wizard\` 內的流程檔(仍是單一編譯單元);`setup.h` 改為 `drf_*.h` 模組標頭的總入口;結果碼在 `src\errors.h` 命名(數值不變);安裝選項以 `install_opts` 傳遞而非全域變數;安裝程式與驅動程式階段共用一張命令列動詞表;精靈改用頁面表與工作表;啟動器與更新檢查共用 `src\applog.h`(log 鎖、時鐘、值名稱、動詞);`dynafix.c` 拆成 `dynafix_*.inc.c`。僅供測試的程式碼(COM 登錄提升、整包 zip 解壓)移出正式原始碼,放到 `tests\`。 +內部重構(不打算改變行為;見「修正」):`setup.c` 依主題拆成 `src\setup\` 內的檔案、`wizard.c` 拆成 `src\wizard\` 內的流程檔(仍是單一編譯單元);`setup.h` 改為 `drf_*.h` 模組標頭的總入口;結果碼在 `src\errors.h` 命名(數值不變);安裝選項以 `install_opts` 傳遞而非全域變數;安裝程式與驅動程式階段共用一張命令列動詞表;精靈改用頁面表與工作表;啟動器與更新檢查共用 `src\applog.h`(log 鎖、時鐘、值名稱、動詞);`dynafix.c` 拆成 `dynafix_*.inc.c`。僅供測試的程式碼(COM 登錄提升、整包 zip 解壓)移出正式原始碼,放到 `tests\`。 修正: - **「關閉 DynaRun 並安裝」只對一次安裝有效。** 先前同意會留給之後的重試:UAC 被取消或安裝失敗後,期間才啟動的 DynaRun 可能未經詢問就被結束。現在會重新詢問。 - **驅動程式準備失敗時不再留下檔案。** 檢查、複製、開機選項或排程工作任一步失敗時,會移除 `%ProgramData%\DynaRunFix\driver` 內已放置的驅動程式檔與 `accepted.txt`。 -- **%APPDATA% 路徑非常長時不再造成緩衝區溢位**(捷徑與工作列釘選程式碼);放不下的資料夾會被略過。 +- **%APPDATA% 路徑非常長時不再造成緩衝區溢位**(捷徑與工作列釘選程式碼);放不下的資料夾會被略過。檢查現在也計入資料夾之後才加上的部分(工作列釘選項目的資料夾與捷徑檔名),約 200 個字元以上的路徑先前仍可能溢位;桌面捷徑也用同一個檢查。 +- **安裝精靈會忽略不是它自己的工作執行緒送出的「工作完成」視窗訊息。** 先前未經檢查就把訊息的數字當成內部表格的索引,雜訊訊息可能使精靈當機。 - **下載進度條**只由下載執行緒更新(大小未知時每個數值只更新一次)。 - **dynafix.dll:** 修正載入回呼與定期檢查之間的競爭,兩個執行緒可能同時修改同一個 hook。 diff --git a/docs/MANUAL.md b/docs/MANUAL.md index 358eb8f..e53a9d2 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -84,6 +84,10 @@ whenever DynaRun starts its first-time setup helper (normally only on the very f ## What's new +v1.7.2 (released 2026-10-18): +- Fix: a very long `%APPDATA%` path (about 200 characters or more) no longer overflows a buffer in the shortcut and taskbar pin code; the folder that does not fit is skipped. +- Fix: the setup wizard ignores a "job finished" window message that its own worker thread did not send (it could crash the wizard). Also: "Close DynaRun and install" applies to one install only, and a failed driver preparation removes the staged driver files. The rest is an internal source reorganisation without intended changes; see the [changelog](../CHANGELOG.md). + v1.7.1 (released 2026-10-17): - Fix: a full install with the driver now asks for administrator permission only once. Before, on an administrator account it asked twice (once for the fix, once to prepare the driver), although the documentation said once. The driver pages (download and check, the restart picture) now come before the install; *Install and restart* then installs the fix and prepares the driver in one step, and *Skip* installs the fix without the driver. After the restart an administrator account is not asked again; a standard user still is (administrator password). @@ -767,6 +771,10 @@ DynaRun 執行中不會裁剪,所以單次執行期間檔案可能超過 10 MB; log 只記模組與 API 名稱、不含模組或匯入位址(可能出現視窗與 GDI 控制代碼的值),寫入時不跟隨符號連結或連接點,但可能含有檔案路徑與視窗標題,貼到網路上之前請先檢查。同一個 dll 也修正[首次設定](#首次設定)與 [OneDrive 檔案打不開](#部分-dpr-打不開file-run-properties-全空沒有曲線)的問題。 ### 更新內容 +v1.7.2(2026-10-18 發行): +- 修正:%APPDATA% 路徑非常長(約 200 個字元以上)時,捷徑與工作列釘選程式碼不再造成緩衝區溢位;放不下的資料夾會被略過。 +- 修正:安裝精靈會忽略不是它自己的工作執行緒送出的「工作完成」視窗訊息(先前可能使精靈當機)。另外:「關閉 DynaRun 並安裝」只對一次安裝有效,驅動程式準備失敗時會移除已放置的驅動程式檔。其餘是內部原始碼整理,不打算改變行為;見[更新紀錄](../CHANGELOG.md)。 + v1.7.1(2026-10-17 發行): - 修正:同時安裝驅動程式的完整安裝,現在只會要求一次系統管理員權限。先前在系統管理員帳戶上會問兩次(一次裝修正、一次準備驅動程式),和文件寫的「問一次」不符。驅動程式的頁面(下載與檢查、重新開機選單的圖片)現在排在安裝之前;按「安裝並重新開機」後,一步完成修正的安裝與驅動程式的準備;選「略過」則只安裝修正、不裝驅動程式。重新開機後,系統管理員帳戶不會再被詢問;標準使用者仍會被詢問(要輸入系統管理員密碼)。