/* strace.cc

   Written by Chris Faylor <cgf@redhat.com>

This file is part of Cygwin.

This software is a copyrighted work licensed under the terms of the
Cygwin license.  Please consult the file "CYGWIN_LICENSE" for
details. */

#include <windows.h>
#include <winternl.h>
#define cygwin_internal cygwin_internal_dontuse
#include <stdio.h>
#include <fcntl.h>
#include <io.h>
#include <getopt.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <errno.h>
#include "sys/strace.h"
#include "sys/cygwin.h"
#include "cygwin/version.h"
#include "cygwin/config.h"
#include "gcc_seh.h"
#include "path.h"
#undef cygwin_internal
#include "loadlib.h"

/* we *know* we're being built with GCC */
#ifndef alloca
#define alloca __builtin_alloca
#endif

static const char *pgm;
static int forkdebug = 1;
static int numerror = 1;
static int show_usecs = 1;
static int delta = 1;
static int events = 1;
static int hhmmss;
static int bufsize;
static int new_window;
static long flush_period;
static int include_hex;
static int quiet = -1;

static unsigned char strace_active = 1;
static int processes;

static BOOL close_handle (HANDLE h, DWORD ok);

#define CloseHandle(h) close_handle(h, 0)

static void *drive_map;

struct child_list
{
  DWORD id;
  HANDLE hproc;
  int saw_stars;
  char nfields;
  long long start_time;
  DWORD last_usecs;
  struct child_list *next;
    child_list ():id (0), hproc (NULL), saw_stars (0), nfields (0),
    start_time (0), last_usecs (0), next (NULL)
  {
  }
};

child_list children;

static void
warn (int geterrno, const char *fmt, ...)
{
  va_list args;
  char buf[4096];

  va_start (args, fmt);
  sprintf (buf, "%s: ", pgm);
  vsprintf (strchr (buf, '\0'), fmt, args);
  if (geterrno)
    perror (buf);
  else
    {
      fputs (buf, stderr);
      fputs ("\n", stderr);
    }
  va_end (args);
}

static void __attribute__ ((noreturn))
error (int geterrno, const char *fmt, ...)
{
  va_list args;
  char buf[4096];

  va_start (args, fmt);
  sprintf (buf, "%s: ", pgm);
  vsprintf (strchr (buf, '\0'), fmt, args);
  if (geterrno)
    perror (buf);
  else
    {
      fputs (buf, stderr);
      fputs ("\n", stderr);
    }
  exit (1);
}

DWORD lastid = 0;
HANDLE lasth;

static child_list *
get_child (DWORD id)
{
  child_list *c;
  for (c = &children; (c = c->next) != NULL;)
    if (c->id == id)
      return c;

  return NULL;
}

static void
add_child (DWORD id, HANDLE hproc)
{
  if (!get_child (id))
    {
      child_list *c = children.next;
      children.next = (child_list *) calloc (1, sizeof (child_list));
      children.next->next = c;
      lastid = children.next->id = id;
      lasth = children.next->hproc = hproc;
      processes++;
      if (!quiet)
	fprintf (stderr, "Windows process %lu attached\n", id);
    }
}

static void
remove_child (DWORD id)
{
  child_list *c;
  if (id == lastid)
    lastid = 0;
  for (c = &children; c->next != NULL; c = c->next)
    if (c->next->id == id)
      {
	child_list *c1 = c->next;
	c->next = c1->next;
	free (c1);
	if (!quiet)
	  fprintf (stderr, "Windows process %lu detached\n", id);
	processes--;
	return;
      }

  error (0, "no process id %d found", id);
}

#define LINE_BUF_CHUNK 128

class linebuf
{
  size_t alloc;
public:
    size_t ix;
  char *buf;
  linebuf ()
  {
    ix = 0;
    alloc = 0;
    buf = NULL;
  }
 ~linebuf ()
  {
    if (buf)
      free (buf);
  }
  void add (const char *what, int len);
  void add (const char *what)
  {
    add (what, strlen (what));
  }
  void prepend (const char *what, int len);
};

void
linebuf::add (const char *what, int len)
{
  size_t newix;
  if ((newix = ix + len) >= alloc)
    {
      alloc += LINE_BUF_CHUNK + len;
      buf = (char *) realloc (buf, alloc + 1);
    }
  memcpy (buf + ix, what, len);
  ix = newix;
  buf[ix] = '\0';
}

void
linebuf::prepend (const char *what, int len)
{
  int buflen;
  size_t newix;
  if ((newix = ix + len) >= alloc)
    {
      alloc += LINE_BUF_CHUNK + len;
      buf = (char *) realloc (buf, alloc + 1);
      buf[ix] = '\0';
    }
  if ((buflen = strlen (buf)))
    memmove (buf + len, buf, buflen + 1);
  else
    buf[newix] = '\0';
  memcpy (buf, what, len);
  ix = newix;
}

