Skip to content

+ Notes and rules about a program, and the one that is allowed to change the machine - #147

Open
Hawkynt wants to merge 6 commits into
mainfrom
process-notes-and-rules
Open

+ Notes and rules about a program, and the one that is allowed to change the machine#147
Hawkynt wants to merge 6 commits into
mainfrom
process-notes-and-rules

Conversation

@Hawkynt

@Hawkynt Hawkynt commented Aug 22, 2026

Copy link
Copy Markdown
Owner

§66 in full, and §91's row for it. Stacked on #146 — the first commit is that PR.

rules.tsv beside the settings, following them wherever --settings, PROCMAN_SETTINGS or a portable marker put them: the same shape §44's usage record has, and for the same two reasons. It is a list that grows rather than a set of preferences, and somebody throwing away every note they wrote should not lose the rest of their settings with it.

A rule attaches a note, colour, category and expected publisher, and recognises a program by path, digest, name, command line or signer.

Order decides, first match wins — not "the most specific", which sounds better and cannot be defined: is a path more specific than a digest, or a command line than a signer? A file read top to bottom is a rule somebody can predict by looking at it.

The pattern language is * and ? and nothing else. A rule file is edited by hand by somebody who wants to say "anything under /usr/bin", and every character of a path — the dots, the plus in a package name, the brackets a build system leaves behind — is a metacharacter in a regular expression and an ordinary character here. A digest is compared and never globbed: an asterisk in a hash is a typo, and treating it as a wildcard would quietly turn one rule into a rule about most of the machine.

A rule keyed on a reading nobody took is a third answer. The digest and the signer are read on request rather than every sample (§5.4), so a hash rule against a table that has not hashed anything has not failed to matchRuleVerdict.Unknown, and the collection reports it. Calling it "no" would silently drop every hash rule and leave a person watching their rule do nothing with nothing to tell them why (§72.3).

The third box is the dangerous one

Recording that a backup job ought to run at idle priority is a note; reniceing it is this program reaching out and changing the machine because of a line in a file. So:

  • apply is a column, not a global preference — twenty notes, one of them acting
  • anything that is not an explicit yes leaves the machine alone, so a file written by a version without that column cannot start acting because a field was missing
  • once per process, never again for that process. Somebody who lowers a priority by hand after the rule ran has overruled it, and a program putting it back every second would be fighting them with no way to win. Keyed on the identity pair, so a recycled pid is a new process (§8.2)

Verification, from the kernel rather than the suite

A rule matching *sleep 200* with apply=yes and 15 / 0-1 / idle, against a real process:

nice affinity I/O class
before 0 0-15 none
after 15 0,1 idle

read back out of /proc/[pid]/stat, taskset -pc and ionice -p. The same rule with apply=no left all three exactly as they were.

Reach

The note, category and publisher comparison are rows on the overview page all three front-ends build from; the colour is a row background in the window, where it beats the derived category — one is what this program worked out and the other is what a person decided — and still yields to a high-contrast desktop (§45.9, §74). Both front-ends run the applier, or a rule would act depending on which program you happened to open (§58).

The publisher is shown as a comparison rather than a value: "expected X" beside a signer field somebody has to scroll back to is two facts a reader has to join. Where the signature has not been read it says so — a publisher nobody checked and a publisher that did not match are opposite conclusions.

The colour reader moved into Core on the way past: two things read colours out of files now, and a hash meaning one thing in the settings file and another in the rules file is the drift the field catalogue was put in one place to prevent.

Suite 2646 passed, 2 skipped, 0 warnings; --self-test 37/37.

… query

Cycles, the ideal processor and the TEB pointer. All three sat unticked under a heading saying the
engine enumerates threads on both platforms, and each was missing for a different reason - which is
the whole of the unknown-is-a-value rule in one row.

Cycles are not another spelling of CPU time. Time is what the clock says a thread held a processor
for; cycles are what the processor actually retired. On a machine whose frequency moves - every
laptop - a thread on a core parked at 800 MHz reads exactly as busy by time as one at 4.8 GHz, and
only the cycle count says which. Linux reports n/i and not n/a: the kernel will charge cycles to a
task through perf_event_open, subject to kernel.perf_event_paranoid, so the absence is ours.

