mirror of
https://github.com/jamesstringer90/appsandbox.git
synced 2026-10-22 02:52:49 -05:00
Expose guest accessibility trees through a host AppKit helper and forward attributes, actions, text operations, and input through shared transport. Package macOS and Windows guest collectors and integrate viewer lifecycle, refresh scheduling, and geometry handling.
2825 lines
97 KiB
C
2825 lines
97 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 */
|
|
static HANDLE g_accessibility_thread;
|
|
|
|
/* ---- 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 HANDLE spawn_accessibility_in_session(DWORD session_id, HANDLE job)
|
|
{
|
|
wchar_t exe_path[MAX_PATH];
|
|
wchar_t *slash;
|
|
HANDLE token = NULL;
|
|
LPVOID env = NULL;
|
|
STARTUPINFOW si = {0};
|
|
PROCESS_INFORMATION pi = {0};
|
|
DWORD length = GetModuleFileNameW(NULL, exe_path, MAX_PATH);
|
|
if (!length || length >= MAX_PATH) return NULL;
|
|
slash = wcsrchr(exe_path, L'\\');
|
|
if (!slash) return NULL;
|
|
slash[1] = L'\0';
|
|
if (wcscat_s(exe_path, MAX_PATH, L"appsandbox-accessibility.exe") != 0 ||
|
|
GetFileAttributesW(exe_path) == INVALID_FILE_ATTRIBUTES) return NULL;
|
|
if (!WTSQueryUserToken(session_id, &token)) return NULL;
|
|
if (!CreateEnvironmentBlock(&env, token, FALSE)) {
|
|
CloseHandle(token);
|
|
return NULL;
|
|
}
|
|
si.cb = sizeof(si);
|
|
si.lpDesktop = L"WinSta0\\Default";
|
|
if (!CreateProcessAsUserW(token, exe_path, NULL, NULL, NULL, FALSE,
|
|
CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED,
|
|
env, NULL, &si, &pi)) {
|
|
DestroyEnvironmentBlock(env);
|
|
CloseHandle(token);
|
|
return NULL;
|
|
}
|
|
DestroyEnvironmentBlock(env);
|
|
CloseHandle(token);
|
|
if (!AssignProcessToJobObject(job, pi.hProcess) || ResumeThread(pi.hThread) == (DWORD)-1) {
|
|
TerminateProcess(pi.hProcess, 1);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
return NULL;
|
|
}
|
|
CloseHandle(pi.hThread);
|
|
agent_log("Accessibility helper: spawned PID %lu in session %lu (as user).", pi.dwProcessId, session_id);
|
|
return pi.hProcess;
|
|
}
|
|
|
|
static DWORD WINAPI accessibility_monitor_thread(LPVOID context)
|
|
{
|
|
HANDLE process = NULL;
|
|
HANDLE job = CreateJobObjectW(NULL, NULL);
|
|
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = {0};
|
|
DWORD current_session = 0xFFFFFFFF;
|
|
(void)context;
|
|
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
if (!job) return 1;
|
|
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &limits, sizeof(limits))) {
|
|
CloseHandle(job);
|
|
return 1;
|
|
}
|
|
while (WaitForSingleObject(g_stop_event, 1000) == WAIT_TIMEOUT) {
|
|
DWORD session_id = WTSGetActiveConsoleSessionId();
|
|
if (process && (session_id != current_session || WaitForSingleObject(process, 0) != WAIT_TIMEOUT)) {
|
|
TerminateProcess(process, 0);
|
|
WaitForSingleObject(process, 2000);
|
|
CloseHandle(process);
|
|
process = NULL;
|
|
}
|
|
if (!process && session_id != 0xFFFFFFFF) {
|
|
process = spawn_accessibility_in_session(session_id, job);
|
|
if (process) current_session = session_id;
|
|
}
|
|
}
|
|
CloseHandle(job);
|
|
if (process) {
|
|
WaitForSingleObject(process, 2000);
|
|
CloseHandle(process);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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();
|
|
|
|
g_accessibility_thread = CreateThread(NULL, 0, accessibility_monitor_thread, NULL, 0, NULL);
|
|
|
|
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.");
|
|
if (g_accessibility_thread) {
|
|
WaitForSingleObject(g_accessibility_thread, INFINITE);
|
|
CloseHandle(g_accessibility_thread);
|
|
g_accessibility_thread = NULL;
|
|
}
|
|
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;
|
|
}
|