Browse Source

LINQ-to-Profiler: fixed crash when there is a ThenBy in the query.

CodeCoverage: set COMPLUS_ProfAPI_ProfilerCompatibilitySetting to run PartCover on .NET 4.

git-svn-id: svn://svn.sharpdevelop.net/sharpdevelop/trunk@5034 1ccf3a8d-04fe-1044-b7c0-cef0b8235c61
shortcuts
Daniel Grunwald 17 years ago
parent
commit
0991e23416
  1. 31
      src/AddIns/Misc/CodeCoverage/Project/Src/PartCoverRunner.cs
  2. 6
      src/AddIns/Misc/Profiler/Controller/Data/Linq/KnownMembers.cs
  3. 33
      src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs
  4. 86
      src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs
  5. 15
      src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs
  6. 48
      src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs
  7. 3
      src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Profiler.Tests.csproj
  8. 10
      src/Main/Base/Project/Src/Util/ProcessRunner.cs

31
src/AddIns/Misc/CodeCoverage/Project/Src/PartCoverRunner.cs

@ -26,7 +26,7 @@ namespace ICSharpCode.CodeCoverage
StringCollection include = new StringCollection(); StringCollection include = new StringCollection();
StringCollection exclude = new StringCollection(); StringCollection exclude = new StringCollection();
string output = String.Empty; string output = String.Empty;
/// <summary> /// <summary>
/// Triggered when PartCover exits. /// Triggered when PartCover exits.
/// </summary> /// </summary>
@ -38,7 +38,7 @@ namespace ICSharpCode.CodeCoverage
public event EventHandler Started; public event EventHandler Started;
/// <summary> /// <summary>
/// The PartCover runner was stopped. Being stopped is not the /// The PartCover runner was stopped. Being stopped is not the
/// same as PartCover exiting. /// same as PartCover exiting.
/// </summary> /// </summary>
public event EventHandler Stopped; public event EventHandler Stopped;
@ -53,7 +53,7 @@ namespace ICSharpCode.CodeCoverage
} }
/// <summary> /// <summary>
/// Gets or sets the full path to the PartCover /// Gets or sets the full path to the PartCover
/// executable. /// executable.
/// </summary> /// </summary>
public string PartCoverFileName { public string PartCoverFileName {
@ -95,7 +95,7 @@ namespace ICSharpCode.CodeCoverage
} }
/// <summary> /// <summary>
/// Gets or sets the regular expressions which specify the items to /// Gets or sets the regular expressions which specify the items to
/// include in the report whilst profiling the target executable. /// include in the report whilst profiling the target executable.
/// </summary> /// </summary>
public StringCollection Include { public StringCollection Include {
@ -103,7 +103,7 @@ namespace ICSharpCode.CodeCoverage
} }
/// <summary> /// <summary>
/// Gets or sets the regular expressions which specify the items to /// Gets or sets the regular expressions which specify the items to
/// exclude in the report whilst profiling the target executable. /// exclude in the report whilst profiling the target executable.
/// </summary> /// </summary>
public StringCollection Exclude { public StringCollection Exclude {
@ -116,7 +116,7 @@ namespace ICSharpCode.CodeCoverage
public string Output { public string Output {
get { return output; } get { return output; }
set { output = value; } set { output = value; }
} }
/// <summary> /// <summary>
/// Returns the full path used to run PartCover. /// Returns the full path used to run PartCover.
@ -141,9 +141,9 @@ namespace ICSharpCode.CodeCoverage
/// so in order for this to be passed to PartCover as a single argument /// so in order for this to be passed to PartCover as a single argument
/// we need to prefix each double quote by a backslash. For example: /// we need to prefix each double quote by a backslash. For example:
/// ///
/// Target args: "C:\Projects\My Tests\Test.dll" /output "C:\Projects\My Tests\Output.xml" /// Target args: "C:\Projects\My Tests\Test.dll" /output "C:\Projects\My Tests\Output.xml"
/// ///
/// PartCover: --target-args "\"C:\Projects\My Tests\Test.dll\" /output \"C:\Projects\My Tests\Output.xml\"" /// PartCover: --target-args "\"C:\Projects\My Tests\Test.dll\" /output \"C:\Projects\My Tests\Output.xml\""
/// </remarks> /// </remarks>
public string GetArguments() public string GetArguments()
{ {
@ -170,15 +170,16 @@ namespace ICSharpCode.CodeCoverage
} }
arguments.Append(GetArguments("--exclude", exclude)); arguments.Append(GetArguments("--exclude", exclude));
return arguments.ToString().Trim(); return arguments.ToString().Trim();
} }
public void Start() public void Start()
{ {
string arguments = GetArguments(); string arguments = GetArguments();
runner = new ProcessRunner(); runner = new ProcessRunner();
runner.EnvironmentVariables.Add("COMPLUS_ProfAPI_ProfilerCompatibilitySetting", "EnableV2Profiler");
runner.WorkingDirectory = workingDirectory; runner.WorkingDirectory = workingDirectory;
runner.ProcessExited += ProcessExited; runner.ProcessExited += ProcessExited;
@ -186,7 +187,7 @@ namespace ICSharpCode.CodeCoverage
runner.OutputLineReceived += OnOutputLineReceived; runner.OutputLineReceived += OnOutputLineReceived;
runner.ErrorLineReceived += OnOutputLineReceived; runner.ErrorLineReceived += OnOutputLineReceived;
} }
runner.Start(partCoverFileName, arguments); runner.Start(partCoverFileName, arguments);
OnStarted(); OnStarted();
} }
@ -241,10 +242,10 @@ namespace ICSharpCode.CodeCoverage
/// <param name="e">The event arguments.</param> /// <param name="e">The event arguments.</param>
void ProcessExited(object sender, EventArgs e) void ProcessExited(object sender, EventArgs e)
{ {
ProcessRunner runner = (ProcessRunner)sender; ProcessRunner runner = (ProcessRunner)sender;
OnExited(runner.StandardOutput, runner.StandardError, runner.ExitCode); OnExited(runner.StandardOutput, runner.StandardError, runner.ExitCode);
} }
/// <summary> /// <summary>
/// Gets the command line option that can have multiple items as specified /// Gets the command line option that can have multiple items as specified
/// in the string array. Each array item will have a separate command line /// in the string array. Each array item will have a separate command line

