Initialversion
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#include "ScriptHookPlugin.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cchannel.h>
|
||||
#include <tsvirtualchannels.h>
|
||||
#include <new>
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(reserved);
|
||||
if (reason == DLL_PROCESS_ATTACH)
|
||||
{
|
||||
DisableThreadLibraryCalls(module);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Der Prototyp ist laut Microsoft nicht in einem öffentlichen Header enthalten
|
||||
// und muss exakt in dieser Form exportiert werden.
|
||||
extern "C" __declspec(dllexport) HRESULT VCAPITYPE VirtualChannelGetInstance(
|
||||
_In_ REFIID refiid,
|
||||
_Inout_ ULONG* pNumObjs,
|
||||
_Out_opt_ VOID** ppObjArray)
|
||||
{
|
||||
if (pNumObjs == nullptr)
|
||||
{
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (refiid != __uuidof(IWTSPlugin))
|
||||
{
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
constexpr ULONG requiredObjects = 1;
|
||||
if (ppObjArray == nullptr)
|
||||
{
|
||||
*pNumObjs = requiredObjects;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
if (*pNumObjs < requiredObjects)
|
||||
{
|
||||
*pNumObjs = requiredObjects;
|
||||
return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER);
|
||||
}
|
||||
|
||||
auto* plugin = new (std::nothrow) ScriptHookPlugin();
|
||||
if (plugin == nullptr)
|
||||
{
|
||||
return E_OUTOFMEMORY;
|
||||
}
|
||||
|
||||
ppObjArray[0] = static_cast<IWTSPlugin*>(plugin);
|
||||
*pNumObjs = requiredObjects;
|
||||
return S_OK;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#include "Logging.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iterator>
|
||||
#include <cwchar>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring GetLogDirectory()
|
||||
{
|
||||
wchar_t localAppData[32768]{};
|
||||
DWORD len = GetEnvironmentVariableW(L"LOCALAPPDATA", localAppData, static_cast<DWORD>(std::size(localAppData)));
|
||||
if (len == 0 || len >= std::size(localAppData))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring base(localAppData);
|
||||
std::wstring plandent = base + L"\\Plandent";
|
||||
std::wstring hookDir = plandent + L"\\MstscScriptHook";
|
||||
CreateDirectoryW(plandent.c_str(), nullptr);
|
||||
CreateDirectoryW(hookDir.c_str(), nullptr);
|
||||
return hookDir;
|
||||
}
|
||||
|
||||
std::string ToUtf8(const std::wstring& value)
|
||||
{
|
||||
if (value.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
int needed = WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (needed <= 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
std::string result(static_cast<size_t>(needed), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), needed, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace hook
|
||||
{
|
||||
std::wstring FormatWin32Error(unsigned long errorCode)
|
||||
{
|
||||
wchar_t* buffer = nullptr;
|
||||
DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
|
||||
DWORD length = FormatMessageW(flags, nullptr, errorCode, 0, reinterpret_cast<wchar_t*>(&buffer), 0, nullptr);
|
||||
if (length == 0 || buffer == nullptr)
|
||||
{
|
||||
return L"Win32-Fehler " + std::to_wstring(errorCode);
|
||||
}
|
||||
|
||||
std::wstring result(buffer, length);
|
||||
LocalFree(buffer);
|
||||
while (!result.empty() && (result.back() == L'\r' || result.back() == L'\n' || result.back() == L' '))
|
||||
{
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Log(bool enabled, const std::wstring& message)
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring directory = GetLogDirectory();
|
||||
if (directory.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SYSTEMTIME st{};
|
||||
GetLocalTime(&st);
|
||||
|
||||
wchar_t prefix[128]{};
|
||||
swprintf_s(prefix, std::size(prefix), L"%04u-%02u-%02u %02u:%02u:%02u.%03u [PID:%lu TID:%lu] ",
|
||||
st.wYear, st.wMonth, st.wDay,
|
||||
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds,
|
||||
GetCurrentProcessId(), GetCurrentThreadId());
|
||||
|
||||
std::wstring fullLine = std::wstring(prefix) + message + L"\r\n";
|
||||
std::string utf8 = ToUtf8(fullLine);
|
||||
if (utf8.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring logPath = directory + L"\\plugin.log";
|
||||
HANDLE file = CreateFileW(
|
||||
logPath.c_str(),
|
||||
FILE_APPEND_DATA,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr,
|
||||
OPEN_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL,
|
||||
nullptr);
|
||||
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD written = 0;
|
||||
WriteFile(file, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
||||
CloseHandle(file);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace hook
|
||||
{
|
||||
void Log(bool enabled, const std::wstring& message);
|
||||
std::wstring FormatWin32Error(unsigned long errorCode);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{D42B7E9A-92FC-4B92-8C3F-E32D1291D2CA}</ProjectGuid>
|
||||
<RootNamespace>PlandentMstscScriptHook</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings" />
|
||||
<ImportGroup Label="Shared" />
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup>
|
||||
<OutDir>$(ProjectDir)..\artifacts\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(ProjectDir)..\artifacts\obj\$(Platform)\$(Configuration)\Plugin\</IntDir>
|
||||
<TargetName>PlandentMstscScriptHook</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>exports.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>Advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32_LEAN_AND_MEAN;NOMINMAX;UNICODE;_UNICODE;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Windows</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>false</GenerateDebugInformation>
|
||||
<ModuleDefinitionFile>exports.def</ModuleDefinitionFile>
|
||||
<AdditionalDependencies>Advapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="DllMain.cpp" />
|
||||
<ClCompile Include="Logging.cpp" />
|
||||
<ClCompile Include="ProcessLauncher.cpp" />
|
||||
<ClCompile Include="RegistryConfig.cpp" />
|
||||
<ClCompile Include="ScriptHookPlugin.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Logging.h" />
|
||||
<ClInclude Include="ProcessLauncher.h" />
|
||||
<ClInclude Include="RegistryConfig.h" />
|
||||
<ClInclude Include="ScriptHookPlugin.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="exports.def" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files"><UniqueIdentifier>{F4A82F73-9E41-44E6-AB6B-69D29AC5C3CC}</UniqueIdentifier><Extensions>cpp;c</Extensions></Filter>
|
||||
<Filter Include="Header Files"><UniqueIdentifier>{D93CE101-7264-440E-A04E-D633530285A9}</UniqueIdentifier><Extensions>h;hpp</Extensions></Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="DllMain.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||
<ClCompile Include="Logging.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||
<ClCompile Include="ProcessLauncher.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||
<ClCompile Include="RegistryConfig.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||
<ClCompile Include="ScriptHookPlugin.cpp"><Filter>Source Files</Filter></ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Logging.h"><Filter>Header Files</Filter></ClInclude>
|
||||
<ClInclude Include="ProcessLauncher.h"><Filter>Header Files</Filter></ClInclude>
|
||||
<ClInclude Include="RegistryConfig.h"><Filter>Header Files</Filter></ClInclude>
|
||||
<ClInclude Include="ScriptHookPlugin.h"><Filter>Header Files</Filter></ClInclude>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,426 @@
|
||||
#include "ProcessLauncher.h"
|
||||
#include "Logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cwctype>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::wstring QuoteWindowsArgument(const std::wstring& arg)
|
||||
{
|
||||
if (arg.empty())
|
||||
{
|
||||
return L"\"\"";
|
||||
}
|
||||
|
||||
if (arg.find_first_of(L" \t\n\v\"") == std::wstring::npos)
|
||||
{
|
||||
return arg;
|
||||
}
|
||||
|
||||
std::wstring result = L"\"";
|
||||
size_t backslashes = 0;
|
||||
for (wchar_t ch : arg)
|
||||
{
|
||||
if (ch == L'\\')
|
||||
{
|
||||
++backslashes;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == L'\"')
|
||||
{
|
||||
result.append(backslashes * 2 + 1, L'\\');
|
||||
result.push_back(L'\"');
|
||||
backslashes = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
result.append(backslashes, L'\\');
|
||||
backslashes = 0;
|
||||
result.push_back(ch);
|
||||
}
|
||||
|
||||
result.append(backslashes * 2, L'\\');
|
||||
result.push_back(L'\"');
|
||||
return result;
|
||||
}
|
||||
|
||||
std::wstring GetExtensionLower(const std::wstring& path)
|
||||
{
|
||||
const size_t dot = path.find_last_of(L'.');
|
||||
const size_t slash = path.find_last_of(L"\\/");
|
||||
if (dot == std::wstring::npos || (slash != std::wstring::npos && dot < slash))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring ext = path.substr(dot);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](wchar_t c) { return static_cast<wchar_t>(std::towlower(c)); });
|
||||
return ext;
|
||||
}
|
||||
|
||||
std::wstring ParentDirectory(const std::wstring& path)
|
||||
{
|
||||
const size_t slash = path.find_last_of(L"\\/");
|
||||
if (slash == std::wstring::npos)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
if (slash == 2 && path.size() >= 3 && path[1] == L':')
|
||||
{
|
||||
return path.substr(0, 3);
|
||||
}
|
||||
return path.substr(0, slash);
|
||||
}
|
||||
|
||||
std::wstring GetEnvironmentValue(const wchar_t* name)
|
||||
{
|
||||
DWORD size = GetEnvironmentVariableW(name, nullptr, 0);
|
||||
if (size == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
std::vector<wchar_t> buffer(size, L'\0');
|
||||
if (GetEnvironmentVariableW(name, buffer.data(), size) == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
return std::wstring(buffer.data());
|
||||
}
|
||||
|
||||
uint64_t HashPath(const std::wstring& path)
|
||||
{
|
||||
constexpr uint64_t fnvOffset = 14695981039346656037ull;
|
||||
constexpr uint64_t fnvPrime = 1099511628211ull;
|
||||
uint64_t hash = fnvOffset;
|
||||
for (wchar_t ch : path)
|
||||
{
|
||||
wchar_t lower = static_cast<wchar_t>(std::towlower(ch));
|
||||
hash ^= static_cast<uint16_t>(lower);
|
||||
hash *= fnvPrime;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::wstring MutexNameForScript(const std::wstring& scriptPath)
|
||||
{
|
||||
std::wstringstream stream;
|
||||
stream << L"Local\\Plandent.MstscScriptHook."
|
||||
<< std::uppercase << std::hex << std::setw(16) << std::setfill(L'0')
|
||||
<< HashPath(scriptPath);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
struct LaunchCommand
|
||||
{
|
||||
std::wstring application;
|
||||
std::wstring commandLine;
|
||||
};
|
||||
|
||||
bool BuildLaunchCommand(const hook::HookConfig& config, LaunchCommand& command)
|
||||
{
|
||||
const std::wstring extension = GetExtensionLower(config.scriptPath);
|
||||
|
||||
if (extension == L".bat" || extension == L".cmd")
|
||||
{
|
||||
std::wstring comspec = GetEnvironmentValue(L"ComSpec");
|
||||
if (comspec.empty())
|
||||
{
|
||||
std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot");
|
||||
if (systemRoot.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
comspec = systemRoot + L"\\System32\\cmd.exe";
|
||||
}
|
||||
|
||||
std::wstring inner = QuoteWindowsArgument(config.scriptPath);
|
||||
if (!config.arguments.empty())
|
||||
{
|
||||
inner += L" " + config.arguments;
|
||||
}
|
||||
|
||||
command.application = comspec;
|
||||
command.commandLine = QuoteWindowsArgument(comspec) + L" /D /S /C \"" + inner + L"\"";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (extension == L".ps1")
|
||||
{
|
||||
std::wstring systemRoot = GetEnvironmentValue(L"SystemRoot");
|
||||
if (systemRoot.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring powershell = systemRoot + L"\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
||||
command.application = powershell;
|
||||
command.commandLine = QuoteWindowsArgument(powershell) +
|
||||
L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
|
||||
QuoteWindowsArgument(config.scriptPath);
|
||||
if (!config.arguments.empty())
|
||||
{
|
||||
command.commandLine += L" " + config.arguments;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (extension == L".exe")
|
||||
{
|
||||
command.application = config.scriptPath;
|
||||
command.commandLine = QuoteWindowsArgument(config.scriptPath);
|
||||
if (!config.arguments.empty())
|
||||
{
|
||||
command.commandLine += L" " + config.arguments;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
namespace hook
|
||||
{
|
||||
ProcessLauncher::~ProcessLauncher()
|
||||
{
|
||||
if (process_ != nullptr)
|
||||
{
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessLauncher::CleanupExitedProcess()
|
||||
{
|
||||
if (process_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WaitForSingleObject(process_, 0) != WAIT_TIMEOUT)
|
||||
{
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
processId_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool ProcessLauncher::IsRunning()
|
||||
{
|
||||
CleanupExitedProcess();
|
||||
return process_ != nullptr;
|
||||
}
|
||||
|
||||
bool ProcessLauncher::Start(const HookConfig& config)
|
||||
{
|
||||
CleanupExitedProcess();
|
||||
if (process_ != nullptr)
|
||||
{
|
||||
Log(config.enableLogging, L"Script ist für diese MSTSC-Instanz bereits aktiv (PID " + std::to_wstring(processId_) + L").");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config.scriptPath.empty())
|
||||
{
|
||||
Log(config.enableLogging, L"Kein ScriptPath konfiguriert; Start übersprungen.");
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD attributes = GetFileAttributesW(config.scriptPath.c_str());
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||
{
|
||||
Log(config.enableLogging, L"Script wurde nicht gefunden: " + config.scriptPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
LaunchCommand command;
|
||||
if (!BuildLaunchCommand(config, command))
|
||||
{
|
||||
Log(config.enableLogging, L"Nicht unterstützter Script-/Dateityp: " + config.scriptPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring workingDirectory = config.workingDirectory.empty()
|
||||
? ParentDirectory(config.scriptPath)
|
||||
: config.workingDirectory;
|
||||
|
||||
HANDLE duplicateMutex = nullptr;
|
||||
if (config.preventDuplicates)
|
||||
{
|
||||
SECURITY_ATTRIBUTES sa{};
|
||||
sa.nLength = sizeof(sa);
|
||||
sa.bInheritHandle = TRUE;
|
||||
|
||||
std::wstring mutexName = MutexNameForScript(config.scriptPath);
|
||||
duplicateMutex = CreateMutexW(&sa, FALSE, mutexName.c_str());
|
||||
if (duplicateMutex == nullptr)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
Log(config.enableLogging, L"Mehrfachstart-Schutz konnte nicht erstellt werden: " + FormatWin32Error(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
CloseHandle(duplicateMutex);
|
||||
Log(config.enableLogging, L"Ein Receiver mit demselben ScriptPath läuft bereits; kein weiterer Start.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
PROCESS_INFORMATION pi{};
|
||||
DWORD creationFlags = 0;
|
||||
if (config.hidden)
|
||||
{
|
||||
creationFlags |= CREATE_NO_WINDOW;
|
||||
}
|
||||
|
||||
std::vector<wchar_t> mutableCommand(command.commandLine.begin(), command.commandLine.end());
|
||||
mutableCommand.push_back(L'\0');
|
||||
|
||||
BOOL created = FALSE;
|
||||
|
||||
if (duplicateMutex != nullptr)
|
||||
{
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
if (config.hidden)
|
||||
{
|
||||
startup.StartupInfo.dwFlags |= STARTF_USESHOWWINDOW;
|
||||
startup.StartupInfo.wShowWindow = SW_HIDE;
|
||||
}
|
||||
|
||||
SIZE_T attributeSize = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeSize);
|
||||
std::vector<BYTE> attributeBuffer(attributeSize);
|
||||
startup.lpAttributeList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributeBuffer.data());
|
||||
|
||||
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &attributeSize))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
CloseHandle(duplicateMutex);
|
||||
Log(config.enableLogging, L"Prozessattributliste konnte nicht initialisiert werden: " + FormatWin32Error(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
HANDLE handles[] = { duplicateMutex };
|
||||
if (!UpdateProcThreadAttribute(
|
||||
startup.lpAttributeList,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
handles,
|
||||
sizeof(handles),
|
||||
nullptr,
|
||||
nullptr))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||
CloseHandle(duplicateMutex);
|
||||
Log(config.enableLogging, L"Handle-Vererbung konnte nicht vorbereitet werden: " + FormatWin32Error(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
creationFlags |= EXTENDED_STARTUPINFO_PRESENT;
|
||||
created = CreateProcessW(
|
||||
command.application.c_str(),
|
||||
mutableCommand.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
TRUE,
|
||||
creationFlags,
|
||||
nullptr,
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
&startup.StartupInfo,
|
||||
&pi);
|
||||
|
||||
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||
|
||||
// Der Child-Prozess hat bei erfolgreichem Start eine eigene Referenz auf den Mutex.
|
||||
// Dadurch bleibt der Mehrfachstart-Schutz auch bestehen, wenn mstsc.exe später endet.
|
||||
CloseHandle(duplicateMutex);
|
||||
}
|
||||
else
|
||||
{
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
if (config.hidden)
|
||||
{
|
||||
startup.dwFlags |= STARTF_USESHOWWINDOW;
|
||||
startup.wShowWindow = SW_HIDE;
|
||||
}
|
||||
|
||||
created = CreateProcessW(
|
||||
command.application.c_str(),
|
||||
mutableCommand.data(),
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
creationFlags,
|
||||
nullptr,
|
||||
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
||||
&startup,
|
||||
&pi);
|
||||
}
|
||||
|
||||
if (!created)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
Log(config.enableLogging, L"Scriptstart fehlgeschlagen: " + FormatWin32Error(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(pi.hThread);
|
||||
process_ = pi.hProcess;
|
||||
processId_ = pi.dwProcessId;
|
||||
|
||||
Log(config.enableLogging,
|
||||
L"Script gestartet. PID=" + std::to_wstring(processId_) +
|
||||
L", Script=" + config.scriptPath +
|
||||
(config.arguments.empty() ? L"" : L", Argumente=" + config.arguments));
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProcessLauncher::Stop(bool loggingEnabled)
|
||||
{
|
||||
CleanupExitedProcess();
|
||||
if (process_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const DWORD pid = processId_;
|
||||
if (!TerminateProcess(process_, 0))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
Log(loggingEnabled, L"Scriptprozess PID " + std::to_wstring(pid) + L" konnte nicht beendet werden: " + FormatWin32Error(error));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log(loggingEnabled, L"Beenden des Scriptprozesses PID " + std::to_wstring(pid) + L" wurde bei RDP-Trennung angefordert.");
|
||||
}
|
||||
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
processId_ = 0;
|
||||
}
|
||||
|
||||
void ProcessLauncher::Detach(bool loggingEnabled)
|
||||
{
|
||||
CleanupExitedProcess();
|
||||
if (process_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log(loggingEnabled, L"MSTSC beendet Plugin; Script PID " + std::to_wstring(processId_) + L" läuft weiter.");
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
processId_ = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "RegistryConfig.h"
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace hook
|
||||
{
|
||||
class ProcessLauncher
|
||||
{
|
||||
public:
|
||||
ProcessLauncher() = default;
|
||||
~ProcessLauncher();
|
||||
|
||||
ProcessLauncher(const ProcessLauncher&) = delete;
|
||||
ProcessLauncher& operator=(const ProcessLauncher&) = delete;
|
||||
|
||||
bool Start(const HookConfig& config);
|
||||
void Stop(bool loggingEnabled);
|
||||
void Detach(bool loggingEnabled);
|
||||
bool IsRunning();
|
||||
|
||||
private:
|
||||
HANDLE process_ = nullptr;
|
||||
DWORD processId_ = 0;
|
||||
|
||||
void CleanupExitedProcess();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "RegistryConfig.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
DWORD ReadDword(HKEY key, const wchar_t* name, DWORD defaultValue)
|
||||
{
|
||||
DWORD value = defaultValue;
|
||||
DWORD size = sizeof(value);
|
||||
DWORD type = 0;
|
||||
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(&value), &size) != ERROR_SUCCESS ||
|
||||
type != REG_DWORD)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring ReadString(HKEY key, const wchar_t* name)
|
||||
{
|
||||
DWORD type = 0;
|
||||
DWORD size = 0;
|
||||
if (RegQueryValueExW(key, name, nullptr, &type, nullptr, &size) != ERROR_SUCCESS ||
|
||||
(type != REG_SZ && type != REG_EXPAND_SZ) || size == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<wchar_t> buffer((size / sizeof(wchar_t)) + 1, L'\0');
|
||||
if (RegQueryValueExW(key, name, nullptr, &type, reinterpret_cast<BYTE*>(buffer.data()), &size) != ERROR_SUCCESS)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring value(buffer.data());
|
||||
if (type == REG_EXPAND_SZ)
|
||||
{
|
||||
return hook::ExpandEnvironment(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
namespace hook
|
||||
{
|
||||
std::wstring ExpandEnvironment(const std::wstring& value)
|
||||
{
|
||||
if (value.empty())
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
DWORD needed = ExpandEnvironmentStringsW(value.c_str(), nullptr, 0);
|
||||
if (needed == 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<wchar_t> buffer(needed, L'\0');
|
||||
if (ExpandEnvironmentStringsW(value.c_str(), buffer.data(), needed) == 0)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
return std::wstring(buffer.data());
|
||||
}
|
||||
|
||||
HookConfig LoadConfig()
|
||||
{
|
||||
HookConfig config;
|
||||
|
||||
HKEY key = nullptr;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, kConfigKey, 0, KEY_QUERY_VALUE, &key) != ERROR_SUCCESS)
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
config.enabled = ReadDword(key, L"Enabled", 1) != 0;
|
||||
config.scriptPath = ReadString(key, L"ScriptPath");
|
||||
config.arguments = ExpandEnvironment(ReadString(key, L"Arguments"));
|
||||
config.workingDirectory = ReadString(key, L"WorkingDirectory");
|
||||
config.startOnConnect = ReadDword(key, L"StartOnConnect", 1) != 0;
|
||||
config.stopOnDisconnect = ReadDword(key, L"StopOnDisconnect", 0) != 0;
|
||||
config.hidden = ReadDword(key, L"Hidden", 1) != 0;
|
||||
config.preventDuplicates = ReadDword(key, L"PreventDuplicates", 1) != 0;
|
||||
config.enableLogging = ReadDword(key, L"EnableLogging", 1) != 0;
|
||||
|
||||
RegCloseKey(key);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace hook
|
||||
{
|
||||
inline constexpr wchar_t kConfigKey[] = L"Software\\Plandent\\MstscScriptHook";
|
||||
|
||||
struct HookConfig
|
||||
{
|
||||
bool enabled = true;
|
||||
std::wstring scriptPath;
|
||||
std::wstring arguments;
|
||||
std::wstring workingDirectory;
|
||||
bool startOnConnect = true;
|
||||
bool stopOnDisconnect = false;
|
||||
bool hidden = true;
|
||||
bool preventDuplicates = true;
|
||||
bool enableLogging = true;
|
||||
};
|
||||
|
||||
HookConfig LoadConfig();
|
||||
std::wstring ExpandEnvironment(const std::wstring& value);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "ScriptHookPlugin.h"
|
||||
|
||||
#include "Logging.h"
|
||||
#include "RegistryConfig.h"
|
||||
|
||||
ScriptHookPlugin::ScriptHookPlugin() = default;
|
||||
ScriptHookPlugin::~ScriptHookPlugin() = default;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::QueryInterface(REFIID riid, void** ppvObject)
|
||||
{
|
||||
if (ppvObject == nullptr)
|
||||
{
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
*ppvObject = nullptr;
|
||||
if (riid == __uuidof(IUnknown) || riid == __uuidof(IWTSPlugin))
|
||||
{
|
||||
*ppvObject = static_cast<IWTSPlugin*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE ScriptHookPlugin::AddRef()
|
||||
{
|
||||
return static_cast<ULONG>(InterlockedIncrement(&refCount_));
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE ScriptHookPlugin::Release()
|
||||
{
|
||||
ULONG remaining = static_cast<ULONG>(InterlockedDecrement(&refCount_));
|
||||
if (remaining == 0)
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Initialize(IWTSVirtualChannelManager* channelManager)
|
||||
{
|
||||
UNREFERENCED_PARAMETER(channelManager);
|
||||
const hook::HookConfig config = hook::LoadConfig();
|
||||
hook::Log(config.enableLogging, L"IWTSPlugin::Initialize aufgerufen.");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Connected()
|
||||
{
|
||||
const hook::HookConfig config = hook::LoadConfig();
|
||||
hook::Log(config.enableLogging, L"RDP-Verbindung hergestellt (IWTSPlugin::Connected).");
|
||||
|
||||
if (!config.enabled || !config.startOnConnect)
|
||||
{
|
||||
hook::Log(config.enableLogging, L"Start ist in der Konfiguration deaktiviert.");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// Nur Registry lesen + CreateProcess aufrufen. Es wird niemals auf das Script gewartet.
|
||||
launcher_.Start(config);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Disconnected(DWORD disconnectCode)
|
||||
{
|
||||
const hook::HookConfig config = hook::LoadConfig();
|
||||
hook::Log(config.enableLogging,
|
||||
L"RDP-Verbindung getrennt (IWTSPlugin::Disconnected), Code=" + std::to_wstring(disconnectCode) + L".");
|
||||
|
||||
if (config.stopOnDisconnect)
|
||||
{
|
||||
launcher_.Stop(config.enableLogging);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE ScriptHookPlugin::Terminated()
|
||||
{
|
||||
const hook::HookConfig config = hook::LoadConfig();
|
||||
hook::Log(config.enableLogging, L"IWTSPlugin::Terminated aufgerufen.");
|
||||
|
||||
if (config.stopOnDisconnect)
|
||||
{
|
||||
launcher_.Stop(config.enableLogging);
|
||||
}
|
||||
else
|
||||
{
|
||||
launcher_.Detach(config.enableLogging);
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProcessLauncher.h"
|
||||
#include <windows.h>
|
||||
#include <tsvirtualchannels.h>
|
||||
|
||||
class ScriptHookPlugin final : public IWTSPlugin
|
||||
{
|
||||
public:
|
||||
ScriptHookPlugin();
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
|
||||
HRESULT STDMETHODCALLTYPE Initialize(IWTSVirtualChannelManager* channelManager) override;
|
||||
HRESULT STDMETHODCALLTYPE Connected() override;
|
||||
HRESULT STDMETHODCALLTYPE Disconnected(DWORD disconnectCode) override;
|
||||
HRESULT STDMETHODCALLTYPE Terminated() override;
|
||||
|
||||
private:
|
||||
~ScriptHookPlugin();
|
||||
|
||||
LONG refCount_ = 1;
|
||||
hook::ProcessLauncher launcher_;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
LIBRARY "PlandentMstscScriptHook"
|
||||
EXPORTS
|
||||
VirtualChannelGetInstance
|
||||
Reference in New Issue
Block a user