|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "Agent.h" |
|
|
|
#include <windows.h> |
|
|
|
#include <stdint.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
|
|
#include <string> |
|
#include <utility> |
|
#include <vector> |
|
|
|
#include "../include/winpty_constants.h" |
|
|
|
#include "../shared/AgentMsg.h" |
|
#include "../shared/Buffer.h" |
|
#include "../shared/DebugClient.h" |
|
#include "../shared/GenRandom.h" |
|
#include "../shared/StringBuilder.h" |
|
#include "../shared/StringUtil.h" |
|
#include "../shared/WindowsVersion.h" |
|
#include "../shared/WinptyAssert.h" |
|
|
|
#include "ConsoleFont.h" |
|
#include "ConsoleInput.h" |
|
#include "NamedPipe.h" |
|
#include "Scraper.h" |
|
#include "Terminal.h" |
|
#include "Win32ConsoleBuffer.h" |
|
|
|
namespace { |
|
|
|
static BOOL WINAPI consoleCtrlHandler(DWORD dwCtrlType) |
|
{ |
|
if (dwCtrlType == CTRL_C_EVENT) { |
|
|
|
return TRUE; |
|
} |
|
return FALSE; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void detectNewWindows10Console( |
|
Win32Console &console, Win32ConsoleBuffer &buffer) |
|
{ |
|
if (!isAtLeastWindows8()) { |
|
return; |
|
} |
|
|
|
ConsoleScreenBufferInfo info = buffer.bufferInfo(); |
|
|
|
|
|
|
|
if (info.srWindow.Left == info.srWindow.Right && |
|
info.srWindow.Top == info.srWindow.Bottom) { |
|
trace("detectNewWindows10Console: Initial console window was 1x1 -- " |
|
"expanding for test"); |
|
setSmallFont(buffer.conout(), 400, false); |
|
buffer.moveWindow(SmallRect(0, 0, 1, 1)); |
|
buffer.resizeBuffer(Coord(400, 1)); |
|
buffer.moveWindow(SmallRect(0, 0, 2, 1)); |
|
|
|
|
|
|
|
|
|
const auto largest = GetLargestConsoleWindowSize(buffer.conout()); |
|
buffer.moveWindow( |
|
SmallRect(0, 0, std::min(largest.X, buffer.bufferSize().X), 1)); |
|
info = buffer.bufferInfo(); |
|
ASSERT(info.srWindow.Right > info.srWindow.Left && |
|
"Could not expand console window from 1x1"); |
|
} |
|
|
|
|
|
const Coord initialPosition(info.srWindow.Right, info.srWindow.Bottom); |
|
buffer.setCursorPosition(initialPosition); |
|
ASSERT(!console.frozen()); |
|
console.setFreezeUsesMark(true); |
|
console.setFrozen(true); |
|
const bool isNewW10 = (buffer.cursorPosition() == initialPosition); |
|
console.setFrozen(false); |
|
buffer.setCursorPosition(Coord(0, 0)); |
|
|
|
trace("Attempting to detect new Windows 10 console using MARK: %s", |
|
isNewW10 ? "detected" : "not detected"); |
|
console.setFreezeUsesMark(false); |
|
console.setNewW10(isNewW10); |
|
} |
|
|
|
static inline WriteBuffer newPacket() { |
|
WriteBuffer packet; |
|
packet.putRawValue<uint64_t>(0); |
|
return packet; |
|
} |
|
|
|
static HANDLE duplicateHandle(HANDLE h) { |
|
HANDLE ret = nullptr; |
|
if (!DuplicateHandle( |
|
GetCurrentProcess(), h, |
|
GetCurrentProcess(), &ret, |
|
0, FALSE, DUPLICATE_SAME_ACCESS)) { |
|
ASSERT(false && "DuplicateHandle failed!"); |
|
} |
|
return ret; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
static int64_t int64FromHandle(HANDLE h) { |
|
return static_cast<int64_t>(reinterpret_cast<intptr_t>(h)); |
|
} |
|
|
|
} |
|
|
|
Agent::Agent(LPCWSTR controlPipeName, |
|
uint64_t agentFlags, |
|
int mouseMode, |
|
int initialCols, |
|
int initialRows) : |
|
m_useConerr((agentFlags & WINPTY_FLAG_CONERR) != 0), |
|
m_plainMode((agentFlags & WINPTY_FLAG_PLAIN_OUTPUT) != 0), |
|
m_mouseMode(mouseMode) |
|
{ |
|
trace("Agent::Agent entered"); |
|
|
|
ASSERT(initialCols >= 1 && initialRows >= 1); |
|
initialCols = std::min(initialCols, MAX_CONSOLE_WIDTH); |
|
initialRows = std::min(initialRows, MAX_CONSOLE_HEIGHT); |
|
|
|
const bool outputColor = |
|
!m_plainMode || (agentFlags & WINPTY_FLAG_COLOR_ESCAPES); |
|
const Coord initialSize(initialCols, initialRows); |
|
|
|
auto primaryBuffer = openPrimaryBuffer(); |
|
if (m_useConerr) { |
|
m_errorBuffer = Win32ConsoleBuffer::createErrorBuffer(); |
|
} |
|
|
|
detectNewWindows10Console(m_console, *primaryBuffer); |
|
|
|
m_controlPipe = &connectToControlPipe(controlPipeName); |
|
m_coninPipe = &createDataServerPipe(false, L"conin"); |
|
m_conoutPipe = &createDataServerPipe(true, L"conout"); |
|
if (m_useConerr) { |
|
m_conerrPipe = &createDataServerPipe(true, L"conerr"); |
|
} |
|
|
|
|
|
{ |
|
auto setupPacket = newPacket(); |
|
setupPacket.putWString(m_coninPipe->name()); |
|
setupPacket.putWString(m_conoutPipe->name()); |
|
if (m_useConerr) { |
|
setupPacket.putWString(m_conerrPipe->name()); |
|
} |
|
writePacket(setupPacket); |
|
} |
|
|
|
std::unique_ptr<Terminal> primaryTerminal; |
|
primaryTerminal.reset(new Terminal(*m_conoutPipe, |
|
m_plainMode, |
|
outputColor)); |
|
m_primaryScraper.reset(new Scraper(m_console, |
|
*primaryBuffer, |
|
std::move(primaryTerminal), |
|
initialSize)); |
|
if (m_useConerr) { |
|
std::unique_ptr<Terminal> errorTerminal; |
|
errorTerminal.reset(new Terminal(*m_conerrPipe, |
|
m_plainMode, |
|
outputColor)); |
|
m_errorScraper.reset(new Scraper(m_console, |
|
*m_errorBuffer, |
|
std::move(errorTerminal), |
|
initialSize)); |
|
} |
|
|
|
m_console.setTitle(m_currentTitle); |
|
|
|
const HANDLE conin = GetStdHandle(STD_INPUT_HANDLE); |
|
m_consoleInput.reset( |
|
new ConsoleInput(conin, m_mouseMode, *this, m_console)); |
|
|
|
|
|
|
|
|
|
|
|
SetConsoleCtrlHandler(NULL, FALSE); |
|
SetConsoleCtrlHandler(consoleCtrlHandler, TRUE); |
|
|
|
setPollInterval(25); |
|
} |
|
|
|
Agent::~Agent() |
|
{ |
|
trace("Agent::~Agent entered"); |
|
agentShutdown(); |
|
if (m_childProcess != NULL) { |
|
CloseHandle(m_childProcess); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
void Agent::sendDsr() |
|
{ |
|
if (!m_plainMode && !m_conoutPipe->isClosed()) { |
|
m_conoutPipe->write("\x1B[6n"); |
|
} |
|
} |
|
|
|
NamedPipe &Agent::connectToControlPipe(LPCWSTR pipeName) |
|
{ |
|
NamedPipe &pipe = createNamedPipe(); |
|
pipe.connectToServer(pipeName, NamedPipe::OpenMode::Duplex); |
|
pipe.setReadBufferSize(64 * 1024); |
|
return pipe; |
|
} |
|
|
|
|
|
NamedPipe &Agent::createDataServerPipe(bool write, const wchar_t *kind) |
|
{ |
|
const auto name = |
|
(WStringBuilder(128) |
|
<< L"\\\\.\\pipe\\winpty-" |
|
<< kind << L'-' |
|
<< GenRandom().uniqueName()).str_moved(); |
|
NamedPipe &pipe = createNamedPipe(); |
|
pipe.openServerPipe( |
|
name.c_str(), |
|
write ? NamedPipe::OpenMode::Writing |
|
: NamedPipe::OpenMode::Reading, |
|
write ? 8192 : 0, |
|
write ? 0 : 256); |
|
if (!write) { |
|
pipe.setReadBufferSize(64 * 1024); |
|
} |
|
return pipe; |
|
} |
|
|
|
void Agent::onPipeIo(NamedPipe &namedPipe) |
|
{ |
|
if (&namedPipe == m_conoutPipe || &namedPipe == m_conerrPipe) { |
|
autoClosePipesForShutdown(); |
|
} else if (&namedPipe == m_coninPipe) { |
|
pollConinPipe(); |
|
} else if (&namedPipe == m_controlPipe) { |
|
pollControlPipe(); |
|
} |
|
} |
|
|
|
void Agent::pollControlPipe() |
|
{ |
|
if (m_controlPipe->isClosed()) { |
|
trace("Agent exiting (control pipe is closed)"); |
|
shutdown(); |
|
return; |
|
} |
|
|
|
while (true) { |
|
uint64_t packetSize = 0; |
|
const auto amt1 = |
|
m_controlPipe->peek(&packetSize, sizeof(packetSize)); |
|
if (amt1 < sizeof(packetSize)) { |
|
break; |
|
} |
|
ASSERT(packetSize >= sizeof(packetSize) && packetSize <= SIZE_MAX); |
|
if (m_controlPipe->bytesAvailable() < packetSize) { |
|
if (m_controlPipe->readBufferSize() < packetSize) { |
|
m_controlPipe->setReadBufferSize(packetSize); |
|
} |
|
break; |
|
} |
|
std::vector<char> packetData; |
|
packetData.resize(packetSize); |
|
const auto amt2 = m_controlPipe->read(packetData.data(), packetSize); |
|
ASSERT(amt2 == packetSize); |
|
try { |
|
ReadBuffer buffer(std::move(packetData)); |
|
buffer.getRawValue<uint64_t>(); |
|
handlePacket(buffer); |
|
} catch (const ReadBuffer::DecodeError&) { |
|
ASSERT(false && "Decode error"); |
|
} |
|
} |
|
} |
|
|
|
void Agent::handlePacket(ReadBuffer &packet) |
|
{ |
|
const int type = packet.getInt32(); |
|
switch (type) { |
|
case AgentMsg::StartProcess: |
|
handleStartProcessPacket(packet); |
|
break; |
|
case AgentMsg::SetSize: |
|
|
|
|
|
|
|
|
|
|
|
handleSetSizePacket(packet); |
|
break; |
|
case AgentMsg::GetConsoleProcessList: |
|
handleGetConsoleProcessListPacket(packet); |
|
break; |
|
default: |
|
trace("Unrecognized message, id:%d", type); |
|
} |
|
} |
|
|
|
void Agent::writePacket(WriteBuffer &packet) |
|
{ |
|
const auto &bytes = packet.buf(); |
|
packet.replaceRawValue<uint64_t>(0, bytes.size()); |
|
m_controlPipe->write(bytes.data(), bytes.size()); |
|
} |
|
|
|
void Agent::handleStartProcessPacket(ReadBuffer &packet) |
|
{ |
|
ASSERT(m_childProcess == nullptr); |
|
ASSERT(!m_closingOutputPipes); |
|
|
|
const uint64_t spawnFlags = packet.getInt64(); |
|
const bool wantProcessHandle = packet.getInt32() != 0; |
|
const bool wantThreadHandle = packet.getInt32() != 0; |
|
const auto program = packet.getWString(); |
|
const auto cmdline = packet.getWString(); |
|
const auto cwd = packet.getWString(); |
|
const auto env = packet.getWString(); |
|
const auto desktop = packet.getWString(); |
|
packet.assertEof(); |
|
|
|
auto cmdlineV = vectorWithNulFromString(cmdline); |
|
auto desktopV = vectorWithNulFromString(desktop); |
|
auto envV = vectorFromString(env); |
|
|
|
LPCWSTR programArg = program.empty() ? nullptr : program.c_str(); |
|
LPWSTR cmdlineArg = cmdline.empty() ? nullptr : cmdlineV.data(); |
|
LPCWSTR cwdArg = cwd.empty() ? nullptr : cwd.c_str(); |
|
LPWSTR envArg = env.empty() ? nullptr : envV.data(); |
|
|
|
STARTUPINFOW sui = {}; |
|
PROCESS_INFORMATION pi = {}; |
|
sui.cb = sizeof(sui); |
|
sui.lpDesktop = desktop.empty() ? nullptr : desktopV.data(); |
|
BOOL inheritHandles = FALSE; |
|
if (m_useConerr) { |
|
inheritHandles = TRUE; |
|
sui.dwFlags |= STARTF_USESTDHANDLES; |
|
sui.hStdInput = GetStdHandle(STD_INPUT_HANDLE); |
|
sui.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); |
|
sui.hStdError = m_errorBuffer->conout(); |
|
} |
|
|
|
const BOOL success = |
|
CreateProcessW(programArg, cmdlineArg, nullptr, nullptr, |
|
inheritHandles, |
|
CREATE_UNICODE_ENVIRONMENT, |
|
envArg, cwdArg, &sui, &pi); |
|
const int lastError = success ? 0 : GetLastError(); |
|
|
|
trace("CreateProcess: %s %u", |
|
(success ? "success" : "fail"), |
|
static_cast<unsigned int>(pi.dwProcessId)); |
|
|
|
auto reply = newPacket(); |
|
if (success) { |
|
int64_t replyProcess = 0; |
|
int64_t replyThread = 0; |
|
if (wantProcessHandle) { |
|
replyProcess = int64FromHandle(duplicateHandle(pi.hProcess)); |
|
} |
|
if (wantThreadHandle) { |
|
replyThread = int64FromHandle(duplicateHandle(pi.hThread)); |
|
} |
|
CloseHandle(pi.hThread); |
|
m_childProcess = pi.hProcess; |
|
m_autoShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_AUTO_SHUTDOWN) != 0; |
|
m_exitAfterShutdown = (spawnFlags & WINPTY_SPAWN_FLAG_EXIT_AFTER_SHUTDOWN) != 0; |
|
reply.putInt32(static_cast<int32_t>(StartProcessResult::ProcessCreated)); |
|
reply.putInt64(replyProcess); |
|
reply.putInt64(replyThread); |
|
} else { |
|
reply.putInt32(static_cast<int32_t>(StartProcessResult::CreateProcessFailed)); |
|
reply.putInt32(lastError); |
|
} |
|
writePacket(reply); |
|
} |
|
|
|
void Agent::handleSetSizePacket(ReadBuffer &packet) |
|
{ |
|
const int cols = packet.getInt32(); |
|
const int rows = packet.getInt32(); |
|
packet.assertEof(); |
|
resizeWindow(cols, rows); |
|
auto reply = newPacket(); |
|
writePacket(reply); |
|
} |
|
|
|
void Agent::handleGetConsoleProcessListPacket(ReadBuffer &packet) |
|
{ |
|
packet.assertEof(); |
|
|
|
auto processList = std::vector<DWORD>(64); |
|
auto processCount = GetConsoleProcessList(&processList[0], processList.size()); |
|
if (processList.size() < processCount) { |
|
processList.resize(processCount); |
|
processCount = GetConsoleProcessList(&processList[0], processList.size()); |
|
} |
|
|
|
if (processCount == 0) { |
|
trace("GetConsoleProcessList failed"); |
|
} |
|
|
|
auto reply = newPacket(); |
|
reply.putInt32(processCount); |
|
for (DWORD i = 0; i < processCount; i++) { |
|
reply.putInt32(processList[i]); |
|
} |
|
writePacket(reply); |
|
} |
|
|
|
void Agent::pollConinPipe() |
|
{ |
|
const std::string newData = m_coninPipe->readAllToString(); |
|
if (hasDebugFlag("input_separated_bytes")) { |
|
|
|
|
|
|
|
for (size_t i = 0; i < newData.size(); ++i) { |
|
m_consoleInput->writeInput(newData.substr(i, 1)); |
|
} |
|
} else { |
|
m_consoleInput->writeInput(newData); |
|
} |
|
} |
|
|
|
void Agent::onPollTimeout() |
|
{ |
|
m_consoleInput->updateInputFlags(); |
|
const bool enableMouseMode = m_consoleInput->shouldActivateTerminalMouse(); |
|
|
|
|
|
|
|
m_consoleInput->flushIncompleteEscapeCode(); |
|
|
|
const bool shouldScrapeContent = !m_closingOutputPipes; |
|
|
|
|
|
if (m_autoShutdown && |
|
m_childProcess != nullptr && |
|
WaitForSingleObject(m_childProcess, 0) == WAIT_OBJECT_0) { |
|
CloseHandle(m_childProcess); |
|
m_childProcess = nullptr; |
|
|
|
|
|
|
|
|
|
m_closingOutputPipes = true; |
|
} |
|
|
|
|
|
|
|
if (shouldScrapeContent) { |
|
syncConsoleTitle(); |
|
scrapeBuffers(); |
|
} |
|
|
|
|
|
|
|
m_primaryScraper->terminal().enableMouseMode( |
|
enableMouseMode && !m_closingOutputPipes); |
|
|
|
autoClosePipesForShutdown(); |
|
} |
|
|
|
void Agent::autoClosePipesForShutdown() |
|
{ |
|
if (m_closingOutputPipes) { |
|
|
|
|
|
|
|
if (m_conoutPipe->isConnected() && |
|
m_conoutPipe->bytesToSend() == 0) { |
|
trace("Closing CONOUT pipe (auto-shutdown)"); |
|
m_conoutPipe->closePipe(); |
|
} |
|
if (m_conerrPipe != nullptr && |
|
m_conerrPipe->isConnected() && |
|
m_conerrPipe->bytesToSend() == 0) { |
|
trace("Closing CONERR pipe (auto-shutdown)"); |
|
m_conerrPipe->closePipe(); |
|
} |
|
if (m_exitAfterShutdown && |
|
m_conoutPipe->isClosed() && |
|
(m_conerrPipe == nullptr || m_conerrPipe->isClosed())) { |
|
trace("Agent exiting (exit-after-shutdown)"); |
|
shutdown(); |
|
} |
|
} |
|
} |
|
|
|
std::unique_ptr<Win32ConsoleBuffer> Agent::openPrimaryBuffer() |
|
{ |
|
|
|
|
|
|
|
|
|
|
|
if (!m_useConerr) { |
|
return Win32ConsoleBuffer::openConout(); |
|
} else { |
|
return Win32ConsoleBuffer::openStdout(); |
|
} |
|
} |
|
|
|
void Agent::resizeWindow(int cols, int rows) |
|
{ |
|
ASSERT(cols >= 1 && rows >= 1); |
|
cols = std::min(cols, MAX_CONSOLE_WIDTH); |
|
rows = std::min(rows, MAX_CONSOLE_HEIGHT); |
|
|
|
Win32Console::FreezeGuard guard(m_console, m_console.frozen()); |
|
const Coord newSize(cols, rows); |
|
ConsoleScreenBufferInfo info; |
|
auto primaryBuffer = openPrimaryBuffer(); |
|
m_primaryScraper->resizeWindow(*primaryBuffer, newSize, info); |
|
m_consoleInput->setMouseWindowRect(info.windowRect()); |
|
if (m_errorScraper) { |
|
m_errorScraper->resizeWindow(*m_errorBuffer, newSize, info); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INPUT_RECORD sizeEvent {}; |
|
sizeEvent.EventType = WINDOW_BUFFER_SIZE_EVENT; |
|
sizeEvent.Event.WindowBufferSizeEvent.dwSize = primaryBuffer->bufferSize(); |
|
DWORD actual {}; |
|
WriteConsoleInputW(GetStdHandle(STD_INPUT_HANDLE), &sizeEvent, 1, &actual); |
|
} |
|
|
|
void Agent::scrapeBuffers() |
|
{ |
|
Win32Console::FreezeGuard guard(m_console, m_console.frozen()); |
|
ConsoleScreenBufferInfo info; |
|
m_primaryScraper->scrapeBuffer(*openPrimaryBuffer(), info); |
|
m_consoleInput->setMouseWindowRect(info.windowRect()); |
|
if (m_errorScraper) { |
|
m_errorScraper->scrapeBuffer(*m_errorBuffer, info); |
|
} |
|
} |
|
|
|
void Agent::syncConsoleTitle() |
|
{ |
|
std::wstring newTitle = m_console.title(); |
|
if (newTitle != m_currentTitle) { |
|
std::string command = std::string("\x1b]0;") + |
|
utf8FromWide(newTitle) + "\x07"; |
|
m_conoutPipe->write(command.c_str()); |
|
m_currentTitle = newTitle; |
|
} |
|
} |
|
|