Update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# AssetManager 0.5.5.81
|
||||
# AssetManager 0.5.5.83
|
||||
|
||||
AssetManager is a self-hosted web application for managing IT equipment and other organizational assets. It provides asset inventory, software inventory, remote job execution, reporting, privacy/retention documentation, and optional integration with MeshCentral.
|
||||
|
||||
@@ -10,7 +10,7 @@ The project is licensed under the **Apache License 2.0** and may be used, modifi
|
||||
- asset lists, detail views, history, bulk editing, Excel import/export, duplicate merging, and tree views
|
||||
- hardware and software inventory with comparison and aggregation views
|
||||
- static Setup Analyzer for Windows installer technology, silent parameters, deployment-script export, and persistent software-package creation
|
||||
- install, uninstall, and reinstall software-package jobs with MeshCentral file transfer and callback tracking
|
||||
- install, uninstall, and reinstall software-package jobs with MeshCentral file transfer, callback tracking, process control, and optional post-install application launch
|
||||
- job definitions and remote job execution with status tracking, callbacks, retries, and logs
|
||||
- optional MeshCentral integration through the MeshCtrl command-line interface
|
||||
- local users, optional LDAP/Active Directory authentication, and a protected local emergency administrator
|
||||
|
||||
+22
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -1,2 +1,2 @@
|
||||
APP_VERSION = "0.5.5.81"
|
||||
APP_VERSION = "0.5.5.83"
|
||||
__version__ = APP_VERSION
|
||||
|
||||
@@ -36,6 +36,14 @@ Version 0.5.5.77 can also create a persistent AssetManager software package dire
|
||||
|
||||
Software-package jobs transfer only the files required for the selected action through MeshCentral, run the generated PowerShell wrapper, and use the normal AssetManager callback/status/retry infrastructure.
|
||||
|
||||
## Post-install application start
|
||||
|
||||
A package can optionally start an application after a successful install or reinstall. The launch happens only after the package detection rule confirms that the software is installed. Because deployment jobs execute as SYSTEM, AssetManager uses the active interactive Windows session and the logged-on user's session token to create the configured application there.
|
||||
|
||||
If no interactive user session is active and **Start only when an interactive user is logged on** is enabled, the application start is skipped without failing the installation job. Application launch is also skipped when the installer reports that a reboot is required.
|
||||
|
||||
For known packages, Setup Analyzer can prefill the application path. Greenshot uses `C:\Program Files\Greenshot\Greenshot.exe`.
|
||||
|
||||
## Installer process handling
|
||||
|
||||
Generated installation scripts no longer use `Start-Process -Wait` for the installer. They start the installer process, wait only for that specific PID with a configurable timeout, and then evaluate its exit code. This prevents a browser or another long-running child process launched by the installer from keeping the PowerShell script open indefinitely.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
- [0.5.5.83](UPDATE-0.5.5.83.md) - fix Windows PowerShell compatibility for post-install application start.
|
||||
- [0.5.5.82](UPDATE-0.5.5.82.md) - add optional post-install application launch in the active interactive user session.
|
||||
- [0.5.5.81](UPDATE-0.5.5.81.md) - fix generated PowerShell process-control syntax for software jobs.
|
||||
- [0.5.5.80](UPDATE-0.5.5.80.md) - add configurable process control before install/uninstall and improve Inno application closing.
|
||||
- [0.5.5.79](UPDATE-0.5.5.79.md) - fix Greenshot SYSTEM deployment and add software-package deletion.
|
||||
@@ -17,7 +19,7 @@
|
||||
|
||||
Release notes are stored outside the project root to keep the repository overview compact.
|
||||
|
||||
The current release is **0.5.5.81**.
|
||||
The current release is **0.5.5.83**.
|
||||
|
||||
Older notes are concise English summaries migrated from the original release documents. Git history remains authoritative for exact implementation details.
|
||||
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
# Update 0.5.5.82
|
||||
|
||||
## Added
|
||||
|
||||
- Optional application start after successful install and reinstall software-package jobs.
|
||||
- Per-package executable path and optional application arguments.
|
||||
- Interactive-session launch from SYSTEM jobs using the logged-on user's Windows session token.
|
||||
- Optional behavior for machines without an active interactive user session.
|
||||
- Optional job failure when a configured post-install application cannot be started.
|
||||
- Setup Analyzer suggestions for the post-install application of known packages such as Greenshot.
|
||||
|
||||
## Behavior
|
||||
|
||||
Application launch occurs only after the installation command has completed and the package detection rule reports the software as installed. If the installer reports that a reboot is required, application launch is skipped.
|
||||
|
||||
Software jobs themselves continue to run as SYSTEM. The optional application is created in the active interactive Windows session of the logged-on user.
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
# Update 0.5.5.83
|
||||
|
||||
## Fixed
|
||||
|
||||
- Replaced the incompatible `Split-Path -LiteralPath ... -Parent` call in the generated post-install launcher.
|
||||
- Uses `System.IO.Path.GetDirectoryName()` for the application working directory.
|
||||
- Uses `System.IO.File.Exists()` for the post-install executable check.
|
||||
- The complete optional application-start call is now protected by error handling.
|
||||
- A start failure no longer changes a successful installation into a failed software job unless `fail_job_on_error` is explicitly enabled.
|
||||
|
||||
## Result
|
||||
|
||||
The install/reinstall result is determined by installer exit code and package detection. Optional application start is handled separately and logged.
|
||||
Reference in New Issue
Block a user