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