-
Notifications
You must be signed in to change notification settings - Fork 52
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
Json Progress File #391
Conversation
} | ||
|
||
let fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: path.string)) | ||
defer { fileHandle.closeFile() } |
There was a problem hiding this comment.
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)") |
There was a problem hiding this comment.
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)") |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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"
Sources/Swiftly/Install.swift
Outdated
) | ||
let animation: ProgressAnimationProtocol = | ||
progressFile != nil | ||
? JsonFileProgressReporter(filePath: progressFile!) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
f792e5f
to
8d9fbcb
Compare
|
||
private func writeProgress(_ progress: ProgressInfo) { | ||
let jsonData = try? self.encoder.encode(progress) | ||
guard let jsonData = jsonData else { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
8d9fbcb
to
9b7d3fe
Compare
9b7d3fe
to
12a74b5
Compare
Fixes Issue #254
swiftly install
, allowing progress to be tracked.