Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agent): Win32 pseudo-console-based agent launcher executable #378

Merged
merged 13 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:path/path.dart' as p;
/// A try-catch block (on AssertionError) can still prevent process exit.
Future<void> buildAgentExe(
String destination, {
String exeName = 'ubuntu-pro-agent.exe',
String exeName = 'ubuntu-pro-agent-launcher.exe',
}) async {
const config = 'Debug';
const platform = 'x64';
Expand Down
2 changes: 1 addition & 1 deletion gui/packages/ubuntupro/lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 2 additions & 2 deletions msix/UbuntuProForWindows/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@
<Extensions>
<uap5:Extension
Category="windows.startupTask"
Executable="agent\ubuntu-pro-agent.exe"
Executable="agent\ubuntu-pro-agent-launcher.exe"
EntryPoint="Windows.FullTrustApplication">
<uap5:StartupTask
TaskId="UbuntuPro"
Enabled="true"
DisplayName="Ubuntu Pro For Windows background agent" />
</uap5:Extension>
<desktop:Extension Category="windows.fullTrustProcess" Executable="agent\ubuntu-pro-agent.exe">
<desktop:Extension Category="windows.fullTrustProcess" Executable="agent\ubuntu-pro-agent-launcher.exe">
<desktop:FullTrustProcess>
<desktop:ParameterGroup GroupId="agent" Parameters=""/>
</desktop:FullTrustProcess>
Expand Down
5 changes: 1 addition & 4 deletions msix/agent/agent.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
<PropertyGroup>
<GoAppRoot>$(MSBuildThisFileDirectory)..\..\windows-agent\</GoAppRoot>
<GoAppDir>$(GoAppRoot)cmd\ubuntu-pro-agent\</GoAppDir>
<GoFlags></GoFlags>
<!-- Comment below to see the console window poping up -->
<GoFlags>-ldflags -H=windowsgui</GoFlags>
<GoBuildTags Condition="'$(UP4W_TEST_WITH_MS_STORE_MOCK)' != ''">-tags=server_mocks</GoBuildTags>
</PropertyGroup>
<ItemGroup>
Expand Down Expand Up @@ -33,7 +30,7 @@
</ItemGroup>
<Message Text="Building Go artifacts to $(OutDir) and bundling @(DepAssemblies)" Importance="high"/>
<MakeDir Directories="$(OutDir)" />
<Exec Command="go build $(GoBuildTags) $(GoFlags) $(GoAppDir)" WorkingDirectory="$(OutDir)" />
<Exec Command="go build $(GoBuildTags) $(GoAppDir)" WorkingDirectory="$(OutDir)" />
</Target>
<Target Name="Clean" Condition="Exists($(TargetPath))">
<Message Text="Cleaning $(TargetPath)" Importance="high" />
Expand Down
3 changes: 3 additions & 0 deletions msix/agent/agent.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
<ProjectReference Include="..\storeapi\storeapi.vcxproj">
<Project>{4ee3b168-c58d-4ae5-a259-1b5a04c018de}</Project>
</ProjectReference>
<ProjectReference Include="..\ubuntu-pro-agent-launcher\ubuntu-pro-agent-launcher.vcxproj">
<Project>{eb330327-e1d2-4ab7-9980-fa2e56bf308b}</Project>
</ProjectReference>
</ItemGroup>
<!-- TODO: Add a reference to the store API DLL project -->
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
Expand Down
8 changes: 7 additions & 1 deletion msix/msix.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
144 changes: 144 additions & 0 deletions msix/ubuntu-pro-agent-launcher/console.cpp
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);
EduardGomezEscandell marked this conversation as resolved.
Show resolved Hide resolved
} 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
85 changes: 85 additions & 0 deletions msix/ubuntu-pro-agent-launcher/console.hpp
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.
EduardGomezEscandell marked this conversation as resolved.
Show resolved Hide resolved
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
37 changes: 37 additions & 0 deletions msix/ubuntu-pro-agent-launcher/error.cpp
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
Loading
Loading