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