Skip to content

Instantly share code, notes, and snippets.

@Beej126
Last active December 22, 2021 17:59
Show Gist options
  • Save Beej126/e9812b583d1e912a6a08e1af0576975f to your computer and use it in GitHub Desktop.
Save Beej126/e9812b583d1e912a6a08e1af0576975f to your computer and use it in GitHub Desktop.
"dotnet watch" output monitor to autorun other tooling
# all syntax is pwsh v7+ compatible
###########################################
# the basic scenario is wanting to fire another codegen tool whenever `dotnet watch run` reports a change to specific source files
# it's very fortunate dotnet watch already reports exactly what we need to trigger on
###########################################
# this code is very customized to watch my ServiceStack webapi project
# and run the ServiceStack CLI tooling that autogens typescript DTOs from C# DTOs.
#
# i've also tailored to watching for specific errors, and trimming the debug output verbosity and changing console text color to catch the eye
#
# it's expected that you would gut those specifics and tailor to your own needs
###########################################
# basic stdout approach taken from: https://stackoverflow.com/questions/24370814/how-to-capture-process-output-asynchronously-in-powershell/#58667388
# .net Process API reference: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.datareceivedeventargs.data
# related issue threads:
# https://github.com/dotnet/aspnetcore/issues/4108
# https://github.com/dotnet/aspnetcore/issues/10256
# i posted a discussion since the issues were closed:
# https://github.com/dotnet/aspnetcore/discussions/29794
# then the aspnetcore teams eradicated their discussions area <sigh>
Using namespace System.Diagnostics;
[console]::TreatControlCAsInput = $true # safely trap CTRL+C so we don't leave orphaned processes behind, nice! =)
# shifted to doing the stdout event handler in C# to see if it would keep up better
# and sure enough it was *** dramatically faster ***
# it was quite pokey in pure powershell when there was a lot of output streaming thru (e.g. exception callstacks)
# admittedly, a bunch of raw C# in ps1 with no syntax highlighting is no fun but keeping it all in this one file is appealing
# i wish i could shift this whole thing to C# without any ps1 but there doesn't yet seem to be a direct way to run pure .cs as a script from command line yet
# e.g. https://github.com/dotnet/csharplang/issues/3502
Add-Type @"
// https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.datareceivedeventargs.data?view=netstandard-2.1#examples
using System;
using System.Diagnostics;
using System.Linq;
public static class ProcessHelper
{
private static string DtoWorkingDirectory;
private static bool IsChangeTriggered;
private static string SkipTime; //certain errors like "cannot resolve host" mean we can skip MANY lines of subsequent error output
//this rudimentary approach skips everything reported during the same **second** and seems to work very practical for the situation
private static string LastTimeStamp;
public static string GetTimeStamp => String.Format("[{0:hh:mm:ss}] ", DateTime.Now);
public static void WriteLineEx(string value, ConsoleColor? color = null, bool includeTime = true) {
var now = GetTimeStamp;
if ( includeTime && (now != LastTimeStamp) ) {
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\n" + GetTimeStamp);
Console.ResetColor();
}
LastTimeStamp = now;
if (color != null) Console.ForegroundColor = color.Value;
Console.WriteLine(value);
Console.ResetColor();
}
public static void RegisterStdOutEventHandler(string dtoWorkingDirectory, Process process)
{
DtoWorkingDirectory = dtoWorkingDirectory;
IsChangeTriggered = true; //set true to fire a DTO regen upon start to make sure we're synced up from the get go
process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
{
if (!String.IsNullOrEmpty(e.Data))
{
var timeStamp = GetTimeStamp;
if (SkipTime == timeStamp) return;
else if (e.Data.StartsWith(" ORA-")) {
SkipTime = timeStamp;
WriteLineEx(e.Data, ConsoleColor.Red);
if (e.Data == " ORA-12545: Network Transport: Unable to resolve connect hostname")
WriteLineEx("Hopefully you just need to connect your VPN", ConsoleColor.Yellow, false);
return;
}
else if (e.Data == " Not Authenticated") {
SkipTime = timeStamp;
WriteLineEx(e.Data, ConsoleColor.Yellow);
return;
}
else SkipTime = null;
//just a simple blank line between sql statements to help see them better
if (e.Data.StartsWith("SQL: ")) Console.WriteLine("");
WriteLineEx(e.Data);
CheckForKeyStrings(e.Data);
}
});
}
private static void CheckForKeyStrings(string stdLine)
{
if (stdLine.StartsWith("watch : File changed"))
{
if ( new[]{
"KrakenStack.Service", //either the DTO definitions have changed -OR- the API implementation
"AppHost" //where the typescript codegen customizations are
}.Any(keyString => stdLine.Contains(keyString)) )
{
IsChangeTriggered = true;
WriteLineEx("*** triggered and waiting for 'Initializing Application' reponse to regen dtos...", ConsoleColor.Yellow);
}
}
// ...but we must wait till service has been restarted before trying to hit its typescript gen api
else if (stdLine.StartsWith(" Initializing Application") && IsChangeTriggered)
{
regenDtos();
}
}
public static void regenDtos()
{
IsChangeTriggered = false;
WriteLineEx("* regenerating typescript definitions...");
// x.exe is ServiceStack's CLI that code gens typescript dto's from one of its built in web apis - https://docs.servicestack.net/dotnet-tool
using var process = new Process();
process.StartInfo.WorkingDirectory = DtoWorkingDirectory;
process.StartInfo.FileName = "x.exe";
process.StartInfo.Arguments = "typescript";
process.StartInfo.UseShellExecute = false;
// process.StartInfo.CreateNoWindow = true;
process.Start();
}
}
"@
function global:printHelp {
Write-Host "`n-------------------------------------"
Write-Host "commands:" -fore yellow
write-host "---------"
Write-Host "d: dto refresh"
Write-Host "r: recompile"
Write-Host "s: print output separator ""---"""
Write-Host "c: clear console"
Write-Host "q: quit the watcher"
Write-Host "-------------------------------------`n"
}
function global:printSeparator {
Write-Host "$([ProcessHelper]::GetTimeStamp)-----------------`n" -fore yellow
}
$process = [Process]::New()
$process.StartInfo.UseShellExecute = $false # required for output stream capture
$process.StartInfo.CreateNoWindow = $true # !!!! crucial to get async stdout !!!!
$process.StartInfo.RedirectStandardOutput = $true # fyi, RedirectStandardOutput generates "OutputDataReceived" events (.RedirectStandardError = $true would generate "ErrorDataReceived" events if ever needed)
$process.StartInfo.WorkingDirectory = $PSScriptRoot
$process.StartInfo.FileName = "dotnet.exe"
$process.StartInfo.Arguments = "watch run KrakenStack.sln --project KrakenStack\KrakenStack.csproj"
# here's where the C# kicks in
[ProcessHelper]::RegisterStdOutEventHandler($PSScriptRoot + "\..\spa\src\app", $process)
if (-not $process.Start()) { write-host "Error occurred trying to launch " + $process.StartInfo.FileName; Exit 0; }
$process.BeginOutputReadLine() # for StandardError we'd need to do BeginErrorReadLine()
printHelp
Write-Host "Starting up dotnet watch..."
# https://powershell.one/tricks/input-devices/detect-key-press
while ($true) {
$process.WaitForExit(500) | Out-Null
if ([Console]::KeyAvailable) {
$keyinput = [Console]::ReadKey($true)
if ($keyinput.Key -eq "D") { [ProcessHelper]::regenDtos() }
elseif ($keyinput.Key -eq "R") { touch ./KrakenStack/Configure.AppHost.cs } # force a compile by touching monitored file
elseif ($keyinput.Key -eq "S") { Write-Host ""; printSeparator }
elseif ($keyinput.Key -eq "C") { cls; printSeparator }
elseif ($keyinput.Key -eq "Q") { break }
else { printHelp }
}
}
# cleanup event subscriptions and process
# fyi, process.Close() wasn't enough to terminate the stack of processes spawned from `dotnet watch`
$process.Kill()
write-host "`nEverything has been shut down. Bye =)`n" -fore yellow
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment