Skip to content

Fixed some Critical Errors - #80

Merged
Core447 merged 3 commits into
StreamController:mainfrom
G4PLS:main
Apr 26, 2024
Merged

Core447 merged 3 commits into
StreamController:mainfrom
G4PLS:main

Conversation

@G4PLS

@G4PLS G4PLS commented Apr 26, 2024

Copy link
Copy Markdown
Member
  • When using main store branch plugins were not able to register at all because version checks were only done on the min_app_version
  • Instead of setting min_app_version to None it will now be set to 0.0.0, this does basically the same as None but instead of exiting the function it will return True, which is expected behaviour when no min_app_version is provided
  • Added an extra security layer when trying to get the local sha in the store backend. It threw an error when id was not present because None values are not allowed, not the path will just point to icons/ and nothing happens
  • Added a check for symlinks when uninstalling a Plugin, only usefull in development and shouldnt do really do anything

@Core447
Core447 merged commit 746a733 into StreamController:main Apr 26, 2024
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 11, 2026
…aches logs.log (StreamController#80)

Install sys.excepthook / threading.excepthook / sys.unraisablehook
(src/backend/log_hooks.py) and route them through loguru: main-thread,
GLib/Gio-callback (PyGObject dispatches via PyErr_Print -> sys.excepthook,
verified headless against a live GLib.MainLoop), plain-thread and GC-time
exceptions now land in logs/logs.log, stderr and the About-dialog ring
with full tracebacks. KeyboardInterrupt passes through to the previous
hook; installation is idempotent; a failure inside the hook itself falls
back to sys.__stderr__ and can never recurse.

The plugin-dispatch asyncio loop gets the same handler for un-retrieved
task exceptions, and dispatch() attaches a done-callback so a failure in
_dispatch_batch itself can no longer vanish into a dropped Future -- the
one surface no process-global hook can see (pool-task exceptions never
reach threading.excepthook).

config_logger() moves above the dbus probe / deck reset / migrations
(that pre-sink startup phase used to be stderr-only) but stays below the
CLI early-returns, so short-lived helper invocations never open (or
rotate) the running app's log files. faulthandler is re-pointed from
stderr to logs/faulthandler.log once gl.DATA_PATH is final (it can come
from --data or the static settings file, so not resolvable at import
time), append-mode with boot markers so a crash dump survives the
restart that makes it readable.

tests/scenario_exception_hooks.py covers all four hook surfaces, the
asyncio handler and batch done-callback, idempotence, the re-entrancy
fallback, and the SIGQUIT dump-to-file path in a child process.
Suite: 33/33.
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 11, 2026
feat(logging): central exception hooks (StreamController#80)

See merge request naz/StreamController!1
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 12, 2026
…ll log sinks

The issue-StreamController#80 central exception hooks route full tracebacks into
logs.log (with backtrace=True, diagnose=True), so frame paths under
/home/<user>, standalone usernames and credentialed URLs now reach disk
unredacted -- users cannot safely share their logs (issue StreamController#105,
upstream StreamController#439).

Add src/backend/log_redaction.py: a stdlib-only scrub() (compiled
regexes, single pass each) plus a core-level loguru patcher. The
patcher runs before every handler.emit, so one install covers logs.log,
stderr, the gl.logs ring and the enqueue'd plugins.log sink, across
log.remove()/add() cycles. Because sinks format {exception} from the
raw exc tuple themselves, the patcher formats the traceback (stdlib,
chained), scrubs it, folds it into the message and clears
record["exception"] -- no sink ever sees raw frames. Deliberate side
effect: diagnose=True local-variable dumps (their own PII vector) no
longer reach any sink.

Redacted: home dir (expanduser/realpath/$HOME variants, boundary-guarded
both sides) -> "~"; username only as a path segment or in user@host;
scheme://user[:pass]@host -> scheme://***@host; secret params (token=,
access_token=, api_key=, password=, ...) -> =***; Bearer tokens.
Preserved: hosts, URL paths, ~-relative project paths, source lines,
exception types/messages -- logs stay debuggable.

Installed at the top of main() beside install_exception_hooks();
tests/scenario_log_redaction.py exercises the real hook + file-sink
pipeline and the scrub unit cases (34/34 harness green).

Closes StreamController#105
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 12, 2026
…longer freezes the deck (StreamController#1)

The writer thread (sole path for paints/brightness/Clear/ClearAndClose
since the single-writer migration) ran unguarded: any uncaught exception
killed it, freezing the deck until replug while control_q grew unbounded.

run() now delegates to _run_one_tick() inside a guarded loop: a tick
exception is logged with full traceback (rate-limited to 1/5s with a
suppressed-repeat count -- local limiter until StreamController#91), _stop is honored in
the except path so a failing body can't strand close(), and a 0.25s
wake-event backoff bounds a persistent failure at ~4 retries/s instead of
a 100% spin. @log.catch on run() would log once and let the thread die --
the guard must live inside the while. Composes with StreamController#80: the hooks report
an escaping death; this prevents the death.

The five audit-verified crash vectors are also fixed at the source:
(a) init_inputs is build-then-swap and the screensaver drops its
`inputs = {}` pre-clear (was an empty-dict window on every screensaver
entry); the loop snapshots the dict once per tick and uses .get();
(b) the loop snapshots background.video once (set_video(None) race);
(c) touchscreen get_current_image guards a nulled active_page (blank
strip); (d) the touchscreen tick gate snapshots state.background_video
(_release_background_video race); (e) _exec_clear snapshots the
touchscreen slot and nulls it identity-compared -- the crash half of StreamController#1
and this site's half of StreamController#8.

tests/scenario_writer_survival.py (first leg of StreamController#61) runs the REAL loop
over the stub tier: survives a poisoned tick, resumes paints, caps the
log storm, and stop() joins while every tick raises. fixtures gains
_QuietInputState (get_active_state) since the live loop path never
executed on stubs before (StreamController#69 stub-drift). Suite: 34/34.
nazbert added a commit to nazbert/Deckard that referenced this pull request Jul 26, 2026
update_ui_on_page_change resolved ...page_settings.settings_page -- an
attribute that never existed after the settings restructure -- so it
raised AttributeError on EVERY page change, a blanket except mislabeled
it as 'okay if you just activated your first deck', and nothing in the
body ever ran. Confirmed hypothesis from the 2026-07-17 field
diagnosis: chronic since the restructure, in every boot (and it cost
real diagnosis time by masquerading as a fresh regression).

Of the four things the body tried, three are dead by design in this UI
generation: the page-scoped PageSettings/DeckGroup is unreferenced, the
deck-scoped Brightness/Screensaver rows have no per-page state and
reload on map, and BackgroundMediaRow.load_defaults_from_page is a
deliberate dead early-return. The method now does the one meaningful
thing -- sidebar.update() -- scoped to the VISIBLE deck (via
get_own_deck_stack_child, so a background deck's page change cannot
reload the sidebar), with no except wrapper: this runs on the GLib main
loop, where a real failure reaches the StreamController#80 central hooks.

scenario_page_change_sidebar_sync fakes the real PageSettingsPage shape
(no settings_page anywhere), so reviving the dead chain fails the test;
it also pins the visible-deck guard and the headless no-op.

Closes StreamController#157
nazbert added a commit to nazbert/Deckard that referenced this pull request Aug 9, 2026
The StreamController#80 hooks are process-global behavior (sys/threading/unraisable hooks +
the asyncio handler + faulthandler redirection). Answering "does this
reproduce without the hooks?" meant reverting and rebuilding; now it is an
env var, following the SC_STRONG_CALLBACKS / SC_MEM_TELEMETRY / SC_REEXEC
convention (read once at import -- a mid-run flip would leave half the hooks
installed).

Two judgement calls inside the flag's blast radius:

  * the issue-StreamController#105 redaction patcher stays installed. install_exception_hooks()
    is its ONLY boot-path install site, so folding it into the opt-out would
    ship a flagged run with no PII scrubbing on ANY log line -- a debugging
    switch must not double as a privacy regression;
  * asyncio_exception_handler() honors the flag by delegating to
    loop.default_exception_handler(). It is one of the four StreamController#80 surfaces, but
    it is wired from event_dispatch rather than from the install path, so the
    opt-out has to live in the handler itself; delegating (rather than
    returning) is what makes the flag pre-StreamController#80 behavior exactly.

scenario_exception_hooks gains a flagged child process: the three interpreter
hooks stay stock, _installed never latches, no faulthandler.log (or even its
directory) is created, the asyncio surface falls back to asyncio's own
handler, and the redaction patcher is still in place.
nazbert added a commit to nazbert/Deckard that referenced this pull request Aug 9, 2026
…amController#91)

A GTK signal handler is not disconnected when it raises, so a broken handler
on a frequent signal (notify::, a 20-30Hz media tick) reached sys.excepthook
on every emission, and one broken __del__ hits sys.unraisablehook once per
object during a GC sweep -- ~30 full diagnose=True tracebacks per second into
logs.log and into the About-dialog ring. The StreamController#80 safety net's first big catch
would have DoS'd the log it reports to.

_log_exc() now allows one record per site per RATE_LIMIT_WINDOW_S (5s), where
a site is (exception type name, innermost traceback frame's file and line).
Repeats inside the window are counted and reported as a summary riding on
that site's next record. Per-site and never global, so a storm on one handler
cannot mask an unrelated one-shot failure; the first hit of any site is always
logged immediately. Placed in _log_exc rather than in the individual hooks, so
all four StreamController#80 surfaces inherit it.

The type NAME rather than the type object keeps a plugin's exception class
(and its module) from being pinned by the key. Tracebackless callers -- the
asyncio message-only context -- key on the message text instead, so two
unrelated tb-less failures still get independent budgets. The dict is pruned
once past 256 keys (expired windows first, then the oldest half) so a broad
storm cannot turn the guard into a leak, and a trailing count from a storm
that simply stops goes unreported rather than being flushed by a timer: these
hooks own no thread and must not grow one.

Thread safety: the window test is a read-modify-write over shared state, and
_log_exc runs on whatever thread failed -- any worker, the main/GLib thread, a
GC-sweeping thread, the dispatch loop. It takes an RLock, not a Lock: GC can
fire on an allocation inside the guarded region and a raising __del__
collected there re-enters the module on the same thread, which a plain Lock
would deadlock on inside the crash handler. The lock is never held across the
loguru call, and the whole guard is wrapped so a failure inside it falls
through to logging -- it may drop repeats, never an original.

scenario_exception_hooks gains: a 100x storm from one site (one record, then
the suppressed-99 summary on the next), two interleaved sites that keep
separate budgets, four distinct non-repeating failures that log untouched
with no summary noise (including via sys.excepthook), a same-line main-thread
storm, and a bound check on the state dict under 1024 distinct sites.
nazbert added a commit to nazbert/Deckard that referenced this pull request Aug 9, 2026
Review M3. The flag accepted "any value except empty/0", so
SC_NO_ERROR_HOOKS=false / no / off -- what an operator writes to turn a
switch OFF -- silently disabled the entire StreamController#80 safety net on the run that was
trying to keep it. Now strictly "1", matching weak_callbacks.py's
SC_STRONG_CALLBACKS precedent and #92's literal spec.

Also: a flagged run now says so, exactly once. The line is emitted from
redirect_faulthandler() rather than install_exception_hooks() because main()
installs the hooks BEFORE config_logger() opens logs.log and the About ring
-- an announcement there would be stderr-only, lost on precisely the detached
autostart/flatpak runs this flag exists to debug.

Scenario: the flagged child asserts the announcement appears once across two
redirect calls; five more children (0, false, off, no, empty) assert the
hooks are installed regardless.
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.

2 participants