Browse Source

Rotate output files.

The files specified by AppStdout and AppStderr can now be rotated prior
to launching the application.  An existing file will be renamed with a
new suffix based on its last write time, to millisecond precision.

Rotation is controlled independently of the CreateFile() arguments used
when opening the files for writing.  It is possible to configure
rotation regardless of whether existing files would be appended or
replaced.

Four new REG_DWORD entries control how rotation happens.

If AppRotateFiles is 0 or missing, rotation is disabled.

If AppRotateSeconds is non-zero, a file will not be rotated if its last
write time is less than the given number of seconds in the past.

If AppRotateBytes is non-zero, a file will not be rotated if it is
smaller than the given number of bytes.

64-bit file sizes can be handled by specifying a high order part in
AppRotateBytesHigh.

Thanks Doug Watson.
Iain Patterson 9 years ago
parent
commit
2f219930f4
11 changed files with 232 additions and 11 deletions
  1. 8 0
      ChangeLog.txt
  2. 29 0
      README.txt
  3. 37 2
      gui.cpp
  4. 73 1
      io.cpp
  5. 2 1
      io.h
  6. 32 0
      messages.mc
  7. 16 1
      nssm.rc
  8. 16 1
      registry.cpp
  9. 4 0
      registry.h
  10. 11 5
      resource.h
  11. 4 0
      service.h

+ 8 - 0
ChangeLog.txt

@@ -1,3 +1,11 @@
+Changes since 2.21
+-----------------
+  * NSSM can now optionally rotate existing files when
+    redirecting I/O.
+
+  * Unqualified path names are now relative to the
+    application startup directory when redirecting I/O.
+
 Changes since 2.20
 -----------------
   * Services installed from the GUI no longer have incorrect

+ 29 - 0
README.txt

@@ -53,6 +53,8 @@ Since version 2.19, NSSM can add to the service's environment by setting
 AppEnvironmentExtra in place of or in addition to the srvany-compatible
 AppEnvironment.
 
+Since version 2.22, NSSM can rotate existing output files when redirecting I/O.
+
 
 Usage
 -----
@@ -234,6 +236,32 @@ work.  Remember, however, that the path must be accessible to the user
 running the service.
 
 
+File rotation
+-------------
+When using I/O redirection, NSSM can rotate existing output files prior to
+opening stdout and/or stderr.  An existing file will be renamed with a
+suffix based on the file's last write time, to millisecond precision.  For
+example, the file nssm.log might be rotated to nssm-20131221T113939.457.log.
+
+NSSM will look in the registry under
+HKLM\SYSTEM\CurrentControlSet\Services\<service>\Parameters for REG_DWORD
+entries which control how rotation happens.
+
+If AppRotateFiles is missing or set to 0, rotation is disabled.  Any non-zero
+value enables rotation.
+
+If AppRotateSeconds is non-zero, a file will not be rotated if its last write
+time is less than the given number of seconds in the past.
+
+If AppRotateBytes is non-zero, a file will not be rotated if it is smaller
+than the given number of bytes.  64-bit file sizes can be handled by setting
+a non-zero value of AppRotateBytesHigh.
+
+Rotation is independent of the CreateFile() parameters used to open the files.
+They will be rotated regardless of whether NSSM would otherwise have appended
+or replaced them.
+
+
 Environment variables
 ---------------------
 NSSM can replace or append to the managed application's environment.  Two
@@ -328,6 +356,7 @@ Thanks to Brian Baxter for suggesting how to escape quotes from the command prom
 Thanks to Russ Holmann for suggesting that the shutdown timeout be configurable.
 Thanks to Paul Spause for spotting a bug with default registry entries.
 Thanks to BUGHUNTER for spotting more GUI bugs.
+Thanks to Doug Watson for suggesting file rotation.
 
 Licence
 -------

+ 37 - 2
gui.cpp

