diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e5ed64..978789e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,50 @@ [繁體中文在下方](#更新紀錄) +## v1.7.0 — 2026-10-17 + +Added (setup wizard): + +- **Hint: a newer DynaRun V3.** With DynaRun installed and the computer online, the first page asks (in the background) whether Dyna Pro's software page names a newer + version than the installed one (the Windows Installer's record; the page's download table names 3.26.0 today, and the parser is tolerant: no version found means + no hint). If the page names none, the zip on the site is asked for its first byte and its ETag / length / date are compared with what the last completed download + recorded. A line and *Download new version* appear (the same acquire flow, then the normal install; a copy of an older package already on the computer is not + used for it). Nothing is automatic, nothing is blocked, each result is a log line. +- **Choose a driver by its folder:** for the driver, *Choose file...* takes a zip, or ftdibus.inf of an unpacked folder; either goes through the same two-layer check and warning page. +- **DynaRun and its fix installed, only the driver missing:** the first page offers just the driver (and *Reinstall the fix...*). "The fix is current" means the + uninstall entry names this version and the launcher and dll are in the install folder. + +Changed (setup wizard, DynaRun and driver): + +- **One acquire flow for the DynaRun setup and the driver** (same pages, same code): network check (Windows' network list; clearly disconnected goes to the error + page at once), look for a copy on the computer, download (connect timeout 10 s, receive 30 s, back-off unchanged) with progress, *Pause* / *Resume* and + *Continue download*, check, result. One error page for both (title "... could not be downloaded", the first real error, the manual steps, *Retry* / + *Continue download*, *Choose file...* and *Skip* only where the driver may be skipped). The duplicated error texts of the two flows are one function. +- **Buttons are the same on every page:** the primary action right-most (the default button), the way out directly left of it (*Cancel*, or *Skip* / *Later* / + *Close* where that is what leaving means), another way on left of that (*Choose file...*, *Download new version*), and ONE link-style action at the bottom left + (*Open website*, *Installed elsewhere...*, ...). The title-bar X and Esc do what *Cancel* does. Wording unified: *Open website*, *Choose file...*, *Retry*, + *Next*. The hash-warning page keeps *Cancel* as its default button on purpose. +- The safe unpacking, copying and checking of files named in a pinned table moved to `package.c` and is shared (the driver's 18 files; the DynaRun `Setup.msi` + goes through the shared size cap and free-target checks). +- Button wording: the page shown when key 7 was missed now offers *Retry* instead of *Try again* (one wording on every page). +- The driver documentation and the README now say that DynaRunFix does not check the digital signature of the driver files (it compares SHA-256 values and, for *Install anyway*, the Dyna Pro device ID in the INF). + +Fixed: + +- Cancelling the warning for a driver copy that was found automatically skips such copies from then on; a file or folder you chose with *Choose file...* is always checked as it is and does not affect that memory. +- *Download new version* now really downloads: before, an older DynaRun package already on the computer could be picked up instead and the old version installed again. +- A damaged DynaRun package found on the computer (not a usable zip file) is no longer offered again and again: the wizard says so and offers *Retry* (download) or *Choose file...*. +- With no network at all, a file you had chosen earlier could be deleted when the wizard was closed after a rejected local copy; the download path now always names the wizard's own file. +- The update hint and a download running at the same time could disturb each other (a wrong address or a spurious refusal); each request now has its own buffers. +- A very long driver file or folder name could overflow a buffer on the page that announces the restart-menu step (English text). +- If turning BitLocker protection back on failed, the wizard forgot that it had suspended it; it now keeps the note and tries again at the next cancel, continue or uninstall. +- Long temporary-folder or install-folder paths no longer risk running past a buffer when the setup copies itself from a network drive or backs up registry keys. +- The manual steps of the download error page come from what is being fetched (no driver wording on the DynaRun page); the outline on the restart-menu picture sits in + the gaps between the lines instead of over the text of lines 6 and 8; the last driver step says up front that Windows shows its red security window twice. +- Logs: a folder is called a folder, the folders are walked once, a text of several lines is several UTF-8 lines (a long one used to be dropped by a fixed buffer), and + `bcdedit /deletevalue ... -> 1` after a successful install is logged as "not present: nothing to remove" instead of looking like an error. +- Docs: the README's list of in-memory import redirections now also names `WaitForSingleObject` in `dynapro.dll` (opt-in USB switch only, for the `DPSetData` watchdog's helper thread). + ## v1.6.0 — 2026-10-17 Added (setup wizard, driver step): @@ -440,6 +484,36 @@ Changed and fixed (after a review of v1.4.0): ## 更新紀錄 +### v1.7.0 — 2026-10-17 + +新增(安裝精靈): + +- **提示:有新版 DynaRun V3。** DynaRun 已安裝且電腦有網路時,第一頁會在背景詢問 Dyna Pro 的軟體頁面有沒有比已安裝版本(Windows Installer 的記錄)更新的版本(頁面的下載表目前寫 3.26.0;解析很寬容:找不到版本就沒有提示)。頁面沒寫版本時,會向網站上的 zip 要第一個位元組,把它的 ETag / 長度 / 日期和上次完成的下載所記錄的比對。有的話會出現一行說明與「下載新版」(同一個取得流程,再走一般安裝;電腦上現有的舊版安裝檔不會被拿來用)。完全不會自動執行、不會擋任何事,每個結果是一行 log。 +- **驅動程式可以選資料夾:** 驅動程式的「選擇檔案…」可以選 zip,或已解開資料夾裡的 ftdibus.inf,走同樣的兩層檢查與警告頁。 +- **已安裝 DynaRun 與修正、只缺驅動程式:** 第一頁只提供驅動程式(以及「重新安裝修正…」)。「修正是最新的」指的是:解除安裝項目的版本就是這個安裝程式的版本,而且安裝資料夾裡有啟動器與 dll。 + +變更(安裝精靈,DynaRun 與驅動程式): + +- **DynaRun 安裝檔與驅動程式共用同一個取得流程**(同樣的頁面、同樣的程式):網路檢查(Windows 的網路清單;明確沒有連線就直接顯示錯誤頁)、找電腦上現有的、下載(連線逾時 10 秒、接收 30 秒,退避不變),附進度、「暫停」/「繼續」與「繼續下載」、檢查、結果。兩者共用一個錯誤頁(標題「無法自動下載…」、第一個真正的錯誤、手動步驟、「重試」/「繼續下載」、「選擇檔案…」,以及只有驅動程式可略過時才有的「略過」)。兩個流程重複的錯誤文字合併成一個函式。 +- **所有頁面的按鈕一致:** 主要動作在最右邊(預設按鈕),離開的方式緊鄰它的左邊(「取消」,離開的意義是略過/稍後/關閉時用該字),再往左是另一個前進的方式(「選擇檔案…」、「下載新版」),最左下角只有一個連結式動作(「開啟官網」、「已裝在其他位置…」…)。標題列的 X 與 Esc 的作用等同「取消」。用字統一:「開啟官網」、「選擇檔案…」、「重試」、「下一步」。雜湊警告頁刻意維持「取消」為預設按鈕。 +- 以固定表格指名的檔案之安全解壓、複製與檢查移到 `package.c` 共用(驅動程式的 18 個檔案;DynaRun 的 `Setup.msi` 走共用的大小上限與釋放目標檢查)。 +- 按鈕用字統一:沒按到 7 時的頁面提供「重試」,不再是「再試一次」。 +- 驅動程式說明與 README 現在寫明:DynaRunFix 不驗證驅動程式檔的數位簽章(只比對 SHA-256,「仍要安裝」時再看 INF 裡的 Dyna Pro 裝置 ID)。 + +修正: + +- 取消「自動找到的驅動程式副本」的警告後,之後的自動搜尋才會跳過這類副本;用「選擇檔案…」自己選的檔案或資料夾一律照實檢查,也不影響這個記憶。 +- 「下載新版」現在真的會下載:以前電腦上已有較舊的 DynaRun 安裝檔時,可能被直接拿來用,結果又裝回舊版。 +- 電腦上找到的 DynaRun 安裝檔如果是損毀的檔案,不再一直重複提供:精靈會說明,並提供「重試」(下載)或「選擇檔案…」。 +- 完全沒有網路時,先前拒絕了電腦上的副本,關閉精靈後可能刪到你自己選的檔案;現在下載路徑一律是精靈自己的檔案。 +- 新版提示和下載同時進行時可能互相干擾(連到錯的位址或誤判被拒絕);現在每個請求各用自己的緩衝區。 +- 過長的驅動程式檔名或資料夾名稱,可能讓說明重新開機選單那一步的頁面緩衝區溢位(英文介面)。 +- 重新開啟 BitLocker 保護失敗時,精靈會忘記自己曾經暫停它;現在會保留記錄,下次取消、繼續或解除安裝時再試一次。 +- 安裝程式從網路磁碟機複製自己、或備份登錄機碼時,過長的暫存資料夾或安裝資料夾路徑不再有超出緩衝區的風險。 +- 下載錯誤頁的手動步驟改由要取得的東西決定(DynaRun 頁面不再出現驅動程式的字眼);重新開機選單圖片上的外框改放在各行之間的空隙,不再壓到第 6、8 行的文字;驅動程式的最後一步會事先說明 Windows 會跳出紅色安全性視窗兩次。 +- Log:資料夾就叫資料夾、資料夾只走一遍、多行文字會寫成多行 UTF-8(太長的原本會被固定緩衝區整行丟掉),安裝成功後的 `bcdedit /deletevalue ... -> 1` 改記為「不存在:沒有東西要移除」,不再像是錯誤。 +- 文件:README 的記憶體內匯入導向清單現在也列出 `dynapro.dll` 的 `WaitForSingleObject`(只有開啟選用的 USB 開關時,供 `DPSetData` 監看的輔助執行緒使用)。 + ### v1.6.0 — 2026-10-17 新增(安裝精靈、驅動程式步驟): diff --git a/README.md b/README.md index 12329fd..cd96303 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ software (Dyna Pro Dynamometers, S68 and similar rigs) when it runs **natively o > APIs in `DynaRun V3.exe`, the other modules in DynaRun's folder and its OCX controls; the font-related imports of > `THBRes25.dll`, `PEGRP32E.dll`, `MSVBVM60.DLL`, `ole32`, `oleaut32`, `MFC42`, `comctl32` and `dynapro.dll` (delay-load imports > included); `CreateFileA` in `dynapro.dll` (to observe the device open; results are passed on unchanged) and, only with the -> opt-in USB switch, the `DPSetData` watchdog of `dynapro.dll` (below); and `LoadLibraryExA` / `LoadLibraryExW` in the graph control's own modules `Pesgo32e.ocx` and +> opt-in USB switch, `WaitForSingleObject` in `dynapro.dll` for the `DPSetData` watchdog (below; it acts on the watchdog's helper thread only: once a +> call is abandoned, that thread's infinite waits on an event return after 250 ms slices, every other call is passed on unchanged); and `LoadLibraryExA` / `LoadLibraryExW` in the graph control's own modules `Pesgo32e.ocx` and > `PEGRP32E.dll` (Gigasoft ProEssentials), only for the request for its language resource `PEGRC32D.dll`, see > [Graph menu in the wrong language](#graph-popup-menu-stays-english-without-administrator-rights); > - replaces the results of DynaRun's `GetProcAddress` lookups of `ShellExecuteA` and `DPConnect` with its own wrappers, and, @@ -86,6 +87,12 @@ whenever DynaRun starts its first-time setup helper (normally only on the very f ## What's new +v1.7.0 (released 2026-10-17): +- New: a hint on the first page when Dyna Pro's software page publishes a newer DynaRun V3 than the installed one, with *Download new version* (nothing is automatic). For the driver, *Choose file...* also takes the ftdibus.inf of an unpacked folder. +- New: when DynaRun and the fix are installed and only the driver is missing, the first page offers just the driver. +- Changed: DynaRun's setup and the driver share one download flow and one error page, and the buttons are the same on every page. +- Fix: *Download new version* really downloads; a damaged DynaRun package found on the computer is not offered again; a file you chose is no longer deleted when the wizard is closed without a network; the update hint and a download no longer disturb each other; over-long names and paths no longer risk overrunning buffers; a failed BitLocker resume is remembered and retried; cancelling the warning for an automatically found driver copy skips only such copies, a file you chose is always checked. + v1.6.0 (released 2026-10-17): - New: the setup wizard checks the driver before using it. It looks for a copy on this computer first (its own cache, the setup's folder, Downloads, Desktop, Documents; a zip or an unpacked folder) and downloads only when none matches. The 18 driver files are compared with a table of SHA-256 values; when they differ, a warning page lists them and *Install anyway* is offered (not the default). See [docs/DRIVER.md](docs/DRIVER.md). - New: the license page is two pages (a *Check the installer* page with the details of the setup file, then the license). Downloads retry by themselves, can be paused and continued, and show their progress; the driver's last page has *Start DynaRun*. @@ -178,12 +185,19 @@ All versions: [CHANGELOG.md](CHANGELOG.md). 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). - If DynaRun V3 is already installed, it only installs the fix. Windows asks once for permission: click **Yes**. + 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**. 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. + 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 + network at all the error page comes at once (no waiting through retries); a host that does not answer is given up after 10 seconds. The driver goes through + the same pages (a copy on the computer is looked for first, then the download); its *Choose file...* takes the zip, or ftdibus.inf in the folder where it is unpacked. The first page of the wizard has up to three check boxes: pin DynaRun to the taskbar, start DynaRun when Windows starts and, on Windows 10/11 when the driver is not installed yet, **Also install the Dyna Pro USB driver (one restart needed)** (see below). + If DynaRun V3 and this version of the fix are already installed and only the driver is missing, the first page offers just the driver (and + *Reinstall the fix...*). When DynaRun V3 is installed and the computer is online, the first page also asks Dyna Pro's software page in the background + 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. @@ -232,10 +246,11 @@ What the installer does for the fix: What DynaRunFix keeps on the machine besides the files above (uninstall removes these where it can; the files `com-copied.txt`, `com-backup`, `com-skipped.txt` and `setup.log` stay in the install folder, which then is not deleted): - `HKLM\SOFTWARE\DynaRunFix`: `InstallDir`; `DynaRunExe` (DynaRun's path, only when its folder and exe cannot be changed by ordinary users); `ManifestInstalled` (the manifest was added); `ExeOrigTimePath` / `ExeOrigTime` (only while the original modification time of `DynaRun V3.exe` still has to be restored); `TaskbarPolicy` / `TaskbarPolicySid` (the Windows 11 pin); and the subkey `Shortcuts` (backup of the original machine-wide shortcut files), besides the entry under `Uninstall`. -- `HKCU\Software\DynaRunFix` of each user who ran DynaRun through the fix: `Shortcuts` (backup of that user's original shortcut files); `UpdateChecked`, `UpdateVersion`, `UpdateSha256`, `UpdateSkip`, `UpdateAsk`, `UpdateETag`, `DownloadETag`, `DriverETag` (the update check and the downloads); `AutostartPending` (the sign-in start that waits for the end of the driver step); `NoticeElevatedLauncher`, `NoticeRunAsAdminLayer` (written only when you answer *No* to showing a one-time notice again). +- `HKCU\Software\DynaRunFix` of each user who ran DynaRun through the fix: `Shortcuts` (backup of that user's original shortcut files); `UpdateChecked`, `UpdateVersion`, `UpdateSha256`, `UpdateSkip`, `UpdateAsk`, `UpdateETag`, `DownloadETag`, `DriverETag` (the update check and the downloads); `AutostartPending` (the sign-in start that waits for the end of the driver step); `DownloadETagDone`, `DriverETagDone`, `UpdateETagDone` (what the last completed downloads were; the "newer DynaRun?" hint compares with the first); `NoticeElevatedLauncher`, `NoticeRunAsAdminLayer` (written only when you answer *No* to showing a one-time notice again). - On Windows 11 with the pin option: `StartLayoutFile` and `LockedStartLayout` = 1 under `HKEY_USERS\\Software\Policies\Microsoft\Windows\Explorer` and `TaskbarLayout.xml` in the install folder. `LockedStartLayout` is the value Microsoft documents together with `StartLayoutFile`: while both are set, Windows treats the layout as locked. They are removed on uninstall (or when you install again without the pin option) only if `StartLayoutFile` still names our file; an organization's own layout is never touched. - In the install folder besides the program files: `TaskbarLayout.xml` and `DynaRun V3.lnk` (the shortcut the pin points at; with the pin option), and `setup.log` (what the administrator step did or skipped). -- Only if the driver option was used: the folder `%ProgramData%\DynaRunFix` (changeable by administrators only) with a copy of the setup, the staged driver files in `driver\` (only the 18 files that get installed), `driver.log`, `bitlocker-before.txt` (secrets masked) and, only while an "Install anyway" is in force, `accepted.txt` (the SHA-256 of exactly the files accepted; deleted when the driver step ends); a scratch folder `%TEMP%\DynaRunFix\drvcheck` while a driver zip is looked into, the downloads `%TEMP%\DynaRunFix\*.zip` / `.part`, and `%TEMP%\DynaRunFix-setup.log` (time-stamped lines of the driver search and your choices, written by the wizard without administrator rights); the value `DriverBitLockerSuspended` = 1 under `HKLM\SOFTWARE\DynaRunFix` (only while BitLocker is suspended by the driver option); the scheduled task `DynaRunFix-DriverContinue` and the one-time boot option `onetimeadvancedoptions` (both only until the installation ends or is cancelled); and the BitLocker suspension itself (only if protection was on; see the driver option below). The driver, once installed, is part of Windows' driver store. +- Whenever the wizard runs: the folder `%TEMP%\DynaRunFix\` (the downloads `*.zip` / `.part`, the unpacked `Setup.msi`, and, while a driver zip is looked into, the scratch folder `drvcheck` and `drvdigest.tmp`; removed when the step ends) and `%TEMP%\DynaRunFix-setup.log` (time-stamped lines of the file searches, the update hint and your choices, written by the wizard without administrator rights). +- Only if the driver option was used: the folder `%ProgramData%\DynaRunFix` (changeable by administrators only) with a copy of the setup, the staged driver files in `driver\` (only the 18 files that get installed), `driver.log`, `bitlocker-before.txt` (secrets masked) and, only while an "Install anyway" is in force, `accepted.txt` (the SHA-256 of exactly the files accepted; deleted when the driver step ends); the value `DriverBitLockerSuspended` = 1 under `HKLM\SOFTWARE\DynaRunFix` (only while BitLocker is suspended by the driver option); the scheduled task `DynaRunFix-DriverContinue` and the one-time boot option `onetimeadvancedoptions` (both only until the installation ends or is cancelled); and the BitLocker suspension itself (only if protection was on; see the driver option below). The driver, once installed, is part of Windows' driver store. Uninstalling restores the original shortcut files and removes the manifest it added only if the file is identical to the installer's own copy (a manifest that differs is left alone); if restoring the exe's modification time fails, the record of the original time is kept and the restore is tried again at the next run; the uninstall runs from the installed program (the install folder is administrators-only; no copy is made in `%TEMP%`), and the install folder with its setup file is removed by Windows at the next restart if it is still in use; other accounts' @@ -248,9 +263,10 @@ in the install folder). DynaRun's exe and program content stay unchanged (the or Windows XP, 7, 10 and 11 are supported (the driver option needs Windows 10 or 11). Command-line options: - `/quiet`: no window; installs only the fix and needs DynaRun V3 installed (exit code 1 if it is not found). It never installs the driver. - `/close`: with `/quiet`, closes a running DynaRun first (see below). -- `/uninstall`: uninstalls (also what *Programs and Features* runs); `/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). +- `/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`. Exit codes of the setup: @@ -284,7 +300,7 @@ Up to three options on the first page of the wizard, all on by default (`/quiet` closed; not while the restart for the driver is still to come), so DynaRun does not start at the sign-in that continues the driver installation. - **Also install the Dyna Pro USB driver (one restart needed).** Shown only on Windows 10/11 and only when the driver is not installed yet. Steps with screenshots: [docs/DRIVER.md](docs/DRIVER.md). In order, the wizard - 1. uses a `Dyna Pro USB.zip` that is already on the computer (its own earlier download, the setup's folder, Downloads, Desktop, Documents; + 1. uses a `Dyna Pro USB.zip` (or the folder it is unpacked in) that is already on the computer (its own earlier download, the setup's folder, Downloads, Desktop, Documents; any file name) when it holds the right driver files, else downloads it (about 2 MB) over HTTPS from `dynapro.co.uk` only. The check is of the content: each of the 18 files Windows installs (2 INFs, 2 catalogs, the i386 / amd64 `.sys` and `.dll` files they name) must have the SHA-256 built into the program; other files of the zip are ignored and not unpacked. If they differ, a warning page says which and offers @@ -310,7 +326,7 @@ Up to three options on the first page of the wizard, all on by default (`/quiet` possible you are told to use the desktop icon. If you do not go on, choose *Cancel* in the wizard or run `DynaRunFix-Setup.exe /driver-cancel`: that takes back the task, the boot option and the BitLocker suspension. Nothing of this is done without ticking the option (or using `/driver`). - Other pages: *Check the installer* has an *Open Dyna Pro website* button; on Windows 8 and older (Windows 8 / 8.1, Vista / 7, XP) `/driver` shows *The driver + Other pages: *Check the installer* has an *Open website* button; on Windows 8 and older (Windows 8 / 8.1, Vista / 7, XP) `/driver` shows *The driver was not installed* with the manual steps for that Windows and an *Open the manual steps* button; if the driver is already installed it shows *The driver is already installed*. The driver search and your choices are logged to `%TEMP%\DynaRunFix-setup.log`. @@ -749,7 +765,7 @@ The taskbar pin technique for Windows 7 to 10 was described by Gee Law ([blog po > - 把該行程中已載入模組的匯入表項目導向自己的函式:`THBRes25.dll` 的 `PostMessageA`;Microsoft 的 `MSVBVM60.DLL` 與 `scrrun.dll` > 匯入的檔案屬性 API、`GetProcAddress` 與文字 API;`DynaRun V3.exe`、DynaRun 資料夾內其他模組及其 OCX 元件的字型 API; > `THBRes25.dll`、`PEGRP32E.dll`、`MSVBVM60.DLL`、`ole32`、`oleaut32`、`MFC42`、`comctl32` 與 `dynapro.dll` 中字型相關的匯入(含延遲載入的匯入);`dynapro.dll` 的 `CreateFileA`(只用來觀察裝置開啟, -> 結果原樣傳回)以及,只有在開啟選用的 USB 開關時才有的 `dynapro.dll` `DPSetData` 監看(見下文);還有繪圖元件自己的模組 `Pesgo32e.ocx` 與 `PEGRP32E.dll` +> 結果原樣傳回)以及,只有在開啟選用的 USB 開關時才有的 `dynapro.dll` 的 `WaitForSingleObject`,供 `DPSetData` 監看使用(見下文;只作用於監看的輔助執行緒:呼叫被放棄後,該執行緒對事件的無限等待改為每 250 毫秒傳回一次,其餘呼叫原樣傳遞);還有繪圖元件自己的模組 `Pesgo32e.ocx` 與 `PEGRP32E.dll` > (Gigasoft ProEssentials)的 `LoadLibraryExA` / `LoadLibraryExW`,只針對它索取語言資源檔 `PEGRC32D.dll` 的那一次要求,見 > [沒有系統管理員權限時,圖表右鍵選單是英文](#沒有系統管理員權限時圖表右鍵選單是英文); > - 把 DynaRun 以 `GetProcAddress` 查到的 `ShellExecuteA`、`DPConnect` 換成自己的包裝函式,只有在開啟選用的 USB 開關時才再加上 `DPSetData`; @@ -791,6 +807,12 @@ DynaRun 執行中不會裁剪,所以單次執行期間檔案可能超過 10 MB; log 只記模組與 API 名稱、不含模組或匯入位址(可能出現視窗與 GDI 控制代碼的值),寫入時不跟隨符號連結或連接點,但可能含有檔案路徑與視窗標題,貼到網路上之前請先檢查。同一個 dll 也修正[首次設定](#首次設定)與 [OneDrive 檔案打不開](#部分-dpr-打不開file-run-properties-全空沒有曲線)的問題。 ### 更新內容 +v1.7.0(2026-10-17 發行): +- 新增:Dyna Pro 的軟體頁面有比已安裝版本更新的 DynaRun V3 時,第一頁會提示並提供「下載新版」(完全不會自動執行)。驅動程式的「選擇檔案…」也可以選已解開資料夾裡的 ftdibus.inf。 +- 新增:已安裝 DynaRun 與修正、只缺驅動程式時,第一頁只提供驅動程式。 +- 變更:DynaRun 安裝檔與驅動程式共用同一個下載流程與錯誤頁,所有頁面的按鈕一致。 +- 修正:「下載新版」現在真的會下載;電腦上找到損毀的 DynaRun 安裝檔不再一直重複提供;沒有網路時關閉精靈不會刪到自己選的檔案;新版提示和下載不再互相干擾;過長的名稱與路徑不再有超出緩衝區的風險;重新開啟 BitLocker 失敗時會記住並再試;取消自動找到的驅動程式副本的警告只會跳過這類副本,自己選的檔案一律照實檢查。 + v1.6.0(2026-10-17 發行): - 新增:安裝精靈在使用驅動程式前會先檢查。先找電腦上現有的(它自己的暫存、安裝程式所在資料夾、下載、桌面、文件;zip 或已解開的資料夾),都不符才下載。18 個驅動程式檔案會和內建的 SHA-256 表比對;不同時警告頁會列出來,並提供「仍要安裝」(不是預設)。見 [docs/DRIVER.md](docs/DRIVER.md#繁體中文)。 - 新增:授權合約頁拆成兩頁(先是列出安裝檔資訊的「確認安裝檔」頁,再是授權合約)。下載會自動重試、可暫停與繼續,並顯示進度;驅動程式的最後一頁有「開始使用 DynaRun」。 @@ -864,11 +886,12 @@ v1.2.2: 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 時,只會安裝修正。 + 先顯示「確認安裝檔」頁(檔案的來源、完整路徑、大小、SHA-256,以及 MSI 的版本與製造商,放在可以複製文字的框裡),再顯示 Dyna Pro 的授權合約(取自安裝檔,由 DynaRunFix 自己顯示),你接受後才安裝 DynaRun V3 和修正(安裝檔裡的合約讀不出來時,必須勾選同意 Dyna Pro 的授權條款才能繼續)。已經裝好 DynaRun V3 時,只會安裝修正。DynaRun V3 裝在安裝程式找不到的資料夾時,第一頁有「已裝在其他位置…」可以指定 `DynaRun V3.exe`。 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 會自己重新啟動。 @@ -899,10 +922,11 @@ DynaRunFix 不包含任何 Dyna Pro 的檔案:安裝檔來自 Dyna Pro 官網 DynaRunFix 除了上述檔案之外,還會在這台電腦上留下這些東西(解除安裝會盡可能移除;`com-copied.txt`、`com-backup`、`com-skipped.txt` 與 `setup.log` 會留在安裝資料夾,該資料夾因此不會被刪除): - `HKLM\SOFTWARE\DynaRunFix`:`InstallDir`;`DynaRunExe`(DynaRun 的路徑,只有在它的資料夾和 exe 一般使用者無法更改時才記錄);`ManifestInstalled`(已加上 manifest);`ExeOrigTimePath`/`ExeOrigTime`(只在 `DynaRun V3.exe` 的原始修改時間還需要還原時存在);`TaskbarPolicy`/`TaskbarPolicySid`(Windows 11 的釘選);子機碼 `Shortcuts`(全機捷徑檔原本內容的備份);另外還有 `Uninstall` 底下的項目。 -- 每位透過修正執行 DynaRun 的使用者的 `HKCU\Software\DynaRunFix`:`Shortcuts`(該使用者原本捷徑檔的備份);`UpdateChecked`、`UpdateVersion`、`UpdateSha256`、`UpdateSkip`、`UpdateAsk`、`UpdateETag`、`DownloadETag`、`DriverETag`(更新檢查與下載);`AutostartPending`(等驅動程式步驟結束才寫入的開機自動啟動);`NoticeElevatedLauncher`、`NoticeRunAsAdminLayer`(只有在你對一次性通知回答「否」之後才寫入)。 +- 每位透過修正執行 DynaRun 的使用者的 `HKCU\Software\DynaRunFix`:`Shortcuts`(該使用者原本捷徑檔的備份);`UpdateChecked`、`UpdateVersion`、`UpdateSha256`、`UpdateSkip`、`UpdateAsk`、`UpdateETag`、`DownloadETag`、`DriverETag`(更新檢查與下載);`AutostartPending`(等驅動程式步驟結束才寫入的開機自動啟動);`DownloadETagDone`、`DriverETagDone`、`UpdateETagDone`(最後一次完成的下載是什麼;「有新版 DynaRun?」的提示會和第一個比對);`NoticeElevatedLauncher`、`NoticeRunAsAdminLayer`(只有在你對一次性通知回答「否」之後才寫入)。 - Windows 11 且選了釘選時:`HKEY_USERS\<使用者>\Software\Policies\Microsoft\Windows\Explorer` 下的 `StartLayoutFile` 與 `LockedStartLayout` = 1,以及安裝資料夾裡的 `TaskbarLayout.xml`。`LockedStartLayout` 是微軟文件中與 `StartLayoutFile` 搭配的值:兩者都設定時,Windows 會把這個配置視為鎖定。解除安裝(或不選釘選重新安裝)時,只有在 `StartLayoutFile` 仍指向我們的檔案時才會移除;組織自己設定的配置絕不會動。 - 安裝資料夾裡除了程式檔之外:`TaskbarLayout.xml` 與 `DynaRun V3.lnk`(釘選所指向的捷徑,選了釘選時才有),以及 `setup.log`(系統管理員那一步做了或略過了什麼)。 -- 只有用過驅動程式選項時:資料夾 `%ProgramData%\DynaRunFix`(只有系統管理員能更改),內有安裝程式的副本、暫存的驅動程式檔案(`driver\`,只有會被安裝的 18 個檔案)、`driver.log`、`bitlocker-before.txt`(已遮蔽機密);只有在「仍要安裝」生效期間才有 `accepted.txt`(被接受的那些檔案的 SHA-256,驅動程式步驟結束時刪除);檢查驅動程式 zip 時的暫存資料夾 `%TEMP%\DynaRunFix\drvcheck`、下載的 `%TEMP%\DynaRunFix\*.zip` / `.part`,以及 `%TEMP%\DynaRunFix-setup.log`(精靈以一般權限寫入的驅動程式搜尋與你的選擇,每行有時間);`HKLM\SOFTWARE\DynaRunFix` 下的值 `DriverBitLockerSuspended` = 1(只在驅動程式選項暫停 BitLocker 期間);排程工作 `DynaRunFix-DriverContinue` 與一次性開機選項 `onetimeadvancedoptions`(兩者只在安裝結束或取消之前存在);以及 BitLocker 暫停本身(只有保護原本為開啟時,見下方的驅動程式選項)。驅動程式一旦安裝,就屬於 Windows 的驅動程式存放區。 +- 精靈每次執行都會有:資料夾 `%TEMP%\DynaRunFix\`(下載的 `*.zip` / `.part`、解開的 `Setup.msi`,以及檢查驅動程式 zip 時的暫存資料夾 `drvcheck` 和 `drvdigest.tmp`;該步驟結束時刪除)與 `%TEMP%\DynaRunFix-setup.log`(檔案搜尋、新版提示與你的選擇,每行有時間,精靈以一般權限寫入)。 +- 只有用過驅動程式選項時:資料夾 `%ProgramData%\DynaRunFix`(只有系統管理員能更改),內有安裝程式的副本、暫存的驅動程式檔案(`driver\`,只有會被安裝的 18 個檔案)、`driver.log`、`bitlocker-before.txt`(已遮蔽機密);只有在「仍要安裝」生效期間才有 `accepted.txt`(被接受的那些檔案的 SHA-256,驅動程式步驟結束時刪除);`HKLM\SOFTWARE\DynaRunFix` 下的值 `DriverBitLockerSuspended` = 1(只在驅動程式選項暫停 BitLocker 期間);排程工作 `DynaRunFix-DriverContinue` 與一次性開機選項 `onetimeadvancedoptions`(兩者只在安裝結束或取消之前存在);以及 BitLocker 暫停本身(只有保護原本為開啟時,見下方的驅動程式選項)。驅動程式一旦安裝,就屬於 Windows 的驅動程式存放區。 解除安裝會把捷徑檔還原成原本的內容,並移除它加上的 manifest(只有檔案與安裝程式自己的副本完全相同才會刪,不同的就留著);若還原 exe 修改時間失敗,會保留原始時間的紀錄,下次執行再試;解除安裝是從已安裝的程式執行(安裝資料夾只有系統管理員能寫入,不會在 `%TEMP%` 另存副本),若安裝資料夾與裡面的安裝程式仍在使用中,會由 Windows 在下次重新開機時移除;其他帳號仍指向修正版的捷徑會改回指向 `DynaRun V3.exe`。解除安裝時可選「只移除修正」或「全部移除」(接著用 Dyna Pro 自己的解除安裝程式移除 DynaRun V3)。解除安裝會先要求系統管理員權限,取消該確認的話什麼都不會改變;DynaRun 執行中時會先請你關閉。解除安裝也會移除驅動程式的開始功能表捷徑、尚未完成的驅動程式安裝(排程工作與一次性開機選項)以及 `%ProgramData%\DynaRunFix`(`driver.log` 也一併刪除)。如果 BitLocker 是 DynaRunFix 為了安裝驅動程式而暫停的、且仍在暫停,解除安裝會把它恢復(即使已沒有等待中的工作,使用 `/keep` 時也一樣)。已安裝的驅動程式會保留。舊版曾把 DynaRun 的 COM 註冊複製到全機登錄;解除安裝會保留這些複本及其紀錄(安裝資料夾裡的 `com-copied.txt`、`com-backup`)。 DynaRun 的 exe 與程式內容不會被改動(捷徑檔會備份並還原;唯一新增的檔案是 manifest,而且只在開啟 UTF-8 選項時),也不會修改你的資料檔。 @@ -910,9 +934,10 @@ DynaRun 的 exe 與程式內容不會被改動(捷徑檔會備份並還原; 支援 XP、7、10、11(驅動程式選項需要 Windows 10 或 11)。命令列參數: - `/quiet`:不顯示視窗;只安裝修正,需要已經裝好 DynaRun V3(找不到時以結束代碼 1 結束)。它絕不會安裝驅動程式。 - `/close`:配合 `/quiet`,先關閉執行中的 DynaRun(見下方)。 -- `/uninstall`:解除安裝(「程式和功能」執行的也是它);`/notaskbar`、`/noautostart`:不釘選到工作列、不在開機時啟動(見下方);`/keep`:讓釘選、開機自動啟動和桌面捷徑維持原樣(更新時使用)。 +- `/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`。 安裝程式的結束代碼: @@ -939,7 +964,7 @@ DynaRun 的 exe 與程式內容不會被改動(捷徑檔會備份並還原; 取消勾選後重新安裝會移除它;解除安裝會移除所有帳號的這個值、執行解除安裝的帳號、安裝時記錄的帳號,以及工作列配置檔仍是我們的其他帳號的工作列原則值(為此會短暫載入它們的登錄 hive),並把釘選的啟動器捷徑改回指向 `DynaRun V3.exe`。同時使用驅動程式選項時,這個值**不會**馬上寫入:先記在 `HKCU\Software\DynaRunFix` 的 `AutostartPending`, 等驅動程式步驟結束(裝好、失敗、取消、略過或關閉精靈;重新開機還沒發生時不算)才寫入,所以 DynaRun 不會在接續驅動程式安裝的那次登入時啟動。 - **同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)。** 只在 Windows 10/11 且尚未安裝驅動程式時顯示。步驟與畫面見 [docs/DRIVER.md](docs/DRIVER.md#繁體中文)。精靈依序會: - 1. 電腦上已有 `Dyna Pro USB.zip`(它自己先前下載的、安裝程式所在資料夾、下載、桌面、文件;檔名不限)且內容正確就直接使用,否則只從 `dynapro.co.uk` 以 HTTPS 下載(約 2 MB)。檢查的是內容:Windows 會安裝的 18 個檔案(2 個 INF、2 個 catalog、它們所指定的 i386 / amd64 `.sys` 與 `.dll`)各自要符合程式內建的 SHA-256;zip 裡的其他檔案不看也不解開。若不同,警告頁會說明是哪些檔案,並在套件看起來仍是 Dyna Pro 的時候提供「仍要安裝」(不是預設按鈕);選擇會被記錄; + 1. 電腦上已有 `Dyna Pro USB.zip` 或已解開它的資料夾(它自己先前下載的、安裝程式所在資料夾、下載、桌面、文件;檔名不限)且內容正確就直接使用,否則只從 `dynapro.co.uk` 以 HTTPS 下載(約 2 MB)。檢查的是內容:Windows 會安裝的 18 個檔案(2 個 INF、2 個 catalog、它們所指定的 i386 / amd64 `.sys` 與 `.dll`)各自要符合程式內建的 SHA-256;zip 裡的其他檔案不看也不解開。若不同,警告頁會說明是哪些檔案,並在套件看起來仍是 Dyna Pro 的時候提供「仍要安裝」(不是預設按鈕);選擇會被記錄; 2. 要求系統管理員權限,把安裝程式與解開的驅動程式存放在 `%ProgramData%\DynaRunFix`(只有系統管理員能更改的資料夾); 3. **只有 BitLocker 保護為開啟或無法讀取其狀態時**(或磁碟機仍在加密或解密中,含暫停,且有金鑰保護裝置;Windows 這時會報成「關閉」,但保護裝置已經在作用)才暫停它(對系統磁碟機執行 `manage-bde -protectors -disable` 並加上 `-RebootCount 3`,所以最多 3 次重新啟動後保護會自動恢復;安裝結束或取消時精靈也會讓它恢復, 並在 `HKLM\SOFTWARE\DynaRunFix` 的值 `DriverBitLockerSuspended` 記住這件事)。暫停期間磁碟機沒有受到保護。保護為關閉且磁碟機已完全加密(你自己暫停的),或本來就沒開的,維持原狀。暫停之後會檢查;如果沒有成功(重新開機選單會要求修復金鑰),精靈會顯示警告頁,說明查詢金鑰的方式(aka.ms/myrecoverykey),讓你選「仍要重新開機」或「稍後」(會撤回所有已設定的項目);「查詢修復金鑰」按鈕會開啟那個頁面。精靈的第一個驅動程式頁面會在做任何事之前先說明這一點。`driver.log` 與 `bitlocker-before.txt`(`manage-bde -status` 和 `-protectors -get` 的輸出,**寫入前先遮蔽修復密碼**)記錄看到與做了什麼; @@ -947,7 +972,7 @@ DynaRun 的 exe 與程式內容不會被改動(捷徑檔會備份並還原; 5. 你按「重新開機」後重新啟動電腦。你在選單按 **7**(停用驅動程式強制簽章);它只在那一次開機期間關閉; 6. 登入後安裝驅動程式(`pnputil`);Windows 會跳出紅色警告兩次(兩個驅動程式檔各一次),每次都請選「仍然安裝此驅動程式軟體」。接著移除工作與開機選項,BitLocker 也恢復。最後一頁提供「開始使用 DynaRun」,以你的一般使用者身分啟動(不是提升權限,儘管那一頁是以系統管理員權限執行);做不到時會請你改用桌面圖示。 不繼續的話,請在精靈按「取消」,或執行 `DynaRunFix-Setup.exe /driver-cancel`:它會撤回工作、開機選項與 BitLocker 暫停。沒有勾選該選項(或使用 `/driver`)就不會做這些事。 - 其他頁面:「確認安裝檔」頁有「開啟 Dyna Pro 官網」按鈕;在 Windows 8 與更舊的版本(Windows 8 / 8.1、Vista / 7、XP)上,`/driver` 會顯示「驅動程式沒有安裝」,附上該版 Windows 的手動步驟與「開啟手動安裝說明」按鈕;驅動程式已經安裝時會顯示「驅動程式已安裝」。驅動程式搜尋與你的選擇會記錄在 `%TEMP%\DynaRunFix-setup.log`。 + 其他頁面:「確認安裝檔」頁有「開啟官網」按鈕;在 Windows 8 與更舊的版本(Windows 8 / 8.1、Vista / 7、XP)上,`/driver` 會顯示「驅動程式沒有安裝」,附上該版 Windows 的手動步驟與「開啟手動安裝說明」按鈕;驅動程式已經安裝時會顯示「驅動程式已安裝」。驅動程式搜尋與你的選擇會記錄在 `%TEMP%\DynaRunFix-setup.log`。 **更新。** DynaRun 一律先開啟,更新不會拖慢它。每天一次,DynaRun 啟動後,安裝好的安裝程式會在背景向 GitHub API 查詢本專案的最新版本(不會送出其他資料)。 檢查只用 HTTPS,而且只連 GitHub 自己的主機(API、`github.com` 與 GitHub 的發行檔案主機;每一次重新導向都會檢查,查詢與下載都一樣)。 diff --git a/docs/DRIVER.md b/docs/DRIVER.md index fe2f1fe..dd9cc2a 100644 --- a/docs/DRIVER.md +++ b/docs/DRIVER.md @@ -25,9 +25,12 @@ available. Dyna Pro's (all 18 files present, the bus INF names the Dyna Pro device id); otherwise the manual method below is the way. Choosing **Install anyway** applies to exactly that zip (or folder) for this one installation and is logged in `driver.log`; while it is in force the staging folder holds `accepted.txt` with the SHA-256 of exactly the files that were staged, and it is deleted when the driver step ends. If you cancel the warning for a - copy found on this computer, later attempts (including resume and retry) download the driver from the Dyna Pro website instead; copies that differ are not offered again until the wizard is started again. -* The download page has *Pause* / *Resume*, retries by itself (after 1, 2, 4, 8 and 15 seconds) and, if it still fails, goes back with the first error, - **Continue download** (from the part that is already there) and **Download manually**. + copy found on this computer, later attempts (including resume and retry) download the driver from the Dyna Pro website instead; copies that differ are not offered again until the wizard is started again. Only copies the wizard found by itself are skipped this way: a file or folder you chose with **Choose file...** is always checked as it is, and the memory of the skipped copies does not apply to it. +* DynaRunFix does not check the digital signature of the driver files. It compares their SHA-256 values with the ones built into the program and, for **Install anyway**, checks that the bus INF names the Dyna Pro device ID. Whether to trust the source of a package that differs is your own decision. +* The download page (the same as for the DynaRun setup) has *Pause* / *Resume*, retries by itself (after 1, 2, 4, 8 and 15 seconds) and, if it still fails, shows + the first error with **Continue download** (from the part that is already there), **Open website**, **Choose file...** (the zip, or ftdibus.inf in the folder where it is + unpacked: the same check follows) and, in the main installer, **Skip**. If Windows says the computer has no network at all, that page comes at once. +* If DynaRun V3 and the fix are already installed and only the driver is missing, the installer's first page offers just the driver. * Windows asks for administrator permission (UAC). On an administrator account that is once; a standard user may be asked for an administrator's password again after the restart. If BitLocker protection is on (or its status cannot be read), the wizard suspends it until the driver is installed (at most 3 restarts as a safety net, then it is resumed automatically; it is also resumed when the installation finishes or you cancel), @@ -38,7 +41,7 @@ available. warning page (Windows will probably ask for the recovery key; find it at https://aka.ms/myrecoverykey) and lets you choose **Restart anyway** or **Later**, which takes everything back; **Find the recovery key** opens that page. `driver.log` and `bitlocker-before.txt` next to it (the output of `manage-bde -status` and `-protectors -get`, recovery passwords masked before they are written) show what the wizard saw and did. -* Other wizard pages: *Check the installer* (before the license page of the main installer) has an **Open Dyna Pro website** button. On Windows 8 and older +* Other wizard pages: *Check the installer* (before the license page of the main installer) has an **Open website** button. On Windows 8 and older (Windows 8 / 8.1, Vista / 7, XP) `/driver` shows **The driver was not installed** with the manual steps for that Windows and an **Open the manual steps** button (this document). If the driver is already installed, the wizard shows **The driver is already installed**. * After the restart, press **7** in the start-up menu (*Disable driver signature enforcement*). The wizard shows the page below; @@ -48,7 +51,7 @@ available. |---| | ![Startup Settings: press 7, Disable driver signature enforcement](img/driver-press-7.png) | -* If you miss 7, the wizard says so after you sign in and offers **Try again**. +* If you miss 7, the wizard says so after you sign in and offers **Retry**. * Then Windows shows its red warning **twice** (once for each of the two driver files, ftdibus and ftdiport): choose **Install this driver software anyway** each time. Driver signature enforcement comes back at the next normal restart. The last wizard page offers **Start DynaRun**, which starts it as your normal user (not elevated). @@ -200,7 +203,8 @@ DynaRunFix 安裝程式可以幫你做完下面的步驟。除非它說做不到 * 在主安裝程式中,保持勾選 **同時安裝 Dyna Pro USB 驅動程式(需重新開機一次)**(預設勾選;驅動程式已安裝或 Windows 低於 10 時會隱藏;Windows 8、7、Vista、XP 請用手動步驟,見[舊版 Windows](#舊版-windows))。 之後隨時也可以用開始功能表捷徑 **安裝 Dyna Pro USB 驅動程式**,或執行 `DynaRunFix-Setup.exe /driver`。 * 精靈會先找你已經有的「Dyna Pro USB.zip」(它自己先前下載的、精靈所在資料夾、下載資料夾與其下一層、桌面、文件;檔名不限,所以「Dyna Pro USB (1).zip」也算,已經解開的資料夾例如 Downloads\Dyna Pro USB 也算),內容正確就直接使用、不用下載;找不到才從 dynapro.co.uk 下載官方的 zip(不內附)。檢查的是**內容**:Windows 會安裝的 18 個檔案(兩個 INF、它們的兩個 catalog,以及它們所指定的 i386 與 amd64 的 `.sys` / `.dll`)必須各自符合程式內建的 SHA-256;zip 裡的其他檔案(readme、FT Clean、重複的「64-Bit Drivers」資料夾)不看,也只解開這 18 個。Windows 安裝的就是解開並檢查過的那份。 -* 如果檔案不同(可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改),精靈會顯示警告頁,列出來源、zip 的 SHA-256 和不同的檔案。只有套件看起來仍是 Dyna Pro 的(18 個檔案都在、bus INF 含 Dyna Pro 的裝置 ID)才會提供「仍要安裝」(不是預設按鈕);否則請用下方的手動方式。選「仍要安裝」只對那一個 zip、這一次安裝有效,並記錄在 `driver.log`。「仍要安裝」生效期間,暫存資料夾裡有 `accepted.txt`(只含實際暫存的那些檔案的 SHA-256),驅動程式步驟結束時刪除。如果你對這台電腦上找到的檔案取消了警告頁,之後的嘗試(包括繼續與重試)都會改從 Dyna Pro 官網下載;在重新開啟精靈之前,不會再提供內容不同的檔案。 +* 如果檔案不同(可能是 Dyna Pro 更新了驅動程式,也可能是檔案損毀或被竄改),精靈會顯示警告頁,列出來源、zip 的 SHA-256 和不同的檔案。只有套件看起來仍是 Dyna Pro 的(18 個檔案都在、bus INF 含 Dyna Pro 的裝置 ID)才會提供「仍要安裝」(不是預設按鈕);否則請用下方的手動方式。選「仍要安裝」只對那一個 zip、這一次安裝有效,並記錄在 `driver.log`。「仍要安裝」生效期間,暫存資料夾裡有 `accepted.txt`(只含實際暫存的那些檔案的 SHA-256),驅動程式步驟結束時刪除。如果你對這台電腦上找到的檔案取消了警告頁,之後的嘗試(包括繼續與重試)都會改從 Dyna Pro 官網下載;在重新開啟精靈之前,不會再提供內容不同的檔案。只有精靈自己找到的副本會這樣被跳過:你用 **選擇檔案…** 選的檔案或資料夾一律照實檢查,不受這個記憶影響。 +* DynaRunFix 不驗證驅動程式檔的數位簽章。它只比對檔案的 SHA-256 與程式內建的值,而在「仍要安裝」時檢查 bus INF 是否含 Dyna Pro 的裝置 ID。內容不同的套件來源可不可信,由你自己判斷。 * Windows 會要求系統管理員權限(UAC)。使用系統管理員帳戶時只會問一次;標準使用者在重新啟動後可能會再被要求輸入系統管理員密碼。若 BitLocker 保護為開啟(或無法讀取其狀態),精靈會暫停它直到驅動程式安裝完成(最多 3 次重新啟動作為保險,之後自動恢復;安裝完成或你按取消時也會恢復),設定一次性的進階開機選項,並登錄一個工作,在你登入後讓精靈繼續。暫停期間磁碟機沒有受到保護;如果你決定不繼續,請在精靈中按取消(或執行 `/driver-cancel`,見下方),讓保護恢復。 * 重新啟動後,在啟動選單按 **7**(停用驅動程式強制簽章)。精靈會顯示下面這個畫面,建議先用手機拍下來。圖上會標出「7) 停用驅動程式強制簽章」那一行並寫著 **按 7**。 @@ -208,10 +212,11 @@ DynaRunFix 安裝程式可以幫你做完下面的步驟。除非它說做不到 |---| | ![啟動設定:按 7,停用驅動程式強制簽章](img/driver-press-7.png) | -* 沒按到 7 的話,登入後精靈會告知,並提供 **再試一次**。 -* 下載頁有「暫停」/「繼續」,中斷時會自動重試(依序等 1、2、4、8、15 秒);仍然失敗就回到上一頁並顯示第一個錯誤,提供 **繼續下載**(從已經下載的部分接續)與 **開啟官網手動下載**。 +* 沒按到 7 的話,登入後精靈會告知,並提供 **重試**。 +* 下載頁(和 DynaRun 安裝檔的一樣)有「暫停」/「繼續」,中斷時會自動重試(依序等 1、2、4、8、15 秒);仍然失敗就顯示第一個錯誤,提供 **繼續下載**(從已經下載的部分接續)、**開啟官網**、**選擇檔案…**(zip,或已解開它的資料夾裡的 ftdibus.inf:接著做同樣的檢查),主安裝程式裡還有 **略過**。Windows 說電腦完全沒有網路時,那一頁會立刻出現。 +* 已安裝 DynaRun V3 與修正、只缺驅動程式時,安裝程式第一頁只提供驅動程式。 * BitLocker 還有一種情況也會暫停:磁碟機仍在加密或解密中(含暫停)且有金鑰保護裝置(Windows 會報成「關閉」,但重新開機選單仍會要求修復金鑰)。暫停之後會檢查;失敗時精靈會顯示警告頁(Windows 很可能會要求修復金鑰;可到 https://aka.ms/myrecoverykey 查詢),讓你選 **仍要重新開機** 或 **稍後**(會撤回所有已設定的項目);**查詢修復金鑰** 會開啟那個頁面。`driver.log` 與旁邊的 `bitlocker-before.txt`(`manage-bde -status` 和 `-protectors -get` 的輸出,修復密碼寫入前先遮蔽)記錄精靈看到與做了什麼。 -* 其他精靈頁面:「確認安裝檔」頁(在主安裝程式的授權合約頁之前)有 **開啟 Dyna Pro 官網** 按鈕。在 Windows 8 與更舊的版本(Windows 8 / 8.1、Vista / 7、XP)上,`/driver` 會顯示 **驅動程式沒有安裝**,附上該版 Windows 的手動步驟與 **開啟手動安裝說明** 按鈕(就是這份文件)。驅動程式已經安裝時,精靈會顯示 **驅動程式已安裝**。 +* 其他精靈頁面:「確認安裝檔」頁(在主安裝程式的授權合約頁之前)有 **開啟官網** 按鈕。在 Windows 8 與更舊的版本(Windows 8 / 8.1、Vista / 7、XP)上,`/driver` 會顯示 **驅動程式沒有安裝**,附上該版 Windows 的手動步驟與 **開啟手動安裝說明** 按鈕(就是這份文件)。驅動程式已經安裝時,精靈會顯示 **驅動程式已安裝**。 * 接著 Windows 會跳出紅色警告,而且會出現兩次(兩個驅動程式檔 ftdibus 與 ftdiport 各一次),每次都請選 **仍然安裝此驅動程式軟體**。驅動程式強制簽章在下次正常重新啟動後恢復。精靈最後一頁提供「開始使用 DynaRun」,以你的一般使用者身分啟動(不是提升權限)。 附註:精靈需要 Windows 10 或 11(舊版 Windows 請看下面的[舊版 Windows](#舊版-windows))。驅動程式檔案放在 `%ProgramData%\DynaRunFix\driver`,記錄檔是 `%ProgramData%\DynaRunFix\driver.log`。驅動程式搜尋與你的選擇會帶著時間記錄在 `%TEMP%\DynaRunFix-setup.log`。 diff --git a/src/driver.c b/src/driver.c index fd5ebdb..c959356 100644 --- a/src/driver.c +++ b/src/driver.c @@ -6,7 +6,8 @@ // while driver signature enforcement is off, which a user can switch on for one start of Windows ("Startup Settings", key 7). // This file automates that: // -// download driver_fetch: the zip from dynapro.co.uk (https, that host only), checked against the pinned SHA-256 below. +// find driver_fetch: a copy already on this computer first (the zip, or an unpacked folder), else the zip from dynapro.co.uk +// (https, that host only). The 18 driver files in it are checked one by one against the pinned SHA-256 values below. // prep (elevated, "/driver-prep ") stages the driver in %ProgramData%\DynaRunFix\driver (a folder that // only SYSTEM and the Administrators can change; an existing one is replaced, it may have been planted by an ordinary // user), copies this setup to %ProgramData%\DynaRunFix, suspends BitLocker (until the driver is installed; at most 3 restarts as a safety net), asks Windows for its @@ -299,7 +300,7 @@ const WCHAR *driver_pinned_sha256(void) { return DRIVER_ZIP_SHA256; } // and amd64\ftd2xx64.dll). Names are relative to that folder and compared without regard to case; '/' and '\' are the same. Size and SHA-256 are // those of the official "Dyna Pro USB.zip" (2335114 bytes, SHA-256 above, served 2026-10-15). Other files of the zip (readme, FT Clean, the // "64-Bit Drivers" copy, the root-level FTD2XX files, ftd2xx.inf) are not installed and not checked. -typedef struct { const WCHAR *rel; DWORD size; const WCHAR *sha; } drv_file; +typedef pkg_file drv_file; static const drv_file DRV_FILES[] = { { L"ftdibus.inf", 4868, L"47e413422655bf578e5a254432d739a816e8b2bfb90356a46f25a9f02559752c" }, { L"ftdibus.cat", 12273, L"c60f69484dfb97d81b5814f5b25844b892d5f0f20d7fe71c11fb9b3dd6bd8bfb" }, @@ -321,7 +322,7 @@ static const drv_file DRV_FILES[] = { { L"amd64/ftd2xx64.dll", 256392, L"0a776a6191c81d3682bb8d6784b45faea858a3dbfbe4e1345386068e02fb7d60" }, }; #define DRV_NFILES ((int)(sizeof(DRV_FILES) / sizeof(DRV_FILES[0]))) -#define DRV_MAXF 24 +#define DRV_MAXF PKG_MAXF int drv_file_count(void) { return DRV_NFILES; } @@ -359,144 +360,19 @@ BOOL drv_file_match(int i, DWORD size, const WCHAR *sha) return i >= 0 && i < DRV_NFILES && size == DRV_FILES[i].size && sha && !lstrcmpiW(sha, DRV_FILES[i].sha); } -// The zip entry name (as stored, '/' separators, not 0-terminated) is the pinned file i when it is that name at the end, after a folder boundary: -// "Dyna Pro USB/i386/ftdibus.sys" and a re-zipped "x/i386/ftdibus.sys" both are. -static int entry_index(const char *name, DWORD nlen) -{ - int i; DWORD rl, k; - for (i = 0; i < DRV_NFILES; i++) { - const WCHAR *r = DRV_FILES[i].rel; BOOL ok = TRUE; - rl = (DWORD)lstrlenW(r); - if (nlen < rl || (nlen > rl && name[nlen - rl - 1] != '/')) continue; - for (k = 0; k < rl && ok; k++) { - char a = name[nlen - rl + k], b = (char)r[k]; - if (a >= 'A' && a <= 'Z') a += 32; - if (b >= 'A' && b <= 'Z') b += 32; - if (a != b) ok = FALSE; - } - if (ok) return i; - } - return -1; -} - -typedef struct { zipent e[DRV_MAXF]; DWORD nlen[DRV_MAXF]; BOOL have[DRV_MAXF]; } zset; - -// Collects the zip entries that are pinned files. Of several entries of the same name (the "64-Bit Drivers" copy) the one with the shortest path -// is taken. An encrypted entry, another method than stored / deflate, or one that is too big does not count: that file is "missing". -static BOOL zset_visit(const char *name, DWORD nlen, const zipent *e, void *ctx) -{ - zset *z = (zset *)ctx; int i = entry_index(name, nlen); - if (i < 0 || (e->flags & 1) || (e->method != 0 && e->method != 8) || e->usize > DRV_MAX_FILE) return TRUE; - if (!z->have[i] || nlen < z->nlen[i]) { z->e[i] = *e; z->nlen[i] = nlen; z->have[i] = TRUE; } - return TRUE; -} - -// dir + sub + (with '\'); FALSE when it does not fit. sub: "\Dyna Pro USB\" (the staging layout and the unpacked zip) or "\" (an already -// unpacked folder: the folder that holds ftdibus.inf). -static BOOL file_path_at(WCHAR *out, const WCHAR *dir, const WCHAR *sub, int i) -{ - int n = lstrlenW(dir), k; - if (n + lstrlenW(sub) + lstrlenW(DRV_FILES[i].rel) + 1 >= MAX_PATH) return FALSE; - lstrcpyW(out, dir); lstrcatW(out, sub); lstrcatW(out, DRV_FILES[i].rel); - for (k = n; out[k]; k++) if (out[k] == '/') out[k] = '\\'; - return TRUE; -} - +// The shared machinery of package.c (safe unpacking, copying, checking) with the driver's table; these small wrappers keep the names the rest of this file +// and the tests use. +typedef pkg_check drv_check; +static int entry_index(const char *name, DWORD nlen) { return pkg_entry_index(DRV_FILES, DRV_NFILES, name, nlen); } +static BOOL file_path_at(WCHAR *out, const WCHAR *dir, const WCHAR *sub, int i) { return pkg_file_path(out, dir, sub, DRV_FILES, i); } static BOOL file_path(WCHAR *out, const WCHAR *dir, int i) { return file_path_at(out, dir, L"\\Dyna Pro USB\\", i); } - -static BOOL is_plain_dir(const WCHAR *p) -{ - DWORD a = GetFileAttributesW(p); - return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY) && !(a & FILE_ATTRIBUTE_REPARSE_POINT); -} - -// The folders of a staging area are ones we made: no link (reparse point) among them. -static BOOL dirs_plain(const WCHAR *dir) -{ - static const WCHAR *sub[] = { L"", L"\\Dyna Pro USB", L"\\Dyna Pro USB\\i386", L"\\Dyna Pro USB\\amd64" }; - WCHAR p[MAX_PATH]; int i; - for (i = 0; i < 4; i++) { - if (lstrlenW(dir) + lstrlenW(sub[i]) + 1 >= MAX_PATH) return FALSE; - lstrcpyW(p, dir); lstrcatW(p, sub[i]); - if (!is_plain_dir(p)) return FALSE; - } - return TRUE; -} - -// Takes the pinned files out of the zip into dir\Dyna Pro USB\ (dir exists; the folders below are created here, so they are new: a link cannot be in -// the way). The destination of every file is built from OUR table, never from the entry's name: nothing a zip says about paths is used. Only those -// files are extracted (the other ones of the zip never touch the disk); the limits are DRV_MAX_FILE / DRV_MAX_TOTAL; a file that is not there counts -// in *missing. Entries are checked against their CRC and size by zip_extract. PK_OK, or PK_FORMAT / PK_IO / PK_CANCEL. +static BOOL is_plain_dir(const WCHAR *p) { return pkg_is_plain_dir(p); } +static BOOL dirs_plain(const WCHAR *dir) { return pkg_dirs_plain(dir, L"Dyna Pro USB", DRV_FILES, DRV_NFILES); } static int stage_files(const WCHAR *zip, const WCHAR *dir, volatile LONG *cancel, int *missing) -{ - static zset z; WCHAR root[MAX_PATH], p[MAX_PATH]; int i, rc; DWORD total = 0, a; - *missing = 0; - zero(&z, sizeof(z)); - if (zip_walk(zip, zset_visit, &z) != PK_OK) return PK_FORMAT; - if (lstrlenW(dir) + 40 >= MAX_PATH) return PK_IO; - lstrcpyW(root, dir); lstrcatW(root, L"\\Dyna Pro USB"); - if (!CreateDirectoryW(root, NULL)) return PK_IO; - lstrcpyW(p, root); lstrcatW(p, L"\\i386"); if (!CreateDirectoryW(p, NULL)) return PK_IO; - lstrcpyW(p, root); lstrcatW(p, L"\\amd64"); if (!CreateDirectoryW(p, NULL)) return PK_IO; - if (!dirs_plain(dir)) return PK_FORMAT; - for (i = 0; i < DRV_NFILES; i++) { - if (!z.have[i]) { (*missing)++; continue; } - if ((total += z.e[i].usize) > DRV_MAX_TOTAL) return PK_FORMAT; // zip-bomb guard - if (!file_path(p, dir, i)) return PK_IO; - if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return PK_FORMAT; // the path is new: whatever is there is not ours - rc = zip_extract(&z.e[i], "", p, NULL, cancel); - if (rc == PK_PASSWORD) rc = PK_FORMAT; // CRC or size mismatch: the entry is damaged - if (rc != PK_OK) return rc; - a = GetFileAttributesW(p); - if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return PK_FORMAT; - } - return PK_OK; -} - -// The same for an unpacked folder (dir: the folder that holds ftdibus.inf): the pinned files are COPIED from there into dir\Dyna Pro USB\ (new -// folders made here, a path that exists is refused). The source folders on the way must be plain ones and a source file that is a link, a folder, or -// too big is refused; what is hashed afterwards is the copy, not the source. *missing: files that are not there. +{ return pkg_stage_files(zip, dir, L"Dyna Pro USB", DRV_FILES, DRV_NFILES, DRV_MAX_FILE, DRV_MAX_TOTAL, cancel, missing); } static int stage_from_dir(const WCHAR *src, const WCHAR *dir, int *missing) -{ - WCHAR root[MAX_PATH], p[MAX_PATH], s[MAX_PATH]; int i; DWORD total = 0, a; WIN32_FILE_ATTRIBUTE_DATA fa; - *missing = 0; - if (lstrlenW(src) + 30 >= MAX_PATH || lstrlenW(dir) + 40 >= MAX_PATH) return PK_IO; - lstrcpyW(s, src); if (!is_plain_dir(s)) return PK_FORMAT; - lstrcpyW(s, src); lstrcatW(s, L"\\i386"); if (!is_plain_dir(s)) return PK_FORMAT; - lstrcpyW(s, src); lstrcatW(s, L"\\amd64"); if (!is_plain_dir(s)) return PK_FORMAT; - lstrcpyW(root, dir); lstrcatW(root, L"\\Dyna Pro USB"); - if (!CreateDirectoryW(root, NULL)) return PK_IO; - lstrcpyW(p, root); lstrcatW(p, L"\\i386"); if (!CreateDirectoryW(p, NULL)) return PK_IO; - lstrcpyW(p, root); lstrcatW(p, L"\\amd64"); if (!CreateDirectoryW(p, NULL)) return PK_IO; - if (!dirs_plain(dir)) return PK_FORMAT; - for (i = 0; i < DRV_NFILES; i++) { - if (!file_path_at(s, src, L"\\", i) || !file_path(p, dir, i)) return PK_IO; - if (!GetFileAttributesExW(s, GetFileExInfoStandard, &fa)) { (*missing)++; continue; } - if ((fa.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || fa.nFileSizeHigh || fa.nFileSizeLow > DRV_MAX_FILE) return PK_FORMAT; - if ((total += fa.nFileSizeLow) > DRV_MAX_TOTAL) return PK_FORMAT; - if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return PK_FORMAT; - if (!CopyFileW(s, p, TRUE)) return PK_IO; - a = GetFileAttributesW(p); - if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return PK_FORMAT; - } - return PK_OK; -} - -// The staged files of dir against the pinned values (expect NULL) or against a list of accepted ones (the user's "install anyway"). -typedef struct { int ok, differ, missing; BYTE st[DRV_MAXF]; WCHAR hex[DRV_MAXF][65]; } drv_check; // st: 0 ok, 1 differs, 2 missing - -static void check_files_at(const WCHAR *dir, const WCHAR *sub, WCHAR (*expect)[65], drv_check *c) -{ - WCHAR p[MAX_PATH]; ULONGLONG sz; int i; DWORD a; - zero(c, sizeof(*c)); - for (i = 0; i < DRV_NFILES; i++) { - if (!file_path_at(p, dir, sub, i) || (a = GetFileAttributesW(p)) == INVALID_FILE_ATTRIBUTES) { c->st[i] = 2; c->missing++; continue; } - if ((a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || !pkg_sha256(p, c->hex[i], &sz)) { c->st[i] = 1; c->differ++; c->hex[i][0] = 0; continue; } - if (lstrcmpiW(c->hex[i], expect ? expect[i] : DRV_FILES[i].sha)) { c->st[i] = 1; c->differ++; continue; } - c->ok++; - } -} - +{ return pkg_stage_from_dir(src, dir, L"Dyna Pro USB", DRV_FILES, DRV_NFILES, DRV_MAX_FILE, DRV_MAX_TOTAL, missing); } +static void check_files_at(const WCHAR *dir, const WCHAR *sub, WCHAR (*expect)[65], drv_check *c) { pkg_check_files(dir, sub, DRV_FILES, DRV_NFILES, expect, c); } static void check_files(const WCHAR *dir, WCHAR (*expect)[65], drv_check *c) { check_files_at(dir, L"\\Dyna Pro USB\\", expect, c); } // Tests: how many of the pinned files in dir (laid out as in the zip: dir\Dyna Pro USB\...) are right; *differ / *missing: the others. @@ -528,8 +404,8 @@ static void check_report(const drv_check *c, WCHAR *out, int cch) static void zip_note(const WCHAR *path, const WCHAR *what) { - static WCHAR l[2 * MAX_PATH + 80]; - lstrcpyW(l, L"driver zip "); lstrcatW(l, path); lstrcatW(l, L": "); lstrcatW(l, what); + static WCHAR l[2 * MAX_PATH + 80]; DWORD a = GetFileAttributesW(path); + lstrcpyW(l, a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY) ? L"driver folder " : L"driver zip "); lstrcatW(l, path); lstrcatW(l, L": "); lstrcatW(l, what); user_log(l); } @@ -574,16 +450,8 @@ static BOOL zip_scan(const WCHAR *zip, BOOL *known, BOOL *sane, WCHAR *rep, int } // For the search of the user's folders: a zip whose pinned files are all there and right, whatever the zip is called or how it was packed. -static BOOL zip_has_all(const WCHAR *zip) // cheap: only the central directory is read -{ - static zset z; int i; - zero(&z, sizeof(z)); - if (zip_walk(zip, zset_visit, &z) != PK_OK) return FALSE; - for (i = 0; i < DRV_NFILES; i++) if (!z.have[i]) return FALSE; - return TRUE; -} +static BOOL zip_has_all(const WCHAR *zip) { return pkg_zip_has(zip, DRV_FILES, DRV_NFILES, DRV_MAX_FILE) == DRV_NFILES; } // cheap: only the central directory is read -// (Every candidate is logged with its size, the SHA-256 of the zip and what the content check found: %TEMP%\DynaRunFix-setup.log.) static BOOL zip_known_cb(const WCHAR *zip) { static WCHAR l[2 * MAX_PATH + 300], hex[65], rep[1500]; WIN32_FILE_ATTRIBUTE_DATA fa; ULONGLONG sz = 0; BOOL known = FALSE, sane = FALSE, ok; @@ -644,6 +512,8 @@ static BOOL digest_of(const drv_check *c, const WCHAR *tmp, WCHAR *hex) } // For the search: a folder whose pinned files are all there and right (counts like a zip with the right content; no zip hash exists, only a log line). +static WCHAR g_other_dir[MAX_PATH]; // the first folder met on the way with all 18 files of which some differ (see driver_fetch: no second walk) + static BOOL dir_known_cb(const WCHAR *dir) { static WCHAR l[2 * MAX_PATH + 200]; static drv_check c; @@ -651,6 +521,7 @@ static BOOL dir_known_cb(const WCHAR *dir) check_files_at(dir, L"\\", NULL, &c); wsprintfW(l, L"driver folder candidate %s: %d of 18 installed files match the pinned ones, %d differ, %d missing", dir, c.ok, c.differ, c.missing); user_log(l); + if (c.ok != DRV_NFILES && !g_other_dir[0] && c.missing == 0 && bus_inf_loose_at(dir)) lstrcpyW(g_other_dir, dir); return c.ok == DRV_NFILES; } @@ -692,81 +563,88 @@ static BOOL g_skip_other; / void driver_skip_local_other(void) { g_skip_other = TRUE; } +// Looks at a driver package that was found, downloaded or chosen: a zip, or the folder it is unpacked in (the folder that holds ftdibus.inf, the given one +// or one up to two levels below it; path is replaced by that folder). PK_OK: the 18 installed files are the pinned ones (how gets DRV_ZIPDIFF when the zip as a +// whole is not the official one). DRV_W_HASH: some differ but all are there and the bus INF names the Dyna Pro device id - the user is asked; hex / size: the zip's +// SHA-256 and size (for a folder: the digest of the files' hashes, size 0), rep: the files that differ. DRV_E_HASH: not a usable driver package. +BOOL driver_layout_ok(const WCHAR *dir) { return dir_layout_ok(dir); } + +int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch) +{ + static drv_check c; WCHAR d[MAX_PATH], tmp[MAX_PATH]; DWORD a = GetFileAttributesW(path); BOOL known, sane; + *how = DRV_SRC_LOCAL; hex[0] = 0; *size = 0; if (rep) rep[0] = 0; + if (a == INVALID_FILE_ATTRIBUTES) return DRV_E_HASH; + if (a & FILE_ATTRIBUTE_DIRECTORY) { + *how |= DRV_SRC_DIR; + if (!pkg_find_dir_in(path, 2, dir_layout_ok, d)) { zip_note(path, L"no ftdibus.inf with i386 and amd64 folders in it"); return DRV_E_HASH; } + lstrcpyW(path, d); + check_files_at(path, L"\\", NULL, &c); + if (c.ok == DRV_NFILES) return PK_OK; + if (c.missing == 0 && bus_inf_loose_at(path)) { + if (rep) check_report(&c, rep, repcch); + pkg_temp_dir(tmp); lstrcatW(tmp, L"\\drvdigest.tmp"); + if (digest_of(&c, tmp, hex)) { + log_differs(L"driver folder, its files differ from the pinned ones: ", path, hex, rep); + user_log(L"it passed the sanity checks: the user is asked"); + return DRV_W_HASH; + } + } + zip_note(path, L"a folder that is not a usable driver package"); + return DRV_E_HASH; + } + if (!zip_scan(path, &known, &sane, rep, repcch) || !pkg_sha256(path, hex, size)) { zip_note(path, L"cannot be read as a driver zip"); return DRV_E_HASH; } + if (known) { + if (lstrcmpiW(hex, DRIVER_ZIP_SHA256)) { *how |= DRV_ZIPDIFF; zip_note(path, L"the zip as a whole is not the official one (re-packed?), the driver files are identical"); } + return PK_OK; + } + log_differs(L"driver zip, its files differ from the pinned ones: ", path, hex, rep); + if (sane) { user_log(L"it passed the sanity checks: the user is asked"); return DRV_W_HASH; } + user_log(L"it failed the sanity checks: not offered"); + return DRV_E_HASH; +} + int driver_fetch(WCHAR *zip, progress_fn cb, volatile LONG *cancel, DWORD *err, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch) { - WCHAR l[2 * MAX_PATH + 160]; int rc; BOOL known, sane, readable, skip_other = g_skip_other; // kept for the whole run, so resume and retry do not offer the declined copy again + int rc, hw; BOOL skip_other = g_skip_other; // kept for the whole run, so resume and retry do not offer the declined copy again *how = DRV_SRC_DOWNLOADED; hex[0] = 0; *size = 0; if (rep) rep[0] = 0; g_pkg_log = user_log; // the folder search says which folders it looks in - user_log(L"driver_fetch: looking for a copy of the driver zip on this computer"); + g_other_dir[0] = 0; + user_log(L"driver_fetch: looking for a copy of the driver on this computer"); if (cb) cb((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts - // 1. a copy on this computer whose driver files are the pinned ones (any name, any packing): nothing to download + // 1. a copy whose driver files are the pinned ones (a zip of any name or packing, or an unpacked folder): nothing to download pkg_temp_dir(zip); lstrcatW(zip, L"\\" DRIVER_ZIP_NAME); - if ((exists(zip) && zip_known_cb(zip)) || pkg_find_zip_if(DRV_MAX_ZIP, zip_known_cb, zip)) { - *how = DRV_SRC_LOCAL; - pkg_sha256(zip, hex, size); - lstrcpyW(l, L"driver zip found on this computer, its driver files match the pinned ones, nothing is downloaded: "); lstrcatW(l, zip); user_log(l); - if (lstrcmpiW(hex, DRIVER_ZIP_SHA256)) { *how |= DRV_ZIPDIFF; zip_note(zip, L"the zip as a whole is not the official one (re-packed?), the driver files are identical"); } - return PK_OK; + if (((exists(zip) && zip_known_cb(zip)) || pkg_find_zip_if(DRV_MAX_ZIP, zip_known_cb, zip)) || + pkg_find_dirs(dir_known_cb, zip)) { + hw = driver_check(zip, how, hex, size, rep, repcch); + if (hw == PK_OK) { zip_note(zip, L"found on this computer, its driver files match the pinned ones: nothing is downloaded"); return PK_OK; } } - // 1b. the same, already unpacked (e.g. Downloads\Dyna Pro USB\): every pinned file is checked in place; the elevated step copies them - if (pkg_find_dirs(dir_known_cb, zip)) { - *how = DRV_SRC_LOCAL | DRV_SRC_DIR; - lstrcpyW(l, L"driver folder found on this computer, its driver files match the pinned ones, nothing is downloaded: "); lstrcatW(l, zip); user_log(l); - return PK_OK; - } - // 2. a zip called "Dyna Pro USB*" whose files differ goes the way of a download that differs: sanity checks, then the user is asked. - // One that is not a usable driver package is skipped (logged) and the official site is tried. + // 2. a zip called "Dyna Pro USB*", or a folder with all 18 files, whose files differ goes the way of a download that differs: sanity checks, then the + // user is asked. One that is not a usable driver package is skipped (logged) and the official site is tried. if (skip_other) user_log(L"the user declined a local copy that differs: not offered again, the official site is used"); - if (!skip_other && driver_local_other(zip)) { - readable = zip_scan(zip, &known, &sane, rep, repcch); - if (readable && sane && !known && pkg_sha256(zip, hex, size)) { - log_differs(L"driver zip on this computer, its files differ from the pinned ones: ", zip, hex, rep); - user_log(L"it passed the sanity checks: not downloaded, the user is asked"); - *how = DRV_SRC_LOCAL; - return DRV_W_HASH; - } + if (!skip_other && (driver_local_other(zip) || (g_other_dir[0] && lstrcpyW(zip, g_other_dir)))) { // (the folders were walked once, above) + hw = driver_check(zip, how, hex, size, rep, repcch); + if (hw == PK_OK || hw == DRV_W_HASH) return hw; zip_note(zip, L"differs from the pinned files and is not a usable driver package: skipped"); driver_forget(zip); - hex[0] = 0; *size = 0; if (rep) rep[0] = 0; - } - // 2b. an unpacked folder with all 18 files of which some differ: the same warning path (the digest of its file hashes stands for the zip hash) - if (!skip_other && pkg_find_dirs(dir_other_cb, zip)) { - static drv_check dc; WCHAR tmp[MAX_PATH]; - check_files_at(zip, L"\\", NULL, &dc); - if (rep) check_report(&dc, rep, repcch); - pkg_temp_dir(tmp); lstrcatW(tmp, L"\\drvdigest.tmp"); - if (digest_of(&dc, tmp, hex)) { - log_differs(L"driver folder on this computer, its files differ from the pinned ones: ", zip, hex, rep); - user_log(L"it passed the sanity checks: not downloaded, the user is asked"); - *how = DRV_SRC_LOCAL | DRV_SRC_DIR; - return DRV_W_HASH; - } - zip_note(zip, L"folder with other driver files: skipped"); - zip[0] = 0; hex[0] = 0; if (rep) rep[0] = 0; + *how = DRV_SRC_DOWNLOADED; hex[0] = 0; *size = 0; if (rep) rep[0] = 0; } // 3. the official site rc = pkg_fetch(DYNAPRO_USB_ZIP, DRIVER_ZIP_NAME, L"DriverETag", DYNAPRO_HOST, zip, cb, cancel, err); if (rc != PK_OK) return rc; if (cb) cb((DWORD)-1, 1); // the download is over: the page shows "checking" (not pausable) until we return - if (!zip_scan(zip, &known, &sane, rep, repcch) || !pkg_sha256(zip, hex, size)) { DeleteFileW(zip); return DRV_E_HASH; } - if (known) { // the driver files are the pinned ones - if (lstrcmpiW(hex, DRIVER_ZIP_SHA256)) { *how |= DRV_ZIPDIFF; zip_note(zip, L"the downloaded zip as a whole is not the official one, the driver files are identical"); } - return PK_OK; - } - log_differs(L"downloaded driver zip, its files differ from the pinned ones: ", zip, hex, rep); - if (sane) { user_log(L"it passed the sanity checks: the user is asked"); return DRV_W_HASH; } // the file stays for the user's choice - user_log(L"it failed the sanity checks: not offered"); - DeleteFileW(zip); - return DRV_E_HASH; + hw = driver_check(zip, how, hex, size, rep, repcch); + *how &= ~DRV_SRC_LOCAL; // it is our own download + if (hw == DRV_E_HASH) DeleteFileW(zip); // the file stays only when the user may still choose "install anyway" + return hw; } /* ---------- running the system tools ---------- */ static WCHAR g_logfile[MAX_PATH]; -static void drv_log(const WCHAR *line) +static void drv_log_line(const WCHAR *line) { - static char u[2048]; int n; HANDLE h; DWORD w; + static char u[4096]; int n; HANDLE h; DWORD w; if (!g_logfile[0] || (n = WideCharToMultiByte(CP_UTF8, 0, line, -1, u, sizeof(u) - 2, NULL, NULL)) <= 0) return; h = CreateFileW(g_logfile, FILE_APPEND_DATA, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (h == INVALID_HANDLE_VALUE) return; @@ -775,6 +653,19 @@ static void drv_log(const WCHAR *line) CloseHandle(h); } +// A text of several lines is written as several lines (each one fits the buffer; a long one used to be dropped whole). The file is UTF-8 throughout. +static void drv_log(const WCHAR *line) +{ + WCHAR seg[1024]; const WCHAR *p = line, *q; int n; + while (*p) { + for (q = p; *q && *q != '\n'; q++); + n = (int)(q - p); if (n > 1000) n = 1000; + while (n > 0 && p[n - 1] == '\r') n--; + if (n > 0) { lstrcpynW(seg, p, n + 1); drv_log_line(seg); } + p = *q ? q + 1 : q; + } +} + static void drv_log2(const WCHAR *what, const WCHAR *arg, DWORD v) { static WCHAR b[1200]; @@ -783,6 +674,9 @@ static void drv_log2(const WCHAR *what, const WCHAR *arg, DWORD v) } // "tool args", hidden, waited for; its exit code, or (DWORD)-1 when it could not be started or did not finish in 3 minutes. +static DWORD g_tool_ok_code; // an exit code that is not an error for the next run_tool (and what it means), see cancel_locked +static const WCHAR *g_tool_ok_text; + static DWORD run_tool(const WCHAR *tool, const WCHAR *args) { static WCHAR win[MAX_PATH], exe[MAX_PATH]; WCHAR *cmd; STARTUPINFOW si; PROCESS_INFORMATION pi; DWORD rc = (DWORD)-1; @@ -795,7 +689,11 @@ static DWORD run_tool(const WCHAR *tool, const WCHAR *args) else GetExitCodeProcess(pi.hProcess, &rc); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); } - drv_log2(tool, args, rc); + if (g_tool_ok_text && rc == g_tool_ok_code) { + static WCHAR l[2 * MAX_PATH + 240]; + lstrcpyW(l, tool); lstrcatW(l, L" "); lstrcatW(l, args); lstrcatW(l, L" -> "); lstrcatW(l, g_tool_ok_text); + drv_log(l); + } else drv_log2(tool, args, rc); release(cmd); return rc; } @@ -1187,12 +1085,16 @@ static void set_bde_flag(BOOL on) static void bde_resume(void) // BitLocker protection on again, if we are the ones who suspended it { - WCHAR a[80], dr[4]; DWORD rc; + WCHAR a[80], dr[4]; DWORD rc; int ps; if (!bde_flag()) return; sys_drive(dr); lstrcpyW(a, L"-protectors -enable "); lstrcatW(a, dr); - rc = run_tool(L"manage-bde.exe", a); // failure is ignored ("already on" is not an error that matters); the log has the code - if (rc) drv_log2(L"bitlocker resume failed", NULL, rc); + rc = run_tool(L"manage-bde.exe", a); // the log has the code + if (rc) { + drv_log2(L"bitlocker resume failed", NULL, rc); + ps = bde_protection(dr); + if (ps != BDE_PS_ON && ps != BDE_PS_NONE) return; // still not on: the flag stays, the next cancel / continue / uninstall tries again ("already on" and "no BitLocker here" clear it below) + } set_bde_flag(FALSE); } @@ -1200,7 +1102,9 @@ static void bde_resume(void) // BitLocker protection on again static void cancel_locked(void) { run_tool(L"schtasks.exe", L"/delete /tn " TASK_NAME L" /f"); + g_tool_ok_code = 1; g_tool_ok_text = L"not present (1: the start-up menu had already used the one-time option up): nothing to remove"; run_tool(L"bcdedit.exe", L"/deletevalue {current} onetimeadvancedoptions"); + g_tool_ok_text = NULL; { WCHAR b[MAX_PATH], f[MAX_PATH]; base_dir(b); if (accept_path(f, b)) DeleteFileW(f); } // an "install anyway" acceptance ends with the driver step bde_resume(); } diff --git a/src/package.c b/src/package.c index 571941d..59c6220 100644 --- a/src/package.c +++ b/src/package.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "setup.h" #include "version.h" #include "miniz.h" // inflate only (third_party/miniz, built with the MINIZ_NO_* options in build.cmd) @@ -15,6 +16,9 @@ #define WIDEN2(x) L##x #define WIDEN(x) WIDEN2(x) #define CHUNK 65536 +#ifndef INTERNET_OPTION_SECURE_PROTOCOLS +#define INTERNET_OPTION_SECURE_PROTOCOLS 184 +#endif #ifndef FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS #define FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS 0x00400000 #endif @@ -204,6 +208,14 @@ BOOL pkg_find_dirs(BOOL (*accept)(const WCHAR *dir), WCHAR *out) return FALSE; } +// The same walk below one given folder (the folder the user picked). +BOOL pkg_find_dir_in(const WCHAR *root, int depth, BOOL (*accept)(const WCHAR *dir), WCHAR *out) +{ + int budget = 600; + out[0] = 0; + return find_dir_in(root, depth, &budget, accept, out); +} + BOOL pkg_find_local(WCHAR *out) { WCHAR dirs[4][MAX_PATH]; DWORD i; candidate *best = alloc(sizeof(candidate)); @@ -217,6 +229,43 @@ BOOL pkg_find_local(WCHAR *out) return out[0] != 0; } +/* ---------- is there a network at all ---------- */ + +// NET_NO only when Windows says it is clearly disconnected (no adapter with a connection). "Local network only" or "no internet" is NOT NET_NO: behind a +// proxy, or at a captive portal, Windows says that although the download works, so it is just tried. NET_YES: Windows reports internet access. +int pkg_net_state(void) +{ + static const GUID clsid = { 0xDCB00C01, 0x570F, 0x4A9B, { 0x8D, 0x69, 0x19, 0x9F, 0xDB, 0xA5, 0x72, 0x3B } }; + static const GUID iid = { 0xDCB00000, 0x570F, 0x4A9B, { 0x8D, 0x69, 0x19, 0x9F, 0xDB, 0xA5, 0x72, 0x3B } }; + INetworkListManager *nlm = NULL; NLM_CONNECTIVITY c = NLM_CONNECTIVITY_DISCONNECTED; HRESULT hrc; int r = NET_UNKNOWN; BOOL asked = FALSE; DWORD f = 0; + hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); // RPC_E_CHANGED_MODE: already initialised another way, usable + if (SUCCEEDED(hrc) || hrc == RPC_E_CHANGED_MODE) { + if (SUCCEEDED(CoCreateInstance(&clsid, NULL, CLSCTX_ALL, &iid, (void **)&nlm))) { + if (SUCCEEDED(nlm->lpVtbl->GetConnectivity(nlm, &c))) { + asked = TRUE; + if (c == NLM_CONNECTIVITY_DISCONNECTED) r = NET_NO; + else if (c & (NLM_CONNECTIVITY_IPV4_INTERNET | NLM_CONNECTIVITY_IPV6_INTERNET)) r = NET_YES; + } + nlm->lpVtbl->Release(nlm); + } + if (SUCCEEDED(hrc)) CoUninitialize(); + } + if (!asked && !InternetGetConnectedState(&f, 0)) r = NET_NO; // no network list manager (Windows XP / Vista): the older, coarser check + return r; +} + +// A WinINet session for our requests: connect timeout and receive timeout in milliseconds, TLS 1.2 and later only (SP_PROT_TLS1_3_CLIENT | SP_PROT_TLS1_2_CLIENT; +// Windows 7 needs TLS 1.2 switched on this way; where the option is not supported (XP, Vista) the system default stays and a TLS 1.2-only site simply fails). +HINTERNET pkg_net_open(DWORD connect_ms, DWORD receive_ms) +{ + HINTERNET in = InternetOpenW(L"DynaRunFix-Setup/" WIDEN(DRF_VERSION), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); DWORD proto = 0x2800; + if (!in) return NULL; + InternetSetOptionW(in, INTERNET_OPTION_CONNECT_TIMEOUT, &connect_ms, sizeof(connect_ms)); + InternetSetOptionW(in, INTERNET_OPTION_RECEIVE_TIMEOUT, &receive_ms, sizeof(receive_ms)); + if (!InternetSetOptionW(in, INTERNET_OPTION_SECURE_PROTOCOLS, &proto, sizeof(proto))) { proto = 0x800; InternetSetOptionW(in, INTERNET_OPTION_SECURE_PROTOCOLS, &proto, sizeof(proto)); } + return in; +} + /* ---------- download (resumable; the server sends ETag and supports Range) ---------- */ static BOOL query_str(HINTERNET h, DWORD what, WCHAR *out, DWORD cch) @@ -271,10 +320,6 @@ static BOOL is_cert_error(DWORD e) #define MAX_DOWNLOAD (300UL * 1024UL * 1024UL) // nothing this program downloads is larger (the DynaRun zip is a few MB) #define MAX_HOPS 5 #define URLCCH 2100 -#ifndef INTERNET_OPTION_SECURE_PROTOCOLS -#define INTERNET_OPTION_SECURE_PROTOCOLS 184 -#endif - static DWORD parse_len(const WCHAR *s) // decimal number, saturating { ULONGLONG v = 0; @@ -308,9 +353,10 @@ BOOL pkg_redirect_target(const WCHAR *cur, const WCHAR *loc, const WCHAR *host, // Opens url without letting WinINet follow redirects: each hop is checked (https, and the pinned host) before a request is sent to it. // Returns the open request (status = its HTTP status, a 3xx without a Location included), or NULL: *refused when a URL was refused, // else *err is the Win32 error. Also used by the update check (update.c). -HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, const WCHAR *hdr, DWORD *status, DWORD *err, BOOL *refused) +static HINTERNET open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, const WCHAR *hdr, DWORD *status, DWORD *err, BOOL *refused, + WCHAR *cur, WCHAR *loc, WCHAR *nxt) { - static WCHAR cur[URLCCH], loc[URLCCH], nxt[URLCCH]; HINTERNET h; DWORD n, hop; + HINTERNET h; DWORD n, hop; *status = 0; *err = 0; *refused = FALSE; lstrcpynW(cur, url, URLCCH); for (hop = 0; hop <= MAX_HOPS; hop++) { @@ -321,7 +367,7 @@ HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, n = sizeof(*status); if (!HttpQueryInfoW(h, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, status, &n, NULL)) *status = 0; if (*status != 301 && *status != 302 && *status != 303 && *status != 307 && *status != 308) { - n = sizeof(loc); loc[0] = 0; // what the connection really is: still https on the host + n = URLCCH * sizeof(WCHAR); loc[0] = 0; // what the connection really is: still https on the host if (!InternetQueryOptionW(h, INTERNET_OPTION_URL, loc, &n) || !url_ok(loc, host)) { InternetCloseHandle(h); *refused = TRUE; return NULL; } return h; } @@ -336,6 +382,16 @@ HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, return NULL; } +// The three URL buffers belong to the call (the update hint and a download can run at the same time); they are on the heap: no C runtime here, so no stack probe for 12 KB. +HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, const WCHAR *hdr, DWORD *status, DWORD *err, BOOL *refused) +{ + WCHAR *u = alloc(3 * URLCCH * sizeof(WCHAR)); HINTERNET h; + if (!u) { *status = 0; *err = ERROR_OUTOFMEMORY; *refused = FALSE; return NULL; } + h = open_following(in, url, host, hdr, status, err, refused, u, u + URLCCH, u + 2 * URLCCH); + release(u); + return h; +} + static BOOL zip_nothing(const char *name, DWORD nlen, const zipent *e, void *ctx) { (void)name; (void)nlen; (void)e; (void)ctx; return TRUE; } static BOOL is_zip_name(const WCHAR *name) @@ -348,15 +404,16 @@ static BOOL is_zip_name(const WCHAR *name) // then accepted only if it is a complete archive (its central directory is there); other files are checked by the caller (update.c: SHA-256). int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const WCHAR *host, WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err) { - static WCHAR part[MAX_PATH], tag[200], oldtag[200], hdr[400], num[32]; + static WCHAR part[MAX_PATH], tag[200], oldtag[200], hdr[400], num[32], lmod[64]; static BYTE buf[CHUNK]; static const DWORD backoff[5] = { 1, 2, 4, 8, 15 }; // seconds to wait after the 1st .. 5th failed attempt in a row; the 6th gives up - HINTERNET in, h = NULL; HANDLE f; DWORD have, start, total, status, n, w, fails, first = 0, ms, timeout = 30000, proto; BOOL refused, nolen; + HINTERNET in, h = NULL; HANDLE f; DWORD have, start, total, status, n, w, fails, first = 0, ms; BOOL refused, nolen; LARGE_INTEGER sz; HKEY k; DWORD tn; int rc = PK_NET; *err = 0; if (!url_ok(url, host)) return PK_URL; - pkg_temp_dir(out); lstrcatW(out, L"\\"); lstrcatW(out, name); + pkg_temp_dir(out); lstrcatW(out, L"\\"); lstrcatW(out, name); // first: out always names our own file in %TEMP%, never a path the caller held before + if (pkg_net_state() == NET_NO) { *err = ERROR_INTERNET_DISCONNECTED; return PK_NET; } // clearly no network at all (cable out, adapter off): the error page at once, no waiting lstrcpyW(part, out); lstrcatW(part, L".part"); oldtag[0] = 0; if (!RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { @@ -364,14 +421,7 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W if (RegQueryValueExW(k, etagval, NULL, NULL, (BYTE *)oldtag, &tn)) oldtag[0] = 0; RegCloseKey(k); } - in = InternetOpenW(L"DynaRunFix-Setup/" WIDEN(DRF_VERSION), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); - if (!in) { *err = GetLastError(); return PK_NET; } - InternetSetOptionW(in, INTERNET_OPTION_CONNECT_TIMEOUT, &timeout, sizeof(timeout)); - InternetSetOptionW(in, INTERNET_OPTION_RECEIVE_TIMEOUT, &timeout, sizeof(timeout)); - // TLS 1.2 and later only (SP_PROT_TLS1_3_CLIENT | SP_PROT_TLS1_2_CLIENT); Windows 7 needs TLS 1.2 switched on this way. Where the - // option is not supported (XP, Vista) the system default stays and the connection to a TLS 1.2-only site simply fails. - proto = 0x2800; - if (!InternetSetOptionW(in, INTERNET_OPTION_SECURE_PROTOCOLS, &proto, sizeof(proto))) { proto = 0x800; InternetSetOptionW(in, INTERNET_OPTION_SECURE_PROTOCOLS, &proto, sizeof(proto)); } + if (!(in = pkg_net_open(10000, 30000))) { *err = GetLastError(); return PK_NET; } // a host that does not answer is given up after 10 s; data that stops coming after 30 s // Attempts follow each other with a growing pause (backoff); an attempt that received bytes starts the count over (the connection worked), // and the error that began the current run of failures is the one reported (a later DNS error while reconnecting is not the cause). @@ -405,6 +455,7 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W RegSetValueExW(k, etagval, 0, REG_SZ, (const BYTE *)tag, (lstrlenW(tag) + 1) * sizeof(WCHAR)); RegCloseKey(k); lstrcpyW(oldtag, tag); } + query_str(h, HTTP_QUERY_LAST_MODIFIED, lmod, 64); nolen = !query_str(h, HTTP_QUERY_CONTENT_LENGTH, num, 32) || !num[0]; total = nolen ? 0 : parse_len(num); if (!nolen) { @@ -437,9 +488,201 @@ int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const W if (rc == PK_NET) { if (first) *err = first; if (*cancel) rc = PK_CANCEL; } // the first error of the failed run, not the last one; stopped by the user between attempts InternetCloseHandle(in); if (rc == PK_OK && !MoveFileExW(part, out, MOVEFILE_REPLACE_EXISTING)) { *err = GetLastError(); rc = PK_IO; } + if (rc == PK_OK && !RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &k, NULL)) { // what a completed download was (for the hint "the site has another file") + WCHAR rec[320], nm[80]; + if (lstrlenW(etagval) + 5 < 80) { + wsprintfW(rec, L"%s|%lu|%s", tag, have, lmod); lstrcpyW(nm, etagval); lstrcatW(nm, L"Done"); + RegSetValueExW(k, nm, 0, REG_SZ, (const BYTE *)rec, (lstrlenW(rec) + 1) * sizeof(WCHAR)); + } + RegCloseKey(k); + } return rc; } +/* ---------- is there a newer DynaRun V3 (a hint on the first page; never an update, never blocking) ---------- */ + +// "3.26.0" -> its numbers; the count (0 when s does not start with a version: digits separated by dots). +static int ver_parts(const WCHAR *s, DWORD v[4]) +{ + int n = 0; + v[0] = v[1] = v[2] = v[3] = 0; + while (n < 4 && *s >= '0' && *s <= '9') { + DWORD x = 0; + while (*s >= '0' && *s <= '9') { if (x < 100000000) x = x * 10 + (*s - '0'); s++; } + v[n++] = x; + if (*s == '.' && s[1] >= '0' && s[1] <= '9') s++; else break; + } + return n; +} + +// <0, 0, >0 as a is older than, the same as, newer than b (missing parts count as 0: "3.26" = "3.26.0"). A text that is not a version compares as 0 ("no news"). +int ver_compare(const WCHAR *a, const WCHAR *b) +{ + DWORD x[4], y[4]; int i; + if (!ver_parts(a, x) || !ver_parts(b, y)) return 0; + for (i = 0; i < 4; i++) if (x[i] != y[i]) return x[i] < y[i] ? -1 : 1; + return 0; +} + +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; } + +// The first case-insensitive occurrence of needle (lower case) in text, from `from`; NULL when there is none. +static const char *ifind(const char *text, const char *needle) +{ + int i; + for (; *text; text++) { for (i = 0; needle[i] && lc(text[i]) == needle[i]; i++); if (!needle[i]) return text; } + return NULL; +} + +// A dotted number of at least three parts ("3.26.0") that starts at or after p within `window` characters; copied to out as a wide string. +static BOOL find_version(const char *p, int window, WCHAR *out, int cch) +{ + int i, j, parts; + for (i = 0; i < window && p[i]; i++) { + if (p[i] < '0' || p[i] > '9' || (i && ((p[i - 1] >= '0' && p[i - 1] <= '9') || p[i - 1] == '.'))) continue; + j = i; parts = 0; + for (;;) { + if (!(p[j] >= '0' && p[j] <= '9')) break; + while (p[j] >= '0' && p[j] <= '9') j++; + parts++; + if (p[j] == '.' && p[j + 1] >= '0' && p[j + 1] <= '9') j++; else break; + } + if (parts >= 3 && j - i < cch) { for (parts = 0; parts < j - i; parts++) out[parts] = (WCHAR)p[i + parts]; out[j - i] = 0; return TRUE; } + i = j; + } + return FALSE; +} + +// The DynaRun V3 version the page https://dynapro.co.uk/Software_Release.htm names. The page's download table has a row "Dyna Run V3 Software (...) 3.26.0 (Released: +// ...)" and its news text says "Version 3.26.0"; both are looked for in the page's text (tags, scripts and entities removed), the table row first. Tolerant: no version +// found (the page changed) is just FALSE. html: the page as received (ASCII / UTF-8 bytes, n of them). +BOOL pkg_page_version(const char *html, DWORD n, WCHAR *out, int cch) +{ + char *t, *o; DWORD i; BOOL sp = TRUE, found = FALSE; const char *p; + out[0] = 0; + if (!(t = alloc(n + 2))) return FALSE; + for (i = 0, o = t; i < n && html[i]; ) { + if (html[i] == '<') { // a tag (a script or style block is skipped as a whole) + const char *e; + if (starts_ci(html + i, "")) != NULL) i = (DWORD)(e - html) + 9; + else if (starts_ci(html + i, "")) != NULL) i = (DWORD)(e - html) + 8; + else { while (i < n && html[i] && html[i] != '>') i++; if (i < n) i++; } + if (!sp) { *o++ = ' '; sp = TRUE; } + continue; + } + if (html[i] == '&') { // the few entities the page uses + if (starts_ci(html + i, " ")) { i += 6; if (!sp) { *o++ = ' '; sp = TRUE; } continue; } + if (starts_ci(html + i, "&")) { i += 5; *o++ = '&'; sp = FALSE; continue; } + } + if (html[i] == ' ' || html[i] == '\t' || html[i] == '\r' || html[i] == '\n') { if (!sp) { *o++ = ' '; sp = TRUE; } i++; continue; } + *o++ = html[i++]; sp = FALSE; + } + *o = 0; + for (p = ifind(t, "dyna run v3 software"); p && !found; p = ifind(p + 20, "dyna run v3 software")) { + const char *q = p + 20; + while (*q == ' ') q++; + if (lc(q[0]) == 'm' && lc(q[1]) == 'a' && lc(q[2]) == 'n' && lc(q[3]) == 'u') continue; // "Dyna Run V3 Software Manual" is the manual + found = find_version(q, 400, out, cch); + } + for (p = ifind(t, "version "); p && !found; p = ifind(p + 8, "version ")) found = find_version(p + 8, 3, out, cch); + release(t); + return found; +} + +// The installed DynaRun V3's version (the Windows Installer's own record of its product, found by the upgrade code), FALSE when it is not installed that way. +BOOL dynarun_version(WCHAR *out, int cch) +{ + WCHAR prod[40]; DWORD n = (DWORD)cch; + out[0] = 0; + if (!dynarun_product(prod)) return FALSE; + return !MsiGetProductInfoW(prod, L"VersionString", out, &n) && out[0]; +} + +static BOOL query_named(HINTERNET h, const WCHAR *name, WCHAR *out, DWORD cch) +{ DWORD n = cch * sizeof(WCHAR); lstrcpyW(out, name); return HttpQueryInfoW(h, HTTP_QUERY_CUSTOM, out, &n, NULL); } + +// A page of the site into buf (at most cap - 1 bytes, NUL-terminated); a single try, short timeouts: this is a hint, nobody waits for it. +static BOOL fetch_text(const WCHAR *url, char *buf, DWORD cap, DWORD *len) +{ + HINTERNET in, h; DWORD status, err, got = 0, n; BOOL refused, ok = FALSE; + *len = 0; buf[0] = 0; + if (!(in = pkg_net_open(10000, 15000))) return FALSE; + h = pkg_open_following(in, url, DYNAPRO_HOST, NULL, &status, &err, &refused); + if (h) { + if (status == 200) { + while (got + 1 < cap && InternetReadFile(h, buf + got, cap - 1 - got, &n) && n) got += n; + buf[got] = 0; *len = got; ok = got > 0; + } + InternetCloseHandle(h); + } + InternetCloseHandle(in); + return ok; +} + +// The zip on the site, asked for its first byte only (headers are what is wanted; the same redirect checks as a download): its ETag, length and Last-Modified, +// to be compared with what the last download of it recorded. +static BOOL probe_zip(WCHAR *etag, DWORD *len, WCHAR *mod) +{ + HINTERNET in, h; DWORD status, err, n; WCHAR range[80]; BOOL refused, ok = FALSE; + etag[0] = mod[0] = 0; *len = 0; + if (!(in = pkg_net_open(10000, 15000))) return FALSE; + h = pkg_open_following(in, DYNAPRO_ZIP, DYNAPRO_HOST, L"Range: bytes=0-0\r\n", &status, &err, &refused); + if (h) { + if (status == 200 || status == 206) { + query_str(h, HTTP_QUERY_ETAG, etag, 200); + query_str(h, HTTP_QUERY_LAST_MODIFIED, mod, 64); + if (status == 206 && query_named(h, L"Content-Range", range, 80)) { WCHAR *s = range; while (*s && *s != '/') s++; if (*s == '/') *len = parse_len(s + 1); } + else if (status == 200 && query_str(h, HTTP_QUERY_CONTENT_LENGTH, range, 80)) *len = parse_len(range); + ok = etag[0] || *len || mod[0]; + } + InternetCloseHandle(h); + } + InternetCloseHandle(in); + (void)n; + return ok; +} + +// What the last completed download of the DynaRun zip recorded (pkg_fetch: "etag|length|last-modified" under HKCU\Software\DynaRunFix\DownloadETagDone). +static BOOL recorded_download(WCHAR *etag, DWORD *len, WCHAR *mod) +{ + WCHAR v[320], *a, *b; HKEY k; DWORD n = sizeof(v) - 2; BOOL ok = FALSE; + etag[0] = mod[0] = 0; *len = 0; v[0] = 0; + if (!RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\DynaRunFix", 0, KEY_QUERY_VALUE, &k)) { + ok = !RegQueryValueExW(k, L"DownloadETagDone", NULL, NULL, (BYTE *)v, &n) && v[0]; + RegCloseKey(k); + } + if (!ok) return FALSE; + for (a = v; *a && *a != '|'; a++); + if (*a) { *a++ = 0; for (b = a; *b && *b != '|'; b++); if (*b) *b++ = 0; *len = parse_len(a); lstrcpynW(mod, b, 64); } + lstrcpynW(etag, v, 200); + return TRUE; +} + +// The hint. 0: nothing to say. 1: the site names a newer version (latest, installed filled in). 2: the site does not name a version (or could not be read) but its +// zip is not the one the last download recorded: "there may be a newer one". Nothing is changed, nothing is asked; every outcome is one line in the log (log may be NULL). +int pkg_update_hint(WCHAR *installed, WCHAR *latest, int cch, void (*log)(const WCHAR *line)) +{ + static char page[262144]; DWORD len; WCHAR l[300], e1[200], m1[64], e2[200], m2[64]; DWORD l1, l2; int r = 0; + latest[0] = 0; + if (!dynarun_version(installed, cch)) { if (log) log(L"DynaRun update hint: no installed DynaRun version (not installed by its MSI)"); return 0; } + if (pkg_net_state() == NET_NO) { if (log) log(L"DynaRun update hint: no network"); return 0; } + if (fetch_text(DYNAPRO_PAGE, page, sizeof(page), &len) && pkg_page_version(page, len, latest, cch)) { + r = ver_compare(latest, installed) > 0 ? 1 : 0; + if (log) { wsprintfW(l, L"DynaRun update hint: installed %s, the website names %s: %s", installed, latest, r ? L"newer version available" : L"nothing newer"); log(l); } + return r; + } + if (log) log(L"DynaRun update hint: the website names no version (or could not be read): comparing its zip with the last download"); + if (!probe_zip(e1, &l1, m1) || !recorded_download(e2, &l2, m2)) { if (log) log(L"DynaRun update hint: nothing to compare"); return 0; } + if (e1[0] && e2[0]) r = lstrcmpW(e1, e2) != 0; + else r = (l1 && l2 && l1 != l2) || (m1[0] && m2[0] && lstrcmpW(m1, m2)); + if (log) log(r ? L"DynaRun update hint: the zip on the site differs from the one downloaded last (there may be a newer version)" : L"DynaRun update hint: the zip on the site is the one downloaded last"); + return r ? 2 : 0; +} + + + + // A partial download that pkg_fetch can continue: .part exists, is not empty, and the ETag of the file it belongs to is known // (without it the whole file would be fetched again). *have: its size in bytes. BOOL pkg_partial(const WCHAR *name, const WCHAR *etagval, DWORD *have) @@ -652,6 +895,192 @@ int zip_extract_all(const WCHAR *zip, const WCHAR *dest, volatile LONG *cancel, return rc; } +/* ---------- the files of a package named in a pinned table: safe unpacking, copying and checking ---------- */ + +// Used by the driver (its 18 files) and by the DynaRun setup (pkg_prepare_target / pkg_extract_checked below): nothing a zip says about a path is ever used +// (every destination is built from the program's own table or name), what is written is written into folders made here, links are refused, sizes are capped. + +static BOOL rel_prefix_eq(const WCHAR *a, const WCHAR *b, int len) +{ + int i; + for (i = 0; i < len; i++) if (a[i] != b[i]) return FALSE; + return TRUE; +} + +// The sub-folders the relative paths of the table need, each once, shortest first ("i386", "amd64", "a" before "a/b"): visit(prefix, len, ctx). +// mode 0: create dir\ below root (new: an existing one is an error); mode 1: check that it is a plain folder. +static BOOL each_subdir(const WCHAR *root, const pkg_file *t, int n, int mode) +{ + WCHAR p[MAX_PATH]; int i, j, k; + for (i = 0; i < n; i++) { + for (k = 0; t[i].rel[k]; k++) { + if (t[i].rel[k] != '/') continue; + for (j = 0; j < i; j++) if (t[j].rel[k] == '/' && rel_prefix_eq(t[j].rel, t[i].rel, k)) break; // an earlier file has made this folder + if (j < i) continue; + if (lstrlenW(root) + 1 + k + 1 >= MAX_PATH) return FALSE; + lstrcpyW(p, root); lstrcatW(p, L"\\"); lstrcpynW(p + lstrlenW(p), t[i].rel, k + 1); + { int q; for (q = lstrlenW(root); p[q]; q++) if (p[q] == '/') p[q] = '\\'; } + if (mode == 0 ? !CreateDirectoryW(p, NULL) : !pkg_is_plain_dir(p)) return FALSE; + } + } + return TRUE; +} + +BOOL pkg_is_plain_dir(const WCHAR *p) +{ + DWORD a = GetFileAttributesW(p); + return a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_DIRECTORY) && !(a & FILE_ATTRIBUTE_REPARSE_POINT); +} + +// dir + sub + (with '\'); FALSE when it does not fit. sub: "\\" (a staging area or an unpacked zip) or "\" (the folder that holds the files). +BOOL pkg_file_path(WCHAR *out, const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int i) +{ + int n = lstrlenW(dir), k; + if (n + lstrlenW(sub) + lstrlenW(t[i].rel) + 1 >= MAX_PATH) return FALSE; + lstrcpyW(out, dir); lstrcatW(out, sub); lstrcatW(out, t[i].rel); + for (k = n; out[k]; k++) if (out[k] == '/') out[k] = '\\'; + return TRUE; +} + +// The zip entry name (as stored, '/' separators, not 0-terminated) is table file i when it is that name at the end, after a folder boundary: "/i386/x.sys" and a +// re-zipped "other/i386/x.sys" both are. Case does not matter. +int pkg_entry_index(const pkg_file *t, int n, const char *name, DWORD nlen) +{ + int i; DWORD rl, k; + for (i = 0; i < n; i++) { + const WCHAR *r = t[i].rel; BOOL ok = TRUE; + rl = (DWORD)lstrlenW(r); + if (nlen < rl || (nlen > rl && name[nlen - rl - 1] != '/')) continue; + for (k = 0; k < rl && ok; k++) { + char a = name[nlen - rl + k], b = (char)r[k]; + if (a >= 'A' && a <= 'Z') a += 32; + if (b >= 'A' && b <= 'Z') b += 32; + if (a != b) ok = FALSE; + } + if (ok) return i; + } + return -1; +} + +// The folders of a staging area are ones we made: dir, dir\ and the sub-folders of the table are plain folders, no link among them. +BOOL pkg_dirs_plain(const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n) +{ + WCHAR root[MAX_PATH]; + if (!pkg_is_plain_dir(dir) || lstrlenW(dir) + 1 + lstrlenW(top) + 1 >= MAX_PATH) return FALSE; + lstrcpyW(root, dir); lstrcatW(root, L"\\"); lstrcatW(root, top); + return pkg_is_plain_dir(root) && each_subdir(root, t, n, 1); +} + +typedef struct { const pkg_file *t; int n; DWORD maxfile; zipent e[PKG_MAXF]; DWORD nlen[PKG_MAXF]; BOOL have[PKG_MAXF]; } pkgzset; + +// Collects the zip entries that are table files. Of several entries of the same name (a duplicate folder) the one with the shortest path is taken. An encrypted +// entry, another method than stored / deflate, or one that is too big does not count: that file is "missing". +static BOOL pkgzset_visit(const char *name, DWORD nlen, const zipent *e, void *ctx) +{ + pkgzset *z = (pkgzset *)ctx; int i = pkg_entry_index(z->t, z->n, name, nlen); + if (i < 0 || (e->flags & 1) || (e->method != 0 && e->method != 8) || e->usize > z->maxfile) return TRUE; + if (!z->have[i] || nlen < z->nlen[i]) { z->e[i] = *e; z->nlen[i] = nlen; z->have[i] = TRUE; } + return TRUE; +} + +// How many table files the zip has (a cheap look at its central directory). +int pkg_zip_has(const WCHAR *zip, const pkg_file *t, int n, DWORD maxfile) +{ + static pkgzset z; int i, c = 0; + zero(&z, sizeof(z)); z.t = t; z.n = n; z.maxfile = maxfile; + if (zip_walk(zip, pkgzset_visit, &z) != PK_OK) return -1; + for (i = 0; i < n; i++) if (z.have[i]) c++; + return c; +} + +// Takes the table's files out of the zip into dir\\ (dir exists; the folders below are created here, so they are new: a link cannot be in the way). Only +// those files are extracted (the other ones of the zip never touch the disk); limits: maxfile each, maxtotal all together (zip-bomb guard); a file that is not there +// counts in *missing. Entries are checked against their CRC and size by zip_extract. PK_OK, or PK_FORMAT / PK_IO / PK_CANCEL. +int pkg_stage_files(const WCHAR *zip, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, volatile LONG *cancel, int *missing) +{ + static pkgzset z; WCHAR root[MAX_PATH], p[MAX_PATH], sub[80]; int i, rc; DWORD total = 0, a; + *missing = 0; + zero(&z, sizeof(z)); z.t = t; z.n = n; z.maxfile = maxfile; + if (zip_walk(zip, pkgzset_visit, &z) != PK_OK) return PK_FORMAT; + if (lstrlenW(dir) + 1 + lstrlenW(top) + 1 >= MAX_PATH || lstrlenW(top) + 3 >= 80) return PK_IO; + lstrcpyW(root, dir); lstrcatW(root, L"\\"); lstrcatW(root, top); + if (!CreateDirectoryW(root, NULL) || !each_subdir(root, t, n, 0)) return PK_IO; + if (!pkg_dirs_plain(dir, top, t, n)) return PK_FORMAT; + lstrcpyW(sub, L"\\"); lstrcatW(sub, top); lstrcatW(sub, L"\\"); + for (i = 0; i < n; i++) { + if (!z.have[i]) { (*missing)++; continue; } + if ((total += z.e[i].usize) > maxtotal) return PK_FORMAT; + if (!pkg_file_path(p, dir, sub, t, i)) return PK_IO; + if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return PK_FORMAT; // the path is new: whatever is there is not ours + rc = zip_extract(&z.e[i], "", p, NULL, cancel); + if (rc == PK_PASSWORD) rc = PK_FORMAT; // CRC or size mismatch: the entry is damaged + if (rc != PK_OK) return rc; + a = GetFileAttributesW(p); + if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return PK_FORMAT; + } + return PK_OK; +} + +// The same for an unpacked folder (src: the folder that holds the files): they are COPIED from there into dir\\ (new folders made here, a path that +// exists is refused). The source folders on the way must be plain ones, a source file that is a link, a folder or too big is refused; what is hashed afterwards +// is the copy, not the source. *missing: files that are not there. +int pkg_stage_from_dir(const WCHAR *src, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, int *missing) +{ + WCHAR root[MAX_PATH], p[MAX_PATH], s[MAX_PATH], sub[80]; int i; DWORD total = 0, a; WIN32_FILE_ATTRIBUTE_DATA fa; + *missing = 0; + if (!pkg_is_plain_dir(src) || !each_subdir(src, t, n, 1)) return PK_FORMAT; + if (lstrlenW(dir) + 1 + lstrlenW(top) + 1 >= MAX_PATH || lstrlenW(top) + 3 >= 80) return PK_IO; + lstrcpyW(root, dir); lstrcatW(root, L"\\"); lstrcatW(root, top); + if (!CreateDirectoryW(root, NULL) || !each_subdir(root, t, n, 0)) return PK_IO; + if (!pkg_dirs_plain(dir, top, t, n)) return PK_FORMAT; + lstrcpyW(sub, L"\\"); lstrcatW(sub, top); lstrcatW(sub, L"\\"); + for (i = 0; i < n; i++) { + if (!pkg_file_path(s, src, L"\\", t, i) || !pkg_file_path(p, dir, sub, t, i)) return PK_IO; + if (!GetFileAttributesExW(s, GetFileExInfoStandard, &fa)) { (*missing)++; continue; } + if ((fa.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || fa.nFileSizeHigh || fa.nFileSizeLow > maxfile) return PK_FORMAT; + if ((total += fa.nFileSizeLow) > maxtotal) return PK_FORMAT; + if (GetFileAttributesW(p) != INVALID_FILE_ATTRIBUTES) return PK_FORMAT; + if (!CopyFileW(s, p, TRUE)) return PK_IO; + a = GetFileAttributesW(p); + if (a == INVALID_FILE_ATTRIBUTES || (a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT))) return PK_FORMAT; + } + return PK_OK; +} + +// The files below dir + sub against the table (expect NULL) or against a list of accepted hashes: a missing file, one that is a link or a folder, a hash that differs. +void pkg_check_files(const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int n, WCHAR (*expect)[65], pkg_check *c) +{ + WCHAR p[MAX_PATH]; ULONGLONG sz; int i; DWORD a; + zero(c, sizeof(*c)); + for (i = 0; i < n; i++) { + if (!pkg_file_path(p, dir, sub, t, i) || (a = GetFileAttributesW(p)) == INVALID_FILE_ATTRIBUTES) { c->st[i] = 2; c->missing++; continue; } + if ((a & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_REPARSE_POINT)) || !pkg_sha256(p, c->hex[i], &sz)) { c->st[i] = 1; c->differ++; c->hex[i][0] = 0; continue; } + if (lstrcmpiW(c->hex[i], expect ? expect[i] : t[i].sha)) { c->st[i] = 1; c->differ++; continue; } + c->ok++; + } +} + +// A path that is going to be written (the DynaRun Setup.msi taken out of the zip, in our temp folder): what is there from an earlier try is a plain file and is +// removed; a link is removed itself and never followed; a folder is refused. FALSE: it cannot be made free. +BOOL pkg_prepare_target(const WCHAR *path) +{ + DWORD a = GetFileAttributesW(path); + if (a == INVALID_FILE_ATTRIBUTES) return TRUE; + if (a & FILE_ATTRIBUTE_REPARSE_POINT) return (a & FILE_ATTRIBUTE_DIRECTORY) ? RemoveDirectoryW(path) : DeleteFileW(path); + if (a & FILE_ATTRIBUTE_DIRECTORY) return FALSE; + return DeleteFileW(path); +} + +// One zip entry into path: refused when it is bigger than maxsize (the entry's own claim; zip_extract also checks what really comes out), and the target is +// made free first (pkg_prepare_target). +int pkg_extract_checked(const zipent *e, const char *pw, const WCHAR *out, DWORD maxsize, progress_fn cb, volatile LONG *cancel) +{ + if (e->usize > maxsize) return PK_FORMAT; + if (!pkg_prepare_target(out)) return PK_IO; + return zip_extract(e, pw, out, cb, cancel); +} + + // The 12-byte encryption header ends with a check byte: right for the right password, and for // about 1 in 256 wrong ones (those are caught by the CRC after extraction). BOOL zip_password_plausible(const zipent *e, const char *pw) diff --git a/src/setup.c b/src/setup.c index 01b0c53..a987147 100644 --- a/src/setup.c +++ b/src/setup.c @@ -12,6 +12,10 @@ // desktop shortcut as they are (an update, see update.c) // DynaRunFix-Setup.exe /uninstall [/quiet] uninstall (also run from "Programs and Features") // DynaRunFix-Setup.exe /checkupdate /update , /offer: the installed copy, run by the launcher (update.c) +// /pin (elevated stage) also set up the Windows 11 taskbar pin +// DynaRunFix-Setup.exe /driver the Dyna Pro USB driver step on its own (driver.c; the wizard's last page) +// DynaRunFix-Setup.exe /driver-cancel undo the driver step's preparation (task, start-up menu option, BitLocker) +// /driver-prep, /driver-install, /driver-continue the driver step's own stages (elevated, or started by its scheduled task) // // Install, in two stages so that per-user changes land in the right profile even when an // administrator's credentials are typed into the UAC prompt: @@ -181,7 +185,7 @@ static BOOL write_file(const WCHAR *p, const void *d, DWORD n) // is refused, so the elevated stage cannot be made to write somewhere else through a file planted in %TEMP%. static void append_line(const WCHAR *p, const WCHAR *line) { - static char u[2048]; int n = WideCharToMultiByte(CP_UTF8, 0, line, -1, u, sizeof(u) - 2, NULL, NULL); DWORD w; + static char u[4096]; int n = WideCharToMultiByte(CP_UTF8, 0, line, -1, u, sizeof(u) - 2, NULL, NULL); DWORD w; BY_HANDLE_FILE_INFORMATION bi; HANDLE h = CreateFileW(p, FILE_APPEND_DATA | FILE_READ_ATTRIBUTES, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_FLAG_OPEN_REPARSE_POINT, NULL); if (h == INVALID_HANDLE_VALUE) return; @@ -614,13 +618,32 @@ static void run_log(const WCHAR *line) } // For the other files (the wizard side of the driver step): the line with a time stamp, so that durations can be read from the log. +// The wizard thread, its worker and the update-hint thread all log: one lock covers the formatting (static buffers) and the write. +static CRITICAL_SECTION g_ulog_cs; +static volatile LONG g_ulog_state; // 0 not set up, 1 being set up, 2 ready + void user_log(const WCHAR *line) { - static WCHAR l[2 * MAX_PATH + 400]; SYSTEMTIME t; + static WCHAR l[2 * MAX_PATH + 400]; SYSTEMTIME t; const WCHAR *p, *q; + if (g_ulog_state != 2) { + if (InterlockedCompareExchange(&g_ulog_state, 1, 0) == 0) { InitializeCriticalSection(&g_ulog_cs); g_ulog_state = 2; } + else while (g_ulog_state != 2) Sleep(1); + } + EnterCriticalSection(&g_ulog_cs); GetLocalTime(&t); - wsprintfW(l, L"[%02u:%02u:%02u.%03u] ", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); - lstrcpynW(l + lstrlenW(l), line, (int)(sizeof(l) / sizeof(l[0])) - lstrlenW(l) - 1); - run_log(l); + for (p = line; *p; p = *q ? q + 1 : q) { // a text of several lines is several lines, each with its time; the log is UTF-8 (append_line) + int n, pre, cnt; + for (q = p; *q && *q != '\n'; q++); + n = (int)(q - p); + while (n > 0 && p[n - 1] == '\r') n--; + if (n <= 0) continue; + wsprintfW(l, L"[%02u:%02u:%02u.%03u] ", t.wHour, t.wMinute, t.wSecond, t.wMilliseconds); + pre = lstrlenW(l); cnt = n + 1; + if (cnt > (int)(sizeof(l) / sizeof(l[0])) - pre) cnt = (int)(sizeof(l) / sizeof(l[0])) - pre; + lstrcpynW(l + pre, p, cnt); + run_log(l); + } + LeaveCriticalSection(&g_ulog_cs); } static void log_running(const WCHAR *stage, const runproc *l, int n, int r) @@ -961,7 +984,7 @@ static void skipped(const WCHAR *key, const WCHAR *why) static int promote_com_in(HKEY hu, HKEY hm, WCHAR roots[][MAX_PATH], int nr) { static const WCHAR *const views[] = { L"WOW6432Node\\CLSID", L"CLSID" }; - static WCHAR path[600], rel[300], id[128], v[MAX_PATH], prog[300], log[MAX_PATH], bak[MAX_PATH], line[700], tl[128], tlkey[200]; + static WCHAR path[600], rel[300], id[128], v[MAX_PATH], prog[300], log[MAX_PATH], bak[MAX_PATH], line[700], tl[128], tlkey[200], bn[48]; const WCHAR *why; HKEY list, s, d; DWORD i, n; int vi, j, copied = 0; WCHAR *dot; BOOL had, ok; g_nkeys = 0; @@ -992,8 +1015,8 @@ static int promote_com_in(HKEY hu, HKEY hm, WCHAR roots[][MAX_PATH], int nr) had = has_key(hm, g_keys[j]); if (had) { // keep what was there before cat3(bak, g_dir, L"\\com-backup", NULL); CreateDirectoryW(bak, NULL); - wsprintfW(bak + lstrlenW(bak), L"\\%lu-%d.reg", GetTickCount(), j); - reg_export(g_keys[j], bak); + wsprintfW(bn, L"\\%lu-%d.reg", GetTickCount(), j); + if (lstrlenW(bak) + lstrlenW(bn) < MAX_PATH) { lstrcatW(bak, bn); reg_export(g_keys[j], bak); } // bounded: a long install path just skips the backup file } if (RegOpenKeyExW(hu, g_keys[j], 0, KEY_READ | WOW, &s)) continue; if (!RegCreateKeyExW(hm, g_keys[j], 0, NULL, 0, KEY_ALL_ACCESS | WOW, NULL, &d, NULL)) { @@ -1925,9 +1948,9 @@ int run_elevated(const WCHAR *args, const WCHAR *exe_path) lstrcpyW(a, args); if (g_quiet) lstrcatW(a, L" /quiet"); if (g_close) lstrcatW(a, L" /close"); if (g_pin) lstrcatW(a, L" /pin"); // the elevated stage sets up the Windows 11 pin if (g_keep) lstrcatW(a, L" /keep"); - // Mapped network drives (e.g. a VM's shared folder) do not exist for the elevated process: run a copy in %TEMP% + // Mapped network drives (e.g. a shared folder) do not exist for the elevated process: run a copy in %TEMP% lstrcpynW(root, g_self, 4); - if (!exe_path && root[1] == ':' && GetDriveTypeW(root) == DRIVE_REMOTE && (tn = GetTempPathW(MAX_PATH - 40, tmp)) != 0 && tn < MAX_PATH - 40) { + if (!exe_path && root[1] == ':' && GetDriveTypeW(root) == DRIVE_REMOTE && (tn = GetTempPathW(MAX_PATH - 60, tmp)) != 0 && tn < MAX_PATH - 60) { // room for the longest name below (17 + 3 x 10 digits + 2 + 4 + NUL) q.QuadPart = 0; QueryPerformanceCounter(&q); wsprintfW(tmp + lstrlenW(tmp), L"DynaRunFix-Setup-%lu-%lu-%lu.exe", GetCurrentProcessId(), GetTickCount(), q.LowPart); // the copy is held open (nobody can change it) from here until the elevated process has ended: what the UAC prompt names is what runs @@ -2094,6 +2117,15 @@ void autostart_flush(void) } } +// The fix is installed and is this version: the uninstall entry names our version and the launcher and the dll are in the install folder. (A dev build +// shows its commit as the version, so a rebuilt one is "not current" until it has been installed once.) +BOOL fix_current(void) +{ + static WCHAR v[64], d[MAX_PATH]; + if (!reg_str(HKEY_LOCAL_MACHINE, UNINSTKEY, L"DisplayVersion", v, 64, 0) || lstrcmpW(v, WIDEN(DRF_DISPLAY))) return FALSE; + return exists(g_launcher) && exists(cat3(d, g_dir, L"\\dynafix.dll", NULL)); +} + int install_fix(void) { static WCHAR sid[200], args[600]; int rc; diff --git a/src/setup.h b/src/setup.h index 663480c..32220af 100644 --- a/src/setup.h +++ b/src/setup.h @@ -24,6 +24,7 @@ BOOL is_admin(void); void user_log(const WCHAR *line); // one line in setup.log (administrator, install folder there) or %TEMP%\DynaRunFix-setup.log DWORD os_build(void); // Windows build number (real, not the compatibility-shimmed one) BOOL user_sid(WCHAR *out, int cch); +BOOL fix_current(void); // the fix is installed and is the version of this setup (uninstall entry, launcher and dll there) BOOL locate_dynarun(void); // fills g_exe from the registry or the default folders; no UI void start_dynarun(void); // the wizard's "Start DynaRun": through our shortcut, like a double click int install_fix(void); // DynaRun is installed: elevated stage + user shortcuts. 0 ok, 2 cancelled @@ -70,6 +71,7 @@ BOOL pkg_find_local(WCHAR *out); // a DynaRun MSI, or a zip with an . BOOL pkg_find_zip_if(DWORD maxsize, BOOL (*accept)(const WCHAR *path), WCHAR *out); // the first zip (at most maxsize bytes) in the same places that accept() approves extern void (*g_pkg_log)(const WCHAR *line); // set to log the folders the zip searches look in (NULL: silent) BOOL pkg_find_dirs(BOOL (*accept)(const WCHAR *dir), WCHAR *out); // the first folder accept() approves, in the same places (two levels down in Downloads) +BOOL pkg_find_dir_in(const WCHAR *root, int depth, BOOL (*accept)(const WCHAR *dir), WCHAR *out); // accept() for root and the folders up to `depth` levels below it BOOL pkg_find_zip_named(const WCHAR *prefix, WCHAR *out); // the newest zip whose name starts with prefix in the same places void pkg_reject(const WCHAR *path); // not the DynaRun setup after all: pkg_find_local skips it int pkg_download(WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); @@ -78,6 +80,13 @@ HINTERNET pkg_open_following(HINTERNET in, const WCHAR *url, const WCHAR *host, BOOL pkg_redirect_target(const WCHAR *cur, const WCHAR *loc, const WCHAR *host, WCHAR *out, int cch); // where a redirect from cur to loc goes, if allowed BOOL pkg_partial(const WCHAR *name, const WCHAR *etagval, DWORD *have); // a partial download pkg_fetch can continue (.part and its ETag are there); *have: bytes so far BOOL pkg_download_partial(DWORD *have); // ... of the DynaRun zip +enum { NET_UNKNOWN = 0, NET_NO = 1, NET_YES = 2 }; +HINTERNET pkg_net_open(DWORD connect_ms, DWORD receive_ms); // a session with these timeouts, TLS 1.2+ (NULL: none) +int ver_compare(const WCHAR *a, const WCHAR *b); // <0 / 0 / >0: a older / the same / newer than b (not versions: 0) +BOOL pkg_page_version(const char *html, DWORD n, WCHAR *out, int cch); // the DynaRun V3 version the Dyna Pro software page names +BOOL dynarun_version(WCHAR *out, int cch); // the installed DynaRun V3's version (Windows Installer record), FALSE when not installed that way +int pkg_update_hint(WCHAR *installed, WCHAR *latest, int cch, void (*log)(const WCHAR *line)); // 0 nothing, 1 the site names a newer version, 2 the site's zip differs from the last download +int pkg_net_state(void); // NET_NO: Windows says it is disconnected (the download is not even tried); NET_YES / NET_UNKNOWN: try int pkg_fetch(const WCHAR *url, const WCHAR *name, const WCHAR *etagval, const WCHAR *host, WCHAR *out, progress_fn cb, volatile LONG *cancel, DWORD *err); int zip_open(const WCHAR *zip, zipent *e); // first *.msi entry // Every entry of the central directory, in order: fn(name as stored (not 0-terminated), its length, entry, ctx). fn returns FALSE to stop. @@ -90,6 +99,21 @@ BOOL zip_name_safe(const char *name, DWORD nlen, BOOL *isdir); // Extracts every entry (stored or deflate, not encrypted) below dest (which must exist); an unsafe name, a damaged entry or a zip // larger than the limits fails the whole call (PK_FORMAT). *nfiles: files written. int zip_extract_all(const WCHAR *zip, const WCHAR *dest, volatile LONG *cancel, int *nfiles); +// The files of a package named in a pinned table (the driver's 18 files): safe unpacking / copying and checking (package.c). Used by the driver; the DynaRun setup +// uses pkg_prepare_target / pkg_extract_checked. Nothing a zip says about a path is used, destinations are built from the table; links are refused; sizes capped. +typedef struct { const WCHAR *rel; DWORD size; const WCHAR *sha; } pkg_file; // name below the package folder ('/' separators), size, SHA-256 (lower case) +#define PKG_MAXF 24 +typedef struct { int ok, differ, missing; BYTE st[PKG_MAXF]; WCHAR hex[PKG_MAXF][65]; } pkg_check; // st: 0 ok, 1 differs, 2 missing; hex: what the file hashes to +int pkg_entry_index(const pkg_file *t, int n, const char *name, DWORD nlen); // the table file a zip entry name is (after a folder boundary, any case), or -1 +BOOL pkg_is_plain_dir(const WCHAR *p); +BOOL pkg_file_path(WCHAR *out, const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int i); +BOOL pkg_dirs_plain(const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n); +int pkg_zip_has(const WCHAR *zip, const pkg_file *t, int n, DWORD maxfile); // how many table files the zip has (central directory only), -1: not a zip +int pkg_stage_files(const WCHAR *zip, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, volatile LONG *cancel, int *missing); +int pkg_stage_from_dir(const WCHAR *src, const WCHAR *dir, const WCHAR *top, const pkg_file *t, int n, DWORD maxfile, DWORD maxtotal, int *missing); +void pkg_check_files(const WCHAR *dir, const WCHAR *sub, const pkg_file *t, int n, WCHAR (*expect)[65], pkg_check *c); +BOOL pkg_prepare_target(const WCHAR *path); // free a path that is going to be written (a plain file is removed, a link removed itself, a folder refused) +int pkg_extract_checked(const zipent *e, const char *pw, const WCHAR *out, DWORD maxsize, progress_fn cb, volatile LONG *cancel); // zip_extract with a size cap and a free target BOOL zip_password_plausible(const zipent *e, const char *pw); int zip_extract(const zipent *e, const char *pw, const WCHAR *out, progress_fn cb, volatile LONG *cancel); BOOL pkg_sha256(const WCHAR *path, WCHAR *hex, ULONGLONG *size); // lower-case hex (65 WCHARs) and size of a file, read-only @@ -112,13 +136,15 @@ enum { DRV_SUCCESS = 0, DRV_E_HASH = 20, DRV_E_EXTRACT, DRV_E_INF, DRV_E_COPY, D enum { DRV_CI_ENFORCED, DRV_CI_OFF, DRV_CI_UNKNOWN }; // driver signature enforcement now BOOL driver_supported(void); // Windows 10 (build 10240) or later: older systems install the driver by hand BOOL driver_installed(void); // a Dyna Pro ftdibus package is in the driver store (%windir%\INF\oem*.inf) -enum { DRV_SRC_DOWNLOADED = 0, DRV_SRC_LOCAL = 1, DRV_ZIPDIFF = 2, DRV_SRC_DIR = 4 }; // *how of driver_fetch: where the zip came from, and (flag) the zip as a whole is not the official one +enum { DRV_SRC_DOWNLOADED = 0, DRV_SRC_LOCAL = 1, DRV_ZIPDIFF = 2, DRV_SRC_DIR = 4, DRV_SRC_PICKED = 8 }; // *how of driver_fetch: where the zip came from, and (flag) the zip as a whole is not the official one // The zip: found on this computer (DRV_SRC_LOCAL; DRV_SRC_DIR: an already unpacked folder, `zip` is its path and hex a digest of the files' hashes) or downloaded. The check is of its CONTENT: the files Windows installs (driver.c, DRV_FILES). // PK_OK: those files are the pinned ones (DRV_ZIPDIFF: although the zip as a whole differs, e.g. re-packed). DRV_W_HASH: some differ or are missing but // the package passed the sanity checks - the user is asked ("install anyway"); hex / size: the SHA-256 and size of the zip, rep: the files that differ. // DRV_E_HASH: not a usable driver package. Other PK_*: the download failed. cb((DWORD)-1, 1) tells the "checking" phase (after the download, not pausable). int driver_fetch(WCHAR *zip, progress_fn cb, volatile LONG *cancel, DWORD *err, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); void driver_skip_local_other(void); // the user declined a local copy that differs: the next driver_fetch skips such copies and downloads +BOOL driver_layout_ok(const WCHAR *dir); // the folder holds ftdibus.inf with i386 and amd64 folders (an unpacked driver) +int driver_check(WCHAR *path, int *how, WCHAR *hex, ULONGLONG *size, WCHAR *rep, int repcch); // a zip or an unpacked folder the user chose: the same check as for a copy that was found (PK_OK / DRV_W_HASH / DRV_E_HASH); path becomes the folder const WCHAR *driver_pinned_sha256(void); // the SHA-256 of the zip this program knows void driver_forget(const WCHAR *zip); // deletes the zip when it is our own cache file in %TEMP%\DynaRunFix (a refused download), never a file of the user's BOOL driver_partial(DWORD *have); // a partial driver download that driver_fetch can continue; *have: bytes so far diff --git a/src/wizard.c b/src/wizard.c index 70ca47c..d491f01 100644 --- a/src/wizard.c +++ b/src/wizard.c @@ -1,8 +1,9 @@ // The setup wizard: one window, one question at a time, for users who never installed software. // // DynaRun V3 installed: Hello -> Install (one UAC prompt) -> Done -// not installed: Hello -> get "Dyna Pro Dynamometers.zip" (found in Downloads/Desktop, or -// downloaded from dynapro.co.uk; the site's download page is one click away) +// not installed: Hello -> get "Dyna Pro Dynamometers.zip" (a network check first; found in Downloads/Desktop, or +// downloaded from dynapro.co.uk with pause / resume; one error page, with the site's download page +// one click away and "Choose file...") // -> password -> extract -> check it is DynaRun (by the MSI's UpgradeCode, never by // file name) -> Dyna Pro's license -> Install (msiexec /qb and the fix, one UAC prompt) // -> Done -> "Start DynaRun" @@ -11,6 +12,7 @@ #include #include #include +#include #include #include #include "setup.h" @@ -19,12 +21,13 @@ #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 }; +enum { ID_TITLE = 100, ID_BODY, ID_EDIT, ID_RICH, ID_CHECK, ID_PROG, ID_STATUS, ID_EXTRA, ID_SECOND, ID_PRIMARY, ID_KEEPUP, ID_PIN, ID_AUTO, ID_DRV, ID_IMG, ID_INFO, ID_THIRD }; enum { P_HELLO, P_FETCH, P_FAIL, P_PASSWORD, P_EXTRACT, P_CHECK, P_LICENSE, P_RUNNING, P_INSTALL, P_DONE, P_ERROR, - P_DRV_INTRO, P_DRV_FETCH, P_DRV_PRESS7, P_DRV_HASH, P_DRV_BDE, P_DRV_PREP, P_DRV_REBOOT, P_DRV_RETRY, P_DRV_WARN, P_DRV_INSTALL, P_DRV_DONE, P_DRV_ERROR }; // driver.c -enum { JOB_DOWNLOAD, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_FETCH, JOB_DRV_PREP, JOB_DRV_INSTALL, JOB_DRV_CANCEL }; + P_DRV_INTRO, P_DRV_PRESS7, P_DRV_HASH, P_DRV_BDE, P_DRV_PREP, P_DRV_REBOOT, P_DRV_RETRY, P_DRV_WARN, P_DRV_INSTALL, P_DRV_DONE, P_DRV_ERROR }; // driver.c +enum { JOB_ACQUIRE, JOB_EXTRACT, JOB_INSTALL_ALL, JOB_INSTALL_FIX, JOB_DRV_PREP, JOB_DRV_INSTALL, JOB_DRV_CANCEL }; #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 static int g_page, g_job, g_dpi, g_lastpct = -1, g_pending, g_lastrun; // g_pending: install job waiting for DynaRun to close @@ -38,14 +41,32 @@ static char g_pw[256]; static char *g_rtf; static WCHAR g_info[2200]; // what the license page shows about the package (path, size, SHA-256, MSI version/manufacturer) static DWORD g_neterr, g_total; -static BOOL g_paused, g_pause_req, g_drv_fetch_fail; // download paused (worker ended, .part kept) / pause asked, worker still stopping / the driver download failed (intro page shows why) +static BOOL g_paused, g_pause_req; // download paused (worker ended, .part kept) / pause asked, worker still stopping +static int g_hint; // DynaRun update hint (pkg_update_hint): 0 none, 1 a newer version is published, 2 the site's zip differs from the last download +static WCHAR g_hint_inst[40], g_hint_new[40]; // the installed and the published version (hint 1) +static BOOL g_driver_only; // DynaRun and its fix are installed (this version) and only the driver is missing: the first page offers just that static BOOL g_checking, g_marquee, g_drv_allow; // the non-pausable "checking" phase / marquee bar (size unknown) / the user chose "install anyway" for a zip with another hash static int g_drv_how; // DRV_SRC_*: the driver zip was downloaded, or found on this computer (nothing downloaded) static WCHAR g_drv_hex[65]; // SHA-256 and size of a driver zip that is not the pinned file (the warning page shows them) static ULONGLONG g_drv_size; static WCHAR g_drv_rep[1500]; // the driver files that differ from the pinned ones, one per line (the warning page lists them) static int g_drv_bl = -1; // BitLocker hint for the press-7 page, asked in the worker (WMI can take seconds): 1 on, 0 off, -1 cannot be told -static int g_dljob = -1; // the download job g_dltotal belongs to +// What the acquire flow needs to know about the thing it gets: the DynaRun V3 setup and the Dyna Pro USB driver go through the same pages (looking for +// a copy on this computer, download with pause / resume, one error page); only these texts, the "partial download" test, the search and the check differ. +typedef struct { + const WCHAR *name_en, *name_zh; // "the DynaRun V3 setup" / "DynaRun V3 安裝檔" + const WCHAR *get_en, *get_zh; // what to click on the website + const WCHAR *pick_en, *pick_zh; // what "Choose file..." takes + BOOL (*partial)(DWORD *have); // a .part that can be continued +} acq_desc; +static const acq_desc ACQ_SETUP = { L"the DynaRun V3 setup", L"DynaRun V3 安裝檔", L"\"Dyna Run V3 Software\"", L"「Dyna Run V3 Software」", + L"the zip (or the MSI) you downloaded", L"你下載的 zip(或 MSI)", pkg_download_partial }; +static const acq_desc ACQ_DRIVER = { L"the Dyna Pro USB driver", L"驅動程式", L"\"Dyna Pro USB Drivers\"", L"「Dyna Pro USB Drivers」", + L"Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf", driver_partial }; +static const acq_desc *g_acq = &ACQ_SETUP, *g_dlacq; // the thing being got now; the one g_dltotal belongs to +static BOOL g_acq_fresh; // "Download new version" (update hint): no package already on this computer is taken, it is downloaded +static BOOL g_acq_local_ok; // the DynaRun search may hand out another local package (pkg_find_local remembers 8 files) +#define ACQ_NAME(d) T((d)->name_en, (d)->name_zh) static DWORD g_dltotal, g_dlhave; // size of the file being downloaded (server's, 0 = not known) and bytes received so far static HFONT g_font, g_big; static HBRUSH g_white, g_grey; @@ -74,12 +95,15 @@ static void status(const WCHAR *t, BOOL err) if (g_hwnd && !g_building && status_need() != g_sth) layout(); } -static void buttons(const WCHAR *primary, const WCHAR *second, const WCHAR *extra) +// The button bar of every page: primary = the right-most one and the default (Enter); second = the way out, directly left of it (Cancel, or Skip / Later / +// Close where that is what leaving means); third = another way on, left of the second one; extra = the one link-style action at the bottom left. +static void buttons4(const WCHAR *primary, const WCHAR *second, const WCHAR *third, const WCHAR *extra) { - text(ID_PRIMARY, primary); text(ID_SECOND, second); text(ID_EXTRA, extra); + text(ID_PRIMARY, primary); text(ID_SECOND, second); text(ID_THIRD, third); text(ID_EXTRA, extra); EnableWindow(H(ID_PRIMARY), TRUE); if (primary) SetFocus(H(ID_PRIMARY)); } +static void buttons(const WCHAR *primary, const WCHAR *second, const WCHAR *extra) { buttons4(primary, second, NULL, extra); } static void progress(BOOL marquee) { @@ -96,11 +120,11 @@ static void progress(BOOL marquee) static void fmt_mb(WCHAR *o, DWORD n) { wsprintfW(o, L"%lu.%lu", n / 1048576, n % 1048576 / 104858); } // A download that can be continued where it stopped (its .part and ETag are there): appends how much is already received to g_note and returns TRUE. -// driver: the driver zip, else the DynaRun zip. The button then reads "Continue download" instead of "Retry". -static BOOL resume_note(BOOL driver) +// The button then reads "Continue download" instead of "Retry". +static BOOL resume_note(void) { DWORD have = 0; WCHAR a[24], t[24], line[200]; - if (!(driver ? driver_partial(&have) : pkg_download_partial(&have))) return FALSE; + if (!g_acq->partial(&have)) return FALSE; fmt_mb(a, have); if (g_dltotal > have) { fmt_mb(t, g_dltotal); @@ -139,13 +163,13 @@ static void running_names(int run, WCHAR *out) // The size is the server's (Content-Length), shown once the download has started. static void fetch_body(DWORD total) { - WCHAR size[40], b[400]; + WCHAR size[40], b[500]; size[0] = 0; if (total) wsprintfW(size, T(L" (%lu.%lu MB)", L"(%lu.%lu MB)"), total / 1048576, total % 1048576 / 104858); - wsprintfW(b, T(L"Downloading the DynaRun V3 setup from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" - L"If it does not work, click \"Download manually\" to get it from the website yourself.", - L"正在從 Dyna Pro 官網下載 DynaRun V3 安裝檔%s,可能需要幾分鐘。\n\n" - L"如果下載不順利,可以按「開啟官網手動下載」,自己從官網下載。"), size); + wsprintfW(b, T(L"Getting %s: a copy on this computer is used if there is one, else it is downloaded from the Dyna Pro website%s.\nThis can take a few minutes.\n\n" + L"If it does not work, click \"Open website\" to get it from the website yourself.", + L"正在取得%s:電腦上已有就直接使用,否則從 Dyna Pro 官網下載%s,可能需要幾分鐘。\n\n" + L"如果下載不順利,可以按「開啟官網」,自己從官網下載。"), ACQ_NAME(g_acq), size); g_total = total; text(ID_BODY, b); } @@ -206,20 +230,22 @@ static HBITMAP load_png(int id, int *w, int *h) // Highlight of the "7) Disable driver signature enforcement" line of the restart menu picture (driver-press-7.png, 683 x 468): the line's // rectangle in picture pixels. Drawn on the scaled copy, so the outline stays sharp at any size and DPI. -#define HL_X0 110 -#define HL_Y0 259 -#define HL_X1 306 -#define HL_Y1 285 +// Line 7 of the picture (683 x 468) has its text in rows 266..278 and columns 121..267; line 6 ends at row 258, line 8 starts at row 286. The outline runs 4 rows +// away from the text on each side, in the middle of the gaps, and is 2 pixels thick. +#define HL_X0 116 +#define HL_Y0 262 +#define HL_X1 272 +#define HL_Y1 282 static void draw_highlight(HDC dc, int sw, int sh, int tw, int th) { int x0 = MulDiv(HL_X0, tw, sw), y0 = MulDiv(HL_Y0, th, sh), x1 = MulDiv(HL_X1, tw, sw), y1 = MulDiv(HL_Y1, th, sh); - int lw = S(3) < 2 ? 2 : S(3), rh = y1 - y0, rad = rh / 2 + 2, lx0 = x1 + lw + rh, lx1; + int lw = S(2) < 2 ? 2 : S(2), rh = y1 - y0, rad = rh / 2 + 2, lx0 = x1 + lw + rh, lx1; HPEN pen = CreatePen(PS_SOLID, lw, RGB(255, 153, 0)), op; HBRUSH red = CreateSolidBrush(RGB(224, 52, 0)), ob = (HBRUSH)SelectObject(dc, GetStockObject(NULL_BRUSH)); LOGFONTW lf; HFONT f, of; const WCHAR *t = T(L"Press 7", L"按 7"); SIZE sz; POINT tri[3]; RECT tr; op = (HPEN)SelectObject(dc, pen); - RoundRect(dc, x0 - lw, y0 - lw, x1 + lw, y1 + lw, rad, rad); // orange outline around the line + RoundRect(dc, x0, y0, x1, y1, rad, rad); // orange outline around the line (the pen is centred on the edge) // callout to the right of the line: red label "Press 7" with a small arrow pointing at it GetObjectW(g_font, sizeof(lf), &lf); lf.lfHeight = -(rh * 7 / 10 < 10 ? 10 : rh * 7 / 10); lf.lfWeight = FW_BOLD; lf.lfQuality = CLEARTYPE_QUALITY; f = CreateFontIndirectW(&lf); of = (HFONT)SelectObject(dc, f); @@ -227,12 +253,12 @@ static void draw_highlight(HDC dc, int sw, int sh, int tw, int th) lx1 = lx0 + sz.cx + rh; if (lx1 > tw - 2) { lx0 -= lx1 - (tw - 2); lx1 = tw - 2; } // keep it inside the picture SelectObject(dc, GetStockObject(NULL_PEN)); SelectObject(dc, red); - RoundRect(dc, lx0, y0 - lw, lx1, y1 + lw, rad, rad); + RoundRect(dc, lx0, y0, lx1, y1, rad, rad); tri[0].x = x1 + lw + 3; tri[0].y = (y0 + y1) / 2; // tip, next to the outline - tri[1].x = lx0 + 1; tri[1].y = y0 - lw; tri[2].x = lx0 + 1; tri[2].y = y1 + lw; + tri[1].x = lx0 + 1; tri[1].y = y0; tri[2].x = lx0 + 1; tri[2].y = y1; Polygon(dc, tri, 3); SetBkMode(dc, TRANSPARENT); SetTextColor(dc, RGB(255, 255, 255)); - tr.left = lx0; tr.right = lx1; tr.top = y0 - lw; tr.bottom = y1 + lw; + tr.left = lx0; tr.right = lx1; tr.top = y0; tr.bottom = y1; DrawTextW(dc, t, -1, &tr, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); SelectObject(dc, of); DeleteObject(f); SelectObject(dc, op); SelectObject(dc, ob); DeleteObject(pen); DeleteObject(red); @@ -283,6 +309,23 @@ static int place_image(int x, int y, int w, int ah) // The "Start DynaRun" button is offered only when DynaRun is installed here and our launcher is in place (g_exe comes from locate_dynarun, which // the wizard runs at start in every mode, so this also holds in the process the sign-in task starts after the restart; g_launcher from init_paths). +// The driver step may be skipped (back to the end of the install) only in the main install flow, not when it is the whole job (the shortcut, /driver, or the driver-only first page). +static BOOL can_skip_driver(void) { return g_drvmode == DRV_MODE_NONE && !g_driver_only; } + +// The line about a newer DynaRun on the first page, and its button. A hint only: nothing happens unless the button is clicked. +static void hint_line(WCHAR *b) +{ + WCHAR t[400]; + if (g_hint == 1) { + wsprintfW(t, T(L"\n\nA newer DynaRun V3 is available on Dyna Pro's website: %s (installed: %s). \"Download new version\" downloads and installs it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網有新版 DynaRun V3:%s(目前已安裝:%s)。按「下載新版」會下載並安裝;不按就什麼都不會發生。"), g_hint_new, g_hint_inst); + lstrcatW(b, t); + } else if (g_hint == 2) + lstrcatW(b, T(L"\n\nDyna Pro's website may have a newer DynaRun V3 (the file there is not the one downloaded last time). \"Download new version\" gets it; nothing happens unless you click it.", + L"\n\nDyna Pro 官網可能有新版 DynaRun V3(那裡的檔案和上次下載的不同)。按「下載新版」會取得它;不按就什麼都不會發生。")); +} +static const WCHAR *hint_btn(void) { return g_hint ? T(L"Download new version", L"下載新版") : NULL; } + static BOOL can_start_dynarun(void) { return g_installed && g_exe[0] && exists(g_launcher); } // Appends s to d (a buffer of cap characters), cutting it where it no longer fits; d always stays terminated. @@ -303,6 +346,20 @@ static void set_page(int p) vis(ID_EDIT, FALSE); vis(ID_RICH, FALSE); vis(ID_CHECK, FALSE); vis(ID_PIN, FALSE); vis(ID_AUTO, FALSE); vis(ID_DRV, FALSE); vis(ID_IMG, FALSE); vis(ID_INFO, FALSE); vis(ID_PROG, FALSE); status(NULL, FALSE); switch (p) { case P_HELLO: + if (g_driver_only) { + wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThe fix for Windows 10/11 is already installed (version %s). The Dyna Pro USB driver is not installed yet; " + L"without it this computer cannot talk to the dynamometer.\n\nThis installs the driver (one restart is needed). When Windows asks whether to allow changes, click \"Yes\".\n\n" + L"If DynaRun does not behave, \"Reinstall the fix...\" installs the fix again.", + L"已找到 DynaRun V3:\n%s\n\nWindows 10/11 的相容性修正已經安裝(版本 %s)。但 Dyna Pro USB 驅動程式還沒有安裝;" + L"沒有它,這台電腦就無法和馬力機連線。\n\n接下來會安裝驅動程式(需要重新開機一次)。Windows 詢問「是否允許變更」時,請按「是」。\n\n" + L"如果 DynaRun 運作不正常,可以按「重新安裝修正…」再安裝一次修正。"), g_exe, WIDEN(DRF_DISPLAY)); + hint_line(b); + text(ID_TITLE, T(L"Install the Dyna Pro USB driver", L"安裝 Dyna Pro USB 驅動程式")); + text(ID_BODY, b); + buttons4(T(L"Install the driver", L"安裝驅動程式"), T(L"Cancel", L"取消"), hint_btn(), T(L"Reinstall the fix...", L"重新安裝修正…")); + if (g_note[0]) status(g_note, TRUE); + break; + } if (g_installed) { text(ID_TITLE, T(L"Fix DynaRun V3 for this Windows", L"讓 DynaRun V3 在這台電腦正常運作")); wsprintfW(b, T(L"DynaRun V3 was found:\n%s\n\nThis installs the fixes for Windows 10/11 (flickering main screen, .Dpr files " @@ -310,8 +367,9 @@ static void set_page(int p) L"When Windows asks whether to allow changes, click \"Yes\".%s", L"已找到 DynaRun V3:\n%s\n\n接下來會安裝 Windows 10/11 的相容性修正(主畫面閃爍、OneDrive 裡的 .Dpr 打不開、" L"中文亂碼、以系統管理員執行卡住)。\n\nWindows 詢問「是否允許變更」時,請按「是」。%s"), g_exe, disclaimer()); + hint_line(b); text(ID_BODY, b); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); + buttons4(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), hint_btn(), NULL); } else { text(ID_TITLE, T(L"Install DynaRun V3", L"安裝 DynaRun V3")); wsprintfW(b, T(L"DynaRun V3 is not installed on this computer yet.\n\nThis program will:\n" @@ -330,24 +388,29 @@ static void set_page(int p) vis(ID_PIN, TRUE); vis(ID_AUTO, TRUE); vis(ID_DRV, g_drv_avail); if (g_note[0]) status(g_note, TRUE); break; - case P_FETCH: - text(ID_TITLE, T(L"Downloading the setup", L"正在下載安裝檔")); + case P_FETCH: // looking for a copy here, downloading, checking: the same page for the DynaRun setup and the driver + wsprintfW(b, T(L"Getting %s", L"正在取得%s"), ACQ_NAME(g_acq)); + text(ID_TITLE, b); fetch_body(0); progress(FALSE); status(L" ", FALSE); - buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Download manually", L"開啟官網手動下載")); + buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); break; - case P_FAIL: - text(ID_TITLE, g_badfile ? T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔") - : T(L"The setup could not be downloaded", L"無法自動下載安裝檔")); + case P_FAIL: // the one error page of the acquire flow + if (g_badfile) lstrcpyW(b, T(L"This is not the DynaRun V3 setup", L"這不是 DynaRun V3 安裝檔")); + else wsprintfW(b, T(L"%s could not be downloaded", L"無法自動下載%s"), ACQ_NAME(g_acq)); + if (!g_zh && !g_badfile) b[0] = (WCHAR)(b[0] >= 'a' && b[0] <= 'z' ? b[0] - 32 : b[0]); // "the DynaRun V3 setup" at the start of a sentence + text(ID_TITLE, b); { - BOOL resume = !g_badfile && resume_note(FALSE); // a .part to carry on from (appends its size to the note) + BOOL resume = !g_badfile && resume_note(); // a .part to carry on from (appends its size to the note) + BOOL skip = g_acq == &ACQ_DRIVER && can_skip_driver(); g_badfile = FALSE; - wsprintfW(b, T(L"%s\n\nDownload it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for \"Dyna Run V3 Software\"\n" - L" 3. come back here and click \"Retry\" or \"Continue download\" (or \"Choose file...\")", - L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載「Dyna Run V3 Software」\n" - L" 3. 下載完成後回到這裡按「重試」或「繼續下載」(或「選擇檔案…」)"), g_note); + wsprintfW(b, T(L"%s\n\nGet it yourself:\n 1. click \"Open website\"\n 2. on the page, click the download for %s\n" + L" 3. come back here and click \"Retry\" or \"Continue download\", or \"Choose file...\" to pick %s", + L"%s\n\n請自己下載:\n 1. 按「開啟官網」\n 2. 在網頁上下載%s\n" + L" 3. 回到這裡按「重試」或「繼續下載」,或按「選擇檔案…」選取%s"), g_note, T(g_acq->get_en, g_acq->get_zh), T(g_acq->pick_en, g_acq->pick_zh)); text(ID_BODY, b); - buttons(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); + buttons4(resume ? T(L"Continue download", L"繼續下載") : T(L"Retry", L"重試"), skip ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), + T(L"Choose file...", L"選擇檔案…"), T(L"Open website", L"開啟官網")); } break; case P_PASSWORD: @@ -356,7 +419,7 @@ static void set_page(int p) L"請輸入 Dyna Pro 給你的 DynaRun V3 安裝密碼。\n(只用來打開安裝檔,不會被儲存。)\n\n安裝檔:%s"), file_name(g_pkg)); text(ID_BODY, b); vis(ID_EDIT, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Other file...", L"選擇其他檔案…")); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Choose file...", L"選擇檔案…")); if (g_note[0]) status(g_note, TRUE); SetFocus(H(ID_EDIT)); SendMessageW(H(ID_EDIT), EM_SETSEL, 0, -1); break; @@ -372,7 +435,7 @@ static void set_page(int p) L"請與 Dyna Pro 提供的資訊比對。Dyna Pro 的安裝檔沒有數位簽章,DynaRunFix 無法證明它與 Dyna Pro 發布的完全一致,請只安裝從 Dyna Pro 取得的檔案。")); SetWindowTextW(H(ID_INFO), g_info); vis(ID_INFO, TRUE); - buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open Dyna Pro website", L"開啟 Dyna Pro 官網")); + buttons(T(L"Next", L"下一步"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); if (g_note[0]) status(g_note, TRUE); break; case P_LICENSE: @@ -384,7 +447,7 @@ static void set_page(int p) SetWindowTextW(H(ID_CHECK), g_nolicense ? T(L"I understand and &accept Dyna Pro's license terms", L"我了解並同意 Dyna Pro 的授權條款(&A)") : T(L"I &accept the license agreement", L"我接受授權合約(&A)")); vis(ID_RICH, TRUE); vis(ID_CHECK, TRUE); - buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open Dyna Pro website", L"開啟 Dyna Pro 官網") : NULL); + buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), g_nolicense ? T(L"Open website", L"開啟官網") : NULL); EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); SetFocus(H(ID_CHECK)); // the disabled "Install" button cannot hold the keyboard focus if (g_note[0]) status(g_note, TRUE); @@ -464,26 +527,9 @@ static void set_page(int p) L"廠商封裝中的驅動程式設定檔(INF)不在隨附的 Microsoft 簽署目錄檔中,Windows 無法驗證,所以只有在這一次開機暫時關閉「驅動程式簽章檢查」時才肯安裝;" L"下次重新開機會自動恢復。\n\n" L"如果這台電腦有啟用 BitLocker,保護會暫停到驅動程式安裝完成為止,之後會自動恢復(最多經過 3 次重新開機)。")); - { // after a failed download: carry on from the .part when there is one, and offer the website - BOOL resume = g_drv_fetch_fail && resume_note(TRUE); - buttons(resume ? T(L"Continue download", L"繼續下載") : g_drv_fetch_fail ? T(L"Retry", L"重試") : T(L"Download and continue", L"下載並繼續"), - g_drvmode == DRV_MODE_NONE ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), - g_drv_fetch_fail ? T(L"Download manually", L"開啟官網手動下載") : NULL); - g_drv_fetch_fail = FALSE; - } + buttons(T(L"Next", L"下一步"), can_skip_driver() ? T(L"Skip", L"略過") : T(L"Cancel", L"取消"), NULL); if (g_note[0]) status(g_note, TRUE); break; - case P_DRV_FETCH: - text(ID_TITLE, T(L"Downloading the driver", L"正在下載驅動程式")); - text(ID_BODY, T(L"Downloading the driver from the Dyna Pro website and checking it...\n\n" - L"If it does not work, click \"Download manually\" to get \"Dyna Pro USB.zip\" (under USB Drivers) from the website yourself, " - L"or install the driver by hand with the steps in the manual.", - L"正在從 Dyna Pro 官網下載驅動程式並檢查…\n\n" - L"如果下載不順利,可以按「開啟官網手動下載」,自己從官網下載「Dyna Pro USB.zip」(USB Drivers 區)," - L"或照說明文件的手動步驟安裝。")); - progress(FALSE); status(L" ", FALSE); - buttons(T(L"Pause", L"暫停"), T(L"Cancel", L"取消"), T(L"Download manually", L"開啟官網手動下載")); - break; case P_DRV_PRESS7: text(ID_TITLE, T(L"After the restart you will see this: press 7", L"重新開機後會看到這個畫面,請按 7")); { @@ -491,7 +537,7 @@ static void set_page(int p) b[0] = 0; if (g_drv_allow) lstrcatW(b, T(L"Note: this driver file is not the version DynaRunFix knows; you chose to install it.\n\n", L"注意:這個驅動程式檔和已知版本不同,是你確認要安裝的。\n\n")); else if (g_drv_how & DRV_SRC_LOCAL) { - WCHAR nm[100], t[160]; lstrcpynW(nm, file_name(g_drvzip), 100); + WCHAR nm[100], t[300]; lstrcpynW(nm, file_name(g_drvzip), 100); // the longest text: ~95 chars + 99 of nm if (g_drv_how & DRV_SRC_DIR) wsprintfW(t, T(L"The unpacked driver folder (%s) was found on this computer and checked: no need to download it.\n\n", L"在這台電腦上找到已解開的驅動程式資料夾(%s),檢查通過,不用重新下載。\n\n"), nm); else wsprintfW(t, T(L"The driver (%s) was found on this computer and checked: no need to download it again.\n\n", L"在這台電腦上找到驅動程式(%s),檢查通過,不用重新下載。\n\n"), nm); lstrcatW(b, t); @@ -561,7 +607,7 @@ static void set_page(int p) } text(ID_BODY, b); // "Cancel" is the default button (the right-most one, Enter); "Install anyway" is the second; the website is bottom-left - buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Download manually", L"開啟官網手動下載")); + buttons(T(L"Cancel", L"取消"), T(L"Install anyway", L"仍要安裝"), T(L"Open website", L"開啟官網")); if (g_note[0]) status(g_note, TRUE); break; } @@ -597,13 +643,13 @@ static void set_page(int p) L"Try again? The computer restarts once more. When the menu appears, press 7.", L"剛才重新開機時沒有選到「停用驅動程式強制簽章」(按 7),所以 Windows 還在檢查簽章,驅動程式現在還不能安裝。\n\n" L"要再試一次嗎?電腦會再重新開機一次,出現選單時請按 7。")); - buttons(T(L"Try again", L"再試一次"), T(L"Cancel", L"取消"), NULL); + buttons(T(L"Retry", L"重試"), T(L"Cancel", L"取消"), NULL); if (g_note[0]) status(g_note, TRUE); break; case P_DRV_WARN: text(ID_TITLE, T(L"Last step: install the driver", L"最後一步:安裝驅動程式")); - text(ID_BODY, T(L"Driver signature checking is off for this start. Windows may show a red warning like this one, twice (one for each driver file): choose \"Install this driver software anyway\" each time.", - L"這次開機已暫時關閉簽章檢查。Windows 可能會跳出像下面這樣的紅色警告(無法驗證此驅動程式軟體的發行者),而且會出現兩次(各對應一個驅動程式檔),每次都請選「仍然安裝此驅動程式軟體」。")); + text(ID_BODY, T(L"Driver signature checking is off for this start. Next, Windows will show a red security window like this one twice (once for each driver file): click \"Install this driver software anyway\" each time.", + L"這次開機已暫時關閉簽章檢查。接下來 Windows 會跳出像下面這樣的紅色安全性視窗兩次(兩個驅動程式檔各一次),請每次都按「仍然安裝此驅動程式軟體」。")); show_image(1); buttons(T(L"Install", L"安裝"), T(L"Cancel", L"取消"), NULL); if (g_note[0]) status(g_note, TRUE); @@ -655,17 +701,23 @@ static DWORD WINAPI job_proc(LPVOID unused) { int rc = 1; switch (g_job) { - case JOB_DOWNLOAD: rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); break; - case JOB_EXTRACT: rc = zip_extract(&g_zip, g_pw, g_msi, on_progress, &g_cancel); SecureZeroMemory(g_pw, sizeof(g_pw)); break; // the password is not needed any more - case JOB_INSTALL_ALL: rc = install_all(g_msi); break; - case JOB_INSTALL_FIX: rc = install_fix(); break; - case JOB_DRV_FETCH: - rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); - if (rc == PK_OK || rc == DRV_W_HASH) { - on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked - g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds + case JOB_ACQUIRE: // the DynaRun setup or the driver: a copy on this computer first, else the download + on_progress((DWORD)-1, 2); // the page shows "looking" (a moving bar) until a download starts + if (g_acq == &ACQ_DRIVER) { + rc = driver_fetch(g_drvzip, on_progress, &g_cancel, &g_neterr, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + if (rc == PK_OK || rc == DRV_W_HASH) { + on_progress((DWORD)-1, 1); // the page says "checking" (marquee) while the BitLocker state is asked + g_drv_bl = driver_bitlocker(); // here, not while the next page is built: the WMI query can block for seconds + } + } else { + g_pkg_found = g_acq_local_ok && pkg_find_local(g_pkg); + if (g_pkg_found) rc = PK_OK; + else { g_pkg_ours = TRUE; rc = pkg_download(g_pkg, on_progress, &g_cancel, &g_neterr); } } break; + case JOB_EXTRACT: rc = pkg_extract_checked(&g_zip, g_pw, g_msi, 400UL * 1024UL * 1024UL, on_progress, &g_cancel); SecureZeroMemory(g_pw, sizeof(g_pw)); break; // a size cap, and a free target (shared with the driver) // the password is not needed any more + case JOB_INSTALL_ALL: rc = install_all(g_msi); break; + case JOB_INSTALL_FIX: rc = install_fix(); break; case JOB_DRV_PREP: // "install anyway" accepts exactly this zip (its SHA-256), nothing else. (No comment after the call on the line of a break: it once swallowed the // break, and the install followed the prep at once, without the restart.) @@ -679,11 +731,22 @@ static DWORD WINAPI job_proc(LPVOID unused) return 0; } +// The update hint is asked in the background when the first page shows an installed DynaRun; the answer comes as WM_HINT. Never waited for. +static DWORD WINAPI hint_proc(LPVOID unused) +{ + static WCHAR i[40], n[40]; int r; + r = pkg_update_hint(i, n, 40, user_log); + if (r) { lstrcpyW(g_hint_inst, i); lstrcpyW(g_hint_new, n); } + if (g_hwnd) PostMessageW(g_hwnd, WM_HINT, (WPARAM)r, 0); + (void)unused; + return 0; +} + static void start_job(int job, int page) { DWORD tid; g_job = job; g_cancel = 0; g_busy = TRUE; - if ((job == JOB_DOWNLOAD || job == JOB_DRV_FETCH) && job != g_dljob) { g_dljob = job; g_dltotal = g_dlhave = 0; } // a resume of the same job keeps what is known + if (job == JOB_ACQUIRE && g_dlacq != g_acq) { g_dlacq = g_acq; g_dltotal = g_dlhave = 0; } // a resume of the same download keeps what is known set_page(page); g_thread = CreateThread(NULL, 0, job_proc, NULL, 0, &tid); if (job == JOB_INSTALL_ALL || job == JOB_INSTALL_FIX || job >= JOB_DRV_PREP) SetTimer(g_hwnd, ID_KEEPUP, 300, NULL); @@ -710,19 +773,20 @@ static void install(int job) else start_job(job, P_INSTALL); } -static void fetch(void) +// Starts the acquire flow for the DynaRun setup or the driver (the same pages; a network check comes first inside the download). +static void acq_start(const acq_desc *d) { - g_pkg_ours = FALSE; - g_pkg_found = g_nfound < 8 && pkg_find_local(g_pkg); // the cap: pkg_reject remembers 8 files - if (g_pkg_found) { g_nfound++; open_package(); return; } - g_pkg_ours = TRUE; - start_job(JOB_DOWNLOAD, P_FETCH); + g_acq = d; + if (d == &ACQ_SETUP) { g_pkg_ours = g_pkg_found = FALSE; g_acq_local_ok = !g_acq_fresh && g_nfound < 8; } // the cap: pkg_reject remembers 8 files + else { g_drvzip[0] = 0; g_drv_allow = FALSE; } + start_job(JOB_ACQUIRE, P_FETCH); } // Whatever the file is called: only the real DynaRun setup is ever installed. static void not_dynarun(void) { WCHAR m[MAX_PATH], nm[100]; + g_acq = &ACQ_SETUP; lstrcpynW(nm, file_name(g_pkg), 100); // wsprintfW does not check the size of g_note: a long file name must not overflow it wsprintfW(g_note, T(L"\"%s\" is not the DynaRun V3 setup, so it will not be installed.", L"「%s」不是 DynaRun V3 的安裝檔,所以不會安裝它。"), nm); @@ -730,7 +794,7 @@ static void not_dynarun(void) pkg_temp_dir(m); lstrcatW(m, L"\\Setup.msi"); if (!lstrcmpiW(g_msi, m)) DeleteFileW(g_msi); // our extracted copy if (g_pkg_ours) DeleteFileW(g_pkg); - if (g_pkg_found) { fetch(); return; } // we picked it, not the user: quietly try the next one or download + if (g_pkg_found) { acq_start(&ACQ_SETUP); return; } // we picked it, not the user: quietly try the next one or download g_badfile = TRUE; set_page(P_FAIL); } @@ -785,11 +849,11 @@ static void after_extract(void) L"and their terms and conditions of sale. The agreement is part of Dyna Pro's setup, but this " L"installer could not read it from this setup file.\r\n\r\n" L"To read it, contact Dyna Pro (support@dynapro.co.uk) or see their website " - L"(\"Open Dyna Pro website\" below).\r\n\r\n" + L"(\"Open website\" below).\r\n\r\n" L"Install only if you accept Dyna Pro's license terms.", L"DynaRun V3 是 Dyna Pro Dynamometers Ltd 依其使用者授權合約及銷售條款授權給你使用的軟體。" L"授權合約包含在 Dyna Pro 的安裝檔中,但本安裝程式無法從這個安裝檔讀出合約內容。\r\n\r\n" - L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟 Dyna Pro 官網」)。\r\n\r\n" + L"如需閱讀合約,請聯絡 Dyna Pro(support@dynapro.co.uk)或參考其官網(下方「開啟官網」)。\r\n\r\n" L"請在同意 Dyna Pro 的授權條款後再安裝。")); SendMessageW(H(ID_CHECK), BM_SETCHECK, BST_UNCHECKED, 0); set_page(P_CHECK); @@ -873,11 +937,6 @@ static void drv_error(int rc) lstrcpyW(g_note, T(L"The downloaded file is not the one this program was made for (its SHA-256 differs), so it was not installed. Dyna Pro may have changed the file.", L"下載到的檔案和這個程式預期的不一樣(SHA-256 不符),為了安全沒有安裝。可能是 Dyna Pro 更新了檔案。")); break; - case PK_URL: lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk.", L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk。")); break; - case PK_CERT: wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu).", L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu)。"), g_neterr); break; - case PK_HTTP: wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); break; - case PK_IO: wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); break; - case PK_NET: wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu).", L"連不上 Dyna Pro 官網(錯誤 %lu)。"), g_neterr); break; case DRV_E_BCD: lstrcpyW(g_note, T(L"Windows could not be told to show its start-up menu at the next start (bcdedit failed).", L"無法設定「下次開機顯示進階選單」(bcdedit 失敗)。")); break; case DRV_E_TASK: lstrcpyW(g_note, T(L"The task that continues the installation after the restart could not be created.", L"無法建立重新開機後繼續安裝的排程工作。")); break; case DRV_E_DIR: lstrcpyW(g_note, T(L"The working folder %ProgramData%\\DynaRunFix could not be made safe (an old folder is still in use, or it can be changed by other users). Nothing was installed. Close other programs, restart the PC and try again.", @@ -926,6 +985,30 @@ static void drv_start(void) } else set_page(g_drv_have ? P_DRV_DONE : P_DRV_INTRO); } +// The one text for every way the acquire flow can end badly (the DynaRun setup and the driver alike): g_note. +static void acq_note(int rc) +{ + switch (rc) { + case PK_CANCEL: lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); break; + case PK_HTTP: wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); break; + case PK_CERT: wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. Check the date and time of this computer.", + L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間。"), g_neterr); break; + case PK_URL: lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", + L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); break; + case PK_IO: wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); break; + case DRV_E_HASH: lstrcpyW(g_note, T(L"That file is not a usable Dyna Pro USB driver package (the driver files in it are missing, or are not Dyna Pro's).", + L"這個檔案不是可用的 Dyna Pro USB 驅動程式套件(裡面缺少驅動程式檔,或不是 Dyna Pro 的)。")); break; + default: // PK_NET and anything unforeseen: the first real error of the attempts + if (g_neterr == ERROR_INTERNET_DISCONNECTED) lstrcpyW(g_note, T(L"This computer is not connected to a network (Windows reports no connection). Connect it and try again.", + L"這台電腦沒有連上網路(Windows 回報沒有網路連線)。請連上網路後再試。")); + else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), g_neterr); + break; + } +} + +// The one error page of the acquire flow. +static void acq_fail(int rc) { acq_note(rc); set_page(P_FAIL); } + // The download worker has ended because "Pause" was clicked: the .part and its ETag are kept, the page stays, the button becomes "Resume". static void fetch_paused(void) { @@ -934,22 +1017,14 @@ static void fetch_paused(void) fmt_mb(a, g_dlhave); if (g_dltotal) { fmt_mb(t, g_dltotal); wsprintfW(s, T(L"Paused: %s of %s MB downloaded.", L"已暫停:已下載 %s / %s MB。"), a, t); } else wsprintfW(s, T(L"Paused: %s MB downloaded.", L"已暫停:已下載 %s MB。"), a); - buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Download manually", L"開啟官網手動下載")); + buttons(T(L"Resume", L"繼續"), T(L"Cancel", L"取消"), T(L"Open website", L"開啟官網")); status(s, FALSE); layout(); } // "Cancel" while paused (no worker is running any more): the same result as a cancel during the transfer. -static void fetch_stopped(void) -{ - BOOL drv = g_page == P_DRV_FETCH; - g_paused = FALSE; - lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); - set_page(drv ? P_DRV_INTRO : P_FAIL); -} +static void fetch_stopped(void) { g_paused = FALSE; acq_fail(PK_CANCEL); } -// A failed driver download (no connection, server error, file could not be saved): back to the first driver page with the reason, the way to carry on -// from the .part, and the website; the other driver errors go to the error page (drv_error). // The choice on the warning page: install the zip anyway (the elevated step is told, see driver_prep) or cancel. Both are logged. static void hash_choice(BOOL install) { @@ -958,20 +1033,116 @@ static void hash_choice(BOOL install) lstrcatW(l, g_drvzip); user_log(l); if (install) { g_drv_allow = TRUE; set_page(P_DRV_PRESS7); return; } if (!(g_drv_how & DRV_SRC_LOCAL)) driver_forget(g_drvzip); // our own refused download is not offered again; a file of the user's stays - if (g_drv_how & DRV_SRC_LOCAL) driver_skip_local_other(); // copies that differ are not offered again in this run: later tries download + if ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) { driver_skip_local_other(); user_log(L"the declined copy was found automatically: automatic searches skip such copies from now on"); } // later tries download + else if (g_drv_how & DRV_SRC_PICKED) user_log(L"the declined file was chosen by the user: automatic searches are not changed"); g_drvzip[0] = 0; g_drv_allow = FALSE; - lstrcpyW(g_note, (g_drv_how & DRV_SRC_LOCAL) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") + lstrcpyW(g_note, ((g_drv_how & DRV_SRC_LOCAL) && !(g_drv_how & DRV_SRC_PICKED)) ? T(L"Cancelled: the driver was not installed. Trying again downloads it from the Dyna Pro website.", L"已取消,沒有安裝驅動程式。再試一次會從 Dyna Pro 官網下載。") : T(L"Cancelled: the driver was not installed.", L"已取消,沒有安裝驅動程式。")); set_page(P_DRV_INTRO); } -static void drv_net_error(int rc) +// What the driver check says (from the search, the download or a file the user chose): on to the restart page, to the warning page, or the error page. +static void acq_driver_result(int rc) { - if (rc == PK_HTTP) wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); - else if (rc == PK_IO) wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); - else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu).", L"連不上 Dyna Pro 官網(錯誤 %lu)。"), g_neterr); - g_drv_fetch_fail = TRUE; - set_page(P_DRV_INTRO); + if (rc == PK_OK) { + g_drv_allow = FALSE; + if (!g_bmp[0]) g_bmp[0] = load_png(18, &g_bw[0], &g_bh[0]); // the picture is decoded before the page changes, so the new page is built at once + set_page(P_DRV_PRESS7); + } else if (rc == DRV_W_HASH) { + WCHAR m[2 * MAX_PATH + 100]; + g_drv_allow = FALSE; + lstrcpyW(m, L"driver package with other files: the warning page is shown for "); lstrcatW(m, g_drvzip); user_log(m); + set_page(P_DRV_HASH); + } else acq_fail(rc); +} + +// The end of the acquire job. The DynaRun setup goes on to its password / check / license pages (open_package), the driver to its restart or warning page. +static void acq_done(int rc) +{ + BOOL drv = g_acq == &ACQ_DRIVER; + if ((rc == PK_OK || (drv && rc == DRV_W_HASH)) && g_cancel == 1) { // "Cancel" while the file was being checked + if (drv && !(g_drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(g_drvzip); + acq_fail(PK_CANCEL); + return; + } + if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); return; } + if (drv) { acq_driver_result(rc); return; } + if (rc == PK_OK) { if (g_pkg_found) g_nfound++; open_package(); return; } + acq_fail(rc); +} + +// A file dialog for the driver: one dialog, filter "Dyna Pro USB driver (*.zip;*.inf)". IFileOpenDialog where there is one (Vista and later), else the classic dialog. +// The path of the chosen file in p (MAX_PATH); FALSE when the user cancelled. +static BOOL pick_driver_file(WCHAR *p) +{ + static const GUID clsid = { 0xDC1C5A9C, 0xE88A, 0x4DDE, { 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7 } }; // CLSID_FileOpenDialog + static const GUID iid = { 0xD57C7288, 0xD4AD, 0x4768, { 0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60 } }; // IID_IFileOpenDialog + IFileOpenDialog *fd = NULL; HRESULT hr, hrc; BOOL ok = FALSE; + p[0] = 0; + hrc = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if ((SUCCEEDED(hrc) || hrc == RPC_E_CHANGED_MODE) && SUCCEEDED(CoCreateInstance(&clsid, NULL, CLSCTX_INPROC_SERVER, &iid, (void **)&fd))) { + COMDLG_FILTERSPEC spec[2]; DWORD opt = 0; + spec[0].pszName = T(L"Dyna Pro USB driver (*.zip;*.inf)", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)"); spec[0].pszSpec = L"*.zip;*.inf"; + spec[1].pszName = T(L"All files", L"所有檔案"); spec[1].pszSpec = L"*.*"; + fd->lpVtbl->SetFileTypes(fd, 2, spec); + fd->lpVtbl->SetTitle(fd, T(L"Choose the Dyna Pro USB driver: Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", + L"選擇 Dyna Pro USB 驅動程式:Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf")); + fd->lpVtbl->SetOkButtonLabel(fd, T(L"Use this", L"使用這個")); + if (SUCCEEDED(fd->lpVtbl->GetOptions(fd, &opt))) fd->lpVtbl->SetOptions(fd, opt | FOS_FORCEFILESYSTEM | FOS_FILEMUSTEXIST | FOS_PATHMUSTEXIST); + hr = fd->lpVtbl->Show(fd, g_hwnd); + if (SUCCEEDED(hr)) { + IShellItem *si = NULL; + if (SUCCEEDED(fd->lpVtbl->GetResult(fd, &si)) && si) { + WCHAR *name = NULL; + if (SUCCEEDED(si->lpVtbl->GetDisplayName(si, SIGDN_FILESYSPATH, &name)) && name) { + if (lstrlenW(name) < MAX_PATH) { lstrcpyW(p, name); ok = TRUE; } + CoTaskMemFree(name); + } + si->lpVtbl->Release(si); + } + } + fd->lpVtbl->Release(fd); + } else { + OPENFILENAMEW of; + zero(&of, sizeof(of)); + of.lStructSize = sizeof(of); of.hwndOwner = g_hwnd; + of.lpstrFilter = T(L"Dyna Pro USB driver (*.zip;*.inf)\0*.zip;*.inf\0All files\0*.*\0", L"Dyna Pro USB 驅動程式 (*.zip;*.inf)\0*.zip;*.inf\0所有檔案\0*.*\0"); + of.lpstrTitle = T(L"Choose Dyna Pro USB.zip, or ftdibus.inf in the folder where it is unpacked", L"選擇 Dyna Pro USB.zip,或已解開它的資料夾裡的 ftdibus.inf"); + of.lpstrFile = p; of.nMaxFile = MAX_PATH; of.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + ok = GetOpenFileNameW(&of); + } + if (SUCCEEDED(hrc)) CoUninitialize(); + return ok && p[0]; +} + +// "Choose file..." for the driver: a zip, or an .inf of the folder it is unpacked in (the folder that holds the expected layout is looked for from there, up to +// two levels up); the same check as for a copy found by the search, then the same pages. +static void choose_driver_source(void) +{ + WCHAR p[MAX_PATH], d[MAX_PATH]; int rc, k, n; HCURSOR old; + if (!pick_driver_file(p)) return; + n = lstrlenW(p); + if (n > 4 && !lstrcmpiW(p + n - 4, L".inf")) { // an .inf: use the folder it is in, or the first of its parents with the layout (ftdibus.inf, i386, amd64) + WCHAR *e; + for (e = p + n; e > p && *e != '\\'; e--); + *e = 0; // the file's folder + lstrcpyW(d, p); + for (k = 0; k < 3; k++) { + if (driver_layout_ok(d)) { lstrcpyW(p, d); break; } + for (e = d + lstrlenW(d); e > d && *e != '\\'; e--); + if (e == d) break; + *e = 0; + } + } + { WCHAR l[MAX_PATH + 60]; lstrcpyW(l, L"driver: the user chose "); lstrcatW(l, p); user_log(l); } + g_acq = &ACQ_DRIVER; lstrcpyW(g_drvzip, p); + old = SetCursor(LoadCursorW(NULL, (LPCWSTR)IDC_WAIT)); + rc = driver_check(g_drvzip, &g_drv_how, g_drv_hex, &g_drv_size, g_drv_rep, 1500); + g_drv_how |= DRV_SRC_PICKED; // chosen by the user: always checked; declining its warning does not touch the memory of declined auto-found copies + user_log(L"driver: a file the user chose is checked as it is (the skip of declined auto-found copies does not apply to it)"); + g_drv_bl = driver_bitlocker(); + SetCursor(old); + acq_driver_result(rc); } static void job_done(int job, int rc) @@ -979,25 +1150,20 @@ static void job_done(int job, int rc) WaitForSingleObject(g_thread, INFINITE); CloseHandle(g_thread); g_thread = NULL; g_busy = FALSE; if (g_closing) { DestroyWindow(g_hwnd); return; } switch (job) { - case JOB_DOWNLOAD: - if (rc == PK_OK) { open_package(); break; } - if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); break; } - if (rc == PK_CANCEL) lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); - else if (rc == PK_HTTP) wsprintfW(g_note, T(L"The Dyna Pro website answered with error %lu.", L"Dyna Pro 官網回應錯誤 %lu。"), g_neterr); - else if (rc == PK_CERT) wsprintfW(g_note, T(L"The security certificate of the Dyna Pro website could not be verified (error %lu), so nothing was downloaded. " - L"Check the date and time of this computer, or download the setup yourself from dynapro.co.uk and choose it with \"Other file...\".", - L"無法驗證 Dyna Pro 官網的安全憑證(錯誤 %lu),因此沒有下載任何東西。請確認電腦的日期與時間,或自行從 dynapro.co.uk 下載安裝檔後用「選擇其他檔案…」選取。"), g_neterr); - else if (rc == PK_URL) lstrcpyW(g_note, T(L"The download was refused: the address did not stay on https://dynapro.co.uk (it was redirected elsewhere or to an unencrypted connection). Nothing was downloaded.", - L"已拒絕下載:連線沒有停留在 https://dynapro.co.uk(被導向其他位置或未加密的連線),因此沒有下載任何東西。")); - else if (rc == PK_IO) wsprintfW(g_note, T(L"The file could not be saved (error %lu).", L"無法儲存檔案(錯誤 %lu)。"), g_neterr); - else wsprintfW(g_note, T(L"No connection to the Dyna Pro website (error %lu). Check the internet connection.", - L"連不上 Dyna Pro 官網(錯誤 %lu),請確認網路是否正常。"), g_neterr); - set_page(P_FAIL); - break; + case JOB_ACQUIRE: acq_done(rc); break; case JOB_EXTRACT: if (rc == PK_OK) { after_extract(); break; } drop_msi(); // a failed or cancelled extraction leaves no decrypted copy if (rc == PK_CANCEL) { DestroyWindow(g_hwnd); break; } + if (g_pkg_found && rc == PK_FORMAT) { // a copy found on this computer that is damaged (not a usable zip): not offered again; a password or disk problem keeps its own page + WCHAR nm[100]; + lstrcpynW(nm, file_name(g_pkg), 100); + pkg_reject(g_pkg); + g_acq = &ACQ_SETUP; g_pkg_found = FALSE; + wsprintfW(g_note, T(L"The file found on this computer (%s) is damaged, so it will not be used.", L"在這台電腦上找到的檔案(%s)已損毀,所以不使用它。"), nm); + set_page(P_FAIL); + break; + } if (rc == PK_PASSWORD) { lstrcpyW(g_note, T(L"Wrong password, or the file is damaged. Please check the password.", L"密碼不正確,或檔案已損毀。請再確認密碼。")); set_page(P_PASSWORD); break; } lstrcpyW(g_note, T(L"The setup could not be unpacked. Is the disk full?", L"無法解開安裝檔,硬碟空間是否不足?")); set_page(P_ERROR); @@ -1015,26 +1181,6 @@ static void job_done(int job, int rc) L"安裝沒有完成(代碼 %d)。\n\n請重新開機後再執行一次本程式。"), rc); set_page(P_ERROR); break; - case JOB_DRV_FETCH: - if ((rc == PK_OK || rc == DRV_W_HASH) && g_cancel == 1) { // "Cancel" while the file was being checked - if (!(g_drv_how & DRV_SRC_LOCAL) && rc == DRV_W_HASH) driver_forget(g_drvzip); - lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); set_page(P_DRV_INTRO); break; - } - if (rc == PK_OK) { - g_drv_allow = FALSE; - if (!g_bmp[0]) g_bmp[0] = load_png(18, &g_bw[0], &g_bh[0]); // the picture is decoded before the page changes, so the new page is built at once - set_page(P_DRV_PRESS7); break; - } - if (rc == DRV_W_HASH) { - g_drv_allow = FALSE; - { WCHAR m[2 * MAX_PATH + 200]; lstrcpyW(m, T(L"driver zip with another hash: the warning page is shown for ", L"driver zip with another hash: the warning page is shown for ")); lstrcatW(m, g_drvzip); user_log(m); } - set_page(P_DRV_HASH); break; - } - if (rc == PK_CANCEL && g_pause_req && g_cancel == 2) { fetch_paused(); break; } - if (rc == PK_CANCEL) { lstrcpyW(g_note, T(L"The download was stopped.", L"已停止下載。")); set_page(P_DRV_INTRO); break; } - if (rc == PK_NET || rc == PK_HTTP || rc == PK_IO) { drv_net_error(rc); break; } - drv_error(rc); - break; case JOB_DRV_PREP: if (rc == 0) { drv_reboot_now(); break; } if (rc == DRV_W_BDE) { set_page(P_DRV_BDE); break; } // armed, but BitLocker is still on: the user decides @@ -1066,11 +1212,12 @@ static void on_primary(void) case P_HELLO: g_pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; g_autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; + if (g_driver_only) { g_drv = TRUE; g_autostart_later = FALSE; set_page(P_DRV_INTRO); break; } // the fix is current: straight to the driver g_drv = g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; g_autostart_later = g_drv; // the driver restart comes first: the sign-in start is registered after it - if (g_installed) install(JOB_INSTALL_FIX); else fetch(); + if (g_installed) install(JOB_INSTALL_FIX); else { g_acq_fresh = FALSE; acq_start(&ACQ_SETUP); } break; - case P_FAIL: fetch(); break; + case P_FAIL: acq_start(g_acq); break; // Retry / Continue download case P_PASSWORD: if (!take_password()) { status(T(L"Wrong password. Please check it (upper/lower case matters).\nIf the setup file above is not the DynaRun setup, click \"Other file...\".", @@ -1091,7 +1238,7 @@ static void on_primary(void) DestroyWindow(g_hwnd); break; case P_ERROR: DestroyWindow(g_hwnd); break; - case P_DRV_INTRO: g_drvzip[0] = 0; g_drv_allow = FALSE; start_job(JOB_DRV_FETCH, P_DRV_FETCH); break; + case P_DRV_INTRO: acq_start(&ACQ_DRIVER); break; case P_DRV_PRESS7: start_job(JOB_DRV_PREP, P_DRV_PREP); break; case P_DRV_HASH: hash_choice(FALSE); break; // "Cancel" is the default button case P_DRV_RETRY: g_drvzip[0] = 0; start_job(JOB_DRV_PREP, P_DRV_PREP); break; // the staged driver is still there @@ -1101,8 +1248,8 @@ static void on_primary(void) if (can_start_dynarun()) start_dynarun(); // "Start DynaRun" (otherwise the button reads "Close") DestroyWindow(g_hwnd); break; - case P_FETCH: case P_DRV_FETCH: // "Pause" / "Resume" - if (g_paused) { g_paused = FALSE; start_job(g_page == P_FETCH ? JOB_DOWNLOAD : JOB_DRV_FETCH, g_page); } // the worker had ended; Range / If-Range continue the .part + case P_FETCH: // "Pause" / "Resume" + if (g_paused) { g_paused = FALSE; start_job(JOB_ACQUIRE, P_FETCH); } // the worker had ended; Range / If-Range continue the .part else if (g_busy && !g_pause_req) { g_pause_req = TRUE; InterlockedExchange(&g_cancel, 2); // 2 = pause (1 = cancel): the worker stops cleanly between two reads and keeps the .part and its ETag @@ -1127,16 +1274,31 @@ static void on_close(void) static void on_second(BOOL button) { if (g_page == P_DRV_HASH) hash_choice(button); // the button is "Install anyway"; Esc is a cancel - else if (g_page == P_FAIL && button) choose_file(); - else if ((g_page == P_FETCH || g_page == P_DRV_FETCH) && button) { + else if (g_page == P_FAIL && button && g_acq == &ACQ_DRIVER && can_skip_driver()) { g_drv = FALSE; set_page(P_DONE); } // "Skip": back to the end of the install + else if (g_page == P_FETCH && button) { if (g_paused) fetch_stopped(); // paused: no worker is running, finish at once else InterlockedExchange(&g_cancel, 1); // stop, then offer the other ways (also overrides a pause that is still taking effect) } - else if (g_page == P_DRV_INTRO && g_drvmode == DRV_MODE_NONE) { g_drv = FALSE; set_page(P_DONE); } // "Skip": back to the end of the install + else if (g_page == P_DRV_INTRO && button && can_skip_driver()) { g_drv = FALSE; set_page(P_DONE); } // "Skip": back to the end of the install else if (g_page == P_DRV_RETRY || g_page == P_DRV_WARN || g_page == P_DRV_BDE) start_job(JOB_DRV_CANCEL, P_DRV_PREP); // the task, the menu flag and BitLocker back else if (g_page != P_INSTALL) on_close(); } +// The third button (left of the second one): another way on. On the error page: choose the file (or folder) yourself. +static void on_third(void) +{ + if (g_page == P_FAIL) { if (g_acq == &ACQ_DRIVER) choose_driver_source(); else choose_file(); } + else if (g_page == P_HELLO && g_hint) { // "Download new version": the same acquire flow, then the normal install (the MSI upgrades DynaRun) + g_pin = SendMessageW(H(ID_PIN), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_autostart = SendMessageW(H(ID_AUTO), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_drv = !g_driver_only && g_drv_avail && SendMessageW(H(ID_DRV), BM_GETCHECK, 0, 0) == BST_CHECKED; + g_autostart_later = g_drv; + user_log(L"DynaRun update hint: the user chose to download the new version"); + g_acq_fresh = TRUE; // an older package found on this computer would not be the new version + acq_start(&ACQ_SETUP); + } +} + /* ---------- window ---------- */ #define FOOTER 76 // height of the grey button band, at 96 dpi @@ -1176,6 +1338,7 @@ static void create_controls(void) add(PROGRESS_CLASSW, 0, ID_PROG); add(L"STATIC", SS_NOPREFIX, ID_STATUS); add(L"BUTTON", WS_TABSTOP, ID_EXTRA); + add(L"BUTTON", WS_TABSTOP, ID_THIRD); add(L"BUTTON", WS_TABSTOP, ID_SECOND); add(L"BUTTON", WS_TABSTOP | BS_DEFPUSHBUTTON, ID_PRIMARY); } @@ -1262,7 +1425,7 @@ static void fit_window(int need) // Lays the visible controls out top-down from the measured text heights (the window grows when a page needs more room); buttons right-aligned in the footer. static void layout(void) { - static const int btn[] = { ID_PRIMARY, ID_SECOND }; + static const int btn[] = { ID_PRIMARY, ID_SECOND, ID_THIRD }; RECT c; int m = S(32), w, x, i, bw, bh = S(36), by, bottom; GetClientRect(g_hwnd, &c); w = c.right - 2 * m; @@ -1271,7 +1434,7 @@ static void layout(void) bottom = c.bottom - S(FOOTER); stack(TRUE, w, bottom); by = bottom + (S(FOOTER) - bh) / 2; - for (x = c.right - m, i = 0; i < 2; i++) { + for (x = c.right - m, i = 0; i < 3; i++) { if (!shown(btn[i])) continue; bw = measure(btn[i], 0) + S(48); if (bw < S(112)) bw = S(112); x -= bw; place(btn[i], x, by, bw, bh); x -= S(10); @@ -1358,13 +1521,14 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) switch (LOWORD(w)) { case IDOK: case ID_PRIMARY: if (IsWindowVisible(H(ID_PRIMARY)) && IsWindowEnabled(H(ID_PRIMARY))) on_primary(); break; case IDCANCEL: case ID_SECOND: on_second(LOWORD(w) == ID_SECOND); break; + case ID_THIRD: on_third(); break; case ID_EXTRA: - if (g_page == P_FAIL || g_page == P_FETCH || g_page == P_DRV_FETCH || g_page == P_CHECK || g_page == P_LICENSE) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); + if (g_page == P_FAIL || g_page == P_FETCH || g_page == P_CHECK || g_page == P_LICENSE) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); else if (g_page == P_DRV_BDE) ShellExecuteW(h, NULL, L"https://aka.ms/myrecoverykey", NULL, NULL, SW_SHOWNORMAL); else if (g_page == P_PASSWORD) choose_file(); - else if (g_page == P_HELLO) choose_exe(); + else if (g_page == P_HELLO) { if (g_driver_only) { g_driver_only = FALSE; set_page(P_HELLO); } else choose_exe(); } // "Reinstall the fix..." / "Installed elsewhere..." else if (g_page == P_DRV_ERROR) ShellExecuteW(h, NULL, DRIVER_DOC_URL, NULL, NULL, SW_SHOWNORMAL); - else if (g_page == P_DRV_INTRO || g_page == P_DRV_HASH) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); // after a failed driver download + else if (g_page == P_DRV_HASH) ShellExecuteW(h, NULL, DYNAPRO_PAGE, NULL, NULL, SW_SHOWNORMAL); // after a failed driver download break; case ID_CHECK: EnableWindow(H(ID_PRIMARY), SendMessageW(H(ID_CHECK), BM_GETCHECK, 0, 0) == BST_CHECKED); break; } @@ -1373,10 +1537,10 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) static WCHAR b[100]; DWORD done = (DWORD)w, total = (DWORD)l; if (done == (DWORD)-1) { // download over, the file is being checked: not pausable, bar and text say so - if (g_page == P_DRV_FETCH && !g_paused) { + if (g_page == P_FETCH && !g_paused) { g_marquee = TRUE; progress(TRUE); - if (total == 2) status(T(L"Looking for the driver on this computer...", L"正在尋找電腦上現有的驅動程式…"), FALSE); // before anything is downloaded: a bar that moves + if (total == 2) { WCHAR s[160]; wsprintfW(s, T(L"Looking for %s on this computer...", L"正在尋找電腦上現有的%s…"), ACQ_NAME(g_acq)); status(s, FALSE); } // before anything is downloaded: a bar that moves else { // the file is being checked: not pausable g_checking = TRUE; text(ID_TITLE, T(L"Checking the driver", L"正在檢查驅動程式")); @@ -1389,11 +1553,11 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) } if (g_checking) return 0; if (total && g_marquee) { g_marquee = FALSE; progress(FALSE); } // the size became known - else if (!total && !g_marquee && (g_page == P_FETCH || g_page == P_DRV_FETCH)) { g_marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received + else if (!total && !g_marquee && g_page == P_FETCH) { g_marquee = TRUE; progress(TRUE); } // size unknown: a moving bar and the MB received if (!g_marquee) SendMessageW(H(ID_PROG), PBM_SETPOS, total ? MulDiv(done, 1000, total) : 0, 0); g_dlhave = done; if (total) g_dltotal = total; - if (g_page == P_FETCH || g_page == P_DRV_FETCH) { - if (g_page == P_FETCH && total != g_total) { fetch_body(total); layout(); } + if (g_page == P_FETCH) { + if (total != g_total) { fetch_body(total); layout(); } wsprintfW(b, total ? L"%lu.%lu / %lu.%lu MB" : L"%lu.%lu MB", done / 1048576, done % 1048576 / 104858, total / 1048576, total % 1048576 / 104858); status(b, FALSE); @@ -1415,6 +1579,10 @@ static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) } if (w == ID_KEEPUP) keep_up(h); return 0; + case WM_HINT: + g_hint = (int)w; + if (g_hint && g_page == P_HELLO && !g_busy) set_page(P_HELLO); + return 0; case WM_JOBDONE: KillTimer(h, ID_KEEPUP); // after the elevated part (which allows it) the wizard comes back in front of other windows @@ -1474,8 +1642,13 @@ int wizard(void) create_controls(); g_installed = locate_dynarun(); g_drv_avail = g_drvmode == DRV_MODE_NONE && driver_supported() && !driver_installed(); + g_driver_only = g_installed && g_drv_avail && fix_current(); // the fix is there and current: only the driver is missing if (g_drvmode != DRV_MODE_NONE) drv_start(); - else set_page(P_HELLO); + else { + DWORD tid; + set_page(P_HELLO); + if (g_installed) CloseHandle(CreateThread(NULL, 0, hint_proc, NULL, 0, &tid)); // is there a newer DynaRun? (a hint; the page does not wait) + } ShowWindow(g_hwnd, SW_SHOWNORMAL); SetForegroundWindow(g_hwnd); while (GetMessageW(&msg, NULL, 0, 0) > 0) { diff --git a/tests/README.md b/tests/README.md index ab0b2c4..f59daeb 100644 --- a/tests/README.md +++ b/tests/README.md @@ -40,6 +40,6 @@ copies COM registrations, that code is only kept under test. Exit code 0 = all p # Driver tests -`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream). It creates only `%TEMP%\drf-drv-` and `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. +`tests\build-driver-tests.cmd` (after `build.cmd`) builds and runs `test_driver.c`, which compiles `src\setup.c` and `src\driver.c` into itself and links `build\package.obj`: the INF matcher (ANSI, UTF-16, other versions and the stock FTDI INF), the code integrity classifier, the scheduled task XML (escaping, size limits, well-formed UTF-16), the tool paths (`Sysnative`), the `pnputil` exit codes, the zip entry name rules and the multi-entry zip extraction (zips made in the test: zip-slip, absolute and drive paths, device names, encrypted/unsupported/damaged entries, a real deflate stream), the shared package machinery of `package.c` (`pkg_*`: table-driven staging from a zip or an unpacked folder, size caps, links refused, target preparation), the DynaRun update hint (version comparison and the parsing of Dyna Pro's software page, on made-up pages; no network), and `pkg_find_dir_in`, `driver_layout_ok` and `driver_check` on synthetic folders and zips (made-up files only: a package whose files differ from the pinned ones, files missing, a bus INF without the device ID, not a zip, no such path). It creates only `%TEMP%\drf-drv-`, `%TEMP%\DynaRunFix\drvcheck` (the scratch folder of the driver check; `test_files()` uses and deletes it) and `%TEMP%\DynaRunFix\drvdigest.tmp` (`driver_check` on a folder) and starts no system tool except `powershell` to parse the task XML. `test_job_table()` reads `src\wizard.c` relative to the current folder (the build script changes to the repo root) and, if it is not run from the repo root, only prints an info line and skips that check. Set `DRF_DRIVER_ZIP` to the real `Dyna Pro USB.zip` to check the pinned SHA-256 and the extraction against it. Exit code 0 = all passed. The CI workflow (`.github/workflows/build.yml`) runs `build-tests.cmd`, `build-shexec-tests.cmd`, `build-hardening-tests.cmd` and `build-driver-tests.cmd` in the `build` job (read-only token); the release is made by a separate job. The scripts use a release Visual Studio; set `DRF_ALLOW_PRERELEASE=1` locally to allow a pre-release one. diff --git a/tests/test_driver.c b/tests/test_driver.c index 8616b69..939a527 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -2,7 +2,8 @@ // not part of the shipped binaries (see tests\build-driver-tests.cmd). setup.c and driver.c are compiled into this program, so the shipped // code is what runs. Nothing outside %TEMP%\drf-drv- (deleted at the end) is touched: no system tool is started, no HKLM write. // -// (The pinned-file tests also use %TEMP%\DynaRunFix\drvcheck, the scratch folder of the zip scan; it is deleted again.) +// (The pinned-file tests also use %TEMP%\DynaRunFix\drvcheck, the scratch folder of the zip scan; it is deleted again. driver_check on a folder +// writes and deletes %TEMP%\DynaRunFix\drvdigest.tmp.) // // Optional: set DRF_DRIVER_ZIP to the real "Dyna Pro USB.zip" to check the pinned SHA-256, the extraction and the INF matcher against it. #define _CRT_SECURE_NO_WARNINGS @@ -656,6 +657,101 @@ static void test_files(void) delete_tree(dir, NULL, 0); } +/* ---------- the folder walk, the layout test and driver_check (synthetic packages only) ---------- */ + +static BOOL has_marker(const WCHAR *dir) { WCHAR p[MAX_PATH]; wsprintfW(p, L"%s\\marker.txt", dir); return exists(p); } + +static void touch_file(const WCHAR *path) +{ + HANDLE h = CreateFileW(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); DWORD w; + if (h != INVALID_HANDLE_VALUE) { WriteFile(h, "x", 1, &w, NULL); CloseHandle(h); } +} + +static void test_check(void) +{ + WCHAR root[MAX_PATH], a[MAX_PATH], b[MAX_PATH], c[MAX_PATH], o[MAX_PATH], p[MAX_PATH], q[MAX_PATH], hex[65], rep[1500], zip[MAX_PATH]; + int how = -1, rc, miss = -1; ULONGLONG size = 1; volatile LONG nocancel = 0; HANDLE h; + // pkg_find_dir_in: accept() for the root, then for the folders below it, down to `depth` levels + wsprintfW(root, L"%s\\walk", g_base); CreateDirectoryW(root, NULL); + wsprintfW(a, L"%s\\a", root); CreateDirectoryW(a, NULL); + wsprintfW(b, L"%s\\b", a); CreateDirectoryW(b, NULL); + wsprintfW(c, L"%s\\c", b); CreateDirectoryW(c, NULL); + wsprintfW(p, L"%s\\marker.txt", c); touch_file(p); + CHECK(!pkg_find_dir_in(root, 0, has_marker, o) && !o[0], L"walk: depth 0 looks at the root only"); + CHECK(!pkg_find_dir_in(root, 2, has_marker, o) && !o[0], L"walk: a folder three levels down is out of reach for depth 2"); + CHECK(pkg_find_dir_in(root, 3, has_marker, o) && !lstrcmpiW(o, c), L"walk: depth 3 finds it and returns its path"); + wsprintfW(p, L"%s\\marker.txt", b); touch_file(p); + CHECK(pkg_find_dir_in(root, 3, has_marker, o) && !lstrcmpiW(o, b), L"walk: the nearest approved folder wins"); + wsprintfW(p, L"%s\\marker.txt", root); touch_file(p); + CHECK(pkg_find_dir_in(root, 0, has_marker, o) && !lstrcmpiW(o, root), L"walk: the root itself can be approved"); + wsprintfW(p, L"%s\\nothing", root); + CHECK(!pkg_find_dir_in(p, 2, has_marker, o) && !o[0], L"walk: a folder that does not exist finds nothing"); + { // a name that leaves no room for the walk is refused, not overrun + WCHAR longp[MAX_PATH + 40]; int i; + lstrcpyW(longp, root); for (i = lstrlenW(longp); i < MAX_PATH; i++) longp[i] = 'x'; longp[MAX_PATH] = 0; + CHECK(!pkg_find_dir_in(longp, 2, has_marker, o), L"walk: a path that is too long is refused"); + } + + // driver_layout_ok: ftdibus.inf (a file) with i386 and amd64 folders beside it + wsprintfW(root, L"%s\\lay", g_base); CreateDirectoryW(root, NULL); + CHECK(!driver_layout_ok(root), L"layout: an empty folder is not a driver folder"); + wsprintfW(p, L"%s\\ftdibus.inf", root); touch_file(p); + CHECK(!driver_layout_ok(root), L"layout: ftdibus.inf alone is not enough"); + wsprintfW(p, L"%s\\i386", root); CreateDirectoryW(p, NULL); + CHECK(!driver_layout_ok(root), L"layout: without amd64 it is not a driver folder"); + wsprintfW(p, L"%s\\amd64", root); CreateDirectoryW(p, NULL); + CHECK(driver_layout_ok(root), L"layout: ftdibus.inf, i386 and amd64: a driver folder"); + wsprintfW(p, L"%s\\nothing", g_base); CHECK(!driver_layout_ok(p), L"layout: a folder that does not exist"); + wsprintfW(p, L"%s\\ftdibus.inf", root); CHECK(!driver_layout_ok(p), L"layout: a file is not a folder"); + wsprintfW(root, L"%s\\lay2", g_base); CreateDirectoryW(root, NULL); + wsprintfW(p, L"%s\\ftdibus.inf", root); CreateDirectoryW(p, NULL); // a folder with that name, not a file + wsprintfW(p, L"%s\\i386", root); CreateDirectoryW(p, NULL); wsprintfW(p, L"%s\\amd64", root); CreateDirectoryW(p, NULL); + CHECK(!driver_layout_ok(root), L"layout: ftdibus.inf must be a file"); + wsprintfW(root, L"%s\\lay3", g_base); CreateDirectoryW(root, NULL); + wsprintfW(p, L"%s\\ftdibus.inf", root); touch_file(p); + wsprintfW(p, L"%s\\i386", root); touch_file(p); // a file called i386, not a folder + wsprintfW(p, L"%s\\amd64", root); CreateDirectoryW(p, NULL); + CHECK(!driver_layout_ok(root), L"layout: i386 must be a folder"); + + // driver_check on a folder: the synthetic 18-file package (the INF fixture + short texts: none of them is a pinned file) + wsprintfW(zip, L"%s\\drv_all.zip", g_base); + wsprintfW(root, L"%s\\chk", g_base); CreateDirectoryW(root, NULL); + CHECK(stage_files(zip, root, &nocancel, &miss) == PK_OK && miss == 0, L"check: the fixture folder is staged"); + wsprintfW(a, L"%s\\Dyna Pro USB", root); + lstrcpyW(p, a); how = -1; hex[0] = 0; size = 1; rep[0] = 0; + rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_W_HASH, L"check: a folder with all 18 files that differ, the bus INF names the device id: the user is asked"); + CHECK(how == (DRV_SRC_LOCAL | DRV_SRC_DIR) && size == 0 && lstrlenW(hex) == 64 && !lstrcmpiW(p, a), L"check: a folder is a local source, size 0, a 64-digit digest, the path stays the folder"); + CHECK(rep[0] != 0, L"check: the report names the files that differ"); + { WCHAR hex2[65]; int how2; ULONGLONG s2; lstrcpyW(q, a); rc = driver_check(q, &how2, hex2, &s2, NULL, 0); + CHECK(rc == DRV_W_HASH && !lstrcmpiW(hex, hex2), L"check: the digest of the same folder is the same (and rep may be NULL)"); } + lstrcpyW(p, root); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_W_HASH && !lstrcmpiW(p, a), L"check: the parent folder is enough: the driver folder below it is found and the path becomes that folder"); + wsprintfW(q, L"%s\\amd64\\ftser2k.sys", a); DeleteFileW(q); + lstrcpyW(p, a); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH && !hex[0], L"check: a folder with a file missing is not a usable package"); + wsprintfW(root, L"%s\\chk2", g_base); CreateDirectoryW(root, NULL); + CHECK(stage_files(zip, root, &nocancel, &miss) == PK_OK, L"check: a second fixture folder"); + wsprintfW(a, L"%s\\Dyna Pro USB\\ftdibus.inf", root); + h = CreateFileW(a, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); if (h != INVALID_HANDLE_VALUE) { DWORD w; WriteFile(h, STOCK_INF, (DWORD)strlen(STOCK_INF), &w, NULL); CloseHandle(h); } + wsprintfW(p, L"%s\\Dyna Pro USB", root); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH, L"check: all files there but the bus INF does not name the Dyna Pro device id: not offered"); + wsprintfW(p, L"%s\\empty", g_base); CreateDirectoryW(p, NULL); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH && how == (DRV_SRC_LOCAL | DRV_SRC_DIR), L"check: a folder without the driver layout"); + wsprintfW(p, L"%s\\nothing", g_base); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH && how == DRV_SRC_LOCAL && !hex[0], L"check: a path that does not exist"); + + // driver_check on a zip + wsprintfW(p, L"%s\\drv_all.zip", g_base); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_W_HASH && how == DRV_SRC_LOCAL && size > 0 && lstrlenW(hex) == 64 && rep[0], L"check: a zip with all 18 files that differ: the user is asked (zip hash and size given)"); + wsprintfW(p, L"%s\\drv_part.zip", g_base); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH, L"check: a zip with files missing is not a usable package"); + wsprintfW(p, L"%s\\drv_enc.zip", g_base); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH, L"check: a zip whose entries are encrypted is not a usable package"); + wsprintfW(p, L"%s\\drv_bad.zip", g_base); rc = driver_check(p, &how, hex, &size, rep, 1500); + CHECK(rc == DRV_E_HASH, L"check: a file that is not a zip"); +} + /* ---------- a regression guard on the wizard's job table ---------- */ // Once a comment after the call in `case JOB_DRV_PREP: rc = driver_prep(...); // ... break;` swallowed the break, and the install ran right after @@ -678,10 +774,79 @@ static void test_job_table(void) labels++; if (!(next && next < end)) break; } - CHECK(labels >= 8, L"all eight jobs were looked at"); + CHECK(labels >= 7, L"all seven jobs were looked at"); release(f); } +/* ---------- the shared package machinery, the DynaRun update hint ---------- */ + +static void test_shared(void) +{ + WCHAR zip[MAX_PATH], dir[MAX_PATH], p[MAX_PATH]; ULONGLONG sz; volatile LONG nocancel = 0; int miss = -1; + static pkg_file tab[2]; static WCHAR h0[65]; + zent e[] = { + { "Pack/", NULL, 0, 0, NULL, 0, 0, 0 }, + { "Pack/sub/deeper/a.txt", TEXT66, 66, 0, NULL, 0, 0, 0 }, + { "Other/b.txt", TEXT66, 66, 0, NULL, 0, 0, 0 }, + { "Other/ignored.txt", TEXT66, 66, 0, NULL, 0, 0, 0 }, + }; + // a table of its own, with a nested path: the generic helpers are not the driver's alone + tab[0].rel = L"sub/deeper/a.txt"; tab[0].size = 66; tab[0].sha = L""; + tab[1].rel = L"b.txt"; tab[1].size = 66; tab[1].sha = L""; + CHECK(pkg_entry_index(tab, 2, "Pack/sub/deeper/a.txt", 21) == 0 && pkg_entry_index(tab, 2, "X/B.TXT", 7) == 1 && pkg_entry_index(tab, 2, "Pack/sub/a.txt", 14) < 0, L"shared: names after a folder boundary, any case"); + wsprintfW(zip, L"%s\\sh.zip", g_base); build_zip(zip, e, 4); + CHECK(pkg_zip_has(zip, tab, 2, 1 << 20) == 2 && pkg_zip_has(zip, tab, 2, 10) == 0, L"shared: the central directory says which table files the zip has, and the size cap counts"); + wsprintfW(dir, L"%s\\sh1", g_base); CreateDirectoryW(dir, NULL); + CHECK(pkg_stage_files(zip, dir, L"Top", tab, 2, 1 << 20, 1 << 20, &nocancel, &miss) == PK_OK && miss == 0, L"shared: staged into dir\\Top with the nested folders made"); + wsprintfW(p, L"%s\\Top\\sub\\deeper\\a.txt", dir); CHECK(exists(p), L"shared: sub\\deeper\\a.txt is there"); + wsprintfW(p, L"%s\\Top\\ignored.txt", dir); CHECK(!exists(p), L"shared: a file that is not in the table is not extracted"); + wsprintfW(p, L"%s\\Top\\sub\\deeper\\a.txt", dir); pkg_sha256(p, h0, &sz); + tab[0].sha = h0; tab[1].sha = h0; // both files hold the same text + { static pkg_check c; pkg_check_files(dir, L"\\Top\\", tab, 2, NULL, &c); CHECK(c.ok == 2 && c.differ == 0 && c.missing == 0, L"shared: the staged files match their table"); } + CHECK(pkg_dirs_plain(dir, L"Top", tab, 2), L"shared: the staging folders are plain ones"); + CHECK(pkg_stage_files(zip, dir, L"Top", tab, 2, 1 << 20, 1 << 20, &nocancel, &miss) == PK_IO, L"shared: never into a staging folder that already exists"); + wsprintfW(dir, L"%s\\sh2", g_base); CreateDirectoryW(dir, NULL); + CHECK(pkg_stage_files(zip, dir, L"Top", tab, 2, 1 << 20, 100, &nocancel, &miss) == PK_FORMAT, L"shared: the total size cap (a zip bomb) stops it"); + // from an unpacked folder + wsprintfW(p, L"%s\\sh1\\Top", g_base); + wsprintfW(dir, L"%s\\sh3", g_base); CreateDirectoryW(dir, NULL); + CHECK(pkg_stage_from_dir(p, dir, L"Top", tab, 2, 1 << 20, 1 << 20, &miss) == PK_OK && miss == 0, L"shared: copied from an unpacked folder"); + { static pkg_check c; pkg_check_files(dir, L"\\Top\\", tab, 2, NULL, &c); CHECK(c.ok == 2, L"shared: the copies are what is checked"); } + // a target that is going to be written + wsprintfW(p, L"%s\\old.msi", g_base); + { HANDLE h = CreateFileW(p, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL); DWORD w; WriteFile(h, "x", 1, &w, NULL); CloseHandle(h); } + CHECK(pkg_prepare_target(p) && !exists(p), L"shared: a leftover file is removed before it is written again"); + CHECK(pkg_prepare_target(p), L"shared: a path that is free is fine"); + wsprintfW(p, L"%s\\sh1", g_base); CHECK(!pkg_prepare_target(p) && exists(p), L"shared: a folder is refused and left alone"); + { zipent big; zero(&big, sizeof(big)); big.usize = 500; wsprintfW(p, L"%s\\x.msi", g_base); + CHECK(pkg_extract_checked(&big, "", p, 100, NULL, &nocancel) == PK_FORMAT, L"shared: an entry that claims to be bigger than the cap is refused"); } +} + +static void test_hint(void) +{ + WCHAR v[40]; + static const char PAGE[] = + "Latest Dyna Pro Software Release." + "

The latest release of ‘Dyna Run V3’ control software Version 3.26.0 has now been released.

" + "" + "" + "" + "
  Dyna Run V3 Software Manual  01/01/2011  9.88MB
  Dyna Run V3 Software
(Please note that you will need a password from Dyna Pro Dynamometers to open this file)
  3.26.0 (Released: 9/05/2026)   82.1MB 
  Dyna Pro USB Drivers 02/05/2018  1.2MB
"; + // comparing versions + 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 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.

Dyna Pro USB Drivers 1.2MB"; + CHECK(pkg_page_version(P2, (DWORD)strlen(P2), v, 40) && !lstrcmpW(v, L"3.27.1"), L"page: without the table, the news text 'Version x.y.z' is used"); } + { static const char P3[] = "Dyna Run V3 Software Manual 01/01/2011 9.88MBDyna Pro USB Drivers 02/05/2018 1.2MB"; + CHECK(!pkg_page_version(P3, (DWORD)strlen(P3), v, 40) && !v[0], L"page: no version named: no hint"); } + CHECK(!pkg_page_version("", 0, v, 40) && !pkg_page_version("