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.
Error(const wchar_t * msg,...)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 
Warn(const wchar_t * msg,...)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 
ErrorMessageToString(DWORD err)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 
RunExe(const tstring & exe_name)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                      &cmdline[0],
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
CpuConcurrencyMetric(const tstring & envvar_name)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 
GetMaxConcurrency(const tstring & base_pipename,ConcurrencyMetricEnum metric)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 
WaitForPipe(const tstring & pipename,HANDLE event,int max_concurrency)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 
WaitAndRun(const tstring & shimmed_exe,const tstring & base_pipename)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 
Usage(const tstring & msg)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 //
wmain(int,wchar_t ** argv)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