CommandLineToolkit

Форк
0
251 строка · 6.7 Кб
1
import Foundation
2
import Logging
3
import TeamcityMessaging
4

5
public final class TeamcityConsoleHandler: ConsoleHandler {
6
    struct ConsoleError: Error {
7
        enum Reason {
8
            case unsupportedOperation
9
        }
10

11
        var reason: Reason
12
        var file: StaticString
13
        var line: UInt
14
    }
15

16
    let terminal: ANSITerminal
17

18
    public var isAtTTY: Bool {
19
        return isatty(STDOUT_FILENO) > 0
20
    }
21

22
    public var isInteractive: Bool {
23
        #if Xcode
24
        return false
25
        #else
26
        return isAtTTY
27
        #endif
28
    }
29

30
    public var verbositySettings: ConsoleVerbositySettings
31
    
32
    private let messageGenerator: TeamcityMessageGenerator
33
    private let messageRenderer: TeamcityMessageRenderer
34

35
    public init(
36
        terminal: ANSITerminal = .shared,
37
        verbositySettings: ConsoleVerbositySettings = .default,
38
        messageGenerator: TeamcityMessageGenerator,
39
        messageRenderer: TeamcityMessageRenderer
40
    ) {
41
        self.terminal = terminal
42
        self.verbositySettings = verbositySettings
43
        self.messageGenerator = messageGenerator
44
        self.messageRenderer = messageRenderer
45
    }
46

47
    public func input(
48
        title: String,
49
        defaultValue: String?,
50
        file: StaticString,
51
        line: UInt
52
    ) async throws -> String {
53
        throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
54
    }
55
    
56
    public func question(
57
        title: String,
58
        defaultAnswer: Bool,
59
        file: StaticString,
60
        line: UInt
61
    ) async throws -> Bool {
62
        throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
63
    }
64
    
65
    public func select<Value>(
66
        title: String,
67
        values: [Selectable<Value>],
68
        mode: SelectionMode,
69
        options: SelectionOptions,
70
        file: StaticString,
71
        line: UInt
72
    ) async throws -> [Value] {
73
        throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
74
    }
75
    
76
    public func trace<Value: Sendable>(
77
        level: Logging.Logger.Level,
78
        name: String,
79
        options: TraceOptions,
80
        file: StaticString,
81
        line: UInt,
82
        work: (any TraceProgressUpdator) async throws -> Value
83
    ) async throws -> Value {
84
        let flowId = UUID()
85

86
        log(controlMessage: messageGenerator.flowStarted(
87
            name: name,
88
            timestamp: Date(),
89
            flowId: flowId.uuidString,
90
            parentFlowId: ConsoleContext.current.activeFlow?.uuidString
91
        ))
92

93
        defer {
94
            log(controlMessage: messageGenerator.flowFinished(timestamp: Date(), flowId: flowId.uuidString))
95
        }
96

97
        log(controlMessage: messageGenerator.blockOpenend(
98
            name: name,
99
            timestamp: Date(),
100
            flowId: flowId.uuidString
101
        ))
102

103
        defer {
104
            log(controlMessage: messageGenerator.blockClosed(
105
                name: name,
106
                timestamp: Date(),
107
                flowId: flowId.uuidString
108
            ))
109
        }
110

111
        return try await ConsoleContext.$current.withUpdated(key: \.activeFlow, value: flowId) {
112
            return try await work(NoOpTraceProgressUpdator())
113
        }
114
    }
115

116
    public func log(
117
        level: Logging.Logger.Level,
118
        message: Logging.Logger.Message,
119
        metadata: Logging.Logger.Metadata,
120
        source: String,
121
        file: String,
122
        function: String,
123
        line: UInt
124
    ) {
125
        guard level >= self.verbositySettings.logLevel else { return }
126
        log(controlMessage: messageGenerator.message(
127
            text: "\(level.teamcityLevelPrefix): \(message.description)" + (prettify(metadata).map { "\n\($0)" } ?? ""),
128
            status: MessageStatus(level: level),
129
            timestamp: Date(),
130
            flowId: ConsoleContext.current.activeFlow?.uuidString
131
        ))
132
    }
133

134
    private func prettify(_ metadata: Logger.Metadata) -> String? {
135
        return !metadata.isEmpty
136
            ? metadata.lazy.sorted(by: { $0.key < $1.key }).map { "\($0)=\($1)" }.joined(separator: " ")
137
            : nil
138
    }
139

140
    public func logStream(
141
        level: Logging.Logger.Level,
142
        name: String,
143
        renderTail: Int,
144
        file: StaticString,
145
        line: UInt
146
    ) -> any LogSink {
147
        let flowId = UUID()
148

149
        log(controlMessage: messageGenerator.flowStarted(
150
            name: name,
151
            timestamp: Date(),
152
            flowId: flowId.uuidString,
153
            parentFlowId: ConsoleContext.current.activeFlow?.uuidString
154
        ))
155

156
        log(controlMessage: messageGenerator.blockOpenend(
157
            name: name,
158
            timestamp: Date(),
159
            flowId: flowId.uuidString
160
        ))
161

162
        return TeamcityLogSink(flowId: flowId) { message in
163
            self.log(controlMessage: self.messageGenerator.message(
164
                text: message,
165
                status: MessageStatus(level: level),
166
                timestamp: Date(),
167
                flowId: flowId.uuidString
168
            ))
169
        } end: {
170
            self.log(controlMessage: self.messageGenerator.blockClosed(
171
                name: name,
172
                timestamp: Date(),
173
                flowId: flowId.uuidString
174
            ))
175
            self.log(controlMessage: self.messageGenerator.flowFinished(timestamp: Date(), flowId: flowId.uuidString))
176
        }
177
    }
178

179
    private func log(controlMessage: ControlMessage) {
180
        guard let message = try? messageRenderer.renderControlMessage(controlMessage: controlMessage) else {
181
            return
182
        }
183
        terminal.writeln(message)
184
    }
185
}
186

187
extension ConsoleContext {
188
    private enum ActiveFlow: ConsoleContextKey {
189
        static let defaultValue: UUID? = nil
190
    }
191

192
    var activeFlow: UUID? {
193
        get { self[ActiveFlow.self] }
194
        set { self[ActiveFlow.self] = newValue }
195
    }
196
}
197

198
private struct TeamcityLogSink: LogSink {
199
    let flowId: UUID
200

201
    let log: (String) -> ()
202
    let end: () -> ()
203

204
    func append(line: String) {
205
        log(line)
206
    }
207

208
    func append(lines: [String]) {
209
        for line in lines {
210
            log(line)
211
        }
212
    }
213

214
    func finish(result: Result<Void, LogStreamError>, cancelled: Bool) {
215
        end()
216
    }
217
}
218

219
private extension Logger.Level {
220
    var teamcityLevelPrefix: String {
221
        switch self {
222
        case .trace:
223
            "TRACE"
224
        case .debug:
225
            "DEBUG"
226
        case .info:
227
            "INFO"
228
        case .notice:
229
            "NOTICE"
230
        case .warning:
231
            "WARNING"
232
        case .error:
233
            "ERROR"
234
        case .critical:
235
            "CRITICAL"
236
        }
237
    }
238
}
239

240
private extension MessageStatus {
241
    init(level: Logging.Logger.Level) {
242
        switch level {
243
        case .trace, .debug, .info:
244
            self = .normal
245
        case .notice, .warning:
246
            self = .warning
247
        case .error, .critical:
248
            self = .failure
249
        }
250
    }
251
}
252

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.