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
+22
View File
@@ -1591,3 +1591,25 @@ BASE_TRANSLATIONS.update({
"software_packages.process_control_saved": ("Process control was saved.", "Prozesssteuerung wurde gespeichert."),
"software_packages.process_control_error": ("Process control could not be saved: {error}", "Prozesssteuerung konnte nicht gespeichert werden: {error}"),
})
# v0.5.5.82 post-install application start
BASE_TRANSLATIONS.update({
"setup_analyzer.start_application": ("Start application after successful installation", "Anwendung nach erfolgreicher Installation starten"),
"setup_analyzer.start_application_help": ("Starts the configured application in the active interactive user session after installation detection succeeds. This option is preselected for known applications such as Greenshot.", "Startet die konfigurierte Anwendung nach erfolgreicher Installationserkennung in der aktiven interaktiven Benutzersitzung. Bei bekannten Anwendungen wie Greenshot wird die Option automatisch vorgeschlagen."),
"setup_analyzer.start_executable": ("Application executable", "Programmdatei"),
"setup_analyzer.start_arguments": ("Application arguments", "Programmparameter"),
"setup_analyzer.start_only_if_user_logged_on": ("Start only when an interactive user is logged on", "Nur starten, wenn ein interaktiver Benutzer angemeldet ist"),
"setup_analyzer.start_only_if_user_logged_on_help": ("If no active user session exists, the application start is skipped and the software job can still finish successfully.", "Wenn keine aktive Benutzersitzung vorhanden ist, wird der Programmstart übersprungen und der Softwarejob kann trotzdem erfolgreich beendet werden."),
"setup_analyzer.start_fail_job_on_error": ("Mark the job as failed if application start fails", "Job als fehlgeschlagen markieren, wenn der Programmstart fehlschlägt"),
"software_packages.post_install": ("After successful installation", "Nach erfolgreicher Installation"),
"software_packages.post_install_help": ("Optionally start an application after install or reinstall. Because software jobs run as SYSTEM, AssetManager launches the application explicitly in the active interactive user session.", "Optional eine Anwendung nach Installation oder Neuinstallation starten. Da Softwarejobs unter SYSTEM laufen, startet AssetManager die Anwendung gezielt in der aktiven interaktiven Benutzersitzung."),
"software_packages.start_application": ("Start application automatically", "Anwendung automatisch starten"),
"software_packages.start_executable": ("Application executable", "Programmdatei"),
"software_packages.start_executable_help": ("Use the full executable path, for example C:\\Program Files\\Greenshot\\Greenshot.exe. System environment variables such as %ProgramFiles% are supported.", "Vollständigen Pfad zur EXE angeben, z. B. C:\\Program Files\\Greenshot\\Greenshot.exe. System-Umgebungsvariablen wie %ProgramFiles% werden unterstützt."),
"software_packages.start_arguments": ("Application arguments", "Programmparameter"),
"software_packages.start_only_if_user_logged_on": ("Start only when an interactive user is logged on", "Nur starten, wenn ein interaktiver Benutzer angemeldet ist"),
"software_packages.start_only_if_user_logged_on_help": ("Without an active user session the start is skipped. This does not fail the installation job.", "Ohne aktive Benutzersitzung wird der Start übersprungen. Der Installationsjob wird dadurch nicht als fehlgeschlagen gewertet."),
"software_packages.start_fail_job_on_error": ("Mark the job as failed if the application cannot be started", "Job als fehlgeschlagen markieren, wenn die Anwendung nicht gestartet werden kann"),
"software_packages.post_install_saved": ("Post-install settings were saved.", "Einstellungen nach der Installation wurden gespeichert."),
"software_packages.post_install_error": ("Post-install settings could not be saved: {error}", "Einstellungen nach der Installation konnten nicht gespeichert werden: {error}"),
})
+48 -1
View File
@@ -56,7 +56,7 @@ from .privacy import merge_privacy_settings, localized_privacy_settings, normali
from .privacy_retention import check_retention_category, delete_retention_category, append_deletion_audit, deletion_audit_tail, IMPLEMENTED_RETENTION_KEYS
from .backup import (BACKUP_DIR, BACKUP_INTERVAL_HOURS, BACKUP_RETENTION_DAYS, backup_path, create_backup, delete_backup, list_backups, restore_backup, store_uploaded_backup, automatic_backup_loop, system_storage_information)
from .setup_analyzer import register_setup_analyzer
from .software_packages import delete_package_storage, human_size, load_package_manifest, package_execution_timeout_seconds, package_summary, update_package_process_control
from .software_packages import delete_package_storage, human_size, load_package_manifest, package_execution_timeout_seconds, package_summary, update_package_post_install, update_package_process_control
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
@@ -5981,6 +5981,53 @@ async def software_package_process_control(package_id: int, request: Request, db
)
@app.post("/software/packages/{package_id}/post-install")
async def software_package_post_install(package_id: int, request: Request, db: Session = Depends(get_db)):
_require_admin(request)
package = db.get(SoftwarePackage, package_id)
if not package or package.package_type != "deployment":
raise HTTPException(404, _translate_request(request, "software_packages.not_found", "Software package not found."))
form = await request.form()
start_application = str(form.get("start_application") or "").strip().lower() in {"1", "true", "yes", "on"}
executable = str(form.get("executable") or "")
arguments = str(form.get("arguments") or "")
only_if_user_logged_on = str(form.get("only_if_user_logged_on") or "").strip().lower() in {"1", "true", "yes", "on"}
fail_job_on_error = str(form.get("fail_job_on_error") or "").strip().lower() in {"1", "true", "yes", "on"}
try:
update_package_post_install(
package.id,
start_application=start_application,
executable=executable,
arguments=arguments,
only_if_user_logged_on=only_if_user_logged_on,
fail_job_on_error=fail_job_on_error,
)
except Exception as exc:
logger.exception("Could not update post-install settings for software package %s", package.id)
message = _translate_request(
request,
"software_packages.post_install_error",
"Post-install settings could not be saved: {error}",
error=str(exc),
)
return RedirectResponse(
f"/software/packages/{package.id}?toast_error=" + quote(message),
status_code=303,
)
message = _translate_request(
request,
"software_packages.post_install_saved",
"Post-install settings were saved.",
)
return RedirectResponse(
f"/software/packages/{package.id}?toast_success=" + quote(message),
status_code=303,
)
@app.post("/software/packages/{package_id}/delete")
async def software_package_delete(package_id: int, request: Request, db: Session = Depends(get_db)):
_require_admin(request)
+47 -4
View File
@@ -518,8 +518,11 @@ def analyze_file(path: Path, token: str, size: int, sha256: str) -> dict[str, An
embedded_switches = [marker.decode("ascii") for marker in SWITCH_MARKERS if marker.decode("ascii").lower() in found_markers]
identity_text = f"{filename} {product_name} {manufacturer}".casefold()
suppress_browser_default = "greenshot" in identity_text
process_names_default = "Greenshot.exe" if "greenshot" in identity_text else ""
is_greenshot = "greenshot" in identity_text
suppress_browser_default = is_greenshot
process_names_default = "Greenshot.exe" if is_greenshot else ""
start_application_default = is_greenshot
start_executable_default = r"C:\Program Files\Greenshot\Greenshot.exe" if is_greenshot else ""
analysis = {
"token": token,
"filename": filename,
@@ -543,6 +546,9 @@ def analyze_file(path: Path, token: str, size: int, sha256: str) -> dict[str, An
"embedded_switches": embedded_switches,
"suppress_browser_default": suppress_browser_default,
"process_names_default": process_names_default,
"start_application_default": start_application_default,
"start_executable_default": start_executable_default,
"start_arguments_default": "",
"warning_keys": warning_keys,
"analyzed_at": datetime.now(timezone.utc).isoformat(),
"msiinfo_available": shutil.which("msiinfo") is not None,
@@ -715,6 +721,11 @@ def _export_values(
reboot_codes: str,
suppress_browser: bool = False,
process_names: str = "",
start_application: bool = False,
start_executable: str = "",
start_arguments: str = "",
start_only_if_user_logged_on: bool = True,
start_fail_job_on_error: bool = False,
) -> dict[str, Any]:
return {
"product_name": _form_value(product_name, meta.get("product_name", "")),
@@ -728,6 +739,11 @@ def _export_values(
"reboot_codes": _parse_codes(reboot_codes, meta.get("reboot_codes") or []),
"suppress_browser": bool(suppress_browser),
"process_names": normalize_process_names(process_names),
"start_application": bool(start_application),
"start_executable": _form_value(start_executable, meta.get("start_executable_default", ""))[:1024],
"start_arguments": str(start_arguments or "").strip()[:2048],
"start_only_if_user_logged_on": bool(start_only_if_user_logged_on),
"start_fail_job_on_error": bool(start_fail_job_on_error),
}
@@ -767,6 +783,13 @@ def _build_package_manifest(
"grace_seconds": 5,
"force_close": True,
},
"post_install": {
"start_application": values["start_application"],
"executable": values["start_executable"],
"arguments": values["start_arguments"],
"only_if_user_logged_on": values["start_only_if_user_logged_on"],
"fail_job_on_error": values["start_fail_job_on_error"],
},
"detection": {
"script": "detect.ps1",
"method": detection_method,
@@ -811,10 +834,15 @@ def register_setup_analyzer(app: Any, templates: Any, require_admin: Callable[[R
reboot_codes: str = Form(""),
suppress_browser: bool = Form(False),
process_names: str = Form(""),
start_application: bool = Form(False),
start_executable: str = Form(""),
start_arguments: str = Form(""),
start_only_if_user_logged_on: bool = Form(False),
start_fail_job_on_error: bool = Form(False),
):
require_admin(request)
target, meta = _analysis_file(token)
values = _export_values(meta, product_name, product_version, manufacturer, architecture, install_arguments, timeout_seconds, run_as, success_codes, reboot_codes, suppress_browser, process_names)
values = _export_values(meta, product_name, product_version, manufacturer, architecture, install_arguments, timeout_seconds, run_as, success_codes, reboot_codes, suppress_browser, process_names, start_application, start_executable, start_arguments, start_only_if_user_logged_on, start_fail_job_on_error)
package = _build_package_manifest(target, meta, values)
package, _profile_notes = normalize_package_manifest(package)
script = build_generated_install_script(package)
@@ -837,10 +865,15 @@ def register_setup_analyzer(app: Any, templates: Any, require_admin: Callable[[R
reboot_codes: str = Form(""),
suppress_browser: bool = Form(False),
process_names: str = Form(""),
start_application: bool = Form(False),
start_executable: str = Form(""),
start_arguments: str = Form(""),
start_only_if_user_logged_on: bool = Form(False),
start_fail_job_on_error: bool = Form(False),
):
require_admin(request)
target, meta = _analysis_file(token)
values = _export_values(meta, product_name, product_version, manufacturer, architecture, install_arguments, timeout_seconds, run_as, success_codes, reboot_codes, suppress_browser, process_names)
values = _export_values(meta, product_name, product_version, manufacturer, architecture, install_arguments, timeout_seconds, run_as, success_codes, reboot_codes, suppress_browser, process_names, start_application, start_executable, start_arguments, start_only_if_user_logged_on, start_fail_job_on_error)
package = _build_package_manifest(target, meta, values)
package, _profile_notes = normalize_package_manifest(package)
install_script = build_generated_install_script(package)
@@ -878,6 +911,11 @@ def register_setup_analyzer(app: Any, templates: Any, require_admin: Callable[[R
reboot_codes: str = Form(""),
suppress_browser: bool = Form(False),
process_names: str = Form(""),
start_application: bool = Form(False),
start_executable: str = Form(""),
start_arguments: str = Form(""),
start_only_if_user_logged_on: bool = Form(False),
start_fail_job_on_error: bool = Form(False),
db: Session = Depends(get_db),
):
require_admin(request)
@@ -895,6 +933,11 @@ def register_setup_analyzer(app: Any, templates: Any, require_admin: Callable[[R
reboot_codes,
suppress_browser,
process_names,
start_application,
start_executable,
start_arguments,
start_only_if_user_logged_on,
start_fail_job_on_error,
)
manifest = _build_package_manifest(target, meta, values)
manifest, _profile_notes = normalize_package_manifest(manifest)
+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)
+11
View File
@@ -3230,3 +3230,14 @@ html[data-theme="dark"] .advanced-filter-popup select,html[data-theme="dark"] .a
.software-package-process-form input[type=number]{max-width:180px;}
.software-package-force-close{display:inline-flex!important;flex-direction:row!important;align-items:center!important;justify-content:flex-start!important;gap:.55rem!important;width:auto!important;max-width:100%!important;margin:0!important;}
.software-package-force-close input[type=checkbox]{width:auto!important;flex:0 0 auto!important;margin:0!important;}
/* v0.5.5.82 post-install application start */
.software-package-post-install-panel{margin-top:1rem;}
.software-package-post-install-form{display:flex;flex-direction:column;align-items:flex-start;gap:.75rem;max-width:720px;padding:0;border:0;background:transparent;}
.software-package-post-install-form>label:not(.checkbox-label){display:flex;flex-direction:column;align-items:flex-start;gap:.35rem;width:100%;max-width:720px;}
.software-package-post-install-form input[type=text],
.software-package-post-install-form input:not([type]){width:100%;max-width:720px;box-sizing:border-box;}
.software-package-start-application,
.software-package-start-option{display:inline-flex!important;flex-direction:row!important;align-items:center!important;justify-content:flex-start!important;gap:.55rem!important;width:auto!important;max-width:100%!important;margin:0!important;}
.software-package-start-application input[type=checkbox],
.software-package-start-option input[type=checkbox]{width:auto!important;flex:0 0 auto!important;margin:0!important;}
+24
View File
@@ -119,6 +119,30 @@
<input id="process_names" name="process_names" value="{{ analysis.process_names_default }}" placeholder="Greenshot.exe">
</label>
<small class="muted">{{ t('setup_analyzer.process_names_help') }}</small>
<div class="setup-analyzer-checkboxes">
<label class="checkbox-label" for="start_application">
<input id="start_application" name="start_application" type="checkbox" value="true" {% if analysis.start_application_default %}checked{% endif %}>
<span>{{ t('setup_analyzer.start_application') }}</span>
</label>
<small class="muted">{{ t('setup_analyzer.start_application_help') }}</small>
</div>
<label for="start_executable">{{ t('setup_analyzer.start_executable') }}
<input id="start_executable" name="start_executable" value="{{ analysis.start_executable_default }}" placeholder="C:\Program Files\Application\Application.exe">
</label>
<label for="start_arguments">{{ t('setup_analyzer.start_arguments') }}
<input id="start_arguments" name="start_arguments" value="{{ analysis.start_arguments_default }}">
</label>
<div class="setup-analyzer-checkboxes">
<label class="checkbox-label" for="start_only_if_user_logged_on">
<input id="start_only_if_user_logged_on" name="start_only_if_user_logged_on" type="checkbox" value="true" checked>
<span>{{ t('setup_analyzer.start_only_if_user_logged_on') }}</span>
</label>
<small class="muted">{{ t('setup_analyzer.start_only_if_user_logged_on_help') }}</small>
<label class="checkbox-label" for="start_fail_job_on_error">
<input id="start_fail_job_on_error" name="start_fail_job_on_error" type="checkbox" value="true">
<span>{{ t('setup_analyzer.start_fail_job_on_error') }}</span>
</label>
</div>
</section>
</div>
+30
View File
@@ -39,6 +39,8 @@
<dt>{{ t('setup_analyzer.reboot_codes') }}</dt><dd>{{ manifest.install.reboot_codes|join(', ') }}</dd>
<dt>{{ t('setup_analyzer.suppress_browser') }}</dt><dd>{{ t('common.yes') if manifest.install.suppress_browser else t('common.no') }}</dd>
<dt>{{ t('software_packages.process_names') }}</dt><dd>{% if manifest.process_control.process_names %}<code>{{ manifest.process_control.process_names|join(', ') }}</code>{% else %}—{% endif %}</dd>
<dt>{{ t('software_packages.start_application') }}</dt><dd>{{ t('common.yes') if manifest.post_install.start_application else t('common.no') }}</dd>
{% if manifest.post_install.start_application %}<dt>{{ t('software_packages.start_executable') }}</dt><dd><code>{{ manifest.post_install.executable or '—' }}</code></dd>{% endif %}
<dt>{{ t('setup_analyzer.detection_method') }}</dt><dd><code>{{ manifest.detection.method }}</code></dd>
</dl>
</section>
@@ -63,6 +65,34 @@
</form>
</section>
<section class="panel software-package-post-install-panel">
<h2>{{ t('software_packages.post_install') }}</h2>
<p class="muted">{{ t('software_packages.post_install_help') }}</p>
<form method="post" action="/software/packages/{{ package.id }}/post-install" class="software-package-post-install-form">
<label class="checkbox-label software-package-start-application" for="package_start_application">
<input id="package_start_application" name="start_application" type="checkbox" value="true" {% if manifest.post_install.start_application %}checked{% endif %}>
<span>{{ t('software_packages.start_application') }}</span>
</label>
<label for="package_start_executable">{{ t('software_packages.start_executable') }}
<input id="package_start_executable" name="executable" value="{{ manifest.post_install.executable }}" placeholder="C:\Program Files\Greenshot\Greenshot.exe">
</label>
<small class="muted">{{ t('software_packages.start_executable_help') }}</small>
<label for="package_start_arguments">{{ t('software_packages.start_arguments') }}
<input id="package_start_arguments" name="arguments" value="{{ manifest.post_install.arguments }}">
</label>
<label class="checkbox-label software-package-start-option" for="package_start_only_if_user">
<input id="package_start_only_if_user" name="only_if_user_logged_on" type="checkbox" value="true" {% if manifest.post_install.only_if_user_logged_on %}checked{% endif %}>
<span>{{ t('software_packages.start_only_if_user_logged_on') }}</span>
</label>
<small class="muted">{{ t('software_packages.start_only_if_user_logged_on_help') }}</small>
<label class="checkbox-label software-package-start-option" for="package_start_fail_job">
<input id="package_start_fail_job" name="fail_job_on_error" type="checkbox" value="true" {% if manifest.post_install.fail_job_on_error %}checked{% endif %}>
<span>{{ t('software_packages.start_fail_job_on_error') }}</span>
</label>
<button class="button button-secondary" type="submit">{{ t('common.save') }}</button>
</form>
</section>
<section class="panel">
<h2>{{ t('software_packages.create_jobs') }}</h2>
<p class="muted">{{ t('software_packages.create_jobs_help') }}</p>
+1 -1
View File
@@ -1,2 +1,2 @@
APP_VERSION = "0.5.5.81"
APP_VERSION = "0.5.5.83"
__version__ = APP_VERSION