The ideal processor sits beside the last-CPU column on purpose. Affinity is where a thread is
allowed, last CPU is where it ran, and this is where the scheduler would put it given a free choice;
a thread preferred on processor 2 that keeps running on 7 is being bounced off its own cache, and no
one of the three columns says that alone. Flattened across processor groups like every other
processor number here, because on a machine with more than sixty-four processors, processor 3 of
group 1 and processor 3 of group 0 are different processors.

The TEB is the address only. What is inside one is another process memory and reading it means
attaching, which the no-driver rule refuses along with the driver, so the box stays partial.

The pass costs a handle per thread, so it runs for the one process somebody has open and never over
the table: four hundred processes carry several thousand threads, and three syscalls each for a page
nobody opened is the reading that has to be asked for rather than taken.

The bulk query now says "not sampled yet" for the three rather than "not supported" - the readings
exist and that call is simply not the one that takes them.

Checked against the kernel on pid 1: 319041 / 57453 voluntary and involuntary switches, affinity
0-15, and 12273 + 12612 ticks of user and kernel time all match /proc/1/stat and /proc/1/status
exactly, with cycles n/i and the other two n/a beside them.
…nge the machine

rules.tsv beside the settings, following them wherever --settings, PROCMAN_SETTINGS or a portable
marker put them - the shape the usage record already has, and for the same two reasons: it is a list
that grows rather than a set of preferences, and somebody throwing away every note they wrote should
not lose the rest of their settings with it.

A rule attaches a note, a colour, a category and an expected publisher, and recognises a program by
path, digest, name, command line or signer. Order decides and the first match wins - not "the most
specific", which sounds better and cannot be defined: is a path more specific than a digest, or a
command line than a signer? A file read top to bottom is a rule somebody can predict by looking at
it.

The pattern language is * and ? and nothing else. A rule file is edited by hand by somebody who wants
to say "anything under /usr/bin", and every character of a path - the dots, the plus in a package
name, the brackets a build system leaves behind - is a metacharacter in a regular expression and an
ordinary character here. A digest is compared and never globbed: an asterisk in a hash is a typo, and
treating it as a wildcard would quietly turn one rule into a rule about most of the machine.

A rule keyed on a reading nobody took is a third answer and not a no. The digest and the signer are
read on request rather than every sample, so a hash rule against a table that has not hashed anything
has not failed to match. Calling it "no" would silently drop every hash rule and leave a person
watching their rule do nothing with nothing to tell them why.

The dangerous part is the applier, so the opt-in is per rule. Recording that a backup job ought to
run at idle priority is a note; reniceing it is this program reaching out and changing the machine
because of a line in a file. apply is a column rather than a global preference, anything that is not
an explicit yes leaves the machine alone, and a file written by a version without that column cannot
start acting because a field was missing.

Once per process and never again for that process. Somebody who lowers a priority by hand after the
rule ran has overruled it, and a program putting it back every second would be fighting them with no
way to win. Keyed on the identity pair, so a recycled pid is a new process rather than one already
handled.

Reachable from all three. The note, the category and the publisher comparison are rows on the
overview page every front-end builds from; the colour is a row background in the window, where it
beats the derived category - one is what this program worked out and the other is what a person
decided - and still yields to a high-contrast desktop. Both front-ends run the applier, or a rule
would act depending on which program you happened to open.

The publisher is a comparison rather than a value. "Expected: Mozilla Corporation" beside a signer
field somebody has to scroll back to is two facts a reader has to join, and whether they agree is the
question they were asking. Where the signature has not been read it says so: a publisher nobody
checked and a publisher that did not match are opposite conclusions.

The colour reader moved into Core on the way past. Two things read colours out of files now, and a
hash meaning one thing in the settings file and another in the rules file is the drift the field
catalogue was put in one place to prevent.

