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