Skip to content

Commit 06bc674

Browse files
authored
Add progress reporter protocol (#394)
* Json Progress File * Improve JSON progress file handling and error reporting * Refactor progress file parsing and improve test validation * Progress Reporter Protocol
1 parent bce44d7 commit 06bc674

File tree

5 files changed

+251
-149
lines changed

5 files changed

+251
-149
lines changed

Sources/Swiftly/Install.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import Foundation
44
import SwiftlyCore
55
import SystemPackage
66
@preconcurrency import TSCBasic
7-
import TSCUtility
87

98
struct Install: SwiftlyCommand {
109
public static let configuration = CommandConfiguration(
@@ -313,16 +312,16 @@ struct Install: SwiftlyCommand {
313312
}
314313
}
315314

316-
let animation: ProgressAnimationProtocol =
315+
let animation: ProgressReporterProtocol =
317316
if let progressFile
318317
{
319318
try JsonFileProgressReporter(ctx, filePath: progressFile)
320319
} else {
321-
PercentProgressAnimation(stream: stdoutStream, header: "Downloading \(version)")
320+
ConsoleProgressReporter(stream: stdoutStream, header: "Downloading \(version)")
322321
}
323322

324323
defer {
325-
try? (animation as? JsonFileProgressReporter)?.close()
324+
try? animation.close()
326325
}
327326

328327
var lastUpdate = Date()
@@ -351,22 +350,28 @@ struct Install: SwiftlyCommand {
351350

352351
lastUpdate = Date()
353352

354-
animation.update(
355-
step: progress.receivedBytes,
356-
total: progress.totalBytes!,
357-
text:
358-
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
359-
)
353+
do {
354+
try await animation.update(
355+
step: progress.receivedBytes,
356+
total: progress.totalBytes!,
357+
text:
358+
"Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
359+
)
360+
} catch {
361+
await ctx.message(
362+
"Failed to update progress: \(error.localizedDescription)"
363+
)
364+
}
360365
}
361366
)
362367
} catch let notFound as DownloadNotFoundError {
363368
throw SwiftlyError(
364369
message: "\(version) does not exist at URL \(notFound.url), exiting")
365370
} catch {
366-
animation.complete(success: false)
371+
try? await animation.complete(success: false)
367372
throw error
368373
}
369-
animation.complete(success: true)
374+
try await animation.complete(success: true)
370375

371376
if verifySignature {
372377
try await Swiftly.currentPlatform.verifyToolchainSignature(

Sources/Swiftly/JsonFileProgressReporter.swift

Lines changed: 0 additions & 62 deletions
This file was deleted.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Foundation
2+
import SwiftlyCore
3+
import SystemPackage
4+
import TSCBasic
5+
import TSCUtility
6+
7+
public protocol ProgressReporterProtocol {
8+
/// Updates the progress animation with the current step, total steps, and an optional text message.
9+
func update(step: Int, total: Int, text: String) async throws
10+
11+
/// Completes the progress animation, indicating success or failure.
12+
func complete(success: Bool) async throws
13+
14+
/// Closes any resources used by the reporter, if applicable.
15+
func close() throws
16+
}
17+
18+
/// Progress reporter that delegates to a `PercentProgressAnimation` for console output.
19+
struct ConsoleProgressReporter: ProgressReporterProtocol {
20+
private let reporter: PercentProgressAnimation
21+
22+
init(stream: WritableByteStream, header: String) {
23+
self.reporter = PercentProgressAnimation(stream: stream, header: header)
24+
}
25+
26+
func update(step: Int, total: Int, text: String) async throws {
27+
self.reporter.update(step: step, total: total, text: text)
28+
}
29+
30+
func complete(success: Bool) async throws {
31+
self.reporter.complete(success: success)
32+
}
33+
34+
func close() throws {
35+
// No resources to close for console reporter
36+
}
37+
}
38+
39+
enum ProgressInfo: Codable {
40+
case step(timestamp: Date, percent: Int, text: String)
41+
case complete(success: Bool)
42+
}
43+
44+
struct JsonFileProgressReporter: ProgressReporterProtocol {
45+
let filePath: FilePath
46+
private let encoder: JSONEncoder
47+
private let ctx: SwiftlyCoreContext
48+
private let fileHandle: FileHandle
49+
50+
init(_ ctx: SwiftlyCoreContext, filePath: FilePath, encoder: JSONEncoder = JSONEncoder()) throws
51+
{
52+
self.ctx = ctx
53+
self.filePath = filePath
54+
self.encoder = encoder
55+
self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: filePath.string))
56+
}
57+
58+
private func writeProgress(_ progress: ProgressInfo) async throws {
59+
let jsonData = try self.encoder.encode(progress)
60+
61+
self.fileHandle.write(jsonData)
62+
self.fileHandle.write("\n".data(using: .utf8) ?? Data())
63+
try self.fileHandle.synchronize()
64+
}
65+
66+
func update(step: Int, total: Int, text: String) async throws {
67+
guard total > 0 && step <= total else {
68+
return
69+
}
70+
try await self.writeProgress(
71+
ProgressInfo.step(
72+
timestamp: Date(),
73+
percent: Int(Double(step) / Double(total) * 100),
74+
text: text
75+
)
76+
)
77+
}
78+
79+
func complete(success: Bool) async throws {
80+
try await self.writeProgress(ProgressInfo.complete(success: success))
81+
}
82+
83+
func close() throws {
84+
try self.fileHandle.close()
85+
}
86+
}

0 commit comments

Comments
 (0)