Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Foundation
import SwiftlyCore
import SystemPackage
@preconcurrency import TSCBasic
import TSCUtility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: It's great to see this TSC dependency being further isolated. The hope is to remove the dependency entirely someday, and use something that isn't deprecated, and adds to the size of the swiftly binary.


struct Install: SwiftlyCommand {
public static let configuration = CommandConfiguration(
Expand Down Expand Up @@ -313,16 +312,16 @@ struct Install: SwiftlyCommand {
}
}

let animation: ProgressAnimationProtocol =
let animation: ProgressReporterProtocol =
if let progressFile
{
try JsonFileProgressReporter(ctx, filePath: progressFile)
} else {
PercentProgressAnimation(stream: stdoutStream, header: "Downloading \(version)")
ConsoleProgressReporter(stream: stdoutStream, header: "Downloading \(version)")
}

defer {
try? (animation as? JsonFileProgressReporter)?.close()
try? animation.close()
}

var lastUpdate = Date()
Expand Down Expand Up @@ -351,7 +350,7 @@ struct Install: SwiftlyCommand {

lastUpdate = Date()

animation.update(
await animation.update(
step: progress.receivedBytes,
total: progress.totalBytes!,
text:
Expand All @@ -363,10 +362,10 @@ struct Install: SwiftlyCommand {
throw SwiftlyError(
message: "\(version) does not exist at URL \(notFound.url), exiting")
} catch {
animation.complete(success: false)
await animation.complete(success: false)
throw error
}
animation.complete(success: true)
await animation.complete(success: true)

if verifySignature {
try await Swiftly.currentPlatform.verifyToolchainSignature(
Expand Down
62 changes: 0 additions & 62 deletions Sources/Swiftly/JsonFileProgressReporter.swift

This file was deleted.

90 changes: 90 additions & 0 deletions Sources/Swiftly/ProgressReporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Foundation
import SwiftlyCore
import SystemPackage
import TSCBasic
import TSCUtility

public protocol ProgressReporterProtocol {
/// Updates the progress animation with the current step, total steps, and an optional text message.
func update(step: Int, total: Int, text: String) async
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Since update can be fallible due to one of the implementations doing I/O operations, this can be declared as throws and then the try? become plain try and the original I/O error can flow all of the way to the top-level error reporting.


/// Completes the progress animation, indicating success or failure.
func complete(success: Bool) async
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Same as above, this can be declared throws so that I/O errors reported on the complete can be thrown to an error reported with the original I/O error details.


/// Closes any resources used by the reporter, if applicable.
func close() throws
}

/// Progress reporter that delegates to a `PercentProgressAnimation` for console output.
struct ConsoleProgressReporter: ProgressReporterProtocol {
private let reporter: PercentProgressAnimation

init(stream: WritableByteStream, header: String) {
self.reporter = PercentProgressAnimation(stream: stream, header: header)
}

func update(step: Int, total: Int, text: String) async {
self.reporter.update(step: step, total: total, text: text)
}

func complete(success: Bool) async {
self.reporter.complete(success: success)
}

func close() {
// No resources to close for console reporter
}
}

enum ProgressInfo: Codable {
case step(timestamp: Date, percent: Int, text: String)
case complete(success: Bool)
}

struct JsonFileProgressReporter: ProgressReporterProtocol {
let filePath: FilePath
private let encoder: JSONEncoder
private let ctx: SwiftlyCoreContext
private let fileHandle: FileHandle

init(_ ctx: SwiftlyCoreContext, filePath: FilePath, encoder: JSONEncoder = JSONEncoder()) throws
{
self.ctx = ctx
self.filePath = filePath
self.encoder = encoder
self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: filePath.string))
}

private func writeProgress(_ progress: ProgressInfo) async {
let jsonData = try? self.encoder.encode(progress)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: From above, if this method is declared throws then the errors can be thrown directly to the caller with the particular details of the failed I/O operation.

guard let jsonData = jsonData else {
await self.ctx.message("Failed to encode progress entry to JSON")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

praise: It's great to see this message being reliably reported now that the method is async.

return
}

self.fileHandle.write(jsonData)
self.fileHandle.write("\n".data(using: .utf8) ?? Data())
try? self.fileHandle.synchronize()
}

func update(step: Int, total: Int, text: String) async {
guard total > 0 && step <= total else {
return
}
await self.writeProgress(
ProgressInfo.step(
timestamp: Date(),
percent: Int(Double(step) / Double(total) * 100),
text: text
)
)
}

func complete(success: Bool) async {
await self.writeProgress(ProgressInfo.complete(success: success))
}

func close() throws {
try self.fileHandle.close()
}
}
Loading