001/*
002 * Copyright 2017-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2017-2018 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.unboundidds.tools;
022
023
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.PrintStream;
028import java.nio.ByteBuffer;
029import java.nio.channels.FileChannel;
030import java.nio.channels.FileLock;
031import java.nio.file.StandardOpenOption;
032import java.nio.file.attribute.FileAttribute;
033import java.nio.file.attribute.PosixFilePermission;
034import java.nio.file.attribute.PosixFilePermissions;
035import java.text.SimpleDateFormat;
036import java.util.Collections;
037import java.util.Date;
038import java.util.EnumSet;
039import java.util.HashSet;
040import java.util.List;
041import java.util.Properties;
042import java.util.Set;
043
044import com.unboundid.util.Debug;
045import com.unboundid.util.ObjectPair;
046import com.unboundid.util.StaticUtils;
047import com.unboundid.util.ThreadSafety;
048import com.unboundid.util.ThreadSafetyLevel;
049
050import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
051
052
053
054/**
055 * This class provides a utility that can log information about the launch and
056 * completion of a tool invocation.
057 * <BR>
058 * <BLOCKQUOTE>
059 *   <B>NOTE:</B>  This class, and other classes within the
060 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
061 *   supported for use against Ping Identity, UnboundID, and
062 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
063 *   for proprietary functionality or for external specifications that are not
064 *   considered stable or mature enough to be guaranteed to work in an
065 *   interoperable way with other types of LDAP servers.
066 * </BLOCKQUOTE>
067 */
068@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
069public final class ToolInvocationLogger
070{
071  /**
072   * The format string that should be used to format log message timestamps.
073   */
074  private static final String LOG_MESSAGE_DATE_FORMAT =
075       "dd/MMM/yyyy:HH:mm:ss.SSS Z";
076
077  /**
078   * The name of a system property that can be used to specify an alternate
079   * instance root path for testing purposes.
080   */
081  static final String PROPERTY_TEST_INSTANCE_ROOT =
082          ToolInvocationLogger.class.getName() + ".testInstanceRootPath";
083
084  /**
085   * Prevent this utility class from being instantiated.
086   */
087  private ToolInvocationLogger()
088  {
089    // No implementation is required.
090  }
091
092
093
094  /**
095   * Retrieves an object with a set of information about the invocation logging
096   * that should be performed for the specified tool, if any.
097   *
098   * @param  commandName      The name of the command (without any path
099   *                          information) for the associated tool.  It must not
100   *                          be {@code null}.
101   * @param  logByDefault     Indicates whether the tool indicates that
102   *                          invocation log messages should be generated for
103   *                          the specified tool by default.  This may be
104   *                          overridden by content in the
105   *                          {@code tool-invocation-logging.properties} file,
106   *                          but it will be used in the absence of the
107   *                          properties file or if the properties file does not
108   *                          specify whether logging should be performed for
109   *                          the specified tool.
110   * @param  toolErrorStream  A print stream that may be used to report
111   *                          information about any problems encountered while
112   *                          attempting to perform invocation logging.  It
113   *                          must not be {@code null}.
114   *
115   * @return  An object with a set of information about the invocation logging
116   *          that should be performed for the specified tool.  The
117   *          {@link ToolInvocationLogDetails#logInvocation()} method may
118   *          be used to determine whether invocation logging should be
119   *          performed.
120   */
121  public static ToolInvocationLogDetails getLogMessageDetails(
122                                              final String commandName,
123                                              final boolean logByDefault,
124                                              final PrintStream toolErrorStream)
125  {
126    // Try to figure out the path to the server instance root.  In production
127    // code, we'll look for an INSTANCE_ROOT environment variable to specify
128    // that path, but to facilitate unit testing, we'll allow it to be
129    // overridden by a Java system property so that we can have our own custom
130    // path.
131    String instanceRootPath = System.getProperty(PROPERTY_TEST_INSTANCE_ROOT);
132    if (instanceRootPath == null)
133    {
134      instanceRootPath = System.getenv("INSTANCE_ROOT");
135      if (instanceRootPath == null)
136      {
137        return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
138      }
139    }
140
141    final File instanceRootDirectory =
142         new File(instanceRootPath).getAbsoluteFile();
143    if ((!instanceRootDirectory.exists()) ||
144         (!instanceRootDirectory.isDirectory()))
145    {
146      return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
147    }
148
149
150    // Construct the paths to the default tool invocation log file and to the
151    // logging properties file.
152    final boolean canUseDefaultLog;
153    final File defaultToolInvocationLogFile = StaticUtils.constructPath(
154         instanceRootDirectory, "logs", "tools", "tool-invocation.log");
155    if (defaultToolInvocationLogFile.exists())
156    {
157      canUseDefaultLog = defaultToolInvocationLogFile.isFile();
158    }
159    else
160    {
161      final File parentDirectory = defaultToolInvocationLogFile.getParentFile();
162      canUseDefaultLog =
163           (parentDirectory.exists() && parentDirectory.isDirectory());
164    }
165
166    final File invocationLoggingPropertiesFile = StaticUtils.constructPath(
167         instanceRootDirectory, "config", "tool-invocation-logging.properties");
168
169
170    // If the properties file doesn't exist, then just use the logByDefault
171    // setting in conjunction with the default tool invocation log file.
172    if (!invocationLoggingPropertiesFile.exists())
173    {
174      if (logByDefault && canUseDefaultLog)
175      {
176        return ToolInvocationLogDetails.createLogDetails(commandName, null,
177             Collections.singleton(defaultToolInvocationLogFile),
178             toolErrorStream);
179      }
180      else
181      {
182        return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
183      }
184    }
185
186
187    // Load the properties file.  If this fails, then report an error and do not
188    // attempt any additional logging.
189    final Properties loggingProperties = new Properties();
190    try (final FileInputStream inputStream =
191              new FileInputStream(invocationLoggingPropertiesFile))
192    {
193      loggingProperties.load(inputStream);
194    }
195    catch (final Exception e)
196    {
197      Debug.debugException(e);
198      printError(
199           ERR_TOOL_LOGGER_ERROR_LOADING_PROPERTIES_FILE.get(
200                invocationLoggingPropertiesFile.getAbsolutePath(),
201                StaticUtils.getExceptionMessage(e)),
202           toolErrorStream);
203      return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
204    }
205
206
207    // See if there is a tool-specific property that indicates whether to
208    // perform invocation logging for the tool.
209    Boolean logInvocation = getBooleanProperty(
210         commandName + ".log-tool-invocations", loggingProperties,
211         invocationLoggingPropertiesFile, null, toolErrorStream);
212
213
214    // If there wasn't a valid tool-specific property to indicate whether to
215    // perform invocation logging, then see if there is a default property for
216    // all tools.
217    if (logInvocation == null)
218    {
219      logInvocation = getBooleanProperty("default.log-tool-invocations",
220           loggingProperties, invocationLoggingPropertiesFile, null,
221           toolErrorStream);
222    }
223
224
225    // If we still don't know whether to log the invocation, then use the
226    // default setting for the tool.
227    if (logInvocation == null)
228    {
229      logInvocation = logByDefault;
230    }
231
232
233    // If we shouldn't log the invocation, then return a "no log" result now.
234    if (!logInvocation)
235    {
236      return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
237    }
238
239
240    // See if there is a tool-specific property that specifies a log file path.
241    final Set<File> logFiles = new HashSet<>(2);
242    final String toolSpecificLogFilePathPropertyName =
243         commandName + ".log-file-path";
244    final File toolSpecificLogFile = getLogFileProperty(
245         toolSpecificLogFilePathPropertyName, loggingProperties,
246         invocationLoggingPropertiesFile, instanceRootDirectory,
247         toolErrorStream);
248    if (toolSpecificLogFile != null)
249    {
250      logFiles.add(toolSpecificLogFile);
251    }
252
253
254    // See if the tool should be included in the default log file.
255    if (getBooleanProperty(commandName + ".include-in-default-log",
256         loggingProperties, invocationLoggingPropertiesFile, true,
257         toolErrorStream))
258    {
259      // See if there is a property that specifies a default log file path.
260      // Otherwise, try to use the default path that we constructed earlier.
261      final String defaultLogFilePathPropertyName = "default.log-file-path";
262      final File defaultLogFile = getLogFileProperty(
263           defaultLogFilePathPropertyName, loggingProperties,
264           invocationLoggingPropertiesFile, instanceRootDirectory,
265           toolErrorStream);
266      if (defaultLogFile != null)
267      {
268        logFiles.add(defaultLogFile);
269      }
270      else if (canUseDefaultLog)
271      {
272        logFiles.add(defaultToolInvocationLogFile);
273      }
274      else
275      {
276        printError(
277             ERR_TOOL_LOGGER_NO_LOG_FILES.get(commandName,
278                  invocationLoggingPropertiesFile.getAbsolutePath(),
279                  toolSpecificLogFilePathPropertyName,
280                  defaultLogFilePathPropertyName),
281             toolErrorStream);
282      }
283    }
284
285
286    // If the set of log files is empty, then don't log anything.  Otherwise, we
287    // can and should perform invocation logging.
288    if (logFiles.isEmpty())
289    {
290      return ToolInvocationLogDetails.createDoNotLogDetails(commandName);
291    }
292    else
293    {
294      return ToolInvocationLogDetails.createLogDetails(commandName, null,
295           logFiles, toolErrorStream);
296    }
297  }
298
299
300
301  /**
302   * Retrieves the Boolean value of the specified property from the set of tool
303   * properties.
304   *
305   * @param  propertyName        The name of the property to retrieve.
306   * @param  properties          The set of tool properties.
307   * @param  propertiesFilePath  The path to the properties file.
308   * @param  defaultValue        The default value that should be returned if
309   *                             the property isn't set or has an invalid value.
310   * @param  toolErrorStream     A print stream that may be used to report
311   *                             information about any problems encountered
312   *                             while attempting to perform invocation logging.
313   *                             It must not be {@code null}.
314   *
315   * @return  {@code true} if the specified property exists with a value of
316   *          {@code true}, {@code false} if the specified property exists with
317   *          a value of {@code false}, or the default value if the property
318   *          doesn't exist or has a value that is neither {@code true} nor
319   *          {@code false}.
320   */
321   private static Boolean getBooleanProperty(final String propertyName,
322                                             final Properties properties,
323                                             final File propertiesFilePath,
324                                             final Boolean defaultValue,
325                                             final PrintStream toolErrorStream)
326   {
327     final String propertyValue = properties.getProperty(propertyName);
328     if (propertyValue == null)
329     {
330       return defaultValue;
331     }
332
333     if (propertyValue.equalsIgnoreCase("true"))
334     {
335       return true;
336     }
337     else if (propertyValue.equalsIgnoreCase("false"))
338     {
339       return false;
340     }
341     else
342     {
343      printError(
344           ERR_TOOL_LOGGER_CANNOT_PARSE_BOOLEAN_PROPERTY.get(propertyValue,
345                propertyName, propertiesFilePath.getAbsolutePath()),
346           toolErrorStream);
347       return defaultValue;
348     }
349   }
350
351
352
353  /**
354   * Retrieves a file referenced by the specified property from the set of
355   * tool properties.
356   *
357   * @param  propertyName           The name of the property to retrieve.
358   * @param  properties             The set of tool properties.
359   * @param  propertiesFilePath     The path to the properties file.
360   * @param  instanceRootDirectory  The path to the server's instance root
361   *                                directory.
362   * @param  toolErrorStream        A print stream that may be used to report
363   *                                information about any problems encountered
364   *                                while attempting to perform invocation
365   *                                logging.  It must not be {@code null}.
366   *
367   * @return  A file referenced by the specified property, or {@code null} if
368   *          the property is not set or does not reference a valid path.
369   */
370  private static File getLogFileProperty(final String propertyName,
371                                         final Properties properties,
372                                         final File propertiesFilePath,
373                                         final File instanceRootDirectory,
374                                         final PrintStream toolErrorStream)
375  {
376    final String propertyValue = properties.getProperty(propertyName);
377    if (propertyValue == null)
378    {
379      return null;
380    }
381
382    final File absoluteFile;
383    final File configuredFile = new File(propertyValue);
384    if (configuredFile.isAbsolute())
385    {
386      absoluteFile = configuredFile;
387    }
388    else
389    {
390      absoluteFile = new File(instanceRootDirectory.getAbsolutePath() +
391           File.separator + propertyValue);
392    }
393
394    if (absoluteFile.exists())
395    {
396      if (absoluteFile.isFile())
397      {
398        return absoluteFile;
399      }
400      else
401      {
402        printError(
403             ERR_TOOL_LOGGER_PATH_NOT_FILE.get(propertyValue, propertyName,
404                  propertiesFilePath.getAbsolutePath()),
405             toolErrorStream);
406      }
407    }
408    else
409    {
410      final File parentFile = absoluteFile.getParentFile();
411      if (parentFile.exists() && parentFile.isDirectory())
412      {
413        return absoluteFile;
414      }
415      else
416      {
417        printError(
418             ERR_TOOL_LOGGER_PATH_PARENT_MISSING.get(propertyValue,
419                  propertyName, propertiesFilePath.getAbsolutePath(),
420                  parentFile.getAbsolutePath()),
421             toolErrorStream);
422      }
423    }
424
425    return null;
426  }
427
428
429
430  /**
431   * Logs a message about the launch of the specified tool.  This method must
432   * acquire an exclusive lock on each log file before attempting to append any
433   * data to it.
434   *
435   * @param  logDetails               The tool invocation log details object
436   *                                  obtained from running the
437   *                                  {@link #getLogMessageDetails} method.  It
438   *                                  must not be {@code null}.
439   * @param  commandLineArguments     A list of the name-value pairs for any
440   *                                  command-line arguments provided when
441   *                                  running the program.  This must not be
442   *                                  {@code null}, but it may be empty.
443   *                                  <BR><BR>
444   *                                  For a tool run in interactive mode, this
445   *                                  should be the arguments that would have
446   *                                  been provided if the tool had been invoked
447   *                                  non-interactively.  For any arguments that
448   *                                  have a name but no value (including
449   *                                  Boolean arguments and subcommand names),
450   *                                  or for unnamed trailing arguments, the
451   *                                  first item in the pair should be
452   *                                  non-{@code null} and the second item
453   *                                  should be {@code null}.  For arguments
454   *                                  whose values may contain sensitive
455   *                                  information, the value should have already
456   *                                  been replaced with the string
457   *                                  "*****REDACTED*****".
458   * @param  propertiesFileArguments  A list of the name-value pairs for any
459   *                                  arguments obtained from a properties file
460   *                                  rather than being supplied on the command
461   *                                  line.  This must not be {@code null}, but
462   *                                  may be empty.  The same constraints
463   *                                  specified for the
464   *                                  {@code commandLineArguments} parameter
465   *                                  also apply to this parameter.
466   * @param  propertiesFilePath       The path to the properties file from which
467   *                                  the {@code propertiesFileArguments} values
468   *                                  were obtained.
469   */
470  public static void logLaunchMessage(
471          final ToolInvocationLogDetails logDetails,
472          final List<ObjectPair<String,String>> commandLineArguments,
473          final List<ObjectPair<String,String>> propertiesFileArguments,
474          final String propertiesFilePath)
475  {
476    // Build the log message.
477    final StringBuilder msgBuffer = new StringBuilder();
478    final SimpleDateFormat dateFormat =
479         new SimpleDateFormat(LOG_MESSAGE_DATE_FORMAT);
480
481    msgBuffer.append("# [");
482    msgBuffer.append(dateFormat.format(new Date()));
483    msgBuffer.append(']');
484    msgBuffer.append(StaticUtils.EOL);
485    msgBuffer.append("# Command Name: ");
486    msgBuffer.append(logDetails.getCommandName());
487    msgBuffer.append(StaticUtils.EOL);
488    msgBuffer.append("# Invocation ID: ");
489    msgBuffer.append(logDetails.getInvocationID());
490    msgBuffer.append(StaticUtils.EOL);
491
492    final String systemUserName = System.getProperty("user.name");
493    if ((systemUserName != null) && (! systemUserName.isEmpty()))
494    {
495      msgBuffer.append("# System User: ");
496      msgBuffer.append(systemUserName);
497      msgBuffer.append(StaticUtils.EOL);
498    }
499
500    if (! propertiesFileArguments.isEmpty())
501    {
502      msgBuffer.append("# Arguments obtained from '");
503      msgBuffer.append(propertiesFilePath);
504      msgBuffer.append("':");
505      msgBuffer.append(StaticUtils.EOL);
506
507      for (final ObjectPair<String,String> argPair : propertiesFileArguments)
508      {
509        msgBuffer.append("#      ");
510
511        final String name = argPair.getFirst();
512        if (name.startsWith("-"))
513        {
514          msgBuffer.append(name);
515        }
516        else
517        {
518          msgBuffer.append(StaticUtils.cleanExampleCommandLineArgument(name));
519        }
520
521        final String value = argPair.getSecond();
522        if (value != null)
523        {
524          msgBuffer.append(' ');
525          msgBuffer.append(getCleanArgumentValue(name, value));
526        }
527
528        msgBuffer.append(StaticUtils.EOL);
529      }
530    }
531
532    msgBuffer.append(logDetails.getCommandName());
533    for (final ObjectPair<String,String> argPair : commandLineArguments)
534    {
535      msgBuffer.append(' ');
536
537      final String name = argPair.getFirst();
538      if (name.startsWith("-"))
539      {
540        msgBuffer.append(name);
541      }
542      else
543      {
544        msgBuffer.append(StaticUtils.cleanExampleCommandLineArgument(name));
545      }
546
547      final String value = argPair.getSecond();
548      if (value != null)
549      {
550        msgBuffer.append(' ');
551        msgBuffer.append(getCleanArgumentValue(name, value));
552      }
553    }
554    msgBuffer.append(StaticUtils.EOL);
555    msgBuffer.append(StaticUtils.EOL);
556
557    final byte[] logMessageBytes = StaticUtils.getBytes(msgBuffer.toString());
558
559
560    // Append the log message to each of the log files.
561    for (final File logFile : logDetails.getLogFiles())
562    {
563      logMessageToFile(logMessageBytes, logFile,
564           logDetails.getToolErrorStream());
565    }
566  }
567
568
569
570  /**
571   * Retrieves a cleaned and possibly redacted version of the provided argument
572   * value.
573   *
574   * @param  name   The name for the argument.  It must not be {@code null}.
575   * @param  value  The value for the argument.  It must not be {@code null}.
576   *
577   * @return  A cleaned and possibly redacted version of the provided argument
578   *          value.
579   */
580  private static String getCleanArgumentValue(final String name,
581                                              final String value)
582  {
583    final String lowerName = StaticUtils.toLowerCase(name);
584    if (lowerName.contains("password") ||
585       lowerName.contains("passphrase") ||
586       lowerName.endsWith("-pin") ||
587       name.endsWith("Pin") ||
588       name.endsWith("PIN"))
589    {
590      if (! (lowerName.contains("passwordfile") ||
591           lowerName.contains("password-file") ||
592           lowerName.contains("passwordpath") ||
593           lowerName.contains("password-path") ||
594           lowerName.contains("passphrasefile") ||
595           lowerName.contains("passphrase-file") ||
596           lowerName.contains("passphrasepath") ||
597           lowerName.contains("passphrase-path")))
598      {
599        if (! StaticUtils.toLowerCase(value).contains("redacted"))
600        {
601          return "'*****REDACTED*****'";
602        }
603      }
604    }
605
606    return StaticUtils.cleanExampleCommandLineArgument(value);
607  }
608
609
610
611  /**
612   * Logs a message about the completion of the specified tool.  This method
613   * must acquire an exclusive lock on each log file before attempting to append
614   * any data to it.
615   *
616   * @param  logDetails   The tool invocation log details object obtained from
617   *                      running the {@link #getLogMessageDetails} method.  It
618   *                      must not be {@code null}.
619   * @param  exitCode     An integer exit code that may be used to broadly
620   *                      indicate whether the tool completed successfully.  A
621   *                      value of zero typically indicates that it did
622   *                      complete successfully, while a nonzero value generally
623   *                      indicates that some error occurred.  This may be
624   *                      {@code null} if the tool did not complete normally
625   *                      (for example, because the tool processing was
626   *                      interrupted by a JVM shutdown).
627   * @param  exitMessage  An optional message that provides information about
628   *                      the completion of the tool processing.  It may be
629   *                      {@code null} if no such message is available.
630   */
631  public static void logCompletionMessage(
632                          final ToolInvocationLogDetails logDetails,
633                          final Integer exitCode, final String exitMessage)
634  {
635    // Build the log message.
636    final StringBuilder msgBuffer = new StringBuilder();
637    final SimpleDateFormat dateFormat =
638         new SimpleDateFormat(LOG_MESSAGE_DATE_FORMAT);
639
640    msgBuffer.append("# [");
641    msgBuffer.append(dateFormat.format(new Date()));
642    msgBuffer.append(']');
643    msgBuffer.append(StaticUtils.EOL);
644    msgBuffer.append("# Command Name: ");
645    msgBuffer.append(logDetails.getCommandName());
646    msgBuffer.append(StaticUtils.EOL);
647    msgBuffer.append("# Invocation ID: ");
648    msgBuffer.append(logDetails.getInvocationID());
649    msgBuffer.append(StaticUtils.EOL);
650
651    if (exitCode != null)
652    {
653      msgBuffer.append("# Exit Code: ");
654      msgBuffer.append(exitCode);
655      msgBuffer.append(StaticUtils.EOL);
656    }
657
658    if (exitMessage != null)
659    {
660      msgBuffer.append("# Exit Message: ");
661      cleanMessage(exitMessage, msgBuffer);
662      msgBuffer.append(StaticUtils.EOL);
663    }
664
665    msgBuffer.append(StaticUtils.EOL);
666
667    final byte[] logMessageBytes = StaticUtils.getBytes(msgBuffer.toString());
668
669
670    // Append the log message to each of the log files.
671    for (final File logFile : logDetails.getLogFiles())
672    {
673      logMessageToFile(logMessageBytes, logFile,
674           logDetails.getToolErrorStream());
675    }
676  }
677
678
679
680  /**
681   * Writes a clean representation of the provided message to the given buffer.
682   * All ASCII characters from the space to the tilde will be preserved.  All
683   * other characters will use the hexadecimal representation of the bytes that
684   * make up that character, with each pair of hexadecimal digits escaped with a
685   * backslash.
686   *
687   * @param  message  The message to be cleaned.
688   * @param  buffer   The buffer to which the message should be appended.
689   */
690  private static void cleanMessage(final String message,
691                                   final StringBuilder buffer)
692  {
693    for (final char c : message.toCharArray())
694    {
695      if ((c >= ' ') && (c <= '~'))
696      {
697        buffer.append(c);
698      }
699      else
700      {
701        for (final byte b : StaticUtils.getBytes(Character.toString(c)))
702        {
703          buffer.append('\\');
704          StaticUtils.toHex(b, buffer);
705        }
706      }
707    }
708  }
709
710
711
712  /**
713   * Acquires an exclusive lock on the specified log file and appends the
714   * provided log message to it.
715   *
716   * @param  logMessageBytes  The bytes that comprise the log message to be
717   *                          appended to the log file.
718   * @param  logFile          The log file to be locked and updated.
719   * @param  toolErrorStream  A print stream that may be used to report
720   *                          information about any problems encountered while
721   *                          attempting to perform invocation logging.  It
722   *                          must not be {@code null}.
723   */
724  private static void logMessageToFile(final byte[] logMessageBytes,
725                                       final File logFile,
726                                       final PrintStream toolErrorStream)
727  {
728    // Open a file channel for the target log file.
729    final Set<StandardOpenOption> openOptionsSet = EnumSet.of(
730            StandardOpenOption.CREATE, // Create the file if it doesn't exist.
731            StandardOpenOption.APPEND, // Append to file if it already exists.
732            StandardOpenOption.DSYNC); // Synchronously flush file on writing.
733
734    final FileAttribute<?>[] fileAttributes;
735    if (StaticUtils.isWindows())
736    {
737      fileAttributes = new FileAttribute<?>[0];
738    }
739    else
740    {
741      final Set<PosixFilePermission> filePermissionsSet = EnumSet.of(
742              PosixFilePermission.OWNER_READ,   // Grant owner read access.
743              PosixFilePermission.OWNER_WRITE); // Grant owner write access.
744      final FileAttribute<Set<PosixFilePermission>> filePermissionsAttribute =
745              PosixFilePermissions.asFileAttribute(filePermissionsSet);
746      fileAttributes = new FileAttribute<?>[] { filePermissionsAttribute };
747    }
748
749    try (final FileChannel fileChannel =
750              FileChannel.open(logFile.toPath(), openOptionsSet,
751                   fileAttributes))
752    {
753      try (final FileLock fileLock =
754                acquireFileLock(fileChannel, logFile, toolErrorStream))
755      {
756        if (fileLock != null)
757        {
758          try
759          {
760            fileChannel.write(ByteBuffer.wrap(logMessageBytes));
761          }
762          catch (final Exception e)
763          {
764            Debug.debugException(e);
765            printError(
766                 ERR_TOOL_LOGGER_ERROR_WRITING_LOG_MESSAGE.get(
767                      logFile.getAbsolutePath(),
768                      StaticUtils.getExceptionMessage(e)),
769                 toolErrorStream);
770          }
771        }
772      }
773    }
774    catch (final Exception e)
775    {
776      Debug.debugException(e);
777      printError(
778           ERR_TOOL_LOGGER_ERROR_OPENING_LOG_FILE.get(logFile.getAbsolutePath(),
779                StaticUtils.getExceptionMessage(e)),
780           toolErrorStream);
781    }
782  }
783
784
785
786  /**
787   * Attempts to acquire an exclusive file lock on the provided file channel.
788   *
789   * @param  fileChannel      The file channel on which to acquire the file
790   *                          lock.
791   * @param  logFile          The path to the log file being locked.
792   * @param  toolErrorStream  A print stream that may be used to report
793   *                          information about any problems encountered while
794   *                          attempting to perform invocation logging.  It
795   *                          must not be {@code null}.
796   *
797   * @return  The file lock that was acquired, or {@code null} if the lock could
798   *          not be acquired.
799   */
800  private static FileLock acquireFileLock(final FileChannel fileChannel,
801                                          final File logFile,
802                                          final PrintStream toolErrorStream)
803  {
804    try
805    {
806      final FileLock fileLock = fileChannel.tryLock();
807      if (fileLock != null)
808      {
809        return fileLock;
810      }
811    }
812    catch (final Exception e)
813    {
814      Debug.debugException(e);
815    }
816
817    int numAttempts = 1;
818    final long stopWaitingTime = System.currentTimeMillis() + 1000L;
819    while (System.currentTimeMillis() <= stopWaitingTime)
820    {
821      try
822      {
823        Thread.sleep(10L);
824        final FileLock fileLock = fileChannel.tryLock();
825        if (fileLock != null)
826        {
827          return fileLock;
828        }
829      }
830      catch (final Exception e)
831      {
832        Debug.debugException(e);
833      }
834
835      numAttempts++;
836    }
837
838    printError(
839         ERR_TOOL_LOGGER_UNABLE_TO_ACQUIRE_FILE_LOCK.get(
840              logFile.getAbsolutePath(), numAttempts),
841         toolErrorStream);
842    return null;
843  }
844
845
846
847  /**
848   * Prints the provided message using the tool output stream.  The message will
849   * be wrapped across multiple lines if necessary, and each line will be
850   * prefixed with the octothorpe character (#) so that it is likely to be
851   * interpreted as a comment by anything that tries to parse the tool output.
852   *
853   * @param  message          The message to be written.
854   * @param  toolErrorStream  The print stream that should be used to write the
855   *                          message.
856   */
857  private static void printError(final String message,
858                                 final PrintStream toolErrorStream)
859  {
860    toolErrorStream.println();
861
862    final int maxWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 3;
863    for (final String line : StaticUtils.wrapLine(message, maxWidth))
864    {
865      toolErrorStream.println("# " + line);
866    }
867  }
868}