Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static support for long running processes #13

Merged
merged 2 commits into from
Jan 15, 2024
Merged
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
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
Loading