@@ -1,6 +1,6 @@
 #include "nssm.h"
 
-static enum { NSSM_TAB_APPLICATION, NSSM_TAB_SHUTDOWN, NSSM_TAB_EXIT, NSSM_TAB_IO, NSSM_TAB_ENVIRONMENT, NSSM_NUM_TABS };
+static enum { NSSM_TAB_APPLICATION, NSSM_TAB_SHUTDOWN, NSSM_TAB_EXIT, NSSM_TAB_IO, NSSM_TAB_ROTATION, NSSM_TAB_ENVIRONMENT, NSSM_NUM_TABS };
 static HWND tablist[NSSM_NUM_TABS];
 static int selected_tab;
 
@@ -135,12 +135,24 @@ int install(HWND window) {
     check_io(_T("stdin"), service->stdin_path, sizeof(service->stdin_path), IDC_STDIN);
     check_io(_T("stdout"), service->stdout_path, sizeof(service->stdout_path), IDC_STDOUT);
     check_io(_T("stderr"), service->stderr_path, sizeof(service->stderr_path), IDC_STDERR);
+
     /* Override stdout and/or stderr. */
-    if (SendDlgItemMessage(tablist[NSSM_TAB_IO], IDC_TRUNCATE, BM_GETCHECK, 0, 0) & BST_CHECKED) {
+    if (SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_TRUNCATE, BM_GETCHECK, 0, 0) & BST_CHECKED) {
       if (service->stdout_path[0]) service->stdout_disposition = CREATE_ALWAYS;
       if (service->stderr_path[0]) service->stderr_disposition = CREATE_ALWAYS;
     }
 
+    /* Get rotation stuff. */
+    if (SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE, BM_GETCHECK, 0, 0) & BST_CHECKED) {
+      service->rotate_files = true;
+    }
+    if (SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_SECONDS_ENABLED, BM_GETCHECK, 0, 0) & BST_CHECKED) {
+      check_method_timeout(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_SECONDS, &service->rotate_seconds);
+    }
+    if (SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_BYTES_LOW_ENABLED, BM_GETCHECK, 0, 0) & BST_CHECKED) {
+      check_method_timeout(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_BYTES_LOW, &service->rotate_bytes_low);
+    }
+
     /* Get environment. */
     unsigned long envlen = (unsigned long) SendMessage(GetDlgItem(tablist[NSSM_TAB_ENVIRONMENT], IDC_ENVIRONMENT), WM_GETTEXTLENGTH, 0, 0);
     if (envlen) {
@@ -367,6 +379,7 @@ INT_PTR CALLBACK tab_dlg(HWND tab, UINT message, WPARAM w, LPARAM l) {
     case WM_COMMAND:
       HWND dlg;
       TCHAR buffer[MAX_PATH];
+      unsigned long state;
 
       switch (LOWORD(w)) {
         /* Browse for application. */
@@ -416,6 +429,17 @@ INT_PTR CALLBACK tab_dlg(HWND tab, UINT message, WPARAM w, LPARAM l) {
           GetDlgItemText(tab, IDC_STDERR, buffer, sizeof(buffer));
           browse(dlg, buffer, 0, NSSM_GUI_BROWSE_FILTER_ALL_FILES, 0);
           break;
+
+        /* Rotation. */
+        case IDC_ROTATE:
+        case IDC_ROTATE_SECONDS_ENABLED:
+        case IDC_ROTATE_BYTES_LOW_ENABLED:
+          if (SendDlgItemMessage(tab, LOWORD(w), BM_GETCHECK, 0, 0) & BST_CHECKED) state = BST_CHECKED;
+          else state = BST_UNCHECKED;
+          SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE, BM_SETCHECK, state, 0);
+          SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_SECONDS_ENABLED, BM_SETCHECK, state, 0);
+          SendDlgItemMessage(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_BYTES_LOW_ENABLED, BM_SETCHECK, state, 0);
+          break;
       }
       return 1;
   }
@@ -486,6 +510,17 @@ INT_PTR CALLBACK install_dlg(HWND window, UINT message, WPARAM w, LPARAM l) {
       tablist[NSSM_TAB_IO] = CreateDialog(0, MAKEINTRESOURCE(IDD_IO), window, tab_dlg);
       ShowWindow(tablist[NSSM_TAB_IO], SW_HIDE);
 
+      /* Rotation tab. */
+      tab.pszText = message_string(NSSM_GUI_TAB_ROTATION);
+      tab.cchTextMax = (int) _tcslen(tab.pszText) + 1;
+      SendMessage(tabs, TCM_INSERTITEM, NSSM_TAB_ROTATION, (LPARAM) &tab);
+      tablist[NSSM_TAB_ROTATION] = CreateDialog(0, MAKEINTRESOURCE(IDD_ROTATION), window, tab_dlg);
+      ShowWindow(tablist[NSSM_TAB_ROTATION], SW_HIDE);
+
+      /* Set defaults. */
+      SetDlgItemInt(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_SECONDS, 0, 0);
+      SetDlgItemInt(tablist[NSSM_TAB_ROTATION], IDC_ROTATE_BYTES_LOW, 0, 0);
+
       /* Environment tab. */
       tab.pszText = message_string(NSSM_GUI_TAB_ENVIRONMENT);
       tab.cchTextMax = (int) _tcslen(tab.pszText) + 1;

+ 73 - 1
io.cpp

@@ -81,7 +81,77 @@ HANDLE append_to_file(TCHAR *path, unsigned long sharing, SECURITY_ATTRIBUTES *a
   return CreateFile(path, FILE_WRITE_DATA, sharing, attributes, disposition, flags, 0);
 }
 
-int get_output_handles(HKEY key, STARTUPINFO *si) {
+void rotate_file(TCHAR *service_name, TCHAR *path, unsigned long seconds, unsigned long low, unsigned long high) {
+  unsigned long error;
+
+  /* Now. */
+  SYSTEMTIME st;
+  GetSystemTime(&st);
+
+  BY_HANDLE_FILE_INFORMATION info;
+
+  /* Try to open the file to check if it exists and to get attributes. */
+  HANDLE file = CreateFile(path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
+  if (file) {
+    /* Get file attributes. */
+    if (! GetFileInformationByHandle(file, &info)) {
+      /* Reuse current time for rotation timestamp. */
+      seconds = low = high = 0;
+      SystemTimeToFileTime(&st, &info.ftLastWriteTime);
+    }
+
+    CloseHandle(file);
+  }
+  else {
+    error = GetLastError();
+    if (error == ERROR_FILE_NOT_FOUND) return;
+    log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_ROTATE_FILE_FAILED, service_name, path, _T("CreateFile()"), path, error_string(error), 0);
+    /* Reuse current time for rotation timestamp. */
+    seconds = low = high = 0;
+    SystemTimeToFileTime(&st, &info.ftLastWriteTime);
+  }
+
+  /* Check file age. */
+  if (seconds) {
+    FILETIME ft;
+    SystemTimeToFileTime(&st, &ft);
+
+    ULARGE_INTEGER s;
+    s.LowPart = ft.dwLowDateTime;
+    s.HighPart = ft.dwHighDateTime;
+    s.QuadPart -= seconds * 10000000LL;
+    ft.dwLowDateTime = s.LowPart;
+    ft.dwHighDateTime = s.HighPart;
+    if (CompareFileTime(&info.ftLastWriteTime, &ft) > 0) return;
+  }
+
+  /* Check file size. */
+  if (low || high) {
+    if (info.nFileSizeHigh < high) return;
+    if (info.nFileSizeHigh == high && info.nFileSizeLow < low) return;
+  }
+
+  /* Get new filename. */
+  FileTimeToSystemTime(&info.ftLastWriteTime, &st);
+
+  TCHAR buffer[MAX_PATH];
+  memmove(buffer, path, sizeof(buffer));
+  TCHAR *ext = PathFindExtension(buffer);
+  TCHAR extension[MAX_PATH];
+  _sntprintf_s(extension, _countof(extension), _TRUNCATE, _T("-%04u%02u%02uT%02u%02u%02u.%03u%s"), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, ext);
+  *ext = _T('\0');
+  TCHAR rotated[MAX_PATH];
+  _sntprintf_s(rotated, _countof(rotated), _TRUNCATE, _T("%s%s"), buffer, extension);
+
+  /* Rotate. */
+  if (MoveFile(path, rotated)) return;
+  error = GetLastError();
+
+  log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_ROTATE_FILE_FAILED, service_name, path, _T("MoveFile()"), rotated, error_string(error), 0);
+  return;
+}
+
+int get_output_handles(nssm_service_t *service, HKEY key, STARTUPINFO *si) {
   TCHAR path[MAX_PATH];
   TCHAR stdout_path[MAX_PATH];
   unsigned long sharing, disposition, flags;
@@ -112,6 +182,7 @@ int get_output_handles(HKEY key, STARTUPINFO *si) {
       return 4;
     }
 
+    if (service->rotate_files) rotate_file(service->name, path, service->rotate_seconds, service->rotate_bytes_low, service->rotate_bytes_high);
     si->hStdOutput = append_to_file(path, sharing, &attributes, disposition, flags);
     if (! si->hStdOutput) return 5;
     set_flags = true;
@@ -130,6 +201,7 @@ int get_output_handles(HKEY key, STARTUPINFO *si) {
       }
     }
     else {
+      if (service->rotate_files) rotate_file(service->name, path, service->rotate_seconds, service->rotate_bytes_low, service->rotate_bytes_high);
       si->hStdError = append_to_file(path, sharing, &attributes, disposition, flags);
       if (! si->hStdError) {
         log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_CREATEFILE_FAILED, path, error_string(GetLastError()));

+ 2 - 1
io.h

@@ -14,7 +14,8 @@
 int get_createfile_parameters(HKEY, TCHAR *, TCHAR *, unsigned long *, unsigned long, unsigned long *, unsigned long, unsigned long *, unsigned long);
 int set_createfile_parameter(HKEY, TCHAR *, TCHAR *, unsigned long);
 HANDLE append_to_file(TCHAR *, unsigned long, SECURITY_ATTRIBUTES *, unsigned long, unsigned long);
-int get_output_handles(HKEY, STARTUPINFO *);
+void rotate_file(TCHAR *, TCHAR *, unsigned long, unsigned long, unsigned long);
+int get_output_handles(nssm_service_t *, HKEY, STARTUPINFO *);
 void close_output_handles(STARTUPINFO *);
 
 #endif

+ 32 - 0
messages.mc

@@ -510,6 +510,19 @@ Language = Italian
 I/O%0
 .
 
+MessageId = +1
+SymbolicName = NSSM_GUI_TAB_ROTATION
+Severity = Informational
+Language = English
+File rotation%0
+.
+Language = French
+File rotation%0
+.
+Language = Italian
+File rotation%0
+.
+
 MessageId = +1
 SymbolicName = NSSM_GUI_TAB_ENVIRONMENT
 Severity = Informational
@@ -1546,3 +1559,22 @@ SetEnvironmentVariable(%1=%2) a 
 Language = Italian
 Chiamata a SetEnvironmentVariable(%1=%2) fallita:
 .
+
+MessageId = +1
+SymbolicName = NSSM_EVENT_ROTATE_FILE_FAILED
+Severity = Error
+Language = English
+Failed to rotate output file %2 for service %1.
+%3 failed for file %4:
+%5
+.
+Language = French
+Failed to rotate output file %2 for service %1.
+%3 failed for file %4:
+%5
+.
+Language = Italian
+Failed to rotate output file %2 for service %1.
+%3 failed for file %4:
+%5
+.

+ 16 - 1
nssm.rc

@@ -154,7 +154,22 @@ FONT 8, "MS Sans Serif"
     LTEXT           "Error (stderr):", IDC_STATIC, 13, 50, 53, 8, SS_LEFT
     EDITTEXT        IDC_STDERR, 70, 48, 167, 12, ES_AUTOHSCROLL, WS_EX_ACCEPTFILES
     DEFPUSHBUTTON   "...", IDC_BROWSE_STDERR, 239, 47, 15, 14
-    AUTOCHECKBOX    "Replace files", IDC_TRUNCATE, 13, 60, 74, 8
+}
+
+LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
+IDD_ROTATION DIALOG 9, 20, 261, 75
+STYLE DS_SHELLFONT | WS_VISIBLE | WS_CHILD | DS_CONTROL
+FONT 8, "MS Sans Serif"
+{
+    GROUPBOX        "File rotation", IDC_STATIC, 7, 7, 251, 68
+    AUTOCHECKBOX    "Replace existing Output and/or Error files", IDC_TRUNCATE, 13, 18, 145, 8
+    AUTOCHECKBOX    "Rotate files", IDC_ROTATE, 13, 32, 51, 8
+    AUTOCHECKBOX    "Restrict rotation to files older than", IDC_ROTATE_SECONDS_ENABLED, 13, 46, 121, 8
+    EDITTEXT        IDC_ROTATE_SECONDS, 140, 44, 29, 12, ES_AUTOHSCROLL | ES_NUMBER
+    LTEXT           "s", IDC_STATIC, 171, 46, 8, 8, SS_LEFT
+    AUTOCHECKBOX    "Restrict rotation to files bigger than", IDC_ROTATE_BYTES_LOW_ENABLED, 13, 60, 125, 8
+    EDITTEXT        IDC_ROTATE_BYTES_LOW, 140, 58, 49, 12, ES_AUTOHSCROLL | ES_NUMBER
+    LTEXT           "kB", IDC_STATIC, 191, 60, 8, 8, SS_LEFT
 }
 
 LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL

+ 16 - 1
registry.cpp

@@ -86,6 +86,10 @@ int create_parameters(nssm_service_t *service) {
     if (service->stderr_disposition != NSSM_STDERR_DISPOSITION) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_DISPOSITION, service->stderr_disposition);
     if (service->stderr_flags != NSSM_STDERR_FLAGS) set_createfile_parameter(key, NSSM_REG_STDERR, NSSM_REG_STDIO_FLAGS, service->stderr_flags);
   }
