feat: add safe command termination - #1012
Conversation
aikawayataro
commented
Feb 21, 2025
Cancelling git commands this way is safe, as git installs handlers for CTRL+C on Windows and few common signals on other systems (incl. |
aikawayataro
commented
Feb 23, 2025
@love-linger will need your help with this, don't know how to wrap it properly. Feel free to close this pull if you don't want to go for this feature |
love-linger
commented
Feb 24, 2025
I'll check this PR and plan to merge it into version |
Signed-off-by: leo <longshuang@msn.cn>
love-linger
commented
Feb 24, 2025
I've pushed a new branch I found that the callback of // Code in src/Commands/Command.csif(CancellationToken.CanBeCanceled){CancellationToken.Register(()=>{if(_proc!=null&&!_isDone)Native.OS.TerminateSafely(_proc);// this line has been called});}// Code in src/Native/Windows.cspublicvoidTerminateSafely(Processprocess){if(SetConsoleCtrlHandler(IntPtr.Zero,true)){try{if(GenerateConsoleCtrlEvent((int)CTRL_EVENT.CTRL_C,process.Id))// Always returns falseprocess.WaitForExit();}finally{SetConsoleCtrlHandler(IntPtr.Zero,false);}}} |
aikawayataro
commented
Feb 24, 2025
Oh, it should be a draft, not ready for merge, you could push to this merge request. I will investigate the issue, now I can debug it properly, thanks! |
Signed-off-by: leo <longshuang@msn.cn>
…h time (#1012) Signed-off-by: leo <longshuang@msn.cn>
Signed-off-by: leo <longshuang@msn.cn>
…1012) Signed-off-by: leo <longshuang@msn.cn>
love-linger
commented
Feb 24, 2025
I pushed some new commits. It seems to fix the above issue (at least in my tests). @aikawayataro Could you please test it? |
Remove unnecessary `process.WaitForExit()` since it has been called in `Command.Exec()` (We use `process.WaitForExit(2000)` on Windows because it is needed after `GenerateConsoleCtrlEvent`) Signed-off-by: leo <longshuang@msn.cn>
aikawayataro
commented
Feb 25, 2025
I've tested it on Linux, it does work, git process exits properly and cleans up partially cloned repository, but the clone popup will stay forever. Windows works just good. |
love-linger
commented
Feb 25, 2025
Can you debug on Linux to see why the popup stacks? My Linux equipment is not around. |
It hangs at sourcegit/src/Commands/Command.cs Line 102 in 76ab7d2 I found out that diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
index 4d368d30..a0bb8bf2 100644
--- a/src/Commands/Command.cs+++ b/src/Commands/Command.cs@@ -99,7 +99,8 @@ namespace SourceGit.Commands
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
- proc.WaitForExit();+ while (!proc.HasExited)+ proc.WaitForExit(1000);
exitCode = proc.ExitCode;
proc.Close(); |
love-linger
commented
Feb 25, 2025
How about close output/error stream manually? publicvoidTerminateSafely(Processprocess){if(kill(process.Id,(int)SIGNAL.TERM)==0){process.StandardOutput.Close();process.StandardError.Close();}} |
aikawayataro
commented
Feb 25, 2025
Will crash with: |
love-linger
commented
Feb 25, 2025
If the workaround metioned above (the |
aikawayataro
commented
Feb 25, 2025
Yes, it fixes the issue. There is remark at https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-9.0#system-diagnostics-process-waitforexit(system-int32) that says:
But I think it's fine as we don't need any output after operation is canceled. |
love-linger
commented
Feb 25, 2025
I found that this code ( |
aikawayataro
commented
Feb 25, 2025
Yeah, it can be used only with selected commands I think. It is still a workaround, I will try to rewrite |
love-linger
commented
Feb 25, 2025
I suggest changing it in the |
aikawayataro
commented
Feb 25, 2025
It will not work since |
love-linger
commented
Feb 25, 2025
try this? if(kill(process.Id,(int)SIGNAL.TERM)==0){process.CancelOutputRead();process.CancelErrorRead();}Say sorry again. My Linux device is not around. |
aikawayataro
commented
Feb 25, 2025
Doesn't change anything.
No problem 😁 I think I have an idea, will try it right now |
Signed-off-by: leo <longshuang@msn.cn>
aikawayataro
commented
Feb 25, 2025
A bit dirty, but can't figure out anything else diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
index 4d368d30..4ebda965 100644
--- a/src/Commands/Command.cs+++ b/src/Commands/Command.cs@@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
+using System.Threading.Tasks;
using Avalonia.Threading;
namespace SourceGit.Commands
@@ -39,37 +40,56 @@ namespace SourceGit.Commands
var errs = new List<string>();
var proc = new Process() { StartInfo = start };
- proc.OutputDataReceived += (_, e) =>+ var linelock = new object();+ var stdoutTask = new Task(() =>
{
- if (e.Data != null)- OnReadline(e.Data);- };-- proc.ErrorDataReceived += (_, e) =>+ string line;+ while ((line = proc.StandardOutput.ReadLine()) != null)+ {+ if (CancellationToken.IsCancellationRequested)+ break;+ lock (linelock)+ {+ OnReadline(line);+ }+ }+ });+ var stderrTask = new Task(() =>
{
- if (string.IsNullOrEmpty(e.Data))+ string line;+ while (!CancellationToken.IsCancellationRequested && (line = proc.StandardError.ReadLine()) != null)
{
- errs.Add(string.Empty);- return;+ if (CancellationToken.IsCancellationRequested)+ break;+ if (string.IsNullOrEmpty(line))+ {+ errs.Add(string.Empty);+ return;+ }++ if (TraitErrorAsOutput)+ {+ lock (linelock)+ {+ OnReadline(line);+ }+ }++ // Ignore progress messages+ if (line.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))+ return;+ if (line.StartsWith("remote: Counting objects:", StringComparison.Ordinal))+ return;+ if (line.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))+ return;+ if (line.StartsWith("Filtering content:", StringComparison.Ordinal))+ return;+ if (REG_PROGRESS().IsMatch(line))+ return;++ errs.Add(line);
}
-- if (TraitErrorAsOutput)- OnReadline(e.Data);-- // Ignore progress messages- if (e.Data.StartsWith("remote: Enumerating objects:", StringComparison.Ordinal))- return;- if (e.Data.StartsWith("remote: Counting objects:", StringComparison.Ordinal))- return;- if (e.Data.StartsWith("remote: Compressing objects:", StringComparison.Ordinal))- return;- if (e.Data.StartsWith("Filtering content:", StringComparison.Ordinal))- return;- if (REG_PROGRESS().IsMatch(e.Data))- return;-- errs.Add(e.Data);- };+ });
if (CancellationToken.CanBeCanceled)
{
@@ -83,6 +103,8 @@ namespace SourceGit.Commands
try
{
proc.Start();
+ stdoutTask.Start();+ stderrTask.Start();
}
catch (Exception e)
{
@@ -97,9 +119,9 @@ namespace SourceGit.Commands
int exitCode;
try
{
- proc.BeginOutputReadLine();- proc.BeginErrorReadLine();
proc.WaitForExit();
+ if (!CancellationToken.IsCancellationRequested)+ Task.WaitAll([stdoutTask, stderrTask]);
exitCode = proc.ExitCode;
proc.Close(); |
love-linger
commented
Feb 26, 2025
I found that follow code just works find on my Ubuntu 24.04. publicvoidTerminateSafely(Processprocess){Process.Start("kill",$"-15 {process.Id}");}
|
love-linger
commented
Feb 26, 2025
One more thing. I've tested the orignal code on my Mac, it works fine. |
Sorry. This was the wrong conclusion... Confirmed. macOS has the same issue. |
aikawayataro
commented
Feb 26, 2025
Does my patch works to fix the issue on your side? The motivation is avoid async reading and to stop reading after cancellation is requested.
That is the exact reason it hangs, because of children and grandchildren keeping output open, diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs
index 4d368d30..af1f0955 100644
--- a/src/Commands/Command.cs+++ b/src/Commands/Command.cs@@ -41,12 +41,18 @@ namespace SourceGit.Commands
proc.OutputDataReceived += (_, e) =>
{
+ if (CancellationToken.IsCancellationRequested)+ return;+
if (e.Data != null)
OnReadline(e.Data);
};
proc.ErrorDataReceived += (_, e) =>
{
+ if (CancellationToken.IsCancellationRequested)+ return;+
if (string.IsNullOrEmpty(e.Data))
{
errs.Add(string.Empty);
@@ -99,7 +105,12 @@ namespace SourceGit.Commands
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
- proc.WaitForExit();+ while (!proc.HasExited)+ {+ proc.WaitForExit(1000);+ }+ if (!CancellationToken.IsCancellationRequested)+ proc.WaitForExit();
exitCode = proc.ExitCode;
proc.Close();It should fix this:
|
love-linger
commented
Feb 26, 2025
I just do not want to change the orignal Finally, I found the solution: publicvoidTerminateSafely(Processprocess){Process.Start("pkill",$"--signal 15 -P {process.Id}");} |
love-linger
commented
Feb 26, 2025
The main reason for this problem is exactly what I observed above, taking After changing to |
I don't think EDIT: it will send SIGTERM only to its children, grandchildren will not get any signal. |
love-linger
commented
Feb 26, 2025
via email
how about add an extra `kill()` after pkill? |
I don't think that it is a good idea to kill children first, but it should work. I think the best solution is to spawn git in separate process group, and use |
love-linger
commented
Feb 26, 2025
I tested the two solutions you provided above. After aborting the process, it is true that the interface logic will not get stuck, but the key problem remaining in the child process has not been solved. Although after a certain period of time (very long in my test, about 30 seconds at least), these child processes will be recycled. |
love-linger
commented
Feb 26, 2025
|
aikawayataro
commented
Feb 26, 2025
Yes could be a problem, even if they die out after time Yes, have seen that one. Sadly, there's no movement on this feature |
WIP implementation of command cancellation. Right now it only implements API for safe process termination, further implementation will require some refactoring.
This will address #753