static void
make_command_line (linebuf & one_line, char **argv)
{
  for (; *argv; argv++)
    {
      char *p = NULL;
      const char *a = *argv;

      int len = strlen (a);
      if (len != 0 && !(p = strpbrk (a, " \t\n\r\"")))
	one_line.add (a, len);
      else
	{
	  one_line.add ("\"", 1);
	  for (; p; a = p, p = strchr (p, '"'))
	    {
	      one_line.add (a, ++p - a);
	      if (p[-1] == '"')
		one_line.add ("\"", 1);
	    }
	  if (*a)
	    one_line.add (a);
	  one_line.add ("\"", 1);
	}
      one_line.add (" ", 1);
    }

  if (one_line.ix)
    one_line.buf[one_line.ix - 1] = '\0';
  else
    one_line.add ("", 1);
}

static DWORD child_pid;

static BOOL WINAPI
ctrl_c (DWORD)
{
  static int tic = 1;
  if ((tic ^= 1) && !GenerateConsoleCtrlEvent (CTRL_C_EVENT, 0))
    error (0, "couldn't send CTRL-C to child, win32 error %d\n",
	   GetLastError ());
  return TRUE;
}

extern "C" {
uintptr_t (*cygwin_internal) (int, ...);
WCHAR cygwin_dll_path[32768];
};

static int
load_cygwin ()
{
  static HMODULE h;

  if (cygwin_internal)
    return 1;

  if (h)
    return 0;

  if (!(h = LoadLibrary ("cygwin1.dll")))
    {
      errno = ENOENT;
      return 0;
    }
  GetModuleFileNameW (h, cygwin_dll_path, 32768);
  if (!(cygwin_internal = (uintptr_t (*) (int, ...)) GetProcAddress (h, "cygwin_internal")))
    {
      errno = ENOSYS;
      return 0;
    }
  return 1;
}

#define DEBUG_PROCESS_DETACH_ON_EXIT    0x00000001
#define DEBUG_PROCESS_ONLY_THIS_PROCESS 0x00000002

static void
attach_process (pid_t pid)
{
  child_pid = (DWORD) cygwin_internal (CW_CYGWIN_PID_TO_WINPID, pid);
  if (!child_pid)
    child_pid = pid;

  if (!DebugActiveProcess (child_pid))
    error (0, "couldn't attach to pid %d for debugging", child_pid);

  if (forkdebug)
    {
      HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, child_pid);

      if (h)
	{
	  /* Try to turn off DEBUG_ONLY_THIS_PROCESS so we can follow forks */
	  ULONG DebugFlags = DEBUG_PROCESS_DETACH_ON_EXIT;
	  NTSTATUS status = NtSetInformationProcess (h, ProcessDebugFlags, &DebugFlags, sizeof (DebugFlags));
	  if (!NT_SUCCESS (status))
	    warn (0, "Could not clear DEBUG_ONLY_THIS_PROCESS (%x), will not trace child processes", status);

	  CloseHandle(h);
	}
    }

  return;
}


static void
create_child (char **argv)
{
  linebuf one_line;

  STARTUPINFO si;
  PROCESS_INFORMATION pi;
  BOOL ret;
  DWORD flags;

  if (strchr (*argv, '/'))
      *argv = cygpath (*argv, NULL);
  memset (&si, 0, sizeof (si));
  si.cb = sizeof (si);

  flags = CREATE_DEFAULT_ERROR_MODE
	  | (forkdebug ? DEBUG_PROCESS : DEBUG_ONLY_THIS_PROCESS);
  if (new_window)
    flags |= CREATE_NEW_CONSOLE | CREATE_NEW_PROCESS_GROUP;

  make_command_line (one_line, argv);

  SetConsoleCtrlHandler (NULL, 0);

  const char *cygwin_env = getenv ("CYGWIN");
  const char *space;

  if (cygwin_env && strlen (cygwin_env) <= 256) /* sanity check */
    space = " ";
  else
    space = cygwin_env = "";
  char *newenv = (char *) malloc (sizeof ("CYGWIN=noglob")
				  + strlen (space) + strlen (cygwin_env));
  sprintf (newenv, "CYGWIN=noglob%s%s", space, cygwin_env);
  _putenv (newenv);
  ret = CreateProcess (0, one_line.buf,	/* command line */
		       NULL,	/* Security */
		       NULL,	/* thread */
		       TRUE,	/* inherit handles */
		       flags,	/* start flags */
		       NULL,	/* default environment */
		       NULL,	/* current directory */
		       &si, &pi);
  if (!ret)
    error (0, "error creating process %s, (error %d)", *argv,
	   GetLastError ());

  CloseHandle (pi.hThread);
  CloseHandle (pi.hProcess);
  child_pid = pi.dwProcessId;
  SetConsoleCtrlHandler (ctrl_c, 1);
}

static int
output_winerror (FILE *ofile, char *s)
{
  char *winerr = strstr (s, "Win32 error ");
  if (!winerr)
    return 0;

  DWORD errnum = atoi (winerr + sizeof ("Win32 error ") - 1);
  if (!errnum)
    return 0;

  /*
   * NOTE: Currently there is no policy for how long the
   * the buffers are, and looks like 256 is a smallest one
   * (dlfcn.cc). Other than error 1395 (length 213) and
   * error 1015 (length 249), the rest are all under 188
   * characters, and so I'll use 189 as the buffer length.
   * For those longer error messages, FormatMessage will
   * return FALSE, and we'll get the old behaviour such as
   * ``Win32 error 1395'' etc.
   */
  char buf[4096];
  if (!FormatMessage (FORMAT_MESSAGE_FROM_SYSTEM
		      | FORMAT_MESSAGE_IGNORE_INSERTS,
		      NULL,
		      errnum,
		      MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT),
		      (LPTSTR) buf, sizeof (buf), NULL))
    return 0;

  /* Get rid the trailing CR/NL pair. */
  char *p = strchr (buf, '\0');
  p[-2] = '\n';
  p[-1] = '\0';

  *winerr = '\0';
  fputs (s, ofile);
  fputs (buf, ofile);
  return 1;
}

static SYSTEMTIME *
syst (long long t)
{
  FILETIME n;
  static SYSTEMTIME st;
  long long now = t /*+ ((long long) usecs * 10)*/;
  n.dwHighDateTime = now >> 32;
  n.dwLowDateTime = now & 0xffffffff;
  FileTimeToSystemTime (&n, &st);
  return &st;
}

