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