Skip to content

Tweak internal IProgress usage for APM and sync UploadFile/DownloadFile#1805

Closed
Rob-Hague wants to merge 1 commit into
sshnet:developfrom
Rob-Hague:progressreport
Closed

Tweak internal IProgress usage for APM and sync UploadFile/DownloadFile#1805
Rob-Hague wants to merge 1 commit into
sshnet:developfrom
Rob-Hague:progressreport

Conversation

@Rob-Hague

Copy link
Copy Markdown
Collaborator

Changes to support IProgress<> callback on UploadAsync/DownloadAsync meant wrapping the Action<> callback on existing methods in a Progress<>, which posts the callback onto the current synchronisation context rather than the threadpool. For the legacy APM methods (Begin[..]), let's just preserve their old behaviour.

For the synchronous methods, posting to the synchronisation context is probably the worst choice (because if there is one, the method itself is running there). We can either revert to the threadpool as well, or take the opportunity to invoke the callback synchronously, which is a behavioural change but probably the least surprising behaviour for a synchronous method.

Changes to support IProgress<> callback on UploadAsync/DownloadAsync meant wrapping
the Action<> callback on existing methods in a Progress<>, which posts the callback
onto the current synchronisation context rather than the threadpool. For the legacy
APM methods (Begin[..]), let's just preserve their old behaviour.

For the synchronous methods, posting to the synchronisation context is probably the
worst choice (because if there is one, the method itself is running there). We can
either revert to the threadpool as well, or take the opportunity to invoke the
callback synchronously, which is a behavioural change but probably the least
surprising behaviour for a synchronous method.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adjusts how SftpClient routes progress callbacks so that legacy APM (Begin*) methods keep their historical “thread pool regardless of SynchronizationContext” behavior, while sync UploadFile/DownloadFile no longer post progress back onto the current synchronization context.

Changes:

  • Replaced Progress<T> wrappers for APM methods with a custom ThreadPoolProgress<T> to avoid SynchronizationContext capture.
  • Changed sync UploadFile/DownloadFile callback wrapping to use a custom SynchronousProgress<T>.
  • Simplified progress reporting sites to report inline via ?.Report(...) with a freshly constructed progress report value.

Comment on lines 1094 to 1097
if (uploadCallback != null)
{
uploadProgress = new Progress<UploadFileProgressReport>(r => uploadCallback(r.TotalBytesUploaded));
uploadProgress = new SynchronousProgress<UploadFileProgressReport>(r => uploadCallback(r.TotalBytesUploaded));
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

fair point

Comment on lines +2652 to +2692
/// <summary>
/// An <see cref="IProgress{T}"/> implementation that posts callbacks to the threadpool.
/// </summary>
private sealed class ThreadPoolProgress<T> : IProgress<T>
{
private readonly Action<T> _handler;

public ThreadPoolProgress(Action<T> handler)
{
Debug.Assert(handler != null);
_handler = handler!;
}

void IProgress<T>.Report(T value)
{
_ = ThreadPool.QueueUserWorkItem(static state =>
{
var (handler, value) = ((Action<T>, T))state!;
handler(value);
},
(_handler, value));
}
}

/// <summary>
/// An <see cref="IProgress{T}"/> implementation that invokes callbacks synchronously.
/// </summary>
private sealed class SynchronousProgress<T> : IProgress<T>
{
private readonly Action<T> _handler;

public SynchronousProgress(Action<T> handler)
{
Debug.Assert(handler != null);
_handler = handler!;
}

void IProgress<T>.Report(T value)
{
_handler.Invoke(value);
}
@Rob-Hague Rob-Hague marked this pull request as draft June 20, 2026 17:49
@Rob-Hague Rob-Hague closed this Jun 20, 2026
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.

SftpUpload() callback executed out of order

2 participants