#ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto kLaunchTimeout = std::chrono::seconds(20); constexpr auto kShutdownTimeout = std::chrono::seconds(25); constexpr auto kPollInterval = std::chrono::milliseconds(100); constexpr const char* kReadyTrace = "[app] shell runtime initialized:"; struct ProcessWindowSearch { DWORD processId = 0; HWND hwnd = nullptr; bool requireVisible = false; }; std::wstring QuoteCommandLine(const std::filesystem::path& executablePath) { return L"\"" + executablePath.wstring() + L"\""; } void PrintFailure(std::string_view message) { std::cerr << "[xceditor_smoke] " << message << '\n'; } std::string ReadTextFile(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::in | std::ios::binary); if (!stream.is_open()) { return {}; } std::ostringstream buffer = {}; buffer << stream.rdbuf(); return buffer.str(); } bool FileContains(const std::filesystem::path& path, std::string_view needle) { const std::string content = ReadTextFile(path); return !content.empty() && content.find(needle) != std::string::npos; } BOOL CALLBACK FindProcessWindowProc(HWND hwnd, LPARAM lParam) { auto& search = *reinterpret_cast(lParam); DWORD processId = 0; GetWindowThreadProcessId(hwnd, &processId); if (processId != search.processId) { return TRUE; } if (search.requireVisible && !IsWindowVisible(hwnd)) { return TRUE; } RECT clientRect = {}; if (!GetClientRect(hwnd, &clientRect)) { return TRUE; } const LONG width = clientRect.right - clientRect.left; const LONG height = clientRect.bottom - clientRect.top; if (width <= 0 || height <= 0) { return TRUE; } search.hwnd = hwnd; return FALSE; } HWND FindProcessWindow(DWORD processId, bool requireVisible) { ProcessWindowSearch search = {}; search.processId = processId; search.requireVisible = requireVisible; EnumWindows(&FindProcessWindowProc, reinterpret_cast(&search)); return search.hwnd; } bool RemoveFileIfPresent(const std::filesystem::path& path) { std::error_code errorCode = {}; if (!std::filesystem::exists(path, errorCode)) { return true; } std::filesystem::remove(path, errorCode); return !errorCode; } bool PrepareLogDirectory( const std::filesystem::path& logDirectory, const std::filesystem::path& runtimeLogPath, const std::filesystem::path& crashLogPath) { std::error_code errorCode = {}; std::filesystem::create_directories(logDirectory, errorCode); if (errorCode) { PrintFailure("failed to create log directory: " + logDirectory.string()); return false; } if (!RemoveFileIfPresent(runtimeLogPath)) { PrintFailure("failed to clear runtime log: " + runtimeLogPath.string()); return false; } if (!RemoveFileIfPresent(crashLogPath)) { PrintFailure("failed to clear crash log: " + crashLogPath.string()); return false; } return true; } bool LaunchEditorProcess( const std::filesystem::path& executablePath, PROCESS_INFORMATION& outProcessInfo) { SetEnvironmentVariableW(L"XCUI_AUTO_CAPTURE_ON_STARTUP", L"0"); SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST", L"1"); SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS", L"12"); SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT", nullptr); STARTUPINFOW startupInfo = {}; startupInfo.cb = sizeof(startupInfo); std::wstring commandLine = QuoteCommandLine(executablePath); std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); mutableCommandLine.push_back(L'\0'); ZeroMemory(&outProcessInfo, sizeof(outProcessInfo)); return CreateProcessW( executablePath.c_str(), mutableCommandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startupInfo, &outProcessInfo) == TRUE; } bool WaitForEditorReady( HANDLE processHandle, const std::filesystem::path& runtimeLogPath) { const auto deadline = std::chrono::steady_clock::now() + kLaunchTimeout; while (std::chrono::steady_clock::now() < deadline) { if (WaitForSingleObject(processHandle, 0) == WAIT_OBJECT_0) { return false; } if (FileContains(runtimeLogPath, kReadyTrace)) { return true; } Sleep(static_cast(kPollInterval.count())); } return false; } bool WaitForEditorExit(HANDLE processHandle, DWORD timeoutMilliseconds, DWORD& outExitCode) { if (WaitForSingleObject(processHandle, timeoutMilliseconds) != WAIT_OBJECT_0) { return false; } outExitCode = 0; return GetExitCodeProcess(processHandle, &outExitCode) == TRUE; } std::string BuildEarlyExitReason( HANDLE processHandle, const std::filesystem::path& runtimeLogPath, const std::filesystem::path& crashLogPath) { DWORD exitCode = 0; GetExitCodeProcess(processHandle, &exitCode); std::ostringstream message = {}; message << "editor exited before ready, code=" << exitCode; const std::string runtimeLog = ReadTextFile(runtimeLogPath); if (!runtimeLog.empty()) { message << "\nruntime.log:\n" << runtimeLog; } const std::string crashLog = ReadTextFile(crashLogPath); if (!crashLog.empty()) { message << "\ncrash.log:\n" << crashLog; } return message.str(); } std::string BuildTimeoutReason( const std::filesystem::path& runtimeLogPath, const std::filesystem::path& crashLogPath) { std::ostringstream message = {}; message << "editor did not reach ready state within " << std::chrono::duration_cast(kLaunchTimeout).count() << "ms"; const std::string runtimeLog = ReadTextFile(runtimeLogPath); if (!runtimeLog.empty()) { message << "\nruntime.log:\n" << runtimeLog; } const std::string crashLog = ReadTextFile(crashLogPath); if (!crashLog.empty()) { message << "\ncrash.log:\n" << crashLog; } return message.str(); } void CloseProcessHandles(PROCESS_INFORMATION& processInfo) { if (processInfo.hThread != nullptr) { CloseHandle(processInfo.hThread); processInfo.hThread = nullptr; } if (processInfo.hProcess != nullptr) { CloseHandle(processInfo.hProcess); processInfo.hProcess = nullptr; } } } // namespace int wmain(int argc, wchar_t* argv[]) { if (argc < 2 || argv[1] == nullptr || argv[1][0] == L'\0') { PrintFailure("missing XCEditor path"); return 1; } const std::filesystem::path executablePath = std::filesystem::path(argv[1]).lexically_normal(); if (!std::filesystem::exists(executablePath)) { PrintFailure("XCEditor not found: " + executablePath.string()); return 1; } const std::filesystem::path logDirectory = (executablePath.parent_path() / "logs").lexically_normal(); const std::filesystem::path runtimeLogPath = (logDirectory / "runtime.log").lexically_normal(); const std::filesystem::path crashLogPath = (logDirectory / "crash.log").lexically_normal(); if (!PrepareLogDirectory(logDirectory, runtimeLogPath, crashLogPath)) { return 1; } PROCESS_INFORMATION processInfo = {}; if (!LaunchEditorProcess(executablePath, processInfo)) { PrintFailure("failed to launch XCEditor"); return 1; } if (!WaitForEditorReady( processInfo.hProcess, runtimeLogPath)) { const DWORD waitState = WaitForSingleObject(processInfo.hProcess, 0); const std::string message = waitState == WAIT_OBJECT_0 ? BuildEarlyExitReason(processInfo.hProcess, runtimeLogPath, crashLogPath) : BuildTimeoutReason(runtimeLogPath, crashLogPath); PrintFailure(message); TerminateProcess(processInfo.hProcess, 2); CloseProcessHandles(processInfo); return 1; } DWORD exitCode = 0; if (!WaitForEditorExit( processInfo.hProcess, static_cast( std::chrono::duration_cast(kShutdownTimeout).count()), exitCode)) { PrintFailure("editor did not exit after WM_CLOSE"); TerminateProcess(processInfo.hProcess, 4); CloseProcessHandles(processInfo); return 1; } CloseProcessHandles(processInfo); if (exitCode != 0) { PrintFailure("editor exited with non-zero code: " + std::to_string(exitCode)); return 1; } const std::string crashLog = ReadTextFile(crashLogPath); if (!crashLog.empty()) { PrintFailure("crash log was generated:\n" + crashLog); return 1; } return 0; }