diff --git a/gui/packages/ubuntupro/integration_test/utils/build_agent.dart b/gui/packages/ubuntupro/integration_test/utils/build_agent.dart index 1aa50f305..c675cc875 100644 --- a/gui/packages/ubuntupro/integration_test/utils/build_agent.dart +++ b/gui/packages/ubuntupro/integration_test/utils/build_agent.dart @@ -7,7 +7,7 @@ import 'package:path/path.dart' as p; /// A try-catch block (on AssertionError) can still prevent process exit. Future buildAgentExe( String destination, { - String exeName = 'ubuntu-pro-agent.exe', + String exeName = 'ubuntu-pro-agent-launcher.exe', }) async { const config = 'Debug'; const platform = 'x64'; diff --git a/gui/packages/ubuntupro/lib/constants.dart b/gui/packages/ubuntupro/lib/constants.dart index 713f326ad..13e1224eb 100644 --- a/gui/packages/ubuntupro/lib/constants.dart +++ b/gui/packages/ubuntupro/lib/constants.dart @@ -11,4 +11,4 @@ const kAddrFileName = 'addr'; const kDefaultMargin = 32.0; /// The path of the agent executable relative to the msix root directory. -const kAgentRelativePath = 'agent/ubuntu-pro-agent.exe'; +const kAgentRelativePath = 'agent/ubuntu-pro-agent-launcher.exe'; diff --git a/msix/UbuntuProForWindows/Package.appxmanifest b/msix/UbuntuProForWindows/Package.appxmanifest index b79934103..64c172b5d 100644 --- a/msix/UbuntuProForWindows/Package.appxmanifest +++ b/msix/UbuntuProForWindows/Package.appxmanifest @@ -62,14 +62,14 @@ - + diff --git a/msix/agent/agent.targets b/msix/agent/agent.targets index f4f6c64bd..c21292473 100644 --- a/msix/agent/agent.targets +++ b/msix/agent/agent.targets @@ -3,9 +3,6 @@ $(MSBuildThisFileDirectory)..\..\windows-agent\ $(GoAppRoot)cmd\ubuntu-pro-agent\ - - - -ldflags -H=windowsgui -tags=server_mocks @@ -33,7 +30,7 @@ - + diff --git a/msix/agent/agent.vcxproj b/msix/agent/agent.vcxproj index f8eca2698..74a46dc51 100644 --- a/msix/agent/agent.vcxproj +++ b/msix/agent/agent.vcxproj @@ -123,6 +123,9 @@ {4ee3b168-c58d-4ae5-a259-1b5a04c018de} + + {eb330327-e1d2-4ab7-9980-fa2e56bf308b} + diff --git a/msix/msix.sln b/msix/msix.sln index bb814a502..41b4b0483 100644 --- a/msix/msix.sln +++ b/msix/msix.sln @@ -7,10 +7,12 @@ Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "UbuntuProForWindows", "Ubun EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ubuntupro", "gui\gui.vcxproj", "{89935278-02C9-414F-8DB5-47A465BC1F2B}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "agent", "agent\agent.vcxproj", "{6427BB31-C4BE-4585-A090-10EFDFB06B04}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ubuntu-pro-agent", "agent\agent.vcxproj", "{6427BB31-C4BE-4585-A090-10EFDFB06B04}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "storeapi", "storeapi\storeapi.vcxproj", "{4EE3B168-C58D-4AE5-A259-1B5A04C018DE}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ubuntu-pro-agent-launcher", "ubuntu-pro-agent-launcher\ubuntu-pro-agent-launcher.vcxproj", "{EB330327-E1D2-4AB7-9980-FA2E56BF308B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -35,6 +37,10 @@ Global {4EE3B168-C58D-4AE5-A259-1B5A04C018DE}.Debug|x64.Build.0 = Debug|x64 {4EE3B168-C58D-4AE5-A259-1B5A04C018DE}.Release|x64.ActiveCfg = Release|x64 {4EE3B168-C58D-4AE5-A259-1B5A04C018DE}.Release|x64.Build.0 = Release|x64 + {EB330327-E1D2-4AB7-9980-FA2E56BF308B}.Debug|x64.ActiveCfg = Debug|x64 + {EB330327-E1D2-4AB7-9980-FA2E56BF308B}.Debug|x64.Build.0 = Debug|x64 + {EB330327-E1D2-4AB7-9980-FA2E56BF308B}.Release|x64.ActiveCfg = Release|x64 + {EB330327-E1D2-4AB7-9980-FA2E56BF308B}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/msix/ubuntu-pro-agent-launcher/console.cpp b/msix/ubuntu-pro-agent-launcher/console.cpp new file mode 100644 index 000000000..6c8287e2b --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/console.cpp @@ -0,0 +1,144 @@ +#include "console.hpp" + +#include +#include + +#include "error.hpp" + +namespace up4w { +PseudoConsole ::~PseudoConsole() { + if (hInRead != nullptr && hInRead != INVALID_HANDLE_VALUE) { + CloseHandle(hInRead); + } + if (hInWrite != nullptr && hInWrite != INVALID_HANDLE_VALUE) { + CloseHandle(hInWrite); + } + if (hOutRead != nullptr && hOutRead != INVALID_HANDLE_VALUE) { + CloseHandle(hOutRead); + } + if (hOutWrite != nullptr && hOutWrite != INVALID_HANDLE_VALUE) { + CloseHandle(hOutWrite); + } + if (hDevice != nullptr && hDevice != INVALID_HANDLE_VALUE) { + ClosePseudoConsole(hDevice); + } +} +PseudoConsole::PseudoConsole(COORD coordinates) { + SECURITY_ATTRIBUTES sa{sizeof(SECURITY_ATTRIBUTES), nullptr, true}; + if (!CreatePipe(&hInRead, &hInWrite, &sa, 0)) { + throw hresult_exception{HRESULT_FROM_WIN32(GetLastError())}; + } + + if (!CreatePipe(&hOutRead, &hOutWrite, &sa, 0)) { + throw hresult_exception{HRESULT_FROM_WIN32(GetLastError())}; + } + + if (auto hr = + CreatePseudoConsole(coordinates, hInRead, hOutWrite, 0, &hDevice); + FAILED(hr)) { + throw hresult_exception{hr}; + } +} + +void attr_list_deleter(PPROC_THREAD_ATTRIBUTE_LIST p) { + if (p) { + DeleteProcThreadAttributeList(p); + HeapFree(GetProcessHeap(), 0, p); + } +}; +using unique_attr_list = + std::unique_ptr, + decltype(&attr_list_deleter)>; + +/// Returns a list of attributes for process/thread creation with the +/// pseudo-console key enabled and set to [con]. +unique_attr_list PseudoConsoleProcessAttrList(HPCON con) { + PPROC_THREAD_ATTRIBUTE_LIST attrs = nullptr; + + size_t bytesRequired = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &bytesRequired); + // Allocate memory to represent the list + attrs = static_cast( + HeapAlloc(GetProcessHeap(), 0, bytesRequired)); + if (!attrs) { + throw hresult_exception{E_OUTOFMEMORY}; + } + + // Initialize the list memory location + if (!InitializeProcThreadAttributeList(attrs, 1, 0, &bytesRequired)) { + throw hresult_exception{HRESULT_FROM_WIN32(GetLastError())}; + } + + unique_attr_list result{attrs, &attr_list_deleter}; + + if (!UpdateProcThreadAttribute(attrs, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + con, sizeof(con), NULL, NULL)) { + throw hresult_exception{HRESULT_FROM_WIN32(GetLastError())}; + } + + return result; +} + +Process PseudoConsole::StartProcess(std::wstring commandLine) { + unique_attr_list attributes = PseudoConsoleProcessAttrList(hDevice); + // Prepare Startup Information structure + STARTUPINFOEX si{}; + si.StartupInfo.cb = sizeof(STARTUPINFOEX); + si.StartupInfo.hStdInput = hInRead; + si.StartupInfo.hStdOutput = hOutWrite; + si.StartupInfo.hStdError = hOutWrite; + si.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + si.lpAttributeList = attributes.get(); + + Process p{}; + if (!CreateProcessW(NULL, commandLine.data(), NULL, NULL, FALSE, + EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, + &p)) { + throw hresult_exception{HRESULT_FROM_WIN32(GetLastError())}; + } + + return p; +} + +void EventLoop::reserve(std::size_t size) { + handles_.reserve(size); + listeners_.reserve(size); +} + +EventLoop::EventLoop( + std::initializer_list< + std::pair(HANDLE)>>> + listeners) { + reserve(listeners.size()); + for (auto& [k, f] : listeners) { + handles_.push_back(k); + listeners_.push_back(f); + } +} + +int EventLoop::Run() { + do { + DWORD signaledIndex = MsgWaitForMultipleObjectsEx( + static_cast(handles_.size()), handles_.data(), INFINITE, + QS_ALLEVENTS, MWMO_INPUTAVAILABLE); + // none of the handles, thus the window message queue was signaled. + if (signaledIndex >= handles_.size()) { + MSG msg; + if (!GetMessage(&msg, NULL, 0, 0)) { + // WM_QUIT + return 0; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } else { + // invoke the listener subscribed to the handle that was signaled. + if (auto done = listeners_.at(signaledIndex)(handles_.at(signaledIndex)); + done.has_value()) { + return done.value(); + } + } + } while (true); +} + +} // namespace up4w diff --git a/msix/ubuntu-pro-agent-launcher/console.hpp b/msix/ubuntu-pro-agent-launcher/console.hpp new file mode 100644 index 000000000..36ee0f457 --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/console.hpp @@ -0,0 +1,85 @@ +#pragma once +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace up4w { +// An RAII wrapper around the PROCESS_INFORMATION structure to ease preventing +// HANDLE leaks. +struct Process : PROCESS_INFORMATION { + Process(Process const& other) = delete; + Process& operator=(Process const& other) = delete; + Process(Process&& other) noexcept { *this = std::move(other); } + Process& operator=(Process&& other) noexcept { + hProcess = std::exchange(other.hProcess, nullptr); + hThread = std::exchange(other.hThread, nullptr); + dwProcessId = std::exchange(other.dwProcessId, 0); + dwThreadId = std::exchange(other.dwThreadId, 0); + return *this; + } + Process() noexcept { + hProcess = nullptr; + hThread = nullptr; + dwProcessId = 0; + dwThreadId = 0; + } + + ~Process() noexcept { + if (hThread != nullptr && hThread != INVALID_HANDLE_VALUE) { + CloseHandle(hThread); + } + if (hProcess != nullptr && hProcess != INVALID_HANDLE_VALUE) { + CloseHandle(hProcess); + } + } +}; + +// An abstraction on top of the pseudo-console device that prevents leaking +// HANDLEs and makes it easier to start processes under itself. +class PseudoConsole { + HANDLE hInRead = nullptr; + HANDLE hInWrite = nullptr; + HANDLE hOutRead = nullptr; + HANDLE hOutWrite = nullptr; + + HPCON hDevice; + + public: + /// Constructs a new pseudo-console with the specified [dimensions]. + explicit PseudoConsole(COORD dimensions); + + HANDLE GetReadHandle() const { return hOutRead; } + + /// Starts a child process under this pseudo-console by running the fully + /// specified [commandLine]. + Process StartProcess(std::wstring commandLine); + + ~PseudoConsole(); +}; + +/// A combination of traditional window message loop with event listening. +/// Listener functions return any integer value other than nullopt to report +/// that the event loop should exit. +class EventLoop { + std::vector handles_; + std::vector(HANDLE)>> listeners_; + void reserve(std::size_t size); + + public: + explicit EventLoop( + std::initializer_list< + std::pair(HANDLE)>>> + listeners); + + // Runs the event loop until one of the listeners return a value or a closing + // message is received in the message queue. + int Run(); +}; + +} // namespace up4w diff --git a/msix/ubuntu-pro-agent-launcher/error.cpp b/msix/ubuntu-pro-agent-launcher/error.cpp new file mode 100644 index 000000000..fcc312c60 --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/error.cpp @@ -0,0 +1,37 @@ +#include "error.hpp" +#include +#include + +namespace up4w { +std::wstring UnderLocalAppDataPath(std::wstring_view destination) { + std::wstring_view localAppData = L"LOCALAPPDATA"; + std::wstring resultPath{}; + resultPath.resize(MAX_PATH); + + auto truncatedLength = + static_cast(resultPath.capacity() - destination.size() - 1); + + auto length = + GetEnvironmentVariable(localAppData.data(), resultPath.data(), truncatedLength); + if (length == 0) { + return {}; + } + + if (length > truncatedLength) { + throw hresult_exception{CO_E_PATHTOOLONG}; + } + + resultPath.insert(length, destination.data()); + return resultPath; +} + +void LogSingleShot(std::filesystem::path const& logFilePath, + std::string_view message) { + auto const time = + std::chrono::current_zone()->to_local(std::chrono::system_clock::now()); + + std::ofstream logfile{logFilePath, std::ios::app}; + logfile << std::format("{:%Y-%m-%d %T}: {}\n", time, message); +} + +} // namespace up4w diff --git a/msix/ubuntu-pro-agent-launcher/error.hpp b/msix/ubuntu-pro-agent-launcher/error.hpp new file mode 100644 index 000000000..a818ec93f --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/error.hpp @@ -0,0 +1,65 @@ +#pragma once +#include + +#include +#include +#include +#include + +namespace up4w { +// A small RAII wrapper around Win32 heap allocated strings. +class unique_string { + static void string_buffer_deleter(char* buffer) { + if (buffer) { + HeapFree(GetProcessHeap(), 0, buffer); + } + } + using unique_str = std::unique_ptr; + unique_str buffer_; + + public: + const char* c_str() { return buffer_.get(); } + explicit unique_string(char* buffer) + : buffer_{buffer, &string_buffer_deleter} {} +}; + +/// Wraps Windows HRESULT into something that resembles a std::exception +class hresult_exception { + HRESULT value; + std::source_location loc_; + + public: + unique_string message() const { + char* buffer = nullptr; + ::FormatMessageA( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, nullptr, + // Due the FORMAT_MESSAGE_ALLOCATE_BUFFER flag, what should be a char* + // (the buffer variable) has to be treated as char**, even though it's + // passed as char*. See + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-formatmessagea + value, 0, (LPSTR)&buffer, 0, nullptr); + return unique_string{buffer}; + } + std::string where() const { + return std::format("{}: {} ({})", loc_.file_name(), loc_.line(), + loc_.function_name()); + } + + explicit hresult_exception(HRESULT value, std::source_location location = + std::source_location::current()) + : value(value), loc_{location} {} + hresult_exception(const hresult_exception& other) = default; + hresult_exception(hresult_exception&& other) noexcept = default; + ~hresult_exception() = default; +}; + +/// Computes the absolute path resulting of joining the [destination] into the +/// value of the environment variable [LOCALAPPDATA]. Returns empty string if the +/// environment variable is undefined. +std::wstring UnderLocalAppDataPath(std::wstring_view destination); + +// Opens the log file, writes the message with a timestamp and closes it. +void LogSingleShot(std::filesystem::path const& logFilePath, + std::string_view message); + +} // namespace up4w diff --git a/msix/ubuntu-pro-agent-launcher/main.cpp b/msix/ubuntu-pro-agent-launcher/main.cpp new file mode 100644 index 000000000..884dd870c --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/main.cpp @@ -0,0 +1,90 @@ +/// A Windows Application that creates an invisible pseudo-console to host the +/// ubuntu-pro-agent.exe; +#include + +#include +#include +#include +#include +#include + +#include "console.hpp" +#include "error.hpp" + +std::filesystem::path const& logPath() { + static std::filesystem::path localAppDataPath = up4w::UnderLocalAppDataPath( + L"\\Ubuntu Pro\\ubuntu-pro-agent-launcher.log"); + return localAppDataPath; +} + +std::filesystem::path thisBinaryDir() { + wchar_t binaryPath[MAX_PATH]; + DWORD fnLength = GetModuleFileName(nullptr, binaryPath, MAX_PATH); + if (fnLength == 0) { + return std::filesystem::path(); + } + std::filesystem::path exePath{std::wstring_view{binaryPath, fnLength}}; + exePath.remove_filename(); + + return exePath; +} + +int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR pCmdLine, int) try { + // setup the app: pipes and console + up4w::PseudoConsole console{{.X = 80, .Y = 80}}; + + // start the child process + auto agent = thisBinaryDir() / L"ubuntu-pro-agent.exe"; + auto p = console.StartProcess(std::format(L"{} {}", agent.c_str(), pCmdLine)); + + // setup the event loop with listeners. + up4w::EventLoop ev{{ + p.hProcess, + [](HANDLE process) { + DWORD exitCode = 0; + GetExitCodeProcess(process, &exitCode); + return exitCode; + }, + }, + { + console.GetReadHandle(), + [](HANDLE read) { + std::array buffer{}; + DWORD bytesRead = 0; + ReadFile(read, &buffer[0], + static_cast(buffer.size() - 1), + &bytesRead, nullptr); + return std::nullopt; + }, + }}; + + // dispatch the event loop + return ev.Run(); + + // log errors, if any. +} catch (up4w::hresult_exception const& err) { + std::filesystem::path const& localAppDataPath = logPath(); + if (localAppDataPath.empty()) { + return 1; + } + + auto msg = std::format("{}\n\t{}", err.message().c_str(), err.where()); + up4w::LogSingleShot(localAppDataPath, msg); + return 2; +} catch (std::exception const& err) { + std::filesystem::path const& localAppDataPath = logPath(); + if (localAppDataPath.empty()) { + return 1; + } + + up4w::LogSingleShot(localAppDataPath, err.what()); + return 3; +} catch (...) { + std::filesystem::path const& localAppDataPath = logPath(); + if (localAppDataPath.empty()) { + return 1; + } + + up4w::LogSingleShot(localAppDataPath, "An unknown exception was thrown.\n"); + return 4; +} diff --git a/msix/ubuntu-pro-agent-launcher/ubuntu-pro-agent-launcher.vcxproj b/msix/ubuntu-pro-agent-launcher/ubuntu-pro-agent-launcher.vcxproj new file mode 100644 index 000000000..fbaeeaf8a --- /dev/null +++ b/msix/ubuntu-pro-agent-launcher/ubuntu-pro-agent-launcher.vcxproj @@ -0,0 +1,88 @@ + + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + Win32Proj + {eb330327-e1d2-4ab7-9980-fa2e56bf308b} + ubuntuproagentlauncher + 10.0 + + + + Application + true + v143 + Unicode + + + Application + false + v143 + true + Unicode + + + + + + + + + + + + + + + + Level3 + true + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + + + Windows + true + + + + + Level3 + true + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + stdcpp20 + + + Windows + true + true + true + + + + + + + + + + + + + + +