static void
handle_output_debug_string (DWORD id, LPVOID p, unsigned mask, FILE *ofile)
{
  int len;
  int special;
  char alen[3 + 8 + 1];
  SIZE_T nbytes;
  child_list *child = get_child (id);
  if (!child)
    error (0, "no process id %d found", id);
  HANDLE hchild = child->hproc;
#define INTROLEN (sizeof (alen) - 1)

  if (id == lastid && hchild != lasth)
    warn (0, "%p != %p", hchild, lasth);

  alen[INTROLEN] = '\0';
  if (!ReadProcessMemory (hchild, p, alen, INTROLEN, &nbytes))
#ifndef DEBUGGING
    return;
#else
    error (0,
	   "couldn't get message length from subprocess %d<%p>, windows error %d",
	   id, hchild, GetLastError ());
#endif

  if (strncmp (alen, "cYg", 3))
    return;
  len = (int) strtoul (alen + 3, NULL, 16);
  if (!len)
    return;

  if (len > 0)
    special = 0;
  else
    {
      special = len;
      if (special == _STRACE_INTERFACE_ACTIVATE_ADDR || special == _STRACE_CHILD_PID)
	len = 17;
    }

  /* Note that the following code deliberately points buf 20 bytes into the
     allocated area.  The subsequent code then overwrites the usecs value
     given in the application's debug string, which potentially prepends
     characters to the string.  If that sounds confusing and dangerous, well...

     TODO: This needs a cleanup. */
  char *buf;
  buf = (char *) alloca (len + 85) + 20;

  if (!ReadProcessMemory (hchild, ((char *) p) + INTROLEN, buf, len, &nbytes))
    error (0, "couldn't get message from subprocess, windows error %d",
	   GetLastError ());

  buf[len] = '\0';
  char *s = strtok (buf, " ");

  unsigned long long n = strtoull (s, NULL, 16);

  s = strchr (s, '\0') + 1;

  if (special == _STRACE_CHILD_PID)
    {
      DebugActiveProcess (n);
      return;
    }

  if (special == _STRACE_INTERFACE_ACTIVATE_ADDR)
    {
      s = strtok (NULL, " ");
      if (*s && *s == '1' && !forkdebug)
	/* don't activate since we are not following forks */;
      else if (!WriteProcessMemory (hchild, (LPVOID) n, &strace_active,
			       sizeof (strace_active), &nbytes))
	error (0, "couldn't write strace flag to subprocess at %p, "
	       "windows error %d", n, GetLastError ());
      return;
    }

  char *origs = s;

  if (mask & n)
    /* got it */ ;
  else if (!(mask & _STRACE_ALL) || (n & _STRACE_NOTALL))
    return;			/* This should not be included in "all" output */

  DWORD dusecs, usecs;
  char *ptusec, *ptrest;

  dusecs = strtoul (s, &ptusec, 10);
  char *q = ptusec;
  while (*q == ' ')
    q++;
  if (*q != '[')
    {
      usecs = strtoul (q, &ptrest, 10);
      while (*ptrest == ' ')
	ptrest++;
    }
  else
    {
      ptrest = q;
      ptusec = show_usecs ? s : ptrest;
      usecs = dusecs;
    }

#if 1
  child->saw_stars = 2;
#else
  if (child->saw_stars == 0)
    {
      FILETIME st;
      char *news;

      GetSystemTimeAsFileTime (&st);
      FileTimeToLocalFileTime (&st, &st);
      child->start_time = st.dwHighDateTime;
      child->start_time <<= 32;
      child->start_time |= st.dwLowDateTime;
      if (*(news = ptrest) != '[')
	child->saw_stars = 2;
      else
	{
	  child->saw_stars++;
	  while ((news = strchr (news, ' ')) != NULL && *++news != '*')
	    child->nfields++;
	  if (news == NULL)
	    child->saw_stars++;
	  else
	    {
	      s = news;
	      child->nfields++;
	    }
	}
    }
  else if (child->saw_stars < 2)
    {
      int i;
      char *news;
      if (*(news = ptrest) != '[')
	child->saw_stars = 2;
      else
	{
	  for (i = 0; i < child->nfields; i++)
	    if ((news = strchr (news, ' ')) == NULL)
	      break;		// Should never happen
	    else
	      news++;

	  if (news == NULL)
	    child->saw_stars = 2;
	  else
	    {
	      s = news;
	      if (*s == '*')
		{
		  SYSTEMTIME *st = syst (child->start_time);
		  fprintf (ofile,
			   "Date/Time:    %d-%02d-%02d %02d:%02d:%02d\n",
			   st->wYear, st->wMonth, st->wDay, st->wHour,
			   st->wMinute, st->wSecond);
		  child->saw_stars++;
		}
	    }
	}
    }
#endif

  long long d = usecs - child->last_usecs;
  char intbuf[40];

  if (child->saw_stars < 2 || s != origs)
    /* Nothing */ ;
  else if (hhmmss)
    {
      s = ptrest - 9;
      SYSTEMTIME *st = syst (child->start_time + (long long) usecs * 10);
      sprintf (s, "%02d:%02d:%02d", st->wHour, st->wMinute, st->wSecond);
      *strchr (s, '\0') = ' ';
    }
  else if (!delta)
    s = ptusec;
  else
    {
      s = ptusec;
      sprintf (intbuf, "%5d ", (int) d);
      int len = strlen (intbuf);

      memcpy ((s -= len), intbuf, len);
    }

  if (include_hex)
    {
      s -= 8;
      sprintf (s, "%012llx", n);
      strchr (s, '\0')[0] = ' ';
    }
  child->last_usecs = usecs;
  if (numerror || !output_winerror (ofile, s))
    fputs (s, ofile);
  if (!bufsize)
    fflush (ofile);
}

static BOOL
GetFileNameFromHandle(HANDLE hFile, WCHAR pszFilename[MAX_PATH+1])
{
  BOOL result = FALSE;
  ULONG len = 0;
  OBJECT_NAME_INFORMATION *ntfn = (OBJECT_NAME_INFORMATION *) alloca (65536);
  NTSTATUS status = NtQueryObject (hFile, ObjectNameInformation,
				   ntfn, 65536, &len);
  if (NT_SUCCESS (status))
    {
      PWCHAR win32path = ntfn->Name.Buffer;
      win32path[ntfn->Name.Length / sizeof (WCHAR)] = L'\0';

      /* NtQueryObject returns a native NT path.  (Try to) convert to Win32. */
      if (drive_map)
	win32path = (PWCHAR) cygwin_internal (CW_MAP_DRIVE_MAP, drive_map,
					      win32path);
      pszFilename[0] = L'\0';
      wcsncat (pszFilename, win32path, MAX_PATH);
      result = TRUE;
    }
  return result;
}

static char *
cygwin_pid (DWORD winpid)
{
  static char buf[48];
  static DWORD max_cygpid = 0;
  DWORD cygpid;

  if (!max_cygpid)
    max_cygpid = (DWORD) cygwin_internal (CW_MAX_CYGWIN_PID);

  cygpid = (DWORD) cygwin_internal (CW_WINPID_TO_CYGWIN_PID, winpid);

  if (cygpid >= max_cygpid)
    snprintf (buf, sizeof buf, "%lu", winpid);
  else
    snprintf (buf, sizeof buf, "%lu (pid: %lu)", winpid, cygpid);
  return buf;
}

