CommandLineToolkit
251 строка · 6.7 Кб
1import Foundation
2import Logging
3import TeamcityMessaging
4
5public final class TeamcityConsoleHandler: ConsoleHandler {
6struct ConsoleError: Error {
7enum Reason {
8case unsupportedOperation
9}
10
11var reason: Reason
12var file: StaticString
13var line: UInt
14}
15
16let terminal: ANSITerminal
17
18public var isAtTTY: Bool {
19return isatty(STDOUT_FILENO) > 0
20}
21
22public var isInteractive: Bool {
23#if Xcode
24return false
25#else
26return isAtTTY
27#endif
28}
29
30public var verbositySettings: ConsoleVerbositySettings
31
32private let messageGenerator: TeamcityMessageGenerator
33private let messageRenderer: TeamcityMessageRenderer
34
35public init(
36terminal: ANSITerminal = .shared,
37verbositySettings: ConsoleVerbositySettings = .default,
38messageGenerator: TeamcityMessageGenerator,
39messageRenderer: TeamcityMessageRenderer
40) {
41self.terminal = terminal
42self.verbositySettings = verbositySettings
43self.messageGenerator = messageGenerator
44self.messageRenderer = messageRenderer
45}
46
47public func input(
48title: String,
49defaultValue: String?,
50file: StaticString,
51line: UInt
52) async throws -> String {
53throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
54}
55
56public func question(
57title: String,
58defaultAnswer: Bool,
59file: StaticString,
60line: UInt
61) async throws -> Bool {
62throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
63}
64
65public func select<Value>(
66title: String,
67values: [Selectable<Value>],
68mode: SelectionMode,
69options: SelectionOptions,
70file: StaticString,
71line: UInt
72) async throws -> [Value] {
73throw ConsoleError(reason: .unsupportedOperation, file: file, line: line)
74}
75
76public func trace<Value: Sendable>(
77level: Logging.Logger.Level,
78name: String,
79options: TraceOptions,
80file: StaticString,
81line: UInt,
82work: (any TraceProgressUpdator) async throws -> Value
83) async throws -> Value {
84let flowId = UUID()
85
86log(controlMessage: messageGenerator.flowStarted(
87name: name,
88timestamp: Date(),
89flowId: flowId.uuidString,
90parentFlowId: ConsoleContext.current.activeFlow?.uuidString
91))
92
93defer {
94log(controlMessage: messageGenerator.flowFinished(timestamp: Date(), flowId: flowId.uuidString))
95}
96
97log(controlMessage: messageGenerator.blockOpenend(
98name: name,
99timestamp: Date(),
100flowId: flowId.uuidString
101))
102
103defer {
104log(controlMessage: messageGenerator.blockClosed(
105name: name,
106timestamp: Date(),
107flowId: flowId.uuidString
108))
109}
110
111return try await ConsoleContext.$current.withUpdated(key: \.activeFlow, value: flowId) {
112return try await work(NoOpTraceProgressUpdator())
113}
114}
115
116public func log(
117level: Logging.Logger.Level,
118message: Logging.Logger.Message,
119metadata: Logging.Logger.Metadata,
120source: String,
121file: String,
122function: String,
123line: UInt
124) {
125guard level >= self.verbositySettings.logLevel else { return }
126log(controlMessage: messageGenerator.message(
127text: "\(level.teamcityLevelPrefix): \(message.description)" + (prettify(metadata).map { "\n\($0)" } ?? ""),
128status: MessageStatus(level: level),
129timestamp: Date(),
130flowId: ConsoleContext.current.activeFlow?.uuidString
131))
132}
133
134private func prettify(_ metadata: Logger.Metadata) -> String? {
135return !metadata.isEmpty
136? metadata.lazy.sorted(by: { $0.key < $1.key }).map { "\($0)=\($1)" }.joined(separator: " ")
137: nil
138}
139
140public func logStream(
141level: Logging.Logger.Level,
142name: String,
143renderTail: Int,
144file: StaticString,
145line: UInt
146) -> any LogSink {
147let flowId = UUID()
148
149log(controlMessage: messageGenerator.flowStarted(
150name: name,
151timestamp: Date(),
152flowId: flowId.uuidString,
153parentFlowId: ConsoleContext.current.activeFlow?.uuidString
154))
155
156log(controlMessage: messageGenerator.blockOpenend(
157name: name,
158timestamp: Date(),
159flowId: flowId.uuidString
160))
161
162return TeamcityLogSink(flowId: flowId) { message in
163self.log(controlMessage: self.messageGenerator.message(
164text: message,
165status: MessageStatus(level: level),
166timestamp: Date(),
167flowId: flowId.uuidString
168))
169} end: {
170self.log(controlMessage: self.messageGenerator.blockClosed(
171name: name,
172timestamp: Date(),
173flowId: flowId.uuidString
174))
175self.log(controlMessage: self.messageGenerator.flowFinished(timestamp: Date(), flowId: flowId.uuidString))
176}
177}
178
179private func log(controlMessage: ControlMessage) {
180guard let message = try? messageRenderer.renderControlMessage(controlMessage: controlMessage) else {
181return
182}
183terminal.writeln(message)
184}
185}
186
187extension ConsoleContext {
188private enum ActiveFlow: ConsoleContextKey {
189static let defaultValue: UUID? = nil
190}
191
192var activeFlow: UUID? {
193get { self[ActiveFlow.self] }
194set { self[ActiveFlow.self] = newValue }
195}
196}
197
198private struct TeamcityLogSink: LogSink {
199let flowId: UUID
200
201let log: (String) -> ()
202let end: () -> ()
203
204func append(line: String) {
205log(line)
206}
207
208func append(lines: [String]) {
209for line in lines {
210log(line)
211}
212}
213
214func finish(result: Result<Void, LogStreamError>, cancelled: Bool) {
215end()
216}
217}
218
219private extension Logger.Level {
220var teamcityLevelPrefix: String {
221switch self {
222case .trace:
223"TRACE"
224case .debug:
225"DEBUG"
226case .info:
227"INFO"
228case .notice:
229"NOTICE"
230case .warning:
231"WARNING"
232case .error:
233"ERROR"
234case .critical:
235"CRITICAL"
236}
237}
238}
239
240private extension MessageStatus {
241init(level: Logging.Logger.Level) {
242switch level {
243case .trace, .debug, .info:
244self = .normal
245case .notice, .warning:
246self = .warning
247case .error, .critical:
248self = .failure
249}
250}
251}
252