6
src/AddIns/Misc/Profiler/Controller/Data/Linq/KnownMembers.cs

@ -30,7 +30,13 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public static readonly MethodInfo QueryableOfCallTreeNode_Take = MethodOf((IQueryable<CallTreeNode> q) => q.Take(1)); public static readonly MethodInfo QueryableOfCallTreeNode_Take = MethodOf((IQueryable<CallTreeNode> q) => q.Take(1));
public static readonly MethodInfo Queryable_OrderBy = MethodOf((IQueryable<CallTreeNode> q) => q.OrderBy(x => x)).GetGenericMethodDefinition(); public static readonly MethodInfo Queryable_OrderBy = MethodOf((IQueryable<CallTreeNode> q) => q.OrderBy(x => x)).GetGenericMethodDefinition();
public static readonly MethodInfo Queryable_OrderByDesc = MethodOf((IQueryable<CallTreeNode> q) => q.OrderByDescending(x => x)).GetGenericMethodDefinition(); public static readonly MethodInfo Queryable_OrderByDesc = MethodOf((IQueryable<CallTreeNode> q) => q.OrderByDescending(x => x)).GetGenericMethodDefinition();
public static readonly MethodInfo Queryable_ThenBy = MethodOf((IOrderedQueryable<CallTreeNode> q) => q.ThenBy(x => x)).GetGenericMethodDefinition();
public static readonly MethodInfo Queryable_ThenBy2 = MethodOf((IOrderedQueryable<CallTreeNode> q) => q.ThenBy(x => x, null)).GetGenericMethodDefinition();
public static readonly MethodInfo Queryable_ThenByDesc = MethodOf((IOrderedQueryable<CallTreeNode> q) => q.ThenByDescending(x => x)).GetGenericMethodDefinition();
public static readonly MethodInfo Queryable_ThenByDesc2 = MethodOf((IOrderedQueryable<CallTreeNode> q) => q.ThenByDescending(x => x, null)).GetGenericMethodDefinition();
public static readonly MethodInfo String_StartsWith = MethodOf((string s) => s.StartsWith(s, default(StringComparison))); public static readonly MethodInfo String_StartsWith = MethodOf((string s) => s.StartsWith(s, default(StringComparison)));
public static readonly MethodInfo String_EndsWith = MethodOf((string s) => s.EndsWith(s, default(StringComparison)));
public static readonly MethodInfo Queryable_Merge = MethodOf((IQueryable<CallTreeNode> q) => q.Merge()); public static readonly MethodInfo Queryable_Merge = MethodOf((IQueryable<CallTreeNode> q) => q.Merge());
public static readonly MethodInfo Queryable_MergeByName = MethodOf((IQueryable<CallTreeNode> q) => q.MergeByName()); public static readonly MethodInfo Queryable_MergeByName = MethodOf((IQueryable<CallTreeNode> q) => q.MergeByName());

33
src/AddIns/Misc/Profiler/Controller/Data/Linq/QueryAst.cs

@ -335,9 +335,10 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} }
} }
struct SortArgument { class SortArgument
LambdaExpression arg; {
bool desc; readonly LambdaExpression arg;
readonly bool desc;
public SortArgument(LambdaExpression arg, bool desc) public SortArgument(LambdaExpression arg, bool desc)
{ {
@ -351,18 +352,21 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public LambdaExpression Argument { public LambdaExpression Argument {
get { return arg; } get { return arg; }
} }
public override string ToString()
{
if (Descending)
return Argument.ToString() + " DESC";
else
return Argument.ToString();
}
} }
sealed class Sort : QueryNode sealed class Sort : QueryNode
{ {
ReadOnlyCollection<SortArgument> arguments; ReadOnlyCollection<SortArgument> arguments;
public Sort(QueryNode target, LambdaExpression argument, bool desc) public Sort(QueryNode target, IList<SortArgument> args)
: this(target, new[] { new SortArgument(argument, desc) })
{
}
Sort(QueryNode target, IList<SortArgument> args)
: base(target) : base(target)
{ {
this.arguments = new ReadOnlyCollection<SortArgument>(args); this.arguments = new ReadOnlyCollection<SortArgument>(args);
@ -407,11 +411,12 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
public override string ToString() public override string ToString()
{ {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
for (int i = 0; i < arguments.Count; i++) {
foreach (var item in arguments) if (i > 0)
builder.Append(item.Argument + " " + (item.Descending ? "DESC" : "ASC") + ","); builder.Append(", ");
builder.Append(arguments[i].ToString());
return Target + ".Sort( {" + builder.ToString() + "} )"; }
return Target + ".Sort(" + builder.ToString() + ")";
} }
} }
} }

86
src/AddIns/Misc/Profiler/Controller/Data/Linq/SQLiteQueryProvider.cs

@ -178,7 +178,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
/// Optimizes the query without executing it. /// Optimizes the query without executing it.
/// Used for unit tests. /// Used for unit tests.
/// </summary> /// </summary>
public Expression OptimizeQuery(Expression inputExpression) public static Expression OptimizeQuery(Expression inputExpression)
{ {
Expression partiallyEvaluatedExpression = PartialEvaluator.Eval(inputExpression, CanBeEvaluatedStatically); Expression partiallyEvaluatedExpression = PartialEvaluator.Eval(inputExpression, CanBeEvaluatedStatically);
Expression expression = new ConvertToQueryAstVisitor(new QueryExecutionOptions()).Visit(partiallyEvaluatedExpression); Expression expression = new ConvertToQueryAstVisitor(new QueryExecutionOptions()).Visit(partiallyEvaluatedExpression);
@ -296,25 +296,79 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
return new Limit(target, 0, (int)ce.Value); return new Limit(target, 0, (int)ce.Value);
} }
} }
} else if ((IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderBy) || } else if (IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderBy) ||
IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderByDesc)) && IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderByDesc)) {
node.Arguments[1].NodeType == ExpressionType.Quote) { SortArgument sortArgument = GetSortArgumentFromSortCall(node);
if (sortArgument != null) {
QueryNode target = Visit(node.Arguments[0]) as QueryNode;
if (target != null) {
return new Sort(target, new [] { sortArgument });
}
}
} else if (IsThenByCall(node)) {
// 'ThenBy' is dangerous: we must not translate an OrderBy inside a ThenBy that we do not support
List<MethodCallExpression> thenByCalls = new List<MethodCallExpression>();
Expression tmp = node;
while (IsThenByCall(tmp)) {
thenByCalls.Add((MethodCallExpression)tmp);
tmp = ((MethodCallExpression)tmp).Arguments[0];
}
MethodCallExpression mc = tmp as MethodCallExpression;
if (mc != null &&
(IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_OrderBy) ||
IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_OrderByDesc)))
{
// TODO: add support for safe expressions in ThenBy
// this is an unsupported OrderBy/ThenBy sequence
// skip visiting the sequence and go directly into the base object
tmp = Visit(mc.Arguments[0]);
// now reconstruct the OrderBy/ThenBy sequence
tmp = Expression.Call(mc.Method, new[] { tmp }.Concat(mc.Arguments.Skip(1))); // reconstruct OrderBy
for (int i = thenByCalls.Count - 1; i >= 0; i--) {
// reconstruct ThenBy
tmp = Expression.Call(thenByCalls[i].Method, new[] { tmp }.Concat(thenByCalls[i].Arguments.Skip(1)));
}
return tmp;
}
// else: we couldn't detect the OrderBy belonging to this ThenBy; so it's probably not one
// of the OrderBy overloads that we support. Go down recursively
}
return base.VisitMethodCall(node);
}
SortArgument GetSortArgumentFromSortCall(MethodCallExpression node)
{
if (node.Arguments[1].NodeType == ExpressionType.Quote) {
UnaryExpression quote = (UnaryExpression)node.Arguments[1]; UnaryExpression quote = (UnaryExpression)node.Arguments[1];
if (quote.Operand.NodeType == ExpressionType.Lambda) { if (quote.Operand.NodeType == ExpressionType.Lambda) {
LambdaExpression lambda = (LambdaExpression)quote.Operand; LambdaExpression lambda = (LambdaExpression)quote.Operand;
if (lambda.Parameters.Count == 1) { if (lambda.Parameters.Count == 1) {
QueryNode target = Visit(node.Arguments[0]) as QueryNode; SafeExpressionImporter importer = new SafeExpressionImporter(lambda.Parameters[0]);
if (target != null) { Expression imported = importer.Import(lambda.Body);
SafeExpressionImporter importer = new SafeExpressionImporter(lambda.Parameters[0]); if (imported != null)
Expression imported = importer.Import(lambda.Body); return new SortArgument(
if (imported != null) Expression.Lambda(imported, lambda.Parameters[0]),
return new Sort(target, Expression.Lambda(imported, lambda.Parameters[0]), IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderByDesc)); IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_OrderByDesc)
} || IsGenericMethodInfoOfCallTreeNode(node, KnownMembers.Queryable_ThenByDesc)
);
} }
} }
} }
return base.VisitMethodCall(node); return null;
}
bool IsThenByCall(Expression potentialCall)
{
MethodCallExpression mc = potentialCall as MethodCallExpression;
if (mc != null) {
return IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_ThenBy) ||
IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_ThenBy2) ||
IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_ThenByDesc) ||
IsGenericMethodInfoOfCallTreeNode(mc, KnownMembers.Queryable_ThenByDesc2);
} else {
return false;
}
} }
} }
#endregion #endregion
@ -409,7 +463,9 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
MethodCallExpression mc = (MethodCallExpression)expr; MethodCallExpression mc = (MethodCallExpression)expr;
if (mc.Method == KnownMembers.String_StartsWith) if (mc.Method == KnownMembers.String_StartsWith)
return CreateCall(mc, (pattern, wildcard) => pattern + wildcard); return CreateStringMatchCall(mc, (pattern, wildcard) => pattern + wildcard);
if (mc.Method == KnownMembers.String_EndsWith)
return CreateStringMatchCall(mc, (pattern, wildcard) => wildcard + pattern);
return null; return null;
} }
@ -442,7 +498,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
} }
} }
Expression CreateCall(MethodCallExpression mc, Func<string, string, string> expressionOf) Expression CreateStringMatchCall(MethodCallExpression mc, Func<string, string, string> expressionOf)
{ {
Expression imported = Import(mc.Object); Expression imported = Import(mc.Object);
if (imported != null && mc.Arguments[0].NodeType == ExpressionType.Constant && mc.Arguments[1].NodeType == ExpressionType.Constant) { if (imported != null && mc.Arguments[0].NodeType == ExpressionType.Constant && mc.Arguments[1].NodeType == ExpressionType.Constant) {

15
src/AddIns/Misc/Profiler/Controller/Data/Linq/SqlQueryContext.cs

@ -32,7 +32,7 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
sealed class CallTreeNodeSqlNameSet sealed class CallTreeNodeSqlNameSet
{ {
public readonly string ID; public readonly string ID;
public readonly string NameID = "nameid"; public readonly string NameID;
public readonly string CpuCyclesSpent; public readonly string CpuCyclesSpent;
public readonly string CallCount; public readonly string CallCount;
public readonly string HasChildren; public readonly string HasChildren;
@ -48,15 +48,10 @@ namespace ICSharpCode.Profiler.Controller.Data.Linq
this.IsCalls = isCalls; this.IsCalls = isCalls;
string prefix = c.GenerateUniqueVariableName(); string prefix = c.GenerateUniqueVariableName();
if (isCalls) { ID = prefix + "ID";
ID = "id"; NameID = prefix + "NameID";
CpuCyclesSpent = "cpucyclesspent"; CpuCyclesSpent = prefix + "CpuCyclesSpent";
CallCount = "callcount"; CallCount = prefix + "CallCount";
} else {
ID = prefix + "ID";
CpuCyclesSpent = prefix + "CpuCyclesSpent";
CallCount = prefix + "CallCount";
}
HasChildren = prefix + "HasChildren"; HasChildren = prefix + "HasChildren";
ActiveCallCount = prefix + "ActiveCallCount"; ActiveCallCount = prefix + "ActiveCallCount";
} }

48
src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Controller/Data/LinqTests.cs

@ -143,5 +143,53 @@ namespace Profiler.Tests.Controller.Data
Assert.AreEqual(1, functions[1].CallCount); Assert.AreEqual(1, functions[1].CallCount);
Assert.AreEqual(350 * k, functions[1].CpuCyclesSpent); Assert.AreEqual(350 * k, functions[1].CpuCyclesSpent);
} }
[Test]
public void TestSupportedOrderByOnRootChildren()
{
CallTreeNode root = provider.GetRoot(0, 0);
var query = root.Children.OrderBy(f => f.CallCount);
Assert.AreEqual("AllCalls.Filter(c => (c.ParentID == 0)).MergeByName().Sort(f => f.CallCount)",
SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
}
[Test]
public void TestOrderByNameMappingName()
{
CallTreeNode root = provider.GetRoot(0, 0);
var query = root.Children.OrderBy(f => f.NameMapping.Name);
Assert.AreEqual("AllCalls.Filter(c => (c.ParentID == 0)).MergeByName().Sort(f => f.NameMapping.Name)",
SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
}
[Test]
public void TestOrderByWithUnsupporedThenBy()
{
CallTreeNode root = provider.GetRoot(0, 0);
var query = root.Children.OrderBy(f => f.CallCount).ThenBy(f => f.Ancestors.Count());
Assert.AreEqual("AllCalls.Filter(c => (c.ParentID == 0)).MergeByName()" +
".OrderBy(f => f.CallCount).ThenBy(f => f.Ancestors.Count())",
SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
}
[Test]
public void TestOrderBySupportedThenByButUnsupportedConverter()
{
CallTreeNode root = provider.GetRoot(0, 0);
var query = root.Children.OrderBy(f => f.CallCount).ThenBy(f => f.NameMapping.Name, StringComparer.CurrentCultureIgnoreCase);
Assert.AreEqual("AllCalls.Filter(c => (c.ParentID == 0)).MergeByName()" +
".OrderBy(f => f.CallCount).ThenBy(f => f.NameMapping.Name, value(System.CultureAwareComparer))",
SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
}
[Test]
public void TestOrderByWithMultiLevelUnsupporedThenBy()
{
CallTreeNode root = provider.GetRoot(0, 0);
var query = root.Children.OrderBy(f => f.CallCount).ThenBy(f => f.NameMapping.Name).ThenBy(f => f.Ancestors.Count());
Assert.AreEqual("AllCalls.Filter(c => (c.ParentID == 0)).MergeByName()" +
".OrderBy(f => f.CallCount).ThenBy(f => f.NameMapping.Name).ThenBy(f => f.Ancestors.Count())",
SQLiteQueryProvider.OptimizeQuery(query.Expression).ToString());
}
} }
} }

3
src/AddIns/Misc/Profiler/Tests/Profiler.Tests/Profiler.Tests.csproj

@ -74,6 +74,9 @@
</PropertyGroup> </PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" /> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />
<ItemGroup> <ItemGroup>
<Reference Include="IQToolkit">
<HintPath>..\..\..\..\..\Libraries\IQToolkit\IQToolkit.dll</HintPath>
</Reference>
<Reference Include="nunit.framework"> <Reference Include="nunit.framework">
<HintPath>..\..\..\..\..\..\bin\Tools\NUnit\nunit.framework.dll</HintPath> <HintPath>..\..\..\..\..\..\bin\Tools\NUnit\nunit.framework.dll</HintPath>
</Reference> </Reference>

10
src/Main/Base/Project/Src/Util/ProcessRunner.cs

@ -6,6 +6,7 @@
// </file> // </file>
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@ -151,6 +152,12 @@ namespace ICSharpCode.SharpDevelop.Util
} }
} }
Dictionary<string, string> environmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, string> EnvironmentVariables {
get { return environmentVariables; }
}
/// <summary> /// <summary>
/// Starts the process. /// Starts the process.
/// </summary> /// </summary>
@ -174,6 +181,9 @@ namespace ICSharpCode.SharpDevelop.Util
process.StartInfo.UseShellExecute = false; process.StartInfo.UseShellExecute = false;
process.StartInfo.Arguments = arguments; process.StartInfo.Arguments = arguments;
foreach (var pair in environmentVariables)
process.StartInfo.EnvironmentVariables.Add(pair.Key, pair.Value);
if (ProcessExited != null) { if (ProcessExited != null) {
process.EnableRaisingEvents = true; process.EnableRaisingEvents = true;
process.Exited += OnProcessExited; process.Exited += OnProcessExited;

Loading…
Cancel
Save