Skip to content

Release v1.7.2: SOLID refactor, fixes, docs - #16

Merged
timliudev merged 46 commits into
developfrom
release/v1.7.2
Oct 17, 2026
Merged

timliudev merged 46 commits into
developfrom
release/v1.7.2

Conversation

@timliudev

Copy link
Copy Markdown
Owner

Internal refactor with no intended behaviour change (setup/wizard/driver/package split into modules, named result codes, install options passed explicitly, verb/page/job tables, launcher and update share one log helper, dynafix split into include files), plus fixes:

  • "Close DynaRun and install" consent covers one install job only.
  • Driver preparation failure cleans up staged files.
  • Long %APPDATA% paths no longer overflow in the shortcut and taskbar code (the folder is skipped).
  • Download progress is written by the worker only.
  • dynafix: race between the loader notification and the PEGRC check.
  • The wizard ignores a stray job-done window message.

Docs aligned (CHANGELOG v1.7.2, manual version entries, refactor record, tests/README, VM checklist). Unit tests and the VM regression checklist passed on the merged refactor; the follow-up fixes are covered by unit tests in CI, the installer smoke test on the final build was not run.

🤖 Generated with Claude Code

timliudev and others added 30 commits October 17, 2026 18:25
…ed files (P6, no logic change)

Pure move: the data tables, the hint UI and find_holder, read_options /
enable_options, work_area / patch_vb_screen_walk and the PEGRC redirect
(plus clear_runasinvoker) now live in src/dynafix_*.inc.c, #included from
dynafix.c at the place the code used to be. Still one compilation unit
(tests/logtest.c includes dynafix.c); build.cmd is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter (applog.h)

