Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture/04-render-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ engage by default on the desktop.
`GetCursorPos`/`SetCursorPos`.
- `DXGI_ERROR_DEVICE_REMOVED/RESET` latches `deviceLost()`. `recoverDeviceLost()` rebuilds through
the same `buildDeviceResources` as `initialize`, so the paths cannot drift.
- **Always restore the OS cursor.** `shutdown` and the crash filter (`CursorRestoreFilter`) restore
- **Always restore the OS cursor.** `shutdown` and the shared crash filter (`wind::CursorCrashFilter`, `mag_host.cpp`: report first, then restore) restore
the pointer and release any `ClipCursor`. Cursor hiding goes through
`MagApiAcquire`/`MagApiRelease`; see [05](05-transform-engine.md) for why the pairing must be
shared.
Expand Down
3 changes: 2 additions & 1 deletion installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ which makes `wind.nsi` define `LOCAL_SIGN`. Setup then runs `local-sign.ps1`:
signs the executables, and has its private key deleted at once, so nothing can sign with that root
again. Older Wind roots are retired on every install; the uninstaller removes them
(`local-sign.ps1 -Remove`).
- Setup installs the ordinary build first and signs the UIAccess build in `$PLUGINSDIR`, copying it
- Setup installs the ordinary build first and signs the UIAccess build in the admin-only
`$INSTDIR\.stage` (removed afterwards; never the user-writable `$PLUGINSDIR`), copying it
over only after its signature verifies. An unsigned UIAccess `Wind.exe` does not start at all, so
any failure leaves the ordinary build.
- A release signed with a real certificate has no `WindUA.exe` and skips this.
Expand Down
25 changes: 15 additions & 10 deletions installer/wind.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,27 @@ Section "Wind" SEC_WIND
; LocalMachine certificate stores are the machine's either way, but the PKI cmdlets are only
; guaranteed in the native one.
;
; The standard build goes in FIRST, and the uiAccess build is signed in $PLUGINSDIR and only
; The standard build goes in FIRST, and the uiAccess build is signed in $INSTDIR\.stage and only
; copied over it once its signature verifies. An unsigned uiAccess build does not start at
; all ("A referral was returned from the server"), so setup must never leave one in place:
; killed mid-signing, it did exactly that (reproduced 2026-09-28). This way an interruption
; at any point leaves a Wind that runs.
File "..\Wind.exe"
File "..\WindConfig.exe"
File "..\WindTray.exe"
InitPluginsDir
CreateDirectory "$PLUGINSDIR\ua"
File "/oname=$PLUGINSDIR\ua\Wind.exe" "..\WindUA.exe"
File "/oname=$PLUGINSDIR\local-sign.ps1" "local-sign.ps1"
; Staged under $INSTDIR, not in $PLUGINSDIR: that is the user's %TEMP%, writable by a
; non-elevated process, and this step runs a script and signs a binary as admin. Program
; Files is admin-only, so nothing unprivileged can swap either between copy and use.
RMDir /r "$INSTDIR\.stage"
CreateDirectory "$INSTDIR\.stage\ua"
File "/oname=$INSTDIR\.stage\ua\Wind.exe" "..\WindUA.exe"
File "/oname=$INSTDIR\.stage\local-sign.ps1" "local-sign.ps1"
DetailPrint "Signing Wind on this PC..."
nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\local-sign.ps1" -Stage "$PLUGINSDIR\ua\Wind.exe" -Dir "$INSTDIR"'
nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\.stage\local-sign.ps1" -Stage "$INSTDIR\.stage\ua\Wind.exe" -Dir "$INSTDIR"'
Pop $0
${If} $0 == 0
ClearErrors
CopyFiles /SILENT "$PLUGINSDIR\ua\Wind.exe" "$INSTDIR\Wind.exe"
CopyFiles /SILENT "$INSTDIR\.stage\ua\Wind.exe" "$INSTDIR\Wind.exe"
${If} ${Errors}
DetailPrint "Could not place the signed build; keeping the standard one."
File "..\Wind.exe"
Expand All @@ -124,6 +127,7 @@ Section "Wind" SEC_WIND
; menus and the Snipping Tool is lost.
DetailPrint "Local signing failed ($0); keeping the standard build."
${EndIf}
RMDir /r "$INSTDIR\.stage"
!else
File "..\Wind.exe"
File "..\WindConfig.exe"
Expand Down Expand Up @@ -181,10 +185,11 @@ Section "Uninstall"

; Retire the trust setup added for the local signature. Always, not only when this build
; signed: an older install may have, and a root left behind for an uninstalled app is litter.
InitPluginsDir
File "/oname=$PLUGINSDIR\local-sign.ps1" "local-sign.ps1"
nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$PLUGINSDIR\local-sign.ps1" -Remove'
CreateDirectory "$INSTDIR\.stage"
File "/oname=$INSTDIR\.stage\local-sign.ps1" "local-sign.ps1"
nsExec::ExecToLog '"$WINDIR\Sysnative\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "$INSTDIR\.stage\local-sign.ps1" -Remove'
Pop $0
RMDir /r "$INSTDIR\.stage"

Delete "$INSTDIR\Wind.exe"
Delete "$INSTDIR\WindConfig.exe"
Expand Down
36 changes: 23 additions & 13 deletions src/cursor_blanker.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "cursor_blanker.h"
#include "tick_span.h" // #361: per-tick spans
#include <chrono>
#include <future>
namespace wind {

Expand Down Expand Up @@ -30,7 +31,7 @@ static void RestoreAll() {
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0);
}