static DWORD
proc_child (unsigned mask, FILE *ofile, pid_t pid)
{
  DWORD res = 0;
  DEBUG_EVENT ev;
  time_t cur_time, last_time;

  SetThreadPriority (GetCurrentThread (), THREAD_PRIORITY_HIGHEST);
  last_time = time (NULL);
  while (1)
    {
      BOOL debug_event = WaitForDebugEvent (&ev, 1000);
      DWORD status = DBG_CONTINUE;

      if (bufsize && flush_period > 0 &&
	  (cur_time = time (NULL)) >= last_time + flush_period)
	{
	  last_time = cur_time;
	  fflush (ofile);
	}

      if (!debug_event)
	continue;

      if (pid)
	{
	  (void) cygwin_internal (CW_STRACE_TOGGLE, pid);
	  pid = 0;
	}

      switch (ev.dwDebugEventCode)
	{
	case CREATE_PROCESS_DEBUG_EVENT:
	  if (events)
	    fprintf (ofile, "--- Process %s created\n",
		     cygwin_pid (ev.dwProcessId));
	  if (ev.u.CreateProcessInfo.hFile)
	    CloseHandle (ev.u.CreateProcessInfo.hFile);
	  add_child (ev.dwProcessId, ev.u.CreateProcessInfo.hProcess);
	  break;

	case CREATE_THREAD_DEBUG_EVENT:
	  if (events)
	    fprintf (ofile, "--- Process %s thread %lu created\n",
		     cygwin_pid (ev.dwProcessId), ev.dwThreadId);
	  break;

	case LOAD_DLL_DEBUG_EVENT:
	  if (events)
	    {
	      // lpImageName is not always populated, so find the filename for
	      // hFile instead
	      WCHAR dllname[MAX_PATH+1];
	      if (!GetFileNameFromHandle(ev.u.LoadDll.hFile, dllname))
		wcscpy(dllname, L"(unknown)");

	      fprintf (ofile, "--- Process %s loaded %ls at %p\n",
		       cygwin_pid (ev.dwProcessId), dllname,
		       ev.u.LoadDll.lpBaseOfDll);
	    }

	  if (ev.u.LoadDll.hFile)
	    CloseHandle (ev.u.LoadDll.hFile);
	  break;

	case UNLOAD_DLL_DEBUG_EVENT:
	  if (events)
	    fprintf (ofile, "--- Process %s unloaded DLL at %p\n",
		     cygwin_pid (ev.dwProcessId), ev.u.UnloadDll.lpBaseOfDll);
	  break;

	case OUTPUT_DEBUG_STRING_EVENT:
	  handle_output_debug_string (ev.dwProcessId,
				      ev.u.DebugString.lpDebugStringData,
				      mask, ofile);
	  break;

	case EXIT_PROCESS_DEBUG_EVENT:
	  if (events)
	    fprintf (ofile, "--- Process %s exited with status 0x%lx\n",
		     cygwin_pid (ev.dwProcessId), ev.u.ExitProcess.dwExitCode);
	  res = ev.u.ExitProcess.dwExitCode;
	  remove_child (ev.dwProcessId);
	  break;

	case EXIT_THREAD_DEBUG_EVENT:
	  if (events)
	    fprintf (ofile, "--- Process %s thread %lu exited with "
			    "status 0x%lx\n",
		     cygwin_pid (ev.dwProcessId), ev.dwThreadId,
		     ev.u.ExitThread.dwExitCode);
	  break;

	case EXCEPTION_DEBUG_EVENT:
	  switch (ev.u.Exception.ExceptionRecord.ExceptionCode)
	    {
	    case STATUS_BREAKPOINT:
	    case 0x406d1388:		/* SetThreadName exception. */
	      break;
	    case STATUS_GCC_THROW:
	    case STATUS_GCC_UNWIND:
	    case STATUS_GCC_FORCED:
	      status = DBG_EXCEPTION_NOT_HANDLED;
	      break;
	    default:
	      status = DBG_EXCEPTION_NOT_HANDLED;
	      if (ev.u.Exception.dwFirstChance)
		fprintf (ofile, "--- Process %s, exception %08lx at %p\n",
			 cygwin_pid (ev.dwProcessId),
			 ev.u.Exception.ExceptionRecord.ExceptionCode,
			 ev.u.Exception.ExceptionRecord.ExceptionAddress);
	      break;
	    }
	  break;
	}
      if (!ContinueDebugEvent (ev.dwProcessId, ev.dwThreadId, status))
	error (0, "couldn't continue debug event, windows error %d",
	       GetLastError ());
      if (!processes)
	break;
    }

  return res;
}

