Fake stdin for applications which exit on EOF.
authorIain Patterson <me@iain.cx>
Sun, 19 Jan 2014 18:01:36 +0000 (18:01 +0000)
committerIain Patterson <me@iain.cx>
Sun, 19 Jan 2014 18:16:00 +0000 (18:16 +0000)
Some applications, like MonetDB server, expect to read commands on
stdin.  If they read a keyword, usually "exit" or "quit" or if stdin is
closed, they will exit.

Such applications will typically work when run in a service context via
NSSM, as Windows will allocate a console window for them.  However, when
the service is configured to redirect stdout and/or stderr but not
stdin, the application will exit immediately because it will see no
input and think that the user closed stdin.

To deal with this situation we interpret an AppStdin value of "|" (a
single pipe character) as requesting a hack mode whereby we open a pipe
and pass the reading end to the application as the stdin handle.  We
never actually write anything to the writing end of pipe but simply keep
it around until the application exits or the service receives a stop
control.  Closing the pipe on receipt of a stop request may even be
sufficient to close the application gracefully without resorting to any
of the other stop methods.

As an aside, MonetDB server can be run in batch mode, wherein it does
not attempt to read from stdin at all.  The hack is not necessary for it
or other applications with similar functionality.

Thanks Bryan Senseman.

README.txt
io.cpp
messages.mc
process.cpp
service.cpp
service.h

index b05fd78..ded8b7a 100644 (file)
@@ -288,6 +288,13 @@ AppStderr to the same path, eg C:\Users\Public\service.log, and it should
 work.  Remember, however, that the path must be accessible to the user\r
 running the service.\r
 \r
+Note that if you set AppStdout and/or AppStderr, applications which attempt\r
+to read stdin will fail due to a combination of factors including the way I/O\r
+redirection is configured on Windows and how a console application starts in\r
+a service context.  NSSM can fake a stdin stream so that applications can\r
+still work when they would otherwise exit when at end of file on stdin.  Set\r
+AppStdin to "|" (a single pipe character) to invoke the fake stdin.\r
+\r
 \r
 File rotation\r
 -------------\r
@@ -624,6 +631,8 @@ Thanks to Арслан Сайдуганов for suggesting setting process prior
 Thanks to Robert Middleton for suggestion and draft implementation of process\r
 affinity support.\r
 Thanks to Andrew RedzMax for suggesting an unconditional restart delay.\r
+Thanks to Bryan Senseman for noticing that applications with redirected stdout\r
+and/or stderr which attempt to read from stdin would fail.\r
 \r
 Licence\r
 -------\r
diff --git a/io.cpp b/io.cpp
index d18e31c..a4f6abb 100644 (file)
--- a/io.cpp
+++ b/io.cpp
@@ -250,21 +250,6 @@ int get_output_handles(nssm_service_t *service, HKEY key, STARTUPINFO *si) {
   ZeroMemory(&attributes, sizeof(attributes));\r
   attributes.bInheritHandle = true;\r
 \r
-  /* stdin */\r
-  if (get_createfile_parameters(key, NSSM_REG_STDIN, service->stdin_path, &service->stdin_sharing, NSSM_STDIN_SHARING, &service->stdin_disposition, NSSM_STDIN_DISPOSITION, &service->stdin_flags, NSSM_STDIN_FLAGS)) {\r
-    service->stdin_sharing = service->stdin_disposition = service->stdin_flags = 0;\r
-    ZeroMemory(service->stdin_path, _countof(service->stdin_path) * sizeof(TCHAR));\r
-    return 1;\r
-  }\r
-  if (si && service->stdin_path[0]) {\r
-    si->hStdInput = CreateFile(service->stdin_path, FILE_READ_DATA, service->stdin_sharing, &attributes, service->stdin_disposition, service->stdin_flags, 0);\r
-    if (! si->hStdInput) {\r
-      log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATEFILE_FAILED, service->stdin_path, error_string(GetLastError()), 0);\r
-      return 2;\r
-    }\r
-    set_flags = true;\r
-  }\r
-\r
   /* stdout */\r
   if (get_createfile_parameters(key, NSSM_REG_STDOUT, service->stdout_path, &service->stdout_sharing, NSSM_STDOUT_SHARING, &service->stdout_disposition, NSSM_STDOUT_DISPOSITION, &service->stdout_flags, NSSM_STDOUT_FLAGS)) {\r
     service->stdout_sharing = service->stdout_disposition = service->stdout_flags = 0;\r
@@ -346,6 +331,50 @@ int get_output_handles(nssm_service_t *service, HKEY key, STARTUPINFO *si) {
     }\r
   }\r
 \r