+  if (service->rotate_files) set_number(key, NSSM_REG_ROTATE, 1);
+  if (service->rotate_seconds) set_number(key, NSSM_REG_ROTATE_SECONDS, service->rotate_seconds);
+  if (service->rotate_bytes_low) set_number(key, NSSM_REG_ROTATE_BYTES_LOW, service->rotate_bytes_low);
+  if (service->rotate_bytes_high) set_number(key, NSSM_REG_ROTATE_BYTES_HIGH, service->rotate_bytes_high);
 
   /* Environment */
   if (service->env) {
@@ -387,13 +391,24 @@ int get_parameters(nssm_service_t *service, STARTUPINFO *si) {
     }
   }
 
+  /* Try to get file rotation settings - may fail. */
+  unsigned long rotate_files;
+  if (get_number(key, NSSM_REG_ROTATE, &rotate_files, false) == 1) {
+    if (rotate_files) service->rotate_files = true;
+    else service->rotate_files = false;
+  }
+  else service->rotate_files = false;
+  if (get_number(key, NSSM_REG_ROTATE_SECONDS, &service->rotate_seconds, false) != 1) service->rotate_seconds = 0;
+  if (get_number(key, NSSM_REG_ROTATE_BYTES_LOW, &service->rotate_bytes_low, false) != 1) service->rotate_bytes_low = 0;
+  if (get_number(key, NSSM_REG_ROTATE_BYTES_HIGH, &service->rotate_bytes_high, false) != 1) service->rotate_bytes_high = 0;
+
   /* Change to startup directory in case stdout/stderr are relative paths. */
   TCHAR cwd[MAX_PATH];
   GetCurrentDirectory(_countof(cwd), cwd);
   SetCurrentDirectory(service->dir);
 
   /* Try to get stdout and stderr */
