Skip to content

Commit

Permalink
Add static support for long running processes (#13)
Browse files Browse the repository at this point in the history
* Add static support for long running programs with manual started confirmations

* add fsharp bindings
  • Loading branch information
Mpdreamz authored Jan 15, 2024
1 parent 027a0c1 commit c01278c
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 80 deletions.
14 changes: 12 additions & 2 deletions examples/ScratchPad.Fs/Program.fs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
open System
open Proc.Fs

(*
let _ = shell {
exec "dotnet" "--version"
exec "uname"
}

exec { run "dotnet" "--help"}
exec {
binary "dotnet"
arguments "--help"
Expand Down Expand Up @@ -38,7 +39,7 @@ let dotnetVersion = exec {
filter (fun l -> l.Line.Contains "clean")
}
printfn "Found lines %i" dotnetVersion.Length
printfn $"Found lines %i{dotnetVersion.Length}"
let dotnetOptions = exec { binary "dotnet" }
Expand All @@ -55,5 +56,14 @@ let _ = shell { exec "dotnet" args }
let statusCode = exec { exit_code_of "dotnet" "--help"}
exec { run "dotnet" "run" "--project" "examples/ScratchPad.Fs.ArgumentPrinter" "--" "With Space" }
*)

let runningProcess = exec {
binary "dotnet"
arguments "run" "--project" "tests/Proc.Tests.Binary" "--" "TrulyLongRunning"
//wait_until (fun l -> l.Line = "Started!")
wait_until_and_disconnect (fun l -> l.Line = "Started!")
}


printfn "That's all folks!"
39 changes: 38 additions & 1 deletion src/Proc.Fs/Bindings.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type ExecOptions = {
Timeout: TimeSpan
ValidExitCodeClassifier: (int -> bool) option

StartedConfirmationHandler: (LineOut -> bool) option
StopBufferingAfterStarted: bool option

NoWrapInThread: bool option
SendControlCFirst: bool option
WaitForStreamReadersTimeout: TimeSpan option
Expand All @@ -26,7 +29,8 @@ with
LineOutFilter = None; WorkingDirectory = None; Environment = None
Timeout = TimeSpan(0, 0, 0, 0, -1)
ValidExitCodeClassifier = None;
NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None;
NoWrapInThread = None; SendControlCFirst = None; WaitForStreamReadersTimeout = None
StartedConfirmationHandler = None; StopBufferingAfterStarted = None
}

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

let private longRunningArguments (opts: ExecOptions) =
let args = opts.Arguments |> Option.defaultValue []
let longRunningArguments = LongRunningArguments(opts.Binary, args)
opts.LineOutFilter |> Option.iter(fun f -> longRunningArguments.LineOutFilter <- f)
opts.Environment |> Option.iter(fun e -> longRunningArguments.Environment <- e)
opts.WorkingDirectory |> Option.iter(fun d -> longRunningArguments.WorkingDirectory <- d)
opts.NoWrapInThread |> Option.iter(fun b -> longRunningArguments.NoWrapInThread <- b)
opts.SendControlCFirst |> Option.iter(fun b -> longRunningArguments.SendControlCFirst <- b)
opts.WaitForStreamReadersTimeout |> Option.iter(fun t -> longRunningArguments.WaitForStreamReadersTimeout <- t)

opts.StartedConfirmationHandler |> Option.iter(fun t -> longRunningArguments.StartedConfirmationHandler <- t)
opts.StopBufferingAfterStarted |> Option.iter(fun t -> longRunningArguments.StopBufferingAfterStarted <- t)

longRunningArguments


type ShellBuilder() =

Expand Down Expand Up @@ -251,6 +270,24 @@ type ExecBuilder() =
let startArgs = startArgs opts
Proc.Start(startArgs, opts.Timeout)

[<CustomOperation("wait_until")>]
member this.WaitUntil(opts, startedConfirmation: LineOut -> bool) =
let opts = { opts with StartedConfirmationHandler = Some startedConfirmation }
let longRunningArguments = longRunningArguments opts
Proc.StartLongRunning(longRunningArguments, opts.Timeout)

[<CustomOperation("wait_until_and_disconnect")>]
member this.WaitUntilQuietAfter(opts, startedConfirmation: LineOut -> bool) =
let opts =
{
opts with
StartedConfirmationHandler = Some startedConfirmation
StopBufferingAfterStarted = Some true
}
let longRunningArguments = longRunningArguments opts
Proc.StartLongRunning(longRunningArguments, opts.Timeout)



let exec = ExecBuilder()

Expand Down
21 changes: 21 additions & 0 deletions src/Proc.Fs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,24 @@ let helpOutput = exec {
```

returns the exit code and the full console output.

```fsharp
let process = exec {
binary "dotnet"
arguments "--help"
wait_until (fun l -> l.Line.Contains "clean")
}
```

returns an already running process but only after it confirms a line was printed
```fsharp
let process = exec {
binary "dotnet"
arguments "--help"
wait_until_and_disconnect (fun l -> l.Line.Contains "clean")
}
```

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.
3 changes: 0 additions & 3 deletions src/Proc/ExecArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ public ExecArguments(string binary, IEnumerable<string> args) : base(binary, arg

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

/// <summary> Force arguments and the current working director NOT to be part of the exception message </summary>
public bool OnlyPrintBinaryInExceptionMessage { get; set; }

public Func<int, bool> ValidExitCodeClassifier
{
get => _validExitCodeClassifier ?? (c => c == 0);
Expand Down
24 changes: 24 additions & 0 deletions src/Proc/LongRunningArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using ProcNet.Std;

namespace ProcNet;

public class LongRunningArguments : StartArguments
{
public LongRunningArguments(string binary, IEnumerable<string> args) : base(binary, args) { }

public LongRunningArguments(string binary, params string[] args) : base(binary, args) { }

/// <summary>
/// A handler that will delay return of the <see cref="IDisposable"/> process until startup is confirmed over
/// standard out/error.
/// </summary>
public Func<LineOut, bool> StartedConfirmationHandler { get; set; }

/// <summary>
/// A helper that sets <see cref="StartArguments.KeepBufferingLines"/> and stops immediately after <see cref="StartedConfirmationHandler"/>
/// indicates the process has started.
/// </summary>
public bool StopBufferingAfterStarted { get; set; }
}
7 changes: 6 additions & 1 deletion src/Proc/ObservableProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,12 @@ public IDisposable Subscribe(IObserver<LineOut> observerLines, IObserver<Charact
)
.ToArray();
})
.TakeWhile(KeepBufferingLines)
.TakeWhile(l =>
{
var keepBuffering = StartArguments.KeepBufferingLines ?? KeepBufferingLines;
var keep = keepBuffering?.Invoke(l);
return keep.GetValueOrDefault(true);
})
.Where(l => l != null)
.Where(observeLinesFilter)
.Subscribe(
Expand Down
45 changes: 24 additions & 21 deletions src/Proc/ObservableProcessBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace ProcNet
{
public delegate void StartedHandler(StreamWriter standardInput);
public delegate void StandardInputHandler(StreamWriter standardInput);

public abstract class ObservableProcessBase<TConsoleOut> : IObservableProcess<TConsoleOut>
where TConsoleOut : ConsoleOut
Expand All @@ -24,14 +24,16 @@ protected ObservableProcessBase(StartArguments startArguments)
{
StartArguments = startArguments ?? throw new ArgumentNullException(nameof(startArguments));
Process = CreateProcess();
if (startArguments.StandardInputHandler != null)
StandardInputReady += startArguments.StandardInputHandler;
CreateObservable();
}

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

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

private readonly ManualResetEvent _completedHandle = new ManualResetEvent(false);
private readonly ManualResetEvent _completedHandle = new(false);

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

protected abstract IObservable<TConsoleOut> CreateConsoleOutObservable();

public event StartedHandler ProcessStarted = (s) => { };
public event StandardInputHandler StandardInputReady = (s) => { };

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

Expand Down Expand Up @@ -185,14 +187,13 @@ public bool WaitForCompletion(TimeSpan timeout)
return false;
}

private readonly object _unpackLock = new object();
private readonly object _sendLock = new object();
private bool _sentControlC = false;
private readonly object _unpackLock = new();
private readonly object _sendLock = new();
private bool _sentControlC;

public void SendControlC()

public bool SendControlC(int processId)
{
if (_sentControlC) return;
if (!ProcessId.HasValue) return;
var platform = (int)Environment.OSVersion.Platform;
var isWindows = platform != 4 && platform != 6 && platform != 128;
if (isWindows)
Expand All @@ -201,35 +202,37 @@ public void SendControlC()
UnpackTempOutOfProcessSignalSender(path);
lock (_sendLock)
{
if (_sentControlC) return;
if (!ProcessId.HasValue) return;
var args = new StartArguments(path, ProcessId.Value.ToString(CultureInfo.InvariantCulture))
var args = new StartArguments(path, processId.ToString(CultureInfo.InvariantCulture))
{
WaitForExit = null,
};
var result = Proc.Start(args, TimeSpan.FromSeconds(2));
_sentControlC = true;
var result = Proc.Start(args, TimeSpan.FromSeconds(5));
SendYesForBatPrompt();
return result.ExitCode == 0;
}
}
else
{
lock (_sendLock)
{
if (_sentControlC) return;
if (!ProcessId.HasValue) return;
// I wish .NET Core had signals baked in but looking at the corefx repos tickets this is not happening any time soon.
var args = new StartArguments("kill", "-SIGINT", ProcessId.Value.ToString(CultureInfo.InvariantCulture))
var args = new StartArguments("kill", "-SIGINT", processId.ToString(CultureInfo.InvariantCulture))
{
WaitForExit = null,
};
var result = Proc.Start(args, TimeSpan.FromSeconds(2));
_sentControlC = true;
var result = Proc.Start(args, TimeSpan.FromSeconds(5));
return result.ExitCode == 0;
}

}
}

public void SendControlC()
{
if (_sentControlC) return;
if (!ProcessId.HasValue) return;

var success = SendControlC(ProcessId.Value);
_sentControlC = true;
}

protected void SendYesForBatPrompt()
Expand Down
Loading

0 comments on commit c01278c

Please sign in to comment.