-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(agent): Win32 pseudo-console-based agent launcher executable (#378)
> One can never find the end of the joy of playing with Win32. Not sure whether this should be marked as feature or bugfix. The aim is to prevent WSL API to cause terminal windows popping up when the agent uses it to take an action inside a WSL instance. That happened because WSL API launches console processes, so if the parent is not a console process, a new one is created. There is no public API to control that behavior. So the solution is to compile the agent as a console application, so it hosts the WSL API processes. To make it still invisible, another layer of indirection is added: a native Win32 window process is created, but instead of showing a window, it creates a pseudo-console device and hosts the agent under it, as if the launcher was a skinny version of `conhost.exe`. Being a pseudo-console, we can fully control how it's presented to the user, or not presented at all, as done in this PR. The logic behind creating a pseudo-console device and using it to host child processes is quite convoluted, in my opinion, lots of possible error paths requiring freeing resources, thus I made it more C++-ish, with RAII like semantics, either by leveraing classes or being creative with `std::unique_ptr`. For more information about this thingy, please check out: https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session and https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/ The GUI, its tests and the appxmanifest were also updated to reflect that the "agent's background process" must be the launcher. A nice side effect is that, if the launcher is killed, let's say by the task manager, the agent quits cleanly, deleting the `addr` file as if it received `Ctrl-C`. That was not (and still is not) true for killing the agent itself, it has no chance to react.
- Loading branch information
Showing
12 changed files
with
524 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
#include "console.hpp" | ||
|
||
#include <memory> | ||
#include <type_traits> | ||
|
||
#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<std::remove_pointer_t<PPROC_THREAD_ATTRIBUTE_LIST>, | ||
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<PPROC_THREAD_ATTRIBUTE_LIST>( | ||
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, std::function<std::optional<int>(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<DWORD>(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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
#pragma once | ||
#include <Windows.h> | ||
|
||
#include <functional> | ||
#include <initializer_list> | ||
#include <optional> | ||
#include <string> | ||
#include <type_traits> | ||
#include <utility> | ||
#include <vector> | ||
|
||
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<HANDLE> handles_; | ||
std::vector<std::function<std::optional<int>(HANDLE)>> listeners_; | ||
void reserve(std::size_t size); | ||
|
||
public: | ||
explicit EventLoop( | ||
std::initializer_list< | ||
std::pair<HANDLE, std::function<std::optional<int>(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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
#include "error.hpp" | ||
#include <chrono> | ||
#include <fstream> | ||
|
||
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<DWORD>(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 |
Oops, something went wrong.