new fortune
[feisty_meow.git] / nucleus / library / processes / process_control.cpp
1
2 //NOTE:
3 //
4 //  this thing is showing bad behavior on win32 when unicode is enabled.
5 //  therefore unicode is currently disabled for win32, which is a shame.
6 //  but something needs to be fixed in our unicode conversion stuff; the unicode versions
7 //  of the file names were not getting correctly back-converted into the ascii counterpart,
8 //  and *that* is broken.
9 //  ** this may be a widespread issue in the win32 code related to unicode right now!
10 //  ** needs further investigation when a reason to better support ms-windows emerges.
11 //
12 //  => but don't panic...  right now things work as long as you do a utf-8 / ascii build,
13 //     which is how everything's configured.
14
15
16 /*
17 *  Name   : process_control
18 *  Author : Chris Koeritz
19 **
20 * Copyright (c) 2000-$now By Author.  This program is free software; you can  *
21 * redistribute it and/or modify it under the terms of the GNU General Public  *
22 * License as published by the Free Software Foundation; either version 2 of   *
23 * the License or (at your option) any later version.  This is online at:      *
24 *     http://www.fsf.org/copyleft/gpl.html                                    *
25 * Please send any updates to: fred@gruntose.com                               *
26 */
27
28 #include "process_entry.h"
29 #include "process_control.h"
30
31 #include <application/windoze_helper.h>
32 #include <basis/astring.h>
33 #include <basis/utf_conversion.h>
34 #include <configuration/application_configuration.h>
35 #include <filesystem/filename.h>
36 #include <loggers/program_wide_logger.h>
37 #include <loggers/standard_log_base.h>
38 #include <mathematics/chaos.h>
39 #include <structures/set.h>
40 #include <structures/version_record.h>
41
42 #include <stdio.h>
43 #include <stdlib.h>
44 #ifndef _MSC_VER
45   #include <unistd.h>
46 #endif
47
48 using namespace basis;
49 using namespace configuration;
50 using namespace filesystem;
51 using namespace loggers;
52 using namespace mathematics;
53 using namespace structures;
54
55 namespace processes {
56
57 #ifdef _MSC_VER
58   #include <tlhelp32.h>
59   const astring NTVDM_NAME = "ntvdm.exe";
60     // the umbrella process that hangs onto 16 bit tasks for NT.
61 //  #ifdef _MSCVER
62 //    #include <vdmdbg.h>
63 //  #endif
64 #else
65   #include <signal.h>
66   #include <stdio.h>
67 #endif
68
69 //#define DEBUG_PROCESS_CONTROL
70   // uncomment for noisier debugging.
71
72 #undef LOG
73 #define LOG(s) CLASS_EMERGENCY_LOG(program_wide_logger::get(), s)
74
75 //////////////
76
77 class process_implementation_hider
78 {
79 public:
80 #ifdef _MSC_VER
81   // psapi members:
82   application_instance psapi_dll;
83   application_instance vdm_dll;
84   BOOL (WINAPI *enumerate_processes)(basis::un_int *, basis::un_int cb, basis::un_int *);
85   BOOL (WINAPI *enumerate_modules)(HANDLE, HMODULE *, basis::un_int, basis::un_int *);
86   basis::un_int (WINAPI *get_module_name)(HANDLE, HMODULE, LPTSTR, basis::un_int);
87 //#ifdef _MSCVER
88 //  INT (WINAPI *tasker_16bit)(basis::un_int, TASKENUMPROCEX  fp, LPARAM);
89 //#endif
90
91   // toolhelp members:
92   application_instance kernel32_dll;
93   HANDLE (WINAPI *create_snapshot)(basis::un_int,basis::un_int);
94   BOOL (WINAPI *first_process)(HANDLE,LPPROCESSENTRY32);
95   BOOL (WINAPI *next_process)(HANDLE,LPPROCESSENTRY32);
96
97   // get an atomic view of the process list, which rapidly becomes out of date.
98 ///  HANDLE hSnapShot;
99
100   process_implementation_hider()
101     : psapi_dll(NULL_POINTER), vdm_dll(NULL_POINTER), enumerate_processes(NULL_POINTER),
102       enumerate_modules(NULL_POINTER), get_module_name(NULL_POINTER),
103 //#ifdef _MSCVER
104 //      tasker_16bit(NULL_POINTER),
105 //#endif
106       kernel32_dll(NULL_POINTER), create_snapshot(NULL_POINTER), first_process(NULL_POINTER),
107       next_process(NULL_POINTER) {}
108
109   ~process_implementation_hider() {
110     if (psapi_dll) FreeLibrary(psapi_dll);
111     if (vdm_dll) FreeLibrary(vdm_dll);
112     if (kernel32_dll) FreeLibrary(kernel32_dll);
113     psapi_dll = NULL_POINTER;
114     vdm_dll = NULL_POINTER;
115     kernel32_dll = NULL_POINTER;
116   }
117 #endif
118 };
119
120 //////////////
121
122 class process_info_clump
123 {
124 public:
125   basis::un_int _process_id;
126   process_entry_array &_to_fill;  // where to add entries.
127
128   process_info_clump(basis::un_int id, process_entry_array &to_fill)
129       : _process_id(id), _to_fill(to_fill) {}
130 };
131
132 //////////////
133
134 // top-level functions...
135
136 process_control::process_control()
137 : _ptrs(new process_implementation_hider),
138 #ifdef _MSC_VER
139   _use_psapi(true),
140 #else
141   _rando(new chaos),
142 #endif
143   _healthy(false)
144 {
145   // Check to see if were running under Windows95 or Windows NT.
146   version osver = application_configuration::get_OS_version();
147
148 #ifdef _MSC_VER
149   if (osver.v_revision() == VER_PLATFORM_WIN32_WINDOWS) {
150     // we're on Windows 95, so use the toolhelp API for the processes.
151     _use_psapi = false;
152   } else if (osver.v_major() >= 5) {
153     // w2k and onward can do the toolhelp instead of psapi.
154     _use_psapi = false;
155   }
156   if (_use_psapi)
157     _healthy = initialize_psapi_support();
158   else
159     _healthy = initialize_toolhelp_support();
160 #else
161   _healthy = true;
162 #endif
163 }
164
165 process_control::~process_control()
166 {
167   WHACK(_ptrs);
168 #ifndef _MSC_VER
169   WHACK(_rando);
170 #endif
171 }
172
173 void process_control::sort_by_name(process_entry_array &v)
174 {
175   process_entry temp;
176   for (int gap = v.length() / 2; gap > 0; gap /= 2)
177     for (int i = gap; i < v.length(); i++)
178       for (int j = i - gap; j >= 0
179           && (filename(v[j].path()).basename().raw()
180              > filename(v[j + gap].path()).basename().raw());
181           j = j - gap)
182       { temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; }
183 }
184
185 void process_control::sort_by_pid(process_entry_array &v)
186 {
187   process_entry temp;
188   for (int gap = v.length() / 2; gap > 0; gap /= 2)
189     for (int i = gap; i < v.length(); i++)
190       for (int j = i - gap; j >= 0 && (v[j]._process_id
191           > v[j + gap]._process_id); j = j - gap)
192       { temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; }
193 }
194
195 bool process_control::query_processes(process_entry_array &to_fill)
196 {
197   if (!_healthy) return false;
198 #ifdef _MSC_VER
199   if (!_use_psapi) {
200     // we're on Windows 95 or something, so use the toolhelp API for the
201     // processes.
202     return get_processes_with_toolhelp(to_fill);
203   } else {
204     // we're on Windows NT and so on; use the process API (PSAPI) to get info.
205     return get_processes_with_psapi(to_fill);
206   }
207 #else
208   return get_processes_with_ps(to_fill);
209 #endif
210 }
211
212 #ifdef _MSC_VER
213 bool process_control::initialize_psapi_support()
214 {
215   // create an instance of the PSAPI dll for querying 32-bit processes and
216   // an instance of the VDM dll support just in case there are also some
217   // 16-bit processes.
218   _ptrs->psapi_dll = LoadLibraryA("psapi.dll");
219   if (!_ptrs->psapi_dll) return false; 
220   _ptrs->vdm_dll = LoadLibraryA("vdmdbg.dll");
221   if (!_ptrs->vdm_dll) return false;
222
223   // locate the functions that we want to call.
224   _ptrs->enumerate_processes = (BOOL(WINAPI *)(basis::un_int *,basis::un_int,basis::un_int*))
225         GetProcAddress(_ptrs->psapi_dll, "EnumProcesses");
226   _ptrs->enumerate_modules
227       = (BOOL(WINAPI *)(HANDLE, HMODULE *, basis::un_int, basis::un_int *))
228         GetProcAddress(_ptrs->psapi_dll, "EnumProcessModules");
229   _ptrs->get_module_name
230       = (basis::un_int (WINAPI *)(HANDLE, HMODULE, LPTSTR, basis::un_int))
231         GetProcAddress(_ptrs->psapi_dll, "GetModuleFileNameExA");
232 //#ifdef _MSCVER
233 //  _ptrs->tasker_16bit = (INT(WINAPI *)(basis::un_int, TASKENUMPROCEX, LPARAM))
234 //        GetProcAddress(_ptrs->vdm_dll, "VDMEnumTaskWOWEx");
235 //#endif
236   if (!_ptrs->enumerate_processes || !_ptrs->enumerate_modules
237       || !_ptrs->get_module_name
238 //#ifdef _MSCVER
239 //      || !_ptrs->tasker_16bit
240 //#endif
241       ) return false;
242
243   return true;
244 }
245
246 bool process_control::initialize_toolhelp_support()
247 {
248   // get hooked up with the kernel dll so we can use toolhelp functions.
249   _ptrs->kernel32_dll = LoadLibraryA("Kernel32.DLL");
250   if (!_ptrs->kernel32_dll) return false;
251
252   // create pointers to the functions we want to invoke.
253   _ptrs->create_snapshot = (HANDLE(WINAPI *)(basis::un_int,basis::un_int))
254         GetProcAddress(_ptrs->kernel32_dll, "CreateToolhelp32Snapshot");
255   _ptrs->first_process = (BOOL(WINAPI *)(HANDLE,LPPROCESSENTRY32))
256         GetProcAddress(_ptrs->kernel32_dll, "Process32First");
257   _ptrs->next_process = (BOOL(WINAPI *)(HANDLE,LPPROCESSENTRY32))
258         GetProcAddress(_ptrs->kernel32_dll, "Process32Next");
259   if (!_ptrs->next_process || !_ptrs->first_process
260       || !_ptrs->create_snapshot) return false;
261   return true;
262 }
263
264 #endif
265
266 bool process_control::zap_process(basis::un_int to_zap)
267 {
268 #ifdef DEBUG_PROCESS_CONTROL
269   FUNCDEF("zap_process");
270 #endif
271   if (!_healthy) return false;
272 #ifndef _MSC_VER
273   int ret = kill(to_zap, 9);
274     // send the serious take-down signal to the process.
275   return !ret;
276 #else
277   HANDLE h = OpenProcess(PROCESS_TERMINATE, false, to_zap);
278   if (!h) {
279 #ifdef DEBUG_PROCESS_CONTROL
280     int err = critical_events::system_error();
281     LOG(a_sprintf("error zapping process %d=", to_zap)
282         + critical_events::system_error_text(err));
283 #endif
284     return false;
285   }
286   int exit_code = 0;
287   BOOL ret = TerminateProcess(h, exit_code);
288   CloseHandle(h);
289   return !!ret;
290 #endif
291 }
292
293 process_entry process_control::query_process(basis::un_int to_query)
294 {
295   FUNCDEF("query_process");
296   process_entry to_return;
297
298   process_entry_array to_fill;
299   bool got_em = query_processes(to_fill);
300   if (!got_em) return to_return;
301
302   for (int i = 0; i < to_fill.length(); i++) {
303     if (to_fill[i]._process_id == to_query)
304       return to_fill[i];
305   }
306
307 //hmmm: implement more specifically.
308 #ifndef _MSC_VER
309 //put in the single process grabber deal.
310 #else
311 //grab the entry from the list.
312 #endif
313
314   return to_return;
315 }
316
317 bool process_control::find_process_in_list(const process_entry_array &processes,
318     const astring &app_name_in, int_set &pids)
319 {
320 #ifdef DEBUG_PROCESS_CONTROL
321   FUNCDEF("find_process_in_list");
322 #endif
323   pids.clear();
324   astring app_name = app_name_in.lower();
325
326   version os_ver = application_configuration::get_OS_version();
327
328   bool compare_prefix = (os_ver.v_major() == 5) && (os_ver.v_minor() == 0);
329     // we only compare the first 15 letters due to a recently noticed bizarre
330     // bug where w2k only shows (and reports) the first 15 letters of file
331     // names through toolhelp.
332
333   bool found = false;  // was it seen in process list?
334   for (int i = 0; i < processes.length(); i++) {
335     filename path = processes[i].path();
336     astring base = path.basename().raw().lower();
337     // a kludge for w2k is needed--otherwise we will miss seeing names that
338     // really are running due to the toolhelp api being busted and only
339     // reporting the first 15 characters of the name.
340     if ( (compare_prefix && (base.compare(app_name, 0, 0, 15, false)))
341         || (base == app_name) ) {
342       found = true;
343       pids.add(processes[i]._process_id);
344     }
345   }
346 #ifdef DEBUG_PROCESS_CONTROL
347   if (!found)
348     LOG(astring("failed to find the program called ") + app_name);
349 #endif
350   return found;
351 }
352
353 //////////////
354
355 #ifdef _MSC_VER
356 // this section is the PSAPI version of the query.
357
358 // called back on each 16 bit task.
359 BOOL WINAPI process_16bit(basis::un_int dwThreadId, WORD module_handle16, WORD hTask16,
360     PSZ pszModName, PSZ pszFileName, LPARAM lpUserDefined)
361 {
362   process_info_clump *to_stuff = (process_info_clump *)lpUserDefined;
363   process_entry to_add;
364   to_add._process_id = to_stuff->_process_id;
365   to_add._module16 = hTask16;
366   to_add.path(pszFileName);
367 //threads, etc?
368   to_stuff->_to_fill += to_add;
369   return true;
370 }
371
372 bool process_control::get_processes_with_psapi(process_entry_array &to_fill)
373 {
374   // prepare the result object.
375   to_fill.reset();
376
377   // loop over the process enumeration function until we are sure that we
378   // have allocated a large enough space for all existing processes.
379   bool got_all = false;
380   basis::un_int *pid_list = NULL_POINTER;
381   basis::un_int max_size = 428 * sizeof(basis::un_int);
382   basis::un_int actual_size = 0;
383   while (!got_all) {
384     pid_list = (basis::un_int *)HeapAlloc(GetProcessHeap(), 0, max_size);
385     if (!pid_list) return false;
386     if (!_ptrs->enumerate_processes(pid_list, max_size, &actual_size)) {
387       HeapFree(GetProcessHeap(), 0, pid_list);
388       return false;
389     }
390     if (actual_size == max_size) {
391       // there were too many to store, so whack the partial list.
392       HeapFree(GetProcessHeap(), 0, pid_list);
393       max_size *= 2;  // try with twice as much space.
394     } else got_all = true;
395   }
396
397   // calculate the number of process ids that got stored.
398   basis::un_int ids = actual_size / sizeof(basis::un_int);
399
400   // examine each process id that we found.
401   for (basis::un_int i = 0; i < ids; i++) {
402     // get process information if security permits.
403 //turn chunk below into "scan process" or something.
404     HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
405         false, pid_list[i]);
406     flexichar process_name[MAX_ABS_PATH + 1] = { '\0' };
407     if (hProcess) {
408       // go over the modules for the process.  the first will be the main
409       // application module and the others will be threads.  ???
410       basis::un_int max_size = 1 * sizeof(HMODULE);
411 //hmmm: could do a rescan loop here if too many.
412       basis::un_int actual_size = 0;
413       HMODULE *module_handles = new HMODULE[max_size + 1];
414       if (!module_handles) {
415         CloseHandle(hProcess);
416         HeapFree(GetProcessHeap(), 0, pid_list);
417         return false;
418       }
419       if (_ptrs->enumerate_modules(hProcess, module_handles, max_size,
420           &actual_size)) {
421         // we want the name of the first module.
422         if (!_ptrs->get_module_name(hProcess, *module_handles, process_name,
423             sizeof(process_name)))
424           process_name[0] = 0;
425       }
426       WHACK(module_handles);
427       CloseHandle(hProcess);
428     }
429
430     // we add whatever information we were able to find about this process.
431     process_entry new_entry;
432     new_entry._process_id = pid_list[i];
433     astring converted_name = from_unicode_temp(process_name);
434     new_entry.path(converted_name);
435
436 //how to get?  performance data helper?
437 ///    new_entry._threads = threads;
438     to_fill += new_entry;
439
440     // if we're looking at ntvdm, then there might be 16 bit processes
441     // attached to it.
442     if (new_entry.path().length() >= NTVDM_NAME.length()) {
443       astring temp = new_entry.path().substring
444           (new_entry.path().end() - NTVDM_NAME.length() + 1,
445           new_entry.path().end());
446       temp.to_lower();
447 //#ifdef _MSCVER
448 ////hmmm: pull this back in for mingw when it seems to be supported, if ever.
449 //      if (temp == NTVDM_NAME) {
450 //        // set up a callback stampede on the 16 bit processes.
451 //        process_info_clump info(pid_list[i], to_fill);
452 //        _ptrs->tasker_16bit(pid_list[i], (TASKENUMPROCEX)process_16bit,
453 //             (LPARAM)&info);
454 //      }
455 //#endif
456     }
457   }
458
459   if (pid_list) HeapFree(GetProcessHeap(), 0, pid_list);
460   return true;
461 }
462
463 //////////////
464
465 // this is the toolhelp version of the query.
466
467 bool process_control::get_processes_with_toolhelp(process_entry_array &to_fill)
468 {
469   // prepare the result object.
470   to_fill.reset();
471
472   // get an atomic view of the process list, which rapidly becomes out of date.
473   HANDLE hSnapShot;
474   hSnapShot = _ptrs->create_snapshot(TH32CS_SNAPPROCESS, 0);
475   if (hSnapShot == INVALID_HANDLE_VALUE) return false;
476
477   // start iterating through the snapshot by getting the first process.
478   PROCESSENTRY32 entry;
479   entry.dwSize = sizeof(PROCESSENTRY32);
480   BOOL keep_going = _ptrs->first_process(hSnapShot, &entry);
481
482   // while we see valid processes, iterate through them.
483   while (keep_going) {
484     // add an entry for the current process.
485     process_entry new_entry;
486     new_entry._process_id = entry.th32ProcessID;
487     new_entry._references = entry.cntUsage;
488     new_entry._threads = entry.cntThreads;
489     new_entry._parent_process_id = entry.th32ParentProcessID;
490     astring exe_file = from_unicode_temp(entry.szExeFile);
491     new_entry.path(exe_file);
492     to_fill += new_entry;
493     entry.dwSize = sizeof(PROCESSENTRY32);  // reset struct size.
494     keep_going = _ptrs->next_process(hSnapShot, &entry);
495   }
496
497   CloseHandle(hSnapShot);
498   return true;
499 }
500 #endif  // __WIN32__
501
502 #ifndef _MSC_VER
503
504 #define CLOSE_TEMPORARY_FILE { \
505 /*  continuable_error("process_control", "get_processes_with_ps", error); */ \
506   if (output) { \
507     fclose(output); \
508     unlink(tmpfile.s()); \
509   } \
510 }
511
512 bool process_control::get_processes_with_ps(process_entry_array &to_fill)
513 {
514   FUNCDEF("get_processes_with_ps");
515   to_fill.reset();
516   // we ask the operating system to give us a list of processes.
517   a_sprintf tmpfile("/tmp/proc_list_%d_%d.txt", application_configuration::process_id(),
518       _rando->inclusive(1, 400000));
519   a_sprintf cmd("ps wax --format \"%%p %%a\" >%s", tmpfile.s());
520 //hmmm: add more info as we expand the process entry.
521   FILE *output = NULL_POINTER;  // initialize now to establish variable for our macro.
522   int sysret = system(cmd.s());
523   if (negative(sysret)) {
524 LOG("got negative return from system()!");
525     CLOSE_TEMPORARY_FILE;
526     return false;
527   }
528   output = fopen(tmpfile.s(), "r");
529   if (!output) {
530 LOG("failed to open process list file!");
531     CLOSE_TEMPORARY_FILE;
532     return false;
533   }
534   const int max_buff = 10000;
535   char buff[max_buff];
536   size_t size_read = 1;
537   astring accumulator;
538   while (size_read > 0) {
539     // read bytes from the file.
540     size_read = fread(buff, 1, max_buff, output);
541     // if there was anything, store it in the string.
542     if (size_read > 0)
543       accumulator += astring(astring::UNTERMINATED, buff, size_read);
544   }
545   CLOSE_TEMPORARY_FILE;
546   // parse the string up now.
547   bool first_line = true;
548   while (accumulator.length()) {
549     // eat any spaces in front.
550     if (first_line) {
551       // we toss the first line since it's a header with columns.
552       int cr_indy = accumulator.find('\n');
553       accumulator.zap(0, cr_indy);
554       if (accumulator[accumulator.end()] == '\r')
555         accumulator.zap(accumulator.end(), accumulator.end());
556       first_line = false;
557       continue;
558     }
559     while (accumulator.length() && (accumulator[0] == ' '))
560       accumulator.zap(0, 0);
561     // look for the first part of the line; the process id.
562     int num_indy = accumulator.find(' ');
563     if (negative(num_indy)) break;
564     basis::un_int p_id = accumulator.substring(0, num_indy).convert(0);
565     accumulator.zap(0, num_indy);
566     int cr_indy = accumulator.find('\n');
567     if (negative(cr_indy))
568       cr_indy = accumulator.end() + 1;
569     astring proc_name = accumulator.substring(0, cr_indy - 1);
570     if (proc_name[proc_name.end()] == '\r')
571       proc_name.zap(proc_name.end(), proc_name.end());
572     accumulator.zap(0, cr_indy);
573     int space_indy = proc_name.find(' ');
574 //hmmm: this is incorrect regarding names that do have spaces in them.
575     if (negative(space_indy))
576       space_indy = proc_name.end() + 1;
577     process_entry to_add;
578     to_add._process_id = p_id;
579     astring path = proc_name.substring(0, space_indy - 1);
580     // patch the pathname if we see any bracketed items.
581     int brackets_in = 0;
582     for (int i = 0; i < path.length(); i++) {
583       if (path[i] == '[') brackets_in++;
584       else if (path[i] == ']') brackets_in--;
585       if (brackets_in) {
586         // if we see a slash inside brackets, then we patch it so it doesn't
587         // confuse the filename object's directory handling.
588         if ( (path[i] == '/') || (path[i] == '\\') )
589           path[i] = '#';
590       }
591     }
592     to_add.path(path);
593     to_fill += to_add;
594   }
595   return true;
596 }
597 #endif  // __UNIX__
598
599 } //namespace.
600