static void
dotoggle (pid_t pid)
{
  child_pid = (DWORD) cygwin_internal (CW_CYGWIN_PID_TO_WINPID, pid);
  if (!child_pid)
    {
      warn (0, "no such cygwin pid - %d", pid);
      child_pid = pid;
    }
  if (cygwin_internal (CW_STRACE_TOGGLE, child_pid))
    error (0, "failed to toggle tracing for process %d<%d>", pid, child_pid);

  return;
}

static DWORD
dostrace (unsigned mask, FILE *ofile, pid_t pid, char **argv)
{
  if (!pid)
    create_child (argv);
  else
    attach_process (pid);

  return proc_child (mask, ofile, pid);
}

typedef struct tag_mask_mnemonic
{
  unsigned long val;
  const char *text;
}
mask_mnemonic;

static const mask_mnemonic mnemonic_table[] = {
  {_STRACE_ALL, "all"},
  {_STRACE_FLUSH, "flush"},
  {_STRACE_INHERIT, "inherit"},
  {_STRACE_UHOH, "uhoh"},
  {_STRACE_SYSCALL, "syscall"},
  {_STRACE_STARTUP, "startup"},
  {_STRACE_DEBUG, "debug"},
  {_STRACE_PARANOID, "paranoid"},
  {_STRACE_TERMIOS, "termios"},
  {_STRACE_SELECT, "select"},
  {_STRACE_WM, "wm"},
  {_STRACE_SIGP, "sigp"},
  {_STRACE_MINIMAL, "minimal"},
  {_STRACE_EXITDUMP, "exitdump"},
  {_STRACE_SYSTEM, "system"},
  {_STRACE_NOMUTEX, "nomutex"},
  {_STRACE_MALLOC, "malloc"},
  {_STRACE_THREAD, "thread"},
  {_STRACE_PTHREAD, "pthread"},
  {_STRACE_SPECIAL, "special"},
  {0, NULL}
};

static unsigned long
mnemonic2ul (const char *nptr, char **endptr)
{
  // Look up mnemonic in table, return value.
  // *endptr = ptr to char that breaks match.
  const mask_mnemonic *mnp = mnemonic_table;

  while (mnp->text != NULL)
    {
      if (strcmp (mnp->text, nptr) == 0)
	{
	  // Found a match.
	  if (endptr != NULL)
	    {
	      *endptr = ((char *) nptr) + strlen (mnp->text);
	    }
	  return mnp->val;
	}
      mnp++;
    }

  // Didn't find it.
  if (endptr != NULL)
    {
      *endptr = (char *) nptr;
    }
  return 0;
}

static unsigned long
parse_mask (const char *ms, char **endptr)
{
  const char *p = ms;
  char *newp;
  unsigned long retval = 0, thisval;
  const size_t bufsize = 16;
  char buffer[bufsize];
  size_t len;

  while (*p != '\0')
    {
      // First extract the term, terminate it, and lowercase it.
      strncpy (buffer, p, bufsize);
      buffer[bufsize - 1] = '\0';
      len = strcspn (buffer, "+,\0");
      buffer[len] = '\0';
      strlwr (buffer);

      // Check if this is a mnemonic.  We have to do this first or strtoul()
      // will false-trigger on anything starting with "a" through "f".
      thisval = mnemonic2ul (buffer, &newp);
      if (buffer == newp)
	{
	  // This term isn't mnemonic, check if it's hex.
	  thisval = strtoul (buffer, &newp, 16);
	  if (newp != buffer + len)
	    {
	      // Not hex either, syntax error.
	      *endptr = (char *) p;
	      return 0;
	    }
	}

      p += len;
      retval += thisval;

      // Handle operators
      if (*p == '\0')
	break;
      if ((*p == '+') || (*p == ','))
	{
	  // For now these both equate to addition/ORing.  Until we get
	  // fancy and add things like "all-<something>", all we need do is
	  // continue the looping.
	  p++;
	  continue;
	}
      else
	{
	  // Syntax error
	  *endptr = (char *) p;
	  return 0;
	}
    }

  *endptr = (char *) p;
  return retval;
}

