From dd5a6b20ee82e4a7aaf06488c407497c3a016d75 Mon Sep 17 00:00:00 2001 From: Gerardo Grignoli Date: Fri, 13 Dec 2019 07:33:36 -0300 Subject: [PATCH] Fixed stabilitiy issues and resource leaking (On client disconnection, the Pseudoconsole was kept open forever.) --- backlog.md | 9 +- src/gsudo/Commands/RunCommand.cs | 14 +- src/gsudo/Commands/ServiceCommand.cs | 2 +- src/gsudo/Helpers/ProcessExtensions.cs | 38 +++++ src/gsudo/Helpers/ProcessFactory.cs | 4 +- src/gsudo/ProcessHosts/PipedProcessHost.cs | 43 +---- src/gsudo/ProcessHosts/VTProcessHost.cs | 76 ++++----- src/gsudo/Program.cs | 2 - .../{Process.cs => PseudoConsoleProcess.cs} | 19 ++- src/gsudo/PseudoConsole/Terminal.cs | 148 ------------------ .../{ => Rpc}/ConnectionKeepAliveThread.cs | 7 +- src/gsudo/Rpc/IRpcClient.cs | 12 +- src/gsudo/gsudo.csproj | 5 + 13 files changed, 139 insertions(+), 240 deletions(-) rename src/gsudo/PseudoConsole/{Process.cs => PseudoConsoleProcess.cs} (80%) delete mode 100644 src/gsudo/PseudoConsole/Terminal.cs rename src/gsudo/{ => Rpc}/ConnectionKeepAliveThread.cs (91%) diff --git a/backlog.md b/backlog.md index e77238c6..9f2ef1b9 100644 --- a/backlog.md +++ b/backlog.md @@ -1,7 +1,6 @@ # gsudo Backlog - Chocolatey package / Scoop package / Release on github. -- Third consecutive Ctrl-C should ask if the child process must be kept running or killed. - gsudo --nocache (service will quit immediately after process ends and will not elevate other commands with cached credentials) (find better --syntax) - Allow to specify other username. (RunAsUser verb) @@ -15,6 +14,8 @@ ## Completed +- Third consecutive Ctrl-C or client disconnect kills the elevated process. +- VT console extended keys (F1-F12, CTRL+?, HOME,PAGEUP) - WinPty/VT100 support: When in VT mode, processes are spawn using a PseudoConsole. Rendering could be done using Windows Console ENABLE_VIRTUAL_TERMINAL processing flag but it is pretty [unstable](https://github.com/microsoft/terminal/issues/3765). So it is disabled by default unless you are running inside ConEmu/Cmder which are VT100 ready terminals. VT Mode is enabled automatically if you run inside a ConEmu/Cmder or if you use `--vt` flag. @@ -24,13 +25,13 @@ - Configuration settings persistent storage. -``` +``` console gsudo config (show all current user config) gsudo config {setting} (return current value) gsudo config {setting} {value} (save new value) - + gsudo config CredentialsCacheDuration 0:0 (no cache) gsudo config CredentialsCacheDuration 5:00 (5 minutes) gsudo config Prompt "$P# " (elevated command prompt) gsudo config CredentialsCache disabled (disable service) -> not implemented -``` \ No newline at end of file +``` diff --git a/src/gsudo/Commands/RunCommand.cs b/src/gsudo/Commands/RunCommand.cs index 6d11fa32..91b6c3dd 100644 --- a/src/gsudo/Commands/RunCommand.cs +++ b/src/gsudo/Commands/RunCommand.cs @@ -57,8 +57,6 @@ public async Task Environment.SetEnvironmentVariable("PROMPT", GlobalSettings.Prompt.Value); } - Logger.Instance.Log($"Using Console mode {elevationRequest.Mode}", LogLevel.Debug); - if (ProcessExtensions.IsAdministrator() && !GlobalSettings.NewWindow) { if (emptyArgs) @@ -101,9 +99,10 @@ public async Task } else // IsAdministrator() == false, or build in Debug Mode { - var cmd = CommandToRun.FirstOrDefault(); + Logger.Instance.Log($"Using Console mode {elevationRequest.Mode}", LogLevel.Debug); + Logger.Instance.Log($"Caller ProcessId is {currentProcess.ParentProcessId()}", LogLevel.Debug); - Logger.Instance.Log($"Calling ProcessId is {currentProcess.ParentProcessId()}", LogLevel.Debug); + var cmd = CommandToRun.FirstOrDefault(); var rpcClient = GetClient(elevationRequest); Rpc.Connection connection = null; @@ -151,6 +150,13 @@ public async Task finally { connection?.Dispose(); + try + { + // cleanup console before returning. + Console.CursorVisible = true; + Console.ResetColor(); + } + catch { } } } diff --git a/src/gsudo/Commands/ServiceCommand.cs b/src/gsudo/Commands/ServiceCommand.cs index c30f05a8..05475628 100644 --- a/src/gsudo/Commands/ServiceCommand.cs +++ b/src/gsudo/Commands/ServiceCommand.cs @@ -61,7 +61,7 @@ private async Task AcceptConnection(Connection connection) catch (Exception e) { Logger.Instance.Log(e.ToString(), LogLevel.Error); - connection.IsAlive = false; + connection.SignalDisconnected(); } } diff --git a/src/gsudo/Helpers/ProcessExtensions.cs b/src/gsudo/Helpers/ProcessExtensions.cs index a59ae8fb..5ab3ec78 100644 --- a/src/gsudo/Helpers/ProcessExtensions.cs +++ b/src/gsudo/Helpers/ProcessExtensions.cs @@ -8,6 +8,7 @@ using System.Security; using System.Security.Permissions; using System.Security.Principal; +using System.Threading; namespace gsudo.Helpers { @@ -188,5 +189,42 @@ protected override bool ReleaseHandle() } #endregion + + public static void Terminate(this Process process) + { + if (process.HasExited) return; + + Logger.Instance.Log($"Killing process {process.Id} {process.ProcessName}", LogLevel.Debug); + + process.SendCtrlC(false); + process.CloseMainWindow(); + + process.WaitForExit(300); + + if (!process.HasExited) + { + process.Kill(); + /* + var p = Process.Start(new ProcessStartInfo() + { + FileName = "taskkill", + Arguments = $"/PID {process.Id} /T", + WindowStyle = ProcessWindowStyle.Hidden + + }); + p.WaitForExit(); + */ + } + } + + /// + /// Get an AutoResetEvent that signals when the process exits + /// + public static AutoResetEvent GetWaitHandle(this Process process) => + new AutoResetEvent(false) + { + SafeWaitHandle = new SafeWaitHandle(process.Handle, ownsHandle: false) + }; + } } diff --git a/src/gsudo/Helpers/ProcessFactory.cs b/src/gsudo/Helpers/ProcessFactory.cs index a6305788..d5cfd772 100644 --- a/src/gsudo/Helpers/ProcessFactory.cs +++ b/src/gsudo/Helpers/ProcessFactory.cs @@ -142,11 +142,11 @@ public static string FindExecutableInPath(string exe) return null; } - public static PseudoConsole.Process StartPseudoConsole(string command, IntPtr attributes, IntPtr hPC, string startFolder) + public static PseudoConsole.PseudoConsoleProcess StartPseudoConsole(string command, IntPtr attributes, IntPtr hPC, string startFolder) { var startupInfo = ConfigureProcessThread(hPC, attributes); var processInfo = RunProcess(ref startupInfo, command, startFolder); - return new PseudoConsole.Process(startupInfo, processInfo); + return new PseudoConsole.PseudoConsoleProcess(startupInfo, processInfo); } private static STARTUPINFOEX ConfigureProcessThread(IntPtr hPC, IntPtr attributes) diff --git a/src/gsudo/ProcessHosts/PipedProcessHost.cs b/src/gsudo/ProcessHosts/PipedProcessHost.cs index 8b4efc1a..f86742cc 100644 --- a/src/gsudo/ProcessHosts/PipedProcessHost.cs +++ b/src/gsudo/ProcessHosts/PipedProcessHost.cs @@ -1,10 +1,10 @@ using gsudo.Helpers; -using gsudo.Native; using gsudo.Rpc; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading; using System.Threading.Tasks; namespace gsudo.ProcessHosts @@ -32,11 +32,8 @@ public async Task Start(Connection connection, ElevationRequest request) var t4 = new StreamReader(connection.ControlStream, GlobalSettings.Encoding).ConsumeOutput((s) => HandleControl(s, process)); int i = 0; - - while (!process.WaitForExit(0) && connection.IsAlive) - { - await Task.Delay(10).ConfigureAwait(false); - } + + WaitHandle.WaitAny(new WaitHandle[] { process.GetWaitHandle(), connection.DisconnectedWaitHandle }); if (process.HasExited && connection.IsAlive) { @@ -47,10 +44,6 @@ public async Task Start(Connection connection, ElevationRequest request) await Task.WhenAll(t1, t2).ConfigureAwait(false); await connection.ControlStream.WriteAsync($"{Constants.TOKEN_EXITCODE}{process.ExitCode}{Constants.TOKEN_EXITCODE}").ConfigureAwait(false); } - else - { - TerminateHostedProcess(); - } await connection.FlushAndCloseAll().ConfigureAwait(false); } @@ -63,6 +56,10 @@ public async Task Start(Connection connection, ElevationRequest request) } finally { + if (process != null && !process.HasExited) + { + process?.Terminate(); + } process?.Dispose(); } } @@ -84,30 +81,6 @@ private bool ShouldWait(StreamReader streamReader) } } - private void TerminateHostedProcess() - { - Logger.Instance.Log($"Killing process {process.Id} {process.ProcessName}", LogLevel.Debug); - - if (process.HasExited) return; - - process.SendCtrlC(true); - - if (process.CloseMainWindow()) - process.WaitForExit(100); - - //if (!process.HasExited) - //{ - // var p = Process.Start(new ProcessStartInfo() - // { - // FileName = "taskkill", - // Arguments = $"/PID {process.Id} /T", - // WindowStyle = ProcessWindowStyle.Hidden - - // }); - // p.WaitForExit(); - //} - } - private async Task WriteToProcessStdIn(string s, Process process) { if (lastInboundMessage == null) @@ -166,7 +139,7 @@ private async Task WriteToPipe(string s) s = s.Substring(c); lastInboundMessage = lastInboundMessage.Substring(c); } - if (GlobalSettings.Debug && !string.IsNullOrEmpty(s)) Logger.Instance.Log($"Last input command was: {s}", LogLevel.Debug); + //if (GlobalSettings.Debug && !string.IsNullOrEmpty(s)) Logger.Instance.Log($"Last input command was: {s}", LogLevel.Debug); } if (string.IsNullOrEmpty(s)) return; // suppress chars n s; diff --git a/src/gsudo/ProcessHosts/VTProcessHost.cs b/src/gsudo/ProcessHosts/VTProcessHost.cs index be703773..40ad2986 100644 --- a/src/gsudo/ProcessHosts/VTProcessHost.cs +++ b/src/gsudo/ProcessHosts/VTProcessHost.cs @@ -21,6 +21,7 @@ public async Task Start(Connection connection, ElevationRequest request) int? exitCode; Task t1 = null, t2 = null, t3=null; _connection = connection; + System.Diagnostics.Process runningProcess = null; try { string command = request.FileName + " " + request.Arguments; @@ -31,6 +32,8 @@ public async Task Start(Connection connection, ElevationRequest request) { using (var process = ProcessFactory.StartPseudoConsole(command, PseudoConsole.PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle, request.StartFolder)) { + runningProcess = System.Diagnostics.Process.GetProcessById(process.ProcessInfo.dwProcessId); + // copy all pseudoconsole output to stdout t1 = Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide)); // prompt for stdin input and send the result to the pseudoconsole @@ -43,12 +46,9 @@ public async Task Start(Connection connection, ElevationRequest request) // var t3 = new StreamReader(pipe, Globals.Encoding).ConsumeOutput((s) => WriteToStdInput(s, process)); OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe)); - WaitForExit(process).WaitOne(); - if (connection.IsAlive) - { - await Task.Delay(10).ConfigureAwait(false); - } + WaitHandle.WaitAny(new WaitHandle[] { process.GetWaitHandle(), connection.DisconnectedWaitHandle }); + exitCode = process.GetExitCode(); } } @@ -68,6 +68,11 @@ public async Task Start(Connection connection, ElevationRequest request) await connection.FlushAndCloseAll().ConfigureAwait(false); return; } + finally + { + if (runningProcess != null && !runningProcess.HasExited) + runningProcess.Terminate(); + } } /// @@ -111,42 +116,43 @@ private async Task CopyPipeToOutput(SafeFileHandle outputReadSide) if (GlobalSettings.Debug) { - streamWriter = new StreamWriter(new FileStream("VTProcessHost.debug.txt", FileMode.Create, FileAccess.Write), new System.Text.UTF8Encoding(true)); + try + { + streamWriter = new StreamWriter(new FileStream("VTProcessHost.debug.txt", FileMode.Create, FileAccess.Write), new System.Text.UTF8Encoding(true)); + } + catch { /* if debug stream fails, lets go on. */ } } - using (var pseudoConsoleOutput = new FileStream(outputReadSide, FileAccess.Read)) + try { - byte[] buffer = new byte[10240]; - int cch; - - while ((cch = await pseudoConsoleOutput.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) + using (var pseudoConsoleOutput = new FileStream(outputReadSide, FileAccess.Read)) { - var s = GlobalSettings.Encoding.GetString(buffer, 0, cch); - await _connection.DataStream.WriteAsync(s).ConfigureAwait(false); - - streamWriter?.Write(s); - streamWriter?.Flush(); - - if (GlobalSettings.Debug) - Console.Write(s - .Replace('\a',' ') // no bell sounds please - .Replace("\r", "\\r") - .Replace("\n", "\\n") - ); + byte[] buffer = new byte[10240]; + int cch; + + while ((cch = await pseudoConsoleOutput.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) + { + var s = GlobalSettings.Encoding.GetString(buffer, 0, cch); + await _connection.DataStream.WriteAsync(s).ConfigureAwait(false); + + streamWriter?.Write(s); + streamWriter?.Flush(); + + if (GlobalSettings.Debug) + Console.Write(s + .Replace('\a', ' ') // no bell sounds please + .Replace("\r", "\\r") + .Replace("\n", "\\n") + ); + } } } - streamWriter?.Close(); + finally + { + streamWriter?.Close(); + } } - - /// - /// Get an AutoResetEvent that signals when the process exits - /// - private static AutoResetEvent WaitForExit(Process process) => - new AutoResetEvent(false) - { - SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, ownsHandle: false) - }; - + /// /// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button). /// Intended for resource cleanup logic. @@ -163,7 +169,7 @@ private static void OnClose(Action handler) }, true); } - private void DisposeResources(params IDisposable[] disposables) + private static void DisposeResources(params IDisposable[] disposables) { foreach (var disposable in disposables) { diff --git a/src/gsudo/Program.cs b/src/gsudo/Program.cs index 3f96a5a9..4943e7d1 100644 --- a/src/gsudo/Program.cs +++ b/src/gsudo/Program.cs @@ -3,9 +3,7 @@ using gsudo.Helpers; using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Reflection; using System.Threading.Tasks; namespace gsudo diff --git a/src/gsudo/PseudoConsole/Process.cs b/src/gsudo/PseudoConsole/PseudoConsoleProcess.cs similarity index 80% rename from src/gsudo/PseudoConsole/Process.cs rename to src/gsudo/PseudoConsole/PseudoConsoleProcess.cs index a3ac1673..70c28b57 100644 --- a/src/gsudo/PseudoConsole/Process.cs +++ b/src/gsudo/PseudoConsole/PseudoConsoleProcess.cs @@ -1,5 +1,7 @@ -using System; +using Microsoft.Win32.SafeHandles; +using System; using System.Runtime.InteropServices; +using System.Threading; using static gsudo.Native.ProcessApi; namespace gsudo.PseudoConsole @@ -7,9 +9,9 @@ namespace gsudo.PseudoConsole /// /// Represents an instance of a process. /// - internal sealed class Process : IDisposable + internal sealed class PseudoConsoleProcess : IDisposable { - public Process(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo) + public PseudoConsoleProcess(STARTUPINFOEX startupInfo, PROCESS_INFORMATION processInfo) { StartupInfo = startupInfo; ProcessInfo = processInfo; @@ -64,7 +66,7 @@ void Dispose(bool disposing) } } - ~Process() + ~PseudoConsoleProcess() { // Do not change this code. Put cleanup code in Dispose(bool disposing) above. Dispose(false); @@ -79,6 +81,15 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Get an AutoResetEvent that signals when the process exits + /// + public AutoResetEvent GetWaitHandle() => + new AutoResetEvent(false) + { + SafeWaitHandle = new SafeWaitHandle(this.ProcessInfo.hProcess, ownsHandle: false) + }; + #endregion } } diff --git a/src/gsudo/PseudoConsole/Terminal.cs b/src/gsudo/PseudoConsole/Terminal.cs deleted file mode 100644 index be0a6d3f..00000000 --- a/src/gsudo/PseudoConsole/Terminal.cs +++ /dev/null @@ -1,148 +0,0 @@ -using gsudo.PseudoConsole; -using gsudo.Helpers; -using Microsoft.Win32.SafeHandles; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using static gsudo.Native.ConsoleApi; - -/* -namespace gsudo.PseudoConsole -{ - /// - /// The UI of the terminal. It's just a normal console window, but we're managing the input/output. - /// In a "real" project this could be some other UI. - /// - internal sealed class Terminal - { - private const string ExitCommand = "exit\r"; - private const string CtrlC_Command = "\x3"; - - public Terminal() - { - EnableVirtualTerminalSequenceProcessing(); - } - - /// - /// Newer versions of the windows console support interpreting virtual terminal sequences, we just have to opt-in - /// - private static void EnableVirtualTerminalSequenceProcessing() - { - var hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); - if (!GetConsoleMode(hStdOut, out uint outConsoleMode)) - { - throw new InvalidOperationException("Could not get console mode"); - } - - outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN; - if (!SetConsoleMode(hStdOut, outConsoleMode)) - { - throw new InvalidOperationException("Could not enable virtual terminal processing"); - } - } - - /// - /// Start the psuedoconsole and run the process as shown in - /// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session#creating-the-pseudoconsole - /// - /// the command to run, e.g. cmd.exe - public void Run(string command) - { - using (var inputPipe = new PseudoConsolePipe()) - using (var outputPipe = new PseudoConsolePipe()) - using (var pseudoConsole = PseudoConsole.Create(inputPipe.ReadSide, outputPipe.WriteSide, (short)Console.WindowWidth, (short)Console.WindowHeight)) - using (var process = ProcessStarter.StartPseudoConsole(command, PseudoConsole.PseudoConsoleThreadAttribute, pseudoConsole.Handle)) - { - // copy all pseudoconsole output to stdout - Task.Run(() => CopyPipeToOutput(outputPipe.ReadSide)); - // prompt for stdin input and send the result to the pseudoconsole - Task.Run(() => CopyInputToPipe(inputPipe.WriteSide)); - // free resources in case the console is ungracefully closed (e.g. by the 'x' in the window titlebar) - OnClose(() => DisposeResources(process, pseudoConsole, outputPipe, inputPipe)); - - WaitForExit(process).WaitOne(Timeout.Infinite); - } - } - - /// - /// Reads terminal input and copies it to the PseudoConsole - /// - /// the "write" side of the pseudo console input pipe - private static void CopyInputToPipe(SafeFileHandle inputWriteSide) - { - using (var writer = new StreamWriter(new FileStream(inputWriteSide, FileAccess.Write))) - { - ForwardCtrlC(writer); - writer.AutoFlush = true; - writer.WriteLine(@"cd \"); - - while (true) - { - // send input character-by-character to the pipe - char key = Console.ReadKey(intercept: true).KeyChar; - writer.Write(key); - } - } - } - - /// - /// Don't let ctrl-c kill the terminal, it should be sent to the process in the terminal. - /// - private static void ForwardCtrlC(StreamWriter writer) - { - Console.CancelKeyPress += (sender, e) => - { - e.Cancel = true; - writer.Write(CtrlC_Command); - }; - } - - /// - /// Reads PseudoConsole output and copies it to the terminal's standard out. - /// - /// the "read" side of the pseudo console output pipe - private static void CopyPipeToOutput(SafeFileHandle outputReadSide) - { - using (var terminalOutput = Console.OpenStandardOutput()) - using (var pseudoConsoleOutput = new FileStream(outputReadSide, FileAccess.Read)) - { - pseudoConsoleOutput.CopyTo(terminalOutput); - } - } - - /// - /// Get an AutoResetEvent that signals when the process exits - /// - private static AutoResetEvent WaitForExit(Process process) => - new AutoResetEvent(false) - { - SafeWaitHandle = new SafeWaitHandle(process.ProcessInfo.hProcess, ownsHandle: false) - }; - - /// - /// Set a callback for when the terminal is closed (e.g. via the "X" window decoration button). - /// Intended for resource cleanup logic. - /// - private static void OnClose(Action handler) - { - SetConsoleCtrlHandler(eventType => - { - if(eventType == CtrlTypes.CTRL_CLOSE_EVENT) - { - handler(); - } - return false; - }, true); - } - - private void DisposeResources(params IDisposable[] disposables) - { - foreach (var disposable in disposables) - { - disposable.Dispose(); - } - } - } -} -*/ \ No newline at end of file diff --git a/src/gsudo/ConnectionKeepAliveThread.cs b/src/gsudo/Rpc/ConnectionKeepAliveThread.cs similarity index 91% rename from src/gsudo/ConnectionKeepAliveThread.cs rename to src/gsudo/Rpc/ConnectionKeepAliveThread.cs index c9f76b58..7cf94d13 100644 --- a/src/gsudo/ConnectionKeepAliveThread.cs +++ b/src/gsudo/Rpc/ConnectionKeepAliveThread.cs @@ -1,7 +1,6 @@ -using gsudo.Rpc; -using System.Threading; +using System.Threading; -namespace gsudo +namespace gsudo.Rpc { // A thread that detect when the NamedPipeConnection is disconnected. // Couldn't find a better way to do this than periodically writing to the pipe. @@ -39,7 +38,7 @@ private void DoWork() } catch { - _connection.IsAlive = false; + _connection.SignalDisconnected(); } } } diff --git a/src/gsudo/Rpc/IRpcClient.cs b/src/gsudo/Rpc/IRpcClient.cs index d3c1b597..90c86ce2 100644 --- a/src/gsudo/Rpc/IRpcClient.cs +++ b/src/gsudo/Rpc/IRpcClient.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.IO.Pipes; +using System.Threading; using System.Threading.Tasks; namespace gsudo.Rpc @@ -14,7 +15,16 @@ class Connection : IDisposable { public Stream DataStream { get; set; } public Stream ControlStream { get; set; } - public bool IsAlive { get; set; } = true; + + private ManualResetEvent DisconnectedResetEvent { get; } = new ManualResetEvent(false); + public WaitHandle DisconnectedWaitHandle => DisconnectedResetEvent; + + public bool IsAlive { get; private set; } = true; + public void SignalDisconnected() + { + IsAlive = false; + DisconnectedResetEvent.Set(); + } public async Task FlushAndCloseAll() { diff --git a/src/gsudo/gsudo.csproj b/src/gsudo/gsudo.csproj index 7b726d85..e71c1f5b 100644 --- a/src/gsudo/gsudo.csproj +++ b/src/gsudo/gsudo.csproj @@ -16,6 +16,11 @@ It is a sudo equivalent for Windows, with a similar user-experience as the origi 0.4 2019 Gerardo Grignoli MIT + https://github.com/gerardog/gsudo + https://github.com/gerardog/gsudo + git + sudo for windows + gsudo