Checked against the kernel and not against the suite. A rule matching *sleep 200* with apply=yes and
15 / 0-1 / idle took a real process from nice 0 to 15, affinity 0-15 to 0,1 and I/O class none to
idle, read back out of /proc/[pid]/stat, taskset and ionice. The same rule with apply=no left all
three exactly as they were.
… and what has happened

Both were described in the checklist as "a filter rather than a feature" and "one call away rather
than a feature away". They were.

Strings shows the ASCII, UTF-8 and UTF-16 runs of this process own image, offset and encoding beside
each, scanned when the tab is opened rather than when the window is and once rather than per tick.
It is the one reading in this window whose cost is the size of the file - every other page reads a
few kilobytes of structure. Bounded to the first 16 MB and the bound is said out loud when it bites,
because this runs on the thread that draws the window and the binary inspector is where somebody goes
to read a whole file on purpose.

Scanning the process own memory stays refused. process_vm_readv and /proc/[pid]/mem are both governed
by PTRACE_MODE_ATTACH, which Yama declines by default for anything this program did not start, and a
memory reverse-engineering suite is the first non-goal. That half is a decision and not a gap, which
is why the box is partial rather than ticked.

Timeline is the window own event ring filtered to this pid, newest first, refilled on the tick
because the whole point is that something happened while somebody was looking. The ring is handed in
rather than made here: one log per program and not one per properties window, or there would be two
bounds, two start times and two chances to disagree about what happened.

Three states and three sentences, because the first two are the same empty table and mean opposite
things - nothing is recording, nothing has been recorded yet, and nothing has happened to this
process while n things happened to others. A blank list with no heading reads as the third when it is
the first.

Both pages can legitimately come up empty, so both are in the capture log by heading rather than by
count: an empty rectangle and "nothing has been recorded yet" are the same picture. The shot proves
it - 8,936 runs out of 1,973,888 bytes with the symbol names legible, and the timeline saying nothing
happened to this process while three things happened to others.

Nineteen tabs is more than 1280 pixels of strip holds and the capture shows the last of them behind
the scroll arrows. Not a fault, and recorded rather than left for somebody to find by looking for a
tab that is off the edge.
…e cursor

W lists every unit and its state; b lists what runs at login and what will not. The window has had
both since the services and startup views were written, and the terminal had only the unit a process
belongs to - which is a different question. "What is running under this pid" is not "what is on this
machine", and a capability reachable from one front-end and not the other is exactly the drift the
one-catalogue rule exists to stop.

Both are read when asked for rather than on the tick. Enumerating every unit on the machine once a
second would be the monitor becoming the thing worth monitoring, and unit state does not move at the
rate a process table does.

Units are grouped running, then ran-and-exited, then inactive: the order the question comes in for
somebody who opened this after something broke. There is no "failed" heading and that is not an
omission - the manager keeps a failed unit state in its own memory and writes it to no file, so a
failed unit looks inactive from out here, and a heading that could never have anything under it is
worse than no heading. The words are the ones the window already uses, so the two cannot describe one
unit differently.

Autostart is split into what will run and what will not, and the second half is the point: a list
that does not say which entries are switched off is a list of things somebody will assume are
running. The reason travels with the entry, because "hidden" and "only shown in GNOME" are different
problems and only one of them is a mistake. A machine where everything runs gets no second heading -
an empty "will not" group reads as a list that failed to load rather than as good news.

Nothing back is a sentence in both, because a machine with no service manager and a machine whose
units this user may not read are the same empty box and opposite conclusions.

Checked against the machine. 377 units with 22 running, which is what systemctl list-units
--state=running counts, and four sampled main PIDs match systemctl show -p MainPID exactly - all off
disk and the cgroup tree, no D-Bus, nothing spawned. 24 autostart entries and 14 that will run: 23
.desktop files in /etc/xdg/autostart plus one systemd user unit sitting in default.target.wants,
which systemctl --user is-enabled calls disabled because it answers a different question - whether
the user enabled it, not whether it runs.