static void __attribute__ ((__noreturn__))
usage (FILE *where = stderr)
{
  fprintf (where, "\
Usage: %s [OPTIONS] <command-line>\n\
Usage: %s [OPTIONS] -p <pid>\n\
\n\
Trace system calls and signals\n\
\n\
  -b, --buffer-size=SIZE       set size of output file buffer\n\
  -d, --no-delta               don't display the delta-t microsecond timestamp\n\
  -e, --events                 log all Windows DEBUG_EVENTS (toggle - default true)\n\
  -f, --trace-children         trace child processes (toggle - default true)\n\
  -h, --help                   output usage information and exit\n\
  -m, --mask=MASK              set message filter mask\n\
  -n, --crack-error-numbers    output descriptive text instead of error\n\
			       numbers for Windows errors\n\
  -o, --output=FILENAME        set output file to FILENAME\n\
  -p, --pid=n                  attach to executing program with cygwin pid n\n\
  -q, --quiet                  suppress messages about attaching, detaching, etc.\n\
  -S, --flush-period=PERIOD    flush buffered strace output every PERIOD secs\n\
  -t, --timestamp              use an absolute hh:mm:ss timestamp insted of \n\
			       the default microsecond timestamp.  Implies -d\n\
  -T, --toggle                 toggle tracing in a process already being\n\
			       traced. Requires -p <pid>\n\
  -u, --usecs                  toggle printing of microseconds timestamp\n\
  -V, --version                output version information and exit\n\
  -w, --new-window             spawn program under test in a new window\n\
\n", pgm, pgm);
  if ( where == stdout)
    fprintf (stdout, "\
    MASK can be any combination of the following mnemonics and/or hex values\n\
    (0x is optional).  Combine masks with '+' or ',' like so:\n\
\n\
		      --mask=wm+system,malloc+0x00800\n\
\n\
    Mnemonic Hex     Corresponding Def  Description\n\
    =========================================================================\n\
    all      0x000001 (_STRACE_ALL)      All strace messages.\n\
    flush    0x000002 (_STRACE_FLUSH)    Flush output buffer after each message.\n\
    inherit  0x000004 (_STRACE_INHERIT)  Children inherit mask from parent.\n\
    uhoh     0x000008 (_STRACE_UHOH)     Unusual or weird phenomenon.\n\
    syscall  0x000010 (_STRACE_SYSCALL)  System calls.\n\
    startup  0x000020 (_STRACE_STARTUP)  argc/envp printout at startup.\n\
    debug    0x000040 (_STRACE_DEBUG)    Info to help debugging. \n\
    paranoid 0x000080 (_STRACE_PARANOID) Paranoid info.\n\
    termios  0x000100 (_STRACE_TERMIOS)  Info for debugging termios stuff.\n\
    select   0x000200 (_STRACE_SELECT)   Info on ugly select internals.\n\
    wm       0x000400 (_STRACE_WM)       Trace Windows msgs (enable _strace_wm).\n\
    sigp     0x000800 (_STRACE_SIGP)     Trace signal and process handling.\n\
    minimal  0x001000 (_STRACE_MINIMAL)  Very minimal strace output.\n\
    pthread  0x002000 (_STRACE_PTHREAD)	 Pthread calls.\n\
    exitdump 0x004000 (_STRACE_EXITDUMP) Dump strace cache on exit.\n\
    system   0x008000 (_STRACE_SYSTEM)   Serious error; goes to console and log.\n\
    nomutex  0x010000 (_STRACE_NOMUTEX)  Don't use mutex for synchronization.\n\
    malloc   0x020000 (_STRACE_MALLOC)   Trace malloc calls.\n\
    thread   0x040000 (_STRACE_THREAD)   Thread-locking calls.\n\
    special  0x100000 (_STRACE_SPECIAL)  Special debugging printfs for\n\
					 non-checked-in code\n\
");
  if (where == stderr)
    fprintf (stderr, "Try `%s --help' for more information.\n", pgm);
  exit (where == stderr ? 1 : 0 );
}

struct option longopts[] = {
  {"buffer-size", required_argument, NULL, 'b'},
  {"events", no_argument, NULL, 'e'},
  {"help", no_argument, NULL, 'h'},
  {"flush-period", required_argument, NULL, 'S'},
  {"hex", no_argument, NULL, 'H'},
  {"mask", required_argument, NULL, 'm'},
  {"new-window", no_argument, NULL, 'w'},
  {"output", required_argument, NULL, 'o'},
  {"no-delta", no_argument, NULL, 'd'},
  {"pid", required_argument, NULL, 'p'},
  {"quiet", no_argument, NULL, 'q'},
  {"timestamp", no_argument, NULL, 't'},
  {"toggle", no_argument, NULL, 'T'},
  {"trace-children", no_argument, NULL, 'f'},
  {"translate-error-numbers", no_argument, NULL, 'n'},
  {"usecs", no_argument, NULL, 'u'},
  {"version", no_argument, NULL, 'V'},
  {NULL, 0, NULL, 0}
};

static const char *const opts = "+b:dehHfm:no:p:qS:tTuVw";

static void
print_version ()
{
  printf ("strace (cygwin) %d.%d.%d\n"
	  "System Trace\n"
	  "Copyright (C) 2000 - %s Cygwin Authors\n"
	  "This is free software; see the source for copying conditions.  There is NO\n"
	  "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
	  CYGWIN_VERSION_DLL_MAJOR / 1000,
	  CYGWIN_VERSION_DLL_MAJOR % 1000,
	  CYGWIN_VERSION_DLL_MINOR,
	  strrchr (__DATE__, ' ') + 1);
}

