Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
9 changes: 9 additions & 0 deletions pwiz_tools/Skyline/Controls/ControlsResources.designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions pwiz_tools/Skyline/Controls/ControlsResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@
<data name="LongWaitDlg_PerformWork_canceled" xml:space="preserve">
<value>canceled</value>
</data>
<data name="RunningJobsDlg_GetProgressText_Stopping" xml:space="preserve">
<value>Stopping</value>
</data>
<data name="MessageBoxHelper_GetControlMessage_Field" xml:space="preserve">
<value>Field</value>
</data>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@
<data name="ExportLiveReportDlg_ExportLiveReportDlg_Invariant" xml:space="preserve">
<value>Invariant</value>
</data>
<data name="ExportLiveReportDlg_ExportReport_Exporting_report___0__" xml:space="preserve">
<value>Exporting report '{0}'</value>
</data>
<data name="ExportLiveReportDlg_ShowPreview_Preview__" xml:space="preserve">
<value>Preview: </value>
</data>
Expand Down
36 changes: 24 additions & 12 deletions pwiz_tools/Skyline/Controls/Databinding/ExportLiveReportDlg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,30 +158,42 @@ private bool ExportReport(string filename, IReportExporter rowItemExporter)
}
try
{
using var fileSaver = new FileSaver(filename, true);
// Not a using: the file is written by the WORK below, which owns the saver from the moment it
// starts. Once the user sends the export to the background there is no caller left to commit it.
var fileSaver = new FileSaver(filename, true);
if (!fileSaver.CanSave(this))
{
fileSaver.Dispose();
return false;
}

using var longWaitDlg = new LongWaitDlg();
longWaitDlg.Text = DatabindingResources.ExportReportDlg_ExportReport_Generating_Report;
IProgressStatus status = new ProgressStatus(DatabindingResources.ExportReportDlg_ExportReport_Building_report);
var dataSchema = GetSkylineDataSchema(true);
// StartJob, not PerformWork: the user may leave this export running in the background. It qualifies
// because it writes only its own file and reads a snapshot of the document (GetSkylineDataSchema
// clones it), so nothing it does depends on what the user does next.
var jobDescription = string.Format(
DatabindingResources.ExportLiveReportDlg_ExportReport_Exporting_report___0__, viewName.Value.Name);
// ReSharper disable once RedundantLambdaParameterType
longWaitDlg.PerformWork(this, 1500, (IProgressMonitor progressMonitor) =>
var outcome = longWaitDlg.StartJob(this, 1500, jobDescription, (IProgressMonitor progressMonitor) =>
{
progressMonitor.UpdateProgress(status);
var rowFactories = RowFactories.GetRowFactories(longWaitDlg.CancellationToken, dataSchema);
rowFactories.ExportReport(fileSaver.Stream, viewName.Value, rowItemExporter, progressMonitor, ref status);
using (fileSaver)
{
progressMonitor.UpdateProgress(status);
var rowFactories = RowFactories.GetRowFactories(longWaitDlg.CancellationToken, dataSchema);
rowFactories.ExportReport(fileSaver.Stream, viewName.Value, rowItemExporter, progressMonitor, ref status);
// A cancelled export stops part way through the file, so it must not be committed. The
// saver discards what it wrote as it is disposed.
if (!progressMonitor.IsCanceled)
{
fileSaver.Commit();
}
}
});
if (longWaitDlg.IsCanceled)
{
return false;
}

fileSaver.Commit();
return true;
// Backgrounding is not cancelling: the export is running, and this dialog is done with it.
return outcome != LongWaitDlg.JobOutcome.canceled;
}
catch (Exception x)
{
Expand Down
12 changes: 11 additions & 1 deletion pwiz_tools/Skyline/Controls/LongWaitDlg.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 117 additions & 3 deletions pwiz_tools/Skyline/Controls/LongWaitDlg.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ public partial class LongWaitDlg : FormEx, ILongWaitBroker, ILongWaitForm
private readonly CancellationTokenSource _cancellationTokenSource;
private ManualResetEvent _completionEvent;

// The job the work was handed to when the user pressed "Run in Background". Set on the UI thread and read
// on the work's thread, which goes on reporting progress to it long after this dialog is gone.
private volatile BackgroundJob _backgroundJob;

// these members should only be accessed in a block which locks on _lock
#region synchronized members
private readonly object _lock = new object();
private bool _finished;
private bool _windowShown;
private bool _backgrounded;
#endregion

/// <summary>
Expand Down Expand Up @@ -82,7 +87,11 @@ public LongWaitDlg(IDocumentContainer documentContainer = null, bool allowCancel
public string Message
{
get { return _message; }
set { _message = value; }
set
{
_message = value;
ReportBackgroundJobProgress();
}
}

public int ProgressValue
Expand All @@ -92,9 +101,23 @@ public int ProgressValue
{
Assume.IsTrue(value <= 100);
_progressValue = value;
ReportBackgroundJobProgress();
}
}

/// <summary>What became of the work <see cref="StartJob"/> ran.</summary>
public enum JobOutcome
{
completed,
canceled,
/// <summary>The user sent it to the background; it is still running, as a job.</summary>
backgrounded
}

// What the work is called once it is running in the background. Set by StartJob only, which is what makes
// the button - and backgrounding at all - something a caller opts into.
private string _jobDescription;

public override string DetailedMessage
{
get { return string.Format(@"[{0}] {1} ({2}%)", Text, Message, ProgressValue); }
Expand Down Expand Up @@ -152,6 +175,39 @@ public IProgressStatus PerformWork(Control parent, int delayMillis, [InstantHand
return progressWaitBroker.Status;
}

/// <summary>
/// Runs work that the user may send to the background, and reports what became of it. The same as
/// <see cref="PerformWork(Control,int,Action{IProgressMonitor})"/> except that this dialog offers a "Run in
/// Background" button: pressing it closes the dialog and returns from here with
/// <see cref="JobOutcome.backgrounded"/>, leaving the work running as a job that reports to the status bar
/// and can be cancelled there (Tools > Running Jobs).
///
/// <para>It is a separate method, and not an option on PerformWork, because the work has to be written for
/// it. The delegate OUTLIVES this call, so it must be SELF-CONTAINED: everything that finishes the
/// operation - committing a file, reporting a result - has to happen inside it, since the caller resumes
/// the moment the button is pressed and there is nobody left to do it afterwards. Work that applies its
/// result to the document when it lands may not be run this way at all - the user goes on editing while it
/// runs, and it would finish by overwriting what they did - nor may work that asks the user something part
/// way through (the ShowDialog below), which has no window left to show it in once this one has closed.
/// Everything else keeps calling PerformWork, whose delegate is always finished with by the time it
/// returns.</para>
/// </summary>
/// <param name="parent">The window the dialog belongs to.</param>
/// <param name="delayMillis">How long to let the work run before showing the dialog at all.</param>
/// <param name="jobDescription">What the job is called in the status bar and the job list once it is
/// backgrounded ("Exporting report 'Peak Areas'").</param>
/// <param name="performWork">The work. Must be self-contained - see above.</param>
public JobOutcome StartJob(Control parent, int delayMillis, string jobDescription,
Action<IProgressMonitor> performWork)
{
_jobDescription = jobDescription;
var progressWaitBroker = new ProgressWaitBroker(performWork);
PerformWork(parent, delayMillis, progressWaitBroker.PerformWork);
if (_backgroundJob != null)
return JobOutcome.backgrounded;
return IsCanceled ? JobOutcome.canceled : JobOutcome.completed;
}

public void PerformWork(Control parent, int delayMillis, [InstantHandle] Action<ILongWaitBroker> performWork)
{
_startTime = DateTime.UtcNow; // Said to be 117x faster than Now and this is for a delta
Expand Down Expand Up @@ -180,6 +236,7 @@ public void PerformWork(Control parent, int delayMillis, [InstantHandle] Action<

progressBar.Value = Math.Max(0, _progressValue);
UpdateLabelMessage();
btnBackground.Visible = _jobDescription != null;

ShowDialog(parent);
}
Expand Down Expand Up @@ -248,9 +305,9 @@ protected override void OnFormClosing(FormClosingEventArgs e)
{
lock (_lock)
{
if (!_finished)
if (!_finished && !_backgrounded)
{
// If the user is trying to close this form, then treat it the
// If the user is trying to close this form, then treat it the
// same as if they had hit "Cancel".
OnClickedCancel();
e.Cancel = true;
Expand Down Expand Up @@ -294,9 +351,31 @@ private void RunWork(Action<ILongWaitBroker> performWork)
{
_completionEvent?.Set();
}

FinishBackgroundJob();
}
}

/// <summary>
/// Ends the job this work was sent to, if it was sent to one. This is where a backgrounded operation
/// reports what became of it: <see cref="PerformWork(Control,int,Action{ILongWaitBroker})"/> returned when
/// the user pressed the button, so the caller that would have been thrown the exception, or seen the
/// operation through, is long gone.
/// </summary>
private void FinishBackgroundJob()
{
var job = _backgroundJob;
if (job == null)
return;

// A cancel is not a failure to report - the same rule PerformWork applies to what it rethrows.
if (_exception != null && !(IsCanceled && _exception.HasException<OperationCanceledException>()))
{
job.Failed(_exception);
}
job.Dispose();
}

private void FinishDialog()
{
if (!_cancellationTokenSource.IsCancellationRequested)
Expand Down Expand Up @@ -334,6 +413,41 @@ private void btnCancel_Click(object sender, EventArgs e)
OnClickedCancel();
}

private void btnBackground_Click(object sender, EventArgs e)
{
RunInBackground();
}

/// <summary>
/// Hands the running work to a <see cref="BackgroundJob"/> and closes this dialog, which returns
/// <see cref="StartJob"/> to its caller while the work goes on. From here the job reports its progress to
/// the main window's status bar, and cancelling the job is what stops it - it is registered to trip this
/// dialog's own cancellation, so the work goes on watching the one token it always has.
/// </summary>
public void RunInBackground()
{
lock (_lock)
{
// Nothing to hand over: the work either finished while the user was reaching for the button, or
// was never started by StartJob, the only caller that says what a job of it would be called.
if (_finished || _backgrounded || _jobDescription == null)
return;
_backgroundJob = BackgroundJobs.Start(_jobDescription);
_backgrounded = true;
}
_backgroundJob.CancellationToken.Register(() => _cancellationTokenSource.Cancel());
ReportBackgroundJobProgress();
Close();
}

// Once the work is in the background its progress belongs to the status bar and the job list, not to this
// dialog, which is gone. Every report the work makes passes through Message and ProgressValue, so this is
// the one place that has to forward.
private void ReportBackgroundJobProgress()
{
_backgroundJob?.UpdateProgress(_message, _progressValue);
}

private void OnClickedCancel()
{
if (!_cancellationTokenSource.IsCancellationRequested)
Expand Down
32 changes: 31 additions & 1 deletion pwiz_tools/Skyline/Controls/LongWaitDlg.resx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,36 @@
<value>$this</value>
</data>
<data name="&gt;&gt;btnCancel.ZOrder" xml:space="preserve">
<value>2</value>
</data>
<data name="btnBackground.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
<value>Bottom, Right</value>
</data>
<data name="btnBackground.Location" type="System.Drawing.Point, System.Drawing">
<value>144, 102</value>
</data>
<data name="btnBackground.Size" type="System.Drawing.Size, System.Drawing">
<value>115, 23</value>
</data>
<data name="btnBackground.TabIndex" type="System.Int32, mscorlib">
<value>4</value>
</data>
<data name="btnBackground.Text" xml:space="preserve">
<value>Run in Background</value>
</data>
<data name="btnBackground.Visible" type="System.Boolean, mscorlib">
<value>False</value>
</data>
<data name="&gt;&gt;btnBackground.Name" xml:space="preserve">
<value>btnBackground</value>
</data>
<data name="&gt;&gt;btnBackground.Type" xml:space="preserve">
<value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="&gt;&gt;btnBackground.Parent" xml:space="preserve">
<value>$this</value>
</data>
<data name="&gt;&gt;btnBackground.ZOrder" xml:space="preserve">
<value>1</value>
</data>
<data name="progressBar.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms">
Expand Down Expand Up @@ -235,7 +265,7 @@
<value>$this</value>
</data>
<data name="&gt;&gt;panel1.ZOrder" xml:space="preserve">
<value>2</value>
<value>3</value>
</data>
<metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
Expand Down
Loading