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}