Skip to content

Commit ae7c1e4

Browse files
committed
✨ Support Ctrl-C and Posix Signal cancellation #85
1 parent 3678920 commit ae7c1e4

15 files changed

+342
-57
lines changed

sdk-docs/Xecrets.Sdk.Abstractions.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,4 +433,51 @@ An [System.Action](https://docs.microsoft.com/en-us/dotnet/api/System.Action 'Sy
433433

434434
#### Returns
435435
[System.Threading.Tasks.Task](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.Task 'System.Threading.Tasks.Task')
436-
A waitable task.
436+
A waitable task.
437+
438+
<a name='Xecrets.Sdk.Abstractions.IXfApiFactory'></a>
439+
440+
## IXfApiFactory Interface
441+
442+
A factory for creating an IXfApi instance.
443+
444+
```csharp
445+
public interface IXfApiFactory
446+
```
447+
448+
Derived
449+
&#8627; [XfApiFactory](Xecrets.Sdk.XfApiFactory.md 'Xecrets.Sdk.XfApiFactory')
450+
### Methods
451+
452+
<a name='Xecrets.Sdk.Abstractions.IXfApiFactory.Create()'></a>
453+
454+
## IXfApiFactory.Create() Method
455+
456+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance without cancellation."/>
457+
458+
```csharp
459+
Xecrets.Sdk.Abstractions.IXfApi Create();
460+
```
461+
462+
#### Returns
463+
[IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi')
464+
465+
<a name='Xecrets.Sdk.Abstractions.IXfApiFactory.Create(System.Threading.CancellationToken)'></a>
466+
467+
## IXfApiFactory.Create(CancellationToken) Method
468+
469+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance.
470+
471+
```csharp
472+
Xecrets.Sdk.Abstractions.IXfApi Create(System.Threading.CancellationToken ct);
473+
```
474+
#### Parameters
475+
476+
<a name='Xecrets.Sdk.Abstractions.IXfApiFactory.Create(System.Threading.CancellationToken).ct'></a>
477+
478+
`ct` [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken')
479+
480+
A [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken') to cancel any long running operation.
481+
482+
#### Returns
483+
[IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi')

sdk-docs/Xecrets.Sdk.XfApiFactory.md

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,100 @@
33

44
## XfApiFactory Class
55

6-
Static factory methods to create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') implementation.
6+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance.
77

88
```csharp
9-
public static class XfApiFactory
9+
public class XfApiFactory :
10+
Xecrets.Sdk.Abstractions.IXfApiFactory
1011
```
1112

1213
Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') &#129106; XfApiFactory
13-
### Methods
14-
15-
<a name='Xecrets.Sdk.XfApiFactory.Create()'></a>
16-
17-
## XfApiFactory.Create() Method
1814

19-
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance with defaults.
15+
Implements [IXfApiFactory](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApiFactory 'Xecrets.Sdk.Abstractions.IXfApiFactory')
16+
### Constructors
2017

21-
```csharp
22-
public static Xecrets.Sdk.Abstractions.IXfApi Create();
23-
```
18+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler)'></a>
2419

25-
#### Returns
26-
[IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi')
27-
An instance of [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') ready to use.
20+
## XfApiFactory(string, bool, string, IScheduler, IScheduler) Constructor
2821

29-
### Remarks
30-
Should normally not be used when using Axantum ready built command line tool binaries, as no license is
31-
provided. Another option to provide the license is to place it in a text file next to the executable.
32-
33-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler)'></a>
34-
35-
## XfApiFactory.Create(string, bool, string, IScheduler, IScheduler) Method
36-
37-
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance with all parameters explicitly defined.
22+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance.
3823

3924
```csharp
40-
public static Xecrets.Sdk.Abstractions.IXfApi Create(string license, bool debugCli, string crashLogFullName, System.Reactive.Concurrency.IScheduler taskpoolScheduler, System.Reactive.Concurrency.IScheduler mainthreadScheduler);
25+
public XfApiFactory(string license, bool debugCli, string crashLogFullName, System.Reactive.Concurrency.IScheduler taskpoolScheduler, System.Reactive.Concurrency.IScheduler mainthreadScheduler);
4126
```
4227
#### Parameters
4328

44-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).license'></a>
29+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).license'></a>
4530

4631
`license` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String')
4732

48-
A Xecrets license string to provide for a ready built version of
49-
XecretsCli.
33+
A Xecrets license string to provide for a ready built version of XecretsCli.
5034

51-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).debugCli'></a>
35+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).debugCli'></a>
5236

5337
`debugCli` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean')
5438

5539
Set to [true](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool') to cause the command line to stop on a breakpoint and
5640
enable attaching a debugger. (Windows only)
5741

58-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).crashLogFullName'></a>
42+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).crashLogFullName'></a>
5943