int
main2 (int argc, char **argv)
{
  unsigned mask = 0;
  FILE *ofile = NULL;
  pid_t pid = 0;
  int opt;
  int toggle = 0;
  int sawquiet = -1;
  DWORD ret = 0;

  if (load_cygwin ())
    {
      char **av = (char **) cygwin_internal (CW_ARGV);
      if (av && (uintptr_t) av != (uintptr_t) -1)
	for (argc = 0, argv = av; *av; av++)
	  argc++;
    }

  _setmode (1, _O_BINARY);
  _setmode (2, _O_BINARY);

  if (!(pgm = strrchr (*argv, '\\')) && !(pgm = strrchr (*argv, '/')))
    pgm = *argv;
  else
    pgm++;

  while ((opt = getopt_long (argc, argv, opts, longopts, NULL)) != EOF)
    switch (opt)
      {
      case 'b':
	bufsize = atoi (optarg);
	break;
      case 'd':
	delta ^= 1;
	break;
      case 'e':
	events ^= 1;
	break;
      case 'f':
	forkdebug ^= 1;
	break;
      case 'h':
	usage (stdout);
      case 'H':
	include_hex ^= 1;
	break;
      case 'm':
	{
	  char *endptr;
	  mask = parse_mask (optarg, &endptr);
	  if (*endptr != '\0')
	    {
	      // Bad mask expression.
	      error (0, "syntax error in mask expression \"%s\" near \
character #%d.\n", optarg, (int) (endptr - optarg), endptr);
	    }
	  break;
	}
      case 'n':
	numerror ^= 1;
	break;
      case 'o':
	if ((ofile = fopen (cygpath (optarg, NULL), "wb")) == NULL)
	  error (1, "can't open %s", optarg);
#ifdef F_SETFD
	(void) fcntl (fileno (ofile), F_SETFD, 0);
#endif
	break;
      case 'p':
	pid = strtoul (optarg, NULL, 10);
	strace_active |= 2;
	break;
      case 'q':
	if (sawquiet < 0)
	  sawquiet = 1;
	else
	  sawquiet ^= 1;
	break;
      case 'S':
	flush_period = strtoul (optarg, NULL, 10);
	break;
      case 't':
	hhmmss ^= 1;
	break;
      case 'T':
	toggle ^= 1;
	break;
      case 'u':
	// FIXME: currently unimplemented
	show_usecs ^= 1;
	delta ^= 1;
	break;
      case 'V':
	// Print version info and exit
	print_version ();
	return 0;
      case 'w':
	new_window ^= 1;
	break;
      default:
	fprintf (stderr, "Try `%s --help' for more information.\n", pgm);
	exit (1);
      }

  if (pid && argv[optind])
    error (0, "cannot provide both a command line and a process id");

  if (!pid && !argv[optind])
    error (0, "must provide either a command line or a process id");

  if (toggle && !pid)
    error (0, "must provide a process id to toggle tracing");

  if (!pid)
    quiet = sawquiet < 0 || !sawquiet;
  else if (sawquiet < 0)
    quiet = 0;
  else
    quiet = sawquiet;

  if (!mask)
    mask = _STRACE_ALL;

  if (!ofile)
    ofile = stdout;

  if (bufsize)
    setvbuf (ofile, (char *) alloca (bufsize), _IOFBF, bufsize);

  if (toggle)
    dotoggle (pid);
  else
    {
      drive_map = (void *) cygwin_internal (CW_ALLOC_DRIVE_MAP);
      ret = dostrace (mask, ofile, pid, argv + optind);
      if (drive_map)
	cygwin_internal (CW_FREE_DRIVE_MAP, drive_map);
    }
  if (ofile && ofile != stdout)
    fclose (ofile);
  ExitProcess (ret);
}

int
main (int argc, char **argv)
{
  /* Make sure to have room for the _cygtls area *and* to initialize it.
     This is required to make sure cygwin_internal calls into Cygwin work
     reliably.  This problem has been noticed under AllocationPreference
     registry setting to 0x100000 (TOP_DOWN). */
  char buf[__CYGTLS_PADSIZE__];

  RtlSecureZeroMemory (buf, sizeof (buf));
  exit (main2 (argc, argv));
}

#undef CloseHandle

static BOOL
close_handle (HANDLE h, DWORD ok)
{
  child_list *c;
  for (c = &children; (c = c->next) != NULL;)
    if (c->hproc == h && c->id != ok)
      error (0, "Closing child handle %p", h);
  return CloseHandle (h);
}
