Last active
December 22, 2021 17:59
-
-
Save Beej126/e9812b583d1e912a6a08e1af0576975f to your computer and use it in GitHub Desktop.
"dotnet watch" output monitor to autorun other tooling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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