|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Swift.org open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors |
| 6 | +// Licensed under Apache License v2.0 with Runtime Library Exception |
| 7 | +// |
| 8 | +// See https://swift.org/LICENSE.txt for license information |
| 9 | +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors |
| 10 | +// |
| 11 | +//===----------------------------------------------------------------------===// |
| 12 | + |
| 13 | +import Foundation |
| 14 | +import RegexBuilder |
| 15 | + |
| 16 | +#if !canImport(os) || SOURCEKITLSP_FORCE_NON_DARWIN_LOGGER |
| 17 | +fileprivate struct FailedToCreateFileError: Error, CustomStringConvertible { |
| 18 | + let logFile: URL |
| 19 | + |
| 20 | + var description: String { |
| 21 | + return "Failed to create log file at \(logFile)" |
| 22 | + } |
| 23 | +} |
| 24 | +/// Creates a file handle that can write to the log file at `logFilePath`. |
| 25 | +/// |
| 26 | +/// If creation of the file handle fails (eg. because the file is not writable), return `nil`. |
| 27 | +private func createLogFileHandle(at logFile: URL) throws -> FileHandle { |
| 28 | + if !FileManager.default.fileExists(atPath: logFile.path) { |
| 29 | + guard FileManager.default.createFile(atPath: logFile.path, contents: nil) else { |
| 30 | + throw FailedToCreateFileError(logFile: logFile) |
| 31 | + } |
| 32 | + } |
| 33 | + let logFileHandle = try FileHandle(forWritingTo: logFile) |
| 34 | + try logFileHandle.seekToEnd() |
| 35 | + return logFileHandle |
| 36 | +} |
| 37 | + |
| 38 | +/// The number of log calls that have been made to the log file handler. |
| 39 | +/// |
| 40 | +/// See comment in `logImpl`. |
| 41 | +@LogHandlerActor |
| 42 | +fileprivate var logCallsCounter = 0 |
| 43 | + |
| 44 | +/// Log the given message to the given log file handle. |
| 45 | +/// |
| 46 | +/// Occasionally check if the log file exceeds the target log size. If this is the case, truncate the beginning of the |
| 47 | +/// log to reduce the log size. |
| 48 | +@LogHandlerActor |
| 49 | +private func logToFile(message: String, logFileHandle: FileHandle, logFile: URL, targetLogSize: Int) throws { |
| 50 | + guard let data = message.data(using: .utf8) else { |
| 51 | + fputs( |
| 52 | + """ |
| 53 | + Failed to convert log message to UTF-8 data |
| 54 | + \(message) |
| 55 | +
|
| 56 | + """, |
| 57 | + stderr |
| 58 | + ) |
| 59 | + return |
| 60 | + } |
| 61 | + try logFileHandle.write(contentsOf: data) |
| 62 | + logCallsCounter &+= 1 |
| 63 | + |
| 64 | + // Every `targetLogSize / 10,000` calls, check if the log file exceeds the maximum size. If this is the case, reduce |
| 65 | + // the log file to 80% of the maximum size by trimming the beginning to allow room for some more log messages. |
| 66 | + // 10,000 is the maximum log message size (capped by `NonDarwinLogger.log`). This means that the log will never exceed |
| 67 | + // the target size by more than 80%: The log might be 80% of the maximum size to start with and we might add up to |
| 68 | + // `targetLogSize` bytes to it until the next check. |
| 69 | + // For a target log size of 5MB, this means that we check the log for its size every 500 log calls. |
| 70 | + if logCallsCounter.isMultiple(of: targetLogSize / 10_000), |
| 71 | + let size = try? FileManager.default.attributesOfItem(atPath: logFile.path)[.size] as? Int, size > targetLogSize, |
| 72 | + let currentContents = try? Data(contentsOf: logFile) |
| 73 | + { |
| 74 | + let trimmedLogContents = currentContents[ |
| 75 | + currentContents.index(currentContents.endIndex, offsetBy: -targetLogSize / 10 * 8)... |
| 76 | + ] |
| 77 | + try trimmedLogContents.write(to: logFile) |
| 78 | + try logFileHandle.seekToEnd() |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +/// If the file at the given path is writable, redirect log messages handled by `NonDarwinLogHandler` to the given file. |
| 83 | +/// |
| 84 | +/// Occasionally checks that the log does not exceed `targetLogSize` (in bytes) and truncates the beginning of the log |
| 85 | +/// when it does. |
| 86 | +@LogHandlerActor |
| 87 | +private func setUpGlobalLogFileHandlerImpl(logFileDirectory: URL, targetLogSize: Int) { |
| 88 | + let logFile: URL |
| 89 | + let logFileHandle: FileHandle |
| 90 | + do { |
| 91 | + try FileManager.default.createDirectory(at: logFileDirectory, withIntermediateDirectories: true) |
| 92 | + // Name must match the regex in `cleanOldLogFiles` and the prefix in `DiagnoseCommand.addNonDarwinLogs`. |
| 93 | + logFile = logFileDirectory.appendingPathComponent("sourcekit-lsp-\(ProcessInfo.processInfo.processIdentifier).log") |
| 94 | + logFileHandle = try createLogFileHandle(at: logFile) |
| 95 | + } catch { |
| 96 | + // We still have a log handler that logs to stderr. So we can log any errors during log file handle creation to it. |
| 97 | + logger.error("Failed to create log file in \(logFileDirectory): \(error.forLogging)") |
| 98 | + return |
| 99 | + } |
| 100 | + |
| 101 | + logHandler = { @LogHandlerActor message in |
| 102 | + do { |
| 103 | + try logToFile( |
| 104 | + message: message, |
| 105 | + logFileHandle: logFileHandle, |
| 106 | + logFile: logFile, |
| 107 | + targetLogSize: targetLogSize |
| 108 | + ) |
| 109 | + } catch { |
| 110 | + fputs( |
| 111 | + """ |
| 112 | + Failed to write message to log file: \(error) |
| 113 | + \(message) |
| 114 | +
|
| 115 | + """, |
| 116 | + stderr |
| 117 | + ) |
| 118 | + } |
| 119 | + } |
| 120 | +} |
| 121 | + |
| 122 | +private func cleanOldLogFilesImpl(logFileDirectory: URL) { |
| 123 | + let enumerator = FileManager.default.enumerator(at: logFileDirectory, includingPropertiesForKeys: nil) |
| 124 | + while let url = enumerator?.nextObject() as? URL { |
| 125 | + let name = url.lastPathComponent |
| 126 | + let regex = Regex { |
| 127 | + "sourcekit-lsp-" |
| 128 | + Capture(ZeroOrMore(.digit)) |
| 129 | + ".log" |
| 130 | + } |
| 131 | + guard let match = name.matches(of: regex).only, let pid = Int32(match.1) else { |
| 132 | + continue |
| 133 | + } |
| 134 | + if kill(pid, 0) == 0 { |
| 135 | + // Process that owns this log file is still alive. Don't delete |
| 136 | + continue |
| 137 | + } |
| 138 | + guard |
| 139 | + let modificationDate = orLog( |
| 140 | + "Getting mtime of old log file", |
| 141 | + { try FileManager.default.attributesOfItem(atPath: url.path)[.modificationDate] } |
| 142 | + ) as? Date, |
| 143 | + Date().timeIntervalSince(modificationDate) > 60 * 60 |
| 144 | + else { |
| 145 | + // File has been modified in the last hour. Don't delete it because it's useful to diagnose issues after |
| 146 | + // sourcekit-lsp has exited. |
| 147 | + continue |
| 148 | + } |
| 149 | + orLog("Deleting old log file") { try FileManager.default.removeItem(at: url) } |
| 150 | + } |
| 151 | +} |
| 152 | +#endif |
| 153 | + |
| 154 | +/// If the file at the given path is writable, redirect log messages handled by `NonDarwinLogHandler` to the given file. |
| 155 | +/// |
| 156 | +/// Occasionally checks that the log does not exceed `targetLogSize` (in bytes) and truncates the beginning of the log |
| 157 | +/// when it does. |
| 158 | +/// |
| 159 | +/// No-op when using OSLog. |
| 160 | +public func setUpGlobalLogFileHandler(logFileDirectory: URL, targetLogSize: Int) async { |
| 161 | + #if !canImport(os) || SOURCEKITLSP_FORCE_NON_DARWIN_LOGGER |
| 162 | + await setUpGlobalLogFileHandlerImpl(logFileDirectory: logFileDirectory, targetLogSize: targetLogSize) |
| 163 | + #endif |
| 164 | +} |
| 165 | + |
| 166 | +/// Deletes all sourcekit-lsp log files in `logFilesDirectory` that are not associated with a running process and that |
| 167 | +/// haven't been modified within the last hour. |
| 168 | +/// |
| 169 | +/// No-op when using OSLog. |
| 170 | +public func cleanOldLogFiles(logFileDirectory: URL) { |
| 171 | + #if !canImport(os) || SOURCEKITLSP_FORCE_NON_DARWIN_LOGGER |
| 172 | + cleanOldLogFilesImpl(logFileDirectory: logFileDirectory) |
| 173 | + #endif |
| 174 | +} |
0 commit comments