diff --git a/CHANGELOG.md b/CHANGELOG.md index ba516ee..99f431e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ [繁體中文在下方](#更新紀錄) +## v1.8.0 — 2026-10-18 + +Added (setup wizard): + +- **Version check of Dyna Pro's setup.** The MSI's ProductVersion is classified in one function (`dynarun_ver_class`) against one constant (3.26.0, the tested version); the UpgradeCode and product name check stays. + - 3.26.0: as before. + - Another 3.x (or older): a notice on the *Check the installer* and license pages: tested only with 3.26.0, there may be additional problems, please open an issue at ; the install goes on. + - Version not readable: a notice, the install goes on. + - **4 or later** (possibly no longer Visual Basic 6, perhaps native Windows 10/11): a page after *Check the installer* explains that the fix may not be needed and suggests installing Dyna Pro's software alone first. Layout like the license page: the check box *I understand that DynaRunFix has not been tested with this version* enables *Install the fix anyway* (right-most); *Install original software only* installs Dyna Pro's MSI alone (the same staged and checked copy, one administrator prompt, new elevated verb `/msi-only`): no fix, no shortcut changes, no taskbar pin, no sign-in start and no USB driver (the driver step belongs to the fix and was made for V3's driver); *Cancel*. The finish page of *Install original software only* (*Dyna Pro's software is installed*) has *Close* only, which just closes the wizard (it does not start DynaRun). The accept check box starts unticked on every page, also when the license page comes back after the administrator prompt was declined, so a license is never pre-accepted. +- **Download instead of the copy found on this computer.** For a setup found on the computer, the *Check the installer* page has *Download it* (the existing download flow, nothing found locally is taken). The user's own file is not touched. +- **Flash and notification.** One helper flashes the taskbar button and shows a Windows notification (a temporary tray icon) when a background step (download, extraction, install, driver step) has ended, the wizard is not the foreground window and the wizard now waits for a click. Not when the wizard goes on to the next step by itself (the extraction right after the download, the driver step right after the install), not while the window is closing, not on the page that restarts the computer by itself, and not when the wizard is in front. At the sign-in start after the restart it is called once, when the wizard opens. The tray icon goes after 10 seconds, when the wizard is activated, and on every way the wizard ends. + +Changed: + +- **First start without a UAC prompt.** Dyna Pro's first-start helper `System Data\Setup_.exe` (exact full-path match as before) is started with `RunAsInvoker` in `__COMPAT_LAYER` for that one call (set and restored around `ShellExecuteExA`), so Windows' installer detection no longer asks for administrator permission. It only writes `HKCU\Software\DynaPro`. Waiting, restoring the language and `/restart` are unchanged; the log says `setup helper started without elevation (RunAsInvoker)`. This replaces the v1.4.0 behaviour that kept the prompt on purpose; DynaRun's other child processes still do not get `RunAsInvoker`. +- **The *Check the installer* page:** the source, the MSI's ProductVersion and Manufacturer come first, then path, size and SHA-256; the box uses the same smaller font as the license text and is sized to its text, so the page grows to fit it without scrolling (as far as the screen allows). +- **Window order during the install:** the wizard keeps itself directly behind the foreground window of the install chain (msiexec's progress window, the UAC prompt, the driver warning, the elevated stage), above everything else but not topmost, so the user can switch programs; an unrelated program in the foreground is left alone and the log says once per change that an unrelated window has the foreground (not whose it is: other programs' names are not logged); Explorer is put behind the wizard once per change, also when it covers the wizard again after the user has been in the wizard (before: on every timer tick). The programs of the install chain are matched by whole name, and the setup's own name by a prefix with a length check. +- **After the restart** the driver pages that wait for a click (*Last step*, *7 was not pressed*) are topmost so that start-up windows do not cover them. The page that runs the driver install is not (Windows' own warning dialog has to stay visible there). +- **The setup you checked is the setup that is installed.** The SHA-256 shown on the *Check the installer* page (of the MSI that will run: the file itself, or the Setup.msi inside the zip) is handed to the elevated stage as `/msi-sha256 <64 hex digits>`. `/full` and `/msi-only` are marked in the verb table as needing it and are refused (exit code 1, nothing runs) without a valid one (exactly 64 hexadecimal digits). The stage computes the SHA-256 of its administrators-only copy and, if it differs, shows the message *The DynaRun V3 setup could not be matched with the SHA-256 shown ...* and stops with exit code 4 (`RC_MSI_FAILED`) before `msiexec` runs. This closes the gap between the check page and the staged copy, because the file lives in a folder the user can write. A setup whose hash could not be read (shown as `?`) is not installed. +- **Values read from the MSI** (ProductVersion, Manufacturer): the conversion is checked, the text is always terminated and cut to its buffer (64 and 128 characters; `msi_info` now takes both sizes), and control characters become spaces before the text is shown in the info box, used for the version gate or logged. +- Fix: `dynafix.dll` puts `__COMPAT_LAYER` back exactly as it was around the helper's start: a variable that was not set is removed again, one that was set to an empty value stays empty. The words of the value are separated by spaces and tabs. The two log lines no longer carry placeholder numbers. +- Internal: `machine_full` shares its MSI staging and run with the new `/msi-only` stage; `df_compat_with_invoker` (pure, tested) builds the `__COMPAT_LAYER` value; `in_install_chain` is a pure function in `src\install_chain.h` (tested); the wizard's flow differences (the license page's install job, the finish page, the page to return to after a declined prompt) are rows of `FLOWS[]` and `JOBS[]` instead of `if (wz.flow ...)` branches, and the MSI-only finish page is a page of its own (`P_MSI_DONE`). + ## v1.7.2 — 2026-10-18 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\`. @@ -259,7 +282,7 @@ Changed and fixed (after a review of v1.4.0): - First start: only option buttons that are clearly read as green (picked) are written; when `dynafix.dll` cannot read DynaRun's option buttons (or is unsure what it sees) it writes **nothing** and only logs it (before, it could write feature flags the user had not chosen). - `dynafix.dll` clears `__COMPAT_LAYER` for DynaRun's own process so a `RunAsInvoker` set by the launcher does not suppress - the one-time UAC prompt of Dyna Pro's configuration helper; the graph language redirect is always a resource-only load and + the one-time UAC prompt of Dyna Pro's configuration helper (superseded in v1.8.0: the helper is now started without elevation); the graph language redirect is always a resource-only load and is refused if the elevation state cannot be determined. - `dynafix.log` no longer contains absolute addresses of other modules' import slots, only module and API names, and the launcher and the dll do not follow links (symbolic links, junctions) when opening it. - The manifest's version is `1.0.0.0`; the installer's product version is `1.4.0`. @@ -509,7 +532,30 @@ Changed and fixed (after a review of v1.4.0): ## 更新紀錄 -### v1.7.2 — 2026-10-18 +### v1.8.0 — 2026-10-18 + +新增(安裝精靈): + +- **檢查 Dyna Pro 安裝檔的版本。** MSI 的 ProductVersion 由同一個函式(`dynarun_ver_class`)對照同一個常數(3.26.0,測試過的版本)分類;升級代碼與產品名稱的檢查照舊。 + - 3.26.0:照舊。 + - 其他 3.x(或更舊):「確認安裝檔」頁與授權合約頁顯示提示——只在 3.26.0 測試過、可能有額外問題、請到 開 issue;安裝繼續。 + - 讀不到版本:顯示提示,安裝繼續。 + - **4 以上**(可能已不是 Visual Basic 6,也許是原生 Windows 10/11):「確認安裝檔」之後多一頁,說明可能不需要修正,並建議先單獨安裝 Dyna Pro 的軟體。版面與授權合約頁相同:勾選「我了解 DynaRunFix 未在此版本測試過」才能按最右邊的「仍要安裝修正」;「僅安裝原版軟體」只安裝 Dyna Pro 的 MSI(同樣是暫存並檢查過的副本、一次系統管理員確認,新的提升權限動詞 `/msi-only`):不裝修正、不改捷徑、不釘選工作列、不設開機自動啟動,也不裝 USB 驅動程式(驅動程式步驟屬於修正,是為 V3 的驅動程式而設);另有「取消」。「僅安裝原版軟體」的結束頁(「Dyna Pro 的軟體已安裝」)只有「關閉」,按下去只會關閉精靈(不會啟動 DynaRun)。接受用的核取方塊在每一頁開始時都是未勾選,從被拒絕的系統管理員確認回到授權合約頁時也一樣,所以授權合約絕不會被預先接受。 +- **改用官網下載,不用電腦上找到的檔案。** 在這台電腦上找到安裝檔時,「確認安裝檔」頁多一個「改從官網下載」(走原本的下載流程,不再採用電腦上找到的檔案)。使用者自己的檔案不會被動到。 +- **閃爍與通知。** 同一個函式負責:背景步驟(下載、解壓縮、安裝、驅動程式步驟)結束、精靈不是前景視窗,而且精靈現在等待使用者點擊時,讓工作列按鈕閃爍並顯示 Windows 通知(暫時的系統匣圖示)。精靈自己接著做下一個步驟時(下載後馬上解壓縮、安裝後馬上做驅動程式步驟)、視窗正在關閉時、自動重新開機的那一頁,以及精靈已在前景時,都不會通知。重新開機後登入時的繼續,在精靈開啟時通知一次。系統匣圖示在 10 秒後、精靈被啟用時,以及精靈以任何方式結束時移除。 + +變更: + +- **第一次啟動不再出現 UAC 提示。** Dyna Pro 的首次啟動設定程式 `System Data\Setup_<編號>.exe`(仍然只接受完整路徑吻合)在那一次呼叫的 `__COMPAT_LAYER` 加上 `RunAsInvoker`(圍繞 `ShellExecuteExA` 設定並還原),Windows 的安裝程式偵測就不會再要求系統管理員權限。它只寫入 `HKCU\Software\DynaPro`。等待、寫回語言與 `/restart` 不變;log 會寫 `setup helper started without elevation (RunAsInvoker)`。這取代 v1.4.0 刻意保留提示的做法;DynaRun 其他子行程仍不會拿到 `RunAsInvoker`。 +- **「確認安裝檔」頁:** 先顯示來源、MSI 的 ProductVersion 與 Manufacturer,再顯示路徑、大小與 SHA-256;文字框使用與授權合約文字相同的較小字型,並依文字量決定高度,頁面會(在螢幕允許的範圍內)長高到不需捲動。 +- **安裝期間的視窗順序:** 精靈讓自己緊貼在安裝流程中取得前景的視窗(msiexec 進度視窗、UAC 提示、驅動程式警告、提升權限的階段)後面,在其他視窗之上但不置頂,使用者仍可切換程式;前景是無關的程式時不干擾,並在前景改變時於 log 記錄一次「有無關的視窗在前景」(不記錄是誰的:不寫其他程式的名稱);Explorer 在前景改變時被放到精靈後面一次,使用者回到精靈之後 Explorer 再度蓋住精靈時也一樣(以前是每個計時週期都處理)。安裝流程中的程式以完整名稱比對,安裝程式自己的名稱以前綴比對並檢查長度。 +- **重新開機後**等待點擊的驅動程式頁面(「最後一步」、「沒有按 7」)會置頂,開機啟動的視窗不會蓋住它。執行驅動程式安裝的頁面不置頂(Windows 自己的警告對話框必須看得到)。 +- **檢查過的安裝檔就是被安裝的安裝檔。** 「確認安裝檔」頁顯示的 SHA-256(將要執行的 MSI:檔案本身,或 zip 裡的 Setup.msi)以 `/msi-sha256 <64 個十六進位字元>` 傳給提升權限的階段。動詞表標記 `/full` 與 `/msi-only` 需要它,沒有有效值(剛好 64 個十六進位字元)就拒絕執行(結束代碼 1、什麼都不做)。該階段計算它那份只有系統管理員能用的副本的 SHA-256,不同就顯示「DynaRun V3 安裝檔和「確認安裝檔」頁面顯示的 SHA-256 對不起來 ...」並以結束代碼 4(`RC_MSI_FAILED`)結束,不會執行 `msiexec`。這補上「確認安裝檔」頁到暫存副本之間的空隙(檔案放在使用者可寫的資料夾)。讀不到雜湊(顯示為 `?`)的安裝檔不會被安裝。 +- **從 MSI 讀到的值**(ProductVersion、Manufacturer):檢查轉換結果、一律補上結尾、依緩衝區截斷(64 與 128 個字元;`msi_info` 現在分別接收兩個大小),控制字元換成空白,之後才顯示在資訊框、用於版本判斷或寫入 log。 +- 修正:`dynafix.dll` 在啟動設定程式前後把 `__COMPAT_LAYER` 還原成原來的樣子:原本沒有設定的變數會再移除,原本設成空值的維持空值。值裡的字以空白與 Tab 分隔。兩行 log 不再帶沒有意義的數字。 +- 內部:`machine_full` 與新的 `/msi-only` 階段共用 MSI 暫存與執行;純函式 `df_compat_with_invoker`(有測試)組出 `__COMPAT_LAYER` 的值;`in_install_chain` 是 `src\install_chain.h` 裡的純函式(有測試);精靈各流程的差異(授權頁的安裝工作、結束頁、被拒絕後要回到的頁面)改成 `FLOWS[]` 與 `JOBS[]` 的欄位,不再是 `if (wz.flow ...)` 分支,「僅安裝原版軟體」的結束頁是獨立的一頁(`P_MSI_DONE`)。 + +## 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\`。 @@ -687,7 +733,7 @@ Changed and fixed (after a review of v1.4.0): - 下載:每一次轉址都檢查 HTTPS 與主機(更新下載只能轉址到 GitHub 自己的主機)、使用 TLS 1.2 以上(Windows 7 需要 KB3140245)、大小上限 300 MB、提前中斷的下載不會被當成完成。系統程式庫只從系統資料夾(System32)載入。 - 精靈取消或失敗時,會從 `%TEMP%\DynaRunFix` 刪除已解密的 Dyna Pro `Setup.msi`。 - 首次啟動:只有明確讀到是綠色(已勾選)的選項按鈕才會寫入;`dynafix.dll` 讀不到 DynaRun 的選項按鈕(或無法確定看到的內容)時**什麼都不寫**,只記 log(以前可能寫入使用者沒有選的功能旗標)。 - - `dynafix.dll` 在 DynaRun 自己的行程中清除 `__COMPAT_LAYER`,使啟動器設的 `RunAsInvoker` 不會讓 Dyna Pro 設定程式的一次性 UAC 提示消失; + - `dynafix.dll` 在 DynaRun 自己的行程中清除 `__COMPAT_LAYER`,使啟動器設的 `RunAsInvoker` 不會讓 Dyna Pro 設定程式的一次性 UAC 提示消失(v1.8.0 起已取代:設定程式改為不提升權限啟動); 圖表語言的導向一律只載入資源,而且無法判斷是否提升權限時一律拒絕。 - `dynafix.log` 不再記錄其他模組匯入表欄位的絕對位址,只記模組與 API 名稱;啟動器與 dll 開啟它時也不跟隨連結(符號連結、連接點)。 - manifest 的版本是 `1.0.0.0`;安裝程式的產品版本是 `1.4.0`。 diff --git a/README.md b/README.md index d946b4d..5ff7afa 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ -# DynaRunFix +# DynaRunFix: Windows 10/11 fix for DynaRun V3 (Dyna Pro dynamometer) / Dyna Pro 馬力機軟體 DynaRun V3 的 Win10/11 修正 [![build](https://github.com/timliudev/DynaRunFix/actions/workflows/build.yml/badge.svg)](https://github.com/timliudev/DynaRunFix/actions/workflows/build.yml) -A small compatibility shim that stops the **main-screen flicker** of the *DynaRun V3* dynamometer -software (Dyna Pro Dynamometers, S68 and similar rigs) when it runs **natively on Windows 10 / 11**. +DynaRunFix is an unofficial compatibility fix that stops the **main-screen flicker** of *DynaRun V3*, the Dyna Pro dynamometer software (S68 and similar rigs), when it runs **natively on Windows 10 / 11**.
+DynaRunFix 是非官方的相容性修正,讓 Dyna Pro 馬力機的 *DynaRun V3* 軟體(S68 等機型)**原生在 Windows 10 / 11 執行時主畫面不再閃爍**。 + +Its installer can also set up DynaRun V3 and the Dyna Pro USB driver; it does not include DynaRun itself.
+安裝程式也能替你安裝 DynaRun V3 與 Dyna Pro USB 驅動程式;本專案不含 DynaRun 本體。 [繁體中文說明在下方](#繁體中文) @@ -30,8 +33,8 @@ software (Dyna Pro Dynamometers, S68 and similar rigs) when it runs **natively o ## Use -Start DynaRun with its usual **DynaRun V3** icon (the shortcuts now go through the fix). On the very first start Windows asks once more -for permission for Dyna Pro's configuration helper: click **Yes**. Uninstall from *Programs and Features* / *Installed apps*; it asks +Start DynaRun with its usual **DynaRun V3** icon (the shortcuts now go through the fix). Since v1.8.0 the very first start no longer asks +for permission: Dyna Pro's configuration helper runs without elevation and DynaRun restarts by itself. Uninstall from *Programs and Features* / *Installed apps*; it asks whether to remove the fix only or DynaRun V3 too. ## More @@ -72,7 +75,7 @@ MIT, see [LICENSE](https://github.com/timliudev/DynaRunFix/blob/main/LICENSE). T ### 使用 -照常點 **DynaRun V3** 圖示啟動(捷徑現在會經過修正)。第一次啟動時,Windows 會再問一次是否允許 Dyna Pro 的設定程式變更:按**是**。 +照常點 **DynaRun V3** 圖示啟動(捷徑現在會經過修正)。從 v1.8.0 起,第一次啟動不再詢問權限:Dyna Pro 的設定程式不提升權限執行,DynaRun 會自動重新啟動。 解除安裝請用「程式和功能」/「已安裝的應用程式」;它會問只移除修正,還是連 DynaRun V3 一起移除。 ### 更多 diff --git a/docs/FIRST_START.md b/docs/FIRST_START.md index 9092115..10035fb 100644 --- a/docs/FIRST_START.md +++ b/docs/FIRST_START.md @@ -10,7 +10,7 @@ about it. Findings from 2026-10-07. The user-facing summary is in the [manual](M On the first start (no `HKCU\Software\DynaPro` yet) DynaRun shows a language dialog, then the system selection window "Dyna Pro系統選擇" (model plus optional features). After **OK**: -* Windows shows a UAC prompt for `Setup_.exe`; +* (before v1.8.0) Windows shows a UAC prompt for `Setup_.exe` (since v1.8.0 the helper is started without elevation, see below); * the selection window stays, DynaRun uses one CPU core at 100 %, and it never continues; * after killing and restarting DynaRun the chosen model is active, but the language is English and the optional features are switched off. @@ -24,10 +24,16 @@ On OK, Process Monitor shows DynaRun storing the selection in the registry and s `Setup_114.exe` for the S68, `Setup_103.exe` for the S125 (file description "Registry Setup "). The helper writes that model's defaults into `HKCU\Software\DynaPro` and exits. -Windows asks for elevation when a helper is started (a UAC prompt was observed). The helpers have "Setup" in their file -name and description; installer detection is the likely reason, which was not checked further. That prompt is a side effect, +Before v1.8.0, Windows asked for elevation when a helper was started (a UAC prompt was observed). The helpers have "Setup" in their file +name and description; installer detection is the likely reason, which was not checked further. That prompt was a side effect, not the cause of the hang (below). +**Since v1.8.0 there is no prompt.** The helper needs no administrator rights: Process Monitor shows that it only writes `HKCU\Software\DynaPro` (and +files under `%APPDATA%`), which a non-elevated process can do (see "Ruled out: UAC virtualization" below). `dynafix.dll` therefore starts it with the compatibility +layer `RunAsInvoker` in `__COMPAT_LAYER`, set for that one `ShellExecuteExA` call and put back right after it, so Windows' installer detection does not ask for +elevation. Earlier versions (since v1.4.0) deliberately kept the prompt, by taking `RunAsInvoker` out of DynaRun's environment, so the helper ran exactly as without DynaRunFix; +the prompt turned out to be unnecessary. DynaRun's other child processes still get the unchanged environment. + ## Ruled out: UAC virtualization File/registry virtualization would hide the helper's writes from the non-elevated DynaRun only if they went to @@ -60,10 +66,10 @@ All in memory, no DynaRun file is changed. | Part | Change | Status | |---|---|---| -| `dynafix.dll` | Wraps the Windows function `ShellExecuteA` for DynaRun (it hooks the VB runtime's `GetProcAddress` import to hand out its wrapper). For `System Data\Setup_.exe` (exact match; any other file passes through untouched) it runs the helper with `ShellExecuteExA`, waits for it (one UAC prompt), puts back the `Default_Language` DynaRun had stored before the helper ran, and restarts DynaRun through `DynaRunFix.exe /restart ` — so DynaRun never gets stuck. | Verified, Win11 zh-TW, DynaRun 3.26.0 | +| `dynafix.dll` | Wraps the Windows function `ShellExecuteA` for DynaRun (it hooks the VB runtime's `GetProcAddress` import to hand out its wrapper). For `System Data\Setup_.exe` (exact match; any other file passes through untouched) it runs the helper with `ShellExecuteExA` without elevation (`__COMPAT_LAYER` has `RunAsInvoker` for that one call, no UAC prompt; the log says `setup helper started without elevation (RunAsInvoker)`), waits for it, puts back the `Default_Language` DynaRun had stored before the helper ran, and restarts DynaRun through `DynaRunFix.exe /restart ` — so DynaRun never gets stuck. | Verified, Win11 zh-TW, DynaRun 3.26.0 (with the UAC prompt, up to v1.7.2); the start without elevation (v1.8.0): to be verified in a VM, `tests\VM_CHECKLIST.md` section 10 | | `DynaRunFix.exe` (launcher) | Starts DynaRun with `CREATE_SUSPENDED`, resumes it and polls `SetWindowsHookEx(WH_CALLWNDPROC)` on its GUI thread until it succeeds (a thread can be hooked only once it has a message queue; before that the call fails with error 87). Through Locale Emulator it starts `LEProc.exe` suspended in a job object, is told at once when DynaRun's process is created and hooks DynaRun's first thread the same way. `dynafix.dll` is then loaded before the first window, so the splash screen and forms created at start-up are covered too. With `/restart ` it first waits (up to 15 s) for the old DynaRun to end. `dynafix.dll` is loaded into the launcher *before* DynaRun is created: the first load of a just-installed dll took seconds on a fresh Win11, and when it happened after the resume DynaRun had already made its first windows and laid out its main screen for the full screen height. `hooked from start` only says the first thread was hooked; `windows DynaRun had before dynafix: 0` says the fix came in time. | Verified, Win11 (log: `hooked from start`), native zh-TW Win11; first start right after install verified on a fresh zh-TW Win11 VM (old order 4/4 wrong, new order 5/5 right) | | `DynaRunFix-Setup.exe` | Elevation from a mapped network drive (e.g. a VM shared folder) first copies the setup to `%TEMP%` and elevates the copy — the elevated process cannot see the user's mapped drives. Before, no UAC prompt appeared and the install failed silently. | Verified, Win11 (UAC names the `%TEMP%` copy) | -| `DynaRunFix-Setup.exe` (wizard) | Comes back to the foreground after the elevated part (the elevated process calls `AllowSetForegroundWindow`; if msiexec's window still holds the foreground, the wizard is raised in z-order and activated through `AttachThreadInput`, else its taskbar button flashes). | Verified, Win11 (final page in front of a maximized Explorer) | +| `DynaRunFix-Setup.exe` (wizard) | Comes back to the foreground after the elevated part (the elevated process calls `AllowSetForegroundWindow`; if msiexec's window still holds the foreground, the wizard is raised in z-order and activated through `AttachThreadInput`, else its taskbar button flashes and a notification appears). While the elevated part runs, a 300 ms timer keeps the wizard directly behind the foreground window of the install chain (msiexec, consent, drvinst, pnputil, dpinst, the elevated stage), never topmost; a foreground window of an unrelated program is left alone and the log says only that an unrelated window has the foreground (not whose it is). When a background step (download, extraction, install, driver step) ends and the wizard then waits for a click while another window is in front, the taskbar button flashes and a notification appears; not when the next step starts at once, while the window closes, or on the page that restarts the computer by itself. The tray icon of the notification is removed after 10 s, when the wizard is activated and whenever the wizard ends. | Verified, Win11 (final page in front of a maximized Explorer) | | `dynafix.dll` (optional features) | While the selection screen is still open, reads which optional-feature buttons are green (controls inside the frame by position, hidden EB-150/EB-250 skipped). After the helper it writes the `System_Setup` flags that *存檔並離開* would write (`Auto_Climate_Enable` + `Property_Auto_Climate`, `Cooling_Fan_Enable`, `Int_AF_Ratio_Enable`, `Water_Cooler_Enable`) as `"-1"`, logs each value, then restarts DynaRun. Only buttons that are clearly read as green are written; if the buttons cannot be read reliably (or any colour is unclear), nothing is written and the log says so. | Verified, Win11 (cooling fan alone; climate + fan + AFR) and native zh-TW Win11 (v1.2.2 code); XP not tested | After the restart DynaRun comes up with the chosen model and language. With the optional-feature fix the @@ -82,7 +88,7 @@ cooler cannot be picked. Recorded on a fresh native Traditional Chinese Windows 11 with `DynaRunFix-Setup.exe` 1.2.2 (DynaRun 3.26.0 downloaded and installed by the setup): Chinese picked, S68 with climate monitor and AFR analyser, one UAC prompt for -`Setup_114.Exe`, automatic restart. Splash screen and main window in Microsoft JhengHei UI, status bar above the taskbar. +`Setup_114.Exe` (v1.2.2 behaviour; no prompt since v1.8.0), automatic restart. Splash screen and main window in Microsoft JhengHei UI, status bar above the taskbar. (Screenshots of DynaRun's windows are not included.) Result: the main window came up in Chinese with the climate strip and the AFR gauge. @@ -134,7 +140,7 @@ DynaRun V3 3.26.0 首次啟動流程的開發紀錄:哪裡出錯、DynaRunFix 首次啟動(還沒有 `HKCU\Software\DynaPro`)時,DynaRun 先顯示語言對話框,再顯示「Dyna Pro系統選擇」(機型與選購功能)。按 **OK** 後: -* 出現 `Setup_<編號>.exe` 的 UAC 提示; +* (v1.8.0 以前)出現 `Setup_<編號>.exe` 的 UAC 提示(v1.8.0 起以不提升權限的方式啟動,見下); * 選擇視窗不消失、DynaRun 佔滿一個 CPU 核心,永遠不會繼續; * 強制結束再啟動後機型正確,但語言變回英文,選購功能都是關閉的。 @@ -142,9 +148,13 @@ DynaRun V3 3.26.0 首次啟動流程的開發紀錄:哪裡出錯、DynaRunFix 按下 OK 後,Process Monitor 可以看到 DynaRun 把選擇存進登錄,然後啟動 `%APPDATA%\Dyna Pro Dynamometers\Dyna Run V3\System Data\Setup_<編號>.exe`。每個機型一個(S68 是 `Setup_114.exe`、 -S125 是 `Setup_103.exe`…,檔案描述為「Registry Setup <編號>」),它把該機型的預設值寫進 `HKCU\Software\DynaPro` 後結束。這些程式的名稱與描述含「Setup」;啟動時 Windows 會要求提升權限(觀察到 UAC 提示),可能的原因是安裝程式偵測,未進一步確認。 +S125 是 `Setup_103.exe`…,檔案描述為「Registry Setup <編號>」),它把該機型的預設值寫進 `HKCU\Software\DynaPro` 後結束。這些程式的名稱與描述含「Setup」;v1.8.0 以前啟動時 Windows 會要求提升權限(觀察到 UAC 提示),可能的原因是安裝程式偵測,未進一步確認。 UAC 只是附帶現象,不是卡住的原因。 +**v1.8.0 起沒有提示。** 這個設定程式不需要系統管理員權限:Process Monitor 顯示它只寫入 `HKCU\Software\DynaPro`(以及 `%APPDATA%` 下的檔案),未提升權限的行程就能做到(見下方「已排除:UAC 虛擬化」)。 +所以 `dynafix.dll` 在啟動它的那一次 `ShellExecuteExA` 呼叫前,把相容層 `RunAsInvoker` 加進 `__COMPAT_LAYER`,呼叫後立刻還原,Windows 的安裝程式偵測就不會要求提升權限。 +先前的版本(自 v1.4.0 起)刻意保留這個提示,方法是把 `RunAsInvoker` 從 DynaRun 的環境清掉,讓設定程式的行為與沒有 DynaRunFix 時完全一樣;事後證明這個提示並不必要。DynaRun 其他子行程的環境維持不變。 + ### 已排除:UAC 虛擬化 所有設定都在 `HKCU\Software\DynaPro` 和 `%APPDATA%`,這兩處不會被虛擬化;同一個管理員帳號提升權限後寫的也是同一個 HKCU。 @@ -173,14 +183,14 @@ Process Monitor 也確認 DynaRun 下次啟動會讀到設定程式寫的值。 | 元件 | 修改 | 狀態 | |---|---|---| -| `dynafix.dll` | 為 DynaRun 包裝 Windows 的 `ShellExecuteA`(hook VB 執行階段的 `GetProcAddress` 匯入,交出包裝函式):遇到 `System Data\Setup_<數字>.exe`(完整路徑吻合才算,其他檔案原樣放行)時用 `ShellExecuteExA` 執行並等待(一次 UAC),寫回 DynaRun 先前存的 `Default_Language`,再以 `DynaRunFix.exe /restart ` 重新啟動 DynaRun,DynaRun 就不會卡住。 | 已驗證(Win11 繁中、3.26.0) | +| `dynafix.dll` | 為 DynaRun 包裝 Windows 的 `ShellExecuteA`(hook VB 執行階段的 `GetProcAddress` 匯入,交出包裝函式):遇到 `System Data\Setup_<數字>.exe`(完整路徑吻合才算,其他檔案原樣放行)時以不提升權限的方式用 `ShellExecuteExA` 執行(只在這一次呼叫的 `__COMPAT_LAYER` 加上 `RunAsInvoker`,沒有 UAC 提示;log 會寫 `setup helper started without elevation (RunAsInvoker)`)並等待,寫回 DynaRun 先前存的 `Default_Language`,再以 `DynaRunFix.exe /restart ` 重新啟動 DynaRun,DynaRun 就不會卡住。 | 已驗證(Win11 繁中、3.26.0;v1.7.2 以前含 UAC 提示);不提升權限的啟動方式(v1.8.0)待在虛擬機驗證,見 `tests\VM_CHECKLIST.md` 第 10 節 | | `DynaRunFix.exe` | 以 `CREATE_SUSPENDED` 啟動 DynaRun,恢復執行後輪詢 `SetWindowsHookEx`,直到其 GUI 執行緒有訊息佇列(之前會回傳錯誤 87)。經 Locale Emulator 時則把 `LEProc.exe` 暫停啟動並放進 job 物件,DynaRun 一建立就收到通知,再以同樣方式掛上它的第一個執行緒。這樣 `dynafix.dll` 在第一個視窗之前就載入,啟動畫面和一開始建立的表單也都涵蓋。`/restart ` 時先等舊的 DynaRun 結束(最多 15 秒)。`dynafix.dll` 在建立 DynaRun 之前就先載入啟動器:剛安裝的 dll 第一次載入在全新 Win11 上要好幾秒,若在 DynaRun 開始執行後才載入,DynaRun 已建立最初的視窗並照整個螢幕高度排好主畫面。log 的 `hooked from start` 只代表掛上了第一個執行緒,`windows DynaRun had before dynafix: 0` 才代表來得及。 | 已驗證(Win11、原生繁中 Win11) | | `DynaRunFix-Setup.exe` | 從網路磁碟機(例如虛擬機共用資料夾)提升權限時,先複製到 `%TEMP%` 再提升(提升後的行程看不到使用者對應的網路磁碟機)。修正前不會出現 UAC,安裝無聲失敗。 | 已驗證(Win11) | -| 安裝精靈 | 提升權限的部分結束後回到前景(`AllowSetForegroundWindow`;若前景仍被 msiexec 視窗佔住,改用 z-order + `AttachThreadInput`,再不行就閃爍工作列按鈕)。 | 已驗證(Win11) | +| 安裝精靈 | 提升權限的部分結束後回到前景(`AllowSetForegroundWindow`;若前景仍被 msiexec 視窗佔住,改用 z-order + `AttachThreadInput`,再不行就閃爍工作列按鈕並顯示通知)。提升權限的部分執行時,300 毫秒的計時器會讓精靈緊貼在安裝流程中取得前景的視窗(msiexec、consent、drvinst、pnputil、dpinst、提升權限的階段本身)後面,但不置頂;前景是無關程式的視窗時不干擾,log 只寫「有無關的視窗在前景」,不寫是誰的。背景步驟(下載、解壓縮、安裝、驅動程式步驟)結束、精靈接著等你點擊而別的視窗在前景時,工作列按鈕閃爍並顯示通知;下一個步驟馬上開始、視窗正在關閉、自動重新開機的那一頁都不通知。通知的系統匣圖示在 10 秒後、精靈被啟用時,以及精靈結束時移除。 | 已驗證(Win11) | | `dynafix.dll`(選購功能) | 選擇畫面還開著時讀出哪些選購功能按鈕是綠色(依位置、略過隱藏的 EB-150/EB-250),設定程式結束後寫入「存檔並離開」會寫的 `"-1"` 旗標並記錄,再重新啟動 DynaRun。只寫入明確讀到是綠色的按鈕;讀不準(或顏色不明確)時什麼都不寫,並在 log 說明。 | 已驗證(Win11;原生繁中 Win11 以 v1.2.2 的程式碼測試);XP 未測 | v1.2.2 在全新原生繁中 Win11 上的紀錄(由 `DynaRunFix-Setup.exe` 1.2.2 下載並安裝 DynaRun 3.26.0;選中文、S68、勾選大氣監測與空燃比分析儀, -`Setup_114.Exe` 一次 UAC,自動重新啟動;啟動畫面與主畫面都是 Microsoft JhengHei UI,狀態列在工作列上方): +`Setup_114.Exe` 一次 UAC(v1.2.2 的行為,v1.8.0 起沒有提示),自動重新啟動;啟動畫面與主畫面都是 Microsoft JhengHei UI,狀態列在工作列上方): (不附 DynaRun 視窗截圖。)結果:主畫面為中文,有大氣資訊與空燃比錶。 diff --git a/docs/MANUAL.md b/docs/MANUAL.md index e53a9d2..becddf9 100644 --- a/docs/MANUAL.md +++ b/docs/MANUAL.md @@ -84,6 +84,13 @@ whenever DynaRun starts its first-time setup helper (normally only on the very f ## What's new +v1.8.0 (released 2026-10-18): +- New: a version check of Dyna Pro's setup. 3.26.0 (the tested version) installs as before; another 3.x, or a version that cannot be read, shows a notice (tested only with 3.26.0, report problems at [GitHub issues](https://github.com/timliudev/DynaRunFix/issues)) and the install goes on; **DynaRun 4 or later** gets its own page that explains it may not need the fix and offers *Install original software only* (Dyna Pro's setup alone: no fix, no driver) next to *Install the fix anyway*. +- Changed: the *Check the installer* page shows the source, the MSI's version and manufacturer first, in a smaller font, and fits without scrolling; for a setup found on this computer, *Download it* takes the file from Dyna Pro's website instead. +- Changed: **the first start no longer asks for permission** (no UAC prompt) for Dyna Pro's configuration helper `Setup_.exe`: it only writes `HKCU\Software\DynaPro`, so it is started without elevation. +- Changed (setup wizard): while Dyna Pro's setup installs, the wizard stays directly behind msiexec's progress window (not always on top); after the restart the driver pages that wait for a click are kept on top of start-up windows (not while the driver installs, so Windows' warning stays visible); when a background step ends, the wizard is not in front and it now waits for a click, its taskbar button flashes and Windows shows a notification. +- Changed (security): the SHA-256 shown on the *Check the installer* page is handed to the elevated step, which does not run Dyna Pro's setup if its own copy has another SHA-256; the version and manufacturer read from the MSI are cleaned (control characters) and cut before they are shown or logged. + 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). @@ -119,9 +126,19 @@ All versions: [CHANGELOG.md](../CHANGELOG.md). [Dyna Pro's website](https://dynapro.co.uk/Software_Release.htm) (or uses a DynaRun setup zip or `Setup.msi` already in *Downloads*, on the desktop, in *Documents* or next to the installer), asks for the **setup password you got - from Dyna Pro**, shows a *Check the installer* page (where the file came from, its full path, size, SHA-256 and the MSI's version and manufacturer, in a box you can copy from), then Dyna Pro's license (read from the setup and shown by DynaRunFix itself) and installs DynaRun V3 and the fix only after you accept it (if the - license cannot be read from the setup, you must confirm that you accept Dyna Pro's license terms instead). + from Dyna Pro**, shows a *Check the installer* page (a box you can copy from, in a smaller font so that all of it fits without scrolling: first where the file came from, the MSI's version and manufacturer, then the file's full path, size and SHA-256; for a setup found on this computer the page also has *Download it*, which gets the file from Dyna Pro's website instead), then Dyna Pro's license (read from the setup and shown by DynaRunFix itself) and installs DynaRun V3 and the fix only after you accept it (if the + license cannot be read from the setup, you must confirm that you accept Dyna Pro's license terms instead). The SHA-256 on that page is also what the install is held to: the elevated step (`/full`, `/msi-only`, which take it as `/msi-sha256 <64 hex digits>` and are refused without it) copies the MSI into an administrators-only folder, computes the SHA-256 of that copy and stops with an error, running nothing, if it is not the one you were shown (the file in your folder could have been replaced after the page). A setup whose SHA-256 could not be read (shown as `?`) is not installed. If DynaRun V3 is already installed, it only installs the fix. If DynaRun V3 is installed in a folder the setup does not find, the first page has *Installed elsewhere...* to point it at `DynaRun V3.exe`. Windows asks once for permission: click **Yes**. That one prompt is for the whole install; when the driver is ticked too it covers the driver's preparation as well (the driver pages come before the install, see below). + **Version check.** The MSI's ProductVersion is classified in one place (`dynarun_ver_class` in `src\package.c`; the UpgradeCode and product name check stays). Exactly 3.26.0, the version the fix was tested with: nothing changes. + Another 3.x (or older): a notice on the *Check the installer* and license pages says it was tested only with 3.26.0, that there may be additional problems, and where to report them (); the install goes on. + A version that cannot be read: a notice too, the install goes on. Version 4 or later (possibly no longer a Visual Basic 6 program, perhaps native Windows 10/11, so it may not need the fix): after *Check the installer* a page says so and + suggests installing Dyna Pro's software alone first and trying it. *Install the fix anyway* (right-most) stays disabled until *I understand that DynaRunFix has not been tested with this version* is ticked. *Install original software only* + goes on to the license page and then installs Dyna Pro's setup alone (one administrator prompt, the same checked copy of the MSI): no fix, no change of shortcuts, no taskbar pin, no start at sign-in, and **no USB driver** + (the driver step belongs to the fix: it exists for V3's unsigned FTDI driver and was never checked with V4; the driver box of the first page is ignored). Its last page, *Dyna Pro's software is installed*, has *Close* only, which just closes the wizard (it does not start DynaRun). If V4 then shows the old problems, run this setup again: it finds the installed DynaRun and offers the fix. + The check box of the V4 page and of the license page starts unticked every time the page is shown, also when you come back to it after declining the administrator prompt. + **Windows and notifications.** While the elevated install runs, the wizard keeps itself directly behind the window of the install that has the foreground (the UAC prompt, Dyna Pro's progress window "Please wait while Windows configures ...", the driver warning), above all other windows, + but never always-on-top: you can switch to another program, and a program you switched to is left alone (the log says only that an unrelated window has the foreground, not whose it is). **Alert.** When a background step (download, extraction, install, driver step) has ended and the wizard now waits for your click, and it is not the window in front, its taskbar button flashes + and Windows shows a notification (a temporary tray icon; it goes after 10 seconds, when you return, or when the wizard ends). Nothing happens when the wizard starts the next step by itself (for example the extraction right after the download), while its window is closing, on the page that restarts the computer by itself, or when the wizard is in front; after the restart the sign-in start calls you once, when the wizard opens. After the restart the driver pages that wait for your click (*Last step*, *7 was not pressed*) are kept on top of the programs that start with Windows; they stop being on top when you click *Install*, so that Windows' red warning is not hidden. A download that is interrupted is retried by itself (after 1, 2, 4, 8 and 15 seconds; the count starts over whenever data arrived), the page shows the first error of the failed attempts, the *Pause* button stops it cleanly and *Resume* (or, after an error, *Continue download*) carries on from the part that is already there; *Open website* and *Choose file...* remain as ways around it. When Windows says the computer is not connected to a @@ -134,8 +151,8 @@ All versions: [CHANGELOG.md](../CHANGELOG.md). whether a newer DynaRun V3 is published (the page names the version next to the download link: 3.26.0 today; if it ever stops naming one, the zip on the site is asked for its first byte, nothing more is downloaded, and its ETag / length / date are compared with those of the last completed download) and, if so, shows a line and *Download new version*. It is only a hint: nothing is downloaded or installed unless you click it, a failed check says nothing, and each result is one line in the setup's log (`%TEMP%\DynaRunFix-setup.log`; `setup.log` in the install folder when run as administrator). -3. Start DynaRun with its usual **DynaRun V3** icon. On the very first start (system selection) Windows asks - once more for permission for Dyna Pro's configuration helper: click **Yes**; DynaRun then restarts by itself. +3. Start DynaRun with its usual **DynaRun V3** icon. On the very first start (system selection) pick your model and click OK; DynaRun then restarts by itself. + Windows no longer asks for permission here (before v1.8.0 it asked once for Dyna Pro's configuration helper). First time on this PC? The Dyna Pro USB driver is needed too. On Windows 10/11 the setup wizard can install it (the check box on its first page; it downloads the driver, restarts the computer once, you press **7** in the menu that appears and choose *Install this driver @@ -202,7 +219,7 @@ Windows XP, 7, 10 and 11 are supported (the driver option needs Windows 10 or 11 - `/uninstall`: uninstalls (also what *Programs and Features* runs); with `/quiet` it asks nothing and removes only the fix (DynaRun V3 stays), a running DynaRun is closed only if `/close` is also given, and files that are in use are removed at the next restart; `/notaskbar`, `/noautostart`: leave out the taskbar pin and the start with Windows (below); `/keep`: leave the pin, the start at sign-in and the desktop shortcut as they are (what an update uses). - `/driver`: opens the driver pages of the wizard directly (download, restart, install), without installing DynaRun or the fix; this is what the Start menu shortcut runs. - `/driver-cancel`: takes back a started driver installation (the scheduled task, the one-time boot option and the BitLocker suspension); it installs and removes no driver. It must be run from an administrator command prompt: otherwise it exits with code 1 and does nothing. -- Internal (do not run them by hand): `/driver-continue` (what the scheduled task `DynaRunFix-DriverContinue` starts after the restart), `/driver-prep`, `/driver-install`, `/machine`, `/full`, `/unmachine`, `/checkupdate`, `/update`, `/offer`. +- Internal (do not run them by hand): `/driver-continue` (what the scheduled task `DynaRunFix-DriverContinue` starts after the restart), `/driver-prep`, `/driver-install`, `/machine`, `/full`, `/msi-only`, `/unmachine`, `/checkupdate`, `/update`, `/offer`. `/full` and `/msi-only` also need `/msi-sha256 <64 hexadecimal digits>` after the MSI path (the SHA-256 the wizard showed); without a valid value they exit with code 1 and run nothing. Exit codes of the setup: @@ -309,7 +326,8 @@ the token was non-elevated, or the compatibility setting was ignored, a one-time (English or Traditional Chinese; answer *No* to *Show this notice again next time?* to switch it off). If a DynaRun that is already running was started with more rights than the launcher has, the launcher asks you to close it and start it again through DynaRunFix. The compatibility variable `__COMPAT_LAYER=RunAsInvoker` that the launcher sets for DynaRun is removed again by `dynafix.dll` inside DynaRun's -process, so that Dyna Pro's configuration helper, which DynaRun starts on the first start, still asks for permission as it normally does. +process, so that DynaRun's other child processes are not changed. The one exception is Dyna Pro's configuration helper `Setup_.exe` of the first start: `dynafix.dll` starts it with `RunAsInvoker` +for that one start only (the variable is set for the call and put back), so Windows' installer detection does not raise a permission prompt for it; it only writes `HKCU\Software\DynaPro`. Do not run `DynaRun V3.exe` itself as administrator, see [below](#run-as-administrator-is-not-needed). ### Installing the Dyna Pro USB driver @@ -356,8 +374,8 @@ Check `%TEMP%\dynafix.log`: it should contain `patched THBRes25.dll import of Po and before installing it checks that the file identifies itself as the DynaRun V3 setup (Windows Installer upgrade code and product name). Dyna Pro does not sign its setup and does not publish checksums, so **DynaRunFix cannot prove that the file is exactly what Dyna Pro - released**. Use only files you got from Dyna Pro's official site. On its *Check the installer* page (just before the license page) the installer shows the file's full - path, size, SHA-256 and the MSI's version and manufacturer, so you can compare them with whatever Dyna Pro gives you; it does + released**. Use only files you got from Dyna Pro's official site. On its *Check the installer* page (just before the license page) the installer shows where the file came from, + the MSI's version and manufacturer, and the file's full path, size and SHA-256, so you can compare them with whatever Dyna Pro gives you; it does not block anything based on them. - **DynaRunFix's own updates:** the installer is checked against the SHA-256 that GitHub lists for the release file. Each release's notes include `SHA256SUMS.txt` (also attached to the release), and the release files carry a build provenance @@ -375,14 +393,14 @@ Check `%TEMP%\dynafix.log`: it should contain `patched THBRes25.dll import of Po ## First-time setup On its first start DynaRun asks for the language, then for the dyno model and optional features -("Dyna Pro系統選擇"). After **OK** Windows asks once for permission for `Setup_.exe`, Dyna Pro's per-model -configuration helper: click **Yes**. DynaRun then restarts by itself with the chosen model, language and optional +("Dyna Pro系統選擇"). After **OK** DynaRun starts `Setup_.exe`, Dyna Pro's per-model configuration helper; Windows' installer detection would ask for +permission for it, so `dynafix.dll` starts it without elevation (since v1.8.0 there is no prompt; the helper only writes `HKCU\Software\DynaPro`, see [FIRST_START](FIRST_START.md)). DynaRun then restarts by itself with the chosen model, language and optional features (climate monitor, water cooler, cooling fans, AFR analyser). Without the fix DynaRun 3.26.0 hangs at this point on every Windows version, XP included: after the helper has run, the selection window stays and one CPU core runs at 100 %. After a forced restart the language is English and the optional features are off (the helper leaves the language value in the registry as `English` and the features' enable flags as `0`). `dynafix.dll` wraps DynaRun's `ShellExecuteA` call in memory: -it waits for the helper, puts the chosen language back, switches on the picked features with the same values +it starts the helper without elevation (`__COMPAT_LAYER` gets `RunAsInvoker` for that one call; the log says `setup helper started without elevation (RunAsInvoker)`), waits for it, puts the chosen language back, switches on the picked features with the same values *Engineering mode → System configuration → Save and exit* writes, and restarts DynaRun through `DynaRunFix.exe /restart `. Each step is logged to `%TEMP%\dynafix.log` (the log never contains module or import addresses, and a symbolic link or junction in its place is not followed; every line starts with the date and time; a line is at most 1000 characters, longer ones end in `...`). `dynafix.dll` and the launcher can write to it at the same time: they take turns through a named lock (`Local\DynaRunFix.dynafix.log`), so lines are never mixed up, but their lines are interleaved. The size is checked only when the dll or the launcher starts: above 10 MB the file is cut to its newest 8 MB, from a full line on. @@ -675,7 +693,7 @@ different foreign hook after the chain is set is left unpatched and logged rathe kernel32/kernelbase/gdi32/gdi32full/user32 and `api-ms-win-*` API sets match; a module without an import name table falls back to the old address match. `dynafix.log` shows `patched ... (by name)`, `... already held a hook: chained onto it`, `... not patched: ...` for each slot that could not be patched, and a per-module `import patch []: N patched, M failed` line. -`tests\build-shexec-tests.cmd` builds and runs the unit tests (the first-start helper path matching, `src\setup_match.h`; the `DYNAFIX_*` switch and `dynapro.dll` name parsing, `src\dfutil.h`; parts of the launcher); it exits with 0 if all pass. `tests\build-tests.cmd` builds the log test harness and +`tests\build-shexec-tests.cmd` builds and runs the unit tests (the first-start helper path matching, `src\setup_match.h`; the `DYNAFIX_*` switch and `dynapro.dll` name parsing and the `RunAsInvoker` layer word, `src\dfutil.h`; parts of the launcher); it exits with 0 if all pass. `tests\build-tests.cmd` builds the log test harness and `tests\build-hardening-tests.cmd` builds and runs the installer hardening tests, and `tests\build-driver-tests.cmd` the driver tests (see `tests\README.md`). Diagnostic `DYNAFIX_STATS=1`: every 5 s `dynafix.log` gets one `stats(5s)` line (only when something happened) with the form `WM_SIZE` count, how many were duplicates, how many THBResize `0x591` messages were passed on or skipped and how many forms were @@ -771,6 +789,13 @@ DynaRun 執行中不會裁剪,所以單次執行期間檔案可能超過 10 MB; log 只記模組與 API 名稱、不含模組或匯入位址(可能出現視窗與 GDI 控制代碼的值),寫入時不跟隨符號連結或連接點,但可能含有檔案路徑與視窗標題,貼到網路上之前請先檢查。同一個 dll 也修正[首次設定](#首次設定)與 [OneDrive 檔案打不開](#部分-dpr-打不開file-run-properties-全空沒有曲線)的問題。 ### 更新內容 +v1.8.0(2026-10-18 發行): +- 新增:檢查 Dyna Pro 安裝檔的版本。3.26.0(測試過的版本)照舊安裝;其他 3.x 或讀不到版本時會顯示提示(只在 3.26.0 測試過,有問題請到 [GitHub issues](https://github.com/timliudev/DynaRunFix/issues) 回報),安裝繼續;**DynaRun 4 以上**有獨立的頁面,說明它可能不需要修正,並提供「僅安裝原版軟體」(只安裝 Dyna Pro 的安裝檔:不裝修正、不裝驅動程式)與「仍要安裝修正」。 +- 變更:「確認安裝檔」頁先顯示來源、MSI 的版本與製造商,字型較小、不需捲動就能看完;在這台電腦上找到的安裝檔,可按「改從官網下載」改用 Dyna Pro 官網的檔案。 +- 變更:**第一次啟動不再詢問是否允許變更**(沒有 UAC 提示):Dyna Pro 的設定程式 `Setup_<編號>.exe` 只寫入 `HKCU\Software\DynaPro`,所以改為不提升權限啟動。 +- 變更(安裝精靈):Dyna Pro 的安裝程式執行時,精靈會緊貼在 msiexec 進度視窗後面(不是永遠在最上層);重新開機後等待點擊的驅動程式頁面會蓋過開機啟動的視窗(驅動程式安裝中則不會,Windows 的警告才看得到);背景步驟結束、精靈不在最前面而且現在等你點擊時,工作列按鈕會閃爍,並顯示 Windows 通知。 +- 變更(安全性):「確認安裝檔」頁顯示的 SHA-256 會傳給提升權限的步驟,該步驟自己的副本 SHA-256 不同時就不會執行 Dyna Pro 的安裝檔;從 MSI 讀到的版本與製造商會先清掉控制字元並截斷,才顯示或寫入 log。 + v1.7.2(2026-10-18 發行): - 修正:%APPDATA% 路徑非常長(約 200 個字元以上)時,捷徑與工作列釘選程式碼不再造成緩衝區溢位;放不下的資料夾會被略過。 - 修正:安裝精靈會忽略不是它自己的工作執行緒送出的「工作完成」視窗訊息(先前可能使精靈當機)。另外:「關閉 DynaRun 並安裝」只對一次安裝有效,驅動程式準備失敗時會移除已放置的驅動程式檔。其餘是內部原始碼整理,不打算改變行為;見[更新紀錄](../CHANGELOG.md)。 @@ -802,14 +827,24 @@ v1.6.0(2026-10-17 發行): 2. 照畫面操作。還沒安裝 DynaRun V3 時,會從 [Dyna Pro 官網](https://dynapro.co.uk/Software_Release.htm)下載安裝檔 (「下載」、桌面、「文件」或安裝程式旁邊已經有 DynaRun 安裝檔 zip 或 `Setup.msi` 就直接用),請你輸入 **Dyna Pro 給的安裝密碼**, - 先顯示「確認安裝檔」頁(檔案的來源、完整路徑、大小、SHA-256,以及 MSI 的版本與製造商,放在可以複製文字的框裡),再顯示 Dyna Pro 的授權合約(取自安裝檔,由 DynaRunFix 自己顯示),你接受後才安裝 DynaRun V3 和修正(安裝檔裡的合約讀不出來時,必須勾選同意 Dyna Pro 的授權條款才能繼續)。已經裝好 DynaRun V3 時,只會安裝修正。DynaRun V3 裝在安裝程式找不到的資料夾時,第一頁有「已裝在其他位置…」可以指定 `DynaRun V3.exe`。 + 先顯示「確認安裝檔」頁(可以複製文字的框,字型較小、不需捲動就能看完:先是檔案的來源、MSI 的版本與製造商,再是檔案的完整路徑、大小與 SHA-256;在這台電腦上找到的安裝檔,頁面另有「改從官網下載」,改用 Dyna Pro 官網的檔案),再顯示 Dyna Pro 的授權合約(取自安裝檔,由 DynaRunFix 自己顯示),你接受後才安裝 DynaRun V3 和修正(安裝檔裡的合約讀不出來時,必須勾選同意 Dyna Pro 的授權條款才能繼續)。該頁顯示的 SHA-256 也是安裝時要符合的值:提升權限的步驟(`/full`、`/msi-only`,以 `/msi-sha256 <64 個十六進位字元>` 取得它,沒有就拒絕執行)把 MSI 複製到只有系統管理員能用的資料夾,計算那份副本的 SHA-256,和你看到的不同就顯示錯誤並結束,什麼都不執行(你資料夾裡的檔案可能在顯示該頁之後被換掉)。讀不到 SHA-256(顯示為 `?`)的安裝檔不會被安裝。已經裝好 DynaRun V3 時,只會安裝修正。DynaRun V3 裝在安裝程式找不到的資料夾時,第一頁有「已裝在其他位置…」可以指定 `DynaRun V3.exe`。 Windows 會詢問一次是否允許變更,請按 **是**。這是整個安裝只問的一次;同時勾選驅動程式時,它也涵蓋驅動程式的準備(驅動程式的頁面會排在安裝之前,見下方)。 + **版本檢查。** 安裝檔 MSI 的 ProductVersion 只在一個地方分類(`src\package.c` 的 `dynarun_ver_class`;升級代碼與產品名稱的檢查照舊)。剛好是 3.26.0(修正測試過的版本):沒有任何變化。 + 其他 3.x(或更舊):「確認安裝檔」頁與授權合約頁會顯示提示——只在 3.26.0 測試過、可能有額外問題、請到 回報;安裝繼續。 + 讀不到版本:同樣顯示提示,安裝繼續。4 以上(可能已不是 Visual Basic 6 程式,也許是原生 Windows 10/11,所以可能不需要修正):「確認安裝檔」之後會有一頁說明,並建議先單獨安裝 Dyna Pro 的軟體試用。 + 必須勾選「我了解 DynaRunFix 未在此版本測試過」,最右邊的「仍要安裝修正」才能按。「僅安裝原版軟體」會先到授權合約頁,再只安裝 Dyna Pro 的安裝檔(一次系統管理員確認,同樣是檢查過的 MSI 副本): + 不裝修正、不改捷徑、不釘選工作列、不設開機自動啟動,也**不裝 USB 驅動程式**(驅動程式步驟屬於修正:它是為 V3 未簽章的 FTDI 驅動程式而設,沒有以 V4 檢查過;第一頁的驅動程式核取方塊會被忽略)。 + 它的最後一頁「Dyna Pro 的軟體已安裝」只有「關閉」,按下去只會關閉精靈(不會啟動 DynaRun)。之後如果 V4 出現舊的問題,再執行一次本程式:它會找到已安裝的 DynaRun 並提供安裝修正。 + V4 頁與授權合約頁的核取方塊,每次顯示該頁時都是未勾選,從被拒絕的系統管理員確認回到該頁時也一樣。 + **視窗順序與通知。** 提升權限的安裝進行時,精靈會讓自己緊貼在目前取得前景、屬於安裝流程的視窗後面(UAC 提示、Dyna Pro 的進度視窗「Please wait while Windows configures ...」、驅動程式警告),並在其他視窗之上, + 但不是永遠在最上層:你可以切換到別的程式,切過去的程式不會被干擾。背景步驟(下載、解壓縮、安裝、驅動程式步驟)結束、精靈現在等你點擊,而它不是最前面的視窗時,工作列按鈕會閃爍, + 並顯示 Windows 通知(暫時的系統匣圖示;10 秒後、你回來時,或精靈結束時消失)。精靈自己接著做下一個步驟時(例如下載後馬上解壓縮)、視窗正在關閉時、自動重新開機的那一頁,以及精靈在最前面時,都不會通知;重新開機後登入時的繼續,會在精靈開啟時通知一次。切過去的程式不會被干擾(log 只會寫「有無關的視窗在前景」,不寫是誰的)。重新開機後等待你點擊的驅動程式頁面(「最後一步」、「沒有按 7」)會蓋過開機啟動的程式;按下「安裝」後就不再置頂,Windows 的紅色警告才不會被擋住。 下載中斷時會自動重試(依序等 1、2、4、8、15 秒;只要有資料進來就重新計數),頁面會顯示失敗過程中的第一個錯誤;「暫停」會乾淨地停止, 「繼續」(錯誤之後是「繼續下載」)會從已經下載的部分接續;「開啟官網」與「選擇檔案…」仍然可用。Windows 說電腦完全沒有連上網路時,錯誤頁會立刻出現(不用等重試);主機不回應時 10 秒就放棄。驅動程式走同樣的頁面(先找電腦上現有的,再下載);它的「選擇檔案…」可以選 zip,或已解開它的資料夾裡的 ftdibus.inf。 精靈第一頁最多有三個核取方塊:釘選到工作列、開機時自動啟動 DynaRun,以及(Windows 10/11 且尚未安裝驅動程式時)**同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)**(見下方)。 DynaRun V3 和這個版本的修正都已安裝、只缺驅動程式時,第一頁只提供驅動程式(以及「重新安裝修正…」)。DynaRun V3 已安裝且電腦有網路時,第一頁還會在背景詢問 Dyna Pro 軟體頁面有沒有新版 DynaRun V3(那一頁在下載連結旁寫著版本,目前是 3.26.0;萬一不再寫版本,就向網站上的 zip 要第一個位元組(不會再下載其他內容),把它的 ETag / 長度 / 日期和上次完成的下載比對),有的話會顯示一行說明與「下載新版」。這只是提示:不按就不會下載或安裝任何東西,檢查失敗也不說什麼,每個結果在安裝程式的記錄檔(`%TEMP%\DynaRunFix-setup.log`;以系統管理員身分執行時是安裝資料夾裡的 `setup.log`)裡是一行。 -3. 以後照常點 **DynaRun V3** 圖示啟動。第一次啟動(選擇系統)時,Windows 會再問一次是否允許 Dyna Pro 的設定程式變更, - 請按 **是**,DynaRun 會自己重新啟動。 +3. 以後照常點 **DynaRun V3** 圖示啟動。第一次啟動(選擇系統)時,選好機型按 OK,DynaRun 會自己重新啟動。 + Windows 不會再詢問是否允許變更(v1.8.0 之前會為 Dyna Pro 的設定程式問一次)。 這台電腦第一次連接 Dyna Pro?還需要 Dyna Pro USB 驅動程式。Windows 10/11 上可以由安裝精靈安裝(精靈第一頁的核取方塊:它會下載驅動程式、重新開機一次, 你在出現的選單按 **7**、並兩次選擇「仍然安裝此驅動程式軟體」,登入後它會自動繼續);之後也可以用開始功能表捷徑「安裝 Dyna Pro USB 驅動程式」或 @@ -853,7 +888,7 @@ DynaRun 的 exe 與程式內容不會被改動(捷徑檔會備份並還原; - `/uninstall`:解除安裝(「程式和功能」執行的也是它);加上 `/quiet` 時不問任何問題、只移除修正(DynaRun V3 保留),執行中的 DynaRun 只有同時加 `/close` 才會被關閉,使用中的檔案在下次重新啟動時移除;`/notaskbar`、`/noautostart`:不釘選到工作列、不在開機時啟動(見下方);`/keep`:讓釘選、開機自動啟動和桌面捷徑維持原樣(更新時使用)。 - `/driver`:直接開啟精靈的驅動程式頁面(下載、重新開機、安裝),不安裝 DynaRun 或修正;開始功能表捷徑執行的就是它。 - `/driver-cancel`:撤回已開始的驅動程式安裝(排程工作、一次性開機選項與 BitLocker 暫停);它不安裝也不移除驅動程式。必須在以系統管理員身分開啟的命令提示字元中執行,否則以結束代碼 1 結束,什麼都不做。 -- 內部使用(請勿手動執行):`/driver-continue`(重新開機後排程工作 `DynaRunFix-DriverContinue` 啟動的)、`/driver-prep`、`/driver-install`、`/machine`、`/full`、`/unmachine`、`/checkupdate`、`/update`、`/offer`。 +- 內部使用(請勿手動執行):`/driver-continue`(重新開機後排程工作 `DynaRunFix-DriverContinue` 啟動的)、`/driver-prep`、`/driver-install`、`/machine`、`/full`、`/msi-only`、`/unmachine`、`/checkupdate`、`/update`、`/offer`。`/full` 與 `/msi-only` 在 MSI 路徑之後還需要 `/msi-sha256 <64 個十六進位字元>`(精靈顯示過的 SHA-256);沒有有效值就以結束代碼 1 結束,什麼都不執行。 安裝程式的結束代碼: @@ -912,7 +947,8 @@ Windows Installer 自己做不到,會出現錯誤 1305。不會改變檔案內 殼層的 token 本身已提升時(UAC 關閉,或使用內建的 Administrator:平常的桌面就是完整系統管理員),啟動器沿用它繼續執行並寫入 `dynafix.log`,沒有什麼需要放掉;此時 `dynafix.dll` 會略過圖表語言的轉向(見下方)。 只有完全取不到殼層 token 時,啟動器才會顯示訊息、不啟動 DynaRun。token 是一般權限,或相容性設定被忽略時,會顯示一次性通知(英文或繁體中文;對「下次還要顯示這個通知嗎?」回答「否」即可關閉)。 如果已經在執行的 DynaRun 是以高於啟動器的權限啟動的,啟動器會請你關閉它,再透過 DynaRunFix 重新啟動。 -啟動器為 DynaRun 設的相容性變數 `__COMPAT_LAYER=RunAsInvoker`,會由 `dynafix.dll` 在 DynaRun 行程內再清除,使首次啟動時 DynaRun 所啟動的 Dyna Pro 設定程式仍照常詢問是否允許變更。 +啟動器為 DynaRun 設的相容性變數 `__COMPAT_LAYER=RunAsInvoker`,會由 `dynafix.dll` 在 DynaRun 行程內再清除,DynaRun 的其他子行程不受影響。唯一的例外是首次啟動的 Dyna Pro 設定程式 `Setup_<編號>.exe`:`dynafix.dll` 只在啟動它的那一次呼叫加上 `RunAsInvoker` +(呼叫前設定、呼叫後立刻還原),Windows 的安裝程式偵測就不會對它要求權限;它只寫入 `HKCU\Software\DynaPro`。 請不要直接以系統管理員身分執行 `DynaRun V3.exe`,見[下方](#不需要以系統管理員身分執行)。 #### 安裝 Dyna Pro USB 驅動程式 @@ -953,8 +989,8 @@ Releases 的 zip 內含同樣的檔案(另外還有 `le\`、manifest 與 `LICE - **DynaRun V3 安裝檔(Dyna Pro 的檔案):** DynaRunFix 只透過 HTTPS 從 `dynapro.co.uk` 下載(憑證無效時會拒絕;每一次重新導向都會再檢查是否仍是 HTTPS 與 `dynapro.co.uk`,導向到其他位置或明文 HTTP 都會被拒絕), 使用 TLS 1.2 以上(Windows 7 需要更新 KB3140245),大小上限 300 MB, 安裝前會檢查檔案自稱是 DynaRun V3 的安裝檔(Windows Installer 的升級代碼與產品名稱)。Dyna Pro 的安裝檔沒有數位簽章,也沒有公布雜湊值, - 所以 **DynaRunFix 無法證明檔案與 Dyna Pro 發布的完全一致**。請只使用從 Dyna Pro 官方網站取得的檔案。「確認安裝檔」頁(授權合約頁之前)會顯示檔案的完整路徑、大小、SHA-256, - 以及 MSI 的版本與製造商,讓你與 Dyna Pro 提供的資訊自行比對;安裝程式不會依此阻擋任何事。 + 所以 **DynaRunFix 無法證明檔案與 Dyna Pro 發布的完全一致**。請只使用從 Dyna Pro 官方網站取得的檔案。「確認安裝檔」頁(授權合約頁之前)會顯示檔案的來源、MSI 的版本與製造商, + 以及檔案的完整路徑、大小與 SHA-256,讓你與 Dyna Pro 提供的資訊自行比對;安裝程式不會依此阻擋任何事。 - **DynaRunFix 自己的更新:** 安裝檔會用 GitHub 為該版本檔案列出的 SHA-256 核對。每個版本的說明都附有 `SHA256SUMS.txt`(也作為檔案附在版本上), 版本檔案並附有建置來源證明(由哪個儲存庫、哪個工作流程建置)。自行檢查下載的檔案: `gh attestation verify DynaRunFix-Setup.exe --repo timliudev/DynaRunFix`。這只能顯示檔案建置自何處,不代表檔案安全;安裝程式沒有程式碼簽章(見上方 SmartScreen 說明)。 @@ -962,8 +998,8 @@ Releases 的 zip 內含同樣的檔案(另外還有 `le\`、manifest 與 `LICE 只會解開 Windows 要安裝的 18 個檔案,每個檔案的目的路徑都由程式自己的表格決定(不採用 zip 內的項目名稱),放進只有系統管理員能更改的新資料夾,並有大小上限;路徑上若有連結(link)會被拒絕。複製到那裡之後才比對它們的 SHA-256 與安裝程式內建的值,安裝的就是這些副本(安裝前再檢查一次)。檔案不同的套件,只有在警告頁上由你明確按「仍要安裝」後才會安裝,而且只對那一個 zip、那一次安裝有效。驅動程式本身不是 DynaRunFix 的:它是 Dyna Pro 的套件,DynaRunFix 不簽署也不更改它。 ### 首次設定 -首次啟動時 DynaRun 會先問語言,再顯示「Dyna Pro系統選擇」(機型與選購功能)。按 **OK** 後 Windows 會詢問一次是否允許 -`Setup_<編號>.exe`(Dyna Pro 各機型的設定程式)變更,請按 **是**。之後 DynaRun 會自動以選好的機型、語言和選購功能 +首次啟動時 DynaRun 會先問語言,再顯示「Dyna Pro系統選擇」(機型與選購功能)。按 **OK** 後 DynaRun 會啟動 +`Setup_<編號>.exe`(Dyna Pro 各機型的設定程式);Windows 的安裝程式偵測本來會為它詢問是否允許變更,所以 `dynafix.dll` 以不提升權限的方式啟動它(v1.8.0 起沒有提示;它只寫入 `HKCU\Software\DynaPro`,見 [FIRST_START](FIRST_START.md))。之後 DynaRun 會自動以選好的機型、語言和選購功能 (大氣監測、水冷、冷卻風扇、空燃比分析儀)重新啟動。 沒有修正時,DynaRun 3.26.0 在任何 Windows 版本(包括 XP)都會卡在這裡:設定程式執行完後,選擇視窗不消失、CPU 一核 100%。 @@ -1170,7 +1206,7 @@ dynafix 所有的匯入掛鉤(字型、文字度量、檔案屬性、`GetProcAdd 來自 kernel32/kernelbase/gdi32/gdi32full/user32 與 `api-ms-win-*` API 集合的匯入都會比對;沒有匯入名稱表的模組退回舊的位址比對。 `dynafix.log` 會出現 `patched ... (by name)`、`... already held a hook: chained onto it`、無法修補的欄位的 `... not patched: ...`,以及每個模組一行 `import patch [
]: N patched, M failed`。 -`tests\build-shexec-tests.cmd` 會建置並執行單元測試(首次啟動設定程式的路徑比對 `src\setup_match.h`、`DYNAFIX_*` 開關與 `dynapro.dll` 檔名的解析 `src\dfutil.h`、啟動器的部分功能),全數通過時結束碼為 0。`tests\build-tests.cmd` 建置 log 測試程式,`tests\build-hardening-tests.cmd` 建置並執行安裝程式強化測試,`tests\build-driver-tests.cmd` 建置並執行驅動程式測試(見 `tests\README.md`)。 +`tests\build-shexec-tests.cmd` 會建置並執行單元測試(首次啟動設定程式的路徑比對 `src\setup_match.h`、`DYNAFIX_*` 開關、`dynapro.dll` 檔名的解析與 `RunAsInvoker` 相容層字詞 `src\dfutil.h`、啟動器的部分功能),全數通過時結束碼為 0。`tests\build-tests.cmd` 建置 log 測試程式,`tests\build-hardening-tests.cmd` 建置並執行安裝程式強化測試,`tests\build-driver-tests.cmd` 建置並執行驅動程式測試(見 `tests\README.md`)。 診斷用的 `DYNAFIX_STATS=1`:每 5 秒 `dynafix.log` 會有一行 `stats(5s)`(只在有事發生時),內容是表單 `WM_SIZE` 的次數、其中重複的次數、THBResize `0x591` 訊息被放行或略過的次數,以及表單被顯示或隱藏的次數。 啟動之後這些行就停止,表示防護已經結束了隱藏/顯示迴圈。 diff --git a/src/dfutil.h b/src/dfutil.h index 9bb82ec..3d38330 100644 --- a/src/dfutil.h +++ b/src/dfutil.h @@ -37,4 +37,36 @@ static int df_is_dynapro(const char *mod, DWORD n, DWORD size) return !lstrcmpiA(mod + n - 11, name); } +// The value for __COMPAT_LAYER that makes a program started with it run as the invoking user (no UAC prompt, also for a program that Windows' installer +// detection or a manifest would elevate): `old` (the current value, NULL or "" when the variable is not set) with the layer "RunAsInvoker" added as a word, not +// again when it is there already (any case). The words are separated by blanks (spaces and tabs). Returns the length written to out (cch characters with the terminator), or 0 when it does not fit. +// No CRT in dynafix.dll: stop newer MSVC from turning the copy loop into a memcpy call. +#pragma optimize("", off) +static int df_compat_with_invoker(const char *old, char *out, int cch) +{ + static const char add[] = "RunAsInvoker"; + const char *p = old ? old : "", *q; int n = 0, i, has = 0; + for (q = p; *q; ) { + const char *e; int len; + while (*q == ' ' || *q == '\t') q++; + if (!*q) break; + for (e = q; *e && *e != ' ' && *e != '\t'; e++); + len = (int)(e - q); + if (len == 12) { + int c, eq = 1; + for (c = 0; c < 12; c++) { char x = q[c], y = add[c]; if (x >= 'A' && x <= 'Z') x += 32; if (y >= 'A' && y <= 'Z') y += 32; if (x != y) eq = 0; } + if (eq) has = 1; + } + q = e; + } + for (i = 0; p[i]; i++) { if (n + 1 >= cch) return 0; out[n++] = p[i]; } + if (!has) { + if (n && out[n - 1] != ' ' && out[n - 1] != '\t') { if (n + 1 >= cch) return 0; out[n++] = ' '; } + for (i = 0; add[i]; i++) { if (n + 1 >= cch) return 0; out[n++] = add[i]; } + } + out[n] = 0; + return n; +} +#pragma optimize("", on) + #endif diff --git a/src/drf_app.h b/src/drf_app.h index 6faa92a..2eafd65 100644 --- a/src/drf_app.h +++ b/src/drf_app.h @@ -29,9 +29,10 @@ typedef struct { 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 + const WCHAR *msisha; // /msi-sha256 <64 hex digits> from argv[4] on: the SHA-256 the user saw for the setup's MSI (the last valid one), else 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 +typedef struct { const WCHAR *name; int min_args; verb_fn run; BOOL needs_msi_sha; } verb; // argv[1] (any case) is name and argc >= min_args; needs_msi_sha: runs only with a valid /msi-sha256 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)) @@ -52,11 +53,12 @@ BOOL fix_current(void); // the fix is installed and is the 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 +int install_all(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *msi_sha); // runs the DynaRun MSI, then install_fix's work. RC_OK, RC_CANCEL. msi_sha: the SHA-256 (64 hex digits) the user saw for msi; the elevated stage checks its copy against it +int install_msi_only(const install_opts *o, const WCHAR *msi, const WCHAR *msi_sha); // Dyna Pro's MSI alone (a DynaRun that is not the tested V3): no fix, no shortcuts, no driver. RC_OK, RC_CANCEL, RC_* // 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); +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *msi_sha, 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) diff --git a/src/drf_package.h b/src/drf_package.h index 89cd07a..4adb134 100644 --- a/src/drf_package.h +++ b/src/drf_package.h @@ -54,8 +54,12 @@ int pkg_extract_checked(const zipent *e, const char *pw, const WCHAR *out, DWORD 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) +void msi_info(const WCHAR *msi, WCHAR *version, int vcch, WCHAR *manufacturer, int mcch); // ProductVersion / Manufacturer (Windows Installer API, read-only; "" when absent), each cut to its buffer, control characters as spaces (msi_text) char *msi_license_rtf(const WCHAR *msi); // RTF of the setup's license page (heap) or NULL +// The DynaRun V3 version the fix was tested with, and how a ProductVersion stands to it (the wizard's notices and the V4 page follow from the class). +#define DYNARUN_TESTED_VERSION L"3.26.0" +enum { VER_UNKNOWN, VER_TESTED, VER_UNTESTED, VER_V4PLUS }; +int dynarun_ver_class(const WCHAR *ver); // VER_UNKNOWN: not readable; VER_TESTED: the tested version; VER_UNTESTED: another version below 4; VER_V4PLUS: 4 or later 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 diff --git a/src/drf_package_internal.h b/src/drf_package_internal.h index 09ce88e..7a6cbf4 100644 --- a/src/drf_package_internal.h +++ b/src/drf_package_internal.h @@ -13,4 +13,5 @@ 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); +void msi_text(const char *s, WCHAR *out, int cch); // an MSI property value as screen / log text: NUL-terminated in out[cch], cut to fit, control characters as spaces 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/dynafix.c b/src/dynafix.c index 8340c7f..c9dc9df 100644 --- a/src/dynafix.c +++ b/src/dynafix.c @@ -403,15 +403,41 @@ static BOOL is_setup_helper(const char *file) return setup_helper_match(file, dirs, 3); } +typedef BOOL (WINAPI *sx_t)(SHELLEXECUTEINFOA *); + +// ShellExecuteExA for the setup helper WITHOUT elevation. Windows' installer detection (the file names say "Setup") would ask for administrator permission for +// the helper, which only writes HKCU\Software\DynaPro (docs\FIRST_START.md). The layer "RunAsInvoker" in __COMPAT_LAYER makes Windows start it as the user +// that runs DynaRun, as DynaRunFix's launcher does for DynaRun itself. The variable is set for the one call and put back at once (the child takes its +// environment when it is created, inside the call); it is not left in DynaRun's environment, whose other child processes are not changed (see clear_runasinvoker). +// The process environment is shared, so a program that DynaRun's other threads start in those few milliseconds would get the layer too; DynaRun starts nothing +// else at that moment (it is waiting on the form's OK), and g_helper_busy keeps a second helper out. +static BOOL shexec_as_invoker(sx_t sx, SHELLEXECUTEINFOA *se) +{ + char old[512], nw[560]; DWORD n, err; BOOL ok, was_set; + old[0] = 0; + SetLastError(0); + n = GetEnvironmentVariableA("__COMPAT_LAYER", old, sizeof(old)); + was_set = n != 0 || GetLastError() != ERROR_ENVVAR_NOT_FOUND; // n == 0 is both "not set" and "set to an empty value": put back exactly what was there + if (n >= sizeof(old) || !df_compat_with_invoker(n ? old : NULL, nw, sizeof(nw))) { + logf("setup helper: __COMPAT_LAYER is too long to add RunAsInvoker, started the usual way\r\n", 0, 0, 0); + return sx(se); + } + SetEnvironmentVariableA("__COMPAT_LAYER", nw); + ok = sx(se); err = GetLastError(); + SetEnvironmentVariableA("__COMPAT_LAYER", was_set ? old : NULL); // NULL removes it; an empty value stays an empty value + SetLastError(err); + if (ok) logf("setup helper started without elevation (RunAsInvoker)\r\n", 0, 0, 0); + return ok; +} + static LONG g_helper_busy; // 1 while the setup helper is being waited for (the message loop below can bring a second request) static HINSTANCE WINAPI H_ShellExecuteA(HWND w, LPCSTR verb, LPCSTR file, LPCSTR params, LPCSTR dir, INT show) { typedef HINSTANCE (WINAPI *se_t)(HWND, LPCSTR, LPCSTR, LPCSTR, LPCSTR, INT); - typedef BOOL (WINAPI *sx_t)(SHELLEXECUTEINFOA *); int i; // First-time setup helper (System Data\Setup_.exe): run it to the end with the message loop - // running, then restart DynaRun. A declined UAC prompt is reported to DynaRun as access denied. + // running, then restart DynaRun. It is started without elevation (shexec_as_invoker): no UAC prompt. // Only that exact file qualifies (see is_setup_helper); everything else goes straight to ShellExecuteA. if (is_setup_helper(file)) do { @@ -440,7 +466,7 @@ static HINSTANCE WINAPI H_ShellExecuteA(HWND w, LPCSTR verb, LPCSTR file, LPCSTR { volatile char *z = (volatile char *)&se; for (i = 0; i < (int)sizeof(se); i++) z[i] = 0; } se.cbSize = sizeof(se); se.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NOASYNC; se.hwnd = w; se.lpVerb = verb; se.lpFile = file; se.lpParameters = params; se.lpDirectory = dir; se.nShow = show; - if (!sx(&se)) { logf("setup helper %s did not start: error %u %u\r\n", (DWORD)(UINT_PTR)file, GetLastError(), 0); InterlockedExchange(&g_helper_busy, 0); return (HINSTANCE)(UINT_PTR)SE_ERR_ACCESSDENIED; } + if (!shexec_as_invoker(sx, &se)) { logf("setup helper %s did not start: error %u %u\r\n", (DWORD)(UINT_PTR)file, GetLastError(), 0); InterlockedExchange(&g_helper_busy, 0); return (HINSTANCE)(UINT_PTR)SE_ERR_ACCESSDENIED; } logf("setup helper %s started, waiting for it %u %u\r\n", (DWORD)(UINT_PTR)file, 0, 0); if (se.hProcess) { while (!quit && MsgWaitForMultipleObjects(1, &se.hProcess, FALSE, INFINITE, QS_ALLINPUT) == WAIT_OBJECT_0 + 1) diff --git a/src/dynafix_pegrc.inc.c b/src/dynafix_pegrc.inc.c index 283f0c9..066ce0f 100644 --- a/src/dynafix_pegrc.inc.c +++ b/src/dynafix_pegrc.inc.c @@ -222,9 +222,11 @@ static void peg_check(void) } // 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. +// environment variable is inherited by DynaRun's own child processes; DynaRun is started that way only so that it never runs +// elevated itself, and its other children should not silently lose a UAC prompt they need, so here the word is taken out again +// (what was there before stays; the variable is removed when nothing else is left). The first-start Setup helper, the one child +// that is wanted without elevation, gets the layer for its own start only (shexec_as_invoker in dynafix.c). 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) diff --git a/src/install_chain.h b/src/install_chain.h new file mode 100644 index 0000000..78870f5 --- /dev/null +++ b/src/install_chain.h @@ -0,0 +1,24 @@ +// install_chain.h - which programs' windows belong to the install chain the wizard stays behind (keep_up in src\wizard\layout.h). +// +// Pure helper (Win32 string API only, no state, no CRT) so tests\test_shexec_match.c can compile it standalone. +#ifndef INSTALL_CHAIN_H +#define INSTALL_CHAIN_H +#include + +// n: the image name (no path) of the process that owns a window. The programs of the chain: the UAC prompt (consent.exe), Dyna Pro's setup (msiexec.exe, whose +// progress window says "Please wait while Windows configures ..."), the driver install (drvinst.exe / pnputil.exe / dpinst.exe: the red Windows Security warning) +// and the elevated stage of this setup itself (its message boxes): DynaRunFix-Setup.exe, or the copy made for a network drive, DynaRunFix-Setup-.exe. +// The names are compared whole (any case); the setup by its prefix "DynaRunFix-Setup" (the copy for a network drive adds -.exe). A name shorter than the +// prefix never matches, and nothing is read past its end. +static BOOL in_install_chain(const WCHAR *n) +{ + static const WCHAR *const names[] = { L"msiexec.exe", L"consent.exe", L"drvinst.exe", L"pnputil.exe", L"dpinst.exe", NULL }; + static const WCHAR setup[] = L"DynaRunFix-Setup"; + const int plen = (int)(sizeof(setup) / sizeof(setup[0])) - 1; + int i; + if (!n) return FALSE; + for (i = 0; names[i]; i++) if (!lstrcmpiW(n, names[i])) return TRUE; + return lstrlenW(n) >= plen && CompareStringW(LOCALE_INVARIANT, NORM_IGNORECASE, n, plen, setup, plen) == CSTR_EQUAL; +} + +#endif diff --git a/src/package.c b/src/package.c index 1c8f6ef..f200aad 100644 --- a/src/package.c +++ b/src/package.c @@ -543,6 +543,17 @@ int ver_compare(const WCHAR *a, const WCHAR *b) return 0; } +// How the MSI's ProductVersion stands to what DynaRunFix was tested with (DYNARUN_TESTED_VERSION). The one place that decides it; the wizard only asks. +// A text that does not start with a version is VER_UNKNOWN; 4.x and later (possibly not Visual Basic 6 any more) is VER_V4PLUS; exactly the tested version +// (a missing part counts as 0) is VER_TESTED; anything else below 4 (3.25.x, 3.26.1, 2.x ...) is VER_UNTESTED. +int dynarun_ver_class(const WCHAR *ver) +{ + DWORD v[4]; + if (!ver || !ver_parts(ver, v)) return VER_UNKNOWN; + if (v[0] >= 4) return VER_V4PLUS; + return ver_compare(ver, DYNARUN_TESTED_VERSION) == 0 ? VER_TESTED : VER_UNTESTED; +} + static char lc(char c) { return c >= 'A' && c <= 'Z' ? (char)(c + 32) : c; } static BOOL starts_ci(const char *p, const char *lower) { int i; for (i = 0; lower[i]; i++) if (lc(p[i]) != lower[i]) return FALSE; return TRUE; } @@ -1181,6 +1192,25 @@ static BOOL names_dynarun(const char *s) // "Dyna Run V3", "DynaRun 4", return FALSE; } +// A Property value of the MSI (ANSI text, written by whoever made the file) as text for the screen and the log: out[cch], always NUL-terminated, cut when it does +// not fit, empty when the conversion fails; control characters (below a space) become spaces so the value cannot break the lines of the info box or the log. +void msi_text(const char *s, WCHAR *out, int cch) +{ + int n, i; + if (!out || cch <= 0) return; + out[0] = 0; + if (!s) return; + n = MultiByteToWideChar(CP_ACP, 0, s, -1, out, cch); + if (n > 0) n--; // the terminator came with it + else { // does not fit (or cannot be converted): the part that fits, if that converts + int len = lstrlenA(s); + if (len > cch - 1) len = cch - 1; + n = len > 0 ? MultiByteToWideChar(CP_ACP, 0, s, len, out, cch - 1) : 0; + } + out[n] = 0; + for (i = 0; i < n; i++) if (out[i] < 0x20) out[i] = L' '; +} + int msi_check(const WCHAR *msi, WCHAR *version, int cch) { MSIHANDLE db; char *code, *name, *ver; int r; @@ -1191,7 +1221,7 @@ int msi_check(const WCHAR *msi, WCHAR *version, int cch) ver = query1(db, L"SELECT `Property`, `Value` FROM `Property` WHERE `Property` = 'ProductVersion'", NULL); MsiCloseHandle(db); r = code && !lstrcmpiA(code, DYNARUN_UPGRADE_CODE) && names_dynarun(name) ? MSI_DYNARUN : MSI_OTHER; - if (version && ver) MultiByteToWideChar(CP_ACP, 0, ver, -1, version, cch); + if (version && ver) msi_text(ver, version, cch); release(code); release(name); release(ver); return r; } @@ -1219,16 +1249,17 @@ BOOL pkg_sha256(const WCHAR *path, WCHAR *hex, ULONGLONG *size) } // ProductVersion and Manufacturer from the MSI's Property table (display only; nothing is decided on them). -void msi_info(const WCHAR *msi, WCHAR *version, WCHAR *manufacturer, int cch) +void msi_info(const WCHAR *msi, WCHAR *version, int vcch, WCHAR *manufacturer, int mcch) { MSIHANDLE db; char *ver, *mfr; - version[0] = manufacturer[0] = 0; + if (vcch > 0) version[0] = 0; + if (mcch > 0) manufacturer[0] = 0; if (MsiOpenDatabaseW(msi, (LPCWSTR)MSIDBOPEN_READONLY, &db)) return; ver = query1(db, L"SELECT `Property`, `Value` FROM `Property` WHERE `Property` = 'ProductVersion'", NULL); mfr = query1(db, L"SELECT `Property`, `Value` FROM `Property` WHERE `Property` = 'Manufacturer'", NULL); MsiCloseHandle(db); - if (ver) MultiByteToWideChar(CP_ACP, 0, ver, -1, version, cch); - if (mfr) MultiByteToWideChar(CP_ACP, 0, mfr, -1, manufacturer, cch); + msi_text(ver, version, vcch); + msi_text(mfr, manufacturer, mcch); release(ver); release(mfr); } diff --git a/src/setup/cmdline.h b/src/setup/cmdline.h index 0310287..b584c88 100644 --- a/src/setup/cmdline.h +++ b/src/setup/cmdline.h @@ -21,7 +21,8 @@ static const WCHAR *unverified_value(int argc, WCHAR **argv, int i) // 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). +// 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); +// c->msisha: the value of /msi-sha256 (from argv[4] on), kept only when it is exactly 64 hexadecimal digits (hex64_ok), else NULL; the last valid one wins. // /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; @@ -31,7 +32,7 @@ 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; + c->pin = FALSE; c->dzip = c->daccept = c->daccept4 = c->msisha = NULL; for (i = 1; i < c->argc; i++) { WCHAR *a = c->argv[i]; if (!lstrcmpiW(a, L"/quiet")) c->opts.quiet = TRUE; @@ -45,6 +46,7 @@ static void cmd_parse(cmdline *c) if ((v = unverified_value(c->argc, c->argv, i)) != NULL) { c->daccept = v; if (i == 4) c->daccept4 = v; } i++; } + else if (i >= 4 && !lstrcmpiW(a, L"/msi-sha256") && i + 1 < c->argc) { if (hex64_ok(c->argv[i + 1])) c->msisha = c->argv[i + 1]; i++; } } } @@ -60,15 +62,21 @@ static int cmd_machine(cmdline *c) // /machine [/with-driver ...]: the same, with the DynaRun MSI first +static int cmd_full(cmdline *c) // /full /msi-sha256 [/with-driver ...]: the same, with the DynaRun MSI first { int rc; CoInitialize(NULL); - rc = machine_full(&c->opts, c->argv[2], c->argv[3]); + rc = machine_full(&c->opts, c->argv[2], c->argv[3], c->msisha); if (!rc && c->dzip) rc = driver_prep_stage(c->argv[2], c->dzip, c->daccept); return rc; } +static int cmd_msi_only(cmdline *c) // /msi-only /msi-sha256 : the elevated stage that installs Dyna Pro's setup and nothing else +{ + CoInitialize(NULL); + return machine_msi_only(&c->opts, c->argv[2], c->argv[3], c->msisha); +} + static int cmd_unmachine(cmdline *c) // /unmachine []: the elevated uninstall stage { reg_str(HKEY_LOCAL_MACHINE, APPKEY, L"DynaRunExe", g_exe, MAX_PATH, 0); @@ -83,9 +91,11 @@ 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). +// needs_msi_sha: the verb runs a Dyna Pro MSI, so it is taken only with a valid /msi-sha256 (64 hex digits: what the user saw on the check page); verb_run refuses it otherwise. static const verb VERBS[] = { { L"/machine", 4, cmd_machine }, - { L"/full", 4, cmd_full }, + { L"/full", 4, cmd_full, TRUE }, + { L"/msi-only", 4, cmd_msi_only, TRUE }, { L"/unmachine", 2, cmd_unmachine }, { L"/driver", 2, cmd_driver }, // driver.c { L"/driver-continue", 2, cmd_driver_continue }, @@ -108,9 +118,12 @@ static const verb *verb_find(int argc, WCHAR **argv) } // 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). +// A verb that needs the setup's SHA-256 and does not get a valid one is not run: RC_FAIL (nothing was changed). static int verb_run(cmdline *c) { const verb *v = verb_find(c->argc, c->argv); - return v ? v->run(c) : DRV_NOVERB; + if (!v) return DRV_NOVERB; + if (v->needs_msi_sha && !c->msisha) { user_log(L"refused: the verb needs /msi-sha256 with 64 hexadecimal digits"); return RC_FAIL; } + return v->run(c); } diff --git a/src/setup/msi_stage.h b/src/setup/msi_stage.h index 2dd9e62..9361bde 100644 --- a/src/setup/msi_stage.h +++ b/src/setup/msi_stage.h @@ -62,12 +62,23 @@ static BOOL stage_msi(const WCHAR *msi, WCHAR *dir, WCHAR *staged) return TRUE; } -static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *msi) +// Is the staged copy the file the user was shown? Its SHA-256 (the wizard's check page) must equal sha. The copy is in the administrators-only folder, so +// nobody can change it between this check and msiexec; the user's own file (a folder they can write) could have been replaced after the check page. +static BOOL staged_is_shown(const WCHAR *staged, const WCHAR *sha) +{ + WCHAR hex[65]; + return hex64_ok(sha) && pkg_sha256(staged, hex, NULL) && !lstrcmpiW(hex, sha); +} + +// Elevated: Dyna Pro's MSI alone (staged, checked, run), nothing of DynaRunFix. 0 when it is installed, else RC_* (the failure was reported to the user). +// sha: the SHA-256 of the MSI as the wizard's check page showed it (64 hex digits); the staged copy must have it, else RC_MSI_FAILED and nothing is run. +// what: names the stage in the log of still_running. +static int msi_staged_run(const install_opts *o, const WCHAR *sid, const WCHAR *msi, const WCHAR *sha, const WCHAR *what) { 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 (still_running(o, what)) { stage_unlock(); return RC_RUNNING; } if (!stage_msi(msi, dir, staged)) { stage_unlock(); msg(TS(ST_THE_DYNARUN_V3_SETUP_COULD), MB_ICONERROR); @@ -78,6 +89,12 @@ static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *ms msg(TS(ST_THIS_IS_NOT_THE_DYNARUN), MB_ICONERROR); return RC_MSI_FAILED; } + if (!staged_is_shown(staged, sha)) { // not the file that was shown on the check page (or no hash was given) + user_log(L"the staged setup does not have the SHA-256 shown on the check page: not run"); + drop_admin_dir(dir); stage_unlock(); + msg(TS(ST_THE_DYNARUN_V3_SETUP_DIFFERS), MB_ICONERROR); + return RC_MSI_FAILED; + } rc = run_msiexec(staged); drop_admin_dir(dir); stage_unlock(); @@ -89,7 +106,15 @@ static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *ms msg(txt, MB_ICONERROR); return RC_MSI_FAILED; } + return 0; +} + +static int machine_full(const install_opts *o, const WCHAR *sid, const WCHAR *msi, const WCHAR *sha) +{ + int rc = msi_staged_run(o, sid, msi, sha, L"full install"); + if (rc) return rc; if (!locate_dynarun()) { msg(TS(ST_DYNARUN_V3_WAS_INSTALLED_BUT), MB_ICONERROR); return RC_NO_DYNARUN; } return machine_install(o, sid); } +static int machine_msi_only(const install_opts *o, const WCHAR *sid, const WCHAR *msi, const WCHAR *sha) { return msi_staged_run(o, sid, msi, sha, L"install of Dyna Pro's setup only"); } diff --git a/src/setup/strings.h b/src/setup/strings.h index 09c02e7..e568332 100644 --- a/src/setup/strings.h +++ b/src/setup/strings.h @@ -5,7 +5,7 @@ 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_THE_DYNARUN_V3_SETUP_DIFFERS, 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, @@ -41,6 +41,8 @@ static const ui_str SETUP_STR[ST_COUNT] = { L"未取得系統管理員權限,沒有做任何變更。" }, [ST_THE_DYNARUN_V3_SETUP_COULD] = { L"The DynaRun V3 setup could not be copied for installation.", L"無法複製 DynaRun V3 安裝檔以進行安裝。" }, + [ST_THE_DYNARUN_V3_SETUP_DIFFERS] = { L"The DynaRun V3 setup could not be matched with the SHA-256 shown on the \"Check the installer\" page (the value is missing, or the file is not the one that was shown), so it will not be installed. Please start the installation again.", + L"DynaRun V3 安裝檔和「確認安裝檔」頁面顯示的 SHA-256 對不起來(沒有取得該值,或檔案不是當時顯示的那一個),因此不會安裝。請重新開始安裝。" }, [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).", diff --git a/src/setup/userstage.h b/src/setup/userstage.h index 803445e..b0b1674 100644 --- a/src/setup/userstage.h +++ b/src/setup/userstage.h @@ -70,6 +70,16 @@ static void add_driver_args(WCHAR *args, const WCHAR *zip, const WCHAR *accept) if (accept && accept[0]) { lstrcatW(args, L" /unverified "); lstrcatW(args, accept); } } +// The elevated stage that runs Dyna Pro's MSI gets the SHA-256 the user saw on the check page (/msi-sha256) and compares its staged copy with it. +// A setup without a readable hash is not installed: msi_sha_usable tells the user and says FALSE before anything is started. +static BOOL msi_sha_usable(const WCHAR *sha) +{ + if (hex64_ok(sha)) return TRUE; + msg(TS(ST_THE_DYNARUN_V3_SETUP_DIFFERS), MB_ICONERROR); + return FALSE; +} +static void add_msi_sha_arg(WCHAR *args, const WCHAR *sha) { lstrcatW(args, L" /msi-sha256 "); lstrcatW(args, sha); } + 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; @@ -89,16 +99,18 @@ int install_fix_driver(const install_opts *o, install_result *res, const WCHAR * 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) +int install_all_driver(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *msi_sha, 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 (!msi_sha_usable(msi_sha)) return RC_MSI_FAILED; 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); } + if (is_admin()) { rc = machine_full(o, sid, msi, msi_sha); if (!rc && zip) rc = driver_prep_stage(sid, zip, accept); } else { wsprintfW(args, L"/full %s \"%s\"", sid, msi); + add_msi_sha_arg(args, msi_sha); add_driver_args(args, zip, accept); rc = run_elevated(args, NULL, o); } @@ -116,5 +128,28 @@ int install_all_driver(const install_opts *o, install_result *res, const WCHAR * return rc; } -int install_all(const install_opts *o, install_result *res, const WCHAR *msi) { return install_all_driver(o, res, msi, NULL, NULL); } +int install_all(const install_opts *o, install_result *res, const WCHAR *msi, const WCHAR *msi_sha) { return install_all_driver(o, res, msi, msi_sha, NULL, NULL); } + +// Dyna Pro's setup alone (a DynaRun that is not the tested V3, see dynarun_ver_class): the MSI in the elevated stage, no fix, no shortcuts, no driver. RC_OK, RC_CANCEL, RC_*. +int install_msi_only(const install_opts *o, const WCHAR *msi, const WCHAR *msi_sha) +{ + static WCHAR sid[200], args[2 * MAX_PATH + 160]; int rc; + if (!user_sid(sid, 200)) return RC_FAIL; + if (!msi_sha_usable(msi_sha)) return RC_MSI_FAILED; + if (running_early(o, L"install of Dyna Pro's setup only (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_msi_only(o, sid, msi, msi_sha); + else { + wsprintfW(args, L"/msi-only %s \"%s\"", sid, msi); + add_msi_sha_arg(args, msi_sha); + rc = run_elevated(args, NULL, o); + } + if (rc == RC_OVER_SHOULDER) { // over-the-shoulder elevation: the setup as this user + rc = run_msiexec(msi); + if (rc == ERROR_INSTALL_USEREXIT) return RC_CANCEL; + return rc && rc != ERROR_SUCCESS_REBOOT_REQUIRED ? RC_MSI_FAILED : RC_OK; + } + return rc; +} diff --git a/src/setup/util.h b/src/setup/util.h index e5adf4c..1f1495e 100644 --- a/src/setup/util.h +++ b/src/setup/util.h @@ -24,6 +24,15 @@ const PfnDliHook __pfnDliNotifyHook2 = dll_hook; /* ---------- small helpers (no C runtime) ---------- */ +// A SHA-256 as the command line and the wizard carry it: exactly 64 hexadecimal digits (any case), nothing else. +static BOOL hex64_ok(const WCHAR *s) +{ + int i; + if (!s) return FALSE; + for (i = 0; i < 64; i++) if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f') || (s[i] >= 'A' && s[i] <= 'F'))) return FALSE; + return s[64] == 0; +} + 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); } diff --git a/src/wizard.c b/src/wizard.c index 99f396f..f3fb453 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -19,19 +19,21 @@ #include "drf_package.h" #include "drf_driver.h" #include "version.h" +#include "install_chain.h" #define WIDEN2(x) L##x #define WIDEN(x) WIDEN2(x) 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, +enum { P_HELLO, P_FETCH, P_FAIL, P_PASSWORD, P_EXTRACT, P_CHECK, P_VER4, P_LICENSE, P_RUNNING, P_INSTALL, P_DONE, P_MSI_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_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 +enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_INSTALL_MSI, 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) #define TIMER_RUNNING 1 +#define TIMER_NOTIFY 2 // the temporary tray icon of alert_user is taken away again // 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. @@ -48,15 +50,30 @@ static const acq_desc ACQ_DRIVER = { L"the Dyna Pro USB driver", L"驅動程式" #define ACQ_NAME(d) T((d)->name_en, (d)->name_zh) // 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. +// /driver and /driver-continue command line) whose combinations were never all possible. It changes only in start (wizard), go_install, skip_driver, sec_ver4 (the V4 page) +// and the "Reinstall the fix..." link; after_extract takes FLOW_MSI_ONLY back. // 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 }; +// FLOW_MSI_ONLY the setup is DynaRun V4 or later and the user chose "Install original software only": Dyna Pro's MSI alone, no fix, no shortcuts, no driver. +// Set by sec_ver4 (the V4 page); after_extract takes it back (a new package is a new decision). Its install job and its finish page +// (P_MSI_DONE: only "Close") are the row of FLOWS[] below. +enum { FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE, FLOW_MSI_ONLY }; + +// One row per flow, indexed by the FLOW_* value: what differs between the ways through the pages where the main flow reaches them. +// license_job the install job the license page's "Install" starts (go_install; the driver flows never come to that page) +// done_page the page that follows an install that finished (jd_install) +typedef struct { int license_job, done_page; } flow_desc; +static const flow_desc FLOWS[] = { + [FLOW_MAIN] = { JOB_INSTALL_ALL, P_DONE }, + [FLOW_MAIN_WITH_DRIVER] = { JOB_INSTALL_ALL, P_DONE }, + [FLOW_DRIVER_ONLY] = { JOB_INSTALL_ALL, P_DONE }, + [FLOW_DRIVER_CONTINUE] = { JOB_INSTALL_ALL, P_DONE }, + [FLOW_MSI_ONLY] = { JOB_INSTALL_MSI, P_MSI_DONE }, +}; // 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 @@ -75,10 +92,12 @@ typedef struct { int nfound; // packages pkg_find_local has handed us so far HANDLE thread; WCHAR pkg[MAX_PATH], msi[MAX_PATH], ver[64], note[1100]; + int verclass; // VER_*: dynarun_ver_class of the setup's ProductVersion (ver), set by after_extract 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) + WCHAR msi_hex[65]; // SHA-256 of wz.msi as the check page shows it ("" = could not be read); the elevated stage checks its copy against it + WCHAR info[2200]; // what the check page shows about the package (source, MSI version/manufacturer, path, size, SHA-256) 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 @@ -108,7 +127,7 @@ typedef struct { install_opts opts; // the options the job runs with int flow; const acq_desc *acq; BOOL acq_local_ok; - WCHAR msi[MAX_PATH]; + WCHAR msi[MAX_PATH], msi_hex[65]; // msi_hex: the SHA-256 the user saw for it 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 @@ -130,14 +149,14 @@ 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 HFONT g_font, g_big, g_small; // g_small: the text boxes (the license text and the details of the setup file) static HBRUSH g_white, g_grey; 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 }; +enum { PF_NOCLOSE = 1, PF_TOPMOST = 2 }; // PF_TOPMOST: topmost while the page is shown in the sign-in task's run (FLOW_DRIVER_CONTINUE), see set_topmost typedef struct { void (*enter)(void); void (*primary)(void); diff --git a/src/wizard/acquire.h b/src/wizard/acquire.h index cfc1458..0563142 100644 --- a/src/wizard/acquire.h +++ b/src/wizard/acquire.h @@ -77,14 +77,41 @@ static void enter_extract(void) buttons(NULL, TX(S_CANCEL), NULL); } +// The notice for a setup whose version is not the tested one (not an error: the install goes on). VER_TESTED, and VER_V4PLUS (which has its own page), say nothing. +static void ver_notice(void) +{ + static WCHAR t[700]; + if (wz.verclass == VER_UNKNOWN) status(TX(S_VER_UNKNOWN_NOTICE), FALSE); + else if (wz.verclass == VER_UNTESTED) { wsprintfW(t, TX(S_VER_UNTESTED_NOTICE), wz.ver, DYNARUN_TESTED_VERSION); status(t, FALSE); } +} + static void enter_check(void) // everything we know about the setup file, to compare with what Dyna Pro gives { + WCHAR *b = pgbuf; text(ID_TITLE, TX(S_CHECK_THE_INSTALLER)); - text(ID_BODY, TX(S_COMPARE_THIS_WITH_WHAT_DYNA)); + lstrcpyW(b, TX(S_COMPARE_THIS_WITH_WHAT_DYNA)); + if (wz.pkg_found) lstrcatW(b, TX(S_FOUND_LOCAL_HINT)); // a copy found on this computer: the way to the website's file is offered + text(ID_BODY, b); SetWindowTextW(H(ID_INFO), wz.info); vis(ID_INFO, TRUE); - buttons(TX(S_NEXT), TX(S_CANCEL), TX(S_OPEN_WEBSITE)); + buttons4(TX(S_NEXT), TX(S_CANCEL), wz.pkg_found ? TX(S_DOWNLOAD_INSTEAD) : NULL, TX(S_OPEN_WEBSITE)); if (wz.note[0]) status(wz.note, TRUE); + else ver_notice(); +} + +// DynaRun V4 or later: the fix was never tested with it and V4 may not need it. Laid out like the license page: the check box where the accept box is, the +// buttons "Install the fix anyway" (needs the box), "Install original software only", "Cancel". +static void enter_ver4(void) +{ + WCHAR *b = pgbuf; + text(ID_TITLE, TX(S_V4_TITLE)); + wsprintfW(b, TX(S_V4_BODY), wz.ver, DYNARUN_TESTED_VERSION); + text(ID_BODY, b); + SetWindowTextW(H(ID_CHECK), TX(S_V4_CHECK)); + vis(ID_CHECK, TRUE); + buttons4(TX(S_V4_INSTALL_FIX), TX(S_V4_ORIGINAL_ONLY), TX(S_CANCEL), NULL); + EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); + SetFocus(H(ID_CHECK)); } static void enter_license(void) @@ -101,6 +128,7 @@ static void enter_license(void) 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); + else ver_notice(); } // 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 @@ -201,20 +229,25 @@ static void fmt_size(WCHAR *o, ULONGLONG n) 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. +// Shown on the check page so the user can compare with what Dyna Pro provides. Display only: nothing here blocks the install. The identity of the setup comes +// first (where it is from, the MSI's ProductVersion and Manufacturer), then the file: path, size, SHA-256 (and the same of the Setup.msi inside a zip). +// The SHA-256 of the MSI that will be installed (the file itself, or the Setup.msi inside the zip) is kept in wz.msi_hex and handed to the elevated stage. static void build_info(void) { 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(wz.pkg, hex, &size); if (!same) okm = pkg_sha256(wz.msi, hex2, &size2); - msi_info(wz.msi, ver, mfr, 64); + msi_info(wz.msi, ver, ARRAYSIZE(ver), mfr, ARRAYSIZE(mfr)); SetCursor(old); + lstrcpyW(wz.msi_hex, same ? (okp ? hex : L"") : (okm ? hex2 : L"")); // the hash of the file that is going to be installed: what the elevated stage is held to wz.info[0] = 0; - lstrcatW(wz.info, TX(S_SOURCE)); + 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_PRODUCT_VERSION)); lstrcatW(wz.info, ver[0] ? ver : L"?"); + lstrcatW(wz.info, TX(S_MANUFACTURER)); lstrcatW(wz.info, mfr[0] ? mfr : L"?"); lstrcatW(wz.info, TX(S_FILE)); lstrcatW(wz.info, wz.pkg); fmt_size(sz, size); lstrcatW(wz.info, TX(S_SIZE)); lstrcatW(wz.info, sz); @@ -224,13 +257,15 @@ static void build_info(void) 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"?"); - lstrcatW(wz.info, L"\r\nManufacturer: "); lstrcatW(wz.info, mfr[0] ? mfr : L"?"); } static void after_extract(void) { - if (msi_check(wz.msi, wz.ver, 64) != MSI_DYNARUN) { not_dynarun(); return; } + WCHAR l[160]; + if (msi_check(wz.msi, wz.ver, ARRAYSIZE(wz.ver)) != MSI_DYNARUN) { not_dynarun(); return; } + wz.verclass = dynarun_ver_class(wz.ver); // the version gate: the check page notices, the V4 page, the license page follow from it + if (wz.flow == FLOW_MSI_ONLY) wz.flow = FLOW_MAIN; // a new package is a new decision + wsprintfW(l, L"setup version %s: class %d (0 unreadable, 1 tested, 2 other, 3 V4 or later)", wz.ver[0] ? wz.ver : L"?", wz.verclass); user_log(l); release(wz.rtf); wz.rtf = msi_license_rtf(wz.msi); wz.nolicense = !wz.rtf; @@ -242,7 +277,6 @@ static void after_extract(void) 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), TX(S_DYNARUN_V3_IS_LICENSED_TO)); - SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); set_page(P_CHECK); } @@ -359,7 +393,7 @@ 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 (rc == PK_CANCEL) { end_wizard(); return; } 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(wz.pkg), 100); @@ -386,8 +420,30 @@ static void pri_password(void) 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_check(void) { set_page(wz.verclass == VER_V4PLUS ? P_VER4 : P_LICENSE); } + +// The V4 page: "Install the fix anyway" goes on to the license as for any version; "Install original software only" does too, but the install is Dyna Pro's MSI alone +// (FLOW_MSI_ONLY: no fix, no shortcuts, no pin, no sign-in start, no USB driver: the driver step belongs to the fix and was made for V3's unsigned FTDI driver). +static void pri_ver4(void) { set_page(P_LICENSE); } +static void sec_ver4(BOOL button) +{ + if (!button) { on_close(); return; } // Esc + user_log(L"V4 or later: the user chose to install only Dyna Pro's original software"); + wz.flow = FLOW_MSI_ONLY; wz.drv = FALSE; wz.opts.autostart_later = FALSE; + set_page(P_LICENSE); +} + +// "Download it" on the check page of a copy found on this computer: the same acquire flow, but the download (nothing found here is taken). The user's own file is not touched. +static void thr_check(void) +{ + if (!wz.pkg_found) return; + user_log(L"the user chose the download from the Dyna Pro website instead of the copy found on this computer"); + drop_msi(); // our decrypted copy of it, if it was a zip + wz.pkg_found = FALSE; wz.acq_fresh = TRUE; + acq_start(&ACQ_SETUP); +} + +static void pri_license(void) { go_install(FLOWS[wz.flow].license_job); } // the install job of the flow (FLOWS[], wizard.c) static void pri_fetch(void) // "Pause" / "Resume" { diff --git a/src/wizard/driver.h b/src/wizard/driver.h index fa934c9..7c6e94e 100644 --- a/src/wizard/driver.h +++ b/src/wizard/driver.h @@ -365,7 +365,7 @@ static void jd_drv_install(int job, int 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) + end_wizard(); // cancelled (or not allowed to: nothing else can be done from here) } static void pri_drv_intro(void) { acq_start(&ACQ_DRIVER); } diff --git a/src/wizard/layout.h b/src/wizard/layout.h index 8100d24..3fe1e88 100644 --- a/src/wizard/layout.h +++ b/src/wizard/layout.h @@ -181,7 +181,7 @@ 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); + SendMessageW(c, WM_SETFONT, (WPARAM)(id == ID_TITLE ? g_big : id == ID_RICH || id == ID_INFO ? g_small : g_font), FALSE); // both text boxes in the same smaller font return c; } @@ -240,6 +240,14 @@ static int status_need(void) return measure(ID_STATUS, c.right - 2 * S(32)) + S(8); } +// Height the details box needs to show all its text without scrolling at width w (the text wrapped to what is left of the box after its border and the +// scroll bar it always has, a line to spare for the differences between this measure and the edit control's own wrapping). +static int info_need(int w) +{ + int h = measure(ID_INFO, w - S(16) - GetSystemMetrics(SM_CXVSCROLL)) + S(24); + return h < S(120) ? S(120) : h; +} + 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 @@ -264,13 +272,13 @@ static int stack(BOOL apply, int w, int bottom) } 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); } + if (shown(ID_RICH) || shown(ID_CHECK)) { // the accept check box sits at the bottom of the page (the license page, and the V4 page which has no text box) + int cy = apply ? bottom - (sh ? sh + S(6) : S(16)) - S(28) : y + (shown(ID_RICH) ? S(200) + S(10) : S(16)); // 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)); if (shown(ID_RICH)) 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 (shown(ID_INFO)) { // the details of the setup file: a read-only text box (selectable, copyable): all of its text if the screen allows, else it scrolls + int need = info_need(w), bh2 = apply ? bottom - S(16) - y - sh : need; if (apply) place(ID_INFO, m, y, w, bh2 > S(60) ? bh2 : S(60)); y += bh2 + S(16); } @@ -356,6 +364,48 @@ static void bring_front(HWND h) } } +// The window is topmost (above all other windows, whatever is activated) or not. Only the difference is applied: HWND_NOTOPMOST on a window that is not topmost +// would still raise it above the other windows. +static void set_topmost(HWND h, BOOL on) +{ + if (((GetWindowLongW(h, GWL_EXSTYLE) & WS_EX_TOPMOST) != 0) == (on != FALSE)) return; + SetWindowPos(h, on ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); +} + +static NOTIFYICONDATAW *tray_data(NOTIFYICONDATAW *n) +{ + zero(n, sizeof(*n)); n->cbSize = NOTIFYICONDATAW_V3_SIZE; n->hWnd = g_hwnd; n->uID = 1; // the size every Windows since XP knows + return n; +} + +// The temporary tray icon (and its notification) is taken away; harmless when there is none. +static void alert_clear(void) +{ + NOTIFYICONDATAW n; + KillTimer(g_hwnd, TIMER_NOTIFY); + Shell_NotifyIconW(NIM_DELETE, tray_data(&n)); +} + +// The one way the wizard calls the user back: the wizard waits for a click after a long or background step (a job ended, the sign-in start after the restart). +// The taskbar button flashes until the window is activated and Windows shows a notification (a temporary tray icon with a balloon / toast; it goes after 10 s, +// when the window is activated, or at the end). Nothing when the wizard is in front already: the user is looking at it. +static void alert_user(void) +{ + NOTIFYICONDATAW n; FLASHWINFO f; WCHAR t[80]; + if (!g_hwnd || !IsWindow(g_hwnd) || GetForegroundWindow() == g_hwnd) return; + f.cbSize = sizeof(f); f.hwnd = g_hwnd; f.dwFlags = FLASHW_ALL | FLASHW_TIMERNOFG; f.uCount = 0; f.dwTimeout = 0; + FlashWindowEx(&f); + GetWindowTextW(H(ID_TITLE), t, 80); + alert_clear(); + tray_data(&n); + n.uFlags = NIF_ICON | NIF_TIP | NIF_INFO; + n.hIcon = LoadIconW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(10)); + lstrcpyW(n.szTip, L"DynaRunFix"); lstrcpyW(n.szInfoTitle, L"DynaRunFix"); + wsprintfW(n.szInfo, TX(S_NOTIFY_TEXT), t); + n.dwInfoFlags = NIIF_INFO; + if (Shell_NotifyIconW(NIM_ADD, &n)) SetTimer(g_hwnd, TIMER_NOTIFY, 10000, NULL); +} + // 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) { @@ -372,20 +422,28 @@ static const WCHAR *owner_name(HWND w, WCHAR *buf, DWORD n) 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. +// While the elevated part installs (every 300 ms): the window of the install chain that has the foreground (the UAC prompt, msiexec's progress window, the +// driver warning) stays frontmost and the wizard is put directly behind it, above everything else: not topmost, so the user can still switch to another +// program. A foreground window of an unrelated program is left alone (the user chose it); that it is there is logged once per change, without saying whose it +// is (another program's name is none of the log's business). Explorer, which Windows activates after the prompt and which would cover the wizard, is put +// behind the wizard once per change. A change is also the user coming back to the wizard and leaving it again: `last` follows the foreground window on every +// round, so an Explorer that covers the wizard after the user has been there is a change. static void keep_up(HWND h) { - static WCHAR b[MAX_PATH]; const WCHAR *n; HWND fg = GetForegroundWindow(); DWORD pid = 0; + static WCHAR b[MAX_PATH]; static HWND last; + const WCHAR *n; HWND fg = GetForegroundWindow(); DWORD pid = 0; BOOL changed = fg != last; + last = fg; 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")) + if (in_install_chain(n)) SetWindowPos(h, fg, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); else if (!lstrcmpiW(n, L"explorer.exe")) { + if (!changed) return; 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); + } else if (changed) { + user_log(L"install: an unrelated window has the foreground (not part of the install, left alone)"); } } diff --git a/src/wizard/main.h b/src/wizard/main.h index 90dfc36..58520fc 100644 --- a/src/wizard/main.h +++ b/src/wizard/main.h @@ -112,6 +112,15 @@ static void enter_install(void) buttons(NULL, NULL, NULL); } +// The finish page of FLOW_MSI_ONLY (see FLOWS[]): Dyna Pro's software alone (V4 or later), so nothing of ours to start or to hint at. "Close" only ends the wizard. +static void enter_msi_done(void) +{ + wz.result = RC_OK; // everything that was asked for is installed + text(ID_TITLE, TX(S_MSI_DONE_TITLE)); + text(ID_BODY, TX(S_MSI_DONE_BODY)); + buttons(TX(S_CLOSE), NULL, NULL); +} + static void enter_done(void) { wz.result = RC_OK; // everything is installed @@ -134,8 +143,9 @@ static void enter_error(void) 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 +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.msi_hex, g_in.drvzip, g_in.drv_allow ? g_in.drv_hex : NULL) : install_all(&g_in.opts, &g_out.instres, g_in.msi, g_in.msi_hex); } // 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 int jp_install_msi(void) { return install_msi_only(&g_in.opts, g_in.msi, g_in.msi_hex); } // FLOW_MSI_ONLY: Dyna Pro's MSI alone static void mo_install(void) { wz.instres = g_out.instres; } // the pin hint static void jd_install(int job, int rc); @@ -146,18 +156,21 @@ static void jd_install(int job, int rc); // 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) +// back_page an install job only: the page the wizard goes back to when the UAC prompt was declined (RC_CANCEL) - the page whose button started the job typedef struct { int (*proc)(void); void (*merge)(void); void (*on_done)(int job, int rc); BOOL keep_up, bring_front; + int back_page; } job_desc; static const job_desc JOBS[JOB_COUNT] = { [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_INSTALL_ALL] = { jp_install_all, mo_install, jd_install, TRUE, TRUE, P_LICENSE }, + [JOB_INSTALL_FIX] = { jp_install_fix, mo_install, jd_install, TRUE, TRUE, P_HELLO }, + [JOB_INSTALL_MSI] = { jp_install_msi, NULL, jd_install, TRUE, TRUE, P_LICENSE }, [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 }, @@ -187,7 +200,7 @@ 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.msi, wz.msi, sizeof(g_in.msi)); CopyMemory(g_in.msi_hex, wz.msi_hex, sizeof(g_in.msi_hex)); 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; @@ -240,7 +253,7 @@ static void choose_exe(void) set_page(P_HELLO); } -static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX +static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INSTALL_FIX / JOB_INSTALL_MSI { 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(); @@ -250,24 +263,33 @@ static void jd_install(int job, int rc) // JOB_INSTALL_ALL / JOB_INS else drv_error(rc); return; } - if (rc == RC_OK) { cleanup_files(); set_page(P_DONE); return; } + if (rc == RC_OK) { cleanup_files(); set_page(FLOWS[wz.flow].done_page); return; } if (rc == RC_RUNNING) { wz.pending = job; set_page(P_RUNNING); return; } // DynaRun was started meanwhile if (rc == RC_CANCEL) { 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 + set_page(wz.flow == FLOW_MAIN_WITH_DRIVER ? P_DRV_PRESS7 : JOBS[job].back_page); // the page with the button that started the job return; } wsprintfW(wz.note, TX(S_THE_INSTALLATION_DID_NOT_FINISH), rc); set_page(P_ERROR); } +// After a job's on_done has run: the wizard waits for the user's click. Not while another job runs (on_done started the next step: the extraction after the +// download, the driver step after the install), not when the window is going, and not on the page that restarts the computer by itself (P_DRV_REBOOT without +// drv_noreboot: nobody has to come back). +static BOOL waiting_for_user(void) +{ + return !wz.busy && !wz.closing && IsWindow(g_hwnd) && !(wz.page == P_DRV_REBOOT && !wz.drv_noreboot); +} + static void job_done(int job, int rc) { 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; } + if (wz.closing) { end_wizard(); return; } JOBS[job].on_done(job, rc); + if (waiting_for_user()) alert_user(); // a long step is over and the wizard waits for a click: call the user back if they are elsewhere } // The first page's options are read when it is left, whichever button leaves it. @@ -277,7 +299,8 @@ static void take_options(void) wz.opts.autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; } -static void end_wizard(void) { DestroyWindow(g_hwnd); } +// The one way the window ends (also when a job ends while it is closing): the temporary tray icon of alert_user goes first, whichever way the process ends after it. +static void end_wizard(void) { alert_clear(); DestroyWindow(g_hwnd); } // The primary button (right-most, Enter), page by page. static void pri_hello(void) @@ -335,6 +358,7 @@ static void ext_hello(void) 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 } }; +static const hotkey KEYS_VER4[] = { { 'U', 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 @@ -342,6 +366,9 @@ static const hotkey KEYS_LICENSE[] = { { 'A', ID_CHECK, NULL }, { 0 } }; // 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 +// PF_TOPMOST: in the sign-in task's run after the restart (FLOW_DRIVER_CONTINUE) the page is topmost while it waits for the user, so the programs that +// start with Windows cannot cover it. Not on the page that runs the driver install (P_DRV_INSTALL): Windows' own warning dialog has to be +// visible there, so topmost ends when the user clicks "Install" (set_page of the next page) and keep_up looks after the order of the windows. // 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 }, @@ -349,11 +376,13 @@ static const page_desc PAGES[P_COUNT] = { [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_CHECK] = { enter_check, pri_check, NULL, thr_check, ext_dynapro, NULL, NULL, 0 }, + [P_VER4] = { enter_ver4, pri_ver4, sec_ver4, on_close, NULL, NULL, KEYS_VER4, 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_MSI_DONE] = { enter_msi_done, end_wizard, 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 }, @@ -361,8 +390,8 @@ static const page_desc PAGES[P_COUNT] = { [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_RETRY] = { enter_drv_retry, pri_drv_retry, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, PF_TOPMOST }, + [P_DRV_WARN] = { enter_drv_warn, pri_drv_warn, sec_drv_cancel, NULL, NULL, cls_drv_task, NULL, PF_TOPMOST }, [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 }, @@ -376,10 +405,12 @@ static void set_page(int p) 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); + SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); // the accept box is shared by the V4 page and the license page: no page starts with it ticked (a license is never pre-accepted) 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(); wz.note[0] = 0; g_building = FALSE; + set_topmost(g_hwnd, (PAGES[p].flags & PF_TOPMOST) && wz.flow == FLOW_DRIVER_CONTINUE); layout(); if (!GetFocus() || !IsWindowEnabled(GetFocus()) || !(GetWindowLongW(GetFocus(), GWL_STYLE) & WS_VISIBLE)) SetFocus(GetNextDlgTabItem(g_hwnd, NULL, FALSE)); // something must hold the keyboard focus @@ -456,7 +487,11 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) } } if (w == ID_KEEPUP) keep_up(h); + if (w == TIMER_NOTIFY) alert_clear(); return 0; + case WM_ACTIVATE: // the user is back: the flashing stops by itself, the notification goes + if (LOWORD(w) != WA_INACTIVE) alert_clear(); + break; case WM_HINT: wz.hint = (int)w; if (wz.hint) { lstrcpyW(wz.hint_inst, g_hint_out.inst); lstrcpyW(wz.hint_new, g_hint_out.nw); } @@ -485,7 +520,7 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) 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(wz.pw, sizeof(wz.pw)); SecureZeroMemory(g_in.pw, sizeof(g_in.pw)); SetWindowTextW(H(ID_EDIT), L""); drop_msi(); PostQuitMessage(0); return 0; + case WM_DESTROY: alert_clear(); 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,6 +541,7 @@ int wizard(const install_opts *init) 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); + lf.lfHeight = -MulDiv(10, g_dpi, 72); lf.lfWeight = FW_NORMAL; g_small = CreateFontIndirectW(&lf); // 10 pt: the license text and the details of the setup file g_white = CreateSolidBrush(RGB(255, 255, 255)); g_grey = CreateSolidBrush(RGB(243, 243, 243)); @@ -536,6 +572,7 @@ int wizard(const install_opts *init) } ShowWindow(g_hwnd, SW_SHOWNORMAL); SetForegroundWindow(g_hwnd); + if (wz.flow == FLOW_DRIVER_CONTINUE) alert_user(); // the sign-in task after the restart: the start-up programs may hold the foreground; flash and notify then 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); diff --git a/src/wizard/strings.h b/src/wizard/strings.h index 24003df..9e28d09 100644 --- a/src/wizard/strings.h +++ b/src/wizard/strings.h @@ -37,6 +37,8 @@ enum { 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_PRODUCT_VERSION, S_MANUFACTURER, S_DOWNLOAD_INSTEAD, S_FOUND_LOCAL_HINT, S_VER_UNKNOWN_NOTICE, S_VER_UNTESTED_NOTICE, S_V4_TITLE, S_V4_BODY, S_V4_CHECK, + S_V4_INSTALL_FIX, S_V4_ORIGINAL_ONLY, S_MSI_DONE_TITLE, S_MSI_DONE_BODY, S_NOTIFY_TEXT, S_COUNT }; typedef struct { const WCHAR *en, *zh; } ui_str; @@ -125,8 +127,8 @@ static const ui_str UI_STR[S_COUNT] = { 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_SIZE] = { L"\r\nSize: ", + L"\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 " @@ -448,5 +450,45 @@ static const ui_str UI_STR[S_COUNT] = { L"DynaRun 已關閉。" }, [S_DYNARUNFIX_SETUP] = { L"DynaRunFix Setup %s", L"DynaRunFix 安裝程式 %s" }, + // the check page, the version gate (acquire.h), the alert (layout.h) + [S_PRODUCT_VERSION] = { L"\r\nMSI ProductVersion: ", + L"\r\nMSI 產品版本 (ProductVersion):" }, + [S_MANUFACTURER] = { L"\r\nManufacturer: ", + L"\r\n製造商 (Manufacturer):" }, + [S_DOWNLOAD_INSTEAD] = { L"Download it", + L"改從官網下載" }, + [S_FOUND_LOCAL_HINT] = { L"\n\nThis file was found on this computer. \"Download it\" gets a fresh copy from the Dyna Pro website instead.", + L"\n\n這個檔案是在這台電腦上找到的。若想改用官網上的檔案,請按「改從官網下載」。" }, + [S_VER_UNKNOWN_NOTICE] = { L"The version of this setup could not be read, so it is not known whether DynaRunFix fits it. The install goes on; if something does not work, " + L"please report it at https://github.com/timliudev/DynaRunFix/issues", + L"讀不到這個安裝檔的版本,所以不確定 DynaRunFix 是否適用。安裝會繼續;如果有狀況,請到 https://github.com/timliudev/DynaRunFix/issues 回報。" }, + [S_VER_UNTESTED_NOTICE] = { L"This setup is version %s. DynaRunFix was tested only with %s: you can install it, but there may be additional problems. " + L"If a test goes wrong, please open an issue on GitHub (https://github.com/timliudev/DynaRunFix/issues).", + L"這個安裝檔的版本是 %s。只在 %s 測試過,可以安裝,但可能有額外問題;如果測試有狀況請到 GitHub 開 issue (https://github.com/timliudev/DynaRunFix/issues)。" }, + [S_V4_TITLE] = { L"This is DynaRun V4 or later", + L"這是 DynaRun V4(或更新版本)" }, + [S_V4_BODY] = { L"This setup is version %s. DynaRunFix was made for DynaRun V3 (tested with %s), which is a Visual Basic 6 program. " + L"V4 and later may be built for Windows 10/11 and may not need the fix at all.\n\n" + L"We suggest: click \"Install original software only\" first and try Dyna Pro's software as it is. Only if it shows the old problems, " + L"run this setup again and add the fix.\n\n" + L"\"Install the fix anyway\" installs Dyna Pro's software and the fix; it was never tested with this version.", + L"這個安裝檔的版本是 %s。DynaRunFix 是為 DynaRun V3(以 %s 測試)做的,V3 是 Visual Basic 6 程式;" + L"V4 以後可能已改為支援 Windows 10/11 的程式,也許根本不需要這個修正。\n\n" + L"建議:先按「僅安裝原版軟體」,直接試用 Dyna Pro 的軟體;只有出現舊的問題時,再執行本程式補裝修正。\n\n" + L"「仍要安裝修正」會安裝 Dyna Pro 的軟體並加上修正,這個版本從未測試過。" }, + [S_V4_CHECK] = { L"I &understand that DynaRunFix has not been tested with this version", + L"我了解 DynaRunFix 未在此版本測試過(&U)" }, + [S_V4_INSTALL_FIX] = { L"Install the fix anyway", + L"仍要安裝修正" }, + [S_V4_ORIGINAL_ONLY] = { L"Install original software only", + L"僅安裝原版軟體" }, + [S_MSI_DONE_TITLE] = { L"Dyna Pro's software is installed", + L"原版軟體已安裝" }, + [S_MSI_DONE_BODY] = { L"Only Dyna Pro's own software was installed. The fix, the shortcuts, the taskbar pin, the start at sign-in and the USB driver of DynaRunFix were not.\n\n" + L"Start it from the Start menu and try it. If it shows the old problems, run this setup again: it then offers to install the fix.", + L"只安裝了 Dyna Pro 的原版軟體,DynaRunFix 的修正、捷徑、工作列釘選、開機自動啟動與 USB 驅動程式都沒有安裝。\n\n" + L"請從開始功能表啟動並試用;如果出現舊的問題,再執行一次本程式,它會提供安裝修正。" }, + [S_NOTIFY_TEXT] = { L"Please come back to DynaRunFix Setup: %s", + L"請回到 DynaRunFix 安裝程式:%s" }, }; #define TX(id) T(UI_STR[id].en, UI_STR[id].zh) diff --git a/tests/README.md b/tests/README.md index ebfad74..5f9253d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -23,7 +23,7 @@ To run the tests against another revision of the sources: `tests\build-tests.cmd # First-run helper path tests -`tests\build-shexec-tests.cmd` builds and runs `tests\test_shexec_match.c` (the path matching in `src\setup_match.h`), `tests\test_dfutil.c` (the `DYNAFIX_*` switch parsing and the `dynapro.dll` file name match in `src\dfutil.h`) and `tests\test_launcher.c` (the launcher's registry path read with long, mistyped and unterminated values, the same-session DynaRun lookup and its session rule, the token elevation query including an unknown state; it writes only `HKCU\Software\DynaRunFixTest-`); exit code 0 = all passed. The log tests above only check that an over-long Setup_ path is passed through and nothing is logged for it. +`tests\build-shexec-tests.cmd` builds and runs `tests\test_shexec_match.c` (the path matching in `src\setup_match.h` and `in_install_chain` in `src\install_chain.h`: whole program names, the setup's name by a prefix with a length check), `tests\test_dfutil.c` (the `DYNAFIX_*` switch parsing, the `dynapro.dll` file name match and the `RunAsInvoker` word added to `__COMPAT_LAYER` in `src\dfutil.h`, words separated by spaces or tabs) and `tests\test_launcher.c` (the launcher's registry path read with long, mistyped and unterminated values, the same-session DynaRun lookup and its session rule, the token elevation query including an unknown state; it writes only `HKCU\Software\DynaRunFixTest-`); exit code 0 = all passed. The log tests above only check that an over-long Setup_ path is passed through and nothing is logged for it. # Installer hardening tests @@ -39,9 +39,11 @@ hashing cannot be changed, renamed or deleted but still starts, and the COM regi (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 (`tests\promote_com.h`, included by `test_hardening.c` after `src\setup.c`). Exit code 0 = all passed. +`test_hardening.c` also covers the SHA-256 handed to the elevated stage that runs Dyna Pro's MSI: `hex64_ok` (exactly 64 hexadecimal digits), the `/msi-sha256` switch in `cmd_parse` (argv[4] on, last valid value wins, bad values dropped), the `needs_msi_sha` flag of `/full` and `/msi-only` in the verb table and that `verb_run` refuses them without a valid value (nothing is run), and `staged_is_shown` (the staged copy must have the shown SHA-256, any case; a changed, missing or unhashable file is refused). + # 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; `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. +`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, the version class of the wizard's version gate including more than four parts, huge parts and leading zeros, and the parsing of Dyna Pro's software page, on made-up pages; no network), `msi_text` (an MSI property as screen / log text: terminated, cut to the buffer, control characters as spaces), 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. The same text is read by `test_wizard_rules()` for the rules only a window shows: the accept box unticked on every page, the MSI-only finish page that only ends the wizard, the flow table `FLOWS[]`, the order of `last = fg` in `keep_up` and that it logs no program name, the alert only when `waiting_for_user()`, and that the tray icon is cleared on the one way the window ends. 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 e5be09e..fd9e9f1 100644 --- a/tests/VM_CHECKLIST.md +++ b/tests/VM_CHECKLIST.md @@ -26,7 +26,7 @@ Start: snapshot "clean", administrator account, network on, no DynaRun, no files 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. +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, MSI version and manufacturer first, then path, size, SHA-256; see section 10), 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). @@ -121,8 +121,23 @@ Every line starts with the date and time and ends with CRLF; a line has at most 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. +3. First start of a fresh profile (no `HKCU\Software\DynaPro`): `setup helper started without elevation (RunAsInvoker)`, `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). + +## 10. v1.8.0 behaviours + +Snapshot "clean". A setup whose version differs: copy Dyna Pro's `Setup.msi` (or the one extracted in `%TEMP%\DynaRunFix`), edit `Property.ProductVersion` with Orca (e.g. `3.25.0`, `4.0.0`, empty/`abc`), and put the MSI in Downloads (found automatically). The UpgradeCode and product name stay, so it is still accepted as DynaRun. Log lines: `DynaRunFix-setup.log` has `setup version : class N`. + +1. **Check page.** With a zip found in Downloads and with a download: the box is in a smaller font (compare with the license text), shows Source, MSI ProductVersion, Manufacturer first, then File, Size, SHA-256 (and the Setup.msi inside a zip), on one page without a scroll bar at the normal window size (the window may grow on a small screen). Check at 100 %, 150 % and 200 % scaling and in English and Chinese. +2. **Download instead.** Source "found on this computer": the page has *Download it* (*改從官網下載*) and the extra sentence in the text. Click it: the download page, then a new check page with "downloaded now from dynapro.co.uk" and no *Download it*; the file in Downloads is untouched; log line "the user chose the download ...". With a downloaded or chosen file the button is absent. +3. **Version 3.26.0:** no notice. **3.25.0 (or any other 3.x)** and **unreadable** (empty, `abc`): a black (not red) notice on the check and license pages with the issues URL; Next works and the install completes. **4.0.0:** after Next a page "This is DynaRun V4 or later" with the check box at the license page's position; *Install the fix anyway* is disabled until it is ticked; Alt+U toggles the box; Esc / *Cancel* closes the wizard. +4. **V4 page, *Install original software only*:** the license page, Install, one UAC prompt, only Dyna Pro's setup runs (no machine stage; the MSI-only stage writes no `setup.log`); then the page "Dyna Pro's software is installed" with only *Close*. No `DynaRunFix` folder changes, no shortcut retargeted, no pin, no `HKCU\...\Run\DynaRunFix`, no driver pages even if the driver box was ticked on page 1. Run the setup again: DynaRun is found installed and the fix is offered. Click *Close* on the finish page: the wizard just closes, DynaRun is NOT started (the normal Done page's *Start DynaRun* is not there). Decline the UAC prompt: back on the license page with the check box unticked (*Install* disabled); the V4 page's box is unticked whenever it is shown. +5. **V4 page, *Install the fix anyway*:** the normal install (fix, shortcuts, driver if ticked). +6. **Z-order during the install.** Open Notepad and a maximized Explorer window before clicking Install. Expected: after the UAC prompt, msiexec's "Please wait while Windows configures Dyna Run V3" window is in front, the wizard directly behind it (visible by moving the msiexec window), Notepad/Explorer behind the wizard. Alt+Tab to Notepad: it stays in front, the wizard does not jump over it; `DynaRunFix-setup.log` has one line "install: an unrelated window has the foreground" per change (no program name in it), not one per tick. Alt+Tab to the wizard and then to Explorer (while the elevated part still runs): Explorer is put behind the wizard again. The wizard is never topmost (another window can be put over it by clicking it). With the elevated stage's error message box (e.g. cancel msiexec), the box stays in front. +7. **Alert.** Start the install, switch to another program. When it ends: the taskbar button flashes and a Windows notification "Please come back to DynaRunFix Setup: Done" appears; the tray icon is gone after ~10 s or when the wizard is activated. With the wizard in front when it ends: no flash, no notification. The same after a download that ends on a page that waits (password, check page, error) or the extraction that ends on the check page while another program is in front. No alert when a step is followed at once by the next one (the extraction right after the download, the driver step after the install) and none on the page that restarts the computer by itself. Close the wizard from the taskbar while a notification is up: the tray icon is gone. +8. **Topmost after the restart (driver, section 1 step 8).** Sign in after pressing 7 with several start-up programs (or open some windows right after the sign-in): the page "Last step" is on top of them (and "7 was not pressed" if 7 is skipped), the taskbar button flashes and a notification appears if it is not in front. Click Install: the wizard stops being topmost (`WS_EX_TOPMOST` cleared) and Windows' red driver warning is fully visible and can be answered; after "The driver is installed" the page is in front without being topmost. In `/driver` (not after a restart) the pages are never topmost. +9. **First start without UAC.** Fresh profile (no `HKCU\Software\DynaPro`), UAC at the default level, standard administrator account: pick a model with an optional feature, OK. Expected: NO UAC prompt for `Setup_.exe`; `dynafix.log` has `setup helper started without elevation (RunAsInvoker)`, `setup helper finished, exit code 0`; the language the user chose is kept, the picked features are on, DynaRun restarts by itself. `HKCU\Software\DynaPro` is filled in (the helper ran as the user, not elevated: Process Explorer shows it at medium integrity). `__COMPAT_LAYER` is not left behind in DynaRun's environment (Process Explorer, environment of DynaRun: no `RunAsInvoker`). A second DynaRun child (e.g. any program started from DynaRun's own menus) behaves as before. +10. **The setup you checked is the setup that is installed.** On the *Check the installer* page note the SHA-256 (of the Setup.msi inside the zip, or of the MSI itself). Before clicking Install (license page), replace the MSI the wizard will use (`%TEMP%\DynaRunFix\Setup.msi` for a zip, or the chosen `.msi`) with another valid DynaRun MSI, or any change of the file. Expected: after the UAC prompt the message *The DynaRun V3 setup could not be matched with the SHA-256 shown ...*, nothing installed, the wizard's page "Something went wrong" with code 4, `setup.log` has "the staged setup does not have the SHA-256 shown on the check page: not run", no msiexec window. Without the replacement the install completes. diff --git a/tests/test_dfutil.c b/tests/test_dfutil.c index 027b85c..602842e 100644 --- a/tests/test_dfutil.c +++ b/tests/test_dfutil.c @@ -63,6 +63,26 @@ int main(void) CHECK(!df_is_dynapro("C:\\x\\dynapro.dll", 16, 16), "path cut to the buffer size (n == size): not terminated, not matched"); CHECK(!df_is_dynapro("xdynapro.dll", lstrlenA("xdynapro.dll"), 260), "bare xdynapro.dll"); CHECK(df_is_dynapro("D:/tools/dynapro.dll", lstrlenA("D:/tools/dynapro.dll"), 260), "forward slash"); + // __COMPAT_LAYER with RunAsInvoker added (the setup helper's start without elevation) + { char o[64]; int n; + n = df_compat_with_invoker(NULL, o, sizeof(o)); CHECK(n == 12 && !lstrcmpA(o, "RunAsInvoker"), "invoker: not set -> just the layer"); + n = df_compat_with_invoker("", o, sizeof(o)); CHECK(n == 12 && !lstrcmpA(o, "RunAsInvoker"), "invoker: empty -> just the layer"); + n = df_compat_with_invoker("WIN7RTM", o, sizeof(o)); CHECK(n == 20 && !lstrcmpA(o, "WIN7RTM RunAsInvoker"), "invoker: appended after the layers there"); + n = df_compat_with_invoker("RunAsInvoker", o, sizeof(o)); CHECK(n == 12 && !lstrcmpA(o, "RunAsInvoker"), "invoker: already there, not added again"); + n = df_compat_with_invoker("~ runasinvoker WIN7RTM", o, sizeof(o)); CHECK(n == (int)lstrlenA("~ runasinvoker WIN7RTM") && !lstrcmpA(o, "~ runasinvoker WIN7RTM"), "invoker: already there in any case, as a word"); + n = df_compat_with_invoker("NotRunAsInvoker", o, sizeof(o)); CHECK(!lstrcmpA(o, "NotRunAsInvoker RunAsInvoker"), "invoker: a longer name that ends in it is another layer"); + n = df_compat_with_invoker("RunAsInvokerX", o, sizeof(o)); CHECK(!lstrcmpA(o, "RunAsInvokerX RunAsInvoker"), "invoker: a longer name that starts with it is another layer"); + n = df_compat_with_invoker("WIN7RTM ", o, sizeof(o)); CHECK(!lstrcmpA(o, "WIN7RTM RunAsInvoker"), "invoker: a trailing space gives no double space"); + CHECK(df_compat_with_invoker(NULL, o, 12) == 0, "invoker: does not fit (no room for the terminator): 0"); + CHECK(df_compat_with_invoker(NULL, o, 13) == 12 && !o[12], "invoker: fits exactly with the terminator"); + CHECK(df_compat_with_invoker("WIN7RTM", o, 20) == 0, "invoker: the appended layer does not fit: 0"); + CHECK(df_compat_with_invoker("WIN7RTM", o, 21) == 20, "invoker: the appended layer fits exactly"); + CHECK(df_compat_with_invoker("0123456789012345678901234567890123456789012345678901234567890123456789", o, sizeof(o)) == 0, "invoker: the old value alone does not fit: 0"); + n = df_compat_with_invoker("WIN7RTM\tWINXPSP3", o, sizeof(o)); CHECK(!lstrcmpA(o, "WIN7RTM\tWINXPSP3 RunAsInvoker"), "invoker: words are separated by tabs too"); + n = df_compat_with_invoker("WIN7RTM\trunasinvoker", o, sizeof(o)); CHECK(n == (int)lstrlenA("WIN7RTM\trunasinvoker") && !lstrcmpA(o, "WIN7RTM\trunasinvoker"), "invoker: already there after a tab, not added again"); + n = df_compat_with_invoker("RunAsInvoker\tWIN7RTM", o, sizeof(o)); CHECK(n == (int)lstrlenA("RunAsInvoker\tWIN7RTM"), "invoker: already there before a tab, not added again"); + n = df_compat_with_invoker("WIN7RTM\t", o, sizeof(o)); CHECK(!lstrcmpA(o, "WIN7RTM\tRunAsInvoker"), "invoker: a trailing tab gives no extra blank"); + n = df_compat_with_invoker("\t \t", o, sizeof(o)); CHECK(n == (int)lstrlenA("\t \t") + 12 + 0 && o[n - 1] == 'r', "invoker: only blanks: kept, the layer follows"); } printf("%d passed, %d failed\n", g_pass, g_fail); return g_fail ? 1 : 0; } diff --git a/tests/test_driver.c b/tests/test_driver.c index 29d9ff9..ad437a0 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -780,6 +780,82 @@ static void *read_wizard(DWORD *n) return all; } +// The definition of the function that starts with `sig` (a declaration, which has a ';' after the signature, is skipped): *end is its closing brace in +// column 0, or the end of the line for a one-line function. NULL when there is none. (The sources have LF or CRLF line ends, depending on the checkout.) +static char *fn_body(char *t, const char *sig, char **end) +{ + char *a = t; size_t n = strlen(sig); + while ((a = strstr(a, sig)) != NULL) { + char *q = a + n, *le; + while (*q == ' ') q++; + if (*q == '\r' || *q == '\n' || *q == '{' || *q == '/') { // the end of the line, the body, or a comment after the signature + le = strchr(a, '\n'); + *end = le && memchr(a, '{', (size_t)(le - a)) ? le : strstr(a, "\n}"); + return *end ? a : NULL; + } + a += n; + } + *end = NULL; + return NULL; +} +static BOOL in_range(char *a, char *end, const char *s) { char *p = a ? strstr(a, s) : NULL; return p && p < end; } +static int count_in(char *a, char *end, const char *s) { int n = 0; char *p = a; while (a && (p = strstr(p, s)) != NULL && p < end) { n++; p += lstrlenA(s); } return n; } + +// Source guards for the wizard's behaviour that only a window can show (the VM checklist covers the window itself). +static void test_wizard_rules(char *t) +{ + char *a, *end, *e2; + // 1. the accept box is shared by the V4 page and the license page: every page starts with it unticked, so a license is never pre-accepted + a = fn_body(t, "static void set_page(int p)", &end); + CHECK(a && in_range(a, end, "BM_SETCHECK, BST_UNCHECKED") && strstr(a, "BM_SETCHECK, BST_UNCHECKED") < strstr(a, "PAGES[p].enter()"), L"wizard: set_page unticks the accept box before any page is built"); + a = fn_body(t, "static void enter_ver4(void)", &end); + CHECK(a && !in_range(a, end, "BM_SETCHECK, BST_CHECKED"), L"wizard: the V4 page does not tick the box"); + a = fn_body(t, "static void enter_license(void)", &end); + CHECK(a && !in_range(a, end, "BM_SETCHECK, BST_CHECKED"), L"wizard: the license page does not tick the box"); + // 2. FLOW_MSI_ONLY ends on its own page whose primary button only ends the wizard (it used to be Done's "Close", which started DynaRun) + a = strstr(t, "[P_MSI_DONE]"); end = a ? strchr(a, '\n') : NULL; + CHECK(a && end && in_range(a, end, "enter_msi_done") && in_range(a, end, "end_wizard") && !in_range(a, end, "pri_done"), L"wizard: the MSI-only finish page's primary button just ends the wizard"); + a = strstr(t, "[P_DONE]"); end = a ? strchr(a, '\n') : NULL; + CHECK(a && end && in_range(a, end, "enter_done") && in_range(a, end, "pri_done"), L"wizard: the normal Done page still starts DynaRun"); + a = fn_body(t, "static void pri_done(void)", &end); + CHECK(a && in_range(a, end, "start_dynarun()"), L"wizard: pri_done starts DynaRun"); + a = fn_body(t, "static void enter_done(void)", &end); + CHECK(a && !in_range(a, end, "FLOW_MSI_ONLY"), L"wizard: the Done page has no MSI-only branch (the flow table picks the page)"); + a = strstr(t, "[FLOW_MSI_ONLY]"); end = a ? strchr(a, '\n') : NULL; + CHECK(a && end && in_range(a, end, "JOB_INSTALL_MSI") && in_range(a, end, "P_MSI_DONE"), L"wizard: FLOWS[] gives FLOW_MSI_ONLY its install job and its finish page"); + a = fn_body(t, "static void jd_install(int job, int rc)", &end); + CHECK(a && in_range(a, end, "FLOWS[wz.flow].done_page") && !in_range(a, end, "set_page(P_DONE)"), L"wizard: the page after an install comes from FLOWS[]"); + a = fn_body(t, "static void pri_license(void)", &end); + CHECK(a && end && in_range(a, end, "FLOWS[wz.flow].license_job") && !in_range(a, end, "FLOW_MSI_ONLY"), L"wizard: the license page's install job comes from FLOWS[]"); + // 3. the foreground rule: keep_up follows the foreground window before it looks at whether the wizard has it, and says nothing about other programs + a = fn_body(t, "static void keep_up(HWND h)", &end); + CHECK(a && in_range(a, end, "last = fg;") && strstr(a, "last = fg;") < strstr(a, "fg == h"), L"wizard: keep_up updates `last` before the early return for the wizard's own window"); + CHECK(a && !in_range(a, end, "belongs to") && !in_range(a, end, "%s") && !in_range(a, end, "wsprintfW"), L"wizard: keep_up logs no other program's name"); + CHECK(a && in_range(a, end, "an unrelated window has the foreground"), L"wizard: keep_up logs a generic line for an unrelated foreground window"); + // 4. the alert: only when the wizard is waiting for a click after the job's own handler has run + a = fn_body(t, "static void job_done(int job, int rc)", &end); + e2 = a ? strstr(a, "JOBS[job].on_done") : NULL; + CHECK(a && e2 && in_range(a, end, "waiting_for_user()") && strstr(a, "waiting_for_user()") > e2 && in_range(a, end, "alert_user()"), L"wizard: job_done alerts only if waiting_for_user() after on_done ran"); + a = fn_body(t, "static BOOL waiting_for_user(void)", &end); + CHECK(a && in_range(a, end, "!wz.busy") && in_range(a, end, "!wz.closing") && in_range(a, end, "IsWindow(g_hwnd)") && in_range(a, end, "P_DRV_REBOOT"), L"wizard: waiting_for_user: no job running, not closing, not the automatic restart"); + // 5. the tray icon goes on every way out: the window ends in one place, which clears it first + a = fn_body(t, "static void end_wizard(void)", &end); + CHECK(a && end && in_range(a, end, "alert_clear()") && in_range(a, end, "DestroyWindow(g_hwnd)") && strstr(a, "alert_clear()") < strstr(a, "DestroyWindow(g_hwnd)"), L"wizard: end_wizard clears the tray icon, then destroys the window"); + CHECK(count_in(t, t + lstrlenA(t), "DestroyWindow(g_hwnd)") == 1, L"wizard: DestroyWindow(g_hwnd) is called in end_wizard only"); + a = strstr(t, "case WM_DESTROY:"); end = a ? strchr(a, '\n') : NULL; + CHECK(a && end && in_range(a, end, "alert_clear()"), L"wizard: WM_DESTROY clears the tray icon too"); + // 6. the MSI's SHA-256 shown on the check page is what the elevated stage is held to + a = fn_body(t, "static void build_info(void)", &end); + CHECK(a && in_range(a, end, "wz.msi_hex") && in_range(a, end, "msi_info(wz.msi, ver, ARRAYSIZE(ver), mfr, ARRAYSIZE(mfr))"), L"wizard: build_info keeps the shown SHA-256 in wz.msi_hex and passes the real buffer sizes to msi_info"); + CHECK(strstr(t, "install_msi_only(&g_in.opts, g_in.msi, g_in.msi_hex)") && strstr(t, "g_in.msi, g_in.msi_hex, g_in.drvzip") && strstr(t, "install_all(&g_in.opts, &g_out.instres, g_in.msi, g_in.msi_hex)"), L"wizard: the install jobs hand the shown SHA-256 to the elevated stage"); + CHECK(strstr(t, "CopyMemory(g_in.msi_hex, wz.msi_hex"), L"wizard: the job gets its own copy of the shown SHA-256"); + // 7. table instead of aliases + CHECK(!strstr(t, "thr_cancel"), L"wizard: no thr_cancel alias (the V4 page's third button is on_close)"); + a = strstr(t, "[P_VER4]"); end = a ? strchr(a, '\n') : NULL; + CHECK(a && end && in_range(a, end, "sec_ver4") && in_range(a, end, "on_close"), L"wizard: the V4 page's Cancel is on_close in the table"); + CHECK(strstr(t, "Set by sec_ver4 (the V4 page)") != NULL && strstr(t, "skip_driver, sec_ver4") != NULL, L"wizard: the flow comment names sec_ver4 as where FLOW_MSI_ONLY is set"); +} + static void test_job_table(void) { char *t, *a, *end, *c, *next; DWORD n = 0; BYTE *f = (BYTE *)read_wizard(&n); int jobs = 0, rows = 0; @@ -829,6 +905,16 @@ static void test_job_table(void) 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, "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"); + // the check page's details start with the identity of the setup (source, MSI version, manufacturer), then the file (path, size, SHA-256) + a = strstr(t, "static void build_info(void)"); end = a ? strstr(a + 10, "\nstatic ") : NULL; + if (a && end) { + char *p1 = strstr(a, "TX(S_SOURCE)"), *p2 = strstr(a, "TX(S_PRODUCT_VERSION)"), *p3 = strstr(a, "TX(S_MANUFACTURER)"), *p4 = strstr(a, "TX(S_FILE)"), *p5 = strstr(a, "TX(S_SIZE)"), *p6 = strstr(a, "SHA-256"); + CHECK(p1 && p2 && p3 && p4 && p5 && p6 && p6 < end, L"build_info writes all of the lines"); + CHECK(p1 && p2 && p3 && p4 && p5 && p6 && p1 < p2 && p2 < p3 && p3 < p4 && p4 < p5 && p5 < p6, L"build_info: source, ProductVersion and Manufacturer come before path, size and SHA-256"); + } else CHECK(FALSE, L"build_info is in the wizard source"); + // the version gate has one constant for the tested version and one function that classifies + CHECK(strstr(t, "dynarun_ver_class(wz.ver)") != NULL && strstr(t, "3.26.0\"") == NULL, L"the wizard classifies with dynarun_ver_class and has no copy of the tested version"); + test_wizard_rules(t); release(f); } @@ -890,6 +976,36 @@ static void test_hint(void) CHECK(ver_compare(L"3.26.0", L"3.25.9") > 0 && ver_compare(L"3.25.9", L"3.26.0") < 0 && ver_compare(L"3.26.0", L"3.26.0") == 0, L"version compare: ordinary cases"); CHECK(ver_compare(L"3.26", L"3.26.0") == 0 && ver_compare(L"3.26.1", L"3.26.0") > 0 && ver_compare(L"10.0.0", L"9.9.9") > 0 && ver_compare(L"3.26.0.1", L"3.26.0") > 0, L"version compare: missing parts, numbers not text, four parts"); CHECK(ver_compare(L"abc", L"3.26.0") == 0 && ver_compare(L"3.26.0", L"") == 0 && ver_compare(L"", L"") == 0, L"version compare: what is not a version says nothing"); + // the class of an MSI ProductVersion (the wizard's version gate) + CHECK(dynarun_ver_class(L"3.26.0") == VER_TESTED && dynarun_ver_class(L"3.26") == VER_TESTED, L"version class: the tested version (a missing part counts as 0)"); + CHECK(dynarun_ver_class(L"3.25.0") == VER_UNTESTED && dynarun_ver_class(L"3.25.9") == VER_UNTESTED && dynarun_ver_class(L"3.27.0") == VER_UNTESTED + && dynarun_ver_class(L"3.26.1") == VER_UNTESTED && dynarun_ver_class(L"3.26.0.1") == VER_UNTESTED && dynarun_ver_class(L"3.9.9") == VER_UNTESTED, L"version class: another 3.x is untested"); + CHECK(dynarun_ver_class(L"4.0.0") == VER_V4PLUS && dynarun_ver_class(L"4") == VER_V4PLUS && dynarun_ver_class(L"10.1.2") == VER_V4PLUS && dynarun_ver_class(L"4.0.0.0") == VER_V4PLUS, L"version class: 4 and later (numbers, not text: 10 > 4)"); + CHECK(dynarun_ver_class(L"2.9.9") == VER_UNTESTED && dynarun_ver_class(L"0.0.1") == VER_UNTESTED, L"version class: below 3 is untested too"); + CHECK(dynarun_ver_class(L"") == VER_UNKNOWN && dynarun_ver_class(L"abc") == VER_UNKNOWN && dynarun_ver_class(L"v4.0.0") == VER_UNKNOWN && dynarun_ver_class(L".") == VER_UNKNOWN + && dynarun_ver_class(L" 3.26.0") == VER_UNKNOWN && dynarun_ver_class(NULL) == VER_UNKNOWN, L"version class: not a version is unknown"); + CHECK(dynarun_ver_class(L"3.26.0 beta") == VER_TESTED && dynarun_ver_class(L"4.0.0-rc1") == VER_V4PLUS, L"version class: a suffix after the numbers does not matter"); + CHECK(dynarun_ver_class(L"4.0.0.0.5") == VER_V4PLUS && dynarun_ver_class(L"1.2.3.4.5.6.7") == VER_UNTESTED && dynarun_ver_class(L"5.1.2.3.4.5") == VER_V4PLUS, L"version class: more than four parts: the first four decide, nothing overruns"); + CHECK(dynarun_ver_class(L"99999999999999999999") == VER_V4PLUS && dynarun_ver_class(L"3.99999999999999999999.1") == VER_UNTESTED && dynarun_ver_class(L"3.26.4294967295") == VER_UNTESTED + && dynarun_ver_class(L"4294967296.0") == VER_V4PLUS, L"version class: a huge part does not wrap around (it stays huge)"); + CHECK(dynarun_ver_class(L"0004.0.0") == VER_V4PLUS && dynarun_ver_class(L"03.026.000") == VER_TESTED, L"version class: leading zeros are numbers"); + CHECK(dynarun_ver_class(L"3..26") == VER_UNTESTED && dynarun_ver_class(L"3.") == VER_UNTESTED && dynarun_ver_class(L"4.") == VER_V4PLUS && dynarun_ver_class(L".4.0.0") == VER_UNKNOWN, L"version class: a dot without a digit ends the version"); + CHECK(ver_compare(L"3.26.0.0.7", L"3.26.0.0.2") == 0 && ver_compare(L"3.26.0.1.0", L"3.26.0") > 0, L"version compare: only four parts are compared"); + // the ANSI text of an MSI property as screen / log text (msi_text): always terminated, cut to the buffer, no control characters + { WCHAR o[8]; int i; + msi_text("3.26.0", o, 8); CHECK(!lstrcmpW(o, L"3.26.0"), L"msi text: a short value is copied"); + { WCHAR c[16]; msi_text("a\r\nb\tc\x1f" "d", c, 16); CHECK(!lstrcmpW(c, L"a b c d"), L"msi text: control characters become spaces"); } + msi_text("0123456789", o, 5); CHECK(!lstrcmpW(o, L"0123") && o[4] == 0, L"msi text: cut to the buffer, terminated"); + msi_text("0123456", o, 8); CHECK(!lstrcmpW(o, L"0123456"), L"msi text: exactly fits with its terminator"); + msi_text("01234567", o, 8); CHECK(!lstrcmpW(o, L"0123456") && o[7] == 0, L"msi text: one character too many: the last one is cut, still terminated"); + msi_text("abc", o, 1); CHECK(o[0] == 0, L"msi text: a buffer of one character gets the empty string"); + o[0] = L'x'; msi_text("abc", o, 0); CHECK(o[0] == L'x', L"msi text: no buffer, nothing is written"); + msi_text(NULL, o, 8); CHECK(o[0] == 0, L"msi text: NULL gives the empty string"); + msi_text("", o, 8); CHECK(o[0] == 0, L"msi text: an empty value"); + for (i = 0; i < 8; i++) o[i] = L'Z'; + msi_text("\x01\x02", o, 8); CHECK(!lstrcmpW(o, L" ") && o[2] == 0, L"msi text: only control characters: spaces, terminated"); + { char big[400]; WCHAR w[64]; for (i = 0; i < 399; i++) big[i] = (char)(i % 40 == 3 ? '\n' : 'x'); big[399] = 0; + msi_text(big, w, 64); CHECK(lstrlenW(w) == 63 && w[63] == 0 && !wcschr(w, L'\n'), L"msi text: a 399 character value in a 64 character buffer is cut and clean"); } } // the page CHECK(pkg_page_version(PAGE, (DWORD)strlen(PAGE), v, 40) && !lstrcmpW(v, L"3.26.0"), L"page: the version of the Dyna Run V3 Software row is found (not the manual, not the script)"); { static const char P2[] = "

The latest release of Dyna Run V3 control software Version 3.27.1 has been released.

"; diff --git a/tests/test_hardening.c b/tests/test_hardening.c index 2ca6c41..80ee749 100644 --- a/tests/test_hardening.c +++ b/tests/test_hardening.c @@ -845,13 +845,13 @@ 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-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 install_opts g_po; static BOOL g_pin_seen; static const WCHAR *g_dz, *g_da, *g_da4, *g_ms; static void parse_case(int argc, WCHAR **argv) { 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; + g_po = c.opts; g_pin_seen = c.pin; g_dz = c.dzip; g_da = c.daccept; g_da4 = c.daccept4; g_ms = c.msisha; } #define PARSE(...) do { WCHAR *av_[] = { __VA_ARGS__ }; parse_case((int)(sizeof(av_) / sizeof(av_[0])), av_); } while (0) static void test_parse_switches(void) @@ -913,6 +913,77 @@ static void test_parse_switches(void) } } +// /msi-sha256 <64 hex digits>: the SHA-256 the user saw on the check page, handed to the elevated stages that run Dyna Pro's MSI (/full, /msi-only). +// Rules: from argv[4] on only, case-insensitive, takes the next argument as its value; the value is kept only when it is exactly 64 hexadecimal digits (else +// consumed and dropped); the last valid one wins. The verb table marks the two verbs that need it (needs_msi_sha) and verb_run refuses them without it. +static int verb_case(int argc, WCHAR **argv) { cmdline c; c.argc = argc; c.argv = argv; cmd_parse(&c); return verb_run(&c); } +static int g_vrc; +#define VERB(...) do { WCHAR *vv_[] = { __VA_ARGS__ }; g_vrc = verb_case((int)(sizeof(vv_) / sizeof(vv_[0])), vv_); } while (0) +static void test_msi_sha(void) +{ + WCHAR h64[80], h63[80], h65[80], hup[80], hbad[80], hlast[80], p[MAX_PATH], hex[65]; int i; + lstrcpyW(h64, L"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); lstrcpyW(h63, h64); h63[63] = 0; + lstrcpyW(h65, h64); lstrcatW(h65, L"0"); lstrcpyW(hup, L"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"); + lstrcpyW(hbad, h64); hbad[10] = L'g'; lstrcpyW(hlast, h64); hlast[0] = L'f'; + // the validator + CHECK(hex64_ok(h64) && hex64_ok(hup), L"msi sha: 64 hexadecimal digits, either case"); + CHECK(!hex64_ok(h63) && !hex64_ok(h65) && !hex64_ok(hbad) && !hex64_ok(L"") && !hex64_ok(NULL), L"msi sha: 63 or 65 characters, a non-hex digit, empty and NULL are refused"); + { WCHAR sp[80]; lstrcpyW(sp, h64); sp[5] = L' '; CHECK(!hex64_ok(sp), L"msi sha: a blank inside is refused"); } + { WCHAR z[80]; lstrcpyW(z, h64); z[0] = L'\u0661'; CHECK(!hex64_ok(z), L"msi sha: a non-ASCII digit is refused"); } + // the command line + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", h64); + CHECK(g_ms == h64, L"msi sha: /msi-sha256 at argv[4] with 64 hex digits"); + PARSE(L"s", L"/msi-only", L"S-1", L"m.msi", L"/MSI-SHA256", hup, L"/quiet"); + CHECK(g_ms == hup && g_po.quiet, L"msi sha: case-insensitive switch and digits, the next switch still counts"); + PARSE(L"s", L"/full", L"S-1", L"/msi-sha256", h64); + CHECK(!g_ms, L"msi sha: at argv[3] it is ignored (index >= 4 only)"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256"); + CHECK(!g_ms, L"msi sha: without a value"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", h63); + CHECK(!g_ms, L"msi sha: 63 characters: dropped"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", h65); + CHECK(!g_ms, L"msi sha: 65 characters: dropped"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", hbad); + CHECK(!g_ms, L"msi sha: 64 characters with a non-hex digit: dropped"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", L"/quiet"); + CHECK(!g_ms && !g_po.quiet, L"msi sha: ODDITY like /unverified, it swallows a following switch as its value, which is no hash"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", h64, L"/msi-sha256", hlast); + CHECK(g_ms == hlast, L"msi sha: the last valid value wins"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/msi-sha256", h64, L"/msi-sha256", h63); + CHECK(g_ms == h64, L"msi sha: a later invalid value does not clear an earlier valid one"); + PARSE(L"s", L"/full", L"S-1", L"m.msi", L"/with-driver", L"z.zip", L"/unverified", h63, L"/msi-sha256", h64); + CHECK(g_ms == h64 && g_dz && !g_da, L"msi sha: independent of /with-driver and /unverified"); + PARSE(L"s", L"/machine", L"S-1", L"e"); + CHECK(!g_ms, L"msi sha: none given: NULL"); + // the verb table: the verbs that run Dyna Pro's MSI need it, and are refused (RC_FAIL, nothing run) without a valid one + { WCHAR *f4[] = { L"s", L"/full", L"S-1", L"m" }, *i4[] = { L"s", L"/msi-only", L"S-1", L"m" }, *m4[] = { L"s", L"/machine", L"S-1", L"e" }, *d2[] = { L"s", L"/driver-prep" }; + CHECK(verb_find(4, f4) && verb_find(4, f4)->needs_msi_sha && verb_find(4, i4) && verb_find(4, i4)->needs_msi_sha, L"msi sha: /full and /msi-only are marked needs_msi_sha in the verb table"); + CHECK(verb_find(4, m4) && !verb_find(4, m4)->needs_msi_sha && verb_find(2, d2) && !verb_find(2, d2)->needs_msi_sha, L"msi sha: /machine and the driver verbs are not"); } + VERB(L"s", L"/full", L"S-1", L"m.msi"); + CHECK(g_vrc == RC_FAIL, L"msi sha: /full without /msi-sha256 is refused"); + VERB(L"s", L"/msi-only", L"S-1", L"m.msi"); + CHECK(g_vrc == RC_FAIL, L"msi sha: /msi-only without /msi-sha256 is refused"); + VERB(L"s", L"/FULL", L"S-1", L"m.msi", L"/msi-sha256", h63); + CHECK(g_vrc == RC_FAIL, L"msi sha: /full with a 63 character value is refused"); + VERB(L"s", L"/msi-only", L"S-1", L"m.msi", L"/msi-sha256", hbad); + CHECK(g_vrc == RC_FAIL, L"msi sha: /msi-only with a non-hex value is refused"); + VERB(L"s", L"/full", L"S-1", L"/msi-sha256", h64); + CHECK(g_vrc == RC_FAIL, L"msi sha: a hash at argv[3] (where the MSI path belongs) does not count: refused"); + VERB(L"s", L"/full", L"S-1"); + CHECK(g_vrc == DRV_NOVERB, L"msi sha: /full with too few arguments is still no verb at all"); + // the staged copy must be the file that was shown: its SHA-256 is compared with the value (any case) + wsprintfW(p, L"%s\\sha1.msi", g_base); mkfile(g_base, L"sha1.msi", "pretend this is an MSI"); + CHECK(pkg_sha256(p, hex, NULL) && lstrlenW(hex) == 64, L"msi sha: the test file has a SHA-256"); + CHECK(staged_is_shown(p, hex), L"msi sha: a staged copy with the shown hash is accepted"); + { WCHAR up[80]; lstrcpyW(up, hex); for (i = 0; up[i]; i++) if (up[i] >= L'a' && up[i] <= L'f') up[i] = (WCHAR)(up[i] - 32); CHECK(staged_is_shown(p, up), L"msi sha: the hash is compared without regard to case"); } + CHECK(!staged_is_shown(p, h64), L"msi sha: another hash is refused"); + CHECK(!staged_is_shown(p, NULL) && !staged_is_shown(p, L"") && !staged_is_shown(p, h63), L"msi sha: no hash, an empty one or a short one is refused"); + mkfile(g_base, L"sha1.msi", "pretend this is an MSI, replaced"); + CHECK(!staged_is_shown(p, hex), L"msi sha: a file changed after the check page no longer matches"); + wsprintfW(p, L"%s\\sha-missing.msi", g_base); + CHECK(!staged_is_shown(p, hex), L"msi sha: a file that does not exist is refused"); +} + // 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) @@ -923,6 +994,8 @@ static void test_verb_table(void) 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"); + { WCHAR *i3[] = { L"s", L"/msi-only", L"S-1" }; WCHAR *i4[] = { L"s", L"/MSI-ONLY", L"S-1", L"m" }; + CHECK(verb_find(3, i3) == NULL && verb_find(4, i4) != NULL, L"verbs: /msi-only 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]"); } @@ -1010,6 +1083,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"[msi sha-256]\n"); test_msi_sha(); wprintf(L"[shortcut folder lengths]\n"); test_sf_fit(); wprintf(L"[result codes]\n"); test_result_codes(); { // clean up diff --git a/tests/test_shexec_match.c b/tests/test_shexec_match.c index a62a2db..a7dcae7 100644 --- a/tests/test_shexec_match.c +++ b/tests/test_shexec_match.c @@ -2,6 +2,7 @@ // (The exe name must not contain "setup": Windows installer detection would demand elevation.) #include #include "../src/setup_match.h" +#include "../src/install_chain.h" static int g_fail, g_run; @@ -96,6 +97,23 @@ int main(void) g_run++; if (!setup_helper_match("C:\\Setup_5.exe", d, 1) || setup_helper_match("C:\\x\\Setup_5.exe", d, 1)) { g_fail++; puts("FAIL: root folder"); } } + { // in_install_chain (src\install_chain.h): the programs whose windows the wizard stays behind while the elevated part works + static const struct { const WCHAR *name; BOOL want; } t[] = { + { L"msiexec.exe", TRUE }, { L"MSIEXEC.EXE", TRUE }, { L"consent.exe", TRUE }, { L"drvinst.exe", TRUE }, { L"pnputil.exe", TRUE }, { L"dpinst.exe", TRUE }, + { L"DynaRunFix-Setup.exe", TRUE }, { L"dynarunfix-setup.exe", TRUE }, { L"DynaRunFix-Setup-1234-5678-9.exe", TRUE }, { L"DynaRunFix-Setup", TRUE }, + { L"explorer.exe", FALSE }, { L"chrome.exe", FALSE }, { L"", FALSE }, { L"a", FALSE }, + { L"msiexec", FALSE }, { L"msiexec.exe.bak", FALSE }, { L"xmsiexec.exe", FALSE }, { L"consent.exe ", FALSE }, + { L"DynaRunFix-Setu", FALSE }, { L"DynaRunFix-Setu.exe", FALSE }, { L"DynaRunFix.exe", FALSE }, { L"xDynaRunFix-Setup.exe", FALSE }, + { L"DynaRunFix-Launcher.exe", FALSE }, { L"DynaRun V3.exe", FALSE }, + }; + int i; + for (i = 0; i < (int)(sizeof(t) / sizeof(t[0])); i++) { + g_run++; + if (in_install_chain(t[i].name) != t[i].want) { g_fail++; printf("FAIL: in_install_chain(%ls) want %d\n", t[i].name, t[i].want); } + } + g_run++; + if (in_install_chain(NULL)) { g_fail++; puts("FAIL: in_install_chain(NULL)"); } + } printf("%d checks, %d failed\n", g_run, g_fail); return g_fail ? 1 : 0; }
Dyna Pro USB Drivers 1.2MB