Skip to content

Commit c01278c

Browse files
authored
Add static support for long running processes (#13)
* Add static support for long running programs with manual started confirmations * add fsharp bindings
1 parent 027a0c1 commit c01278c

17 files changed

+384
-80
lines changed

examples/ScratchPad.Fs/Program.fs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
open System
22
open Proc.Fs
33

4+
(*
45
let _ = shell {
56
exec "dotnet" "--version"
67
exec "uname"
78
}
89
9-
1010
exec { run "dotnet" "--help"}
11+
1112
exec {
1213
binary "dotnet"
1314
arguments "--help"
@@ -38,7 +39,7 @@ let dotnetVersion = exec {
3839
filter (fun l -> l.Line.Contains "clean")
3940
}
4041
41-
printfn "Found lines %i" dotnetVersion.Length
42+
printfn $"Found lines %i{dotnetVersion.Length}"
4243
4344
4445
let dotnetOptions = exec { binary "dotnet" }
@@ -55,5 +56,14 @@ let _ = shell { exec "dotnet" args }
5556
let statusCode = exec { exit_code_of "dotnet" "--help"}
5657
5758
exec { run "dotnet" "run" "--project" "examples/ScratchPad.Fs.ArgumentPrinter" "--" "With Space" }
59+
*)
60+
61+
let runningProcess = exec {
62+
binary "dotnet"
63+
arguments "run" "--project" "tests/Proc.Tests.Binary" "--" "TrulyLongRunning"
64+
//wait_until (fun l -> l.Line = "Started!")
65+
wait_until_and_disconnect (fun l -> l.Line = "Started!")
66+
}
67+
5868

5969
printfn "That's all folks!"

src/Proc.Fs/Bindings.fs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ type ExecOptions = {
1515
Timeout: TimeSpan
1616
ValidExitCodeClassifier: (int -> bool) option
1717

18+
StartedConfirmationHandler: (LineOut -> bool) option
19+
StopBufferingAfterStarted: bool option
20+
1821
NoWrapInThread: bool option
1922
SendControlCFirst: bool option
2023
WaitForStreamReadersTimeout: TimeSpan option
@@ -26,7 +29,8 @@ with
2629
LineOutFilter = None; WorkingDirectory = None; Environment = None
2730
Timeout = TimeSpan(0, 0, 0, 0, -1)
2831
ValidExitCodeClassifier = None;
29-
NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None;
32+
NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None
33+
StartedConfirmationHandler = None; StopBufferingAfterStarted = None
3034
}
3135

3236
let private startArgs (opts: ExecOptions) =
@@ -48,6 +52,21 @@ let private execArgs (opts: ExecOptions) =
4852
opts.ValidExitCodeClassifier |> Option.iter(fun f -> execArguments.ValidExitCodeClassifier <- f)
4953
execArguments
5054

55+
let private longRunningArguments (opts: ExecOptions) =
56+
let args = opts.Arguments |> Option.defaultValue []
57+
let longRunningArguments = LongRunningArguments(opts.Binary, args)
58+
opts.LineOutFilter |> Option.iter(fun f -> longRunningArguments.LineOutFilter <- f)
59+
opts.Environment |> Option.iter(fun e -> longRunningArguments.Environment <- e)
60+
opts.WorkingDirectory |> Option.iter(fun d -> longRunningArguments.WorkingDirectory <- d)
61+
opts.NoWrapInThread |> Option.iter(fun b -> longRunningArguments.NoWrapInThread <- b)
62+
opts.SendControlCFirst |> Option.iter(fun b -> longRunningArguments.SendControlCFirst <- b)
63+
opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> longRunningArguments.WaitForStreamReadersTimeout <- t)
64+
65+
opts.StartedConfirmationHandler |> Option.iter(fun t -> longRunningArguments.StartedConfirmationHandler <- t)
66+
opts.StopBufferingAfterStarted |> Option.iter(fun t -> longRunningArguments.StopBufferingAfterStarted <- t)
67+
68+
longRunningArguments
69+
5170

5271
type ShellBuilder() =
5372

@@ -251,6 +270,24 @@ type ExecBuilder() =
251270
let startArgs = startArgs opts
252271
Proc.Start(startArgs, opts.Timeout)
253272

273+
[<CustomOperation("wait_until")>]
274+
member this.WaitUntil(opts, startedConfirmation: LineOut -> bool) =
275+
let opts = { opts with StartedConfirmationHandler = Some startedConfirmation }
276+
let longRunningArguments = longRunningArguments opts
277+
Proc.StartLongRunning(longRunningArguments, opts.Timeout)
278+
279+
[<CustomOperation("wait_until_and_disconnect")>]
280+
member this.WaitUntilQuietAfter(opts, startedConfirmation: LineOut -> bool) =
281+
let opts =
282+
{
283+
opts with
284+
StartedConfirmationHandler = Some startedConfirmation
285+
StopBufferingAfterStarted = Some true
286+
}
287+
let longRunningArguments = longRunningArguments opts
288+
Proc.StartLongRunning(longRunningArguments, opts.Timeout)
289+
290+
254291

255292
let exec = ExecBuilder()
256293

src/Proc.Fs/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,24 @@ let helpOutput = exec {
103103
```
104104

105105
returns the exit code and the full console output.
106+
107+
```fsharp
108+
109+
let process = exec {
110+
binary "dotnet"
111+
arguments "--help"
112+
wait_until (fun l -> l.Line.Contains "clean")
113+
}
114+
```
115+
116+
returns an already running process but only after it confirms a line was printed
117+
```fsharp
118+
119+
let process = exec {
120+
binary "dotnet"
121+
arguments "--help"
122+
wait_until_and_disconnect (fun l -> l.Line.Contains "clean")
123+
}
124+
```
125+
126+
returns an already running process but only after it confirms a line was printed. This version will stop the yielding standard/out lines which may utilize memory consumption which is no longer needed.

src/Proc/ExecArguments.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ public ExecArguments(string binary, IEnumerable<string> args) : base(binary, arg
1010

1111
public ExecArguments(string binary, params string[] args) : base(binary, args) { }
1212

13-
/// <summary> Force arguments and the current working director NOT to be part of the exception message </summary>
14-
public bool OnlyPrintBinaryInExceptionMessage { get; set; }
15-
1613
public Func<int, bool> ValidExitCodeClassifier
1714
{
1815
get => _validExitCodeClassifier ?? (c => c == 0);

src/Proc/LongRunningArguments.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using ProcNet.Std;
4+
5+
namespace ProcNet;
6+
7+
public class LongRunningArguments : StartArguments
8+
{
9+
public LongRunningArguments(string binary, IEnumerable<string> args) : base(binary, args) { }
10+
11+
public LongRunningArguments(string binary, params string[] args) : base(binary, args) { }
12+
13+
/// <summary>
14+
/// A handler that will delay return of the <see cref="IDisposable"/> process until startup is confirmed over
15+
/// standard out/error.
16+
/// </summary>
17+
public Func<LineOut, bool> StartedConfirmationHandler { get; set; }
18+
19+
/// <summary>
20+
/// A helper that sets <see cref="StartArguments.KeepBufferingLines"/> and stops immediately after <see cref="StartedConfirmationHandler"/>
21+
/// indicates the process has started.
22+
/// </summary>
23+
public bool StopBufferingAfterStarted { get; set; }
24+
}

src/Proc/ObservableProcess.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,12 @@ public IDisposable Subscribe(IObserver<LineOut> observerLines, IObserver<Charact
110110
)
111111
.ToArray();
112112
})
113-
.TakeWhile(KeepBufferingLines)
113+
.TakeWhile(l =>
114+
{
115+
var keepBuffering = StartArguments.KeepBufferingLines ?? KeepBufferingLines;
116+
var keep = keepBuffering?.Invoke(l);
117+
return keep.GetValueOrDefault(true);
118+
})
114119
.Where(l => l != null)
115120
.Where(observeLinesFilter)
116121
.Subscribe(

src/Proc/ObservableProcessBase.cs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace ProcNet
1414
{
15-
public delegate void StartedHandler(StreamWriter standardInput);
15+
public delegate void StandardInputHandler(StreamWriter standardInput);
1616

1717
public abstract class ObservableProcessBase<TConsoleOut> : IObservableProcess<TConsoleOut>
1818
where TConsoleOut : ConsoleOut
@@ -24,14 +24,16 @@ protected ObservableProcessBase(StartArguments startArguments)
2424
{
2525
StartArguments = startArguments ?? throw new ArgumentNullException(nameof(startArguments));
2626
Process = CreateProcess();
27+
if (startArguments.StandardInputHandler != null)
28+
StandardInputReady += startArguments.StandardInputHandler;
2729
CreateObservable();
2830
}
2931

3032
public virtual IDisposable Subscribe(IObserver<TConsoleOut> observer) => OutStream.Subscribe(observer);
3133

3234
public IDisposable Subscribe(IConsoleOutWriter writer) => OutStream.Subscribe(writer.Write, writer.Write, delegate { });
3335

34-
private readonly ManualResetEvent _completedHandle = new ManualResetEvent(false);
36+
private readonly ManualResetEvent _completedHandle = new(false);
3537

3638
public StreamWriter StandardInput => Process.StandardInput;
3739
public string Binary => StartArguments.Binary;
@@ -57,7 +59,7 @@ private void CreateObservable()
5759

5860
protected abstract IObservable<TConsoleOut> CreateConsoleOutObservable();
5961

60-
public event StartedHandler ProcessStarted = (s) => { };
62+
public event StandardInputHandler StandardInputReady = (s) => { };
6163

6264
protected bool StartProcess(IObserver<TConsoleOut> observer)
6365
{
@@ -77,7 +79,7 @@ protected bool StartProcess(IObserver<TConsoleOut> observer)
7779
// best effort, Process could have finished before even attempting to read .Id and .ProcessName
7880
// which can throw if the process exits in between
7981
}
80-
ProcessStarted(Process.StandardInput);
82+
StandardInputReady(Process.StandardInput);
8183
return true;
8284
}
8385

@@ -185,14 +187,13 @@ public bool WaitForCompletion(TimeSpan timeout)
185187
return false;
186188
}
187189

188-
private readonly object _unpackLock = new object();
189-
private readonly object _sendLock = new object();
190-
private bool _sentControlC = false;
190+
private readonly object _unpackLock = new();
191+
private readonly object _sendLock = new();
192+
private bool _sentControlC;
191193

192-
public void SendControlC()
194+
195+
public bool SendControlC(int processId)
193196
{
194-
if (_sentControlC) return;
195-
if (!ProcessId.HasValue) return;
196197
var platform = (int)Environment.OSVersion.Platform;
197198
var isWindows = platform != 4 && platform != 6 && platform != 128;
198199
if (isWindows)
@@ -201,35 +202,37 @@ public void SendControlC()
201202
UnpackTempOutOfProcessSignalSender(path);
202203
lock (_sendLock)
203204
{
204-
if (_sentControlC) return;
205-
if (!ProcessId.HasValue) return;
206-
var args = new StartArguments(path, ProcessId.Value.ToString(CultureInfo.InvariantCulture))
205+
var args = new StartArguments(path, processId.ToString(CultureInfo.InvariantCulture))
207206
{
208207
WaitForExit = null,
209208
};
210-
var result = Proc.Start(args, TimeSpan.FromSeconds(2));
211-
_sentControlC = true;
209+
var result = Proc.Start(args, TimeSpan.FromSeconds(5));
212210
SendYesForBatPrompt();
211+
return result.ExitCode == 0;
213212
}
214213
}
215214
else
216215
{
217216
lock (_sendLock)
218217
{
219-
if (_sentControlC) return;
220-
if (!ProcessId.HasValue) return;
221218
// I wish .NET Core had signals baked in but looking at the corefx repos tickets this is not happening any time soon.
222-
var args = new StartArguments("kill", "-SIGINT", ProcessId.Value.ToString(CultureInfo.InvariantCulture))
219+
var args = new StartArguments("kill", "-SIGINT", processId.ToString(CultureInfo.InvariantCulture))
223220
{
224221
WaitForExit = null,
225222
};
226-
var result = Proc.Start(args, TimeSpan.FromSeconds(2));
227-
_sentControlC = true;
223+
var result = Proc.Start(args, TimeSpan.FromSeconds(5));
224+
return result.ExitCode == 0;
228225
}
229-
230226
}
227+
}
231228

229+
public void SendControlC()
230+
{
231+
if (_sentControlC) return;
232+
if (!ProcessId.HasValue) return;
232233

234+
var success = SendControlC(ProcessId.Value);
235+
_sentControlC = true;
233236
}
234237

235238
protected void SendYesForBatPrompt()

0 commit comments

Comments
 (0)