wow. that was easy: git mv core nucleus
[feisty_meow.git] / nucleus / library / processes / process_control.cpp
diff --git a/nucleus/library/processes/process_control.cpp b/nucleus/library/processes/process_control.cpp
new file mode 100644 (file)
index 0000000..ce0dbf0
--- /dev/null
@@ -0,0 +1,606 @@
+
+//NOTE:
+//
+//  this thing is showing bad behavior on win32 when unicode is enabled.
+//  therefore unicode is currently disabled for win32, which is a shame.
+//  but something needs to be fixed in our unicode conversion stuff; the unicode versions
+//  of the file names were not getting correctly back-converted into the ascii counterpart,
+//  and *that* is broken.
+//  ** this may be a widespread issue in the win32 code related to unicode right now!
+//  ** needs further investigation when a reason to better support ms-windows emerges.
+//
+//  => but don't panic...  right now things work as long as you do a utf-8 / ascii build,
+//     which is how everything's configured.
+
+
+/*
+*  Name   : process_control
+*  Author : Chris Koeritz
+**
+* Copyright (c) 2000-$now By Author.  This program is free software; you can  *
+* redistribute it and/or modify it under the terms of the GNU General Public  *
+* License as published by the Free Software Foundation; either version 2 of   *
+* the License or (at your option) any later version.  This is online at:      *
+*     http://www.fsf.org/copyleft/gpl.html                                    *
+* Please send any updates to: fred@gruntose.com                               *
+*/
+
+#include "process_entry.h"
+#include "process_control.h"
+
+#include <application/windoze_helper.h>
+#include <basis/astring.h>
+#include <basis/utf_conversion.h>
+#include <configuration/application_configuration.h>
+#include <filesystem/filename.h>
+#include <loggers/program_wide_logger.h>
+#include <loggers/standard_log_base.h>
+#include <mathematics/chaos.h>
+#include <structures/set.h>
+#include <structures/version_record.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#ifdef __UNIX__
+  #include <unistd.h>
+#endif
+
+using namespace basis;
+using namespace configuration;
+using namespace filesystem;
+using namespace loggers;
+using namespace mathematics;
+using namespace structures;
+
+namespace processes {
+
+#ifdef __WIN32__
+  #include <tlhelp32.h>
+  const astring NTVDM_NAME = "ntvdm.exe";
+    // the umbrella process that hangs onto 16 bit tasks for NT.
+  #ifdef _MSCVER
+    #include <vdmdbg.h>
+  #endif
+#endif
+#ifdef __UNIX__
+  #include <signal.h>
+  #include <stdio.h>
+#endif
+
+//#define DEBUG_PROCESS_CONTROL
+  // uncomment for noisier debugging.
+
+#undef LOG
+#define LOG(s) CLASS_EMERGENCY_LOG(program_wide_logger::get(), s)
+
+//////////////
+
+class process_implementation_hider
+{
+public:
+#ifdef __WIN32__
+  // psapi members:
+  application_instance psapi_dll;
+  application_instance vdm_dll;
+  BOOL (WINAPI *enumerate_processes)(basis::un_int *, basis::un_int cb, basis::un_int *);
+  BOOL (WINAPI *enumerate_modules)(HANDLE, HMODULE *, basis::un_int, basis::un_int *);
+  basis::un_int (WINAPI *get_module_name)(HANDLE, HMODULE, LPTSTR, basis::un_int);
+#ifdef _MSCVER
+  INT (WINAPI *tasker_16bit)(basis::un_int, TASKENUMPROCEX  fp, LPARAM);
+#endif
+
+  // toolhelp members:
+  application_instance kernel32_dll;
+  HANDLE (WINAPI *create_snapshot)(basis::un_int,basis::un_int);
+  BOOL (WINAPI *first_process)(HANDLE,LPPROCESSENTRY32);
+  BOOL (WINAPI *next_process)(HANDLE,LPPROCESSENTRY32);
+
+  // get an atomic view of the process list, which rapidly becomes out of date.
+///  HANDLE hSnapShot;
+
+  process_implementation_hider()
+    : psapi_dll(NIL), vdm_dll(NIL), enumerate_processes(NIL),
+      enumerate_modules(NIL), get_module_name(NIL),
+#ifdef _MSCVER
+      tasker_16bit(NIL),
+#endif
+      kernel32_dll(NIL), create_snapshot(NIL), first_process(NIL),
+      next_process(NIL) {}
+
+  ~process_implementation_hider() {
+    if (psapi_dll) FreeLibrary(psapi_dll);
+    if (vdm_dll) FreeLibrary(vdm_dll);
+    if (kernel32_dll) FreeLibrary(kernel32_dll);
+    psapi_dll = NIL;
+    vdm_dll = NIL;
+    kernel32_dll = NIL;
+  }
+#endif
+};
+
+//////////////
+
+class process_info_clump
+{
+public:
+  basis::un_int _process_id;
+  process_entry_array &_to_fill;  // where to add entries.
+
+  process_info_clump(basis::un_int id, process_entry_array &to_fill)
+      : _process_id(id), _to_fill(to_fill) {}
+};
+
+//////////////
+
+// top-level functions...
+
+process_control::process_control()
+: _ptrs(new process_implementation_hider),
+#ifdef __WIN32__
+  _use_psapi(true),
+#endif
+#ifdef __UNIX__
+  _rando(new chaos),
+#endif
+  _healthy(false)
+{
+  // Check to see if were running under Windows95 or Windows NT.
+  version osver = application_configuration::get_OS_version();
+
+#ifdef __WIN32__
+  if (osver.v_revision() == VER_PLATFORM_WIN32_WINDOWS) {
+    // we're on Windows 95, so use the toolhelp API for the processes.
+    _use_psapi = false;
+  } else if (osver.v_major() >= 5) {
+    // w2k and onward can do the toolhelp instead of psapi.
+    _use_psapi = false;
+  }
+  if (_use_psapi)
+    _healthy = initialize_psapi_support();
+  else
+    _healthy = initialize_toolhelp_support();
+#endif
+#ifdef __UNIX__
+  _healthy = true;
+#endif
+}
+
+process_control::~process_control()
+{
+  WHACK(_ptrs);
+#ifdef __UNIX__
+  WHACK(_rando);
+#endif
+}
+
+void process_control::sort_by_name(process_entry_array &v)
+{
+  process_entry temp;
+  for (int gap = v.length() / 2; gap > 0; gap /= 2)
+    for (int i = gap; i < v.length(); i++)
+      for (int j = i - gap; j >= 0
+          && (filename(v[j].path()).basename().raw()
+             > filename(v[j + gap].path()).basename().raw());
+          j = j - gap)
+      { temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; }
+}
+
+void process_control::sort_by_pid(process_entry_array &v)
+{
+  process_entry temp;
+  for (int gap = v.length() / 2; gap > 0; gap /= 2)
+    for (int i = gap; i < v.length(); i++)
+      for (int j = i - gap; j >= 0 && (v[j]._process_id
+          > v[j + gap]._process_id); j = j - gap)
+      { temp = v[j]; v[j] = v[j + gap]; v[j + gap] = temp; }
+}
+
+bool process_control::query_processes(process_entry_array &to_fill)
+{
+  if (!_healthy) return false;
+#ifdef __WIN32__
+  if (!_use_psapi) {
+    // we're on Windows 95 or something, so use the toolhelp API for the
+    // processes.
+    return get_processes_with_toolhelp(to_fill);
+  } else {
+    // we're on Windows NT and so on; use the process API (PSAPI) to get info.
+    return get_processes_with_psapi(to_fill);
+  }
+#endif
+#ifdef __UNIX__
+  return get_processes_with_ps(to_fill);
+#endif
+}
+
+#ifdef __WIN32__
+bool process_control::initialize_psapi_support()
+{
+  // create an instance of the PSAPI dll for querying 32-bit processes and
+  // an instance of the VDM dll support just in case there are also some
+  // 16-bit processes.
+  _ptrs->psapi_dll = LoadLibraryA("psapi.dll");
+  if (!_ptrs->psapi_dll) return false; 
+  _ptrs->vdm_dll = LoadLibraryA("vdmdbg.dll");
+  if (!_ptrs->vdm_dll) return false;
+
+  // locate the functions that we want to call.
+  _ptrs->enumerate_processes = (BOOL(WINAPI *)(basis::un_int *,basis::un_int,basis::un_int*))
+        GetProcAddress(_ptrs->psapi_dll, "EnumProcesses");
+  _ptrs->enumerate_modules
+      = (BOOL(WINAPI *)(HANDLE, HMODULE *, basis::un_int, basis::un_int *))
+        GetProcAddress(_ptrs->psapi_dll, "EnumProcessModules");
+  _ptrs->get_module_name
+      = (basis::un_int (WINAPI *)(HANDLE, HMODULE, LPTSTR, basis::un_int))
+        GetProcAddress(_ptrs->psapi_dll, "GetModuleFileNameExA");
+#ifdef _MSCVER
+  _ptrs->tasker_16bit = (INT(WINAPI *)(basis::un_int, TASKENUMPROCEX, LPARAM))
+        GetProcAddress(_ptrs->vdm_dll, "VDMEnumTaskWOWEx");
+#endif
+  if (!_ptrs->enumerate_processes || !_ptrs->enumerate_modules
+      || !_ptrs->get_module_name
+#ifdef _MSCVER
+      || !_ptrs->tasker_16bit
+#endif
+      ) return false;
+
+  return true;
+}
+
+bool process_control::initialize_toolhelp_support()
+{
+  // get hooked up with the kernel dll so we can use toolhelp functions.
+  _ptrs->kernel32_dll = LoadLibraryA("Kernel32.DLL");
+  if (!_ptrs->kernel32_dll) return false;
+
+  // create pointers to the functions we want to invoke.
+  _ptrs->create_snapshot = (HANDLE(WINAPI *)(basis::un_int,basis::un_int))
+        GetProcAddress(_ptrs->kernel32_dll, "CreateToolhelp32Snapshot");
+  _ptrs->first_process = (BOOL(WINAPI *)(HANDLE,LPPROCESSENTRY32))
+        GetProcAddress(_ptrs->kernel32_dll, "Process32First");
+  _ptrs->next_process = (BOOL(WINAPI *)(HANDLE,LPPROCESSENTRY32))
+        GetProcAddress(_ptrs->kernel32_dll, "Process32Next");
+  if (!_ptrs->next_process || !_ptrs->first_process
+      || !_ptrs->create_snapshot) return false;
+  return true;
+}
+
+#endif
+
+bool process_control::zap_process(basis::un_int to_zap)
+{
+#ifdef DEBUG_PROCESS_CONTROL
+  FUNCDEF("zap_process");
+#endif
+  if (!_healthy) return false;
+#ifdef __UNIX__
+  int ret = kill(to_zap, 9);
+    // send the serious take-down signal to the process.
+  return !ret;
+#endif
+#ifdef __WIN32__
+  HANDLE h = OpenProcess(PROCESS_TERMINATE, false, to_zap);
+  if (!h) {
+#ifdef DEBUG_PROCESS_CONTROL
+    int err = critical_events::system_error();
+    LOG(a_sprintf("error zapping process %d=", to_zap)
+        + critical_events::system_error_text(err));
+#endif
+    return false;
+  }
+  int exit_code = 0;
+  BOOL ret = TerminateProcess(h, exit_code);
+  CloseHandle(h);
+  return !!ret;
+#endif
+}
+
+process_entry process_control::query_process(basis::un_int to_query)
+{
+//  FUNCDEF("query_process");
+  process_entry to_return;
+
+  process_entry_array to_fill;
+  bool got_em = query_processes(to_fill);
+  if (!got_em) return to_return;
+
+  for (int i = 0; i < to_fill.length(); i++) {
+    if (to_fill[i]._process_id == to_query)
+      return to_fill[i];
+  }
+
+//hmmm: implement more specifically.
+#ifdef __UNIX__
+//put in the single process grabber deal.
+#endif
+#ifdef __WIN32__
+//grab the entry from the list.
+#endif
+
+  return to_return;
+}
+
+bool process_control::find_process_in_list(const process_entry_array &processes,
+    const astring &app_name_in, int_set &pids)
+{
+#ifdef DEBUG_PROCESS_CONTROL
+  FUNCDEF("find_process_in_list");
+#endif
+  pids.clear();
+  astring app_name = app_name_in.lower();
+
+  version os_ver = application_configuration::get_OS_version();
+
+  bool compare_prefix = (os_ver.v_major() == 5) && (os_ver.v_minor() == 0);
+    // we only compare the first 15 letters due to a recently noticed bizarre
+    // bug where w2k only shows (and reports) the first 15 letters of file
+    // names through toolhelp.
+
+  bool found = false;  // was it seen in process list?
+  for (int i = 0; i < processes.length(); i++) {
+    filename path = processes[i].path();
+    astring base = path.basename().raw().lower();
+    // a kludge for w2k is needed--otherwise we will miss seeing names that
+    // really are running due to the toolhelp api being busted and only
+    // reporting the first 15 characters of the name.
+    if ( (compare_prefix && (base.compare(app_name, 0, 0, 15, false)))
+        || (base == app_name) ) {
+      found = true;
+      pids.add(processes[i]._process_id);
+    }
+  }
+#ifdef DEBUG_PROCESS_CONTROL
+  if (!found)
+    LOG(astring("failed to find the program called ") + app_name);
+#endif
+  return found;
+}
+
+//////////////
+
+#ifdef __WIN32__
+// this section is the PSAPI version of the query.
+
+// called back on each 16 bit task.
+BOOL WINAPI process_16bit(basis::un_int dwThreadId, WORD module_handle16, WORD hTask16,
+    PSZ pszModName, PSZ pszFileName, LPARAM lpUserDefined)
+{
+  process_info_clump *to_stuff = (process_info_clump *)lpUserDefined;
+  process_entry to_add;
+  to_add._process_id = to_stuff->_process_id;
+  to_add._module16 = hTask16;
+  to_add.path(pszFileName);
+//threads, etc?
+  to_stuff->_to_fill += to_add;
+  return true;
+}
+
+bool process_control::get_processes_with_psapi(process_entry_array &to_fill)
+{
+  // prepare the result object.
+  to_fill.reset();
+
+  // loop over the process enumeration function until we are sure that we
+  // have allocated a large enough space for all existing processes.
+  bool got_all = false;
+  basis::un_int *pid_list = NIL;
+  basis::un_int max_size = 428 * sizeof(basis::un_int);
+  basis::un_int actual_size = 0;
+  while (!got_all) {
+    pid_list = (basis::un_int *)HeapAlloc(GetProcessHeap(), 0, max_size);
+    if (!pid_list) return false;
+    if (!_ptrs->enumerate_processes(pid_list, max_size, &actual_size)) {
+      HeapFree(GetProcessHeap(), 0, pid_list);
+      return false;
+    }
+    if (actual_size == max_size) {
+      // there were too many to store, so whack the partial list.
+      HeapFree(GetProcessHeap(), 0, pid_list);
+      max_size *= 2;  // try with twice as much space.
+    } else got_all = true;
+  }
+
+  // calculate the number of process ids that got stored.
+  basis::un_int ids = actual_size / sizeof(basis::un_int);
+
+  // examine each process id that we found.
+  for (basis::un_int i = 0; i < ids; i++) {
+    // get process information if security permits.
+//turn chunk below into "scan process" or something.
+    HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
+        false, pid_list[i]);
+    flexichar process_name[MAX_ABS_PATH + 1] = { '\0' };
+    if (hProcess) {
+      // go over the modules for the process.  the first will be the main
+      // application module and the others will be threads.  ???
+      basis::un_int max_size = 1 * sizeof(HMODULE);
+//hmmm: could do a rescan loop here if too many.
+      basis::un_int actual_size = 0;
+      HMODULE *module_handles = new HMODULE[max_size + 1];
+      if (!module_handles) {
+        CloseHandle(hProcess);
+        HeapFree(GetProcessHeap(), 0, pid_list);
+        return false;
+      }
+      if (_ptrs->enumerate_modules(hProcess, module_handles, max_size,
+          &actual_size)) {
+        // we want the name of the first module.
+        if (!_ptrs->get_module_name(hProcess, *module_handles, process_name,
+            sizeof(process_name)))
+          process_name[0] = 0;
+      }
+      WHACK(module_handles);
+      CloseHandle(hProcess);
+    }
+
+    // we add whatever information we were able to find about this process.
+    process_entry new_entry;
+    new_entry._process_id = pid_list[i];
+    astring converted_name = from_unicode_temp(process_name);
+    new_entry.path(converted_name);
+
+//how to get?  performance data helper?
+///    new_entry._threads = threads;
+    to_fill += new_entry;
+
+    // if we're looking at ntvdm, then there might be 16 bit processes
+    // attached to it.
+    if (new_entry.path().length() >= NTVDM_NAME.length()) {
+      astring temp = new_entry.path().substring
+          (new_entry.path().end() - NTVDM_NAME.length() + 1,
+          new_entry.path().end());
+      temp.to_lower();
+#ifdef _MSCVER
+//hmmm: pull this back in for mingw when it seems to be supported, if ever.
+      if (temp == NTVDM_NAME) {
+        // set up a callback stampede on the 16 bit processes.
+        process_info_clump info(pid_list[i], to_fill);
+        _ptrs->tasker_16bit(pid_list[i], (TASKENUMPROCEX)process_16bit,
+             (LPARAM)&info);
+      }
+#endif
+    }
+  }
+
+  if (pid_list) HeapFree(GetProcessHeap(), 0, pid_list);
+  return true;
+}
+
+//////////////
+
+// this is the toolhelp version of the query.
+
+bool process_control::get_processes_with_toolhelp(process_entry_array &to_fill)
+{
+  // prepare the result object.
+  to_fill.reset();
+
+  // get an atomic view of the process list, which rapidly becomes out of date.
+  HANDLE hSnapShot;
+  hSnapShot = _ptrs->create_snapshot(TH32CS_SNAPPROCESS, 0);
+  if (hSnapShot == INVALID_HANDLE_VALUE) return false;
+
+  // start iterating through the snapshot by getting the first process.
+  PROCESSENTRY32 entry;
+  entry.dwSize = sizeof(PROCESSENTRY32);
+  BOOL keep_going = _ptrs->first_process(hSnapShot, &entry);
+
+  // while we see valid processes, iterate through them.
+  while (keep_going) {
+    // add an entry for the current process.
+    process_entry new_entry;
+    new_entry._process_id = entry.th32ProcessID;
+    new_entry._references = entry.cntUsage;
+    new_entry._threads = entry.cntThreads;
+    new_entry._parent_process_id = entry.th32ParentProcessID;
+    astring exe_file = from_unicode_temp(entry.szExeFile);
+    new_entry.path(exe_file);
+    to_fill += new_entry;
+    entry.dwSize = sizeof(PROCESSENTRY32);  // reset struct size.
+    keep_going = _ptrs->next_process(hSnapShot, &entry);
+  }
+
+  CloseHandle(hSnapShot);
+  return true;
+}
+#endif  // __WIN32__
+
+#ifdef __UNIX__
+
+#define CLOSE_TMP_FILE { \
+/*  continuable_error("process_control", "get_processes_with_ps", error); */ \
+  if (output) { \
+    fclose(output); \
+    unlink(tmpfile.s()); \
+  } \
+}
+
+bool process_control::get_processes_with_ps(process_entry_array &to_fill)
+{
+  FUNCDEF("get_processes_with_ps");
+  to_fill.reset();
+  // we ask the operating system to give us a list of processes.
+  a_sprintf tmpfile("/tmp/proc_list_%d_%d.txt", application_configuration::process_id(),
+      _rando->inclusive(1, 400000));
+  a_sprintf cmd("ps wax --format \"%%p %%a\" >%s", tmpfile.s());
+//hmmm: add more info as we expand the process entry.
+  FILE *output = NIL;  // initialize now to establish variable for our macro.
+  int sysret = system(cmd.s());
+  if (negative(sysret)) {
+LOG("got negative return from system()!");
+    CLOSE_TMP_FILE;
+    return false;
+  }
+  output = fopen(tmpfile.s(), "r");
+  if (!output) {
+LOG("failed to open process list file!");
+    CLOSE_TMP_FILE;
+    return false;
+  }
+  const int max_buff = 10000;
+  char buff[max_buff];
+  size_t size_read = 1;
+  astring accumulator;
+  while (size_read > 0) {
+    // read bytes from the file.
+    size_read = fread(buff, 1, max_buff, output);
+    // if there was anything, store it in the string.
+    if (size_read > 0)
+      accumulator += astring(astring::UNTERMINATED, buff, size_read);
+  }
+  CLOSE_TMP_FILE;
+  // parse the string up now.
+  bool first_line = true;
+  while (accumulator.length()) {
+    // eat any spaces in front.
+    if (first_line) {
+      // we toss the first line since it's a header with columns.
+      int cr_indy = accumulator.find('\n');
+      accumulator.zap(0, cr_indy);
+      if (accumulator[accumulator.end()] == '\r')
+        accumulator.zap(accumulator.end(), accumulator.end());
+      first_line = false;
+      continue;
+    }
+    while (accumulator.length() && (accumulator[0] == ' '))
+      accumulator.zap(0, 0);
+    // look for the first part of the line; the process id.
+    int num_indy = accumulator.find(' ');
+    if (negative(num_indy)) break;
+    basis::un_int p_id = accumulator.substring(0, num_indy).convert(0);
+    accumulator.zap(0, num_indy);
+    int cr_indy = accumulator.find('\n');
+    if (negative(cr_indy))
+      cr_indy = accumulator.end() + 1;
+    astring proc_name = accumulator.substring(0, cr_indy - 1);
+    if (proc_name[proc_name.end()] == '\r')
+      proc_name.zap(proc_name.end(), proc_name.end());
+    accumulator.zap(0, cr_indy);
+    int space_indy = proc_name.find(' ');
+//hmmm: this is incorrect regarding names that do have spaces in them.
+    if (negative(space_indy))
+      space_indy = proc_name.end() + 1;
+    process_entry to_add;
+    to_add._process_id = p_id;
+    astring path = proc_name.substring(0, space_indy - 1);
+    // patch the pathname if we see any bracketed items.
+    int brackets_in = 0;
+    for (int i = 0; i < path.length(); i++) {
+      if (path[i] == '[') brackets_in++;
+      else if (path[i] == ']') brackets_in--;
+      if (brackets_in) {
+        // if we see a slash inside brackets, then we patch it so it doesn't
+        // confuse the filename object's directory handling.
+        if ( (path[i] == '/') || (path[i] == '\\') )
+          path[i] = '#';
+      }
+    }
+    to_add.path(path);
+    to_fill += to_add;
+  }
+  return true;
+}
+#endif  // __UNIX__
+
+} //namespace.
+