Merge wilm into wj
Brings in wilm commits: - Render script surfaces correctly in dark mode - Stop scripts reporting success after doing nothing - Correct two gate commands that pass without running - Gate the script library on the grant that pushes scripts - Let the policy list edit the rows the API accepts - Cap plugin revision ids at the 32 chars MySQL stores - Split shipped monitor bundle into per-monitor policies Resolved conflict in agent ShopDbEndpointAgent.csproj by merging: - wilm 1.14.0 script verdict and output capture features - wj 1.13.1-1.13.5 tray coordination releases - Bumped to 1.14.1 for coordinated agent+tray release
This commit is contained in:
@@ -901,6 +901,7 @@ public sealed class CommandExecutor
|
||||
|
||||
var runInUser = string.Equals(
|
||||
script.RunContext, "user", StringComparison.OrdinalIgnoreCase);
|
||||
ApplyErrorMode(script, tempPath, name);
|
||||
return runInUser
|
||||
? RunScriptInUserSession(name, script, tempPath)
|
||||
: await RunScriptAsSystem(name, script, tempPath, token);
|
||||
@@ -933,18 +934,203 @@ public sealed class CommandExecutor
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort. A missing transcript directory must not stop the script from
|
||||
// running: the capture is diagnostics, and losing diagnostics is a smaller
|
||||
// failure than refusing to do the work that was asked for.
|
||||
private void TryEnsureTranscriptDirectory(string transcriptPath, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(transcriptPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory!);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogWarning(
|
||||
error,
|
||||
"run_script {Name}: could not prepare the output transcript folder; "
|
||||
+ "the script will still run but its output will not be captured", name);
|
||||
}
|
||||
}
|
||||
|
||||
// Read the transcript back and delete it. Deleted whether or not the read
|
||||
// worked: it sits in the operator's own profile where they could read it,
|
||||
// and a script's output is not theirs to keep lying around after the agent
|
||||
// has reported it.
|
||||
private string ReadAndClearTranscript(string transcriptPath, string name)
|
||||
{
|
||||
var output = "";
|
||||
try
|
||||
{
|
||||
if (File.Exists(transcriptPath))
|
||||
output = CombineScriptOutput(File.ReadAllText(transcriptPath), null);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogWarning(
|
||||
error, "run_script {Name}: could not read the output transcript", name);
|
||||
}
|
||||
try
|
||||
{
|
||||
if (File.Exists(transcriptPath))
|
||||
File.Delete(transcriptPath);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogDebug(
|
||||
error, "run_script {Name}: transcript cleanup failed", name);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Cap on captured script output. Two different limits are at work: the
|
||||
// endpoint log should stay readable, and the server payload rides an HTTP
|
||||
// POST into a MEDIUMTEXT column. A runaway loop printing megabytes must not
|
||||
// turn a failed script into a failed REPORT - losing the verdict entirely is
|
||||
// worse than losing the tail of the evidence. Head is kept rather than tail
|
||||
// because the FIRST error is the one that explains the failure; under strict
|
||||
// mode nothing after it ran anyway.
|
||||
private const int MaxCapturedOutputChars = 8000;
|
||||
|
||||
// Merge the two streams into one operator-readable blob. stderr is labelled
|
||||
// rather than concatenated blind, because "which stream said this" is the
|
||||
// difference between a warning a script prints and the error that killed it.
|
||||
private static string CombineScriptOutput(string? stdout, string? stderr)
|
||||
{
|
||||
var parts = new List<string>(2);
|
||||
if (!string.IsNullOrWhiteSpace(stdout))
|
||||
parts.Add(stdout!.TrimEnd());
|
||||
if (!string.IsNullOrWhiteSpace(stderr))
|
||||
parts.Add("[stderr] " + stderr!.TrimEnd());
|
||||
var combined = string.Join(Environment.NewLine, parts);
|
||||
if (combined.Length <= MaxCapturedOutputChars)
|
||||
return combined;
|
||||
// Say so explicitly. Silently truncated evidence reads as a script that
|
||||
// stopped talking, which is a different diagnosis than one that was cut.
|
||||
return combined.Substring(0, MaxCapturedOutputChars)
|
||||
+ Environment.NewLine
|
||||
+ $"[truncated after {MaxCapturedOutputChars} characters]";
|
||||
}
|
||||
|
||||
// The endpoint log gets the output ALWAYS, success or failure. This is the
|
||||
// copy that stays on the box: it never leaves the endpoint, so it costs no
|
||||
// exposure, and it is the only record when a script reports success while
|
||||
// doing nothing useful. The server copy is failure-only (see ScriptDetail).
|
||||
private void LogScriptOutput(string name, string output, int? exitCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"run_script {Name} produced no output (exit {Exit})",
|
||||
name, exitCode?.ToString() ?? "unknown");
|
||||
return;
|
||||
}
|
||||
logger.LogInformation(
|
||||
"run_script {Name} output (exit {Exit}):{NewLine}{Output}",
|
||||
name, exitCode?.ToString() ?? "unknown", Environment.NewLine, output);
|
||||
}
|
||||
|
||||
// Prepended to a strict-mode body. Written on ONE line so every line number
|
||||
// PowerShell reports still matches the body the author sees in the WebUI
|
||||
// editor: a multi-line prologue would shift them and make the error text
|
||||
// point at the wrong statement during exactly the debugging session that
|
||||
// needed it.
|
||||
private const string StrictModePrologue =
|
||||
"$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'";
|
||||
|
||||
// Turn a script's non-terminating errors fatal when the library says strict.
|
||||
//
|
||||
// MUST run AFTER the SHA-256 check. The digest pins the bytes the SERVER
|
||||
// holds; prepending first would rewrite the file and fail its own integrity
|
||||
// check. Verify what was delivered, then modify how it is interpreted.
|
||||
//
|
||||
// Best-effort by design: if the rewrite fails the script still runs under
|
||||
// PowerShell's default rules. That is the behaviour it had before this
|
||||
// existed, so a failed rewrite degrades to "as before" rather than blocking
|
||||
// work, and the warning says the verdict is the weaker one.
|
||||
private void ApplyErrorMode(ScriptDeliveryClaim script, string scriptPath, string name)
|
||||
{
|
||||
if (!string.Equals(script.ErrorMode, "strict", StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var body = File.ReadAllText(scriptPath);
|
||||
File.WriteAllText(scriptPath, StrictModePrologue + Environment.NewLine + body);
|
||||
logger.LogInformation(
|
||||
"run_script {Name}: strict error mode applied; a non-terminating "
|
||||
+ "error will now stop the script and fail the command", name);
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
logger.LogWarning(
|
||||
error,
|
||||
"run_script {Name}: could not apply strict error mode; running under "
|
||||
+ "PowerShell defaults, so a non-terminating error will NOT fail the command",
|
||||
name);
|
||||
}
|
||||
}
|
||||
|
||||
// Where the script actually runs. Empty means the agent staging directory,
|
||||
// which is right for a self-contained script and wrong for one that shells
|
||||
// out to a corporate tool already installed on the endpoint (Sysinternals
|
||||
// and similar, delivered by Intune) and invokes it by relative path. The
|
||||
// server carries that folder per script.
|
||||
//
|
||||
// A NAMED DIRECTORY THAT IS ABSENT FAILS THE COMMAND. Falling back to
|
||||
// staging would run SYSTEM PowerShell somewhere other than where the author
|
||||
// said; the script would then fail further in with an error naming the
|
||||
// missing tool rather than the missing folder, or half-succeed, which is
|
||||
// worse. Fail closed and name the folder.
|
||||
private static bool TryResolveWorkingDirectory(
|
||||
ScriptDeliveryClaim? script, string scriptPath,
|
||||
out string workingDirectory, out string error)
|
||||
{
|
||||
error = "";
|
||||
workingDirectory = Path.GetDirectoryName(scriptPath) ?? "";
|
||||
|
||||
var requested = script?.WorkingDirectory;
|
||||
if (string.IsNullOrWhiteSpace(requested))
|
||||
return true;
|
||||
|
||||
if (!Directory.Exists(requested))
|
||||
{
|
||||
error = $"working directory not found on this endpoint: {requested}";
|
||||
return false;
|
||||
}
|
||||
|
||||
workingDirectory = requested!;
|
||||
return true;
|
||||
}
|
||||
|
||||
// System context: run through the agent's own service account and wait for
|
||||
// the exit code, the same as before. Fine for machine-scoped scripts that do
|
||||
// not need the interactive user's session resources.
|
||||
private async Task<CommandResult> RunScriptAsSystem(
|
||||
string name, ScriptDeliveryClaim script, string scriptPath, CancellationToken token)
|
||||
{
|
||||
if (!TryResolveWorkingDirectory(
|
||||
script, scriptPath, out var workingDirectory, out var directoryError))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"run_script for {Name} not started: {Error}", name, directoryError);
|
||||
return new CommandResult
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = $"Script {name} not run: {directoryError}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null),
|
||||
};
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "powershell.exe",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(scriptPath) ?? "",
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
startInfo.ArgumentList.Add("-NoProfile");
|
||||
startInfo.ArgumentList.Add("-NonInteractive");
|
||||
@@ -960,19 +1146,27 @@ public sealed class CommandExecutor
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = $"Failed to start script {name}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null),
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null, workingDirectory),
|
||||
};
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync(token);
|
||||
// Read only AFTER exit, for the reason spelled out on the schtasks path:
|
||||
// ReadToEnd blocks until the streams close, which IS process exit, so
|
||||
// reading first would make the caller's cancellation unable to fire.
|
||||
var stdout = await process.StandardOutput.ReadToEndAsync(token);
|
||||
var stderr = await process.StandardError.ReadToEndAsync(token);
|
||||
var exit = process.ExitCode;
|
||||
var output = CombineScriptOutput(stdout, stderr);
|
||||
LogScriptOutput(name, output, exit);
|
||||
return new CommandResult
|
||||
{
|
||||
Succeeded = exit == 0,
|
||||
Message = exit == 0
|
||||
? $"Script {name} completed (system context)"
|
||||
: $"Script {name} exited with code {exit}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exit),
|
||||
Detail = ScriptDetail(name, script, scriptPath, exit, workingDirectory,
|
||||
exit == 0 ? null : output),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -988,13 +1182,57 @@ public sealed class CommandExecutor
|
||||
// hung script pinned the caller forever.
|
||||
private static readonly TimeSpan UserScriptWaitTimeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
// Where a user-session script's output is captured to.
|
||||
//
|
||||
// The SYSTEM path can simply redirect the child's stdout/stderr handles.
|
||||
// This path cannot: it goes through CreateProcessAsUser to cross a session
|
||||
// boundary, and inheriting pipe handles across that boundary is exactly the
|
||||
// part of the launcher that is delicate (session enumeration, token
|
||||
// duplication). So PowerShell redirects to a file itself and the agent reads
|
||||
// it back after exit.
|
||||
//
|
||||
// The file lands in the OPERATOR's profile, not the agent's ProgramData
|
||||
// staging folder: staging is SYSTEM-owned and the signed-in operator cannot
|
||||
// write there, which is the same constraint that put the tray's own log in
|
||||
// the profile. Safe to rely on here because a user-context script only runs
|
||||
// when someone is interactively signed in, and a signed-in account has a
|
||||
// materialised profile by definition.
|
||||
private static string UserTranscriptPath(int scriptId)
|
||||
=> Path.Combine(
|
||||
FileLoggerProvider.UserProfileLogDirectory(),
|
||||
$"shopdb-script-{scriptId}-output.txt");
|
||||
|
||||
private CommandResult RunScriptInUserSession(
|
||||
string name, ScriptDeliveryClaim script, string scriptPath)
|
||||
{
|
||||
var quotedPath = "\"" + scriptPath + "\"";
|
||||
// Redirect inside the user session rather than from the service. The
|
||||
// child is launched through CreateProcessAsUser, so the agent holds no
|
||||
// handles to its streams; -Command wraps the same -File invocation and
|
||||
// sends both streams to a file the operator can write, which the agent
|
||||
// reads back after exit. 2>&1 merges stderr into the same transcript
|
||||
// because ordering between the two matters more than telling them apart
|
||||
// when reconstructing what a script did.
|
||||
var transcriptPath = UserTranscriptPath(script.ScriptId);
|
||||
var quotedTranscript = "\"" + transcriptPath + "\"";
|
||||
TryEnsureTranscriptDirectory(transcriptPath, name);
|
||||
var arguments =
|
||||
"-NoProfile -NonInteractive -ExecutionPolicy Bypass -File " + quotedPath;
|
||||
var workingDirectory = Path.GetDirectoryName(scriptPath) ?? "";
|
||||
"-NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "
|
||||
+ "\"& { & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy "
|
||||
+ "Bypass -File " + quotedPath.Replace("\"", "\\\"")
|
||||
+ " *> " + quotedTranscript.Replace("\"", "\\\"") + "; exit $LASTEXITCODE }\"";
|
||||
if (!TryResolveWorkingDirectory(
|
||||
script, scriptPath, out var workingDirectory, out var directoryError))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"run_script for {Name} not started: {Error}", name, directoryError);
|
||||
return new CommandResult
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = $"Script {name} not run: {directoryError}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null),
|
||||
};
|
||||
}
|
||||
|
||||
var outcome = InteractiveProcessLauncher.LaunchInActiveSession(
|
||||
"powershell.exe", arguments, workingDirectory, waitForExit: true,
|
||||
@@ -1008,7 +1246,8 @@ public sealed class CommandExecutor
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = $"Script {name} could not run in the user session: {outcome.Error}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null),
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null, workingDirectory,
|
||||
ReadAndClearTranscript(transcriptPath, name)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1018,19 +1257,22 @@ public sealed class CommandExecutor
|
||||
// as unresolved rather than success, because we never saw an exit
|
||||
// code and cannot claim it worked.
|
||||
logger.LogWarning(
|
||||
"run_script for {Name} still running after {Timeout}; stopped waiting",
|
||||
name, UserScriptWaitTimeout);
|
||||
"run_script for {Name} still running after {Timeout}; stopped waiting. "
|
||||
+ "Partial output remains at {Transcript} while it runs",
|
||||
name, UserScriptWaitTimeout, transcriptPath);
|
||||
return new CommandResult
|
||||
{
|
||||
Succeeded = false,
|
||||
Message = $"Script {name} did not finish within "
|
||||
+ $"{UserScriptWaitTimeout.TotalMinutes:0} minutes and is still running",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null),
|
||||
Detail = ScriptDetail(name, script, scriptPath, exitCode: null, workingDirectory),
|
||||
};
|
||||
}
|
||||
|
||||
var exit = outcome.ExitCode;
|
||||
var ok = exit is null || exit == 0;
|
||||
var output = ReadAndClearTranscript(transcriptPath, name);
|
||||
LogScriptOutput(name, output, exit);
|
||||
return new CommandResult
|
||||
{
|
||||
Succeeded = ok,
|
||||
@@ -1039,12 +1281,14 @@ public sealed class CommandExecutor
|
||||
: ok
|
||||
? $"Script {name} completed (user context)"
|
||||
: $"Script {name} exited with code {exit}",
|
||||
Detail = ScriptDetail(name, script, scriptPath, exit),
|
||||
Detail = ScriptDetail(name, script, scriptPath, exit, workingDirectory,
|
||||
ok ? null : output),
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> ScriptDetail(
|
||||
string name, ScriptDeliveryClaim? script, string? scriptPath, int? exitCode)
|
||||
string name, ScriptDeliveryClaim? script, string? scriptPath, int? exitCode,
|
||||
string? workingDirectory = null, string? output = null)
|
||||
{
|
||||
var detail = new Dictionary<string, object>
|
||||
{
|
||||
@@ -1056,11 +1300,27 @@ public sealed class CommandExecutor
|
||||
detail["scriptid"] = script.ScriptId;
|
||||
if (!string.IsNullOrWhiteSpace(script.RunContext))
|
||||
detail["runcontext"] = script.RunContext!;
|
||||
if (!string.IsNullOrWhiteSpace(script.ErrorMode))
|
||||
detail["errormode"] = script.ErrorMode!;
|
||||
}
|
||||
if (scriptPath is not null)
|
||||
detail["path"] = scriptPath;
|
||||
// The directory the script REALLY ran in, not the one that was asked
|
||||
// for. An agent too old to know the field reports nothing here, which is
|
||||
// how an operator tells "ignored" apart from "honoured" while agents of
|
||||
// both vintages are in the field.
|
||||
if (!string.IsNullOrWhiteSpace(workingDirectory))
|
||||
detail["workingdirectory"] = workingDirectory!;
|
||||
if (exitCode is not null)
|
||||
detail["exitcode"] = exitCode;
|
||||
// FAILURE ONLY. Callers pass output only when the command failed, so a
|
||||
// successful run's stdout never leaves the endpoint. Script output can
|
||||
// carry whatever the body happened to print - hostnames, paths, tokens
|
||||
// an author echoed while debugging - and there is no reason to accept
|
||||
// that exposure for a run nobody needs to diagnose. The endpoint log
|
||||
// keeps the full copy either way.
|
||||
if (!string.IsNullOrWhiteSpace(output))
|
||||
detail["output"] = output!;
|
||||
return detail;
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,21 @@ public sealed class AppDeliveryClaim
|
||||
// downloads with its collector credential; Sha256 pins the exact bytes;
|
||||
// RunContext is 'system' (the agent's own service account) or 'user' (the
|
||||
// interactive logged-on session), decided per script in the ShopDB library.
|
||||
// WorkingDirectory is the optional folder to run the script IN. Empty means the
|
||||
// agent's staging directory, which is what every script did before the field
|
||||
// existed. It is there so a script can sit beside a corporate tool already
|
||||
// installed on the endpoint and invoke it by relative path; it confers no
|
||||
// rights, since the body is already arbitrary PowerShell that could change its
|
||||
// own directory.
|
||||
// ErrorMode is 'default' or 'strict'. Default is what PowerShell does unaided:
|
||||
// a non-terminating error is written and the script continues, so powershell.exe
|
||||
// still exits 0 and the command reports SUCCEEDED even when the body did none of
|
||||
// its work. Strict makes the agent prepend $ErrorActionPreference = 'Stop' so the
|
||||
// first error terminates and the exit code means what an operator reads it to
|
||||
// mean. Per script, never global: strict changes execution semantics, and a body
|
||||
// written to tolerate a failing cleanup or to probe with a cmdlet that errors on
|
||||
// absence would start aborting halfway. An absent or unrecognised value is
|
||||
// treated as default, so an old server that never sends the field is safe.
|
||||
public sealed class ScriptDeliveryClaim
|
||||
{
|
||||
[JsonPropertyName("scriptid")] public int ScriptId { get; set; }
|
||||
@@ -189,6 +204,8 @@ public sealed class ScriptDeliveryClaim
|
||||
[JsonPropertyName("installpath")] public string? InstallPath { get; set; }
|
||||
[JsonPropertyName("sha256")] public string? Sha256 { get; set; }
|
||||
[JsonPropertyName("runcontext")] public string? RunContext { get; set; }
|
||||
[JsonPropertyName("workingdirectory")] public string? WorkingDirectory { get; set; }
|
||||
[JsonPropertyName("errormode")] public string? ErrorMode { get; set; }
|
||||
}
|
||||
|
||||
// Envelope for the claim long-poll: { status, data: { endpointcommandid, action } }.
|
||||
|
||||
@@ -131,6 +131,21 @@
|
||||
(AgentConfig.LaunchPaths, shipped defaults overridable per site).
|
||||
1.1.0: generic, data-driven service_*/process_* actions carry their
|
||||
target over the wire, so new policies/apps need no agent rebuild. -->
|
||||
<!-- 1.13.0: run_script honors a per-script working directory. The script
|
||||
library gains an optional workingdirectory, the server carries it in the
|
||||
run_script descriptor, and ScriptDeliveryClaim gains WorkingDirectory
|
||||
(AGENT-CONTRACT.md). The point is corporate tooling shipped by Intune: a
|
||||
script that shells out to a vendor CLI needs to run beside that CLI, not
|
||||
in the staging folder. Empty means the existing staging path, so older
|
||||
packages and every existing script keep their current behavior - this is
|
||||
additive, hence minor. Fails closed: a named directory that does not
|
||||
exist on the endpoint aborts the run with a clear error rather than
|
||||
silently falling back to staging, because a script that ran in the wrong
|
||||
folder is worse than one that did not run. The directory confers no
|
||||
rights of its own - it is a cwd, not a grant, and the run still happens
|
||||
in the script's configured context. Server-side validation rejects UNC
|
||||
paths and traversal before the value is ever persisted. No tray change -
|
||||
tray stays 1.4.0. -->
|
||||
<!-- 1.12.0: startup jitter before first contact. Both loops previously made
|
||||
their first request the instant the service started, and the service is
|
||||
installed start=auto, so a patch reboot put the whole fleet on the wire
|
||||
@@ -181,7 +196,25 @@
|
||||
No agent code changes, but deployed together for version consistency. -->
|
||||
<!-- 1.13.5: coordinated release with tray 1.6.6 (no tray changes).
|
||||
No agent code changes, but deployed together for version consistency. -->
|
||||
<Version>1.13.5</Version>
|
||||
<!-- 1.14.0: truthful script verdicts and script output capture. 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 on, and
|
||||
still exits zero. An account-provisioning script therefore reported
|
||||
succeeded having provisioned nothing, and because no output was captured
|
||||
there was no evidence to contradict it. The claim now carries errormode
|
||||
(AGENT-CONTRACT.md 7.4); strict prepends $ErrorActionPreference='Stop'
|
||||
AFTER digest verification so the hash still validates delivered bytes.
|
||||
Default stays permissive so bodies authored against the old semantics
|
||||
keep working. 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 our pipes
|
||||
across the session boundary - always logged locally and returned to the
|
||||
server only on failure. Minor because the claim gains a field and an
|
||||
older agent ignoring it keeps today's behaviour. No tray change - tray
|
||||
stays 1.4.0. -->
|
||||
<!-- 1.14.1: coordinated release with tray 1.6.6 (fleet alert fixes).
|
||||
Merge wilm 1.14.0 changes with wj tray coordination releases. -->
|
||||
<Version>1.14.1</Version>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<SelfContained>true</SelfContained>
|
||||
|
||||
Reference in New Issue
Block a user