A second assertion came out of writing this: no two actions share a default key. A duplicate is a
binding that silently never fires.
…ices

GetServices answered with an empty list, so the view existed on Windows and had no rows in it. That
is not a view of a machine with nothing running; it is a feature nobody wrote, wearing the appearance
of one.

EnumServicesStatusEx for the list, then QueryServiceConfig and QueryServiceConfig2 per service.
Read-only by construction: the handles are asked for SERVICE_QUERY_CONFIG and SERVICE_QUERY_STATUS
and nothing else, so this path cannot start or stop anything even by accident - commanding a service
is IServiceControl job and asks for its own rights when it is used. Nothing needs elevation, and it
is read on demand rather than on the tick, for the same reason the systemd side is: two queries per
service several hundred times a second is the monitor becoming the thing worth monitoring.

Drivers are rows on purpose. The driver-service indicator is a column that can only exist if they
are, and a view that silently omitted half the service control manager would answer a narrower
question than the one it appears to.

The state map does not invent a systemd word for a Windows state. A pending start reports as running
because that is what the manager said and where it is going; paused reports as unknown, because
Windows has a state systemd does not and folding it into either of the other two would say something
untrue about a service that is neither.

Splitting ImagePath is text rather than syscalls, so it is in Core with no platform attribute and
runs on every CI leg. It is emphatically not a split on the first space: most Windows program paths
have one, so that would name C:\\Program for the majority of services on the machine, and the wrong
program is worse than no program because it looks like an answer. Quoted first, then the first .exe,
then the first space for a driver that has no extension at all. It does not try to resolve the
unquoted-path ambiguity Windows resolves by trying each prefix - reporting what the registry says is
a reading, and deciding what the loader would pick is a claim about a filesystem nobody looked at.

The Windows half is covered by a gated fixture whose assertions are about distinguishing things
rather than about counting them: something runs and something does not, something starts by itself
and something does not, at least some gave up a command line and an account. A reader that got the
start type wrong would most likely report one answer for every row, and every simpler assertion
would pass.

AOT publish stays at zero warnings with the new imports.
…iting on them

exit.time had nowhere for its answer to live, the exited-highlight had nothing to highlight, and the
Exited colour had been in the palette and the legend since they were written with nothing ever
producing it.

keep.exited in the settings file, in seconds, off by default. A table that keeps its dead is showing
something that is not there: a considered thing to ask for and a bad thing to assume, and on a
machine that churns through processes it doubles the table for nobody who was not looking for one
that ended. With it off, exit.time is a dash everywhere, which is the honest rendering of a question
nobody has arranged to be able to answer.

A kept row carries the last reading that was taken and every rate over it is unsampled. A row that
has stopped moving must not go on reporting the rate it had when it stopped - that draws a dead
process using a processor. Exited is classified ahead of New, because a short-lived process can be
born and buried inside one frame and "it has gone" is the more urgent of the two things to say.

A death is announced once. A tombstone left in the previous-sample index would go unmatched every
sample and report the same exit for as long as the row was kept, which would put one death in a
timeline sixty times a minute.

Two bounds, because they fail differently: the duration is what somebody asked for, and a cap of two
thousand rows is what stops a build machine ending a thousand processes a second from making the
table unreadable. A recycled pid is a new row beside the old one rather than a resurrection.

exit.code is the column this asked for and it is honest: NotPermitted everywhere, because neither
kernel tells a bystander what a process it did not start exited with - the status goes to the parent
through wait on Unix and needs a handle held across the exit on Windows. Nought is the code that
means success, which makes it the one value that must never be invented; a defaulted counter here
would report every process on the machine as having exited cleanly.

A defect the tests caught and a reader would not have. The delta sizes its per-row arrays to the
snapshot the probe left, which is before the kept rows are appended - so a kept row was in the table,
looked correct, and threw the moment any front-end asked it for a percentage. The delta is extended
after the append now.

Checked on the machine and not only in the suite: a process started and left to end came back in
--list as a row with a true exit time three seconds after it was gone, and a dash under the code.
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