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