aboutsummaryrefslogtreecommitdiff
path: root/src/zencompute/runners/windowsrunner.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zencompute/runners/windowsrunner.cpp')
-rw-r--r--src/zencompute/runners/windowsrunner.cpp460
1 files changed, 460 insertions, 0 deletions
diff --git a/src/zencompute/runners/windowsrunner.cpp b/src/zencompute/runners/windowsrunner.cpp
new file mode 100644
index 000000000..e9a1ae8b6
--- /dev/null
+++ b/src/zencompute/runners/windowsrunner.cpp
@@ -0,0 +1,460 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include "windowsrunner.h"
+
+#if ZEN_WITH_COMPUTE_SERVICES && ZEN_PLATFORM_WINDOWS
+
+# include <zencore/compactbinary.h>
+# include <zencore/compactbinarypackage.h>
+# include <zencore/except.h>
+# include <zencore/except_fmt.h>
+# include <zencore/filesystem.h>
+# include <zencore/fmtutils.h>
+# include <zencore/scopeguard.h>
+# include <zencore/trace.h>
+# include <zencore/system.h>
+# include <zencore/timer.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+# include <userenv.h>
+# include <aclapi.h>
+# include <sddl.h>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+namespace zen::compute {
+
+using namespace std::literals;
+
+WindowsProcessRunner::WindowsProcessRunner(ChunkResolver& Resolver,
+ const std::filesystem::path& BaseDir,
+ DeferredDirectoryDeleter& Deleter,
+ WorkerThreadPool& WorkerPool,
+ bool Sandboxed,
+ int32_t MaxConcurrentActions)
+: LocalProcessRunner(Resolver, BaseDir, Deleter, WorkerPool, MaxConcurrentActions)
+, m_Sandboxed(Sandboxed)
+{
+ if (!m_Sandboxed)
+ {
+ return;
+ }
+
+ // Build a unique profile name per process to avoid collisions
+ m_AppContainerName = L"zenserver-sandbox-" + std::to_wstring(GetCurrentProcessId());
+
+ // Clean up any stale profile from a previous crash
+ DeleteAppContainerProfile(m_AppContainerName.c_str());
+
+ PSID Sid = nullptr;
+
+ HRESULT Hr = CreateAppContainerProfile(m_AppContainerName.c_str(),
+ m_AppContainerName.c_str(), // display name
+ m_AppContainerName.c_str(), // description
+ nullptr, // no capabilities
+ 0, // capability count
+ &Sid);
+
+ if (FAILED(Hr))
+ {
+ throw zen::runtime_error("CreateAppContainerProfile failed: HRESULT 0x{:08X}", static_cast<uint32_t>(Hr));
+ }
+
+ m_AppContainerSid = Sid;
+
+ ZEN_INFO("AppContainer sandboxing enabled for child processes (profile={})", WideToUtf8(m_AppContainerName));
+}
+
+WindowsProcessRunner::~WindowsProcessRunner()
+{
+ if (m_AppContainerSid)
+ {
+ FreeSid(m_AppContainerSid);
+ m_AppContainerSid = nullptr;
+ }
+
+ if (!m_AppContainerName.empty())
+ {
+ DeleteAppContainerProfile(m_AppContainerName.c_str());
+ }
+}
+
+void
+WindowsProcessRunner::GrantAppContainerAccess(const std::filesystem::path& Path, DWORD AccessMask)
+{
+ PACL ExistingDacl = nullptr;
+ PSECURITY_DESCRIPTOR SecurityDescriptor = nullptr;
+
+ DWORD Result = GetNamedSecurityInfoW(Path.c_str(),
+ SE_FILE_OBJECT,
+ DACL_SECURITY_INFORMATION,
+ nullptr,
+ nullptr,
+ &ExistingDacl,
+ nullptr,
+ &SecurityDescriptor);
+
+ if (Result != ERROR_SUCCESS)
+ {
+ throw zen::runtime_error("GetNamedSecurityInfoW failed for '{}': {}", Path.string(), GetSystemErrorAsString(Result));
+ }
+
+ auto $0 = MakeGuard([&] { LocalFree(SecurityDescriptor); });
+
+ EXPLICIT_ACCESSW Access{};
+ Access.grfAccessPermissions = AccessMask;
+ Access.grfAccessMode = SET_ACCESS;
+ Access.grfInheritance = OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE;
+ Access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
+ Access.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
+ Access.Trustee.ptstrName = static_cast<LPWSTR>(m_AppContainerSid);
+
+ PACL NewDacl = nullptr;
+
+ Result = SetEntriesInAclW(1, &Access, ExistingDacl, &NewDacl);
+ if (Result != ERROR_SUCCESS)
+ {
+ throw zen::runtime_error("SetEntriesInAclW failed for '{}': {}", Path.string(), GetSystemErrorAsString(Result));
+ }
+
+ auto $1 = MakeGuard([&] { LocalFree(NewDacl); });
+
+ Result = SetNamedSecurityInfoW(const_cast<LPWSTR>(Path.c_str()),
+ SE_FILE_OBJECT,
+ DACL_SECURITY_INFORMATION,
+ nullptr,
+ nullptr,
+ NewDacl,
+ nullptr);
+
+ if (Result != ERROR_SUCCESS)
+ {
+ throw zen::runtime_error("SetNamedSecurityInfoW failed for '{}': {}", Path.string(), GetSystemErrorAsString(Result));
+ }
+}
+
+SubmitResult
+WindowsProcessRunner::SubmitAction(Ref<RunnerAction> Action)
+{
+ ZEN_TRACE_CPU("WindowsProcessRunner::SubmitAction");
+ std::optional<PreparedAction> Prepared = PrepareActionSubmission(Action);
+
+ if (!Prepared)
+ {
+ return SubmitResult{.IsAccepted = false};
+ }
+
+ // Set up environment variables
+
+ CbObject WorkerDescription = Prepared->WorkerPackage.GetObject();
+
+ StringBuilder<1024> EnvironmentBlock;
+
+ for (auto& It : WorkerDescription["environment"sv])
+ {
+ EnvironmentBlock.Append(It.AsString());
+ EnvironmentBlock.Append('\0');
+ }
+ EnvironmentBlock.Append('\0');
+ EnvironmentBlock.Append('\0');
+
+ // Execute process - this spawns the child process immediately without waiting
+ // for completion
+
+ std::string_view ExecPath = WorkerDescription["path"sv].AsString();
+ std::filesystem::path ExePath = Prepared->WorkerPath / std::filesystem::path(ExecPath).make_preferred();
+
+ ExtendableWideStringBuilder<512> CommandLine;
+ CommandLine.Append(L'"');
+ CommandLine.Append(ExePath.c_str());
+ CommandLine.Append(L'"');
+ CommandLine.Append(L" -Build=build.action");
+
+ LPSECURITY_ATTRIBUTES lpProcessAttributes = nullptr;
+ LPSECURITY_ATTRIBUTES lpThreadAttributes = nullptr;
+ BOOL bInheritHandles = FALSE;
+ DWORD dwCreationFlags = 0;
+
+ ZEN_DEBUG("Executing: {} (sandboxed={})", WideToUtf8(CommandLine.c_str()), m_Sandboxed);
+
+ CommandLine.EnsureNulTerminated();
+
+ PROCESS_INFORMATION ProcessInformation{};
+
+ if (m_Sandboxed)
+ {
+ // Grant AppContainer access to sandbox and worker directories
+ GrantAppContainerAccess(Prepared->SandboxPath, FILE_ALL_ACCESS);
+ GrantAppContainerAccess(Prepared->WorkerPath, FILE_GENERIC_READ | FILE_GENERIC_EXECUTE);
+
+ // Set up extended startup info with AppContainer security capabilities
+ SECURITY_CAPABILITIES SecurityCapabilities{};
+ SecurityCapabilities.AppContainerSid = m_AppContainerSid;
+ SecurityCapabilities.Capabilities = nullptr;
+ SecurityCapabilities.CapabilityCount = 0;
+
+ SIZE_T AttrListSize = 0;
+ InitializeProcThreadAttributeList(nullptr, 1, 0, &AttrListSize);
+
+ auto AttrList = static_cast<PPROC_THREAD_ATTRIBUTE_LIST>(malloc(AttrListSize));
+ auto $0 = MakeGuard([&] { free(AttrList); });
+
+ if (!InitializeProcThreadAttributeList(AttrList, 1, 0, &AttrListSize))
+ {
+ zen::ThrowLastError("InitializeProcThreadAttributeList failed");
+ }
+
+ auto $1 = MakeGuard([&] { DeleteProcThreadAttributeList(AttrList); });
+
+ if (!UpdateProcThreadAttribute(AttrList,
+ 0,
+ PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES,
+ &SecurityCapabilities,
+ sizeof(SecurityCapabilities),
+ nullptr,
+ nullptr))
+ {
+ zen::ThrowLastError("UpdateProcThreadAttribute (SECURITY_CAPABILITIES) failed");
+ }
+
+ STARTUPINFOEXW StartupInfoEx{};
+ StartupInfoEx.StartupInfo.cb = sizeof(STARTUPINFOEXW);
+ StartupInfoEx.lpAttributeList = AttrList;
+
+ dwCreationFlags |= EXTENDED_STARTUPINFO_PRESENT;
+
+ BOOL Success = CreateProcessW(nullptr,
+ CommandLine.Data(),
+ lpProcessAttributes,
+ lpThreadAttributes,
+ bInheritHandles,
+ dwCreationFlags,
+ (LPVOID)EnvironmentBlock.Data(),
+ Prepared->SandboxPath.c_str(),
+ &StartupInfoEx.StartupInfo,
+ /* out */ &ProcessInformation);
+
+ if (!Success)
+ {
+ zen::ThrowLastError("Unable to launch sandboxed process");
+ }
+ }
+ else
+ {
+ STARTUPINFO StartupInfo{};
+ StartupInfo.cb = sizeof StartupInfo;
+
+ BOOL Success = CreateProcessW(nullptr,
+ CommandLine.Data(),
+ lpProcessAttributes,
+ lpThreadAttributes,
+ bInheritHandles,
+ dwCreationFlags,
+ (LPVOID)EnvironmentBlock.Data(),
+ Prepared->SandboxPath.c_str(),
+ &StartupInfo,
+ /* out */ &ProcessInformation);
+
+ if (!Success)
+ {
+ zen::ThrowLastError("Unable to launch process");
+ }
+ }
+
+ CloseHandle(ProcessInformation.hThread);
+
+ Ref<RunningAction> NewAction{new RunningAction()};
+ NewAction->Action = Action;
+ NewAction->ProcessHandle = ProcessInformation.hProcess;
+ NewAction->SandboxPath = std::move(Prepared->SandboxPath);
+
+ {
+ RwLock::ExclusiveLockScope _(m_RunningLock);
+
+ m_RunningMap[Prepared->ActionLsn] = std::move(NewAction);
+ }
+
+ Action->SetActionState(RunnerAction::State::Running);
+
+ return SubmitResult{.IsAccepted = true};
+}
+
+void
+WindowsProcessRunner::SweepRunningActions()
+{
+ ZEN_TRACE_CPU("WindowsProcessRunner::SweepRunningActions");
+ std::vector<Ref<RunningAction>> CompletedActions;
+
+ m_RunningLock.WithExclusiveLock([&] {
+ for (auto It = begin(m_RunningMap), ItEnd = end(m_RunningMap); It != ItEnd;)
+ {
+ Ref<RunningAction> Running = It->second;
+
+ DWORD ExitCode = 0;
+ BOOL IsSuccess = GetExitCodeProcess(Running->ProcessHandle, &ExitCode);
+
+ if (IsSuccess && ExitCode != STILL_ACTIVE)
+ {
+ CloseHandle(Running->ProcessHandle);
+ Running->ProcessHandle = INVALID_HANDLE_VALUE;
+ Running->ExitCode = ExitCode;
+
+ CompletedActions.push_back(std::move(Running));
+ It = m_RunningMap.erase(It);
+ }
+ else
+ {
+ ++It;
+ }
+ }
+ });
+
+ ProcessCompletedActions(CompletedActions);
+}
+
+void
+WindowsProcessRunner::CancelRunningActions()
+{
+ ZEN_TRACE_CPU("WindowsProcessRunner::CancelRunningActions");
+ Stopwatch Timer;
+ std::unordered_map<int, Ref<RunningAction>> RunningMap;
+
+ m_RunningLock.WithExclusiveLock([&] { std::swap(RunningMap, m_RunningMap); });
+
+ if (RunningMap.empty())
+ {
+ return;
+ }
+
+ ZEN_INFO("cancelling all running actions");
+
+ // For expedience we initiate the process termination for all known
+ // processes before attempting to wait for them to exit.
+
+ // Initiate termination for all known processes before waiting for them to exit.
+
+ for (const auto& Kv : RunningMap)
+ {
+ Ref<RunningAction> Running = Kv.second;
+
+ BOOL TermSuccess = TerminateProcess(Running->ProcessHandle, 222);
+
+ if (!TermSuccess)
+ {
+ DWORD LastError = GetLastError();
+
+ if (LastError != ERROR_ACCESS_DENIED)
+ {
+ ZEN_WARN("TerminateProcess for LSN {} not successful: {}", Running->Action->ActionLsn, GetSystemErrorAsString(LastError));
+ }
+ }
+ }
+
+ // Wait for all processes and clean up, regardless of whether TerminateProcess succeeded.
+
+ for (auto& [Lsn, Running] : RunningMap)
+ {
+ if (Running->ProcessHandle != INVALID_HANDLE_VALUE)
+ {
+ DWORD WaitResult = WaitForSingleObject(Running->ProcessHandle, 2000);
+
+ if (WaitResult != WAIT_OBJECT_0)
+ {
+ ZEN_WARN("wait for LSN {}: process exit did not succeed, result = {}", Running->Action->ActionLsn, WaitResult);
+ }
+ else
+ {
+ ZEN_DEBUG("LSN {}: process exit OK", Running->Action->ActionLsn);
+ }
+
+ CloseHandle(Running->ProcessHandle);
+ Running->ProcessHandle = INVALID_HANDLE_VALUE;
+ }
+
+ m_DeferredDeleter.Enqueue(Running->Action->ActionLsn, std::move(Running->SandboxPath));
+ Running->Action->SetActionState(RunnerAction::State::Failed);
+ }
+
+ ZEN_INFO("DONE - cancelled {} running processes (took {})", RunningMap.size(), NiceTimeSpanMs(Timer.GetElapsedTimeMs()));
+}
+
+bool
+WindowsProcessRunner::CancelAction(int ActionLsn)
+{
+ ZEN_TRACE_CPU("WindowsProcessRunner::CancelAction");
+
+ // Hold the shared lock while terminating to prevent the sweep thread from
+ // closing the handle between our lookup and TerminateProcess call.
+ bool Sent = false;
+
+ m_RunningLock.WithSharedLock([&] {
+ auto It = m_RunningMap.find(ActionLsn);
+ if (It == m_RunningMap.end())
+ {
+ return;
+ }
+
+ Ref<RunningAction> Target = It->second;
+ if (Target->ProcessHandle == INVALID_HANDLE_VALUE)
+ {
+ return;
+ }
+
+ BOOL TermSuccess = TerminateProcess(Target->ProcessHandle, 222);
+
+ if (!TermSuccess)
+ {
+ DWORD LastError = GetLastError();
+
+ if (LastError != ERROR_ACCESS_DENIED)
+ {
+ ZEN_WARN("CancelAction: TerminateProcess for LSN {} not successful: {}", ActionLsn, GetSystemErrorAsString(LastError));
+ }
+
+ return;
+ }
+
+ ZEN_DEBUG("CancelAction: initiated cancellation of LSN {}", ActionLsn);
+ Sent = true;
+ });
+
+ // The monitor thread will pick up the process exit and mark the action as Failed.
+ return Sent;
+}
+
+void
+WindowsProcessRunner::SampleProcessCpu(RunningAction& Running)
+{
+ FILETIME CreationTime, ExitTime, KernelTime, UserTime;
+ if (!GetProcessTimes(Running.ProcessHandle, &CreationTime, &ExitTime, &KernelTime, &UserTime))
+ {
+ return;
+ }
+
+ auto FtToU64 = [](FILETIME Ft) -> uint64_t { return (static_cast<uint64_t>(Ft.dwHighDateTime) << 32) | Ft.dwLowDateTime; };
+
+ // FILETIME values are in 100-nanosecond intervals
+ const uint64_t CurrentOsTicks = FtToU64(KernelTime) + FtToU64(UserTime);
+ const uint64_t NowTicks = GetHifreqTimerValue();
+
+ // Cumulative CPU seconds (absolute, available from first sample): 100ns → seconds
+ Running.Action->CpuSeconds.store(static_cast<float>(static_cast<double>(CurrentOsTicks) / 10'000'000.0), std::memory_order_relaxed);
+
+ if (Running.LastCpuSampleTicks != 0 && Running.LastCpuOsTicks != 0)
+ {
+ const uint64_t ElapsedMs = Stopwatch::GetElapsedTimeMs(NowTicks - Running.LastCpuSampleTicks);
+ if (ElapsedMs > 0)
+ {
+ const uint64_t DeltaOsTicks = CurrentOsTicks - Running.LastCpuOsTicks;
+ // 100ns → ms: divide by 10000; then as percent of elapsed ms
+ const float CpuPct = static_cast<float>(static_cast<double>(DeltaOsTicks) / 10000.0 / ElapsedMs * 100.0);
+ Running.Action->CpuUsagePercent.store(CpuPct, std::memory_order_relaxed);
+ }
+ }
+
+ Running.LastCpuSampleTicks = NowTicks;
+ Running.LastCpuOsTicks = CurrentOsTicks;
+}
+
+} // namespace zen::compute
+
+#endif