Header-only src/applog.h (static functions, build.cmd unchanged): make_sa and its
SDDL, the dynafix.log mutex and append, now() and DAY, the HKCU Update* value
names and the launcher <-> setup command-line verbs. launcher.c and update.c use
it. Mutex name, line format, trim threshold and line length limits are unchanged;
only the launcher trims, as before. dynafix.dll keeps its own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One flow for both ways a noted release is offered: BEFORE_DYNARUN (/update:
Yes/No, DynaRun not running, launcher started afterwards) and BESIDE_DYNARUN
(/offer: Yes/No/Cancel beside the running DynaRun, install once it is closed).
Texts, buttons, log lines, registry writes and their order are unchanged;
update_run and update_offer keep their signatures. The repeated RegDeleteValueW
blocks become clear_note(k, skip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt_le, start_plain and hook_and_finish

Code moved as it was: "/wasadmin" is still taken before the elevation check,
log_start still runs before "/noupdate", "/autostart" and "/restart" are parsed,
and the start order (attach / notice / dll load / LE or plain / hook) is the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d result codes

Locks today's behaviour: the switch parsing of setup.c and driver_verb, the argument string
run_elevated builds (ShellExecuteEx stubbed), FIX_DONE for 0..40 and the PK_/DRV_/RC_ values.
The two parsing loops are moved unchanged into static helpers (parse_switches, prep_accept_arg)
so they can be called; no behaviour change. test_hardening.c gets the missing driver_prep_stage stub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the tests

Neither function has a caller in the shipped exe: the installer no longer
copies COM registrations (promote_com_in, with reg_export, skipped and
g_export_root, only used by it), and nothing unpacks a whole zip
(zip_extract_all, xall_visit, make_dirs, ZIP_MAX_*). They now live in
tests\promote_com.h (included by test_hardening.c) and
tests\zip_extract_all.h (included by test_driver.c), so the tests that use
them keep compiling and the code does not rot. No change to the shipped
behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…b only

The button on the "DynaRun is running" page set g_close to TRUE and nothing
ever reset it. run_elevated forwards g_close as /close to every later elevated
stage, and the stage then ends a running DynaRun with TerminateProcess without
asking. So after one consent, a retry (UAC cancelled, install failed) that
met a DynaRun started during the UAC prompt killed it instead of returning
RC_RUNNING and showing the page again; running_early also skipped its check.

The button now records that it set the flag (g_close_asked) and job_done takes
it back when that job has ended. A /close given on the command line is not
touched. The text check of wizard.c in test_driver.c covers both places.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the installer stopped copying COM registrations, the whole check
machinery that promote_com_in used stayed behind in src/setup.c with no
shipped caller: add_key, free_keys, has_key, g_keys/g_ids/g_kinds/g_nkeys,
com_roots, g_com_nfolder, under, com_path_ok, value_state, has_subkey,
clsid_ok, progid_ok, typelib_ok, typelib_complete, copy_tree and
delete_key_tree. They are only compiled into the shipped installer, never
called. Move them to tests/promote_com.h, which test_hardening.c already
includes after setup.c, so the same checks are still exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…en held

peg_ldr_note patched through patch_base without the busy flag peg_check
uses, so on two threads both could modify a hook_t's raw/chain at once.
The flag is now file-scope and shared; the callback only try-acquires it
(it runs under the loader lock and must not wait). When it is taken, the
callback sets a pending flag and logs nothing; peg_check then forgets its
per-module bookkeeping and patches the loaded modules again (idempotent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New src/errors.h (included by setup.h) holds every result code: RC_* for the
install/uninstall result and process exit code (RC_OK, RC_FAIL, RC_CANCEL,
RC_OVER_SHOULDER, RC_MSI_FAILED, RC_NO_DYNARUN, RC_RUNNING), PK_*, PKC_* (file
check status), DRV_*, and small enums for the pin result, the update hint,
the BitLocker answer and remove_dynarun's "not installed". Bare 2/3/4/5 in
setup.c, wizard.c, package.c and driver.c now use the name of their own
meaning (cancel 2 vs PK_NET 2, RC_RUNNING 6 vs PK_PASSWORD 6 stay separate).
FIX_DONE moves to errors.h with the done codes listed; same truth value for
every int. The compiled code is identical (checked by disassembly).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setup.c now defines (declared in setup.h) path_join (dir + name with the
old `lstrlen + 2 >= limit` test), run_process (CreateProcess + wait with
timeout), dir_walk (one folder level with a callback; skips '.' names and
too long paths) and app_key_open / app_key_create (HKCU\Software\DynaRunFix,
HKLM\SOFTWARE\DynaRunFix).

Used by package.c (scan, find_zip_in, find_dir_in, hydrate_tree, the
msiexec calls, the registry notes), driver.c (run_tool, the BitLocker flag,
two joins) and setup.c (delete_tree, the lnk name check, the install keys).
The four shortcut walks in setup.c and the differing process starts
(run_tool_to_file, touch, run_elevated) are left as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…globals

g_quiet, g_close, g_pin, g_autostart, g_autostart_later and g_keep become the
fields of install_opts. The command line fills one in main, the wizard owns one
(and hands each job a copy, taken in start_job) and run_elevated builds its
forwarded switches from the struct it is given: no hidden dependency on globals.
Same switches in the same order (<args> /quiet /close /pin /keep). The wizard's
"close DynaRun" consent (g_close_asked) still covers one install job only.
g_pinresult was a result, not an option: it is install_result.pin, written by the
user stage and read by the wizard once the job is done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setup.c's if/else chain on argv[1] and driver.c's driver_verb become one table
{name, min_args, handler} (VERBS); driver.c supplies the handlers of its five
verbs. The command line is parsed once (cmd_parse) into a cmdline struct.
Verbs, argument positions, the argc needed (/machine, /full: 4) and exit codes
are unchanged.

/unverified keeps its two rules, now computed by the same loop: daccept (any
position from argv[4], the last valid one: /machine, /full) and daccept4 (only
argv[4] argv[5]: /driver-prep). The setup builds /unverified at argv[4]
(/driver-prep) or argv[6] (/machine, /full) where both agree; they differ only
for hand-typed lines (a valid /unverified after another switch, or two of them),
which the tests lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shell folders that can hold a DynaRun shortcut were listed in six places
(icon search, fix, point-back, search for our own shortcut, restore path
check, other accounts' point-back). They are now one table (SF[]) with a
flag per use and a deep flag per use, read through shortcut_folders(); each
user keeps exactly the folders and recursion it had. The four walks
(find_icon, scan, find_ours, unretarget) use dir_walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…verflowing

The unified sf_path() now always checks the length of the %APPDATA% path
before the sub folder is appended. Until now only the uninstall's point-back
(unretarget_shell) and the restore path check (lnk_path_ok) did; fix_shortcuts,
desktop_link, pin_taskbar, pinned_ours and the creation of the DynaRunFix
shortcut appended "\Microsoft\Internet Explorer\Quick Launch" or
"\DynaRunFix" to a d[MAX_PATH] buffer without looking, so an %APPDATA% path
of about 210 characters or more overflowed it.

Before: those callers wrote past the end of the buffer for such a profile.
After: the folder is skipped (fix_shortcuts does not scan the Quick Launch
folder, desktop_link/pin_taskbar report that there is none to use). A shorter
path behaves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st and the MSI API

g_user_sid and g_exe_trusted are now a trust_ctx that each stage makes and
passes to user_writable, check_exe and g_is_writable (path_user_writable
makes its own from the sid it is given). The MSI function pointers are
looked up by one idempotent msi_api_init(), called by classify before its
check of advertised shortcuts, so classify no longer depends on
fix_shortcuts having run first. The tests pass a trust_ctx of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nit)

src\setup.c keeps the header comment, the system includes and the entry point;
the rest moved unchanged, in the original order, into src\setup\*.h (defs, util,
stage, trust, exetime, procs, shortcuts, taskbar, users, machine, elevate,
msi_stage, userstage, uninstall, cmdline). They are fragments of the same
translation unit: build.cmd and the tests still compile and include only
setup.c. Object code of setup.c is identical before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zip_open and zip_walk each had their own copy of the end-of-central-directory
scan, the central directory read and the entry field decoding. They now share
zipcd_load (EOCD scan + directory read) and zipent_fill (entry fields + local
header); the per-reader rules stay where they were: zip_walk still refuses
Zip64 markers and any damaged entry, zip_open still takes the first .msi and
ignores the entry count. crc_init is one idempotent entry point (done flag set
after the table is complete) called by zipcd_load, zip_extract and
zip_password_plausible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_tool_ok takes the tolerated exit code and its meaning as arguments
(run_tool is the plain form), replacing g_tool_ok_code / g_tool_ok_text.
The zip and folder searches take a pkg_search (accept callback with a context,
and the log function), replacing g_pkg_log. The first folder with all 18 files
of which some differ is now the folder search's own result (known_ctx.other,
replacing g_other_dir); that needed one callback context for both candidate
kinds, so zip_known_cb and dir_known_cb became one known_cb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every return after the argument checks is now 'set res, goto out'; the exit
does the cleanup the former return sites did inline (remove base\driver,
cancel_locked, bde_resume), so the leftovers are exactly the same as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fails

prep_locked, first attempt (a zip or folder is given, the staging folder was
just made by make_base), now removes <ProgramData>\DynaRunFix\driver\ and
accepted.txt on these failures, which used to leave them behind:
  - staged_ok fails after staging (DRV_E_INF)
  - copying the setup exe into the folder fails (DRV_E_COPY)
  - bcdedit fails (DRV_E_BCD; accepted.txt was not removed there either)
  - the task XML cannot be written or schtasks fails (DRV_E_TASK; cancel_locked
    already removed accepted.txt, the staged tree stayed)
Nothing is armed at those points and every later attempt stages again
(make_base wipes the folder), so nothing can use the leftovers; the other
staging failures (extract, hash, not accepted) already dropped the tree.
Unchanged on purpose: a retry (zip "-", the staged files from the first
attempt, task armed) keeps its staged files on DRV_E_INF and DRV_E_COPY;
success and DRV_W_BDE keep everything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
drf_app.h (language, paths, options, command line, basic helpers, install
entry points), drf_util.h (path_join, run_process, dir_walk, app key, the
elevated-stage helpers), drf_package.h, drf_driver.h, drf_update.h,
drf_wizard.h. Declarations used only by the tests moved to
drf_package_internal.h and drf_driver_internal.h. The drf_ prefix keeps the
names apart from src\setup\util.h. setup.h includes the public headers so
update.c is unchanged. Declarations only moved; the generated code is
identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JOBS[] holds, per job, the worker proc, the on_done handler and the two flags that
used to be enum-order comparisons (keep the wizard behind the UAC/msiexec windows,
bring it back to the front when done). job_proc and job_done no longer switch.
test_job_table checks the table: a row with proc and on_done for every job, procs
that return outside a comment, job_done still takes the close consent back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ombination

g_flow (FLOW_MAIN, FLOW_MAIN_WITH_DRIVER, FLOW_DRIVER_ONLY, FLOW_DRIVER_CONTINUE)
replaces g_pre, g_driver_only and the later reads of g_drvmode, which is now read
once at the start of wizard(). g_fix_done stays: it is progress, not flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_page (a 350-line switch) is split into one enter_<page> function per page,
and PAGES[] has one row per page: enter, primary, second, third, extra, the
close hook, the page's Alt+key shortcuts and PF_NOCLOSE. on_primary, on_second,
on_third, ID_EXTRA, on_close and the Alt+key handling in the message loop go
through the row. A new page is a P_* value, an enter_* function and a row.

Behaviour is unchanged: every page keeps the same buttons, texts, shortcuts and
close behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
g_result is the process exit code of wizard(). It is RC_CANCEL until a page decides
otherwise: the success pages (Done, driver installed, restart for the driver) set
RC_OK, the error pages (acquire error, "Something went wrong", "The driver was not
installed") set RC_FAIL; set_page takes it back to RC_CANCEL first. wizard() returns
it instead of testing g_page. Every exit code is the one the old page mapping gave.
The install_result that held the name is now g_instres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unit)

wizard.c keeps the includes, the ids, the enums and the state; the code is in
src\wizard\layout.h (UI helpers, screenshots, controls, layout), acquire.h (getting
the DynaRun setup and the driver package: download, password, check, license),
driver.h (the driver pages and jobs) and main.h (the main flow, JOBS[], PAGES[], the
window procedure, wizard()). They are fragments of the same translation unit, like
src\setup\*.h; build.cmd is unchanged. Only the order of the functions and a few
forward declarations in wizard.c differ: every function compiles to the same code.

tests: test_job_table() reads wizard.c and the four fragments as one text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wizard's state is one UI-owned wizard_state (wz). A job gets a job_in copied by start_job (like the old g_jobopts) and writes only a job_out; job_done merges the part the job produced (JOBS[].merge) after the join. The hint thread writes its own hint_out before it posts WM_HINT. test_job_table follows the new names and checks that no job proc touches wz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timliudev and others added 16 commits October 18, 2026 01:07
on_progress remembers the last percentage it posted (lastpct) so that it posts
only when the value changes. The UI's progress() used to reset that value too,
from the UI thread while the worker could be inside on_progress. Now start_job
sets it before the worker exists and only the worker changes it afterwards.

The race was benign (an int store that at worst made one more or one fewer
identical update get through), but the visible difference is that with an
unknown download size (percentage 0 throughout) the UI's reset on a page or
marquee change let a second or third update through; now the progress bar
gets exactly one update per value. The bar position is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wizard's 208 T(en, zh) calls (169 different pairs) become TX(S_...) over one table, UI_STR in src/wizard/strings.h; the setup's 29 calls (27 pairs) become TS(ST_...) over SETUP_STR in src/setup/strings.h. Exact duplicates (Cancel, Close, Open website ...) are in a table once. The texts are moved unchanged: the sequence of pairs used and the strings in wizard.obj / setup.obj are identical to before. setup.rc is untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sf_path() now takes the number of characters the caller appends afterwards
(the pinned-items folder, the shortcut file name) and skips the folder when the
result would not fit in MAX_PATH. The check is split into sf_fit() so that
test_hardening can exercise it with any path length.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The message parameter was used as an index into JOBS[] unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment only. The branch that wins the busy flag calls logf (log mutex, at most
2 s) and patch_base; only the flag itself is never waited for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timliudev
timliudev enabled auto-merge October 17, 2026 19:52
@timliudev
timliudev merged commit 657d770 into develop Oct 17, 2026
2 checks passed
@timliudev timliudev mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant