Stop scripts reporting success after doing nothing
A run_script command was judged solely on PowerShell's exit code, and PowerShell treats most errors as non-terminating: it writes the error, runs the next line, and still exits zero. The account-provisioning script reported succeeded having provisioned nothing, and because no output was captured there was no evidence to contradict the verdict. Scripts now carry an errormode. Strict prepends $ErrorActionPreference = 'Stop' so the first error ends the run and the exit code means what it says. It is opt-in per script rather than global because bodies authored against the permissive semantics rely on them - tolerating a failing cleanup, probing with a cmdlet that errors when a thing is absent, looping past a bad item - and would start aborting midway, leaving endpoints half-configured. The default is unchanged, so every existing script keeps running exactly as before. The prologue is applied after digest verification, so the SHA-256 still validates the bytes the server delivered. stdout/stderr is captured on both paths: redirected streams for SYSTEM, a transcript file under the ShopFloor profile for user-session runs, since CreateProcessAsUser cannot inherit the service's pipes across the session boundary. Output is always logged locally and returned to the server only on failure, so routine output stays on the machine that produced it instead of filling command history. Also adds the optional per-script working directory, which the same surfaces needed, and a collapsible explainer on the scripts page - the dropdown is not self-explanatory, and an operator who does not know about non-terminating errors has no reason to ever leave the default. Agent 1.14.0. An older agent ignoring errormode keeps today's behaviour.
This commit is contained in:
@@ -20,6 +20,66 @@
|
||||
written to act "as the logged-on user" will not behave the way it reads.
|
||||
</div>
|
||||
|
||||
<details class="explainer">
|
||||
<summary>How does this work?</summary>
|
||||
<div class="explainer-body">
|
||||
<h4>Delivery</h4>
|
||||
<p>
|
||||
The body is stored here and hashed. At push time the agent downloads it
|
||||
with its own credential and checks the digest before running anything,
|
||||
so the bytes that run are the bytes that were reviewed. The body never
|
||||
travels on the command channel and is deleted from the endpoint after
|
||||
the run.
|
||||
</p>
|
||||
|
||||
<h4>Runs in</h4>
|
||||
<p>
|
||||
<strong>System</strong> is the agent's own service account: no desktop,
|
||||
no mapped drives, no signed-in person's credentials.
|
||||
<strong>User</strong> runs in the interactive session instead, and only
|
||||
works when somebody is actually signed in.
|
||||
</p>
|
||||
|
||||
<h4>Run in folder</h4>
|
||||
<p>
|
||||
The working directory for the run. Blank means the agent's staging
|
||||
folder, which is right for a self-contained script. Set it when the
|
||||
script calls a tool installed separately on the endpoint by relative
|
||||
path. If the folder is missing the run fails outright rather than
|
||||
running somewhere unintended.
|
||||
</p>
|
||||
|
||||
<h4>When a line fails</h4>
|
||||
<p>
|
||||
This is the one worth understanding. PowerShell treats most errors as
|
||||
<em>non-terminating</em>: it writes the error, carries on to the next
|
||||
line, and still exits zero. Because the command's verdict comes from
|
||||
that exit code, <strong>Continue</strong> means a script can report
|
||||
success having accomplished nothing.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Stop</strong> makes the first error end the run, so the verdict
|
||||
means what you would assume it means. It is not the default because it
|
||||
changes how a script behaves: a body that tolerates a failing cleanup,
|
||||
probes with a cmdlet that errors when something is absent, or loops
|
||||
past bad items will start aborting partway through, leaving the
|
||||
endpoint half-configured. Read the script first, then switch it on.
|
||||
</p>
|
||||
|
||||
<h4>Where the output goes</h4>
|
||||
<p>
|
||||
Script output is always written to the agent log on the endpoint. It is
|
||||
sent back to ShopDB only when the run fails, so routine output stays on
|
||||
the machine that produced it. Long output is truncated and marked as
|
||||
such.
|
||||
</p>
|
||||
<p class="hint">
|
||||
In Stop mode the agent adds one line to the top of the script, so an
|
||||
error reported on line N refers to line N-1 of the body shown here.
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="card">
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<template v-else>
|
||||
@@ -29,6 +89,8 @@
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Runs in</th>
|
||||
<th>On error</th>
|
||||
<th>Body</th>
|
||||
<th>Digest</th>
|
||||
<th>Active</th>
|
||||
@@ -41,6 +103,17 @@
|
||||
<td class="cell-truncate" :title="s.scriptdescription">
|
||||
{{ s.scriptdescription || '-' }}
|
||||
</td>
|
||||
<td class="cell-truncate mono" :title="s.workingdirectory">
|
||||
{{ s.workingdirectory || 'Agent staging folder' }}
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="s.errormode === 'strict'" class="badge badge-secondary">
|
||||
Stop on first error
|
||||
</span>
|
||||
<span v-else class="badge badge-warning" title="A failing line does not fail the command">
|
||||
Continue
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="s.hasbody" class="badge badge-secondary">{{ s.filename }}</span>
|
||||
<span v-else class="badge badge-warning">No body - cannot run</span>
|
||||
@@ -53,7 +126,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="scripts.length === 0">
|
||||
<td colspan="6" style="text-align:center; color: var(--text-light);">
|
||||
<td colspan="8" style="text-align:center; color: var(--text-light);">
|
||||
No scripts
|
||||
</td>
|
||||
</tr>
|
||||
@@ -99,6 +172,41 @@
|
||||
No body uploaded yet. A script without a body cannot be pushed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Run in folder
|
||||
<span class="hint">(optional, endpoint-local absolute path)</span>
|
||||
</label>
|
||||
<input
|
||||
v-model="form.workingdirectory"
|
||||
type="text"
|
||||
class="form-control mono"
|
||||
placeholder="C:\Program Files\CorporateTools"
|
||||
/>
|
||||
<p class="hint">
|
||||
Leave blank to run from the agent's staging folder. Set this only
|
||||
when the script calls a tool already installed on the endpoint by
|
||||
relative path. The folder must exist on the endpoint or the push
|
||||
fails rather than running somewhere else.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
When a line fails
|
||||
<span class="hint">(how the run is judged)</span>
|
||||
</label>
|
||||
<select v-model="form.errormode" class="form-control">
|
||||
<option value="default">Continue - report success if the script reaches the end</option>
|
||||
<option value="strict">Stop on the first error and fail the command</option>
|
||||
</select>
|
||||
<p class="hint">
|
||||
PowerShell normally writes an error and carries on, and still
|
||||
exits zero, so a script can report success having done none of
|
||||
its work. Stop makes the first failure end the run and fail the
|
||||
command. Leave this on Continue unless you have read the script
|
||||
and know it does not rely on a step being allowed to fail.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input v-model="form.isactive" type="checkbox" />
|
||||
@@ -143,6 +251,8 @@ function emptyForm() {
|
||||
return {
|
||||
scriptname: '',
|
||||
scriptdescription: '',
|
||||
workingdirectory: '',
|
||||
errormode: 'default',
|
||||
isactive: true
|
||||
}
|
||||
}
|
||||
@@ -175,6 +285,8 @@ function openModal(script = null) {
|
||||
? {
|
||||
scriptname: script.scriptname,
|
||||
scriptdescription: script.scriptdescription || '',
|
||||
workingdirectory: script.workingdirectory || '',
|
||||
errormode: script.errormode || 'default',
|
||||
isactive: script.isactive
|
||||
}
|
||||
: emptyForm()
|
||||
@@ -194,6 +306,8 @@ async function save() {
|
||||
const payload = {
|
||||
scriptname: form.value.scriptname,
|
||||
scriptdescription: form.value.scriptdescription,
|
||||
workingdirectory: form.value.workingdirectory,
|
||||
errormode: form.value.errormode,
|
||||
isactive: form.value.isactive
|
||||
}
|
||||
let scriptid
|
||||
@@ -249,4 +363,27 @@ onMounted(loadData)
|
||||
margin-bottom: 1rem;
|
||||
background: rgba(184, 134, 11, 0.08);
|
||||
}
|
||||
.explainer {
|
||||
border: 1px solid var(--border, #ddd);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--surface, #fff);
|
||||
}
|
||||
.explainer summary {
|
||||
cursor: pointer;
|
||||
padding: 0.6rem 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.explainer-body {
|
||||
padding: 0 1rem 0.75rem;
|
||||
}
|
||||
.explainer-body h4 {
|
||||
margin: 0.9rem 0 0.25rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.explainer-body p {
|
||||
margin: 0 0 0.5rem;
|
||||
color: var(--text-light);
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user