+  /* stdin */\r
+  if (get_createfile_parameters(key, NSSM_REG_STDIN, service->stdin_path, &service->stdin_sharing, NSSM_STDIN_SHARING, &service->stdin_disposition, NSSM_STDIN_DISPOSITION, &service->stdin_flags, NSSM_STDIN_FLAGS)) {\r
+    service->stdin_sharing = service->stdin_disposition = service->stdin_flags = 0;\r
+    ZeroMemory(service->stdin_path, _countof(service->stdin_path) * sizeof(TCHAR));\r
+    return 1;\r
+  }\r
+  if (si && service->stdin_path[0]) {\r
+    if (str_equiv(service->stdin_path, _T("|"))) {\r
+      /* Fake stdin with a pipe. */\r
+      if (set_flags) {\r
+        /*\r
+          None of this is necessary if we aren't redirecting stdout and/or\r
+          stderr as well.\r
+\r
+          If we don't redirect any handles the application will start and be\r
+          quite happy with its console.  If we start it with\r
+          STARTF_USESTDHANDLES set it will interpret a NULL value for\r
+          hStdInput as meaning no input.  Because the service starts with\r
+          no stdin we can't just pass GetStdHandle(STD_INPUT_HANDLE) to\r
+          the application.\r
+\r
+          The only way we can successfully redirect the application's output\r
+          while preventing programs which exit after reading all input from\r
+          exiting prematurely is to create a pipe between ourselves and the\r
+          application but write nothing to it.\r
+        */\r
+        if (! CreatePipe(&si->hStdInput, &service->stdin_pipe, 0, 0)) {\r
+          log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_STDIN_CREATEPIPE_FAILED, service->name, error_string(GetLastError()), 0);\r
+          return 2;\r
+        }\r
+        SetHandleInformation(si->hStdInput, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);\r
+      }\r
+    }\r
+    else {\r
+      si->hStdInput = CreateFile(service->stdin_path, FILE_READ_DATA, service->stdin_sharing, &attributes, service->stdin_disposition, service->stdin_flags, 0);\r
+      if (! si->hStdInput) {\r
+        log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATEFILE_FAILED, service->stdin_path, error_string(GetLastError()), 0);\r
+        return 2;\r
+      }\r
+\r
+      set_flags = true;\r
+    }\r
+  }\r
+\r
   if (! set_flags) return 0;\r
 \r
   /*\r
index 49ade07..79234c4 100644 (file)
Binary files a/messages.mc and b/messages.mc differ
index 19b893a..75956de 100644 (file)
@@ -149,6 +149,13 @@ int kill_process(nssm_service_t *service, HANDLE process_handle, unsigned long p
 
   kill_t k = { pid, exitcode, 0 };
 
+  /* Close the stdin pipe. */
+  if (service->stdin_pipe) {
+    CloseHandle(service->stdin_pipe);
+    service->stdin_pipe = 0;
+    if (! await_shutdown(service, _T(__FUNCTION__), service->kill_console_delay)) return 1;
+  }
+
   /* Try to send a Control-C event to the console. */
   if (service->stop_method & NSSM_STOP_METHOD_CONSOLE) {
     if (! kill_console(service)) return 1;
index 9cdf9de..c920af8 100644 (file)
@@ -1522,6 +1522,7 @@ int start_service(nssm_service_t *service) {
   bool inherit_handles = false;\r
   if (si.dwFlags & STARTF_USESTDHANDLES) inherit_handles = true;\r
   unsigned long flags = service->priority & priority_mask();\r
+  if (service->stdin_pipe) flags |= DETACHED_PROCESS;\r
   if (service->affinity) flags |= CREATE_SUSPENDED;\r
 #ifdef UNICODE\r
   flags |= CREATE_UNICODE_ENVIRONMENT;\r
@@ -1614,6 +1615,10 @@ int stop_service(nssm_service_t *service, unsigned long exitcode, bool graceful,
     UnregisterWait(service->wait_handle);\r
     service->wait_handle = 0;\r
   }\r
+  if (service->stdin_pipe) {\r
+    CloseHandle(service->stdin_pipe);\r
+    service->stdin_pipe = 0;\r
+  }\r
 \r
   service->rotate_stdout_online = service->rotate_stderr_online = NSSM_ROTATE_OFFLINE;\r
 \r
index 6da55f3..2eb2b42 100644 (file)
--- a/service.h
+++ b/service.h
@@ -57,6 +57,7 @@ typedef struct {
   unsigned long stdin_sharing;\r
   unsigned long stdin_disposition;\r
   unsigned long stdin_flags;\r
+  HANDLE stdin_pipe;\r
   TCHAR stdout_path[PATH_LENGTH];\r
   unsigned long stdout_sharing;\r
   unsigned long stdout_disposition;\r