-  if (get_output_handles(key, si)) {
+  if (get_output_handles(service, key, si)) {
     log_event(EVENTLOG_ERROR_TYPE, NSSM_EVENT_GET_OUTPUT_HANDLES_FAILED, service->name, 0);
     RegCloseKey(key);
     SetCurrentDirectory(cwd);

+ 4 - 0
registry.h

@@ -19,6 +19,10 @@
 #define NSSM_REG_STDIO_SHARING _T("ShareMode")
 #define NSSM_REG_STDIO_DISPOSITION _T("CreationDisposition")
 #define NSSM_REG_STDIO_FLAGS _T("FlagsAndAttributes")
+#define NSSM_REG_ROTATE _T("AppRotateFiles")
+#define NSSM_REG_ROTATE_SECONDS _T("AppRotateSeconds")
+#define NSSM_REG_ROTATE_BYTES_LOW _T("AppRotateBytes")
+#define NSSM_REG_ROTATE_BYTES_HIGH _T("AppRotateBytesHigh")
 #define NSSM_STDIO_LENGTH 29
 
 int create_messages();

+ 11 - 5
resource.h

@@ -8,9 +8,10 @@
 #define IDD_REMOVE            103
 #define IDD_APPLICATION            104
 #define IDD_IO            105
-#define IDD_APPEXIT            106
-#define IDD_SHUTDOWN            107
-#define IDD_ENVIRONMENT            108
+#define IDD_ROTATION        106
+#define IDD_APPEXIT            107
+#define IDD_SHUTDOWN            108
+#define IDD_ENVIRONMENT            109
 #define IDC_PATH                        1000
 #define IDC_TAB1                        1001
 #define IDC_CANCEL                      1002
@@ -38,14 +39,19 @@
 #define IDC_ENVIRONMENT                 1025
 #define IDC_ENVIRONMENT_REPLACE         1026
 #define IDC_TRUNCATE                    1027
+#define IDC_ROTATE                      1028
+#define IDC_ROTATE_SECONDS_ENABLED      1029
+#define IDC_ROTATE_SECONDS              1030
+#define IDC_ROTATE_BYTES_LOW_ENABLED    1031
+#define IDC_ROTATE_BYTES_LOW            1032
 
 // Next default values for new objects
 // 
 #ifdef APSTUDIO_INVOKED
 #ifndef APSTUDIO_READONLY_SYMBOLS
-#define _APS_NEXT_RESOURCE_VALUE        109
+#define _APS_NEXT_RESOURCE_VALUE        110
 #define _APS_NEXT_COMMAND_VALUE         40001
-#define _APS_NEXT_CONTROL_VALUE         1028
+#define _APS_NEXT_CONTROL_VALUE         1033
 #define _APS_NEXT_SYMED_VALUE           101
 #endif
 #endif

+ 4 - 0
service.h

@@ -37,6 +37,10 @@ typedef struct {
   unsigned long stderr_sharing;
   unsigned long stderr_disposition;
   unsigned long stderr_flags;
+  bool rotate_files;
+  unsigned long rotate_seconds;
+  unsigned long rotate_bytes_low;
+  unsigned long rotate_bytes_high;
   unsigned long default_exit_action;
   unsigned long throttle_delay;
   unsigned long stop_method;