338 lines
11 KiB
C++
338 lines
11 KiB
C++
|
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
// found in the LICENSE file.
|
||
|
|
||
|
#include <stdio.h>
|
||
|
#include <stdlib.h>
|
||
|
|
||
|
#define NOMINMAX
|
||
|
#include <windows.h>
|
||
|
|
||
|
#include <algorithm>
|
||
|
#include <iterator>
|
||
|
#include <sstream>
|
||
|
#include <string>
|
||
|
#include <vector>
|
||
|
|
||
|
typedef std::basic_string<TCHAR> tstring;
|
||
|
|
||
|
namespace {
|
||
|
const bool g_is_debug = (_wgetenv(L"LIMITER_DEBUG") != NULL);
|
||
|
}
|
||
|
|
||
|
// Don't use stderr for errors because VS has large buffers on them, leading
|
||
|
// to confusing error output.
|
||
|
static void Error(const wchar_t* msg, ...) {
|
||
|
tstring new_msg = tstring(L"limiter fatal error: ") + msg + L"\n";
|
||
|
va_list args;
|
||
|
va_start(args, msg);
|
||
|
vwprintf(new_msg.c_str(), args);
|
||
|
va_end(args);
|
||
|
}
|
||
|
|
||
|
static void Warn(const wchar_t* msg, ...) {
|
||
|
if (!g_is_debug)
|
||
|
return;
|
||
|
tstring new_msg = tstring(L"limiter warning: ") + msg + L"\n";
|
||
|
va_list args;
|
||
|
va_start(args, msg);
|
||
|
vwprintf(new_msg.c_str(), args);
|
||
|
va_end(args);
|
||
|
}
|
||
|
|
||
|
static tstring ErrorMessageToString(DWORD err) {
|
||
|
TCHAR* msg_buf = NULL;
|
||
|
DWORD rc = FormatMessage(
|
||
|
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
|
||
|
NULL, // lpSource
|
||
|
err,
|
||
|
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||
|
reinterpret_cast<LPTSTR>(&msg_buf),
|
||
|
0, // nSize
|
||
|
NULL); // Arguments
|
||
|
if (!rc)
|
||
|
return L"unknown error";
|
||
|
tstring ret(msg_buf);
|
||
|
LocalFree(msg_buf);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static DWORD RunExe(const tstring& exe_name) {
|
||
|
STARTUPINFO startup_info = { sizeof(STARTUPINFO) };
|
||
|
PROCESS_INFORMATION process_info;
|
||
|
DWORD exit_code;
|
||
|
|
||
|
GetStartupInfo(&startup_info);
|
||
|
tstring cmdline = tstring(GetCommandLine());
|
||
|
|
||
|
size_t first_space = cmdline.find(' ');
|
||
|
if (first_space == -1) {
|
||
|
// I'm not sure why this would ever happen, but just in case...
|
||
|
cmdline = exe_name;
|
||
|
} else {
|
||
|
cmdline = exe_name + cmdline.substr(first_space);
|
||
|
}
|
||
|
|
||
|
if (!CreateProcess(NULL, // lpApplicationName
|
||
|
const_cast<TCHAR*>(cmdline.c_str()),
|
||
|
NULL, // lpProcessAttributes
|
||
|
NULL, // lpThreadAttributes
|
||
|
TRUE, // bInheritHandles
|
||
|
0, // dwCreationFlags,
|
||
|
NULL, // lpEnvironment,
|
||
|
NULL, // lpCurrentDirectory,
|
||
|
&startup_info,
|
||
|
&process_info)) {
|
||
|
Error(L"Error in CreateProcess[%s]: %s",
|
||
|
cmdline.c_str(), ErrorMessageToString(GetLastError()).c_str());
|
||
|
return MAXDWORD;
|
||
|
}
|
||
|
CloseHandle(process_info.hThread);
|
||
|
WaitForSingleObject(process_info.hProcess, INFINITE);
|
||
|
GetExitCodeProcess(process_info.hProcess, &exit_code);
|
||
|
CloseHandle(process_info.hProcess);
|
||
|
return exit_code;
|
||
|
}
|
||
|
|
||
|
// Returns 0 if there was an error
|
||
|
static int CpuConcurrencyMetric(const tstring& envvar_name) {
|
||
|
int max_concurrent = 0;
|
||
|
std::vector<char> buffer(1);
|
||
|
BOOL ok = false;
|
||
|
DWORD last_error = 0;
|
||
|
do {
|
||
|
DWORD bufsize = buffer.size();
|
||
|
ok = GetLogicalProcessorInformation(
|
||
|
reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION>(&buffer[0]),
|
||
|
&bufsize);
|
||
|
last_error = GetLastError();
|
||
|
if (!ok && last_error == ERROR_INSUFFICIENT_BUFFER &&
|
||
|
bufsize > buffer.size()) {
|
||
|
buffer.resize(bufsize);
|
||
|
}
|
||
|
} while (!ok && last_error == ERROR_INSUFFICIENT_BUFFER);
|
||
|
|
||
|
if (!ok) {
|
||
|
Warn(L"Error while getting number of cores. Try setting the "
|
||
|
L" environment variable '%s' to (num_cores - 1): %s",
|
||
|
envvar_name.c_str(), ErrorMessageToString(last_error).c_str());
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pproc_info =
|
||
|
reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION>(&buffer[0]);
|
||
|
int num_entries = buffer.size() /
|
||
|
sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
|
||
|
|
||
|
for (int i = 0; i < num_entries; ++i) {
|
||
|
SYSTEM_LOGICAL_PROCESSOR_INFORMATION& info = pproc_info[i];
|
||
|
if (info.Relationship == RelationProcessorCore) {
|
||
|
++max_concurrent;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Leave one core for other tasks
|
||
|
return max_concurrent - 1;
|
||
|
}
|
||
|
|
||
|
// TODO(defaults): Create a better heuristic than # of CPUs. It seems likely
|
||
|
// that the right value will, in fact, be based on the memory capacity of the
|
||
|
// machine, not on the number of CPUs.
|
||
|
enum ConcurrencyMetricEnum {
|
||
|
CONCURRENCY_METRIC_ONE,
|
||
|
CONCURRENCY_METRIC_CPU,
|
||
|
CONCURRENCY_METRIC_DEFAULT = CONCURRENCY_METRIC_CPU
|
||
|
};
|
||
|
|
||
|
static int GetMaxConcurrency(const tstring& base_pipename,
|
||
|
ConcurrencyMetricEnum metric) {
|
||
|
static int max_concurrent = -1;
|
||
|
|
||
|
if (max_concurrent == -1) {
|
||
|
tstring envvar_name = base_pipename + L"_MAXCONCURRENCY";
|
||
|
|
||
|
const LPTSTR max_concurrent_str = _wgetenv(envvar_name.c_str());
|
||
|
max_concurrent = max_concurrent_str ? _wtoi(max_concurrent_str) : 0;
|
||
|
|
||
|
if (max_concurrent == 0) {
|
||
|
switch (metric) {
|
||
|
case CONCURRENCY_METRIC_CPU:
|
||
|
max_concurrent = CpuConcurrencyMetric(envvar_name);
|
||
|
if (max_concurrent)
|
||
|
break;
|
||
|
// else fall through
|
||
|
case CONCURRENCY_METRIC_ONE:
|
||
|
max_concurrent = 1;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
max_concurrent = std::min(std::max(max_concurrent, 1),
|
||
|
PIPE_UNLIMITED_INSTANCES);
|
||
|
}
|
||
|
|
||
|
return max_concurrent;
|
||
|
}
|
||
|
|
||
|
static HANDLE WaitForPipe(const tstring& pipename,
|
||
|
HANDLE event,
|
||
|
int max_concurrency) {
|
||
|
// We're using a named pipe instead of a semaphore so the Kernel can clean up
|
||
|
// after us if we crash while holding onto the pipe (A real semaphore will
|
||
|
// not release on process termination).
|
||
|
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||
|
for (;;) {
|
||
|
pipe = CreateNamedPipe(
|
||
|
pipename.c_str(),
|
||
|
PIPE_ACCESS_DUPLEX,
|
||
|
PIPE_TYPE_BYTE,
|
||
|
max_concurrency,
|
||
|
1, // nOutBufferSize
|
||
|
1, // nInBufferSize
|
||
|
0, // nDefaultTimeOut
|
||
|
NULL); // Default security attributes (noinherit)
|
||
|
if (pipe != INVALID_HANDLE_VALUE)
|
||
|
break;
|
||
|
|
||
|
DWORD error = GetLastError();
|
||
|
if (error == ERROR_PIPE_BUSY) {
|
||
|
if (event) {
|
||
|
WaitForSingleObject(event, 60 * 1000 /* ms */);
|
||
|
} else {
|
||
|
// TODO(iannucci): Maybe we should error out here instead of falling
|
||
|
// back to a sleep-poll
|
||
|
Sleep(5 * 1000 /* ms */);
|
||
|
}
|
||
|
} else {
|
||
|
Warn(L"Got error %d while waiting for pipe: %s", error,
|
||
|
ErrorMessageToString(error).c_str());
|
||
|
return INVALID_HANDLE_VALUE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return pipe;
|
||
|
}
|
||
|
|
||
|
static int WaitAndRun(const tstring& shimmed_exe,
|
||
|
const tstring& base_pipename) {
|
||
|
ULONGLONG start_time = 0, end_time = 0;
|
||
|
tstring pipename = L"\\\\.\\pipe\\" + base_pipename;
|
||
|
tstring event_name = L"Local\\EVENT_" + base_pipename;
|
||
|
|
||
|
// This event lets us do better than strict polling, but we don't rely on it
|
||
|
// (in case a process crashes before signalling the event).
|
||
|
HANDLE event = CreateEvent(
|
||
|
NULL, // Default security attributes
|
||
|
FALSE, // Manual reset
|
||
|
FALSE, // Initial state
|
||
|
event_name.c_str());
|
||
|
|
||
|
if (g_is_debug)
|
||
|
start_time = GetTickCount64();
|
||
|
|
||
|
HANDLE pipe =
|
||
|
WaitForPipe(pipename, event,
|
||
|
GetMaxConcurrency(base_pipename, CONCURRENCY_METRIC_DEFAULT));
|
||
|
|
||
|
if (g_is_debug) {
|
||
|
end_time = GetTickCount64();
|
||
|
wprintf(L" took %.2fs to acquire semaphore.\n",
|
||
|
(end_time - start_time) / 1000.0);
|
||
|
}
|
||
|
|
||
|
DWORD ret = RunExe(shimmed_exe);
|
||
|
|
||
|
if (pipe != INVALID_HANDLE_VALUE)
|
||
|
CloseHandle(pipe);
|
||
|
if (event != NULL)
|
||
|
SetEvent(event);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
void Usage(const tstring& msg) {
|
||
|
tstring usage(msg);
|
||
|
usage += L"\n"
|
||
|
L"Usage: SHIMED_NAME__SEMAPHORE_NAME\n"
|
||
|
L"\n"
|
||
|
L" SHIMMED_NAME - ex. 'link.exe' or 'lib.exe'\n"
|
||
|
L" - can be exe, bat, or com\n"
|
||
|
L" - must exist in PATH\n"
|
||
|
L"\n"
|
||
|
L" SEMAPHORE_NAME - ex. 'SOME_NAME' or 'GROOVY_SEMAPHORE'\n"
|
||
|
L"\n"
|
||
|
L" Example:\n"
|
||
|
L" link.exe__LINK_LIMITER.exe\n"
|
||
|
L" lib.exe__LINK_LIMITER.exe\n"
|
||
|
L" * Both will limit on the same semaphore\n"
|
||
|
L"\n"
|
||
|
L" link.exe__LINK_LIMITER.exe\n"
|
||
|
L" lib.exe__LIB_LIMITER.exe\n"
|
||
|
L" * Both will limit on independent semaphores\n"
|
||
|
L"\n"
|
||
|
L" This program is meant to be run after renaming it into the\n"
|
||
|
L" above format. Once you have done so, executing it will block\n"
|
||
|
L" on the availability of the semaphore SEMAPHORE_NAME. Once\n"
|
||
|
L" the semaphore is obtained, it will execute SHIMMED_NAME, \n"
|
||
|
L" passing through all arguments as-is.\n"
|
||
|
L"\n"
|
||
|
L" The maximum concurrency can be manually set by setting the\n"
|
||
|
L" environment variable <SEMAPHORE_NAME>_MAXCONCURRENCY to an\n"
|
||
|
L" integer value (1, 254).\n"
|
||
|
L" * This value must be set the same for ALL invocations.\n"
|
||
|
L" * If the value is not set, it defaults to (num_cores-1).\n"
|
||
|
L"\n"
|
||
|
L" The semaphore is automatically released when the program\n"
|
||
|
L" completes normally, OR if the program crashes (or even if\n"
|
||
|
L" limiter itself crashes).\n";
|
||
|
Error(usage.c_str());
|
||
|
exit(-1);
|
||
|
}
|
||
|
|
||
|
// Input command line is assumed to be of the form:
|
||
|
//
|
||
|
// thing.exe__PIPE_NAME.exe ...
|
||
|
//
|
||
|
// Specifically, wait for a semaphore (whose concurrency is specified by
|
||
|
// LIMITER_MAXCONCURRENT), and then pass through everything once we have
|
||
|
// acquired the semaphore.
|
||
|
//
|
||
|
// argv[0] is parsed for:
|
||
|
// * exe_to_shim_including_extension.exe
|
||
|
// * This could also be a bat or com. Anything that CreateProcess will
|
||
|
// accept.
|
||
|
// * "__"
|
||
|
// * We search for this separator from the end of argv[0], so the exe name
|
||
|
// could contain a double underscore if necessary.
|
||
|
// * PIPE_NAME
|
||
|
// * Can only contain single underscores, not a double underscore.
|
||
|
// * i.e. HELLO_WORLD_PIPE will work, but HELLO__WORLD_PIPE will not.
|
||
|
// * This would allow the shimmed exe to contain arbitrary numbers of
|
||
|
// underscores. We control the pipe name, but not necessarily the thing
|
||
|
// we're shimming.
|
||
|
//
|
||
|
int wmain(int, wchar_t** argv) {
|
||
|
tstring shimmed_plus_pipename = argv[0];
|
||
|
size_t last_slash = shimmed_plus_pipename.find_last_of(L"/\\");
|
||
|
if (last_slash != tstring::npos) {
|
||
|
shimmed_plus_pipename = shimmed_plus_pipename.substr(last_slash + 1);
|
||
|
}
|
||
|
|
||
|
size_t separator = shimmed_plus_pipename.rfind(L"__");
|
||
|
if (separator == tstring::npos) {
|
||
|
Usage(L"Cannot parse argv[0]. No '__' found. "
|
||
|
L"Should be like '[...(\\|/)]link.exe__PIPE_NAME.exe'");
|
||
|
}
|
||
|
tstring shimmed_exe = shimmed_plus_pipename.substr(0, separator);
|
||
|
tstring base_pipename = shimmed_plus_pipename.substr(separator + 2);
|
||
|
|
||
|
size_t dot = base_pipename.find(L'.');
|
||
|
if (dot == tstring::npos) {
|
||
|
Usage(L"Expected an executable extension in argv[0]. No '.' found.");
|
||
|
}
|
||
|
base_pipename = base_pipename.substr(0, dot);
|
||
|
|
||
|
return WaitAndRun(shimmed_exe, base_pipename);
|
||
|
}
|
||
|
|