mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-19 01:31:28 -05:00
2741 lines
94 KiB
C
2741 lines
94 KiB
C
/*
|
|
* appsandbox-agent.exe — Guest-side Windows service for AppSandbox.
|
|
*
|
|
* Listens on a Hyper-V socket (AF_HYPERV) for commands from the host.
|
|
* Maintains a persistent connection: sends "hello" on connect, then
|
|
* periodic "heartbeat" messages. Processes commands inline.
|
|
*
|
|
* Also handles GPU driver file copy via embedded 9P client (p9copy)
|
|
* when the host sends gpu_query_response with share metadata.
|
|
*
|
|
* Supports: ping, shutdown, restart, gpu_copy, gpu_query_response,
|
|
* gpu_none, idd_connect, set_ip.
|
|
*
|
|
* Usage:
|
|
* appsandbox-agent.exe --install Install and start the service
|
|
* appsandbox-agent.exe --remove Stop and remove the service
|
|
* (no args) Run as Windows service (SCM only)
|
|
*/
|
|
|
|
#include <winsock2.h>
|
|
#include <windows.h>
|
|
#include <iphlpapi.h>
|
|
#include <setupapi.h>
|
|
#include <cfgmgr32.h>
|
|
#include <wtsapi32.h>
|
|
#include <userenv.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include "p9copy.h"
|
|
#include "gl_vk_provision.h"
|
|
#include "../transport/asb_transport.h"
|
|
|
|
#pragma comment(lib, "ws2_32.lib")
|
|
#pragma comment(lib, "iphlpapi.lib")
|
|
#pragma comment(lib, "advapi32.lib")
|
|
#pragma comment(lib, "setupapi.lib")
|
|
#pragma comment(lib, "cfgmgr32.lib")
|
|
#pragma comment(lib, "wtsapi32.lib")
|
|
#pragma comment(lib, "userenv.lib")
|
|
|
|
/* GUID_DEVCLASS_DISPLAY = {4D36E968-E325-11CE-BFC1-08002BE10318} */
|
|
static const GUID GUID_DISPLAY_CLASS =
|
|
{ 0x4d36e968, 0xe325, 0x11ce, { 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18 } };
|
|
|
|
/* DEVPKEY_Device_DriverInfPath = {A8B865DD-2E3D-4094-AD97-E593A70C75D6}, 5 */
|
|
static const DEVPROPKEY DPKEY_DriverInfPath =
|
|
{ { 0xa8b865dd, 0x2e3d, 0x4094, { 0xad, 0x97, 0xe5, 0x93, 0xa7, 0x0c, 0x75, 0xd6 } }, 5 };
|
|
|
|
/* ---- Hyper-V socket definitions ---- */
|
|
|
|
#define AF_HYPERV 34
|
|
|
|
typedef struct _SOCKADDR_HV {
|
|
ADDRESS_FAMILY Family;
|
|
USHORT Reserved;
|
|
GUID VmId;
|
|
GUID ServiceId;
|
|
} SOCKADDR_HV;
|
|
|
|
/* HV_GUID_WILDCARD = {00000000-0000-0000-0000-000000000000} */
|
|
static const GUID HV_GUID_WILDCARD =
|
|
{ 0, 0, 0, { 0, 0, 0, 0, 0, 0, 0, 0 } };
|
|
|
|
/* Must match VM_AGENT_SERVICE_GUID_STR in vm_agent.h
|
|
{A5B0CAFE-0001-4000-8000-000000000001} */
|
|
static const GUID AGENT_SERVICE_GUID =
|
|
{ 0xa5b0cafe, 0x0001, 0x4000, { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 } };
|
|
|
|
/* ---- Service constants ---- */
|
|
|
|
#define SERVICE_NAME "AppSandboxAgent"
|
|
#define DISPLAY_NAME "AppSandbox Guest Agent"
|
|
|
|
static SERVICE_STATUS g_status;
|
|
static SERVICE_STATUS_HANDLE g_status_handle;
|
|
static HANDLE g_stop_event;
|
|
static AsbConn * g_client_sock = NULL; /* Active persistent connection */
|
|
static volatile BOOL g_os_shutting_down = FALSE;
|
|
static CRITICAL_SECTION g_send_cs; /* Protects send_line from concurrent callers */
|
|
|
|
/* ---- Logging ---- */
|
|
|
|
static int send_line(AsbConn *s, const char *msg);
|
|
|
|
static void agent_log(const char *fmt, ...)
|
|
{
|
|
FILE *f;
|
|
va_list ap;
|
|
SYSTEMTIME st;
|
|
|
|
if (fopen_s(&f, "C:\\Windows\\AppSandbox\\agent.log", "a") != 0 || !f)
|
|
return;
|
|
GetLocalTime(&st);
|
|
fprintf(f, "[%04d-%02d-%02d %02d:%02d:%02d] ",
|
|
st.wYear, st.wMonth, st.wDay,
|
|
st.wHour, st.wMinute, st.wSecond);
|
|
va_start(ap, fmt);
|
|
vfprintf(f, fmt, ap);
|
|
va_end(ap);
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
}
|
|
|
|
/* Send a log message to the host via the command channel (non-recursive, won't call agent_log) */
|
|
static void agent_log_to_host(const char *fmt, ...)
|
|
{
|
|
char msg[1024];
|
|
char line[1040];
|
|
va_list ap;
|
|
|
|
if (g_client_sock == NULL) return;
|
|
|
|
va_start(ap, fmt);
|
|
vsnprintf(msg, sizeof(msg), fmt, ap);
|
|
va_end(ap);
|
|
msg[sizeof(msg) - 1] = '\0';
|
|
|
|
snprintf(line, sizeof(line), "log:%s", msg);
|
|
line[sizeof(line) - 1] = '\0';
|
|
send_line(g_client_sock, line);
|
|
}
|
|
|
|
/* ---- p9copy log adapter ---- */
|
|
|
|
/* Wraps agent_log for p9copy's P9LogFn signature (identical, but explicit). */
|
|
static void p9copy_log(const char *fmt, ...)
|
|
{
|
|
FILE *f;
|
|
va_list ap;
|
|
SYSTEMTIME st;
|
|
|
|
if (fopen_s(&f, "C:\\Windows\\AppSandbox\\agent.log", "a") != 0 || !f)
|
|
return;
|
|
GetLocalTime(&st);
|
|
fprintf(f, "[%04d-%02d-%02d %02d:%02d:%02d] ",
|
|
st.wYear, st.wMonth, st.wDay,
|
|
st.wHour, st.wMinute, st.wSecond);
|
|
va_start(ap, fmt);
|
|
vfprintf(f, fmt, ap);
|
|
va_end(ap);
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
}
|
|
|
|
/* ---- GPU copy state ---- */
|
|
|
|
#define MAX_GPU_SHARES 64
|
|
|
|
typedef struct {
|
|
char share_name[128];
|
|
char dest_path[512]; /* wchar_t as UTF-8 — converted when used */
|
|
char filter[4096];
|
|
} GpuShareInfo;
|
|
|
|
typedef struct {
|
|
GpuShareInfo shares[MAX_GPU_SHARES];
|
|
int count;
|
|
AsbConn * notify_sock; /* Socket to send progress/done to host */
|
|
volatile BOOL copying;
|
|
} GpuCopyState;
|
|
|
|
static GpuCopyState g_gpu_copy = {0};
|
|
|
|
/* Forward declarations — defined later but needed by GPU copy */
|
|
static int recv_line(AsbConn *s, char *buf, int buf_size);
|
|
static BOOL enable_privilege(LPCWSTR priv_name);
|
|
|
|
/* ---- GPU copy background thread ---- */
|
|
|
|
/* Check if any Display class device using vrd.inf has error code 43.
|
|
Returns TRUE if at least one vrd.inf device has problem 43. */
|
|
static BOOL check_gpu_error43(void)
|
|
{
|
|
HDEVINFO devs;
|
|
SP_DEVINFO_DATA dev_info;
|
|
DWORD idx;
|
|
BOOL found_error43 = FALSE;
|
|
|
|
devs = SetupDiGetClassDevsW(&GUID_DISPLAY_CLASS, NULL, NULL, DIGCF_PRESENT);
|
|
if (devs == INVALID_HANDLE_VALUE) {
|
|
agent_log("GPU check: SetupDiGetClassDevs failed (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
dev_info.cbSize = sizeof(dev_info);
|
|
for (idx = 0; SetupDiEnumDeviceInfo(devs, idx, &dev_info); idx++) {
|
|
DEVPROPTYPE prop_type;
|
|
wchar_t inf_path[MAX_PATH] = {0};
|
|
ULONG status = 0, problem = 0;
|
|
CONFIGRET cr;
|
|
wchar_t dev_id[512] = {0};
|
|
|
|
CM_Get_Device_IDW(dev_info.DevInst, dev_id, 512, 0);
|
|
|
|
/* Get driver inf path */
|
|
if (!SetupDiGetDevicePropertyW(devs, &dev_info, &DPKEY_DriverInfPath,
|
|
&prop_type, (PBYTE)inf_path,
|
|
sizeof(inf_path), NULL, 0)) {
|
|
continue;
|
|
}
|
|
|
|
/* Only care about vrd.inf (GPU-PV virtual render driver) */
|
|
if (_wcsicmp(inf_path, L"vrd.inf") != 0)
|
|
continue;
|
|
|
|
agent_log("GPU check: found vrd.inf device: %ls", dev_id);
|
|
|
|
cr = CM_Get_DevNode_Status(&status, &problem, dev_info.DevInst, 0);
|
|
if (cr != CR_SUCCESS) {
|
|
agent_log("GPU check: CM_Get_DevNode_Status failed (%lu) for %ls.", cr, dev_id);
|
|
continue;
|
|
}
|
|
|
|
if (problem == 43) {
|
|
agent_log("GPU check: device %ls has error 43.", dev_id);
|
|
found_error43 = TRUE;
|
|
} else {
|
|
agent_log("GPU check: device %ls OK (status=0x%lx, problem=%lu).",
|
|
dev_id, status, problem);
|
|
}
|
|
}
|
|
|
|
SetupDiDestroyDeviceInfoList(devs);
|
|
return found_error43;
|
|
}
|
|
|
|
static void disable_hyperv_video(AsbConn *notify_sock);
|
|
|
|
|
|
/* Disable and re-enable vrd.inf display devices.
|
|
If only_error43 is TRUE, only cycles devices with problem code 43.
|
|
If FALSE, cycles all vrd.inf devices (used at preshutdown). */
|
|
static void cycle_gpu_devices(AsbConn *notify_sock, BOOL only_error43)
|
|
{
|
|
HDEVINFO devs;
|
|
SP_DEVINFO_DATA dev_info;
|
|
DWORD idx;
|
|
char msg[512];
|
|
|
|
devs = SetupDiGetClassDevsW(&GUID_DISPLAY_CLASS, NULL, NULL, DIGCF_PRESENT);
|
|
if (devs == INVALID_HANDLE_VALUE) return;
|
|
|
|
dev_info.cbSize = sizeof(dev_info);
|
|
for (idx = 0; SetupDiEnumDeviceInfo(devs, idx, &dev_info); idx++) {
|
|
DEVPROPTYPE prop_type;
|
|
wchar_t inf_path[MAX_PATH] = {0};
|
|
ULONG status = 0, problem = 0;
|
|
CONFIGRET cr;
|
|
wchar_t dev_id[512] = {0};
|
|
|
|
CM_Get_Device_IDW(dev_info.DevInst, dev_id, 512, 0);
|
|
|
|
if (!SetupDiGetDevicePropertyW(devs, &dev_info, &DPKEY_DriverInfPath,
|
|
&prop_type, (PBYTE)inf_path,
|
|
sizeof(inf_path), NULL, 0))
|
|
continue;
|
|
|
|
if (_wcsicmp(inf_path, L"vrd.inf") != 0)
|
|
continue;
|
|
|
|
if (only_error43) {
|
|
cr = CM_Get_DevNode_Status(&status, &problem, dev_info.DevInst, 0);
|
|
if (cr != CR_SUCCESS || problem != 43)
|
|
continue;
|
|
}
|
|
|
|
agent_log("GPU cycle: disabling %ls...", dev_id);
|
|
sprintf_s(msg, sizeof(msg), "gpu_device_status:disabling %ls", dev_id);
|
|
if (notify_sock != NULL) send_line(notify_sock, msg);
|
|
|
|
cr = CM_Disable_DevNode(dev_info.DevInst, 0);
|
|
if (cr != CR_SUCCESS) {
|
|
agent_log("GPU cycle: CM_Disable_DevNode failed (%lu).", cr);
|
|
sprintf_s(msg, sizeof(msg), "gpu_device_status:disable failed (%lu)", cr);
|
|
if (notify_sock != NULL) send_line(notify_sock, msg);
|
|
continue;
|
|
}
|
|
|
|
Sleep(1000);
|
|
|
|
agent_log("GPU cycle: re-enabling %ls...", dev_id);
|
|
sprintf_s(msg, sizeof(msg), "gpu_device_status:re-enabling %ls", dev_id);
|
|
if (notify_sock != NULL) send_line(notify_sock, msg);
|
|
|
|
cr = CM_Enable_DevNode(dev_info.DevInst, 0);
|
|
if (cr != CR_SUCCESS) {
|
|
agent_log("GPU cycle: CM_Enable_DevNode failed (%lu).", cr);
|
|
sprintf_s(msg, sizeof(msg), "gpu_device_status:enable failed (%lu)", cr);
|
|
if (notify_sock != NULL) send_line(notify_sock, msg);
|
|
continue;
|
|
}
|
|
|
|
agent_log("GPU cycle: device %ls re-enabled.", dev_id);
|
|
sprintf_s(msg, sizeof(msg), "gpu_device_status:re-enabled %ls", dev_id);
|
|
if (notify_sock != NULL) send_line(notify_sock, msg);
|
|
}
|
|
|
|
SetupDiDestroyDeviceInfoList(devs);
|
|
}
|
|
|
|
/* WTS constants — defined manually to avoid wtsapi32.h dependency (dynamic load) */
|
|
#define MY_WTS_USERNAME 5 /* WTS_INFO_CLASS value for WTSUserName */
|
|
|
|
/* Wait for a real user to log in (not defaultuser0 or SYSTEM).
|
|
Polls WTS sessions every 2 seconds up to timeout_ms.
|
|
Returns the session ID, or 0xFFFFFFFF on timeout. */
|
|
static DWORD wait_for_user_login(AsbConn *notify_sock, int timeout_ms,
|
|
const char *notify_msg)
|
|
{
|
|
typedef BOOL (WINAPI *PFN_WTSQuerySessionInformationW)(
|
|
HANDLE, DWORD, DWORD, LPWSTR *, DWORD *);
|
|
typedef void (WINAPI *PFN_WTSFreeMemory)(PVOID);
|
|
|
|
HMODULE wts = LoadLibraryW(L"wtsapi32.dll");
|
|
PFN_WTSQuerySessionInformationW pfnQuery;
|
|
PFN_WTSFreeMemory pfnFree;
|
|
DWORD start, now;
|
|
|
|
if (!wts) {
|
|
agent_log("wait_for_user_login: cannot load wtsapi32.dll.");
|
|
return 0xFFFFFFFF;
|
|
}
|
|
|
|
pfnQuery = (PFN_WTSQuerySessionInformationW)
|
|
GetProcAddress(wts, "WTSQuerySessionInformationW");
|
|
pfnFree = (PFN_WTSFreeMemory)GetProcAddress(wts, "WTSFreeMemory");
|
|
|
|
if (!pfnQuery || !pfnFree) {
|
|
agent_log("wait_for_user_login: WTS functions not found.");
|
|
FreeLibrary(wts);
|
|
return 0xFFFFFFFF;
|
|
}
|
|
|
|
agent_log("Waiting for user login...");
|
|
if (notify_sock != NULL && notify_msg)
|
|
send_line(notify_sock, notify_msg);
|
|
|
|
start = GetTickCount();
|
|
|
|
for (;;) {
|
|
DWORD session_id = WTSGetActiveConsoleSessionId();
|
|
if (session_id != 0xFFFFFFFF) {
|
|
LPWSTR username = NULL;
|
|
DWORD size = 0;
|
|
|
|
if (pfnQuery(NULL, session_id, MY_WTS_USERNAME, &username, &size)) {
|
|
if (username && wcslen(username) > 0 &&
|
|
_wcsicmp(username, L"defaultuser0") != 0 &&
|
|
_wcsicmp(username, L"SYSTEM") != 0) {
|
|
agent_log("User '%ls' logged in (session %lu).", username, session_id);
|
|
pfnFree(username);
|
|
FreeLibrary(wts);
|
|
return session_id;
|
|
}
|
|
if (username) pfnFree(username);
|
|
}
|
|
}
|
|
|
|
now = GetTickCount();
|
|
if ((now - start) >= (DWORD)timeout_ms) {
|
|
agent_log("wait_for_user_login: timed out after %d ms.", timeout_ms);
|
|
FreeLibrary(wts);
|
|
return 0xFFFFFFFF;
|
|
}
|
|
|
|
Sleep(2000);
|
|
}
|
|
}
|
|
|
|
/* Log off a specific user session via WTSLogoffSession. */
|
|
static BOOL logoff_session(DWORD session_id)
|
|
{
|
|
typedef BOOL (WINAPI *PFN_WTSLogoffSession)(HANDLE, DWORD, BOOL);
|
|
|
|
HMODULE wts = LoadLibraryW(L"wtsapi32.dll");
|
|
PFN_WTSLogoffSession pfnLogoff;
|
|
BOOL result;
|
|
|
|
if (!wts) return FALSE;
|
|
|
|
pfnLogoff = (PFN_WTSLogoffSession)GetProcAddress(wts, "WTSLogoffSession");
|
|
if (!pfnLogoff) {
|
|
FreeLibrary(wts);
|
|
return FALSE;
|
|
}
|
|
|
|
agent_log("Logging off session %lu...", session_id);
|
|
result = pfnLogoff(NULL, session_id, TRUE /* bWait */);
|
|
if (!result)
|
|
agent_log("WTSLogoffSession failed: %lu", GetLastError());
|
|
else
|
|
agent_log("Session %lu logged off.", session_id);
|
|
|
|
FreeLibrary(wts);
|
|
return result;
|
|
}
|
|
|
|
/* Run a command hidden and wait for it (used for takeown/icacls). The cmd buffer
|
|
must be writable (CreateProcessW may modify it). */
|
|
static DWORD run_quiet(wchar_t *cmd)
|
|
{
|
|
STARTUPINFOW si = { sizeof(si) };
|
|
PROCESS_INFORMATION pi = { 0 };
|
|
DWORD result;
|
|
si.dwFlags = STARTF_USESHOWWINDOW;
|
|
si.wShowWindow = SW_HIDE;
|
|
if (CreateProcessW(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW,
|
|
NULL, NULL, &si, &pi)) {
|
|
DWORD wait = WaitForSingleObject(pi.hProcess, 30000);
|
|
if (wait == WAIT_OBJECT_0) {
|
|
if (!GetExitCodeProcess(pi.hProcess, &result))
|
|
result = GetLastError();
|
|
} else {
|
|
result = wait == WAIT_TIMEOUT ? ERROR_TIMEOUT : GetLastError();
|
|
}
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
} else {
|
|
result = GetLastError();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* Deploy the AppSandbox public key into administrators_authorized_keys -- the file
|
|
Windows OpenSSH uses for members of the Administrators group (the guest account
|
|
is an admin). sshd's StrictModes requires that file be writable only by the
|
|
Administrators group + SYSTEM with inheritance removed, so we fix the ACL too. */
|
|
static BOOL deploy_ssh_key(const char *pubkey)
|
|
{
|
|
wchar_t base[MAX_PATH], ssh_dir[MAX_PATH], authk[MAX_PATH], cmd[MAX_PATH + 256];
|
|
FILE *f = NULL;
|
|
|
|
if (!pubkey || !*pubkey) return FALSE;
|
|
if (!GetEnvironmentVariableW(L"ProgramData", base, MAX_PATH))
|
|
wcscpy_s(base, MAX_PATH, L"C:\\ProgramData");
|
|
swprintf_s(ssh_dir, MAX_PATH, L"%s\\ssh", base);
|
|
swprintf_s(authk, MAX_PATH, L"%s\\administrators_authorized_keys", ssh_dir);
|
|
CreateDirectoryW(ssh_dir, NULL);
|
|
|
|
if (_wfopen_s(&f, authk, L"w") != 0 || !f) {
|
|
agent_log("SSH key: cannot open authorized_keys (%lu)", GetLastError());
|
|
return FALSE;
|
|
}
|
|
fprintf(f, "%s\n", pubkey);
|
|
fclose(f);
|
|
|
|
swprintf_s(cmd, MAX_PATH + 256,
|
|
L"icacls \"%s\" /inheritance:r /grant *S-1-5-32-544:F /grant *S-1-5-18:F",
|
|
authk);
|
|
run_quiet(cmd);
|
|
agent_log("SSH key: deployed to administrators_authorized_keys");
|
|
return TRUE;
|
|
}
|
|
|
|
static void gl_provision(const wchar_t *dir, const wchar_t *native_dir)
|
|
{
|
|
wchar_t path[MAX_PATH], sys[MAX_PATH], dst[MAX_PATH];
|
|
HKEY key;
|
|
DWORD zero = 0;
|
|
BOOL native_runtime = FALSE;
|
|
|
|
agent_log("GL: provisioning GPU runtimes from %ls", dir[0] ? dir : native_dir);
|
|
|
|
/* dxil.dll -> System32 (OpenGLOn12 / vulkan_dzn load it by leaf name). */
|
|
if (dir[0] && GetSystemDirectoryW(sys, MAX_PATH)) {
|
|
swprintf_s(path, MAX_PATH, L"%s\\dxil.dll", dir);
|
|
swprintf_s(dst, MAX_PATH, L"%s\\dxil.dll", sys);
|
|
if (GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES) {
|
|
if (CopyFileW(path, dst, FALSE))
|
|
agent_log("GL: staged dxil.dll to System32.");
|
|
else
|
|
agent_log("GL: dxil.dll -> System32 failed (%lu).", GetLastError());
|
|
}
|
|
}
|
|
|
|
if (GetSystemDirectoryW(sys, MAX_PATH)) {
|
|
if (gl_vk_provision_runtime(dir, native_dir, sys, &native_runtime))
|
|
agent_log("GL: OpenGL runtime provisioned in System32.");
|
|
else
|
|
agent_log("GL: opengl32 -> System32 failed (%lu).", GetLastError());
|
|
} else {
|
|
agent_log("GL: GetSystemDirectory failed; OpenGL not deployed.");
|
|
}
|
|
|
|
/* OpenCL: Khronos vendor key — value name = ICD path, data 0 (= load it). */
|
|
if (dir[0]) {
|
|
swprintf_s(path, MAX_PATH, L"%s\\OpenCLOn12.dll", dir);
|
|
if (GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES) {
|
|
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Khronos\\OpenCL\\Vendors",
|
|
0, NULL, 0, KEY_SET_VALUE, NULL, &key, NULL) == ERROR_SUCCESS) {
|
|
RegSetValueExW(key, path, 0, REG_DWORD, (const BYTE *)&zero, sizeof(zero));
|
|
RegCloseKey(key);
|
|
agent_log("GL: registered OpenCL ICD %ls", path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Vulkan: Khronos driver key — value name = ICD manifest path.
|
|
The manifest's library_path is the absolute guest DLL path (set host-side
|
|
in d3dlayers.c) so the loader can LoadLibraryEx it under
|
|
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, which requires a fully-qualified path. */
|
|
if (dir[0]) {
|
|
swprintf_s(path, MAX_PATH, L"%s\\dzn_icd.json", dir);
|
|
} else {
|
|
UINT length = GetWindowsDirectoryW(dst, MAX_PATH);
|
|
if (!length || length >= MAX_PATH ||
|
|
swprintf_s(path, MAX_PATH, L"%s\\AppSandbox\\d3dlayers\\dzn_icd.json", dst) < 0)
|
|
return;
|
|
}
|
|
if (native_runtime || GetFileAttributesW(path) != INVALID_FILE_ATTRIBUTES) {
|
|
DWORD disabled = native_runtime ? 1 : 0;
|
|
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Khronos\\Vulkan\\Drivers",
|
|
0, NULL, 0, KEY_SET_VALUE, NULL, &key, NULL) == ERROR_SUCCESS) {
|
|
RegSetValueExW(key, path, 0, REG_DWORD, (const BYTE *)&disabled, sizeof(disabled));
|
|
RegCloseKey(key);
|
|
if (!native_runtime) agent_log("GL: registered Vulkan (Dozen) ICD %ls", path);
|
|
}
|
|
}
|
|
|
|
agent_log("GL: provisioning complete.");
|
|
}
|
|
|
|
/* NVIDIA's installer makes DRS writable by desktop and packaged applications.
|
|
Plan9 transfers file bytes, not the host ACL, so establish these permissions
|
|
on the guest before seeding the profile database. */
|
|
static BOOL nvidia_drs_prepare(const wchar_t *dir)
|
|
{
|
|
wchar_t parent[MAX_PATH], sys[MAX_PATH], cmd[MAX_PATH * 2 + 160];
|
|
wchar_t *slash;
|
|
DWORD err, attrs;
|
|
|
|
wcscpy_s(parent, MAX_PATH, dir);
|
|
slash = wcsrchr(parent, L'\\');
|
|
if (!slash) return FALSE;
|
|
*slash = L'\0';
|
|
if (!CreateDirectoryW(parent, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
agent_log("NVIDIA DRS: cannot create parent directory (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
if (!CreateDirectoryW(dir, NULL) && GetLastError() != ERROR_ALREADY_EXISTS) {
|
|
agent_log("NVIDIA DRS: cannot create directory (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
attrs = GetFileAttributesW(dir);
|
|
if (attrs == INVALID_FILE_ATTRIBUTES || !(attrs & FILE_ATTRIBUTE_DIRECTORY)) {
|
|
agent_log("NVIDIA DRS: destination is not an accessible directory.");
|
|
return FALSE;
|
|
}
|
|
if (!GetSystemDirectoryW(sys, MAX_PATH)) {
|
|
agent_log("NVIDIA DRS: GetSystemDirectory failed (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
/* Grant Everyone and ALL APPLICATION PACKAGES inherited full control
|
|
within DRS. Numeric SIDs work with every guest language; preserve ACEs. */
|
|
swprintf_s(cmd, MAX_PATH * 2 + 160,
|
|
L"\"%s\\icacls.exe\" \"%s\" /grant *S-1-1-0:(OI)(CI)F "
|
|
L"*S-1-15-2-1:(OI)(CI)F /T /C /Q", sys, dir);
|
|
err = run_quiet(cmd);
|
|
if (err != ERROR_SUCCESS) {
|
|
agent_log("NVIDIA DRS: setting guest permissions failed (%lu).", err);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
static DWORD WINAPI gpu_copy_thread(LPVOID param)
|
|
{
|
|
GpuCopyState *state = (GpuCopyState *)param;
|
|
int i;
|
|
int total_files = 0;
|
|
int failed_shares = 0;
|
|
char msg[256];
|
|
wchar_t gl_dir[MAX_PATH], native_dir[MAX_PATH];
|
|
|
|
gl_dir[0] = 0;
|
|
native_dir[0] = 0;
|
|
agent_log("GPU copy starting (%d shares)...", state->count);
|
|
|
|
for (i = 0; i < state->count; i++) {
|
|
GpuShareInfo *si = &state->shares[i];
|
|
wchar_t dest_wide[MAX_PATH];
|
|
int files = 0;
|
|
int rc;
|
|
|
|
dest_wide[0] = 0;
|
|
if (MultiByteToWideChar(CP_UTF8, 0, si->dest_path, -1, dest_wide, MAX_PATH) == 0) {
|
|
agent_log("GPU copy share '%s': dest path conversion failed (%lu), skipping.",
|
|
si->share_name, GetLastError());
|
|
failed_shares++;
|
|
continue;
|
|
}
|
|
|
|
/* The GL mapping-layer share is provisioned after the copy loop. */
|
|
if (strcmp(si->share_name, "AppSandbox.GlLayers") == 0)
|
|
wcscpy_s(gl_dir, MAX_PATH, dest_wide);
|
|
|
|
agent_log("GPU copy share %d/%d: %s -> %s%s%s",
|
|
i + 1, state->count, si->share_name, si->dest_path,
|
|
si->filter[0] ? " [filter: " : "",
|
|
si->filter[0] ? si->filter : "");
|
|
|
|
if (strcmp(si->share_name, "AppSandbox.NvidiaDrs") == 0) {
|
|
/* This list is also sent after reconnects. Seed missing files;
|
|
existing profiles belong to the guest. The host's runtime
|
|
lock file is recreated locally by NVIDIA when needed. */
|
|
const P9CopyOptions options = { TRUE, "nvdrswr.lk", NULL };
|
|
if (nvidia_drs_prepare(dest_wide))
|
|
rc = p9_copy_share_ex(50001, si->share_name, dest_wide,
|
|
si->filter[0] ? si->filter : NULL,
|
|
&options, &files);
|
|
else
|
|
rc = P9_ERR_IO;
|
|
} else if (strcmp(si->share_name, "AppSandbox.Nvidia") == 0) {
|
|
const P9CopyOptions options = {
|
|
FALSE, NULL, "appsandbox-nvidia-vk-gl-shim.dll;appsandbox-nvidia-vk-gl-shim32.dll;"
|
|
"appsandbox-nvidia-dlss-shim.dll"
|
|
};
|
|
rc = p9_copy_share_ex(50001, si->share_name, dest_wide,
|
|
si->filter[0] ? si->filter : NULL,
|
|
&options, &files);
|
|
} else {
|
|
rc = p9_copy_share(50001, si->share_name, dest_wide,
|
|
si->filter[0] ? si->filter : NULL, &files);
|
|
}
|
|
|
|
if (rc != P9_OK) {
|
|
agent_log("GPU copy share '%s' failed (rc=%d).", si->share_name, rc);
|
|
failed_shares++;
|
|
} else {
|
|
agent_log("GPU copy share '%s' done (%d files).", si->share_name, files);
|
|
if (strcmp(si->share_name, "AppSandbox.Nvidia") == 0)
|
|
wcscpy_s(native_dir, MAX_PATH, dest_wide);
|
|
}
|
|
total_files += files;
|
|
|
|
/* Send progress to host */
|
|
sprintf_s(msg, sizeof(msg), "gpu_copy_progress:%d/%d", i + 1, state->count);
|
|
if (state->notify_sock != NULL)
|
|
send_line(state->notify_sock, msg);
|
|
}
|
|
|
|
/* If files were copied and vrd.inf has error 43, restart GPU + IDD devices
|
|
and re-disable Hyper-V Video adapter. */
|
|
if (total_files > 0 && check_gpu_error43()) {
|
|
agent_log("GPU drivers copied and error 43 detected - attempting device restart.");
|
|
cycle_gpu_devices(state->notify_sock, TRUE);
|
|
|
|
/* TODO: re-enable once IDD display is stable
|
|
Sleep(2000);
|
|
disable_hyperv_video(state->notify_sock);
|
|
*/
|
|
} else if (total_files > 0) {
|
|
agent_log("GPU drivers copied, no error 43 - no device restart needed.");
|
|
} else {
|
|
agent_log("All GPU driver files already present (pre-staged) - no copy or restart needed.");
|
|
}
|
|
|
|
if (failed_shares == 0 && (gl_dir[0] || native_dir[0] || gpu_prefers_system_opengl()))
|
|
gl_provision(gl_dir, native_dir);
|
|
if (failed_shares == 0 && !nvidia_dlss_provision(native_dir))
|
|
failed_shares++;
|
|
|
|
/* Send final result to host */
|
|
if (failed_shares == 0) {
|
|
sprintf_s(msg, sizeof(msg), "gpu_copy_done:%d", total_files);
|
|
agent_log("GPU copy complete: %d files, %d shares.", total_files, state->count);
|
|
} else {
|
|
sprintf_s(msg, sizeof(msg), "gpu_copy_error:%d/%d shares failed",
|
|
failed_shares, state->count);
|
|
agent_log("GPU copy finished with errors: %d/%d shares failed.",
|
|
failed_shares, state->count);
|
|
}
|
|
|
|
if (state->notify_sock != NULL)
|
|
send_line(state->notify_sock, msg);
|
|
|
|
state->copying = FALSE;
|
|
return 0;
|
|
}
|
|
|
|
/* Parse gpu_query_response and start background copy.
|
|
Format: "gpu_query_response:N" followed by N lines of "share|dest|filter"
|
|
(filter may be empty). */
|
|
static void handle_gpu_query_response(AsbConn *client, int share_count)
|
|
{
|
|
int i;
|
|
HANDLE thread;
|
|
|
|
if (share_count <= 0 || share_count > MAX_GPU_SHARES) {
|
|
agent_log("GPU query response: invalid share count %d.", share_count);
|
|
return;
|
|
}
|
|
|
|
if (g_gpu_copy.copying) {
|
|
agent_log("GPU copy already in progress, ignoring.");
|
|
return;
|
|
}
|
|
|
|
g_gpu_copy.count = 0;
|
|
|
|
for (i = 0; i < share_count; i++) {
|
|
char line[8192];
|
|
int n = recv_line(client, line, sizeof(line));
|
|
if (n <= 0) {
|
|
agent_log("GPU query: failed to read share line %d.", i);
|
|
return;
|
|
}
|
|
|
|
/* Parse "share_name|dest_path|filter" */
|
|
{
|
|
char *p1, *p2;
|
|
GpuShareInfo *si = &g_gpu_copy.shares[g_gpu_copy.count];
|
|
|
|
p1 = strchr(line, '|');
|
|
if (!p1) {
|
|
agent_log("GPU query: malformed share line: %s", line);
|
|
continue;
|
|
}
|
|
*p1 = '\0';
|
|
p1++;
|
|
|
|
p2 = strchr(p1, '|');
|
|
if (p2) {
|
|
*p2 = '\0';
|
|
p2++;
|
|
strncpy_s(si->filter, sizeof(si->filter), p2, _TRUNCATE);
|
|
} else {
|
|
si->filter[0] = '\0';
|
|
}
|
|
|
|
strncpy_s(si->share_name, sizeof(si->share_name), line, _TRUNCATE);
|
|
strncpy_s(si->dest_path, sizeof(si->dest_path), p1, _TRUNCATE);
|
|
g_gpu_copy.count++;
|
|
|
|
agent_log("GPU share [%d]: %s -> %s%s%s",
|
|
g_gpu_copy.count, si->share_name, si->dest_path,
|
|
si->filter[0] ? " filter=" : "",
|
|
si->filter[0] ? si->filter : "");
|
|
}
|
|
}
|
|
|
|
if (g_gpu_copy.count == 0) {
|
|
agent_log("GPU query: no valid shares to copy.");
|
|
send_line(client, "gpu_copy_done:0");
|
|
return;
|
|
}
|
|
|
|
/* Start background copy */
|
|
g_gpu_copy.notify_sock = client;
|
|
g_gpu_copy.copying = TRUE;
|
|
thread = CreateThread(NULL, 0, gpu_copy_thread, &g_gpu_copy, 0, NULL);
|
|
if (thread)
|
|
CloseHandle(thread);
|
|
else {
|
|
agent_log("Failed to create GPU copy thread (error %lu).", GetLastError());
|
|
g_gpu_copy.copying = FALSE;
|
|
send_line(client, "gpu_copy_error:thread creation failed");
|
|
}
|
|
}
|
|
|
|
/* ---- Line I/O ---- */
|
|
|
|
static int recv_line(AsbConn *s, char *buf, int buf_size)
|
|
{
|
|
int pos = 0;
|
|
while (pos < buf_size - 1) {
|
|
char c;
|
|
int n = asb_recv(s, &c, 1);
|
|
if (n <= 0) return n;
|
|
if (c == '\n') break;
|
|
if (c != '\r') buf[pos++] = c;
|
|
}
|
|
buf[pos] = '\0';
|
|
return pos;
|
|
}
|
|
|
|
static int send_line(AsbConn *s, const char *msg)
|
|
{
|
|
int len = (int)strlen(msg);
|
|
int n;
|
|
EnterCriticalSection(&g_send_cs);
|
|
n = asb_send(s, msg, len);
|
|
if (n > 0) n = asb_send(s, "\n", 1);
|
|
LeaveCriticalSection(&g_send_cs);
|
|
return n;
|
|
}
|
|
|
|
/* ---- Privilege helper ---- */
|
|
|
|
static BOOL enable_privilege(LPCWSTR priv_name)
|
|
{
|
|
HANDLE token;
|
|
TOKEN_PRIVILEGES tp;
|
|
if (!OpenProcessToken(GetCurrentProcess(),
|
|
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token))
|
|
return FALSE;
|
|
tp.PrivilegeCount = 1;
|
|
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
|
if (!LookupPrivilegeValueW(NULL, priv_name, &tp.Privileges[0].Luid)) {
|
|
CloseHandle(token);
|
|
return FALSE;
|
|
}
|
|
AdjustTokenPrivileges(token, FALSE, &tp, 0, NULL, NULL);
|
|
CloseHandle(token);
|
|
return GetLastError() == ERROR_SUCCESS;
|
|
}
|
|
|
|
/* ---- Input helper process (spawned into console session as SYSTEM) ---- */
|
|
|
|
static HANDLE g_input_process = NULL;
|
|
static DWORD g_input_session = 0xFFFFFFFF; /* session the helper was spawned into */
|
|
static volatile BOOL g_input_monitor_running = FALSE;
|
|
static HANDLE g_input_monitor_thread = NULL;
|
|
|
|
static void kill_input_helper(void)
|
|
{
|
|
if (g_input_process) {
|
|
TerminateProcess(g_input_process, 0);
|
|
WaitForSingleObject(g_input_process, 3000);
|
|
CloseHandle(g_input_process);
|
|
g_input_process = NULL;
|
|
g_input_session = 0xFFFFFFFF;
|
|
}
|
|
}
|
|
|
|
/* Spawn appsandbox-input.exe as SYSTEM in the given session.
|
|
Duplicates our own token, sets the session ID, then CreateProcessAsUser. */
|
|
static BOOL spawn_input_in_session(DWORD session_id)
|
|
{
|
|
HANDLE cur_token = NULL, dup_token = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
|
|
/* Build path to appsandbox-input.exe (same directory as agent) */
|
|
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
wcscat_s(exe_path, MAX_PATH, L"appsandbox-input.exe");
|
|
|
|
if (GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) {
|
|
agent_log("Input helper: %ls not found.", exe_path);
|
|
return FALSE;
|
|
}
|
|
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &cur_token)) {
|
|
agent_log("Input helper: OpenProcessToken failed (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
if (!DuplicateTokenEx(cur_token, TOKEN_ALL_ACCESS, NULL,
|
|
SecurityImpersonation, TokenPrimary, &dup_token)) {
|
|
agent_log("Input helper: DuplicateTokenEx failed (%lu).", GetLastError());
|
|
CloseHandle(cur_token);
|
|
return FALSE;
|
|
}
|
|
CloseHandle(cur_token);
|
|
|
|
if (!SetTokenInformation(dup_token, TokenSessionId,
|
|
&session_id, sizeof(session_id))) {
|
|
agent_log("Input helper: SetTokenInformation(session=%lu) failed (%lu).",
|
|
session_id, GetLastError());
|
|
CloseHandle(dup_token);
|
|
return FALSE;
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessAsUserW(dup_token, exe_path, NULL, NULL, NULL,
|
|
FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
agent_log("Input helper: CreateProcessAsUserW failed (%lu).", GetLastError());
|
|
CloseHandle(dup_token);
|
|
return FALSE;
|
|
}
|
|
|
|
agent_log("Input helper: spawned PID %lu in session %lu.", pi.dwProcessId, session_id);
|
|
g_input_process = pi.hProcess;
|
|
g_input_session = session_id;
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(dup_token);
|
|
return TRUE;
|
|
}
|
|
|
|
/* Monitor thread: every 3 seconds, check if the console session changed
|
|
or if the helper process died. Respawn as needed. */
|
|
static DWORD WINAPI input_monitor_thread(LPVOID param)
|
|
{
|
|
(void)param;
|
|
agent_log("Input monitor: started.");
|
|
|
|
while (g_input_monitor_running) {
|
|
DWORD cur_session = WTSGetActiveConsoleSessionId();
|
|
|
|
if (cur_session != 0xFFFFFFFF) {
|
|
BOOL helper_alive = g_input_process &&
|
|
WaitForSingleObject(g_input_process, 0) != WAIT_OBJECT_0;
|
|
|
|
if (cur_session != g_input_session) {
|
|
/* Console session changed — kill old helper and respawn */
|
|
if (helper_alive) {
|
|
agent_log("Input monitor: console session changed %lu -> %lu, respawning.",
|
|
g_input_session, cur_session);
|
|
kill_input_helper();
|
|
}
|
|
spawn_input_in_session(cur_session);
|
|
} else if (!helper_alive) {
|
|
/* Same session but helper died — respawn */
|
|
if (g_input_process) {
|
|
CloseHandle(g_input_process);
|
|
g_input_process = NULL;
|
|
}
|
|
agent_log("Input monitor: helper died, respawning in session %lu.", cur_session);
|
|
spawn_input_in_session(cur_session);
|
|
}
|
|
}
|
|
|
|
/* Sleep 3 seconds, but check stop flag every 500ms */
|
|
{
|
|
int i;
|
|
for (i = 0; i < 6 && g_input_monitor_running; i++)
|
|
Sleep(500);
|
|
}
|
|
}
|
|
|
|
kill_input_helper();
|
|
agent_log("Input monitor: stopped.");
|
|
return 0;
|
|
}
|
|
|
|
static void start_input_monitor(void)
|
|
{
|
|
g_input_monitor_running = TRUE;
|
|
g_input_monitor_thread = CreateThread(NULL, 0, input_monitor_thread, NULL, 0, NULL);
|
|
}
|
|
|
|
static void stop_input_monitor(void)
|
|
{
|
|
g_input_monitor_running = FALSE;
|
|
if (g_input_monitor_thread) {
|
|
WaitForSingleObject(g_input_monitor_thread, 5000);
|
|
CloseHandle(g_input_monitor_thread);
|
|
g_input_monitor_thread = NULL;
|
|
}
|
|
}
|
|
|
|
/* ---- Clipboard helper process (same pattern as input helper) ---- */
|
|
|
|
/* Protects the clipboard helper/reader process globals below. Both the SCM
|
|
control handler thread (SESSIONCHANGE LOGON/UNLOCK) and the clipboard
|
|
monitor threads mutate these globals (kill/spawn plus the test-then-close),
|
|
so without serialization the same HANDLE can be closed twice. */
|
|
static CRITICAL_SECTION g_clip_proc_cs;
|
|
|
|
static HANDLE g_clipboard_process = NULL;
|
|
static DWORD g_clipboard_session = 0xFFFFFFFF;
|
|
static volatile BOOL g_clipboard_monitor_running = FALSE;
|
|
static HANDLE g_clipboard_monitor_thread = NULL;
|
|
|
|
static void kill_clipboard_helper(void)
|
|
{
|
|
HANDLE proc;
|
|
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
proc = g_clipboard_process;
|
|
g_clipboard_process = NULL;
|
|
g_clipboard_session = 0xFFFFFFFF;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
|
|
if (proc) {
|
|
TerminateProcess(proc, 0);
|
|
WaitForSingleObject(proc, 3000);
|
|
CloseHandle(proc);
|
|
}
|
|
}
|
|
|
|
static BOOL spawn_clipboard_in_session(DWORD session_id)
|
|
{
|
|
HANDLE user_token = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
LPVOID env = NULL;
|
|
|
|
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
wcscat_s(exe_path, MAX_PATH, L"appsandbox-clipboard.exe");
|
|
|
|
if (GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) {
|
|
agent_log("Clipboard helper: %ls not found.", exe_path);
|
|
return FALSE;
|
|
}
|
|
|
|
if (!WTSQueryUserToken(session_id, &user_token)) {
|
|
agent_log("Clipboard helper: WTSQueryUserToken(session=%lu) failed (%lu).",
|
|
session_id, GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
if (!CreateEnvironmentBlock(&env, user_token, FALSE)) {
|
|
agent_log("Clipboard helper: CreateEnvironmentBlock failed (%lu).", GetLastError());
|
|
env = NULL;
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessAsUserW(user_token, exe_path, NULL, NULL, NULL,
|
|
FALSE,
|
|
CREATE_NO_WINDOW | (env ? CREATE_UNICODE_ENVIRONMENT : 0),
|
|
env, NULL, &si, &pi)) {
|
|
agent_log("Clipboard helper: CreateProcessAsUserW failed (%lu).", GetLastError());
|
|
if (env) DestroyEnvironmentBlock(env);
|
|
CloseHandle(user_token);
|
|
return FALSE;
|
|
}
|
|
|
|
agent_log("Clipboard helper: spawned PID %lu in session %lu (as user).",
|
|
pi.dwProcessId, session_id);
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
g_clipboard_process = pi.hProcess;
|
|
g_clipboard_session = session_id;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
CloseHandle(pi.hThread);
|
|
if (env) DestroyEnvironmentBlock(env);
|
|
CloseHandle(user_token);
|
|
return TRUE;
|
|
}
|
|
|
|
static DWORD WINAPI clipboard_monitor_thread(LPVOID param)
|
|
{
|
|
(void)param;
|
|
agent_log("Clipboard monitor: started.");
|
|
|
|
while (g_clipboard_monitor_running) {
|
|
DWORD cur_session = WTSGetActiveConsoleSessionId();
|
|
|
|
if (cur_session != 0xFFFFFFFF) {
|
|
HANDLE proc_snap;
|
|
DWORD sess_snap;
|
|
BOOL helper_alive;
|
|
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
proc_snap = g_clipboard_process;
|
|
sess_snap = g_clipboard_session;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
|
|
helper_alive = proc_snap &&
|
|
WaitForSingleObject(proc_snap, 0) != WAIT_OBJECT_0;
|
|
|
|
if (cur_session != sess_snap) {
|
|
if (helper_alive) {
|
|
agent_log("Clipboard monitor: console session changed %lu -> %lu, respawning.",
|
|
sess_snap, cur_session);
|
|
kill_clipboard_helper();
|
|
}
|
|
spawn_clipboard_in_session(cur_session);
|
|
} else if (!helper_alive) {
|
|
HANDLE dead;
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
dead = g_clipboard_process;
|
|
g_clipboard_process = NULL;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
if (dead) CloseHandle(dead);
|
|
agent_log("Clipboard monitor: helper died, respawning in session %lu.", cur_session);
|
|
spawn_clipboard_in_session(cur_session);
|
|
}
|
|
}
|
|
|
|
{
|
|
int i;
|
|
for (i = 0; i < 6 && g_clipboard_monitor_running; i++)
|
|
Sleep(500);
|
|
}
|
|
}
|
|
|
|
kill_clipboard_helper();
|
|
agent_log("Clipboard monitor: stopped.");
|
|
return 0;
|
|
}
|
|
|
|
static void start_clipboard_monitor(void)
|
|
{
|
|
g_clipboard_monitor_running = TRUE;
|
|
g_clipboard_monitor_thread = CreateThread(NULL, 0, clipboard_monitor_thread, NULL, 0, NULL);
|
|
}
|
|
|
|
static void stop_clipboard_monitor(void)
|
|
{
|
|
g_clipboard_monitor_running = FALSE;
|
|
if (g_clipboard_monitor_thread) {
|
|
WaitForSingleObject(g_clipboard_monitor_thread, 5000);
|
|
CloseHandle(g_clipboard_monitor_thread);
|
|
g_clipboard_monitor_thread = NULL;
|
|
}
|
|
}
|
|
|
|
/* ---- Clipboard reader process (runs as USER, :0006) ---- */
|
|
|
|
static HANDLE g_clipboard_reader_process = NULL;
|
|
static DWORD g_clipboard_reader_session = 0xFFFFFFFF;
|
|
static volatile BOOL g_clipboard_reader_monitor_running = FALSE;
|
|
static HANDLE g_clipboard_reader_monitor_thread = NULL;
|
|
|
|
static void kill_clipboard_reader(void)
|
|
{
|
|
HANDLE proc;
|
|
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
proc = g_clipboard_reader_process;
|
|
g_clipboard_reader_process = NULL;
|
|
g_clipboard_reader_session = 0xFFFFFFFF;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
|
|
if (proc) {
|
|
TerminateProcess(proc, 0);
|
|
WaitForSingleObject(proc, 3000);
|
|
CloseHandle(proc);
|
|
}
|
|
}
|
|
|
|
static BOOL spawn_clipboard_reader_in_session(DWORD session_id)
|
|
{
|
|
HANDLE user_token = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
LPVOID env = NULL;
|
|
|
|
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
wcscat_s(exe_path, MAX_PATH, L"appsandbox-clipboard-reader.exe");
|
|
|
|
if (GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) {
|
|
agent_log("Clipboard reader: %ls not found.", exe_path);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Get the logged-in user's token — this runs the process as the user,
|
|
which allows it to read the user's clipboard. */
|
|
if (!WTSQueryUserToken(session_id, &user_token)) {
|
|
agent_log("Clipboard reader: WTSQueryUserToken(session=%lu) failed (%lu).",
|
|
session_id, GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
/* Create environment block for the user */
|
|
if (!CreateEnvironmentBlock(&env, user_token, FALSE)) {
|
|
agent_log("Clipboard reader: CreateEnvironmentBlock failed (%lu).", GetLastError());
|
|
env = NULL; /* proceed without it */
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessAsUserW(user_token, exe_path, NULL, NULL, NULL,
|
|
FALSE,
|
|
CREATE_NO_WINDOW | (env ? CREATE_UNICODE_ENVIRONMENT : 0),
|
|
env, NULL, &si, &pi)) {
|
|
agent_log("Clipboard reader: CreateProcessAsUserW failed (%lu).", GetLastError());
|
|
if (env) DestroyEnvironmentBlock(env);
|
|
CloseHandle(user_token);
|
|
return FALSE;
|
|
}
|
|
|
|
agent_log("Clipboard reader: spawned PID %lu in session %lu (as user).",
|
|
pi.dwProcessId, session_id);
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
g_clipboard_reader_process = pi.hProcess;
|
|
g_clipboard_reader_session = session_id;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
CloseHandle(pi.hThread);
|
|
if (env) DestroyEnvironmentBlock(env);
|
|
CloseHandle(user_token);
|
|
return TRUE;
|
|
}
|
|
|
|
static DWORD WINAPI clipboard_reader_monitor_thread(LPVOID param)
|
|
{
|
|
(void)param;
|
|
agent_log("Clipboard reader monitor: started.");
|
|
|
|
while (g_clipboard_reader_monitor_running) {
|
|
DWORD cur_session = WTSGetActiveConsoleSessionId();
|
|
|
|
if (cur_session != 0xFFFFFFFF) {
|
|
HANDLE proc_snap;
|
|
DWORD sess_snap;
|
|
BOOL helper_alive;
|
|
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
proc_snap = g_clipboard_reader_process;
|
|
sess_snap = g_clipboard_reader_session;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
|
|
helper_alive = proc_snap &&
|
|
WaitForSingleObject(proc_snap, 0) != WAIT_OBJECT_0;
|
|
|
|
if (cur_session != sess_snap) {
|
|
if (helper_alive) {
|
|
agent_log("Clipboard reader monitor: console session changed %lu -> %lu, respawning.",
|
|
sess_snap, cur_session);
|
|
kill_clipboard_reader();
|
|
}
|
|
spawn_clipboard_reader_in_session(cur_session);
|
|
} else if (!helper_alive) {
|
|
HANDLE dead;
|
|
EnterCriticalSection(&g_clip_proc_cs);
|
|
dead = g_clipboard_reader_process;
|
|
g_clipboard_reader_process = NULL;
|
|
LeaveCriticalSection(&g_clip_proc_cs);
|
|
if (dead) CloseHandle(dead);
|
|
agent_log("Clipboard reader monitor: helper died, respawning in session %lu.", cur_session);
|
|
spawn_clipboard_reader_in_session(cur_session);
|
|
}
|
|
}
|
|
|
|
{
|
|
int i;
|
|
for (i = 0; i < 6 && g_clipboard_reader_monitor_running; i++)
|
|
Sleep(500);
|
|
}
|
|
}
|
|
|
|
kill_clipboard_reader();
|
|
agent_log("Clipboard reader monitor: stopped.");
|
|
return 0;
|
|
}
|
|
|
|
static void start_clipboard_reader_monitor(void)
|
|
{
|
|
g_clipboard_reader_monitor_running = TRUE;
|
|
g_clipboard_reader_monitor_thread = CreateThread(NULL, 0, clipboard_reader_monitor_thread, NULL, 0, NULL);
|
|
}
|
|
|
|
static void stop_clipboard_reader_monitor(void)
|
|
{
|
|
g_clipboard_reader_monitor_running = FALSE;
|
|
if (g_clipboard_reader_monitor_thread) {
|
|
WaitForSingleObject(g_clipboard_reader_monitor_thread, 5000);
|
|
CloseHandle(g_clipboard_reader_monitor_thread);
|
|
g_clipboard_reader_monitor_thread = NULL;
|
|
}
|
|
}
|
|
|
|
/* ---- Audio capture helper process (SYSTEM in console session, :0004) ---- */
|
|
|
|
static HANDLE g_audio_process = NULL;
|
|
static DWORD g_audio_session = 0xFFFFFFFF;
|
|
static volatile BOOL g_audio_monitor_running = FALSE;
|
|
static HANDLE g_audio_monitor_thread = NULL;
|
|
|
|
static void kill_audio_helper(void)
|
|
{
|
|
if (g_audio_process) {
|
|
TerminateProcess(g_audio_process, 0);
|
|
WaitForSingleObject(g_audio_process, 3000);
|
|
CloseHandle(g_audio_process);
|
|
g_audio_process = NULL;
|
|
g_audio_session = 0xFFFFFFFF;
|
|
}
|
|
}
|
|
|
|
static BOOL spawn_audio_in_session(DWORD session_id)
|
|
{
|
|
HANDLE cur_token = NULL, dup_token = NULL;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
|
|
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
wcscat_s(exe_path, MAX_PATH, L"appsandbox-audio.exe");
|
|
|
|
if (GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) {
|
|
agent_log("Audio helper: %ls not found.", exe_path);
|
|
return FALSE;
|
|
}
|
|
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &cur_token)) {
|
|
agent_log("Audio helper: OpenProcessToken failed (%lu).", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
if (!DuplicateTokenEx(cur_token, TOKEN_ALL_ACCESS, NULL,
|
|
SecurityImpersonation, TokenPrimary, &dup_token)) {
|
|
agent_log("Audio helper: DuplicateTokenEx failed (%lu).", GetLastError());
|
|
CloseHandle(cur_token);
|
|
return FALSE;
|
|
}
|
|
CloseHandle(cur_token);
|
|
|
|
if (!SetTokenInformation(dup_token, TokenSessionId,
|
|
&session_id, sizeof(session_id))) {
|
|
agent_log("Audio helper: SetTokenInformation(session=%lu) failed (%lu).",
|
|
session_id, GetLastError());
|
|
CloseHandle(dup_token);
|
|
return FALSE;
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessAsUserW(dup_token, exe_path, NULL, NULL, NULL,
|
|
FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
agent_log("Audio helper: CreateProcessAsUserW failed (%lu).", GetLastError());
|
|
CloseHandle(dup_token);
|
|
return FALSE;
|
|
}
|
|
|
|
agent_log("Audio helper: spawned PID %lu in session %lu.", pi.dwProcessId, session_id);
|
|
g_audio_process = pi.hProcess;
|
|
g_audio_session = session_id;
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(dup_token);
|
|
return TRUE;
|
|
}
|
|
|
|
static DWORD WINAPI audio_monitor_thread(LPVOID param)
|
|
{
|
|
(void)param;
|
|
agent_log("Audio monitor: started.");
|
|
|
|
while (g_audio_monitor_running) {
|
|
DWORD cur_session = WTSGetActiveConsoleSessionId();
|
|
|
|
if (cur_session != 0xFFFFFFFF) {
|
|
BOOL helper_alive = g_audio_process &&
|
|
WaitForSingleObject(g_audio_process, 0) != WAIT_OBJECT_0;
|
|
|
|
if (cur_session != g_audio_session) {
|
|
if (helper_alive) {
|
|
agent_log("Audio monitor: console session changed %lu -> %lu, respawning.",
|
|
g_audio_session, cur_session);
|
|
kill_audio_helper();
|
|
}
|
|
spawn_audio_in_session(cur_session);
|
|
} else if (!helper_alive) {
|
|
if (g_audio_process) {
|
|
CloseHandle(g_audio_process);
|
|
g_audio_process = NULL;
|
|
}
|
|
agent_log("Audio monitor: helper died, respawning in session %lu.", cur_session);
|
|
spawn_audio_in_session(cur_session);
|
|
}
|
|
}
|
|
|
|
{
|
|
int i;
|
|
for (i = 0; i < 6 && g_audio_monitor_running; i++)
|
|
Sleep(500);
|
|
}
|
|
}
|
|
|
|
kill_audio_helper();
|
|
agent_log("Audio monitor: stopped.");
|
|
return 0;
|
|
}
|
|
|
|
static void start_audio_monitor(void)
|
|
{
|
|
g_audio_monitor_running = TRUE;
|
|
g_audio_monitor_thread = CreateThread(NULL, 0, audio_monitor_thread, NULL, 0, NULL);
|
|
}
|
|
|
|
static void stop_audio_monitor(void)
|
|
{
|
|
g_audio_monitor_running = FALSE;
|
|
if (g_audio_monitor_thread) {
|
|
WaitForSingleObject(g_audio_monitor_thread, 5000);
|
|
CloseHandle(g_audio_monitor_thread);
|
|
g_audio_monitor_thread = NULL;
|
|
}
|
|
}
|
|
|
|
/* ---- IDD driver status check ---- */
|
|
|
|
/* Check the AppSandboxVDD driver state and report "idd_status:<status>" to the host
|
|
(ok / error / disabled / not_found / unknown), derived from devcon's PnP devnode
|
|
state. Called once at connect (force=1: always send, so a reconnect re-syncs the
|
|
host's latched idd_ready) AND periodically from the device-check loop (force=0:
|
|
send only on change) -- so the host's display-readiness tracks the LIVE VDD state,
|
|
which can self-heal via ensure_vdd_running(). The host re-latches every line, so
|
|
the change-gate avoids spamming the daemon log/events. */
|
|
static void report_idd_status(AsbConn *client, int force)
|
|
{
|
|
static char last_status[16]; /* last value sent (process-global; force=1 overrides on (re)connect) */
|
|
char output[4096];
|
|
wchar_t cmd[MAX_PATH];
|
|
wchar_t exe_dir[MAX_PATH];
|
|
wchar_t *slash;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
HANDLE hRead = NULL, hWrite = NULL;
|
|
SECURITY_ATTRIBUTES sa;
|
|
DWORD bytes_read;
|
|
int pos = 0;
|
|
const char *st = "not_found";
|
|
|
|
GetModuleFileNameW(NULL, exe_dir, MAX_PATH);
|
|
slash = wcsrchr(exe_dir, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
swprintf_s(cmd, MAX_PATH, L"\"%sdrivers\\devcon.exe\" status Root\\AppSandboxVDD", exe_dir);
|
|
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = TRUE;
|
|
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
|
|
st = "not_found"; goto emit;
|
|
}
|
|
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = hWrite;
|
|
si.hStdError = hWrite;
|
|
si.hStdInput = NULL;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
agent_log("IDD status: devcon failed to launch (%lu).", GetLastError());
|
|
CloseHandle(hRead);
|
|
CloseHandle(hWrite);
|
|
st = "not_found"; goto emit;
|
|
}
|
|
CloseHandle(hWrite);
|
|
|
|
while (ReadFile(hRead, output + pos, (DWORD)(sizeof(output) - pos - 1), &bytes_read, NULL) && bytes_read > 0)
|
|
pos += (int)bytes_read;
|
|
output[pos] = '\0';
|
|
CloseHandle(hRead);
|
|
|
|
WaitForSingleObject(pi.hProcess, 5000);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
|
|
if (strstr(output, "running")) {
|
|
agent_log("IDD status: AppSandboxVDD running.");
|
|
st = "ok";
|
|
} else if (strstr(output, "problem")) {
|
|
agent_log("IDD status: AppSandboxVDD has a problem. Output: %s", output);
|
|
st = "error";
|
|
} else if (strstr(output, "disabled")) {
|
|
agent_log("IDD status: AppSandboxVDD disabled.");
|
|
st = "disabled";
|
|
} else if (strstr(output, "No matching")) {
|
|
agent_log("IDD status: AppSandboxVDD not found.");
|
|
st = "not_found";
|
|
} else {
|
|
agent_log("IDD status: unknown. Output: %s", output);
|
|
st = "unknown";
|
|
}
|
|
|
|
emit:
|
|
/* Send only when the value changes (force overrides on connect). */
|
|
if (force || strcmp(st, last_status) != 0) {
|
|
char line[32];
|
|
sprintf_s(line, sizeof(line), "idd_status:%s", st);
|
|
send_line(client, line);
|
|
strcpy_s(last_status, sizeof(last_status), st);
|
|
}
|
|
}
|
|
|
|
/* ---- Helper: run devcon command and capture stdout ---- */
|
|
|
|
static BOOL run_devcon(const wchar_t *args, char *output, int output_size, DWORD *out_exit_code)
|
|
{
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
HANDLE hRead = NULL, hWrite = NULL;
|
|
SECURITY_ATTRIBUTES sa;
|
|
wchar_t cmd[MAX_PATH];
|
|
wchar_t exe_dir[MAX_PATH];
|
|
wchar_t *slash;
|
|
DWORD bytes_read;
|
|
int pos = 0;
|
|
|
|
GetModuleFileNameW(NULL, exe_dir, MAX_PATH);
|
|
slash = wcsrchr(exe_dir, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
|
|
swprintf_s(cmd, MAX_PATH, L"\"%sdrivers\\devcon.exe\" %s", exe_dir, args);
|
|
|
|
sa.nLength = sizeof(sa);
|
|
sa.lpSecurityDescriptor = NULL;
|
|
sa.bInheritHandle = TRUE;
|
|
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) return FALSE;
|
|
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = hWrite;
|
|
si.hStdError = hWrite;
|
|
si.hStdInput = NULL;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
CloseHandle(hRead);
|
|
CloseHandle(hWrite);
|
|
return FALSE;
|
|
}
|
|
CloseHandle(hWrite);
|
|
|
|
/* Wait for devcon to finish (10s max). If it hangs, kill it. */
|
|
if (WaitForSingleObject(pi.hProcess, 2000) == WAIT_TIMEOUT) {
|
|
agent_log("run_devcon: process timed out, killing.");
|
|
TerminateProcess(pi.hProcess, 1);
|
|
WaitForSingleObject(pi.hProcess, 3000);
|
|
}
|
|
|
|
/* Now read whatever output is available (process is dead, pipe will EOF) */
|
|
while (ReadFile(hRead, output + pos, (DWORD)(output_size - pos - 1), &bytes_read, NULL) && bytes_read > 0)
|
|
pos += (int)bytes_read;
|
|
output[pos] = '\0';
|
|
CloseHandle(hRead);
|
|
|
|
if (out_exit_code) GetExitCodeProcess(pi.hProcess, out_exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
return TRUE;
|
|
}
|
|
|
|
/* ---- VDD device check: restart if not running ---- */
|
|
|
|
static void ensure_vdd_running(void)
|
|
{
|
|
char output[4096];
|
|
DWORD exit_code = 0;
|
|
|
|
if (!run_devcon(L"status Root\\AppSandboxVDD", output, sizeof(output), &exit_code)) {
|
|
agent_log("ensure_vdd_running: devcon failed to launch (%lu).", GetLastError());
|
|
return;
|
|
}
|
|
|
|
if (strstr(output, "running"))
|
|
return;
|
|
|
|
if (strstr(output, "No matching"))
|
|
return; /* not installed yet */
|
|
|
|
/* A device in CM_PROB_DISABLED (explicitly disabled) has its CONFIGFLAG_DISABLED bit set;
|
|
`devcon restart` (DICS_PROPCHANGE: stop->start) reports success but CANNOT clear that bit,
|
|
so the device snaps straight back to disabled. Only `devcon enable` (DICS_ENABLE) clears it.
|
|
So: ENABLE a disabled device; RESTART a stuck-but-enabled ("problem") one. */
|
|
if (strstr(output, "disabled")) {
|
|
agent_log("VDD: disabled - enabling.");
|
|
if (!run_devcon(L"enable Root\\AppSandboxVDD", output, sizeof(output), &exit_code)) {
|
|
agent_log("VDD enable: devcon failed to launch (%lu).", GetLastError());
|
|
return;
|
|
}
|
|
agent_log("VDD enable (exit=%lu): %.400s", exit_code, output);
|
|
return;
|
|
}
|
|
|
|
if (strstr(output, "problem"))
|
|
agent_log("VDD: problem - restarting.");
|
|
else
|
|
agent_log("VDD: unknown state - restarting.");
|
|
|
|
if (!run_devcon(L"restart Root\\AppSandboxVDD", output, sizeof(output), &exit_code)) {
|
|
agent_log("VDD restart: devcon failed to launch (%lu).", GetLastError());
|
|
return;
|
|
}
|
|
agent_log("VDD restart (exit=%lu): %.400s", exit_code, output);
|
|
}
|
|
|
|
/* ---- Hyper-V Video device check: disable if running ---- */
|
|
|
|
#define HYPERV_VIDEO_HWID L"*DA0A7802*"
|
|
|
|
static void ensure_hyperv_video_disabled(AsbConn *notify)
|
|
{
|
|
char output[4096];
|
|
DWORD exit_code = 0;
|
|
|
|
if (!run_devcon(L"status " HYPERV_VIDEO_HWID, output, sizeof(output), &exit_code)) {
|
|
agent_log("ensure_hyperv_video_disabled: devcon failed to launch (%lu).", GetLastError());
|
|
return;
|
|
}
|
|
|
|
if (strstr(output, "disabled") || strstr(output, "No matching"))
|
|
return;
|
|
|
|
if (!strstr(output, "running"))
|
|
return;
|
|
|
|
agent_log("Hyper-V Video: running — disabling.");
|
|
|
|
if (!run_devcon(L"disable " HYPERV_VIDEO_HWID, output, sizeof(output), &exit_code)) {
|
|
agent_log("Hyper-V Video disable: devcon failed to launch (%lu).", GetLastError());
|
|
return;
|
|
}
|
|
|
|
if (notify != NULL)
|
|
send_line(notify, "hyperv_video:disabled");
|
|
agent_log("Hyper-V Video disable (exit=%lu): %.400s", exit_code, output);
|
|
}
|
|
|
|
/* ---- Report display monitor info to host ---- */
|
|
|
|
/* Spawn appsandbox-displays.exe in the interactive session and capture its
|
|
stdout. The helper prints "N,WxH,WxH,..." and exits. We use the same
|
|
token-dup + SetTokenInformation pattern as spawn_input_in_session(). */
|
|
static void report_displays(AsbConn *notify)
|
|
{
|
|
HANDLE cur_token = NULL, dup_token = NULL;
|
|
HANDLE hReadPipe = NULL, hWritePipe = NULL;
|
|
SECURITY_ATTRIBUTES sa;
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
DWORD session_id;
|
|
char output[512];
|
|
DWORD bytes_read;
|
|
char msg[512];
|
|
|
|
output[0] = '\0';
|
|
|
|
session_id = WTSGetActiveConsoleSessionId();
|
|
if (session_id == 0xFFFFFFFF || session_id == 0) {
|
|
/* No interactive user session — skip display enumeration.
|
|
Session 0 is the services session (no desktop).
|
|
0xFFFFFFFF means no console session at all. */
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
|
|
/* Build path to appsandbox-displays.exe (same directory as agent) */
|
|
GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (slash) *(slash + 1) = L'\0';
|
|
wcscat_s(exe_path, MAX_PATH, L"appsandbox-displays.exe");
|
|
|
|
if (GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) {
|
|
agent_log("report_displays: %ls not found.", exe_path);
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
|
|
/* Create pipe for stdout capture */
|
|
sa.nLength = sizeof(sa);
|
|
sa.bInheritHandle = TRUE;
|
|
sa.lpSecurityDescriptor = NULL;
|
|
if (!CreatePipe(&hReadPipe, &hWritePipe, &sa, 0)) {
|
|
agent_log("report_displays: CreatePipe failed (%lu).", GetLastError());
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
SetHandleInformation(hReadPipe, HANDLE_FLAG_INHERIT, 0);
|
|
|
|
/* Duplicate our token and set the interactive session */
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &cur_token)) {
|
|
agent_log("report_displays: OpenProcessToken failed (%lu).", GetLastError());
|
|
CloseHandle(hReadPipe); CloseHandle(hWritePipe);
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
if (!DuplicateTokenEx(cur_token, TOKEN_ALL_ACCESS, NULL,
|
|
SecurityImpersonation, TokenPrimary, &dup_token)) {
|
|
agent_log("report_displays: DuplicateTokenEx failed (%lu).", GetLastError());
|
|
CloseHandle(cur_token); CloseHandle(hReadPipe); CloseHandle(hWritePipe);
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
CloseHandle(cur_token);
|
|
|
|
if (!SetTokenInformation(dup_token, TokenSessionId,
|
|
&session_id, sizeof(session_id))) {
|
|
agent_log("report_displays: SetTokenInformation(session=%lu) failed (%lu).",
|
|
session_id, GetLastError());
|
|
CloseHandle(dup_token); CloseHandle(hReadPipe); CloseHandle(hWritePipe);
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = hWritePipe;
|
|
si.hStdError = hWritePipe;
|
|
si.hStdInput = NULL;
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (!CreateProcessAsUserW(dup_token, exe_path, NULL, NULL, NULL,
|
|
TRUE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
agent_log("report_displays: CreateProcessAsUserW failed (%lu).", GetLastError());
|
|
CloseHandle(dup_token); CloseHandle(hReadPipe); CloseHandle(hWritePipe);
|
|
if (notify != NULL) send_line(notify, "displays:0,");
|
|
return;
|
|
}
|
|
CloseHandle(dup_token);
|
|
CloseHandle(hWritePipe); /* Close write end so ReadFile sees EOF */
|
|
hWritePipe = NULL;
|
|
|
|
/* Wait for process (max 2s) then read output.
|
|
Timeout is normal at early boot before the desktop is ready. */
|
|
if (WaitForSingleObject(pi.hProcess, 2000) == WAIT_TIMEOUT) {
|
|
agent_log("report_displays: helper timed out (desktop may not be ready).");
|
|
TerminateProcess(pi.hProcess, 1);
|
|
WaitForSingleObject(pi.hProcess, 1000);
|
|
}
|
|
|
|
{
|
|
int total = 0;
|
|
while (ReadFile(hReadPipe, output + total,
|
|
(DWORD)(sizeof(output) - 1 - total), &bytes_read, NULL) &&
|
|
bytes_read > 0) {
|
|
total += (int)bytes_read;
|
|
if (total >= (int)sizeof(output) - 1) break;
|
|
}
|
|
output[total] = '\0';
|
|
}
|
|
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(hReadPipe);
|
|
|
|
/* Strip trailing whitespace */
|
|
{
|
|
int len = (int)strlen(output);
|
|
while (len > 0 && (output[len-1] == '\r' || output[len-1] == '\n' || output[len-1] == ' '))
|
|
output[--len] = '\0';
|
|
}
|
|
|
|
snprintf(msg, sizeof(msg), "displays:%s", output);
|
|
agent_log("Displays: %s", output);
|
|
if (notify != NULL) send_line(notify, msg);
|
|
}
|
|
|
|
/* ---- IDD connect: respawn helpers in console session ---- */
|
|
|
|
/* Send a response, prepending the sequence tag if present */
|
|
static void send_reply(AsbConn *s, const char *tag, const char *msg)
|
|
{
|
|
if (tag[0]) {
|
|
char rb[512];
|
|
sprintf_s(rb, sizeof(rb), "%s%s", tag, msg);
|
|
send_line(s, rb);
|
|
} else {
|
|
send_line(s, msg);
|
|
}
|
|
}
|
|
|
|
static void handle_idd_connect(AsbConn *client, const char *tag)
|
|
{
|
|
DWORD new_console = WTSGetActiveConsoleSessionId();
|
|
|
|
/* Kill and respawn input helper in the console session */
|
|
kill_input_helper();
|
|
agent_log("idd_connect: respawning input helper in session %lu.", new_console);
|
|
if (new_console != 0xFFFFFFFF)
|
|
spawn_input_in_session(new_console);
|
|
|
|
/* Kill and respawn clipboard helper (SYSTEM, :0005) in the console session */
|
|
kill_clipboard_helper();
|
|
agent_log("idd_connect: respawning clipboard helper in session %lu.", new_console);
|
|
if (new_console != 0xFFFFFFFF)
|
|
spawn_clipboard_in_session(new_console);
|
|
|
|
/* Kill and respawn clipboard reader (user, :0006) in the console session */
|
|
kill_clipboard_reader();
|
|
agent_log("idd_connect: respawning clipboard reader in session %lu.", new_console);
|
|
if (new_console != 0xFFFFFFFF)
|
|
spawn_clipboard_reader_in_session(new_console);
|
|
|
|
send_reply(client, tag, "ok");
|
|
}
|
|
|
|
/* ---- Persistent client handler ---- */
|
|
|
|
/* Disable the Hyper-V synthetic video adapter (if present).
|
|
Hardware ID: VMBUS\{da0a7802-e377-4aac-8e77-0558eb1073f8}
|
|
Silently does nothing if the device doesn't exist. */
|
|
static void disable_hyperv_video(AsbConn *notify_sock)
|
|
{
|
|
HDEVINFO devs;
|
|
SP_DEVINFO_DATA dev_info;
|
|
DWORD idx;
|
|
wchar_t hw_id[512];
|
|
CONFIGRET cr;
|
|
static const wchar_t *TARGET_HWID = L"VMBUS\\{DA0A7802-E377-4AAC-8E77-0558EB1073F8}";
|
|
|
|
devs = SetupDiGetClassDevsW(&GUID_DISPLAY_CLASS, NULL, NULL, DIGCF_PRESENT);
|
|
if (devs == INVALID_HANDLE_VALUE) {
|
|
agent_log("disable_hyperv_video: SetupDiGetClassDevs(PRESENT) failed, trying ALLCLASSES.");
|
|
devs = SetupDiGetClassDevsW(NULL, NULL, NULL, DIGCF_ALLCLASSES);
|
|
if (devs == INVALID_HANDLE_VALUE) return;
|
|
}
|
|
|
|
dev_info.cbSize = sizeof(dev_info);
|
|
for (idx = 0; SetupDiEnumDeviceInfo(devs, idx, &dev_info); idx++) {
|
|
ULONG dev_status = 0, dev_problem = 0;
|
|
hw_id[0] = L'\0';
|
|
CM_Get_Device_IDW(dev_info.DevInst, hw_id, 512, 0);
|
|
|
|
if (_wcsnicmp(hw_id, TARGET_HWID, wcslen(TARGET_HWID)) != 0)
|
|
continue;
|
|
|
|
/* Check if already disabled */
|
|
cr = CM_Get_DevNode_Status(&dev_status, &dev_problem, dev_info.DevInst, 0);
|
|
if (cr == CR_SUCCESS && !(dev_status & DN_STARTED) && (dev_problem == CM_PROB_DISABLED)) {
|
|
agent_log("Hyper-V Video adapter already disabled: %ls", hw_id);
|
|
if (notify_sock != NULL)
|
|
send_line(notify_sock, "hyperv_video:already_disabled");
|
|
SetupDiDestroyDeviceInfoList(devs);
|
|
return;
|
|
}
|
|
|
|
agent_log("Disabling Hyper-V Video adapter: %ls (status=0x%lX problem=%lu)", hw_id, dev_status, dev_problem);
|
|
if (notify_sock != NULL)
|
|
send_line(notify_sock, "hyperv_video:disabling");
|
|
|
|
cr = CM_Disable_DevNode(dev_info.DevInst, 0);
|
|
if (cr == CR_SUCCESS) {
|
|
agent_log("Hyper-V Video adapter disabled.");
|
|
if (notify_sock != NULL)
|
|
send_line(notify_sock, "hyperv_video:disabled");
|
|
} else {
|
|
agent_log("Hyper-V Video disable failed (%lu).", cr);
|
|
}
|
|
SetupDiDestroyDeviceInfoList(devs);
|
|
return;
|
|
}
|
|
|
|
agent_log("Hyper-V Video adapter not found (enumerated %lu devices).", idx);
|
|
if (notify_sock != NULL)
|
|
send_line(notify_sock, "hyperv_video:not_found");
|
|
SetupDiDestroyDeviceInfoList(devs);
|
|
}
|
|
|
|
/* Forward declaration — defined after SSH proxy section */
|
|
static void handle_ssh_enable(AsbConn *client, const char *tag);
|
|
|
|
static ULONG primary_nic_index(void)
|
|
{
|
|
ULONG buf_len = 15000;
|
|
ULONG index = 0;
|
|
|
|
for (int attempt = 0; attempt < 3; attempt++) {
|
|
IP_ADAPTER_ADDRESSES *addrs, *cur;
|
|
ULONG ret;
|
|
|
|
addrs = (IP_ADAPTER_ADDRESSES *)HeapAlloc(GetProcessHeap(), 0, buf_len);
|
|
if (!addrs) return 0;
|
|
ret = GetAdaptersAddresses(AF_INET,
|
|
GAA_FLAG_INCLUDE_ALL_INTERFACES | GAA_FLAG_SKIP_ANYCAST |
|
|
GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER,
|
|
NULL, addrs, &buf_len);
|
|
if (ret == NO_ERROR) {
|
|
for (cur = addrs; cur; cur = cur->Next) {
|
|
if (cur->IfType != IF_TYPE_ETHERNET_CSMACD || !cur->IfIndex)
|
|
continue;
|
|
if (!index) index = cur->IfIndex;
|
|
if (cur->OperStatus == IfOperStatusUp) {
|
|
index = cur->IfIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
HeapFree(GetProcessHeap(), 0, addrs);
|
|
if (ret != ERROR_BUFFER_OVERFLOW) break;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
static void handle_client(AsbConn *client)
|
|
{
|
|
char buf[256];
|
|
int n;
|
|
DWORD heartbeat_interval = 5000; /* ms between heartbeats */
|
|
DWORD device_check_interval = 20000; /* ms between VDD/Hyper-V device checks */
|
|
DWORD last_heartbeat;
|
|
DWORD last_device_check;
|
|
|
|
agent_log("Client connected.");
|
|
g_client_sock = client;
|
|
|
|
/* Set timeouts so recv/send don't block forever if host disconnects (no-op on ivshmem) */
|
|
asb_set_timeout(client, 10000, 10000);
|
|
|
|
/* Send hello */
|
|
if (send_line(client, "hello") <= 0) {
|
|
agent_log("Failed to send hello.");
|
|
asb_close(client);
|
|
g_client_sock = NULL;
|
|
return;
|
|
}
|
|
|
|
/* Report IDD driver status to host (force: always send on connect so a reconnect re-syncs) */
|
|
report_idd_status(client, 1);
|
|
|
|
/* Initial device checks */
|
|
ensure_vdd_running();
|
|
ensure_hyperv_video_disabled(client);
|
|
|
|
last_heartbeat = GetTickCount();
|
|
last_device_check = last_heartbeat;
|
|
|
|
/* Persistent connection loop */
|
|
while (WaitForSingleObject(g_stop_event, 0) != WAIT_OBJECT_0) {
|
|
int ret;
|
|
DWORD now;
|
|
|
|
ret = asb_poll(client, 1000); /* 1s: 1=data, 0=timeout, <0=closed/error */
|
|
if (ret < 0) break;
|
|
|
|
/* Send heartbeat if interval elapsed */
|
|
now = GetTickCount();
|
|
if (now - last_heartbeat >= heartbeat_interval) {
|
|
if (send_line(client, "heartbeat") <= 0) {
|
|
agent_log("Heartbeat send failed, client disconnected.");
|
|
break;
|
|
}
|
|
last_heartbeat = now;
|
|
}
|
|
|
|
/* Device checks on a slower cadence (VDD recovery is expensive) */
|
|
if (now - last_device_check >= device_check_interval) {
|
|
last_device_check = now;
|
|
ensure_vdd_running();
|
|
ensure_hyperv_video_disabled(client);
|
|
report_idd_status(client, 0); /* re-report the (possibly self-healed) VDD state; on-change only */
|
|
}
|
|
|
|
|
|
if (ret == 0) continue; /* select timeout, no data */
|
|
|
|
/* Read command */
|
|
n = recv_line(client, buf, sizeof(buf));
|
|
if (n <= 0) {
|
|
agent_log("Client disconnected.");
|
|
break;
|
|
}
|
|
|
|
/* Extract sequence tag (e.g. "42:ping" → tag="42:", cmd="ping") */
|
|
{
|
|
char *cmd = buf;
|
|
char tag[32] = {0};
|
|
char *colon = strchr(buf, ':');
|
|
if (colon && colon > buf && colon - buf < 16) {
|
|
/* Check all chars before colon are digits */
|
|
char *p;
|
|
BOOL is_seq = TRUE;
|
|
for (p = buf; p < colon; p++) {
|
|
if (*p < '0' || *p > '9') { is_seq = FALSE; break; }
|
|
}
|
|
if (is_seq) {
|
|
int tlen = (int)(colon - buf + 1);
|
|
memcpy(tag, buf, tlen);
|
|
tag[tlen] = '\0';
|
|
cmd = colon + 1;
|
|
}
|
|
}
|
|
|
|
agent_log("Command: %s", buf);
|
|
|
|
/* Helper macro: send response with tag prefix */
|
|
#define REPLY(msg) send_reply(client, tag, msg)
|
|
|
|
if (strcmp(cmd, "ping") == 0) {
|
|
REPLY("ok");
|
|
}
|
|
else if (strncmp(cmd, "ssh_deploy_key ", 15) == 0) {
|
|
REPLY(deploy_ssh_key(cmd + 15) ? "ssh_key_deployed" : "ssh_key_failed");
|
|
}
|
|
else if (strcmp(cmd, "shutdown") == 0) {
|
|
REPLY("ok");
|
|
agent_log("Initiating shutdown... killing input helper first.");
|
|
stop_input_monitor();
|
|
agent_log("Input monitor stopped, calling InitiateSystemShutdownExW...");
|
|
if (!enable_privilege(SE_SHUTDOWN_NAME))
|
|
agent_log("Warning: could not enable SeShutdownPrivilege (%lu)", GetLastError());
|
|
if (!InitiateSystemShutdownExW(NULL, L"AppSandbox shutdown", 0, TRUE, FALSE,
|
|
SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_FLAG_PLANNED))
|
|
agent_log("InitiateSystemShutdownExW failed: %lu", GetLastError());
|
|
/* Keep connection open — host will see it drop when VM powers off */
|
|
}
|
|
else if (strcmp(cmd, "restart") == 0) {
|
|
REPLY("ok");
|
|
agent_log("Initiating restart...");
|
|
stop_input_monitor();
|
|
if (!enable_privilege(SE_SHUTDOWN_NAME))
|
|
agent_log("Warning: could not enable SeShutdownPrivilege (%lu)", GetLastError());
|
|
if (!InitiateSystemShutdownExW(NULL, L"AppSandbox restart", 0, TRUE, TRUE,
|
|
SHTDN_REASON_MAJOR_OTHER | SHTDN_REASON_FLAG_PLANNED))
|
|
agent_log("InitiateSystemShutdownExW failed: %lu", GetLastError());
|
|
}
|
|
else if (strncmp(cmd, "gpu_query_response:", 19) == 0) {
|
|
int share_count = atoi(cmd + 19);
|
|
agent_log("Received GPU share list (%d shares).", share_count);
|
|
handle_gpu_query_response(client, share_count);
|
|
}
|
|
else if (strcmp(cmd, "gpu_none") == 0) {
|
|
agent_log("Host reports no GPU-PV assigned.");
|
|
}
|
|
else if (strcmp(cmd, "ssh_enable") == 0) {
|
|
handle_ssh_enable(client, tag);
|
|
}
|
|
else if (strncmp(cmd, "set_ip:", 7) == 0) {
|
|
/* set_ip:172.20.0.X/PREFIX:GATEWAY */
|
|
char ip[32] = {0}, prefix[8] = {0}, gateway[32] = {0};
|
|
char *slash, *colon2;
|
|
char *arg = cmd + 7;
|
|
|
|
/* Parse IP/prefix:gateway */
|
|
slash = strchr(arg, '/');
|
|
colon2 = slash ? strchr(slash, ':') : NULL;
|
|
if (slash && colon2) {
|
|
int ip_len = (int)(slash - arg);
|
|
int pfx_len = (int)(colon2 - slash - 1);
|
|
if (ip_len > 0 && ip_len < (int)sizeof(ip))
|
|
strncpy_s(ip, sizeof(ip), arg, ip_len);
|
|
if (pfx_len > 0 && pfx_len < (int)sizeof(prefix))
|
|
strncpy_s(prefix, sizeof(prefix), slash + 1, pfx_len);
|
|
strncpy_s(gateway, sizeof(gateway), colon2 + 1, sizeof(gateway) - 1);
|
|
}
|
|
|
|
if (ip[0] && prefix[0] && gateway[0]) {
|
|
wchar_t wcmd[512];
|
|
wchar_t nic[32] = L"Ethernet";
|
|
ULONG nic_index = primary_nic_index();
|
|
STARTUPINFOW si;
|
|
PROCESS_INFORMATION pi;
|
|
DWORD exit_code = 1;
|
|
|
|
if (nic_index)
|
|
swprintf_s(nic, 32, L"%lu", nic_index);
|
|
|
|
swprintf_s(wcmd, 512,
|
|
L"netsh interface ip set address \"%s\" static %S %S %S",
|
|
nic, ip,
|
|
/* Convert prefix length to subnet mask */
|
|
atoi(prefix) == 16 ? "255.255.0.0" :
|
|
atoi(prefix) == 24 ? "255.255.255.0" :
|
|
atoi(prefix) == 8 ? "255.0.0.0" : "255.255.255.0",
|
|
gateway);
|
|
|
|
agent_log("Setting IP: %S", wcmd);
|
|
|
|
ZeroMemory(&si, sizeof(si));
|
|
si.cb = sizeof(si);
|
|
ZeroMemory(&pi, sizeof(pi));
|
|
|
|
if (CreateProcessW(NULL, wcmd, NULL, NULL, FALSE,
|
|
CREATE_NO_WINDOW, NULL, NULL, &si, &pi)) {
|
|
WaitForSingleObject(pi.hProcess, 10000);
|
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
}
|
|
|
|
/* Set DNS to gateway (host NAT) + 8.8.8.8 fallback */
|
|
if (exit_code == 0) {
|
|
wchar_t dns_cmd[512];
|
|
STARTUPINFOW si2;
|
|
PROCESS_INFORMATION pi2;
|
|
|
|
swprintf_s(dns_cmd, 512,
|
|
L"netsh interface ip set dns \"%s\" static %S", nic, gateway);
|
|
ZeroMemory(&si2, sizeof(si2));
|
|
si2.cb = sizeof(si2);
|
|
ZeroMemory(&pi2, sizeof(pi2));
|
|
if (CreateProcessW(NULL, dns_cmd, NULL, NULL, FALSE,
|
|
CREATE_NO_WINDOW, NULL, NULL, &si2, &pi2)) {
|
|
WaitForSingleObject(pi2.hProcess, 10000);
|
|
CloseHandle(pi2.hProcess);
|
|
CloseHandle(pi2.hThread);
|
|
}
|
|
|
|
swprintf_s(dns_cmd, 512,
|
|
L"netsh interface ip add dns \"%s\" 8.8.8.8 index=2", nic);
|
|
ZeroMemory(&si2, sizeof(si2));
|
|
si2.cb = sizeof(si2);
|
|
ZeroMemory(&pi2, sizeof(pi2));
|
|
if (CreateProcessW(NULL, dns_cmd, NULL, NULL, FALSE,
|
|
CREATE_NO_WINDOW, NULL, NULL, &si2, &pi2)) {
|
|
WaitForSingleObject(pi2.hProcess, 10000);
|
|
CloseHandle(pi2.hProcess);
|
|
CloseHandle(pi2.hThread);
|
|
}
|
|
|
|
agent_log("IP configured: %s/%s gw %s dns %s,8.8.8.8",
|
|
ip, prefix, gateway, gateway);
|
|
REPLY("ok");
|
|
} else {
|
|
agent_log("netsh failed (exit %lu)", exit_code);
|
|
REPLY("error:netsh_failed");
|
|
}
|
|
} else {
|
|
agent_log("set_ip: bad format: %s", cmd);
|
|
REPLY("error:bad_format");
|
|
}
|
|
}
|
|
else if (strcmp(cmd, "gpu_copy") == 0) {
|
|
/* Host re-triggered GPU copy — ask for share list */
|
|
REPLY("gpu_query");
|
|
}
|
|
else if (strcmp(cmd, "idd_connect") == 0) {
|
|
handle_idd_connect(client, tag);
|
|
}
|
|
else {
|
|
REPLY("error:unknown");
|
|
}
|
|
|
|
#undef REPLY
|
|
}
|
|
}
|
|
|
|
/* If GPU copy is still running, wait for it to finish */
|
|
if (g_gpu_copy.copying) {
|
|
agent_log("Waiting for GPU copy thread to finish...");
|
|
/* Invalidate notify socket so it doesn't try to send on closed socket */
|
|
g_gpu_copy.notify_sock = NULL;
|
|
{
|
|
int wait;
|
|
for (wait = 0; wait < 10000 && g_gpu_copy.copying; wait += 500)
|
|
Sleep(500);
|
|
}
|
|
}
|
|
|
|
g_client_sock = NULL;
|
|
asb_close(client);
|
|
agent_log("Client handler exiting.");
|
|
}
|
|
|
|
/* ---- Socket listener ---- */
|
|
|
|
static DWORD WINAPI listener_thread(LPVOID param)
|
|
{
|
|
AsbListener *l;
|
|
(void)param;
|
|
|
|
if (asb_transport_init() != 0) {
|
|
agent_log("asb_transport_init failed.");
|
|
return 1;
|
|
}
|
|
|
|
while (WaitForSingleObject(g_stop_event, 0) != WAIT_OBJECT_0) {
|
|
l = asb_listen(ASB_CH_AGENT);
|
|
if (!l) {
|
|
agent_log("asb_listen(ASB_CH_AGENT) failed, retrying in 3s");
|
|
if (WaitForSingleObject(g_stop_event, 3000) == WAIT_OBJECT_0) break;
|
|
continue;
|
|
}
|
|
agent_log("Listening on agent control channel (transport=%s)",
|
|
asb_transport_is_ivshmem() ? "ivshmem" : "hyperv");
|
|
|
|
/* Accept loop — one client at a time */
|
|
while (WaitForSingleObject(g_stop_event, 0) != WAIT_OBJECT_0) {
|
|
AsbConn *client = asb_accept(l, 1000); /* 1s timeout so we re-check g_stop_event */
|
|
if (client) handle_client(client);
|
|
}
|
|
asb_close_listener(l);
|
|
}
|
|
|
|
agent_log("Listener stopped.");
|
|
return 0;
|
|
}
|
|
|
|
/* ---- SSH: sshd detection + HV proxy (guest-side: HV socket → localhost:22) ---- */
|
|
|
|
/* SSH proxy service GUID: {A5B0CAFE-0007-4000-8000-000000000001} */
|
|
static const GUID SSH_SERVICE_GUID =
|
|
{ 0xa5b0cafe, 0x0007, 0x4000, { 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 } };
|
|
|
|
static HANDLE g_ssh_proxy_thread = NULL;
|
|
static volatile BOOL g_ssh_proxy_running = FALSE;
|
|
static HANDLE g_ssh_wait_thread = NULL;
|
|
static volatile BOOL g_ssh_waiting = FALSE;
|
|
|
|
#define SSH_RELAY_BUF 8192
|
|
|
|
typedef struct SshRelayCtx {
|
|
AsbConn *hv; /* transport connection to the host (AF_HYPERV on PC, ivshmem on Mac) */
|
|
SOCKET tcp_sock; /* localhost:22 (sshd) */
|
|
} SshRelayCtx;
|
|
|
|
static DWORD WINAPI ssh_relay_thread(LPVOID param)
|
|
{
|
|
SshRelayCtx *ctx = (SshRelayCtx *)param;
|
|
char buf[SSH_RELAY_BUF];
|
|
int n;
|
|
SOCKET hv = (SOCKET)asb_conn_socket_u64(ctx->hv);
|
|
|
|
if (hv != INVALID_SOCKET) {
|
|
/* PC (AF_HYPERV): the connection exposes a real SOCKET, so relay both directions with a single dual-fd select(). */
|
|
fd_set rfds;
|
|
struct timeval tv;
|
|
for (;;) {
|
|
FD_ZERO(&rfds);
|
|
FD_SET(hv, &rfds);
|
|
FD_SET(ctx->tcp_sock, &rfds);
|
|
tv.tv_sec = 5;
|
|
tv.tv_usec = 0;
|
|
|
|
n = select(0, &rfds, NULL, NULL, &tv);
|
|
if (n < 0) break;
|
|
if (n == 0) continue;
|
|
|
|
if (FD_ISSET(hv, &rfds)) {
|
|
n = recv(hv, buf, SSH_RELAY_BUF, 0);
|
|
if (n <= 0) break;
|
|
if (send(ctx->tcp_sock, buf, n, 0) != n) break;
|
|
}
|
|
if (FD_ISSET(ctx->tcp_sock, &rfds)) {
|
|
n = recv(ctx->tcp_sock, buf, SSH_RELAY_BUF, 0);
|
|
if (n <= 0) break;
|
|
if (send(hv, buf, n, 0) != n) break;
|
|
}
|
|
}
|
|
} else {
|
|
/* ivshmem: the connection has no fd to select() on, so relay each direction separately —
|
|
poll the transport with a short timeout, non-blocking-check the TCP side. */
|
|
fd_set rfds;
|
|
struct timeval tv;
|
|
for (;;) {
|
|
int pr = asb_poll(ctx->hv, 5);
|
|
if (pr < 0) break;
|
|
if (pr > 0) {
|
|
n = asb_recv(ctx->hv, buf, SSH_RELAY_BUF);
|
|
if (n <= 0) break;
|
|
{ int off = 0; while (off < n) { int s = send(ctx->tcp_sock, buf + off, n - off, 0);
|
|
if (s <= 0) goto relay_done; off += s; } }
|
|
}
|
|
FD_ZERO(&rfds); FD_SET(ctx->tcp_sock, &rfds);
|
|
tv.tv_sec = 0; tv.tv_usec = 0;
|
|
if (select(0, &rfds, NULL, NULL, &tv) > 0) {
|
|
n = recv(ctx->tcp_sock, buf, SSH_RELAY_BUF, 0);
|
|
if (n <= 0) break;
|
|
if (asb_send(ctx->hv, buf, n) != n) break;
|
|
}
|
|
}
|
|
}
|
|
relay_done:
|
|
asb_close(ctx->hv);
|
|
closesocket(ctx->tcp_sock);
|
|
free(ctx);
|
|
return 0;
|
|
}
|
|
|
|
/* Check if sshd service is running. Returns TRUE if running. */
|
|
static BOOL is_sshd_running(void)
|
|
{
|
|
SC_HANDLE scm, svc;
|
|
SERVICE_STATUS ss;
|
|
BOOL running = FALSE;
|
|
|
|
scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CONNECT);
|
|
if (!scm) return FALSE;
|
|
|
|
svc = OpenServiceA(scm, "sshd", SERVICE_QUERY_STATUS);
|
|
if (svc) {
|
|
if (QueryServiceStatus(svc, &ss) && ss.dwCurrentState == SERVICE_RUNNING)
|
|
running = TRUE;
|
|
CloseServiceHandle(svc);
|
|
}
|
|
|
|
CloseServiceHandle(scm);
|
|
return running;
|
|
}
|
|
|
|
/* Try to start sshd and set it to auto-start. Returns TRUE if running. */
|
|
static BOOL ensure_sshd_running(void)
|
|
{
|
|
SC_HANDLE scm, svc;
|
|
SERVICE_STATUS ss;
|
|
BOOL ok = FALSE;
|
|
|
|
scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_ALL_ACCESS);
|
|
if (!scm) return FALSE;
|
|
|
|
svc = OpenServiceA(scm, "sshd", SERVICE_ALL_ACCESS);
|
|
if (svc) {
|
|
ChangeServiceConfigA(svc, SERVICE_NO_CHANGE, SERVICE_AUTO_START,
|
|
SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL);
|
|
if (QueryServiceStatus(svc, &ss) && ss.dwCurrentState == SERVICE_RUNNING) {
|
|
ok = TRUE;
|
|
} else {
|
|
if (StartServiceA(svc, 0, NULL))
|
|
ok = TRUE;
|
|
else if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING)
|
|
ok = TRUE;
|
|
}
|
|
CloseServiceHandle(svc);
|
|
}
|
|
|
|
CloseServiceHandle(scm);
|
|
return ok;
|
|
}
|
|
|
|
/* HV proxy listener — runs after sshd is confirmed running */
|
|
static DWORD WINAPI ssh_proxy_thread(LPVOID param)
|
|
{
|
|
AsbListener *l;
|
|
struct sockaddr_in tcp_addr;
|
|
(void)param;
|
|
|
|
l = asb_listen(ASB_CH_SSH);
|
|
if (!l) {
|
|
agent_log("SSH proxy: asb_listen(ASB_CH_SSH) failed.");
|
|
return 1;
|
|
}
|
|
agent_log("SSH proxy: listening on ssh channel (transport=%s)",
|
|
asb_transport_is_ivshmem() ? "ivshmem" : "hyperv");
|
|
|
|
while (g_ssh_proxy_running) {
|
|
AsbConn *client = asb_accept(l, 1000); /* 1s so we re-check g_ssh_proxy_running */
|
|
SOCKET tcp_s;
|
|
if (!client) continue;
|
|
|
|
/* Connect to local sshd */
|
|
tcp_s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (tcp_s == INVALID_SOCKET) { asb_close(client); continue; }
|
|
|
|
memset(&tcp_addr, 0, sizeof(tcp_addr));
|
|
tcp_addr.sin_family = AF_INET;
|
|
tcp_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
tcp_addr.sin_port = htons(22);
|
|
|
|
if (connect(tcp_s, (struct sockaddr *)&tcp_addr, sizeof(tcp_addr)) != 0) {
|
|
agent_log("SSH proxy: connect to localhost:22 failed: %d", WSAGetLastError());
|
|
closesocket(tcp_s);
|
|
asb_close(client);
|
|
continue;
|
|
}
|
|
|
|
/* Spawn relay thread */
|
|
{
|
|
SshRelayCtx *ctx = (SshRelayCtx *)malloc(sizeof(SshRelayCtx));
|
|
if (ctx) {
|
|
HANDLE t;
|
|
ctx->hv = client;
|
|
ctx->tcp_sock = tcp_s;
|
|
t = CreateThread(NULL, 0, ssh_relay_thread, ctx, 0, NULL);
|
|
if (t) CloseHandle(t); /* detached — relay cleans up itself */
|
|
else { free(ctx); asb_close(client); closesocket(tcp_s); }
|
|
} else {
|
|
asb_close(client);
|
|
closesocket(tcp_s);
|
|
}
|
|
}
|
|
}
|
|
|
|
asb_close_listener(l);
|
|
agent_log("SSH proxy: stopped.");
|
|
return 0;
|
|
}
|
|
|
|
static void start_ssh_proxy(void)
|
|
{
|
|
if (g_ssh_proxy_thread) return; /* already running */
|
|
g_ssh_proxy_running = TRUE;
|
|
g_ssh_proxy_thread = CreateThread(NULL, 0, ssh_proxy_thread, NULL, 0, NULL);
|
|
}
|
|
|
|
static void stop_ssh_proxy(void)
|
|
{
|
|
g_ssh_proxy_running = FALSE;
|
|
g_ssh_waiting = FALSE;
|
|
if (g_ssh_wait_thread) {
|
|
WaitForSingleObject(g_ssh_wait_thread, 5000);
|
|
CloseHandle(g_ssh_wait_thread);
|
|
g_ssh_wait_thread = NULL;
|
|
}
|
|
if (g_ssh_proxy_thread) {
|
|
WaitForSingleObject(g_ssh_proxy_thread, 5000);
|
|
CloseHandle(g_ssh_proxy_thread);
|
|
g_ssh_proxy_thread = NULL;
|
|
}
|
|
}
|
|
|
|
/* Background thread: poll for sshd service to become running.
|
|
SetupComplete.cmd installs OpenSSH via MSI during first boot — sshd may
|
|
not be running yet when the agent first handles ssh_enable. */
|
|
static DWORD WINAPI ssh_wait_thread(LPVOID param)
|
|
{
|
|
int attempts = 0;
|
|
(void)param;
|
|
|
|
agent_log("SSH: waiting for sshd to become available...");
|
|
|
|
while (g_ssh_waiting && attempts < 120) { /* up to ~2 minutes */
|
|
Sleep(1000);
|
|
attempts++;
|
|
|
|
if (ensure_sshd_running()) {
|
|
agent_log("SSH: sshd is running (after %d seconds).", attempts);
|
|
start_ssh_proxy();
|
|
EnterCriticalSection(&g_send_cs);
|
|
if (g_client_sock != NULL)
|
|
send_line(g_client_sock, "ssh_ready");
|
|
LeaveCriticalSection(&g_send_cs);
|
|
g_ssh_waiting = FALSE;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
agent_log("SSH: sshd did not start within timeout.");
|
|
EnterCriticalSection(&g_send_cs);
|
|
if (g_client_sock != NULL)
|
|
send_line(g_client_sock, "ssh_failed");
|
|
LeaveCriticalSection(&g_send_cs);
|
|
g_ssh_waiting = FALSE;
|
|
return 1;
|
|
}
|
|
|
|
/* Handle "ssh_enable" command from host.
|
|
sshd is installed by SetupComplete.cmd (MSI). Agent just detects + proxies. */
|
|
static void handle_ssh_enable(AsbConn *client, const char *tag)
|
|
{
|
|
/* Already running? */
|
|
if (is_sshd_running()) {
|
|
agent_log("SSH: sshd already running.");
|
|
send_reply(client, tag, "ssh_ready");
|
|
start_ssh_proxy();
|
|
return;
|
|
}
|
|
|
|
/* Try to start it (may be installed but stopped) */
|
|
if (ensure_sshd_running()) {
|
|
agent_log("SSH: sshd started successfully.");
|
|
send_reply(client, tag, "ssh_ready");
|
|
start_ssh_proxy();
|
|
return;
|
|
}
|
|
|
|
/* Not installed yet — SetupComplete.cmd may still be running.
|
|
Poll on a background thread. */
|
|
if (g_ssh_waiting) {
|
|
send_reply(client, tag, "ssh_installing");
|
|
return;
|
|
}
|
|
|
|
g_ssh_waiting = TRUE;
|
|
send_reply(client, tag, "ssh_installing");
|
|
g_ssh_wait_thread = CreateThread(NULL, 0, ssh_wait_thread, NULL, 0, NULL);
|
|
if (!g_ssh_wait_thread) {
|
|
agent_log("SSH: failed to create wait thread.");
|
|
g_ssh_waiting = FALSE;
|
|
send_reply(client, tag, "ssh_failed");
|
|
}
|
|
}
|
|
|
|
/* ---- Windows service plumbing ---- */
|
|
|
|
static void set_service_status(DWORD state, DWORD exit_code)
|
|
{
|
|
g_status.dwCurrentState = state;
|
|
g_status.dwWin32ExitCode = exit_code;
|
|
SetServiceStatus(g_status_handle, &g_status);
|
|
}
|
|
|
|
static DWORD WINAPI service_ctrl_ex(DWORD ctrl, DWORD event_type, LPVOID event_data, LPVOID context)
|
|
{
|
|
(void)context;
|
|
agent_log("service_ctrl received: %lu (event_type=%lu)", ctrl, event_type);
|
|
|
|
if (ctrl == SERVICE_CONTROL_STOP || ctrl == SERVICE_CONTROL_SHUTDOWN) {
|
|
if (ctrl == SERVICE_CONTROL_SHUTDOWN)
|
|
g_os_shutting_down = TRUE;
|
|
|
|
/* Notify host */
|
|
if (g_client_sock != NULL) {
|
|
if (ctrl == SERVICE_CONTROL_SHUTDOWN)
|
|
send_line(g_client_sock, "os_shutdown");
|
|
else
|
|
send_line(g_client_sock, "service_stopping");
|
|
agent_log("Sent notification to host (ctrl=%lu).", ctrl);
|
|
}
|
|
|
|
set_service_status(SERVICE_STOP_PENDING, 0);
|
|
agent_log("Setting stop event...");
|
|
SetEvent(g_stop_event);
|
|
return NO_ERROR;
|
|
}
|
|
|
|
if (ctrl == SERVICE_CONTROL_SESSIONCHANGE) {
|
|
WTSSESSION_NOTIFICATION *sn = (WTSSESSION_NOTIFICATION *)event_data;
|
|
if (event_type == WTS_SESSION_LOGOFF) {
|
|
agent_log("Session logoff detected (session %lu).",
|
|
sn ? sn->dwSessionId : 0);
|
|
/* Do NOT restart the VDD device here. IddCx handles the DWM
|
|
transition naturally: UnassignSwapChain (user DWM dies) then
|
|
AssignSwapChain (login-screen DWM starts compositing).
|
|
A devcon restart mid-transition destroys the adapter/monitor
|
|
and prevents IddCx from completing the handoff. */
|
|
|
|
/* Disable Hyper-V Video after logoff (may have been re-enabled) */
|
|
ensure_hyperv_video_disabled(NULL);
|
|
}
|
|
if (event_type == WTS_SESSION_LOGON || event_type == WTS_SESSION_UNLOCK) {
|
|
DWORD sid = sn ? sn->dwSessionId : WTSGetActiveConsoleSessionId();
|
|
agent_log("Session %s detected (session %lu), respawning clipboard helpers.",
|
|
event_type == WTS_SESSION_LOGON ? "logon" : "unlock", sid);
|
|
kill_clipboard_helper();
|
|
if (sid != 0xFFFFFFFF)
|
|
spawn_clipboard_in_session(sid);
|
|
kill_clipboard_reader();
|
|
if (sid != 0xFFFFFFFF)
|
|
spawn_clipboard_reader_in_session(sid);
|
|
}
|
|
return NO_ERROR;
|
|
}
|
|
|
|
return ERROR_CALL_NOT_IMPLEMENTED;
|
|
}
|
|
|
|
static void WINAPI service_main(DWORD argc, LPSTR *argv)
|
|
{
|
|
HANDLE thread;
|
|
|
|
(void)argc; (void)argv;
|
|
|
|
g_status_handle = RegisterServiceCtrlHandlerExA(SERVICE_NAME, service_ctrl_ex, NULL);
|
|
if (!g_status_handle) return;
|
|
|
|
/* Init before accepting SESSIONCHANGE so the control handler and the
|
|
clipboard monitor threads share this lock for the helper-process globals. */
|
|
InitializeCriticalSection(&g_clip_proc_cs);
|
|
|
|
g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
|
|
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE;
|
|
set_service_status(SERVICE_START_PENDING, 0);
|
|
|
|
g_stop_event = CreateEventW(NULL, TRUE, FALSE, NULL);
|
|
InitializeCriticalSection(&g_send_cs);
|
|
|
|
/* Wire p9copy logging into agent_log */
|
|
p9_set_log(p9copy_log);
|
|
|
|
/* Start listener on a worker thread */
|
|
thread = CreateThread(NULL, 0, listener_thread, NULL, 0, NULL);
|
|
|
|
/* Start input helper monitor (spawns/respawns in console session as SYSTEM) */
|
|
start_input_monitor();
|
|
|
|
/* Start clipboard helper monitor (SYSTEM, :0005) */
|
|
start_clipboard_monitor();
|
|
|
|
/* Start clipboard reader monitor (user, :0006) */
|
|
start_clipboard_reader_monitor();
|
|
|
|
/* Start audio capture monitor (SYSTEM, :0004) */
|
|
start_audio_monitor();
|
|
|
|
set_service_status(SERVICE_RUNNING, 0);
|
|
agent_log("Service started.");
|
|
|
|
/* Ensure VDD device is running (may need restart after logout teardown) */
|
|
ensure_vdd_running();
|
|
|
|
/* Ensure Hyper-V Video adapter is disabled (VDD replaces it) */
|
|
ensure_hyperv_video_disabled(NULL);
|
|
|
|
/* Wait until stop is signaled */
|
|
WaitForSingleObject(g_stop_event, INFINITE);
|
|
|
|
/* Clean up */
|
|
agent_log("Stop event signaled, cleaning up...");
|
|
agent_log("Stopping SSH proxy...");
|
|
stop_ssh_proxy();
|
|
agent_log("SSH proxy stopped.");
|
|
agent_log("Stopping audio monitor...");
|
|
stop_audio_monitor();
|
|
agent_log("Audio monitor stopped.");
|
|
agent_log("Stopping clipboard reader monitor...");
|
|
stop_clipboard_reader_monitor();
|
|
agent_log("Clipboard reader monitor stopped.");
|
|
agent_log("Stopping clipboard monitor...");
|
|
stop_clipboard_monitor();
|
|
agent_log("Clipboard monitor stopped.");
|
|
agent_log("Stopping input monitor...");
|
|
stop_input_monitor();
|
|
agent_log("Input monitor stopped.");
|
|
/* The control listener is owned by listener_thread (asb_close_listener); its accept loop
|
|
polls g_stop_event every 1s and exits on its own. Nothing socket-level to close here. */
|
|
if (thread) {
|
|
agent_log("Waiting for listener thread...");
|
|
WaitForSingleObject(thread, 5000);
|
|
CloseHandle(thread);
|
|
agent_log("Listener thread exited.");
|
|
}
|
|
CloseHandle(g_stop_event);
|
|
DeleteCriticalSection(&g_clip_proc_cs);
|
|
DeleteCriticalSection(&g_send_cs);
|
|
|
|
agent_log("Service stopped.");
|
|
set_service_status(SERVICE_STOPPED, 0);
|
|
}
|
|
|
|
/* ---- Install / Remove ---- */
|
|
|
|
static int install_service(void)
|
|
{
|
|
SC_HANDLE scm, svc;
|
|
char path[MAX_PATH];
|
|
|
|
GetModuleFileNameA(NULL, path, MAX_PATH);
|
|
|
|
scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_CREATE_SERVICE);
|
|
if (!scm) {
|
|
printf("OpenSCManager failed: %lu\n", GetLastError());
|
|
return 1;
|
|
}
|
|
|
|
svc = CreateServiceA(scm, SERVICE_NAME, DISPLAY_NAME,
|
|
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
|
|
SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
|
|
path, NULL, NULL, NULL, NULL, NULL);
|
|
|
|
if (!svc) {
|
|
if (GetLastError() == ERROR_SERVICE_EXISTS) {
|
|
printf("Service already exists.\n");
|
|
svc = OpenServiceA(scm, SERVICE_NAME, SERVICE_START);
|
|
} else {
|
|
printf("CreateService failed: %lu\n", GetLastError());
|
|
CloseServiceHandle(scm);
|
|
return 1;
|
|
}
|
|
} else {
|
|
printf("Service installed.\n");
|
|
|
|
/* Set description */
|
|
SERVICE_DESCRIPTIONA desc;
|
|
desc.lpDescription = "AppSandbox guest agent for host-guest communication via Hyper-V sockets.";
|
|
ChangeServiceConfig2A(svc, SERVICE_CONFIG_DESCRIPTION, &desc);
|
|
|
|
/* Set recovery: restart on failure */
|
|
SC_ACTION actions[3] = {
|
|
{ SC_ACTION_RESTART, 5000 },
|
|
{ SC_ACTION_RESTART, 10000 },
|
|
{ SC_ACTION_RESTART, 30000 }
|
|
};
|
|
SERVICE_FAILURE_ACTIONSA sfa = { 0 };
|
|
sfa.dwResetPeriod = 86400;
|
|
sfa.cActions = 3;
|
|
sfa.lpsaActions = actions;
|
|
ChangeServiceConfig2A(svc, SERVICE_CONFIG_FAILURE_ACTIONS, &sfa);
|
|
}
|
|
|
|
/* Start the service */
|
|
if (svc) {
|
|
if (StartServiceA(svc, 0, NULL))
|
|
printf("Service started.\n");
|
|
else if (GetLastError() == ERROR_SERVICE_ALREADY_RUNNING)
|
|
printf("Service already running.\n");
|
|
else
|
|
printf("StartService failed: %lu\n", GetLastError());
|
|
CloseServiceHandle(svc);
|
|
}
|
|
|
|
CloseServiceHandle(scm);
|
|
return 0;
|
|
}
|
|
|
|
static int remove_service(void)
|
|
{
|
|
SC_HANDLE scm, svc;
|
|
SERVICE_STATUS ss;
|
|
|
|
scm = OpenSCManagerA(NULL, NULL, SC_MANAGER_ALL_ACCESS);
|
|
if (!scm) {
|
|
printf("OpenSCManager failed: %lu\n", GetLastError());
|
|
return 1;
|
|
}
|
|
|
|
svc = OpenServiceA(scm, SERVICE_NAME, SERVICE_ALL_ACCESS);
|
|
if (!svc) {
|
|
printf("Service not found.\n");
|
|
CloseServiceHandle(scm);
|
|
return 1;
|
|
}
|
|
|
|
ControlService(svc, SERVICE_CONTROL_STOP, &ss);
|
|
Sleep(1000);
|
|
|
|
if (DeleteService(svc))
|
|
printf("Service removed.\n");
|
|
else
|
|
printf("DeleteService failed: %lu\n", GetLastError());
|
|
|
|
CloseServiceHandle(svc);
|
|
CloseServiceHandle(scm);
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Entry point ---- */
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
if (argc > 1) {
|
|
if (strcmp(argv[1], "--install") == 0)
|
|
return install_service();
|
|
if (strcmp(argv[1], "--remove") == 0)
|
|
return remove_service();
|
|
printf("Usage: appsandbox-agent.exe [--install | --remove]\n");
|
|
return 1;
|
|
}
|
|
|
|
/* Launched by SCM — run as service */
|
|
{
|
|
SERVICE_TABLE_ENTRYA table[] = {
|
|
{ SERVICE_NAME, (LPSERVICE_MAIN_FUNCTIONA)service_main },
|
|
{ NULL, NULL }
|
|
};
|
|
if (!StartServiceCtrlDispatcherA(table)) {
|
|
/* Not running as service — run listener directly for debugging */
|
|
printf("Running in console mode (Ctrl+C to stop)...\n");
|
|
p9_set_log(p9copy_log);
|
|
InitializeCriticalSection(&g_send_cs);
|
|
InitializeCriticalSection(&g_clip_proc_cs);
|
|
g_stop_event = CreateEventW(NULL, TRUE, FALSE, NULL);
|
|
listener_thread(NULL);
|
|
CloseHandle(g_stop_event);
|
|
DeleteCriticalSection(&g_clip_proc_cs);
|
|
DeleteCriticalSection(&g_send_cs);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|