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