Make the server authoritative for what endpoints watch and launch
Fleet health showed dncMain.exe red on endpoints that never ran eDNC. Three
layers each re-added the monitor, so fixing any one of them in isolation left
the others to put it back.
The installer baked a watch list into appsettings.json on every package, and
appsettings.json is never rewritten on the box, so an entry outlived whatever
policy justified it. The server auto-attached shipped monitors by PC type name.
The agent then UNIONED the server's resolved list with its local one, so an
entry could never be removed from the server side at all.
Now: the installer seeds nothing, auto-attach by PC type is gone, and
MonitorConfig.Merge replaces the local list with the server's. Null and empty
stay distinct - null means the fetch failed so keep what we have, empty means
watch nothing. Collapsing those blanks monitoring fleet-wide on one failed HTTP
call, or makes detaching the last monitor silently do nothing forever.
Ingest filters reported services against the resolved monitors as well. The
agent config sits on the endpoint, so it is not a trustworthy account of what
that endpoint should be reporting.
Launch paths move the same way, which exposed an older bug. MTConnect installs
to ProgramData and starts through run.bat, but LaunchPathPolicy approved only
the Program Files roots, so the launchpath on its shipped monitor row - seeded
in 0006, present from the start - was rejected every time as outside every
approved root. That was invisible because a local launchpaths entry wins in
ResolveLaunchPath and never reaches validation; remote start worked only
through the bypass. The same precedence meant editing an eDNC, UDC or MTConnect
path in the WebUI could never reach an existing endpoint.
So ProgramData is approved and the local launch map ships empty. ProgramData is
world-writable and the agent launches as SYSTEM, which is why it was excluded
originally; it is deliberately too broad for now and carries a REVISIT comment.
The remaining gates still hold - admin-configured monitor row, exists,
launchable extension, re-checked after symlink resolution. Narrowing it to
ProgramData\udc needs the fleet's non-Program-Files install locations
confirmed first.
Also seeds the Office ProPlus LTSC monitor templates, x64 and x86 as separate
rows since they differ only by path, and adds an agent test project. It links
the platform-pure sources rather than referencing the net8.0-windows assembly
so the semantics run on the Linux CI runners. Writing those tests caught a
merge bug where only the dedupe key was trimmed and the stored name was not.
Note for the wj merge: plugins/usb/frontend/views/usbSettings.spec.js fails on
this branch and is untouched here. Commit da91a29 added four kiosk settings the
save assertion does not expect. The spec looks like the stale side, but that is
the usb author's call - flagging rather than guessing.
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""Seed the Office ProPlus LTSC monitor templates (x64 and x86).
|
||||
|
||||
Revision ID: endpoints0025officemonitors
|
||||
Revises: endpoints0024bootstraptokens
|
||||
Create Date: 2026-09-19
|
||||
|
||||
Adds shipped process-monitor definitions for the Office ProPlus LTSC suite at
|
||||
both install roots:
|
||||
|
||||
C:\\Program Files\\Microsoft Office\\root\\Office16\\<app>.EXE (x64)
|
||||
C:\\Program Files (x86)\\Microsoft Office\\root\\Office16\\<app>.EXE (x86)
|
||||
|
||||
Apps: WINWORD, EXCEL, POWERPNT, OUTLOOK, MSACCESS.
|
||||
|
||||
These are TEMPLATES, not an instruction to watch anything. Seeding a definition
|
||||
only puts a row in the catalog; nothing is watched until a site policy attaches
|
||||
it to a PC type. That separation is the whole point of the monitor rework - the
|
||||
installer no longer bakes a watch list into appsettings.json, and the agent
|
||||
replaces its list from the server each cycle - so adding rows here cannot
|
||||
resurrect the "dncMain.exe on every endpoint" symptom.
|
||||
|
||||
x64 and x86 are SEPARATE rows rather than one row with two paths. The launchpath
|
||||
column holds a single path, and the two roots are genuinely different installs;
|
||||
a site pins whichever one it actually deploys. The kind/displayname unique
|
||||
constraint is what forces the architecture into the display name, which is also
|
||||
what an operator needs to see to pick the right one.
|
||||
|
||||
processname is the same image name for both (WINWORD.EXE and friends do not
|
||||
differ by architecture), so these rows are distinguished by launchpath. A site
|
||||
that runs both would attach only the row matching its install root.
|
||||
|
||||
Idempotent on (kind, displayname): a re-run inserts nothing. Unlike 0006 this
|
||||
does NOT bail out when the table is non-empty - that guard would make this a
|
||||
no-op on every existing site, which is precisely where these templates are
|
||||
wanted. Guards on the table being present and no-ops if it is not.
|
||||
|
||||
Downgrade removes only the rows this added, and only when no policy action
|
||||
references them, so a site that composed one into a policy does not lose it to a
|
||||
rollback.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# Alembic stores this in alembic_version.version_num, a VARCHAR(32).
|
||||
revision = 'endpoints0025officemonitors'
|
||||
down_revision = 'endpoints0024bootstraptokens'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
plugin_name = 'endpoints'
|
||||
|
||||
MONITORS = 'endpointmonitors'
|
||||
|
||||
_X64_ROOT = r'C:\Program Files\Microsoft Office\root\Office16'
|
||||
_X86_ROOT = r'C:\Program Files (x86)\Microsoft Office\root\Office16'
|
||||
|
||||
# (label, image name). The image name is identical across architectures.
|
||||
_OFFICE_APPS = [
|
||||
('Word', 'WINWORD.EXE'),
|
||||
('Excel', 'EXCEL.EXE'),
|
||||
('PowerPoint', 'POWERPNT.EXE'),
|
||||
('Outlook', 'OUTLOOK.EXE'),
|
||||
('Access', 'MSACCESS.EXE'),
|
||||
]
|
||||
|
||||
|
||||
def _rows():
|
||||
"""The shipped Office rows: every app at both install roots."""
|
||||
for label, image in _OFFICE_APPS:
|
||||
for architecture, root in (('x64', _X64_ROOT), ('x86', _X86_ROOT)):
|
||||
yield {
|
||||
'kind': 'process',
|
||||
'displayname': 'Office {0} ({1})'.format(label, architecture),
|
||||
'processname': image,
|
||||
'servicename': None,
|
||||
'launchpath': '{0}\\{1}'.format(root, image),
|
||||
'datacollectorpath': None,
|
||||
'logcollectorpath': None,
|
||||
'source': 'shipped',
|
||||
'isactive': True,
|
||||
}
|
||||
|
||||
|
||||
def _inspector():
|
||||
from sqlalchemy import inspect
|
||||
|
||||
return inspect(op.get_bind())
|
||||
|
||||
|
||||
def _has_table(name):
|
||||
return _inspector().has_table(name)
|
||||
|
||||
|
||||
def _existing(bind, kind, displayname):
|
||||
return bind.execute(
|
||||
sa.text('SELECT endpointmonitorid FROM endpointmonitors '
|
||||
'WHERE kind = :kind AND displayname = :displayname'),
|
||||
{'kind': kind, 'displayname': displayname}).scalar()
|
||||
|
||||
|
||||
def upgrade():
|
||||
if not _has_table(MONITORS):
|
||||
return
|
||||
|
||||
bind = op.get_bind()
|
||||
now = datetime.utcnow()
|
||||
|
||||
monitors = sa.table(
|
||||
MONITORS,
|
||||
sa.column('kind', sa.String),
|
||||
sa.column('displayname', sa.String),
|
||||
sa.column('processname', sa.String),
|
||||
sa.column('servicename', sa.String),
|
||||
sa.column('launchpath', sa.String),
|
||||
sa.column('datacollectorpath', sa.String),
|
||||
sa.column('logcollectorpath', sa.String),
|
||||
sa.column('source', sa.String),
|
||||
sa.column('isactive', sa.Boolean),
|
||||
sa.column('createdat', sa.DateTime),
|
||||
)
|
||||
|
||||
pending = []
|
||||
for row in _rows():
|
||||
if _existing(bind, row['kind'], row['displayname']) is not None:
|
||||
continue
|
||||
record = dict(row)
|
||||
record['createdat'] = now
|
||||
pending.append(record)
|
||||
|
||||
if pending:
|
||||
op.bulk_insert(monitors, pending)
|
||||
|
||||
|
||||
def downgrade():
|
||||
if not _has_table(MONITORS):
|
||||
return
|
||||
|
||||
bind = op.get_bind()
|
||||
policies_present = _has_table('configurationpolicyactions')
|
||||
|
||||
for row in _rows():
|
||||
monitorid = _existing(bind, row['kind'], row['displayname'])
|
||||
if monitorid is None:
|
||||
continue
|
||||
|
||||
# Leave behind anything a site has already composed into a policy:
|
||||
# dropping it would silently break that policy's action.
|
||||
if policies_present:
|
||||
referenced = bind.execute(
|
||||
sa.text('SELECT COUNT(*) FROM configurationpolicyactions '
|
||||
'WHERE action = :action'),
|
||||
{'action': 'monitor:{0}'.format(monitorid)}).scalar()
|
||||
if referenced:
|
||||
continue
|
||||
|
||||
bind.execute(
|
||||
sa.text('DELETE FROM endpointmonitors '
|
||||
'WHERE endpointmonitorid = :id'),
|
||||
{'id': monitorid})
|
||||
Reference in New Issue
Block a user