diff --git a/.github/workflows/dotnet-format.yaml b/.github/workflows/dotnet-format.yaml new file mode 100644 index 0000000..da1b8f7 --- /dev/null +++ b/.github/workflows/dotnet-format.yaml @@ -0,0 +1,69 @@ +name: "Dotnet Format" + +# Controls when the action will run. +on: + schedule: + # Weekly At 19:00 on Monday. - 5am Australian/Brisbane time + - cron: '0 19 * * 1' + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + # Allows you to run this workflow manually from the Actions tab + +jobs: + build: + name: "Format code and Create Pull Request if any changes" + runs-on: ubuntu-latest + permissions: + contents: write # Read Required to check out code, Write to create Git Tags + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Run dotnet format + run: dotnet format --verbosity normal + working-directory: ./source + + - name: Check for changes + id: detect_changes + run: | + set +e + git diff --quiet + + if [ "$?" -eq 0 ]; then + echo "No changes detected." + else + echo "Changes detected." + echo "changes_detected=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request if Changes Detected + if: steps.detect_changes.outputs.changes_detected == 'true' + env: + GH_TOKEN: ${{ secrets.RENOVATE_GITHUB_TOKEN }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + git config user.email "bob@octopus.com" + git config user.name "GitHub Actions" + + dateTimeStamp="$(date +'%Y%m%d%H%M%S')" + branchName="dotnet-format/$dateTimeStamp" + + git checkout -b "$branchName" + git add -A ./source + git commit -m "Run dotnet format" + git push -f --set-upstream origin "$branchName" + + # Always target PR's at the default branch + targetBranchName="${{ github.event.repository.default_branch }}" + gh pr create -t "$branchName" --head "$branchName" --base "$targetBranchName" --body "This PR was created automatically by the dotnet-format workflow." + diff --git a/source/Shellfish/CallbackOutputTarget.cs b/source/Shellfish/CallbackOutputTarget.cs index 0a331fe..38a2644 100644 --- a/source/Shellfish/CallbackOutputTarget.cs +++ b/source/Shellfish/CallbackOutputTarget.cs @@ -13,7 +13,7 @@ public static partial class ShellCommandExtensionMethods { public static ShellCommand WithStdOutTarget(this ShellCommand shellCommand, Action callback) => shellCommand.WithStdOutTarget(new CallbackOutputTarget(callback)); - + public static ShellCommand WithStdErrTarget(this ShellCommand shellCommand, Action callback) => shellCommand.WithStdErrTarget(new CallbackOutputTarget(callback)); } \ No newline at end of file diff --git a/source/Shellfish/InputQueue.cs b/source/Shellfish/InputQueue.cs index 6f520c3..a16dcfe 100644 --- a/source/Shellfish/InputQueue.cs +++ b/source/Shellfish/InputQueue.cs @@ -70,17 +70,17 @@ async Task BeginMessagePump() switch (notification.Type) { case NotificationType.Empty: // wait for the next wakeup signal - { - TaskCompletionSource sig; - lock (queue) { - sig = wakeupSignal; + TaskCompletionSource sig; + lock (queue) + { + sig = wakeupSignal; + } + + await sig.Task; + continue; // go round again } - await sig.Task; - continue; // go round again - } - case NotificationType.Next when notification.Line is not null: // onNext await processStdInput.WriteLineAsync(notification.Line); await processStdInput.FlushAsync(); @@ -90,8 +90,8 @@ async Task BeginMessagePump() processStdInput.Close(); return; // exit the entire message pump - // Normally we would have a default: case which logged or threw an "Unhandled case" exception, - // but we are a background task, there's nobody to observe such a thing. + // Normally we would have a default: case which logged or threw an "Unhandled case" exception, + // but we are a background task, there's nobody to observe such a thing. } } } diff --git a/source/Shellfish/PasteArguments.cs b/source/Shellfish/PasteArguments.cs index 359a738..b500165 100644 --- a/source/Shellfish/PasteArguments.cs +++ b/source/Shellfish/PasteArguments.cs @@ -22,14 +22,14 @@ static class PasteArguments internal static string JoinArguments(IEnumerable arguments) { var stringBuilder = new StringBuilder(); - foreach(var argument in arguments) + foreach (var argument in arguments) { AppendArgument(stringBuilder, argument); } return stringBuilder.ToString(); } - + internal static void AppendArgument(StringBuilder stringBuilder, string argument) { if (stringBuilder.Length != 0) diff --git a/source/Shellfish/ShellCommand.cs b/source/Shellfish/ShellCommand.cs index 3010d7f..9be9dd5 100644 --- a/source/Shellfish/ShellCommand.cs +++ b/source/Shellfish/ShellCommand.cs @@ -210,15 +210,15 @@ public string ToString(bool includeArguments) switch (arguments) { case ShellCommandArguments.StringType s: - { - var argumentsAsString = includeArguments ? s.Value : ""; - return $"{executable} {argumentsAsString}"; - } + { + var argumentsAsString = includeArguments ? s.Value : ""; + return $"{executable} {argumentsAsString}"; + } case ShellCommandArguments.ArgumentListType { Values.Length: > 0 } l: - { - var argumentsAsString = includeArguments ? PasteArguments.JoinArguments(l.Values) : $"<{l.Values.Length} arguments>"; - return $"{executable} {argumentsAsString}"; - } + { + var argumentsAsString = includeArguments ? PasteArguments.JoinArguments(l.Values) : $"<{l.Values.Length} arguments>"; + return $"{executable} {argumentsAsString}"; + } default: return executable; diff --git a/source/Shellfish/ShellCommandArguments.cs b/source/Shellfish/ShellCommandArguments.cs index 5699721..f8e4db1 100644 --- a/source/Shellfish/ShellCommandArguments.cs +++ b/source/Shellfish/ShellCommandArguments.cs @@ -5,16 +5,16 @@ abstract class ShellCommandArguments public static NoArgumentsType None { get; } = new(); public static StringType String(string value) => new(value); public static ArgumentListType List(string[] value) => new(value); - + // Don't construct this type directly, use ShellCommandArguments.None public class NoArgumentsType : ShellCommandArguments; - + // Don't construct this type directly, use ShellCommandArguments.String(value) public class StringType(string value) : ShellCommandArguments { public string Value { get; } = value; } - + // Don't construct this type directly, use ShellCommandArguments.List(value) public class ArgumentListType(string[] values) : ShellCommandArguments { diff --git a/source/Shellfish/ShellCommandOptions.cs b/source/Shellfish/ShellCommandOptions.cs index 57a90d9..5b41dfe 100644 --- a/source/Shellfish/ShellCommandOptions.cs +++ b/source/Shellfish/ShellCommandOptions.cs @@ -8,7 +8,7 @@ public enum ShellCommandOptions /// Default value, equivalent to not specifying any options. /// None = 0, - + /// /// By default, if the CancellationToken is cancelled, the running process will be killed, and an OperationCanceledException /// will be thrown, like the vast majority of other .NET code. diff --git a/source/Shellfish/ShellfishProcess.cs b/source/Shellfish/ShellfishProcess.cs index 5466ccb..28975c1 100644 --- a/source/Shellfish/ShellfishProcess.cs +++ b/source/Shellfish/ShellfishProcess.cs @@ -155,7 +155,7 @@ public int SafelyGetExitCode() } } - // Common code for Execute and ExecuteAsync to handle stdin and stdout streaming + // Common code for Execute and ExecuteAsync to handle stdin and stdout streaming void BeginIoStreams() { if (stdOutRedirected) process.BeginOutputReadLine(); @@ -180,8 +180,8 @@ void ConfigureArguments(ShellCommandArguments arguments) process.StartInfo.Arguments = PasteArguments.JoinArguments(l.Values); #endif break; - - // Deliberately no default case here: ShellCommandArguments.NoArgumentsType and Empty list are no-ops + + // Deliberately no default case here: ShellCommandArguments.NoArgumentsType and Empty list are no-ops } } diff --git a/source/Shellfish/StringBuilderOutputTarget.cs b/source/Shellfish/StringBuilderOutputTarget.cs index 2b76a9b..e83491b 100644 --- a/source/Shellfish/StringBuilderOutputTarget.cs +++ b/source/Shellfish/StringBuilderOutputTarget.cs @@ -13,7 +13,7 @@ public static partial class ShellCommandExtensionMethods { public static ShellCommand WithStdOutTarget(this ShellCommand shellCommand, StringBuilder stringBuilder) => shellCommand.WithStdOutTarget(new StringBuilderOutputTarget(stringBuilder)); - + public static ShellCommand WithStdErrTarget(this ShellCommand shellCommand, StringBuilder stringBuilder) => shellCommand.WithStdErrTarget(new StringBuilderOutputTarget(stringBuilder)); } \ No newline at end of file diff --git a/source/Shellfish/Windows/AccessToken.cs b/source/Shellfish/Windows/AccessToken.cs index 8391c1e..5584ad4 100644 --- a/source/Shellfish/Windows/AccessToken.cs +++ b/source/Shellfish/Windows/AccessToken.cs @@ -33,7 +33,7 @@ static SafeAccessTokenHandle LogonUser(string username, Interop.Advapi32.LogonType logonType, Interop.Advapi32.LogonProvider logonProvider) { - if(!Interop.Advapi32.LogonUser(username, domain, password, logonType, logonProvider, out var handle)) + if (!Interop.Advapi32.LogonUser(username, domain, password, logonType, logonProvider, out var handle)) throw new Win32Exception(); return handle; diff --git a/source/Shellfish/Windows/Interop.cs b/source/Shellfish/Windows/Interop.cs index 86c1fa6..04f36bb 100644 --- a/source/Shellfish/Windows/Interop.cs +++ b/source/Shellfish/Windows/Interop.cs @@ -59,7 +59,7 @@ internal static extern bool GetCPInfoEx([MarshalAs(UnmanagedType.U4)] int codePa [MarshalAs(UnmanagedType.U4)] int dwFlags, out CpInfoEx lpCPInfoEx); - + const int MAX_DEFAULTCHAR = 2; const int MAX_LEADBYTES = 12; const int MAX_PATH = 260; @@ -105,7 +105,7 @@ internal static class Userenv // See https://msdn.microsoft.com/en-us/library/windows/desktop/bb762274(v=vs.85).aspx [DllImport(Libraries.Userenv, SetLastError = true)] internal static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); - + // See https://msdn.microsoft.com/en-us/library/windows/desktop/bb762281(v=vs.85).aspx [DllImport(Libraries.Userenv, SetLastError = true)] internal static extern bool LoadUserProfile(SafeAccessTokenHandle hToken, ref ProfileInfo lpProfileInfo); diff --git a/source/Shellfish/Windows/WindowStationAndDesktopAccess.cs b/source/Shellfish/Windows/WindowStationAndDesktopAccess.cs index 67f8180..de14cee 100644 --- a/source/Shellfish/Windows/WindowStationAndDesktopAccess.cs +++ b/source/Shellfish/Windows/WindowStationAndDesktopAccess.cs @@ -17,7 +17,7 @@ public static void GrantAccessToWindowStationAndDesktop(string username, string? var hWindowStation = GetProcessWindowStation(); const int windowStationAllAccess = 0x000f037f; GrantAccess(username, domainName, hWindowStation, windowStationAllAccess); - + var hDesktop = GetThreadDesktop(); const int desktopRightsAllAccess = 0x000f01ff; GrantAccess(username, domainName, hDesktop, desktopRightsAllAccess); diff --git a/source/Tests/Plumbing/WindowsFactAttribute.cs b/source/Tests/Plumbing/WindowsFactAttribute.cs index 33cd8f3..21e5e2a 100644 --- a/source/Tests/Plumbing/WindowsFactAttribute.cs +++ b/source/Tests/Plumbing/WindowsFactAttribute.cs @@ -9,16 +9,16 @@ public sealed class WindowsFactAttribute : FactAttribute { public WindowsFactAttribute() { - if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = $"This test only runs on Windows"; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = $"This test only runs on Windows"; } } - + [AttributeUsage(AttributeTargets.Method)] public sealed class WindowsTheoryAttribute : TheoryAttribute { public WindowsTheoryAttribute() { - if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = $"This test only runs on Windows"; + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Skip = $"This test only runs on Windows"; } } } \ No newline at end of file diff --git a/source/Tests/Plumbing/WindowsUserClassFixture.cs b/source/Tests/Plumbing/WindowsUserClassFixture.cs index 33b4c9b..0d44aae 100644 --- a/source/Tests/Plumbing/WindowsUserClassFixture.cs +++ b/source/Tests/Plumbing/WindowsUserClassFixture.cs @@ -4,7 +4,7 @@ public class WindowsUserClassFixture { static readonly object Gate = new(); - + const string Username = "test-shellexecutor"; internal TestUserPrincipal User { get; } diff --git a/source/Tests/ShellCommandFixture.StdInput.cs b/source/Tests/ShellCommandFixture.StdInput.cs index 2176cc5..1343603 100644 --- a/source/Tests/ShellCommandFixture.StdInput.cs +++ b/source/Tests/ShellCommandFixture.StdInput.cs @@ -14,7 +14,7 @@ public class ShellCommandFixtureStdInput { readonly CancellationTokenSource cancellationTokenSource = new(ShellCommandFixture.TestTimeout); CancellationToken CancellationToken => cancellationTokenSource.Token; - + [Theory, InlineData(SyncBehaviour.Sync), InlineData(SyncBehaviour.Async)] public async Task ShouldWork(SyncBehaviour behaviour) { @@ -71,7 +71,7 @@ read lastname var stdOut = new StringBuilder(); var stdErr = new StringBuilder(); - + // it's going to ask us for the names, we need to answer back or the process will stall forever; we can preload this var stdIn = new TestInputSource(); @@ -94,7 +94,7 @@ read lastname stdErr.ToString().Should().BeEmpty("no messages should be written to stderr"); stdOut.ToString().Should().Be("Enter First Name:" + Environment.NewLine + "Enter Last Name:" + Environment.NewLine + "Hello 'Bob' 'Octopus'" + Environment.NewLine); } - + [Theory, InlineData(SyncBehaviour.Sync), InlineData(SyncBehaviour.Async)] public async Task ClosingStdInEarly(SyncBehaviour behaviour) { @@ -117,7 +117,7 @@ read lastname var stdOut = new StringBuilder(); var stdErr = new StringBuilder(); - + // it's going to ask us for the names, we need to answer back or the process will stall forever; we can preload this var stdIn = new TestInputSource(); @@ -141,7 +141,7 @@ read lastname // When we close stdin the waiting process receives an EOF; Our trivial shell script interprets this as an empty string stdOut.ToString().Should().Be("Enter First Name:" + Environment.NewLine + "Enter Last Name:" + Environment.NewLine + "Hello 'Bob' ''" + Environment.NewLine); } - + [Theory, InlineData(SyncBehaviour.Sync), InlineData(SyncBehaviour.Async)] public async Task ShouldReleaseInputSourceWhenProgramExits(SyncBehaviour behaviour) { @@ -171,14 +171,14 @@ read firstname .WithStdOutTarget(l => { stdIn.Subscriber.Should().NotBeNull("the shellcommand should still be subscribed to the input source while the process is running"); - + // when we receive the first prompt, cancel and kill the process if (l.Contains("Enter First Name:")) stdIn.OnNext("Bob"); }) .WithStdErrTarget(stdErr); stdIn.Subscriber.Should().BeNull("the shellcommand should not subscribe to the input source until the process starts"); - + var result = behaviour == SyncBehaviour.Async ? await executor.ExecuteAsync(CancellationToken) : executor.Execute(CancellationToken); @@ -186,7 +186,7 @@ read firstname result.ExitCode.Should().Be(0, "the process should have run to completion"); stdErr.ToString().Should().BeEmpty("no messages should be written to stderr"); stdOut.ToString().Should().Be("Enter First Name:" + Environment.NewLine + "Hello 'Bob'" + Environment.NewLine); - + stdIn.Subscriber.Should().BeNull("the shellcommand should have unsubscribed from the input source after the process exits"); } @@ -294,7 +294,7 @@ read name "Enter Name:" + Environment.NewLine + "Hello ''" + Environment.NewLine, ], because: "When we cancel the process we close StdIn and it shuts down. The process observes the EOF as empty string and prints 'Hello ' but there is a benign race condition which means we may not observe this output. Test needs to handle both cases"); } - + // If someone wants to have an interactive back-and-forth with a process, they // can use a type like this to do it. We don't want to quite commit to putting it // in the public API though until we have a stronger use-case for it. @@ -314,7 +314,7 @@ public void OnNext(string line) { Subscriber?.OnNext(line); } - + public void OnCompleted() { Subscriber?.OnCompleted(); diff --git a/source/Tests/ShellCommandFixture.Windows.cs b/source/Tests/ShellCommandFixture.Windows.cs index 8304516..5bbb65e 100644 --- a/source/Tests/ShellCommandFixture.Windows.cs +++ b/source/Tests/ShellCommandFixture.Windows.cs @@ -29,7 +29,7 @@ static ShellCommandFixtureWindows() #endif readonly TestUserPrincipal user = fx.User; - + // If unspecified, ShellCommand will default to the current directory, which our temporary user may not have access to. // Our tests that run as a different user need to set a different working directory or they may fail. readonly string commonAppDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); diff --git a/source/Tests/ShellCommandFixture.cs b/source/Tests/ShellCommandFixture.cs index 6b6c54b..70eadb3 100644 --- a/source/Tests/ShellCommandFixture.cs +++ b/source/Tests/ShellCommandFixture.cs @@ -94,7 +94,7 @@ public async Task CancellationToken_ShouldForceKillTheProcess(SyncBehaviour beha Process? process = null; executor = executor .CaptureProcess(p => process = p); - // Do not capture stdout or stderr; the windows timeout command will fail with ERROR: Input redirection is not supported + // Do not capture stdout or stderr; the windows timeout command will fail with ERROR: Input redirection is not supported var cancellationToken = cts.Token; if (behaviour == SyncBehaviour.Async) @@ -127,7 +127,7 @@ public async Task CancellationToken_ShouldForceKillTheProcess_DoNotThrowOnCancel executor = executor .WithOptions(ShellCommandOptions.DoNotThrowOnCancellation) .CaptureProcess(p => process = p); - // Do not capture stdout or stderr; the windows timeout command will fail with ERROR: Input redirection is not supported + // Do not capture stdout or stderr; the windows timeout command will fail with ERROR: Input redirection is not supported var result = behaviour == SyncBehaviour.Async ? await executor.ExecuteAsync(cts.Token) @@ -340,7 +340,7 @@ setlocal enabledelayedexpansion string[] runScriptArgs = tempScript.GetCommandArgs(); var executor = new ShellCommand(tempScript.GetHostExecutable()) - .WithArguments([..runScriptArgs, ..inputArgs]) + .WithArguments([.. runScriptArgs, .. inputArgs]) .WithStdOutTarget(stdOut) .WithStdErrTarget(stdErr);