This commit is contained in:
2026-09-23 21:20:11 +02:00
parent 4567df7ebb
commit c187b0c45e
14 changed files with 743 additions and 10 deletions
+517
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import base64
import json
import os
import re
@@ -305,6 +306,443 @@ def _process_control_ps_lines(manifest: dict[str, Any], phase: str) -> list[str]
]
INTERACTIVE_LAUNCHER_CSHARP = r"""using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
public sealed class AssetManagerLaunchResult
{
public bool UserFound { get; set; }
public bool Success { get; set; }
public int SessionId { get; set; }
public int ProcessId { get; set; }
public int ErrorCode { get; set; }
public string UserName { get; set; }
public string ErrorMessage { get; set; }
}
public static class AssetManagerInteractiveLauncher
{
private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400;
private const int WTS_CURRENT_SERVER = 0;
private const int WTS_USER_NAME = 5;
private const int WTS_DOMAIN_NAME = 7;
private const int WTS_ACTIVE = 0;
private const uint INVALID_SESSION_ID = 0xFFFFFFFF;
[StructLayout(LayoutKind.Sequential)]
private struct WTS_SESSION_INFO
{
public int SessionID;
public IntPtr pWinStationName;
public int State;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[DllImport("kernel32.dll")]
private static extern uint WTSGetActiveConsoleSessionId();
[DllImport("wtsapi32.dll", SetLastError = true)]
private static extern bool WTSEnumerateSessionsW(
IntPtr hServer,
int Reserved,
int Version,
out IntPtr ppSessionInfo,
out int pCount
);
[DllImport("wtsapi32.dll")]
private static extern void WTSFreeMemory(IntPtr pMemory);
[DllImport("wtsapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WTSQuerySessionInformationW(
IntPtr hServer,
int sessionId,
int wtsInfoClass,
out IntPtr ppBuffer,
out int pBytesReturned
);
[DllImport("wtsapi32.dll", SetLastError = true)]
private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token);
[DllImport("userenv.dll", SetLastError = true)]
private static extern bool CreateEnvironmentBlock(
out IntPtr lpEnvironment,
IntPtr hToken,
bool bInherit
);
[DllImport("userenv.dll", SetLastError = true)]
private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CreateProcessAsUserW(
IntPtr hToken,
string lpApplicationName,
StringBuilder lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation
);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
private static string QuerySessionString(int sessionId, int infoClass)
{
IntPtr buffer = IntPtr.Zero;
int bytes = 0;
try
{
if (!WTSQuerySessionInformationW(
new IntPtr(WTS_CURRENT_SERVER),
sessionId,
infoClass,
out buffer,
out bytes
))
{
return string.Empty;
}
if (buffer == IntPtr.Zero || bytes <= 1)
{
return string.Empty;
}
return Marshal.PtrToStringUni(buffer) ?? string.Empty;
}
finally
{
if (buffer != IntPtr.Zero)
{
WTSFreeMemory(buffer);
}
}
}
private static string SessionUserName(int sessionId)
{
string user = QuerySessionString(sessionId, WTS_USER_NAME).Trim();
if (user.Length == 0)
{
return string.Empty;
}
string domain = QuerySessionString(sessionId, WTS_DOMAIN_NAME).Trim();
return domain.Length == 0 ? user : domain + "\\" + user;
}
private static int FindActiveSession(out string userName)
{
userName = string.Empty;
uint consoleId = WTSGetActiveConsoleSessionId();
IntPtr buffer = IntPtr.Zero;
int count = 0;
if (WTSEnumerateSessionsW(
new IntPtr(WTS_CURRENT_SERVER),
0,
1,
out buffer,
out count
))
{
int firstActiveSession = -1;
string firstActiveUser = string.Empty;
try
{
int size = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
IntPtr current = buffer;
for (int i = 0; i < count; i++)
{
WTS_SESSION_INFO row = (WTS_SESSION_INFO)Marshal.PtrToStructure(
current,
typeof(WTS_SESSION_INFO)
);
current = new IntPtr(current.ToInt64() + size);
if (row.State != WTS_ACTIVE)
{
continue;
}
string user = SessionUserName(row.SessionID);
if (user.Length == 0)
{
continue;
}
if (consoleId != INVALID_SESSION_ID && row.SessionID == (int)consoleId)
{
userName = user;
return row.SessionID;
}
if (firstActiveSession < 0)
{
firstActiveSession = row.SessionID;
firstActiveUser = user;
}
}
if (firstActiveSession >= 0)
{
userName = firstActiveUser;
return firstActiveSession;
}
}
finally
{
if (buffer != IntPtr.Zero)
{
WTSFreeMemory(buffer);
}
}
}
if (consoleId != INVALID_SESSION_ID)
{
string consoleUser = SessionUserName((int)consoleId);
if (consoleUser.Length > 0)
{
userName = consoleUser;
return (int)consoleId;
}
}
return -1;
}
private static string QuoteCommandArgument(string value)
{
return "\"" + (value ?? string.Empty).Replace("\"", "\\\"") + "\"";
}
public static AssetManagerLaunchResult Launch(
string applicationPath,
string arguments,
string workingDirectory
)
{
AssetManagerLaunchResult result = new AssetManagerLaunchResult();
result.UserName = string.Empty;
result.ErrorMessage = string.Empty;
string userName;
int sessionId = FindActiveSession(out userName);
if (sessionId < 0)
{
result.UserFound = false;
result.ErrorMessage = "No active interactive user session found.";
return result;
}
result.UserFound = true;
result.SessionId = sessionId;
result.UserName = userName;
IntPtr userToken = IntPtr.Zero;
IntPtr environment = IntPtr.Zero;
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
try
{
if (!WTSQueryUserToken((uint)sessionId, out userToken))
{
int error = Marshal.GetLastWin32Error();
result.ErrorCode = error;
result.ErrorMessage = "WTSQueryUserToken failed: " + new Win32Exception(error).Message;
return result;
}
if (!CreateEnvironmentBlock(out environment, userToken, false))
{
int error = Marshal.GetLastWin32Error();
result.ErrorCode = error;
result.ErrorMessage = "CreateEnvironmentBlock failed: " + new Win32Exception(error).Message;
return result;
}
STARTUPINFO startup = new STARTUPINFO();
startup.cb = Marshal.SizeOf(typeof(STARTUPINFO));
startup.lpDesktop = "winsta0\\default";
string currentDirectory = workingDirectory;
if (string.IsNullOrWhiteSpace(currentDirectory))
{
currentDirectory = Path.GetDirectoryName(applicationPath);
}
StringBuilder commandLine = new StringBuilder();
commandLine.Append(QuoteCommandArgument(applicationPath));
if (!string.IsNullOrWhiteSpace(arguments))
{
commandLine.Append(" ");
commandLine.Append(arguments);
}
bool created = CreateProcessAsUserW(
userToken,
applicationPath,
commandLine,
IntPtr.Zero,
IntPtr.Zero,
false,
CREATE_UNICODE_ENVIRONMENT,
environment,
currentDirectory,
ref startup,
out processInfo
);
if (!created)
{
int error = Marshal.GetLastWin32Error();
result.ErrorCode = error;
result.ErrorMessage = "CreateProcessAsUser failed: " + new Win32Exception(error).Message;
return result;
}
result.Success = true;
result.ProcessId = processInfo.dwProcessId;
return result;
}
finally
{
if (processInfo.hThread != IntPtr.Zero)
{
CloseHandle(processInfo.hThread);
}
if (processInfo.hProcess != IntPtr.Zero)
{
CloseHandle(processInfo.hProcess);
}
if (environment != IntPtr.Zero)
{
DestroyEnvironmentBlock(environment);
}
if (userToken != IntPtr.Zero)
{
CloseHandle(userToken);
}
}
}
}
"""
def _manifest_bool(value: Any, default: bool = False) -> bool:
if value is None:
return bool(default)
if isinstance(value, str):
return value.strip().lower() in {"1", "true", "yes", "on"}
return bool(value)
def _clean_manifest_text(value: Any, limit: int) -> str:
text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]+", "", str(value or "")).strip()
return text[:limit]
def _post_install_values(manifest: dict[str, Any]) -> tuple[bool, str, str, bool, bool]:
post_install = manifest.get("post_install") or {}
if not isinstance(post_install, dict):
post_install = {}
return (
_manifest_bool(post_install.get("start_application"), False),
_clean_manifest_text(post_install.get("executable"), 1024),
_clean_manifest_text(post_install.get("arguments"), 2048),
_manifest_bool(post_install.get("only_if_user_logged_on"), True),
_manifest_bool(post_install.get("fail_job_on_error"), False),
)
def _interactive_launch_ps_lines(manifest: dict[str, Any]) -> list[str]:
start_application, executable, arguments, only_if_user_logged_on, fail_job_on_error = _post_install_values(manifest)
if not start_application or not executable:
return []
launcher_source = base64.b64encode(INTERACTIVE_LAUNCHER_CSHARP.encode("utf-8")).decode("ascii")
return [
f"$postInstallExecutable = {_ps_quote(executable)}",
f"$postInstallArguments = {_ps_quote(arguments)}",
"$postInstallOnlyIfUserLoggedOn = $" + ("true" if only_if_user_logged_on else "false"),
"$postInstallFailJobOnError = $" + ("true" if fail_job_on_error else "false"),
"$postInstallStartStatus = 'pending'",
"$postInstallStartUser = ''",
"$postInstallStartPid = 0",
"",
"function Invoke-PackagePostInstallStart {",
" if ($rebootRequired) {",
" $script:postInstallStartStatus = 'skipped_reboot'",
" Write-JobLog 'Application start skipped because a reboot is required.' | Out-Null",
" return",
" }",
" $applicationPath = [Environment]::ExpandEnvironmentVariables([string]$postInstallExecutable)",
" if (-not [System.IO.File]::Exists($applicationPath)) {",
" $script:postInstallStartStatus = 'executable_not_found'",
" $message = 'Post-install application executable not found: ' + $applicationPath",
" Write-JobLog $message | Out-Null",
" if ($postInstallFailJobOnError) { throw $message }",
" return",
" }",
" if (-not ([System.Management.Automation.PSTypeName]'AssetManagerInteractiveLauncher').Type) {",
f" $launcherSource = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String({_ps_quote(launcher_source)}))",
" Add-Type -TypeDefinition $launcherSource -Language CSharp -ErrorAction Stop",
" }",
" $workingDirectory = [System.IO.Path]::GetDirectoryName($applicationPath)",
" if ([string]::IsNullOrWhiteSpace($workingDirectory)) { $workingDirectory = $PSScriptRoot }",
" $launchResult = [AssetManagerInteractiveLauncher]::Launch($applicationPath, [string]$postInstallArguments, $workingDirectory)",
" if (-not $launchResult.UserFound) {",
" $script:postInstallStartStatus = 'skipped_no_user'",
" Write-JobLog 'No active interactive user session found. Application start skipped.' | Out-Null",
" if (-not $postInstallOnlyIfUserLoggedOn -and $postInstallFailJobOnError) { throw 'No active interactive user session found for application start.' }",
" return",
" }",
" $script:postInstallStartUser = [string]$launchResult.UserName",
" Write-JobLog ('Logged-on user for application start: ' + [string]$launchResult.UserName + '; session=' + [string]$launchResult.SessionId) | Out-Null",
" if (-not $launchResult.Success) {",
" $script:postInstallStartStatus = 'failed'",
" $message = 'Application start failed: ' + [string]$launchResult.ErrorMessage + '; Win32=' + [string]$launchResult.ErrorCode",
" Write-JobLog $message | Out-Null",
" if ($postInstallFailJobOnError) { throw $message }",
" return",
" }",
" $script:postInstallStartStatus = 'started'",
" $script:postInstallStartPid = [int]$launchResult.ProcessId",
" Write-JobLog ('Application start requested successfully: ' + $applicationPath + '; PID=' + [string]$launchResult.ProcessId) | Out-Null",
"}",
"",
]
def normalize_package_manifest(manifest: dict[str, Any]) -> tuple[dict[str, Any], list[str]]:
if not isinstance(manifest, dict):
raise ValueError("package manifest is not an object")
@@ -361,6 +799,26 @@ def normalize_package_manifest(manifest: dict[str, Any]) -> tuple[dict[str, Any]
arguments = _append_install_argument(arguments, "/FORCECLOSEAPPLICATIONS", r"(^|\s)/(NO)?FORCECLOSEAPPLICATIONS(?=\s|$)")
install["arguments"] = arguments
existing_post_install = result.get("post_install")
start_configured = isinstance(existing_post_install, dict) and "start_application" in existing_post_install
executable_configured = isinstance(existing_post_install, dict) and "executable" in existing_post_install
post_install = result.setdefault("post_install", {})
if not isinstance(post_install, dict):
post_install = {}
result["post_install"] = post_install
if "greenshot" in identity and not start_configured:
post_install["start_application"] = True
notes.append("greenshot_post_install_start")
else:
post_install["start_application"] = _manifest_bool(post_install.get("start_application"), False)
if "greenshot" in identity and not executable_configured:
post_install["executable"] = r"C:\Program Files\Greenshot\Greenshot.exe"
else:
post_install["executable"] = _clean_manifest_text(post_install.get("executable"), 1024)
post_install["arguments"] = _clean_manifest_text(post_install.get("arguments"), 2048)
post_install["only_if_user_logged_on"] = _manifest_bool(post_install.get("only_if_user_logged_on"), True)
post_install["fail_job_on_error"] = _manifest_bool(post_install.get("fail_job_on_error"), False)
return result, notes
def delete_package_storage(package_id: int) -> None:
@@ -794,6 +1252,31 @@ def update_package_process_control(
return manifest
def update_package_post_install(
package_id: int,
start_application: bool,
executable: str,
arguments: str = "",
only_if_user_logged_on: bool = True,
fail_job_on_error: bool = False,
) -> dict[str, Any]:
manifest = load_package_manifest(package_id)
manifest["post_install"] = {
"start_application": bool(start_application),
"executable": _clean_manifest_text(executable, 1024),
"arguments": _clean_manifest_text(arguments, 2048),
"only_if_user_logged_on": bool(only_if_user_logged_on),
"fail_job_on_error": bool(fail_job_on_error),
}
manifest, _notes = normalize_package_manifest(manifest)
manifest_path = package_directory(package_id) / "package.json"
manifest_path.write_text(
json.dumps(manifest, ensure_ascii=True, indent=2) + "\n",
encoding="utf-8",
)
return manifest
def build_deployment_user_script(manifest: dict[str, Any], action: str) -> str:
action = str(action or "").strip().lower()
if action not in {"install", "uninstall", "reinstall"}:
@@ -862,6 +1345,12 @@ def build_deployment_user_script(manifest: dict[str, Any], action: str) -> str:
" return [bool]$last",
"}",
"",
]
if action in {"install", "reinstall"}:
lines += _interactive_launch_ps_lines(manifest)
lines += [
"Write-JobLog (\"Software deployment started: $deploymentPackage; action=$deploymentAction\")",
]
@@ -872,6 +1361,16 @@ def build_deployment_user_script(manifest: dict[str, Any], action: str) -> str:
"$detected = Wait-PackageDetection $true 30",
"if ($detected -eq $false) { throw 'Installation completed but the detection rule did not find the software.' }",
]
if _interactive_launch_ps_lines(manifest):
lines += [
"try { Invoke-PackagePostInstallStart }",
"catch {",
" $script:postInstallStartStatus = 'failed'",
" $message = 'Post-install application start error: ' + [string]$_.Exception.Message",
" Write-JobLog $message | Out-Null",
" if ($postInstallFailJobOnError) { throw }",
"}",
]
elif action == "uninstall":
lines += [
f"$deploymentExitCode = [int](Invoke-PackagePowerShell {ps(uninstall_script)})",
@@ -890,6 +1389,16 @@ def build_deployment_user_script(manifest: dict[str, Any], action: str) -> str:
"$detected = Wait-PackageDetection $true 30",
"if ($detected -eq $false) { throw 'Reinstallation completed but the detection rule did not find the software.' }",
]
if _interactive_launch_ps_lines(manifest):
lines += [
"try { Invoke-PackagePostInstallStart }",
"catch {",
" $script:postInstallStartStatus = 'failed'",
" $message = 'Post-install application start error: ' + [string]$_.Exception.Message",
" Write-JobLog $message | Out-Null",
" if ($postInstallFailJobOnError) { throw }",
"}",
]
lines += [
"$JobResult['deployment_action'] = $deploymentAction",
@@ -897,6 +1406,14 @@ def build_deployment_user_script(manifest: dict[str, Any], action: str) -> str:
"$JobResult['process_exit_code'] = $deploymentExitCode",
"$JobResult['reboot_required'] = $rebootRequired",
"if ($null -ne $detected) { $JobResult['detected_after_action'] = [bool]$detected }",
]
if action in {"install", "reinstall"} and _interactive_launch_ps_lines(manifest):
lines += [
"$JobResult['post_install_start_status'] = $postInstallStartStatus",
"if (-not [string]::IsNullOrWhiteSpace($postInstallStartUser)) { $JobResult['post_install_start_user'] = $postInstallStartUser }",
"if ($postInstallStartPid -gt 0) { $JobResult['post_install_start_pid'] = $postInstallStartPid }",
]
lines += [
"Write-JobLog (\"Software deployment completed: $deploymentPackage; action=$deploymentAction; reboot=$rebootRequired\")",
]
return "\n".join(lines)