Skip to content

Instantly share code, notes, and snippets.

@ruxo
Last active June 1, 2024 23:48
Show Gist options
  • Save ruxo/2c29ec0b4f7e0f282a84abbc58584685 to your computer and use it in GitHub Desktop.
Save ruxo/2c29ec0b4f7e0f282a84abbc58584685 to your computer and use it in GitHub Desktop.
A helper library to assist running multiple command lines and combine their output into the current console. See `sample.fsx` about how to use it. To run the sample, use the command line `dotnet fsi sample.fsx`
[<AutoOpen>]
module RunHelpers
#r "nuget: FSharp.Control.Reactive"
open System
open System.Diagnostics
open System.IO
open System.Threading
open FSharp.Control.Reactive
printfn "run-helpers 0.1.0"
module private Process =
let private streamToObservable (stream: StreamReader) =
let observable = Subject.broadcast
let rec loop () =
async {
let! line = stream.ReadLineAsync() |> Async.AwaitTask
if line = null then
observable |> Subject.onCompleted |> ignore
return ()
else
observable |> Subject.onNext line |> ignore
return! loop ()
}
Async.StartAsTask (loop ()) |> ignore
observable :> IObservable<string>
let getProcessOutput (p: Process) =
let toObservable = streamToObservable >> Observable.map (fun line -> $"[{p.Id}] {line}")
let standard = p.StandardOutput |> toObservable
let error = p.StandardError |> toObservable
Observable.mergeSeq [standard; error]
let run (commands: string) =
let parts = commands.Split(' ', StringSplitOptions.RemoveEmptyEntries)
let start_info = match parts.Length with
| 0 -> failwith "No command provided"
| 1 -> ProcessStartInfo(parts[0])
| 2 -> ProcessStartInfo(parts[0], parts[1])
| _ -> ProcessStartInfo(parts[0], parts[1..])
start_info.UseShellExecute <- false
start_info.RedirectStandardInput <- true
start_info.RedirectStandardOutput <- true
start_info.RedirectStandardError <- true
start_info
let startFrom (paths: string seq) (start_info: ProcessStartInfo) =
let current = Directory.GetCurrentDirectory()
let full_path = paths |> Seq.toArray |> Path.Combine
start_info.WorkingDirectory <- full_path
full_path |> Directory.SetCurrentDirectory
let p = start_info |> Process.Start
Directory.SetCurrentDirectory current
p
[<Literal>]
let private ControlCSequence = "\x3"
let consumeOutput (processes: Process seq) =
use wait_to_quit = new AutoResetEvent(false)
let checkProcessesExited() =
if processes |> Seq.forall _.HasExited then
wait_to_quit.Set() |> ignore
true
else
false
let output = processes |> Seq.map Process.getProcessOutput
|> Observable.mergeSeq
use subscription =
output |> Observable.subscribeObserver
{ new IObserver<string> with
member _.OnNext value = printfn $"%s{value}"
member _.OnError ex = printfn $"Error: {ex.Message}"
member _.OnCompleted () = checkProcessesExited() |> ignore
}
Console.CancelKeyPress.Add(
fun args ->
processes |> Seq.iter (fun p -> p.StandardInput.WriteLine ControlCSequence)
let ``continue`` = not <| checkProcessesExited()
args.Cancel <- ``continue``
printfn $"Cancel: {``continue``}"
)
wait_to_quit.WaitOne()
#load "run-helpers.fsx"
[<Literal>]
let dms_path = @"C:\Workspace\TCRB\Document"
let all_processes =
[ run "dotnet run" |> startFrom [ dms_path; @"dms\src\DMS.Api" ]
run "dotnet run" |> startFrom [ dms_path; @"dms\src\DMS.Background"]
run "yarn.cmd dev" |> startFrom [ dms_path; "dms-fe" ]
]
printfn "Start!"
all_processes |> consumeOutput
printfn "Done!"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment