Skip to content

Commit dfcc57b

Browse files
committed
Support asynchronous stdout/stderr processing
1 parent ec2deb0 commit dfcc57b

3 files changed

Lines changed: 120 additions & 0 deletions

File tree

csharp/autobuilder/Semmle.Autobuild.CSharp.Tests/BuildScripts.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory,
8585
return ret;
8686
}
8787

88+
int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary<string, string>? env, BuildOutputHandler onOutput, BuildOutputHandler onError)
89+
{
90+
var ret = (this as IBuildActions).RunProcess(cmd, args, workingDirectory, env, out var stdout);
91+
92+
foreach (var line in stdout)
93+
{
94+
onOutput(line);
95+
}
96+
97+
return ret;
98+
}
99+
88100
public IList<string> DirectoryDeleteIn { get; } = new List<string>();
89101

90102
void IBuildActions.DirectoryDelete(string dir, bool recursive)

csharp/autobuilder/Semmle.Autobuild.Shared/BuildActions.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,26 @@
1111

1212
namespace Semmle.Autobuild.Shared
1313
{
14+
public delegate void BuildOutputHandler(string? data);
15+
1416
/// <summary>
1517
/// Wrapper around system calls so that the build scripts can be unit-tested.
1618
/// </summary>
1719
public interface IBuildActions
1820
{
21+
22+
/// <summary>
23+
/// Runs a process, captures its output, and provides it asynchronously.
24+
/// </summary>
25+
/// <param name="exe">The exe to run.</param>
26+
/// <param name="args">The other command line arguments.</param>
27+
/// <param name="workingDirectory">The working directory (<code>null</code> for current directory).</param>
28+
/// <param name="env">Additional environment variables.</param>
29+
/// <param name="onOutput">A handler for stdout output.</param>
30+
/// <param name="onError">A handler for stderr output.</param>
31+
/// <returns>The process exit code.</returns>
32+
int RunProcess(string exe, string args, string? workingDirectory, IDictionary<string, string>? env, BuildOutputHandler onOutput, BuildOutputHandler onError);
33+
1934
/// <summary>
2035
/// Runs a process and captures its output.
2136
/// </summary>
@@ -182,6 +197,26 @@ private static ProcessStartInfo GetProcessStartInfo(string exe, string arguments
182197
return pi;
183198
}
184199

200+
int IBuildActions.RunProcess(string exe, string args, string? workingDirectory, System.Collections.Generic.IDictionary<string, string>? env, BuildOutputHandler onOutput, BuildOutputHandler onError)
201+
{
202+
var pi = GetProcessStartInfo(exe, args, workingDirectory, env, true);
203+
using var p = new Process
204+
{
205+
StartInfo = pi
206+
};
207+
p.StartInfo.RedirectStandardError = true;
208+
p.OutputDataReceived += new DataReceivedEventHandler((sender, e) => onOutput(e.Data));
209+
p.ErrorDataReceived += new DataReceivedEventHandler((sender, e) => onError(e.Data));
210+
211+
p.Start();
212+
213+
p.BeginErrorReadLine();
214+
p.BeginOutputReadLine();
215+
216+
p.WaitForExit();
217+
return p.ExitCode;
218+
}
219+
185220
int IBuildActions.RunProcess(string cmd, string args, string? workingDirectory, IDictionary<string, string>? environment)
186221
{
187222
var pi = GetProcessStartInfo(cmd, args, workingDirectory, environment, false);

csharp/autobuilder/Semmle.Autobuild.Shared/BuildScript.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ public abstract class BuildScript
4646
/// <returns>The exit code from this build script.</returns>
4747
public abstract int Run(IBuildActions actions, Action<string, bool> startCallback, Action<int, string, bool> exitCallBack, out IList<string> stdout);
4848

49+
/// <summary>
50+
/// Runs this build command.
51+
/// </summary>
52+
/// <param name="actions">
53+
/// The interface used to implement the build actions.
54+
/// </param>
55+
/// <param name="startCallback">
56+
/// A call back that is called every time a new process is started. The
57+
/// argument to the call back is a textual representation of the process.
58+
/// </param>
59+
/// <param name="exitCallBack">
60+
/// A call back that is called every time a new process exits. The first
61+
/// argument to the call back is the exit code, and the second argument is
62+
/// an exit message.
63+
/// </param>
64+
/// <param name="onOutput">
65+
/// A handler for data read from stdout.
66+
/// </param>
67+
/// <param name="onError">
68+
/// A handler for data read from stderr.
69+
/// </param>
70+
/// <returns>The exit code from this build script.</returns>
71+
public abstract int Run(IBuildActions actions, Action<string, bool> startCallback, Action<int, string, bool> exitCallBack, BuildOutputHandler onOutput, BuildOutputHandler onError);
72+
4973
private class BuildCommand : BuildScript
5074
{
5175
private readonly string exe, arguments;
@@ -110,6 +134,24 @@ public override int Run(IBuildActions actions, Action<string, bool> startCallbac
110134
return ret;
111135
}
112136

137+
public override int Run(IBuildActions actions, Action<string, bool> startCallback, Action<int, string, bool> exitCallBack, BuildOutputHandler onOutput, BuildOutputHandler onError)
138+
{
139+
startCallback(this.ToString(), silent);
140+
var ret = 1;
141+
var retMessage = "";
142+
try
143+
{
144+
ret = actions.RunProcess(exe, arguments, workingDirectory, environment, onOutput, onError);
145+
}
146+
catch (Exception ex)
147+
when (ex is System.ComponentModel.Win32Exception || ex is FileNotFoundException)
148+
{
149+
retMessage = ex.Message;
150+
}
151+
exitCallBack(ret, retMessage, silent);
152+
return ret;
153+
}
154+
113155
}
114156

115157
private class ReturnBuildCommand : BuildScript
@@ -127,8 +169,13 @@ public override int Run(IBuildActions actions, Action<string, bool> startCallbac
127169
stdout = Array.Empty<string>();
128170
return func(actions);
129171
}
172+
173+
public override int Run(IBuildActions actions, Action<string, bool> startCallback, Action<int, string, bool> exitCallBack, BuildOutputHandler onOutput, BuildOutputHandler onError) => func(actions);
130174
}
131175

176+
/// <summary>
177+
/// Allows two build scripts to be composed sequentially.
178+
/// </summary>
132179
private class BindBuildScript : BuildScript
133180
{
134181
private readonly BuildScript s1;
@@ -175,6 +222,32 @@ public override int Run(IBuildActions actions, Action<string, bool> startCallbac
175222
stdout = @out;
176223
return ret2;
177224
}
225+
226+
public override int Run(IBuildActions actions, Action<string, bool> startCallback, Action<int, string, bool> exitCallBack, BuildOutputHandler onOutput, BuildOutputHandler onError)
227+
{
228+
int ret1;
229+
if (s2a is not null)
230+
{
231+
var stdout1 = new List<string>();
232+
var onOutputWrapper = new BuildOutputHandler(data =>
233+
{
234+
if (data is not null)
235+
stdout1.Add(data);
236+
237+
onOutput(data);
238+
});
239+
ret1 = s1.Run(actions, startCallback, exitCallBack, onOutputWrapper, onError);
240+
return s2a(stdout1, ret1).Run(actions, startCallback, exitCallBack, onOutput, onError);
241+
}
242+
243+
if (s2b is not null)
244+
{
245+
ret1 = s1.Run(actions, startCallback, exitCallBack, onOutput, onError);
246+
return s2b(ret1).Run(actions, startCallback, exitCallBack, onOutput, onError);
247+
}
248+
249+
throw new InvalidOperationException("Unexpected error");
250+
}
178251
}
179252

180253
/// <summary>

0 commit comments

Comments
 (0)