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