CursorBlanker::CursorBlanker() {
CursorBlanker::CursorBlanker() : st_(std::make_shared<State>()) {
// If a previous Wind was hard-killed while cursors were blanked, the desktop still has blank
// shared cursors; reload the user's scheme FIRST or the blanks get captured as "originals".
RestoreAll();
Expand All @@ -40,37 +41,46 @@ CursorBlanker::CursorBlanker() {
HCURSOR copy = CopyCursor(shared);
if (copy) originals_[shared] = copy;
}
worker_ = std::thread([this] { run(); });
worker_ = std::thread([st = st_] { run(st); });
}

CursorBlanker::~CursorBlanker() {
if (blanked_) restoreSync();
{ std::lock_guard<std::mutex> lk(mx_); stop_ = true; }
cv_.notify_one();
if (worker_.joinable()) worker_.join();
std::unique_lock<std::mutex> lk(st_->mx);
st_->stop = true;
st_->cv.notify_one();
// A worker wedged inside a system call must not hang exit: give it 2 s, then let it go (the
// shared state outlives this object). restoreSync above already restored from this thread.
const bool exited = st_->exitCv.wait_for(lk, std::chrono::seconds(2), [this] { return st_->exited; });
lk.unlock();
if (worker_.joinable()) { if (exited) worker_.join(); else worker_.detach(); }
}

void CursorBlanker::run() {
void CursorBlanker::run(std::shared_ptr<State> st) {
SetThreadDescription(GetCurrentThread(), L"Wind cursor swaps");
// Above normal: under a build the swap must still land promptly, or the pointer comes back
// late. It never competes with the tick for long: a swap is a few ms of work.
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL);
for (;;) {
std::function<void()> op;
{
std::unique_lock<std::mutex> lk(mx_);
cv_.wait(lk, [this] { return stop_ || !q_.empty(); });
if (q_.empty()) return; // stop_ with nothing left to do
op = std::move(q_.front());
q_.pop_front();
std::unique_lock<std::mutex> lk(st->mx);
st->cv.wait(lk, [&] { return st->stop || !st->q.empty(); });
if (st->q.empty()) { // stop with nothing left to do
st->exited = true;
st->exitCv.notify_all();
return;
}
op = std::move(st->q.front());
st->q.pop_front();
}
op();
}
}

void CursorBlanker::post(std::function<void()> op) {
{ std::lock_guard<std::mutex> lk(mx_); q_.push_back(std::move(op)); }
cv_.notify_one();
{ std::lock_guard<std::mutex> lk(st_->mx); st_->q.push_back(std::move(op)); }
st_->cv.notify_one();
}

bool CursorBlanker::runSync(std::function<void()> op) {
Expand Down
17 changes: 12 additions & 5 deletions src/cursor_blanker.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <condition_variable>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <thread>
#include <unordered_map>
Expand All @@ -28,15 +29,21 @@ class CursorBlanker {
// Restore and wait for it (up to 2 s). For shutdown, which must not exit with blank cursors.
void restoreSync();
private:
// The queue is shared with the worker, so a worker wedged in a system call at exit can be
// detached without leaving it a dangling `this` (the same shape as FocusLookup).
struct State {
std::mutex mx;
std::condition_variable cv; // wakes the worker
std::condition_variable exitCv; // wakes the destructor when the worker has returned
std::deque<std::function<void()>> q;
bool stop = false, exited = false;
};
void post(std::function<void()> op);
bool runSync(std::function<void()> op);
void run();
static void run(std::shared_ptr<State> st);
std::unordered_map<HCURSOR, HCURSOR> originals_;
bool blanked_ = false;
std::mutex mx_;
std::condition_variable cv_;
std::deque<std::function<void()>> q_;
bool stop_ = false;
std::shared_ptr<State> st_;
std::thread worker_;
};
}
10 changes: 10 additions & 0 deletions src/mag_host.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ void MagApiRelease() {

bool MagApiAlive() { return g_magRefs > 0; }

LONG WINAPI CursorCrashFilter(EXCEPTION_POINTERS* ep) {
static LONG s_inHandler = 0;
if (InterlockedExchange(&s_inHandler, 1)) return EXCEPTION_CONTINUE_SEARCH;
wind::WriteCrashReport(ep); // minidump + text summary into the log dir
MagShowSystemCursor(TRUE); // no-op if the Magnification API was never initialized this run
ClipCursor(nullptr); // never leave the cursor clipped if we crash while Inspect-locked
SystemParametersInfoW(SPI_SETCURSORS, 0, nullptr, 0); // heals a blanked cursor scheme, no broadcast
return EXCEPTION_CONTINUE_SEARCH;
}

bool MagHost::initialize() {
initialized_ = MagApiAcquire();
privateBroken_ = false; // re-probe the private channel on every (re-)init, not once ever
Expand Down
6 changes: 6 additions & 0 deletions src/mag_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ namespace wind {
bool MagApiAcquire();
void MagApiRelease();
bool MagApiAlive();
// Crash safety net shared by main.cpp (installed at startup) and RenderEngine (on its first cursor
// hide): if we go down while the cursor is hidden or confined, make it visible and free again.
// Writes the crash report FIRST (the restore can otherwise delay the dump), restores without the
// SPI broadcast (SENDCHANGE would run other windows' handlers, our own tint capture included,
// inside a faulted process), and returns EXCEPTION_CONTINUE_SEARCH so the default handler still runs.
LONG WINAPI CursorCrashFilter(EXCEPTION_POINTERS* ep);
class MagHost {
public:
bool initialize();
Expand Down
Loading
Loading