001/* 002 * Copyright 2017-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2017-2019 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 062 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 063 * for proprietary functionality or for external specifications that are not 064 * considered stable or mature enough to be guaranteed to work in an 065 * interoperable way with 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<>(StaticUtils.computeMapCapacity(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.isEmpty())) 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(getCleanArgumentValue(name, 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(getCleanArgumentValue(name, 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 * Retrieves a cleaned and possibly redacted version of the provided argument 572 * value. 573 * 574 * @param name The name for the argument. It must not be {@code null}. 575 * @param value The value for the argument. It must not be {@code null}. 576 * 577 * @return A cleaned and possibly redacted version of the provided argument 578 * value. 579 */ 580 private static String getCleanArgumentValue(final String name, 581 final String value) 582 { 583 final String lowerName = StaticUtils.toLowerCase(name); 584 if (lowerName.contains("password") || 585 lowerName.contains("passphrase") || 586 lowerName.endsWith("-pin") || 587 name.endsWith("Pin") || 588 name.endsWith("PIN")) 589 { 590 if (! (lowerName.contains("passwordfile") || 591 lowerName.contains("password-file") || 592 lowerName.contains("passwordpath") || 593 lowerName.contains("password-path") || 594 lowerName.contains("passphrasefile") || 595 lowerName.contains("passphrase-file") || 596 lowerName.contains("passphrasepath") || 597 lowerName.contains("passphrase-path"))) 598 { 599 if (! StaticUtils.toLowerCase(value).contains("redacted")) 600 { 601 return "'*****REDACTED*****'"; 602 } 603 } 604 } 605 606 return StaticUtils.cleanExampleCommandLineArgument(value); 607 } 608 609 610 611 /** 612 * Logs a message about the completion of the specified tool. This method 613 * must acquire an exclusive lock on each log file before attempting to append 614 * any data to it. 615 * 616 * @param logDetails The tool invocation log details object obtained from 617 * running the {@link #getLogMessageDetails} method. It 618 * must not be {@code null}. 619 * @param exitCode An integer exit code that may be used to broadly 620 * indicate whether the tool completed successfully. A 621 * value of zero typically indicates that it did 622 * complete successfully, while a nonzero value generally 623 * indicates that some error occurred. This may be 624 * {@code null} if the tool did not complete normally 625 * (for example, because the tool processing was 626 * interrupted by a JVM shutdown). 627 * @param exitMessage An optional message that provides information about 628 * the completion of the tool processing. It may be 629 * {@code null} if no such message is available. 630 */ 631 public static void logCompletionMessage( 632 final ToolInvocationLogDetails logDetails, 633 final Integer exitCode, final String exitMessage) 634 { 635 // Build the log message. 636 final StringBuilder msgBuffer = new StringBuilder(); 637 final SimpleDateFormat dateFormat = 638 new SimpleDateFormat(LOG_MESSAGE_DATE_FORMAT); 639 640 msgBuffer.append("# ["); 641 msgBuffer.append(dateFormat.format(new Date())); 642 msgBuffer.append(']'); 643 msgBuffer.append(StaticUtils.EOL); 644 msgBuffer.append("# Command Name: "); 645 msgBuffer.append(logDetails.getCommandName()); 646 msgBuffer.append(StaticUtils.EOL); 647 msgBuffer.append("# Invocation ID: "); 648 msgBuffer.append(logDetails.getInvocationID()); 649 msgBuffer.append(StaticUtils.EOL); 650 651 if (exitCode != null) 652 { 653 msgBuffer.append("# Exit Code: "); 654 msgBuffer.append(exitCode); 655 msgBuffer.append(StaticUtils.EOL); 656 } 657 658 if (exitMessage != null) 659 { 660 msgBuffer.append("# Exit Message: "); 661 cleanMessage(exitMessage, msgBuffer); 662 msgBuffer.append(StaticUtils.EOL); 663 } 664 665 msgBuffer.append(StaticUtils.EOL); 666 667 final byte[] logMessageBytes = StaticUtils.getBytes(msgBuffer.toString()); 668 669 670 // Append the log message to each of the log files. 671 for (final File logFile : logDetails.getLogFiles()) 672 { 673 logMessageToFile(logMessageBytes, logFile, 674 logDetails.getToolErrorStream()); 675 } 676 } 677 678 679 680 /** 681 * Writes a clean representation of the provided message to the given buffer. 682 * All ASCII characters from the space to the tilde will be preserved. All 683 * other characters will use the hexadecimal representation of the bytes that 684 * make up that character, with each pair of hexadecimal digits escaped with a 685 * backslash. 686 * 687 * @param message The message to be cleaned. 688 * @param buffer The buffer to which the message should be appended. 689 */ 690 private static void cleanMessage(final String message, 691 final StringBuilder buffer) 692 { 693 for (final char c : message.toCharArray()) 694 { 695 if ((c >= ' ') && (c <= '~')) 696 { 697 buffer.append(c); 698 } 699 else 700 { 701 for (final byte b : StaticUtils.getBytes(Character.toString(c))) 702 { 703 buffer.append('\\'); 704 StaticUtils.toHex(b, buffer); 705 } 706 } 707 } 708 } 709 710 711 712 /** 713 * Acquires an exclusive lock on the specified log file and appends the 714 * provided log message to it. 715 * 716 * @param logMessageBytes The bytes that comprise the log message to be 717 * appended to the log file. 718 * @param logFile The log file to be locked and updated. 719 * @param toolErrorStream A print stream that may be used to report 720 * information about any problems encountered while 721 * attempting to perform invocation logging. It 722 * must not be {@code null}. 723 */ 724 private static void logMessageToFile(final byte[] logMessageBytes, 725 final File logFile, 726 final PrintStream toolErrorStream) 727 { 728 // Open a file channel for the target log file. 729 final Set<StandardOpenOption> openOptionsSet = EnumSet.of( 730 StandardOpenOption.CREATE, // Create the file if it doesn't exist. 731 StandardOpenOption.APPEND, // Append to file if it already exists. 732 StandardOpenOption.DSYNC); // Synchronously flush file on writing. 733 734 final FileAttribute<?>[] fileAttributes; 735 if (StaticUtils.isWindows()) 736 { 737 fileAttributes = new FileAttribute<?>[0]; 738 } 739 else 740 { 741 final Set<PosixFilePermission> filePermissionsSet = EnumSet.of( 742 PosixFilePermission.OWNER_READ, // Grant owner read access. 743 PosixFilePermission.OWNER_WRITE); // Grant owner write access. 744 final FileAttribute<Set<PosixFilePermission>> filePermissionsAttribute = 745 PosixFilePermissions.asFileAttribute(filePermissionsSet); 746 fileAttributes = new FileAttribute<?>[] { filePermissionsAttribute }; 747 } 748 749 try (FileChannel fileChannel = 750 FileChannel.open(logFile.toPath(), openOptionsSet, 751 fileAttributes)) 752 { 753 try (FileLock fileLock = 754 acquireFileLock(fileChannel, logFile, toolErrorStream)) 755 { 756 if (fileLock != null) 757 { 758 try 759 { 760 fileChannel.write(ByteBuffer.wrap(logMessageBytes)); 761 } 762 catch (final Exception e) 763 { 764 Debug.debugException(e); 765 printError( 766 ERR_TOOL_LOGGER_ERROR_WRITING_LOG_MESSAGE.get( 767 logFile.getAbsolutePath(), 768 StaticUtils.getExceptionMessage(e)), 769 toolErrorStream); 770 } 771 } 772 } 773 } 774 catch (final Exception e) 775 { 776 Debug.debugException(e); 777 printError( 778 ERR_TOOL_LOGGER_ERROR_OPENING_LOG_FILE.get(logFile.getAbsolutePath(), 779 StaticUtils.getExceptionMessage(e)), 780 toolErrorStream); 781 } 782 } 783 784 785 786 /** 787 * Attempts to acquire an exclusive file lock on the provided file channel. 788 * 789 * @param fileChannel The file channel on which to acquire the file 790 * lock. 791 * @param logFile The path to the log file being locked. 792 * @param toolErrorStream A print stream that may be used to report 793 * information about any problems encountered while 794 * attempting to perform invocation logging. It 795 * must not be {@code null}. 796 * 797 * @return The file lock that was acquired, or {@code null} if the lock could 798 * not be acquired. 799 */ 800 private static FileLock acquireFileLock(final FileChannel fileChannel, 801 final File logFile, 802 final PrintStream toolErrorStream) 803 { 804 try 805 { 806 final FileLock fileLock = fileChannel.tryLock(); 807 if (fileLock != null) 808 { 809 return fileLock; 810 } 811 } 812 catch (final Exception e) 813 { 814 Debug.debugException(e); 815 } 816 817 int numAttempts = 1; 818 final long stopWaitingTime = System.currentTimeMillis() + 1000L; 819 while (System.currentTimeMillis() <= stopWaitingTime) 820 { 821 try 822 { 823 Thread.sleep(10L); 824 final FileLock fileLock = fileChannel.tryLock(); 825 if (fileLock != null) 826 { 827 return fileLock; 828 } 829 } 830 catch (final Exception e) 831 { 832 Debug.debugException(e); 833 } 834 835 numAttempts++; 836 } 837 838 printError( 839 ERR_TOOL_LOGGER_UNABLE_TO_ACQUIRE_FILE_LOCK.get( 840 logFile.getAbsolutePath(), numAttempts), 841 toolErrorStream); 842 return null; 843 } 844 845 846 847 /** 848 * Prints the provided message using the tool output stream. The message will 849 * be wrapped across multiple lines if necessary, and each line will be 850 * prefixed with the octothorpe character (#) so that it is likely to be 851 * interpreted as a comment by anything that tries to parse the tool output. 852 * 853 * @param message The message to be written. 854 * @param toolErrorStream The print stream that should be used to write the 855 * message. 856 */ 857 private static void printError(final String message, 858 final PrintStream toolErrorStream) 859 { 860 toolErrorStream.println(); 861 862 final int maxWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 3; 863 for (final String line : StaticUtils.wrapLine(message, maxWidth)) 864 { 865 toolErrorStream.println("# " + line); 866 } 867 } 868}