mirror of
https://github.com/ElectronNET/Electron.NET.git
synced 2026-02-14 05:34:48 +00:00
ElectronNET.API: Add ProcessRunner (for running electron from dotnet)
This commit is contained in:
595
src/ElectronNET.API/Common/ProcessRunner.cs
Normal file
595
src/ElectronNET.API/Common/ProcessRunner.cs
Normal file
@@ -0,0 +1,595 @@
|
||||
namespace ElectronNET.Common
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
/// <summary>
|
||||
/// Class encapsulating out-of-process execution of console applications.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Why this class?
|
||||
/// Probably everybody who has tried to use System.Diagnotics.Process cross-platform and with reading
|
||||
/// stderr and stdout will know that it is a pretty quirky API.
|
||||
/// The code below may look weird, even non-sensical, but it works 100% reliable with all .net frameworks
|
||||
/// and .net versions and on every platform where .net runs. This is just the innermost core, that's why
|
||||
/// there are many dead ends, but it has all the crucial parts.
|
||||
/// </remarks>
|
||||
/// <seealso cref="System.IDisposable" />
|
||||
[SuppressMessage("ReSharper", "SuspiciousLockOverSynchronizationPrimitive")]
|
||||
public class ProcessRunner : IDisposable
|
||||
{
|
||||
private volatile Process process;
|
||||
private readonly StringBuilder stdOut = new StringBuilder(4 * 1024);
|
||||
private readonly StringBuilder stdErr = new StringBuilder(4 * 1024);
|
||||
|
||||
private volatile ManualResetEvent stdOutEvent;
|
||||
private volatile ManualResetEvent stdErrEvent;
|
||||
private volatile Stopwatch stopwatch;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="ProcessRunner" /> class.</summary>
|
||||
/// <param name="name">A name identifying the process to execute.</param>
|
||||
public ProcessRunner(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
public event EventHandler<EventArgs> ProcessExited;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private Process Process
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.process;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
var proc = this.process;
|
||||
if (proc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return !proc.HasExited;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the name identifying the process.</summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; }
|
||||
|
||||
public string CommandLine { get; private set; }
|
||||
|
||||
public string ExecutableFileName { get; private set; }
|
||||
|
||||
public string WorkingFolder { get; private set; }
|
||||
|
||||
public bool RecordStandardOutput { get; set; }
|
||||
|
||||
public bool RecordStandardError { get; set; }
|
||||
|
||||
public string StandardOutput
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this.stdOut)
|
||||
{
|
||||
return this.stdOut.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string StandardError
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this.stdErr)
|
||||
{
|
||||
return this.stdErr.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int? LastExitCode { get; private set; }
|
||||
|
||||
public bool Run(string exeFileName, string commandLineArgs, string workingDirectory)
|
||||
{
|
||||
this.CommandLine = commandLineArgs;
|
||||
this.WorkingFolder = workingDirectory;
|
||||
this.ExecutableFileName = exeFileName;
|
||||
|
||||
var startInfo = new RunnerParams(exeFileName)
|
||||
{
|
||||
Arguments = commandLineArgs,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = true,
|
||||
ErrorDialog = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = workingDirectory
|
||||
};
|
||||
|
||||
return this.Run(startInfo);
|
||||
}
|
||||
|
||||
protected bool Run(RunnerParams runnerParams)
|
||||
{
|
||||
if (this.IsDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().ToString());
|
||||
}
|
||||
|
||||
this.Close();
|
||||
|
||||
this.LastExitCode = null;
|
||||
|
||||
lock (this.stdOut)
|
||||
{
|
||||
this.stdOut.Clear();
|
||||
}
|
||||
|
||||
lock (this.stdErr)
|
||||
{
|
||||
this.stdErr.Clear();
|
||||
}
|
||||
|
||||
this.stdOutEvent = new ManualResetEvent(false);
|
||||
this.stdErrEvent = new ManualResetEvent(false);
|
||||
|
||||
if (!this.OnBeforeStartProcessCore(runnerParams))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo(runnerParams.FileName)
|
||||
{
|
||||
Arguments = runnerParams.Arguments,
|
||||
UseShellExecute = runnerParams.UseShellExecute,
|
||||
RedirectStandardOutput = runnerParams.RedirectStandardOutput,
|
||||
RedirectStandardError = runnerParams.RedirectStandardError,
|
||||
RedirectStandardInput = runnerParams.RedirectStandardInput,
|
||||
ErrorDialog = runnerParams.ErrorDialog,
|
||||
CreateNoWindow = runnerParams.CreateNoWindow,
|
||||
WorkingDirectory = runnerParams.WorkingDirectory
|
||||
};
|
||||
|
||||
foreach (var variableSetting in runnerParams.EnvironmentVariables)
|
||||
{
|
||||
startInfo.EnvironmentVariables[variableSetting.Key] = variableSetting.Value;
|
||||
}
|
||||
|
||||
var proc = new Process { StartInfo = startInfo };
|
||||
|
||||
proc.EnableRaisingEvents = true;
|
||||
|
||||
this.RegisterProcessEvents(proc);
|
||||
|
||||
this.process = proc;
|
||||
|
||||
try
|
||||
{
|
||||
this.process.Start();
|
||||
this.stopwatch = Stopwatch.StartNew();
|
||||
this.process.BeginOutputReadLine();
|
||||
this.process.BeginErrorReadLine();
|
||||
this.process.Refresh();
|
||||
this.OnProcessStartedCore();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.OnProcessErrorCore(ex);
|
||||
this.Close();
|
||||
throw;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> WriteAsync(string data)
|
||||
{
|
||||
var proc = this.Process;
|
||||
if (proc != null && !proc.HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
await proc.StandardInput.WriteAsync(data).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("{0}.{1}: {2}", ex, nameof(ProcessRunner), nameof(this.WriteAsync));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool WaitForExit()
|
||||
{
|
||||
var proc = this.process;
|
||||
|
||||
if (proc == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Wait for process and all I/O to finish.
|
||||
proc.WaitForExit();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.OnProcessErrorCore(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sychronously waits for the specified amount and ends the process afterwards.</summary>
|
||||
/// <param name="timeoutMs">The timeout ms.</param>
|
||||
/// <remarks>This method allows for a clean exit, since it also waits until the StandardOutput and
|
||||
/// StandardError pipes are processed to the end.</remarks>
|
||||
/// <returns>true, if the process has exited gracefully; false otherwise.</returns>
|
||||
public bool WaitAndKill(int timeoutMs)
|
||||
{
|
||||
var proc = this.process;
|
||||
|
||||
if (proc == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (timeoutMs <= 0)
|
||||
{
|
||||
throw new ArgumentException("Argument must be greater then 0", nameof(timeoutMs));
|
||||
}
|
||||
|
||||
// Timed waiting. We need to wait for I/O ourselves.
|
||||
if (!proc.WaitForExit(timeoutMs))
|
||||
{
|
||||
this.Cancel();
|
||||
}
|
||||
|
||||
// Wait for the I/O to finish.
|
||||
var waitMs = (int)(timeoutMs - this.stopwatch.ElapsedMilliseconds);
|
||||
waitMs = Math.Max(waitMs, 10);
|
||||
this.stdOutEvent?.WaitOne(waitMs);
|
||||
|
||||
waitMs = (int)(timeoutMs - this.stopwatch.ElapsedMilliseconds);
|
||||
waitMs = Math.Max(waitMs, 10);
|
||||
return this.stdErrEvent?.WaitOne(waitMs) ?? false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup.
|
||||
this.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Asynchronously waits for the specified amount and ends the process afterwards.</summary>
|
||||
/// <param name="timeoutMs">The timeout ms.</param>
|
||||
/// <remarks>Tjhis method performs the wait operation on a threadpool thread.
|
||||
/// Only recommended for short timeouts and situations where a synchronous call is undesired.</remarks>
|
||||
/// <returns>true, if the process has exited gracefully; false otherwise.</returns>
|
||||
public Task<bool> WaitAndKillAsync(int timeoutMs)
|
||||
{
|
||||
var task = Task.Run(() => this.WaitAndKill(timeoutMs));
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>Waits asynchronously for the process to exit.</summary>
|
||||
/// <param name="timeoutMs">The timeout ms.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>true, if the process has exited, false if the process is still running.</returns>
|
||||
/// <remarks>
|
||||
/// This methods waits until the process has existed or the
|
||||
/// <paramref name="timeoutMs" /> has elapsed.
|
||||
/// This method does not end the process itself.
|
||||
/// </remarks>
|
||||
public Task<bool> WaitForExitAsync(int timeoutMs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
timeoutMs = Math.Max(0, timeoutMs);
|
||||
|
||||
var timeoutSource = new CancellationTokenSource(timeoutMs);
|
||||
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, cancellationToken);
|
||||
|
||||
return this.WaitForExitAsync(linkedSource.Token);
|
||||
}
|
||||
|
||||
/// <summary>Waits asynchronously for the process to exit.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <remarks>This methods waits until the process has existed or the
|
||||
/// <paramref name="cancellationToken"/> has been triggered.
|
||||
/// This method does not end the process itself.</remarks>
|
||||
/// <returns>true, if the process has exited, false if the process is still running.</returns>
|
||||
public async Task<bool> WaitForExitAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
var proc = this.process;
|
||||
|
||||
if (proc == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
// Use local function instead of a lambda to allow proper deregistration of the event
|
||||
void ProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
proc.EnableRaisingEvents = true;
|
||||
proc.Exited += ProcessExited;
|
||||
|
||||
if (proc.HasExited)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using (cancellationToken.Register(() => tcs.TrySetResult(false)))
|
||||
{
|
||||
return await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
proc.Exited -= ProcessExited;
|
||||
}
|
||||
}
|
||||
|
||||
public void Cancel()
|
||||
{
|
||||
var proc = this.process;
|
||||
|
||||
if (proc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Invalidate cached data to requery.
|
||||
proc.Refresh();
|
||||
|
||||
// We need to do this in case of a non-UI proc
|
||||
// or one to be forced to cancel.
|
||||
if (!proc.HasExited)
|
||||
{
|
||||
// Cancel all pending IO ops.
|
||||
proc.CancelErrorRead();
|
||||
proc.CancelOutputRead();
|
||||
}
|
||||
|
||||
if (!proc.HasExited)
|
||||
{
|
||||
proc.Kill();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Kill will throw when/if the process has already exited.
|
||||
}
|
||||
}
|
||||
|
||||
var outEvent = this.stdOutEvent;
|
||||
this.stdOutEvent = null;
|
||||
if (outEvent != null)
|
||||
{
|
||||
lock (outEvent)
|
||||
{
|
||||
outEvent.Close();
|
||||
outEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
var errEvent = this.stdErrEvent;
|
||||
this.stdErrEvent = null;
|
||||
if (errEvent != null)
|
||||
{
|
||||
lock (errEvent)
|
||||
{
|
||||
errEvent.Close();
|
||||
errEvent.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Close()
|
||||
{
|
||||
this.Cancel();
|
||||
|
||||
var proc = this.process;
|
||||
this.process = null;
|
||||
if (proc != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.UnRegisterProcessEvents(proc);
|
||||
|
||||
// Dispose in all cases.
|
||||
proc.Close();
|
||||
proc.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.OnProcessErrorCore(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDispose()
|
||||
{
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
this.IsDisposed = true;
|
||||
this.Close();
|
||||
this.OnDispose();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0}: {1} {2}", this.GetType().Name, this.Name, this.process);
|
||||
}
|
||||
|
||||
protected virtual bool OnBeforeStartProcessCore(RunnerParams processRunnerInfo)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual void OnProcessStartedCore()
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnProcessErrorCore(Exception processException)
|
||||
{
|
||||
}
|
||||
|
||||
protected virtual void OnProcessExitCore(int exitCode)
|
||||
{
|
||||
}
|
||||
|
||||
private void RegisterProcessEvents(Process proc)
|
||||
{
|
||||
proc.ErrorDataReceived += this.Process_ErrorDataReceived;
|
||||
proc.OutputDataReceived += this.Process_OutputDataReceived;
|
||||
proc.Exited += this.Process_Exited;
|
||||
}
|
||||
|
||||
private void UnRegisterProcessEvents(Process proc)
|
||||
{
|
||||
proc.ErrorDataReceived -= this.Process_ErrorDataReceived;
|
||||
proc.OutputDataReceived -= this.Process_OutputDataReceived;
|
||||
proc.Exited -= this.Process_Exited;
|
||||
}
|
||||
|
||||
private void Process_Exited(object sender, EventArgs e)
|
||||
{
|
||||
this.WaitForExitAfterExited();
|
||||
this.SetExitCode();
|
||||
this.OnProcessExitCore(this.LastExitCode ?? -9998);
|
||||
this.ProcessExited?.Invoke(this, new EventArgs());
|
||||
}
|
||||
|
||||
private void WaitForExitAfterExited()
|
||||
{
|
||||
try
|
||||
{
|
||||
// This shouldn't throw here, but the mono process implementation doesn't always behave as it should.
|
||||
this.process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Error when calling WaitForExit after exited event has fired: {0}.{1}: {2}", ex, nameof(ProcessRunner), nameof(this.WaitForExitAfterExited));
|
||||
}
|
||||
}
|
||||
|
||||
private void SetExitCode()
|
||||
{
|
||||
int exitCode = -9999;
|
||||
|
||||
try
|
||||
{
|
||||
if (this.Process != null)
|
||||
{
|
||||
exitCode = this.Process.ExitCode;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore error.
|
||||
}
|
||||
|
||||
this.LastExitCode = exitCode;
|
||||
}
|
||||
|
||||
private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (this.RecordStandardError)
|
||||
{
|
||||
lock (this.stdErr)
|
||||
{
|
||||
this.stdErr.AppendLine(e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.Data != null)
|
||||
{
|
||||
Console.WriteLine("|| " + e.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
var evt = this.stdErrEvent;
|
||||
if (evt != null)
|
||||
{
|
||||
lock (evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
evt.Set();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
if (this.RecordStandardOutput)
|
||||
{
|
||||
lock (this.stdOut)
|
||||
{
|
||||
this.stdOut.AppendLine(e.Data);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.Data != null)
|
||||
{
|
||||
Console.WriteLine("|| " + e.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
var evt = this.stdOutEvent;
|
||||
if (evt != null)
|
||||
{
|
||||
lock (evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
evt.Set();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore error.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/ElectronNET.API/Common/RunnerParams.cs
Normal file
163
src/ElectronNET.API/Common/RunnerParams.cs
Normal file
@@ -0,0 +1,163 @@
|
||||
namespace ElectronNET.Common
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
public sealed class RunnerParams
|
||||
{
|
||||
private string fileName;
|
||||
private string arguments;
|
||||
private string directory;
|
||||
private string userName;
|
||||
private string verb;
|
||||
private ProcessWindowStyle windowStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor. At least the <see cref='System.Diagnostics.ProcessStartInfo.FileName'/>
|
||||
/// property must be set before starting the process.
|
||||
/// </summary>
|
||||
public RunnerParams()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the name of the application or document that is to be started.
|
||||
/// </summary>
|
||||
public RunnerParams(string fileName)
|
||||
{
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the name of the application that is to be started, as well as a set
|
||||
/// of command line arguments to pass to the application.
|
||||
/// </summary>
|
||||
public RunnerParams(string fileName, string arguments)
|
||||
{
|
||||
this.fileName = fileName;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the set of command line arguments to use when starting the application.
|
||||
/// </summary>
|
||||
public string Arguments
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.arguments ?? string.Empty;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.arguments = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CreateNoWindow { get; set; }
|
||||
|
||||
public Dictionary<string, string> EnvironmentVariables { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public bool RedirectStandardInput { get; set; }
|
||||
|
||||
public bool RedirectStandardOutput { get; set; }
|
||||
|
||||
public bool RedirectStandardError { get; set; }
|
||||
|
||||
public Encoding StandardInputEncoding { get; set; }
|
||||
|
||||
public Encoding StandardErrorEncoding { get; set; }
|
||||
|
||||
public Encoding StandardOutputEncoding { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Returns or sets the application, document, or URL that is to be launched.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public string FileName
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.fileName ?? string.Empty;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.fileName = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns or sets the initial directory for the process that is started.
|
||||
/// Specify "" to if the default is desired.
|
||||
/// </summary>
|
||||
public string WorkingDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.directory ?? string.Empty;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.directory = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ErrorDialog { get; set; }
|
||||
|
||||
public IntPtr ErrorDialogParentHandle { get; set; }
|
||||
|
||||
public string UserName
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.userName ?? string.Empty;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.userName = value;
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue("")]
|
||||
public string Verb
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.verb ?? string.Empty;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
this.verb = value;
|
||||
}
|
||||
}
|
||||
|
||||
[DefaultValue(ProcessWindowStyle.Normal)]
|
||||
public ProcessWindowStyle WindowStyle
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.windowStyle;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (!Enum.IsDefined(typeof(ProcessWindowStyle), value))
|
||||
{
|
||||
throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(ProcessWindowStyle));
|
||||
}
|
||||
|
||||
this.windowStyle = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool UseShellExecute { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user