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