6044
`crashLogFullName` [System.String](https://docs.microsoft.com/en-us/dotnet/api/System.String 'System.String')
6145

6246
The full path where the command line can write a crash log on non-zero return
6347
status.
6448

65-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).taskpoolScheduler'></a>
49+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).taskpoolScheduler'></a>
6650

6751
`taskpoolScheduler` [System.Reactive.Concurrency.IScheduler](https://docs.microsoft.com/en-us/dotnet/api/System.Reactive.Concurrency.IScheduler 'System.Reactive.Concurrency.IScheduler')
6852

6953
An [System.Reactive.Concurrency.IScheduler](https://docs.microsoft.com/en-us/dotnet/api/System.Reactive.Concurrency.IScheduler 'System.Reactive.Concurrency.IScheduler') instance to schedule work on the
7054
taskpool.
7155

72-
<a name='Xecrets.Sdk.XfApiFactory.Create(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).mainthreadScheduler'></a>
56+
<a name='Xecrets.Sdk.XfApiFactory.XfApiFactory(string,bool,string,System.Reactive.Concurrency.IScheduler,System.Reactive.Concurrency.IScheduler).mainthreadScheduler'></a>
7357

7458
`mainthreadScheduler` [System.Reactive.Concurrency.IScheduler](https://docs.microsoft.com/en-us/dotnet/api/System.Reactive.Concurrency.IScheduler 'System.Reactive.Concurrency.IScheduler')
7559

7660
An [System.Reactive.Concurrency.IScheduler](https://docs.microsoft.com/en-us/dotnet/api/System.Reactive.Concurrency.IScheduler 'System.Reactive.Concurrency.IScheduler') instance to schedule work on the main (GUI)
7761
thread.
62+
### Methods
63+
64+
<a name='Xecrets.Sdk.XfApiFactory.Create(System.Threading.CancellationToken)'></a>
65+
66+
## XfApiFactory.Create(CancellationToken) Method
67+
68+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance.
69+
70+
```csharp
71+
public Xecrets.Sdk.Abstractions.IXfApi Create(System.Threading.CancellationToken ct);
72+
```
73+
#### Parameters
74+
75+
<a name='Xecrets.Sdk.XfApiFactory.Create(System.Threading.CancellationToken).ct'></a>
76+
77+
`ct` [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken')
78+
79+
A cancellation token to cancel any long running operation.
80+
81+
Implements [Create(CancellationToken)](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApiFactory.Create(System.Threading.CancellationToken) 'Xecrets.Sdk.Abstractions.IXfApiFactory.Create(System.Threading.CancellationToken)')
82+
83+
#### Returns
84+
[IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi')
85+
86+
<a name='Xecrets.Sdk.XfApiFactory.Safe()'></a>
87+
88+
## XfApiFactory.Safe() Method
89+
90+
Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance with safe defaults.
91+
92+
```csharp
93+
public static Xecrets.Sdk.Abstractions.IXfApi Safe();
94+
```
7895

7996
#### Returns
8097
[IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi')
81-
An instance of [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') ready to use.
98+
An instance of [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') ready to use.
99+
100+
### Remarks
101+
Should normally not be used when using Axantum ready built command line tool binaries, as no license is
102+
provided. Another option to provide the license is to place it in a text file next to the executable.

sdk-docs/Xecrets.Sdk.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
| Classes | |
66
| :--- | :--- |
7-
| [XfApiFactory](Xecrets.Sdk.XfApiFactory.md 'Xecrets.Sdk.XfApiFactory') | Static factory methods to create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') implementation. |
7+
| [XfApiFactory](Xecrets.Sdk.XfApiFactory.md 'Xecrets.Sdk.XfApiFactory') | Create an [IXfApi](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi 'Xecrets.Sdk.Abstractions.IXfApi') instance. |
88
| [XfException](Xecrets.Sdk.XfException.md 'Xecrets.Sdk.XfException') | The exception type thrown when something goes wrong calling the command line tool. In addition to the base class<br/>[System.Exception](https://docs.microsoft.com/en-us/dotnet/api/System.Exception 'System.Exception') there is an [ExitCode](Xecrets.Sdk.XfException.md#Xecrets.Sdk.XfException.ExitCode 'Xecrets.Sdk.XfException.ExitCode') property containing the actual exit code from the<br/>tool. Exit codes are [System.Int32](https://docs.microsoft.com/en-us/dotnet/api/System.Int32 'System.Int32') but they are mapped to enum [Xecrets.Cli.Public.XfStatusCode](https://docs.microsoft.com/en-us/dotnet/api/Xecrets.Cli.Public.XfStatusCode 'Xecrets.Cli.Public.XfStatusCode') here. To ensure<br/>that the mapping is correct, call [IsSdkCompatibleWith(Version)](Xecrets.Sdk.Abstractions.md#Xecrets.Sdk.Abstractions.IXfApi.IsSdkCompatibleWith(System.Version) 'Xecrets.Sdk.Abstractions.IXfApi.IsSdkCompatibleWith(System.Version)') ./> |
99
| [XfExtensions](Xecrets.Sdk.XfExtensions.md 'Xecrets.Sdk.XfExtensions') | Useful extension methods for file names |
1010
| [XfSdkVersion](Xecrets.Sdk.XfSdkVersion.md 'Xecrets.Sdk.XfSdkVersion') | Helper to determine if the current SDK version is compatible with a given command line tool API version. |

src/Xecrets.Cli/CancelSignal.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#region Copyright and GPL License
2+
3+
/*
4+
* Xecrets Cli - Copyright © 2022-2024, Svante Seleborg, All Rights Reserved.
5+
*
6+
* This code file is part of Xecrets Cli, parts of which in turn are derived from AxCrypt as licensed under GPL v3 or later.
7+
*
8+
* However, this code is not derived from AxCrypt and is separately copyrighted and only licensed as follows unless
9+
* explicitly licensed otherwise. If you use any part of this code in your software, please see https://www.gnu.org/licenses/
10+
* for details of what this means for you.
11+
*
12+
* Xecrets Cli is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
13+
* as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
14+
*
15+
* Xecrets Cli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
16+
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License along with Xecrets Cli. If not, see <https://www.gnu.org/licenses/>.
19+
*
20+
* The source repository can be found at https://github.com/ please go there for more information, suggestions and
21+
* contributions. You may also visit https://www.axantum.com for more information about the author.
22+
*/
23+
24+
#endregion Copyright and GPL License
25+
26+
using System.Runtime.InteropServices;
27+
28+
namespace Xecrets.Cli;
29+
30+
internal class CancelSignal : IDisposable
31+
{
32+
private PosixSignalRegistration? _posixRegistration;
33+
34+
private CancellationTokenSource? _immediateShutdownSource = new();
35+
36+
public CancellationToken ImmediateToken => _immediateShutdownSource?.Token ??
37+
throw new ObjectDisposedException(nameof(ImmediateToken));
38+
39+
public CancelSignal()
40+
{
41+
_posixRegistration = PosixSignalRegistration.Create(PosixSignal.SIGINT, (h) =>
42+
{
43+
h.Cancel = true;
44+
_immediateShutdownSource.Cancel();
45+
});
46+
}
47+
48+
protected virtual void Dispose(bool disposing)
49+
{
50+
if (!disposing)
51+
{
52+
return;
53+
}
54+
55+
if (_immediateShutdownSource != null)
56+
{
57+
_immediateShutdownSource.Dispose();
58+
_immediateShutdownSource = null;
59+
}
60+
61+
if (_posixRegistration != null)
62+
{
63+
_posixRegistration.Dispose();
64+
_posixRegistration = null;
65+
}
66+
}
67+
68+
public void Dispose()
69+
{
70+
Dispose(true);
71+
GC.SuppressFinalize(this);
72+
}
73+
74+
~CancelSignal()
75+
{
76+
Dispose(false);
77+
}
78+
}

src/Xecrets.Cli/Executor.cs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,37 @@ internal class Executor(Parameters parameters) : IDisposable
3535
{
3636
public async Task<Status> RunAsync()
3737
{
38-
Status status = await RunAsync(new DryRunFactory(parameters));
39-
if ((parameters.Parser.IsQuiet || parameters.Parser.Internal) && status.IsSuccess)
38+
try
4039
{
41-
New<Splash>().Clear();
42-
}
40+
Status status = await RunAsync(new DryRunFactory(parameters));
41+
if ((parameters.Parser.IsQuiet || parameters.Parser.Internal) && status.IsSuccess)
42+
{
43+
New<Splash>().Clear();
44+
}
4345

44-
if (!status.IsSuccess)
45-
{
46-
return status;
47-
}
46+
if (!status.IsSuccess)
47+
{
48+
return status;
49+
}
4850

49-
if (parameters.Parser.IsDryRunOnly)
50-
{
51-
return new Status("A successful dry run was executed, no files were changed.");
52-
}
51+
if (parameters.Parser.IsDryRunOnly)
52+
{
53+
return new Status("A successful dry run was executed, no files were changed.");
54+
}
5355

54-
ResetParametersForRealRun();
56+
ResetParametersForRealRun();
5557

56-
return await RunAsync(new RealRunFactory(parameters));
58+
return await RunAsync(new RealRunFactory(parameters));
59+
}
60+
catch (OperationCanceledException oce)
61+
{
62+
return new Status(XfStatusCode.Canceled, oce.Message)
63+
{
64+
Id = parameters.TotalsTracker.Id,
65+
Arg1 = parameters.Arg1,
66+
Arg2 = parameters.Arg2,
67+
};
68+
}
5769
}
5870

5971
private void ResetParametersForRealRun()

src/Xecrets.Cli/Log/TotalsTracker.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
using Xecrets.Cli.Public;
2727
using Xecrets.Cli.Run;
2828

29+
using static AxCrypt.Abstractions.TypeResolve;
30+
2931
namespace Xecrets.Cli.Log;
3032

3133
internal class TotalsTracker
@@ -48,8 +50,11 @@ internal class TotalsTracker
4850

4951
public string Id { get; set; } = string.Empty;
5052

53+
private readonly CancelSignal _cancelSignal;
54+
5155
public TotalsTracker()
5256
{
57+
_cancelSignal = New<CancelSignal>();
5358
Logger = GetCurrentLogger();
5459
}
5560

@@ -84,20 +89,32 @@ public void AddWorkItem(long count)
8489
{
8590
AddWork(count);
8691
ItemsTotal += 1;
92+
AssertNoCancel();
8793
}
8894

8995
public void AddWork(long count)
9096
{
9197
TotalWork += count;
98+
AssertNoCancel();
9299
}
93100

94101
public void DoWork(long count)
95102
{
96103
TotalDone += count;
104+
AssertNoCancel();
97105
}
98106

99107
public void DoItems(int items)
100108
{
101109
ItemsDone += items;
110+
AssertNoCancel();
111+
}
112+
113+
private void AssertNoCancel()
114+
{
115+
if (_cancelSignal.ImmediateToken.IsCancellationRequested)
116+
{
117+
throw new OperationCanceledException("Operation immediately canceled.");
118+
}
102119
}
103120
}

0 commit comments

Comments
 (0)