Skip to content

Json Progress File #391

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 24, 2025
Merged

Conversation

roulpriya
Copy link
Contributor

Fixes Issue #254

  • Added a JsonFileProgressReporter that writes progress updates and completion status as JSON lines to a file during toolchain installation.
  • Integrated with swiftly install, allowing progress to be tracked.
  • Implemented and tested progress reporting, including step updates, completion (success/failure), and file clearing.

}

let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: path.string))
defer { fileHandle.closeFile() }
Copy link
Member

Choose a reason for hiding this comment

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

praise: This is exactly what I hope for with the defer capability in Swift. When I see something allocated, the release logic follows it, answering my question of "how does this get deallocated?"

do {
try jsonLine.append(to: self.filePath)
} catch {
print("Failed to write progress entry to \(self.filePath): \(error)")
Copy link
Member

Choose a reason for hiding this comment

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

issue: Printing directly to stdout here could do things like corrupt JSON output going there, depending on where we decide to use the progress reporter in the future. This should use the message printing infrastructure that you made in one of the previous PR's instead that will print it to stderr in that case.

do {
try FileManager.default.removeItem(atPath: self.filePath.string)
} catch {
print("Failed to clear progress file at \(self.filePath): \(error)")
Copy link
Member

Choose a reason for hiding this comment

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

issue: see above, printing directly to stdout can be problematic

#expect(!lines.isEmpty, "Progress file should contain progress entries")

// Verify that at least one progress entry exists
let hasProgressEntry = lines.contains { line in
Copy link
Member

Choose a reason for hiding this comment

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

question: Why not decode the lines into a ProgressInfo value, so that values can be checked directly instead of inspecting the line strings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed all the tests to do this now

@@ -190,6 +190,22 @@ extension String {
try self.write(to: URL(fileURLWithPath: path.string), atomically: atomically, encoding: enc)
}

public func append(to path: FilePath, encoding enc: String.Encoding = .utf8) throws {
if !FileManager.default.fileExists(atPath: path.string) {
Copy link
Member

Choose a reason for hiding this comment

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

issue: The progress file lifecycle (and type) is managed by the client of swiftly, so there should be no need to detect if it is exists here. Just go ahead and append to it, and fail if the append fails. This allows the client to allocate a "named pipe" so that they can get the progress streamed directly to a file descriptor that it monitors so that it can get immediate progress monitor updates.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean that the file must always exist?

Copy link
Member

Choose a reason for hiding this comment

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

Right, that's the contract between swiftly and its client. The client creates the file first and swiftly just appends to it when there's progress to report. It's a good idea to have that in the description of the command-line option.

This gives the client the flexibility to create the file as either a plain text file that it polls, or a pipe that streams directly to a reader thread. The inspiration for this is the way that VSCode currently gets updates from the Swift test runner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed the existing check.


let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: path.string))
defer { fileHandle.closeFile() }
fileHandle.seekToEndOfFile()
Copy link
Member

Choose a reason for hiding this comment

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

thought: I wonder if repeatedly opening, seeking to the end, and closing the file is a good idea in the context of a pipe (named pipe in Unix). swiftly will be a long running process here, so it could keep the file handle, and write to it when it has progress to report, close it when it is done. That approach might be much better in terms of performance, and perhaps less error prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I was not sure how files with multiple readers and writers worked. But just now I tested it and should be good to keep a file handle open.

Copy link
Member

Choose a reason for hiding this comment

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

You might find that you need to fflush() each line. Hopefully there's a Swift way of doing this instead of having to make the sys call.


*A file path where progress information will be written in JSONL format*

Progress information will be appended to this file as JSON objects, one per line.
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: Let's spell out that the contract that the client must create this file first, and swiftly will append to it as it progresses.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added:"The file must be writable, else an error will be thrown"

)
let animation: ProgressAnimationProtocol =
progressFile != nil
? JsonFileProgressReporter(filePath: progressFile!)
Copy link
Member

Choose a reason for hiding this comment

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

It's generally bad practice in Swift to use the exclamation operator on optionals, except in certain cases.

suggestion: this could be rewritten like this:

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

return
}

self.fileHandle.seekToEndOfFile()
Copy link
Member

Choose a reason for hiding this comment

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

suggestion: I think that seeking shouldn't be required here since we have a long-lived file handle, and the expectation is that there's only one writer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed this line.

self.writeProgress(ProgressInfo.complete(success: success))
}

func clear() {
Copy link
Member

Choose a reason for hiding this comment

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

question: Where's the requirement for a progress file clear action coming from? I feel that this isn't a necessary function, and is likely to fail on a pipe instead of a regular file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is part of the ProgressAnimationProtocol. Do you think we can keep it unimplemented then?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that a no-op makes sense here. The next progress entry will probably go backwards, and it will be up to a client to handle that case.

@roulpriya roulpriya force-pushed the json-progress-file-install branch from f792e5f to 8d9fbcb Compare June 24, 2025 15:31

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

Choose a reason for hiding this comment

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

thought: The progress animation protocol is limiting here because nothing is async. This message, while probably not very likely to happen, might not get presented in time because it's emitted in a separate task. If we did want to revisit this, I expect that we would make our own protocol that's async friendly, and some kind of a shim for the PercentProgressAnimation.

group.addTask { try? await installTask.value }
}

try await Task.sleep(nanoseconds: 100_000_000) // 100ms
Copy link
Member

Choose a reason for hiding this comment

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

question: Is this sleep required? I think that awaiting the withTaskGroup above should wait until both child tasks are done.

Copy link
Contributor Author

@roulpriya roulpriya Jun 24, 2025

Choose a reason for hiding this comment

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

I was unsure, hence I added sleep. I removed it.

@roulpriya roulpriya force-pushed the json-progress-file-install branch from 8d9fbcb to 9b7d3fe Compare June 24, 2025 16:02
@roulpriya roulpriya force-pushed the json-progress-file-install branch from 9b7d3fe to 12a74b5 Compare June 24, 2025 16:05
@cmcgee1024 cmcgee1024 merged commit bce44d7 into swiftlang:main Jun 24, 2025
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants