-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathConsoleWorkerfs.fs
266 lines (200 loc) · 9.11 KB
/
ConsoleWorkerfs.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
namespace Workerfs
// ConsoleWorkerfs.fs
type ExitCode =
| Normal = 0
| Error = 1
| Cancel = -1
| Close = 2
| LogOff = 5 // received only by services
| Shutdown = 6 // received only by services
open System
open System.Threading
open System.Threading.Tasks
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.Logging
open Microsoft.Extensions.Hosting
// This module manages the handling of control signals for the console application,
// such as CTRL+C, CTRL+BREAK, and console window closing signals.
module private CtrlSignals =
open System.Runtime.InteropServices
// Enum defining the different types of control signals.
// HandlerRoutine callback function : https://learn.microsoft.com/en-us/windows/console/handlerroutine
type CtrlTypes =
| CTRL_C_EVENT = 0 // CTRL+C signal
| CTRL_BREAK_EVENT = 1 // CTRL+BREAK signal
| CTRL_CLOSE_EVENT = 2 // Signal when the console window is closing
| CTRL_LOGOFF_EVENT = 5 // Signal when the user logs off ( received only by services )
| CTRL_SHUTDOWN_EVENT = 6 // Signal when the system is shutting down (received only by services )
// Delegate type for handling console control signals.
type private ConsoleCtrlDelegate = delegate of CtrlTypes -> bool
let private handlerRef: ConsoleCtrlDelegate option ref = ref None
// Sets the custom control signal handler.
[<DllImport("kernel32.dll")>]
extern bool private SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add)
// Public method to set the control signals handler.
let setCtrlSignalsHandler cancelfunc =
let handler = ConsoleCtrlDelegate(fun ctrlType ->
match ctrlType with
// Let generic host handle the signal
| CtrlTypes.CTRL_C_EVENT | CtrlTypes.CTRL_BREAK_EVENT ->
false
// Directly call the provided async function and run it synchronously
// Halt further processing
| _ ->
ctrlType |> cancelfunc |> Async.RunSynchronously
true
)
// Set the custom control signal handler using the created delegate
handlerRef.Value <- Some(handler)
SetConsoleCtrlHandler(handler, true) |> ignore
// Defines the ConsoleWorkerfs class for handling the application's lifecycle and cleanup.
type ConsoleWorkerfs(logger: ILogger<ConsoleWorkerfs>, cfg:IConfiguration, appLifetime: IHostApplicationLifetime) as this =
// Initializes default mutable fields.
[<DefaultValue>] val mutable applicationCts : CancellationTokenSource
[<DefaultValue>] val mutable applicationTask : Task
[<DefaultValue>] val mutable exitCode : Nullable<ExitCode>
[<DefaultValue>] val mutable alreadyCleanUp : bool
// Creates a new CancellationTokenSource upon instantiation.
do this.applicationCts <- new CancellationTokenSource()
// Handles exceptions uniformly, setting appropriate exit codes and logging errors.
let errorAction (ex:exn) =
match ex with
// Handles cancellation-related exceptions by setting the exit code to -1 (cancel).
| :? TaskCanceledException | :? OperationCanceledException -> if this.exitCode.HasValue |> not then this.exitCode <- Nullable(ExitCode.Cancel)
// Logs other exceptions and sets the exit code to 1 (error).
| _ as ex ->
logger.LogError(ex,ex.Message)
this.exitCode <- Nullable(ExitCode.Error)
// Implements IDisposable for resource cleanup.
interface IDisposable with
member this.Dispose() =
// Disposes of the CancellationTokenSource safely.
if isNull this.applicationCts |> not
then
try this.applicationCts.Dispose()
with e -> logger.LogError($"Exception during applicationCts disposal: {e.Message}")
this.applicationCts <- null
// Implements IHostedLifecycleService for managing the application's lifecycle.
interface IHostedLifecycleService with
// Prepares the service for starting, including setting up cancellation tokens and exit codes.
member _.StartingAsync(ct:CancellationToken) = task {
let registration = appLifetime.ApplicationStopping.Register( fun () ->
// exitCode is null when ctrl + C
if this.exitCode.HasValue |> not then this.exitCode <- Nullable(ExitCode.Cancel)
if isNull this.applicationCts |> not
then
try this.applicationCts.Cancel()
with e -> errorAction e
)
ct.Register(fun () -> registration.Dispose()) |> ignore
// Handles specific shutdown signals, applying relevant exit codes.
let ctrlSignalHander (ctrlTypes:CtrlSignals.CtrlTypes) = async{
let exitCode =
match ctrlTypes with
| CtrlSignals.CtrlTypes.CTRL_CLOSE_EVENT -> ExitCode.Close
| CtrlSignals.CtrlTypes.CTRL_LOGOFF_EVENT -> ExitCode.LogOff
| CtrlSignals.CtrlTypes.CTRL_SHUTDOWN_EVENT -> ExitCode.Shutdown
| _ -> ExitCode.Cancel
if this.exitCode.HasValue |> not
then
this.exitCode <- Nullable(exitCode)
appLifetime.StopApplication()
while this.alreadyCleanUp |> not do
do! Async.Sleep 1000 // polling time is 1s
}
CtrlSignals.setCtrlSignalsHandler ctrlSignalHander
}
// Placeholder for the service's start logic; might include configuration settings or initialization tasks.
member _.StartAsync(ct:CancellationToken) = task {
try
() // Add initialization logic here.
with e ->
errorAction e
appLifetime.StopApplication()
}
// Contains the main logic to be executed once the service has started.
member _.StartedAsync(ct:CancellationToken) = task {
if this.exitCode.HasValue
then return Task.CompletedTask
else
// Add primary task execution logic here.
this.applicationTask <-
async {
try
let! ct = Async.CancellationToken
// (* 1.normal *)
// logger.LogWarning "Hello World!"
// this.exitCode <- Nullable(0) // 0:normal
// appLifetime.StopApplication()
(* 2.error *)
// failwith "my error!"
(* 3.user cancel *)
while ct.IsCancellationRequested |> not do
$"{DateTime.Now}" |> logger.LogInformation
do! Async.Sleep 1000
with e ->
errorAction e
appLifetime.StopApplication()
}
|> fun cmp -> Async.StartAsTask(computation=cmp,cancellationToken=this.applicationCts.Token)
let registration = appLifetime.ApplicationStarted.Register(fun () ->
this.applicationTask |> ignore
)
ct.Register(fun () -> registration.Dispose()) |> ignore
return Task.CompletedTask
}
// Defines actions to be taken when the service is stopping.
member _.StoppingAsync(ct:CancellationToken) = Task.CompletedTask
// Defines actions to be taken after the service has stopped.
member _.StopAsync(ct:CancellationToken) = Task.CompletedTask
// Cleans up resources and performs final actions after the service has completely stopped.
member _.StoppedAsync(ct:CancellationToken) = task {
// Wait for the application logic to fully complete any cleanup tasks.
// Note that this relies on the cancellation token to be properly used in the application.
if isNull this.applicationTask |> not
then
try do! this.applicationTask
with e -> errorAction e
// Matches the exit code to determine the appropriate cleanup actions.
match this.exitCode.Value with
// NORMAL exit
| ExitCode.Normal ->
for _ in [1..10] do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for NORMAl!")
this.alreadyCleanUp <- true
// Error exit
| ExitCode.Error ->
for _ in [1..10] do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for ERROR!")
this.alreadyCleanUp <- true
// Cancelled
| ExitCode.Cancel ->
for _ in [1..10] do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for CANCEl!")
this.alreadyCleanUp <- true
// Closed (dafault time is 5s)
| ExitCode.Close ->
while true do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for CLOSE!")
this.alreadyCleanUp <- true
// Logoff (dafault time is 5s , received only by services)
| ExitCode.LogOff ->
while true do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for LOGOFF!")
this.alreadyCleanUp <- true
// Shutdown (dafault time is 20s , received only by services)
| ExitCode.Shutdown ->
while true do
do! Async.Sleep 1000 // 1s
logger.LogDebug("clean up for SHUTDOWN!")
this.alreadyCleanUp <- true
// Ohter cases
| _ ->
this.alreadyCleanUp <- true
logger.LogDebug("clean up!")
}