1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include <stdio.h> 6 #include <stdlib.h> 7 8 #define NOMINMAX 9 #include <windows.h> 10 11 #include <algorithm> 12 #include <iterator> 13 #include <sstream> 14 #include <string> 15 #include <vector> 16 17 typedef std::basic_string<TCHAR> tstring; 18 19 namespace { 20 const bool g_is_debug = (_wgetenv(L"LIMITER_DEBUG") != NULL); 21 } 22 23 // Don't use stderr for errors because VS has large buffers on them, leading 24 // to confusing error output. 25 static void Error(const wchar_t* msg, ...) { 26 tstring new_msg = tstring(L"limiter fatal error: ") + msg + L"\n"; 27 va_list args; 28 va_start(args, msg); 29 vwprintf(new_msg.c_str(), args); 30 va_end(args); 31 } 32 33 static void Warn(const wchar_t* msg, ...) { 34 if (!g_is_debug) 35 return; 36 tstring new_msg = tstring(L"limiter warning: ") + msg + L"\n"; 37 va_list args; 38 va_start(args, msg); 39 vwprintf(new_msg.c_str(), args); 40 va_end(args); 41 } 42 43 static tstring ErrorMessageToString(DWORD err) { 44 TCHAR* msg_buf = NULL; 45 DWORD rc = FormatMessage( 46 FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, 47 NULL, // lpSource 48 err, 49 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), 50 reinterpret_cast<LPTSTR>(&msg_buf), 51 0, // nSize 52 NULL); // Arguments 53 if (!rc) 54 return L"unknown error"; 55 tstring ret(msg_buf); 56 LocalFree(msg_buf); 57 return ret; 58 } 59 60 static DWORD RunExe(const tstring& exe_name) { 61 STARTUPINFO startup_info = { sizeof(STARTUPINFO) }; 62 PROCESS_INFORMATION process_info; 63 DWORD exit_code; 64 65 GetStartupInfo(&startup_info); 66 tstring cmdline = tstring(GetCommandLine()); 67 68 size_t first_space = cmdline.find(' '); 69 if (first_space == -1) { 70 // I'm not sure why this would ever happen, but just in case... 71 cmdline = exe_name; 72 } else { 73 cmdline = exe_name + cmdline.substr(first_space); 74 } 75 76 if (!CreateProcess(NULL, // lpApplicationName 77 const_cast<TCHAR*>(cmdline.c_str()), 78 NULL, // lpProcessAttributes 79 NULL, // lpThreadAttributes 80 TRUE, // bInheritHandles 81 0, // dwCreationFlags, 82 NULL, // lpEnvironment, 83 NULL, // lpCurrentDirectory, 84 &startup_info, 85 &process_info)) { 86 Error(L"Error in CreateProcess[%s]: %s", 87 cmdline.c_str(), ErrorMessageToString(GetLastError()).c_str()); 88 return MAXDWORD; 89 } 90 CloseHandle(process_info.hThread); 91 WaitForSingleObject(process_info.hProcess, INFINITE); 92 GetExitCodeProcess(process_info.hProcess, &exit_code); 93 CloseHandle(process_info.hProcess); 94 return exit_code; 95 } 96 97 // Returns 0 if there was an error 98 static int CpuConcurrencyMetric(const tstring& envvar_name) { 99 int max_concurrent = 0; 100 std::vector<char> buffer(1); 101 BOOL ok = false; 102 DWORD last_error = 0; 103 do { 104 DWORD bufsize = buffer.size(); 105 ok = GetLogicalProcessorInformation( 106 reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION>(&buffer[0]), 107 &bufsize); 108 last_error = GetLastError(); 109 if (!ok && last_error == ERROR_INSUFFICIENT_BUFFER && 110 bufsize > buffer.size()) { 111 buffer.resize(bufsize); 112 } 113 } while (!ok && last_error == ERROR_INSUFFICIENT_BUFFER); 114 115 if (!ok) { 116 Warn(L"Error while getting number of cores. Try setting the " 117 L" environment variable '%s' to (num_cores - 1): %s", 118 envvar_name.c_str(), ErrorMessageToString(last_error).c_str()); 119 return 0; 120 } 121 122 PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pproc_info = 123 reinterpret_cast<PSYSTEM_LOGICAL_PROCESSOR_INFORMATION>(&buffer[0]); 124 int num_entries = buffer.size() / 125 sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); 126 127 for (int i = 0; i < num_entries; ++i) { 128 SYSTEM_LOGICAL_PROCESSOR_INFORMATION& info = pproc_info[i]; 129 if (info.Relationship == RelationProcessorCore) { 130 ++max_concurrent; 131 } 132 } 133 134 // Leave one core for other tasks 135 return max_concurrent - 1; 136 } 137 138 // TODO(defaults): Create a better heuristic than # of CPUs. It seems likely 139 // that the right value will, in fact, be based on the memory capacity of the 140 // machine, not on the number of CPUs. 141 enum ConcurrencyMetricEnum { 142 CONCURRENCY_METRIC_ONE, 143 CONCURRENCY_METRIC_CPU, 144 CONCURRENCY_METRIC_DEFAULT = CONCURRENCY_METRIC_CPU 145 }; 146 147 static int GetMaxConcurrency(const tstring& base_pipename, 148 ConcurrencyMetricEnum metric) { 149 static int max_concurrent = -1; 150 151 if (max_concurrent == -1) { 152 tstring envvar_name = base_pipename + L"_MAXCONCURRENCY"; 153 154 const LPTSTR max_concurrent_str = _wgetenv(envvar_name.c_str()); 155 max_concurrent = max_concurrent_str ? _wtoi(max_concurrent_str) : 0; 156 157 if (max_concurrent == 0) { 158 switch (metric) { 159 case CONCURRENCY_METRIC_CPU: 160 max_concurrent = CpuConcurrencyMetric(envvar_name); 161 if (max_concurrent) 162 break; 163 // else fall through 164 case CONCURRENCY_METRIC_ONE: 165 max_concurrent = 1; 166 break; 167 } 168 } 169 170 max_concurrent = std::min(std::max(max_concurrent, 1), 171 PIPE_UNLIMITED_INSTANCES); 172 } 173 174 return max_concurrent; 175 } 176 177 static HANDLE WaitForPipe(const tstring& pipename, 178 HANDLE event, 179 int max_concurrency) { 180 // We're using a named pipe instead of a semaphore so the Kernel can clean up 181 // after us if we crash while holding onto the pipe (A real semaphore will 182 // not release on process termination). 183 HANDLE pipe = INVALID_HANDLE_VALUE; 184 for (;;) { 185 pipe = CreateNamedPipe( 186 pipename.c_str(), 187 PIPE_ACCESS_DUPLEX, 188 PIPE_TYPE_BYTE, 189 max_concurrency, 190 1, // nOutBufferSize 191 1, // nInBufferSize 192 0, // nDefaultTimeOut 193 NULL); // Default security attributes (noinherit) 194 if (pipe != INVALID_HANDLE_VALUE) 195 break; 196 197 DWORD error = GetLastError(); 198 if (error == ERROR_PIPE_BUSY) { 199 if (event) { 200 WaitForSingleObject(event, 60 * 1000 /* ms */); 201 } else { 202 // TODO(iannucci): Maybe we should error out here instead of falling 203 // back to a sleep-poll 204 Sleep(5 * 1000 /* ms */); 205 } 206 } else { 207 Warn(L"Got error %d while waiting for pipe: %s", error, 208 ErrorMessageToString(error).c_str()); 209 return INVALID_HANDLE_VALUE; 210 } 211 } 212 213 return pipe; 214 } 215 216 static int WaitAndRun(const tstring& shimmed_exe, 217 const tstring& base_pipename) { 218 ULONGLONG start_time = 0, end_time = 0; 219 tstring pipename = L"\\\\.\\pipe\\" + base_pipename; 220 tstring event_name = L"Local\\EVENT_" + base_pipename; 221 222 // This event lets us do better than strict polling, but we don't rely on it 223 // (in case a process crashes before signalling the event). 224 HANDLE event = CreateEvent( 225 NULL, // Default security attributes 226 FALSE, // Manual reset 227 FALSE, // Initial state 228 event_name.c_str()); 229 230 if (g_is_debug) 231 start_time = GetTickCount64(); 232 233 HANDLE pipe = 234 WaitForPipe(pipename, event, 235 GetMaxConcurrency(base_pipename, CONCURRENCY_METRIC_DEFAULT)); 236 237 if (g_is_debug) { 238 end_time = GetTickCount64(); 239 wprintf(L" took %.2fs to acquire semaphore.\n", 240 (end_time - start_time) / 1000.0); 241 } 242 243 DWORD ret = RunExe(shimmed_exe); 244 245 if (pipe != INVALID_HANDLE_VALUE) 246 CloseHandle(pipe); 247 if (event != NULL) 248 SetEvent(event); 249 250 return ret; 251 } 252 253 void Usage(const tstring& msg) { 254 tstring usage(msg); 255 usage += L"\n" 256 L"Usage: SHIMED_NAME__SEMAPHORE_NAME\n" 257 L"\n" 258 L" SHIMMED_NAME - ex. 'link.exe' or 'lib.exe'\n" 259 L" - can be exe, bat, or com\n" 260 L" - must exist in PATH\n" 261 L"\n" 262 L" SEMAPHORE_NAME - ex. 'SOME_NAME' or 'GROOVY_SEMAPHORE'\n" 263 L"\n" 264 L" Example:\n" 265 L" link.exe__LINK_LIMITER.exe\n" 266 L" lib.exe__LINK_LIMITER.exe\n" 267 L" * Both will limit on the same semaphore\n" 268 L"\n" 269 L" link.exe__LINK_LIMITER.exe\n" 270 L" lib.exe__LIB_LIMITER.exe\n" 271 L" * Both will limit on independent semaphores\n" 272 L"\n" 273 L" This program is meant to be run after renaming it into the\n" 274 L" above format. Once you have done so, executing it will block\n" 275 L" on the availability of the semaphore SEMAPHORE_NAME. Once\n" 276 L" the semaphore is obtained, it will execute SHIMMED_NAME, \n" 277 L" passing through all arguments as-is.\n" 278 L"\n" 279 L" The maximum concurrency can be manually set by setting the\n" 280 L" environment variable <SEMAPHORE_NAME>_MAXCONCURRENCY to an\n" 281 L" integer value (1, 254).\n" 282 L" * This value must be set the same for ALL invocations.\n" 283 L" * If the value is not set, it defaults to (num_cores-1).\n" 284 L"\n" 285 L" The semaphore is automatically released when the program\n" 286 L" completes normally, OR if the program crashes (or even if\n" 287 L" limiter itself crashes).\n"; 288 Error(usage.c_str()); 289 exit(-1); 290 } 291 292 // Input command line is assumed to be of the form: 293 // 294 // thing.exe__PIPE_NAME.exe ... 295 // 296 // Specifically, wait for a semaphore (whose concurrency is specified by 297 // LIMITER_MAXCONCURRENT), and then pass through everything once we have 298 // acquired the semaphore. 299 // 300 // argv[0] is parsed for: 301 // * exe_to_shim_including_extension.exe 302 // * This could also be a bat or com. Anything that CreateProcess will 303 // accept. 304 // * "__" 305 // * We search for this separator from the end of argv[0], so the exe name 306 // could contain a double underscore if necessary. 307 // * PIPE_NAME 308 // * Can only contain single underscores, not a double underscore. 309 // * i.e. HELLO_WORLD_PIPE will work, but HELLO__WORLD_PIPE will not. 310 // * This would allow the shimmed exe to contain arbitrary numbers of 311 // underscores. We control the pipe name, but not necessarily the thing 312 // we're shimming. 313 // 314 int wmain(int, wchar_t** argv) { 315 tstring shimmed_plus_pipename = argv[0]; 316 size_t last_slash = shimmed_plus_pipename.find_last_of(L"/\\"); 317 if (last_slash != tstring::npos) { 318 shimmed_plus_pipename = shimmed_plus_pipename.substr(last_slash + 1); 319 } 320 321 size_t separator = shimmed_plus_pipename.rfind(L"__"); 322 if (separator == tstring::npos) { 323 Usage(L"Cannot parse argv[0]. No '__' found. " 324 L"Should be like '[...(\\|/)]link.exe__PIPE_NAME.exe'"); 325 } 326 tstring shimmed_exe = shimmed_plus_pipename.substr(0, separator); 327 tstring base_pipename = shimmed_plus_pipename.substr(separator + 2); 328 329 size_t dot = base_pipename.find(L'.'); 330 if (dot == tstring::npos) { 331 Usage(L"Expected an executable extension in argv[0]. No '.' found."); 332 } 333 base_pipename = base_pipename.substr(0, dot); 334 335 return WaitAndRun(shimmed_exe, base_pipename); 336 } 337 338