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