How to use ExitAsync method of PuppeteerSharp.States.DisposedState class

Best Puppeteer-sharp code snippet using PuppeteerSharp.States.DisposedState.ExitAsync

Run Puppeteer-sharp automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

DisposedState.cs

Source: DisposedState.cs Github

copy
1using System;
2using System.Threading.Tasks;
3
4namespace PuppeteerSharp.States
5{
6    internal class DisposedState : State
7    {
8        public DisposedState(StateManager stateManager) : base(stateManager)
9        {
10        }
11
12        public override Task EnterFromAsync(LauncherBase p, State fromState, TimeSpan timeSpan)
13        {
14            if (fromState == StateManager.Exited)
15            {
16                return null;
17            }
18
19            Kill(p);
20
21            p.ExitCompletionSource.TrySetException(new ObjectDisposedException(p.ToString()));
22            p.TempUserDataDir?.Dispose();
23
24            return null;
25        }
26
27        public override Task StartAsync(LauncherBase p) => throw new ObjectDisposedException(p.ToString());
28
29        public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => throw new ObjectDisposedException(p.ToString());
30
31        public override Task KillAsync(LauncherBase p) => throw new ObjectDisposedException(p.ToString());
32
33        public override void Dispose(LauncherBase p)
34        {
35            // Nothing to do
36        }
37    }
38}
39
Full Screen

LauncherBase.cs

Source: LauncherBase.cs Github

copy
1using PuppeteerSharp.Helpers;
2using System;
3using System.Collections;
4using System.Collections.Generic;
5using System.Diagnostics;
6using System.Text;
7using System.Text.RegularExpressions;
8using System.Threading;
9using System.Threading.Tasks;
10
11namespace PuppeteerSharp
12{
13    /// <summary>
14    /// Represents a Base process and any associated temporary user data directory that have created
15    /// by Puppeteer and therefore must be cleaned up when no longer needed.
16    /// </summary>
17    public class LauncherBase : IDisposable
18    {
19
20        #region Instance fields
21
22        private readonly LaunchOptions _options;
23        private readonly TaskCompletionSource<string> _startCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
24        private readonly TaskCompletionSource<bool> _exitCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
25        private State _currentState = State.Initial;
26
27        #endregion
28
29        #region Constructor
30
31        /// <summary>
32        /// Creates a new <see cref="LauncherBase"/> instance.
33        /// </summary>
34        /// <param name="executable">Full path of executable.</param>
35        /// <param name="options">Options for launching Base.</param>
36        /// <param name="loggerFactory">Logger factory</param>
37        public LauncherBase(string executable, LaunchOptions options)
38        {
39            _options = options;
40
41            Process = new Process
42            {
43                EnableRaisingEvents = true
44            };
45            Process.StartInfo.UseShellExecute = false;
46            Process.StartInfo.FileName = executable;
47            Process.StartInfo.RedirectStandardError = true;
48
49            SetEnvVariables(Process.StartInfo.Environment, options.Env, Environment.GetEnvironmentVariables());
50
51            if (options.DumpIO)
52            {
53                Process.ErrorDataReceived += (sender, e) => Console.Error.WriteLine(e.Data);
54            }
55        }
56
57        #endregion
58
59        #region Dispose
60
61        /// <summary>
62        /// Finalizer.
63        /// </summary>
64        ~LauncherBase()
65        {
66            Dispose(false);
67        }
68
69        /// <inheritdoc />
70        public void Dispose()
71        {
72            Dispose(true);
73            GC.SuppressFinalize(this);
74        }
75
76        /// <summary>
77        /// Disposes Base process and any temporary user directory.
78        /// </summary>
79        /// <param name="disposing">Indicates whether disposal was initiated by <see cref="Dispose()"/> operation.</param>
80        protected virtual void Dispose(bool disposing) => _currentState.Dispose(this);
81
82        #endregion
83
84        #region Properties
85
86        /// <summary>
87        /// Gets Base process details.
88        /// </summary>
89        public Process Process { get; }
90
91        /// <summary>
92        /// Gets Base endpoint.
93        /// </summary>
94        public string EndPoint => _startCompletionSource.Task.IsCompleted
95            ? _startCompletionSource.Task.Result
96            : null;
97
98        /// <summary>
99        /// Indicates whether Base process is exiting.
100        /// </summary>
101        public bool IsExiting => _currentState.IsExiting;
102
103        /// <summary>
104        /// Indicates whether Base process has exited.
105        /// </summary>
106        public bool HasExited => _currentState.IsExited;
107
108        internal TempDirectory TempUserDataDir { get; set; }
109
110        /// <summary>
111        /// Gets Base process current state.
112        /// </summary>
113        protected State CurrentState { get => _currentState; }
114
115        #endregion
116
117        #region Public methods
118
119        /// <summary>
120        /// Asynchronously starts Base process.
121        /// </summary>
122        /// <returns></returns>
123        public Task StartAsync() => _currentState.StartAsync(this);
124
125        /// <summary>
126        /// Asynchronously waits for graceful Base process exit within a given timeout period.
127        /// Kills the Base process if it has not exited within this period.
128        /// </summary>
129        /// <param name="timeout">The maximum waiting time for a graceful process exit.</param>
130        /// <returns></returns>
131        public Task EnsureExitAsync(TimeSpan? timeout) => timeout.HasValue
132            ? _currentState.ExitAsync(this, timeout.Value)
133            : _currentState.KillAsync(this);
134
135        /// <summary>
136        /// Asynchronously kills Base process.
137        /// </summary>
138        /// <returns></returns>
139        public Task KillAsync() => _currentState.KillAsync(this);
140
141        /// <summary>
142        /// Waits for Base process exit within a given timeout.
143        /// </summary>
144        /// <param name="timeout">The maximum wait period.</param>
145        /// <returns><c>true</c> if Base process has exited within the given <paramref name="timeout"/>,
146        /// or <c>false</c> otherwise.</returns>
147        public async Task<bool> WaitForExitAsync(TimeSpan? timeout)
148        {
149            if (timeout.HasValue)
150            {
151                bool taskCompleted = true;
152                await _exitCompletionSource.Task.WithTimeout(
153                    () =>
154                    {
155                        taskCompleted = false;
156                    }, timeout.Value).ConfigureAwait(false);
157                return taskCompleted;
158            }
159
160            await _exitCompletionSource.Task.ConfigureAwait(false);
161            return true;
162        }
163
164        #endregion
165
166        #region Private methods
167
168        /// <summary>
169        /// Set Env Variables
170        /// </summary>
171        /// <param name="environment">The environment.</param>
172        /// <param name="customEnv">The customEnv.</param>
173        /// <param name="realEnv">The realEnv.</param>
174        protected static void SetEnvVariables(IDictionary<string, string> environment, IDictionary<string, string> customEnv, IDictionary realEnv)
175        {
176            foreach (DictionaryEntry item in realEnv)
177            {
178                environment[item.Key.ToString()] = item.Value.ToString();
179            }
180
181            if (customEnv != null)
182            {
183                foreach (var item in customEnv)
184                {
185                    environment[item.Key] = item.Value;
186                }
187            }
188        }
189
190        #endregion
191
192        #region State machine
193
194        /// <summary>
195        /// Represents state machine for Base process instances. The happy path runs along the
196        /// following state transitions: <see cref="Initial"/>
197        /// -> <see cref="Starting"/>
198        /// -> <see cref="Started"/>
199        /// -> <see cref="Exiting"/>
200        /// -> <see cref="Exited"/>.
201        /// -> <see cref="Disposed"/>.
202        /// </summary>
203        /// <remarks>
204        /// <para>
205        /// This state machine implements the following state transitions:
206        /// <code>
207        /// State     Event              Target State Action
208        /// ======== =================== ============ ==========================================================
209        /// Initial  --StartAsync------> Starting     Start process and wait for endpoint
210        /// Initial  --ExitAsync-------> Exited       Cleanup temp user data
211        /// Initial  --KillAsync-------> Exited       Cleanup temp user data
212        /// Initial  --Dispose---------> Disposed     Cleanup temp user data
213        /// Starting --StartAsync------> Starting     -
214        /// Starting --ExitAsync-------> Exiting      Wait for process exit
215        /// Starting --KillAsync-------> Killing      Kill process
216        /// Starting --Dispose---------> Disposed     Kill process; Cleanup temp user data;  throw ObjectDisposedException on outstanding async operations;
217        /// Starting --endpoint ready--> Started      Complete StartAsync successfully; Log process start
218        /// Starting --process exit----> Exited       Complete StartAsync with exception; Cleanup temp user data
219        /// Started  --StartAsync------> Started      -
220        /// Started  --EnsureExitAsync-> Exiting      Start exit timer; Log process exit
221        /// Started  --KillAsync-------> Killing      Kill process; Log process exit
222        /// Started  --Dispose---------> Disposed     Kill process; Log process exit; Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
223        /// Started  --process exit----> Exited       Log process exit; Cleanup temp user data
224        /// Exiting  --StartAsync------> Exiting      - (StartAsync throws InvalidOperationException)
225        /// Exiting  --ExitAsync-------> Exiting      -
226        /// Exiting  --KillAsync-------> Killing      Kill process
227        /// Exiting  --Dispose---------> Disposed     Kill process; Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
228        /// Exiting  --exit timeout----> Killing      Kill process
229        /// Exiting  --process exit----> Exited       Cleanup temp user data; complete outstanding async operations;
230        /// Killing  --StartAsync------> Killing      - (StartAsync throws InvalidOperationException)
231        /// Killing  --KillAsync-------> Killing      -
232        /// Killing  --Dispose---------> Disposed     Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
233        /// Killing  --process exit----> Exited       Cleanup temp user data; complete outstanding async operations;
234        /// Exited   --StartAsync------> Killing      - (StartAsync throws InvalidOperationException)
235        /// Exited   --KillAsync-------> Exited       -
236        /// Exited   --Dispose---------> Disposed     -
237        /// Disposed --StartAsync------> Disposed     -
238        /// Disposed --KillAsync-------> Disposed     -
239        /// Disposed --Dispose---------> Disposed     -
240        /// </code>
241        /// </para>
242        /// </remarks>
243        protected abstract class State
244        {
245            #region Predefined states
246
247            /// <summary>
248            /// Set initial state.
249            /// </summary>
250            public static readonly State Initial = new InitialState();
251            private static readonly StartingState Starting = new StartingState();
252            private static readonly StartedState Started = new StartedState();
253            private static readonly ExitingState Exiting = new ExitingState();
254            private static readonly KillingState Killing = new KillingState();
255            private static readonly ExitedState Exited = new ExitedState();
256            private static readonly DisposedState Disposed = new DisposedState();
257
258            #endregion
259
260            #region Properties
261
262            /// <summary>
263            /// Get If process exists.
264            /// </summary>
265            public bool IsExiting => this == Killing || this == Exiting;
266            /// <summary>
267            /// Get If process is exited.
268            /// </summary>
269            public bool IsExited => this == Exited || this == Disposed;
270
271            #endregion
272
273            #region Methods
274
275            /// <summary>
276            /// Attempts thread-safe transitions from a given state to this state.
277            /// </summary>
278            /// <param name="p">The Base process</param>
279            /// <param name="fromState">The state from which state transition takes place</param>
280            /// <returns>Returns <c>true</c> if transition is successful, or <c>false</c> if transition
281            /// cannot be made because current state does not equal <paramref name="fromState"/>.</returns>
282            protected bool TryEnter(LauncherBase p, State fromState)
283            {
284                if (Interlocked.CompareExchange(ref p._currentState, this, fromState) == fromState)
285                {
286                    fromState.Leave(p);
287                    return true;
288                }
289
290                return false;
291            }
292
293            /// <summary>
294            /// Notifies that state machine is about to transition to another state.
295            /// </summary>
296            /// <param name="p">The Base process</param>
297            protected virtual void Leave(LauncherBase p)
298            {
299            }
300
301            /// <summary>
302            /// Handles process start request.
303            /// </summary>
304            /// <param name="p">The Base process</param>
305            /// <returns></returns>
306            public virtual Task StartAsync(LauncherBase p) => Task.FromException(InvalidOperation("start"));
307
308            /// <summary>
309            /// Handles process exit request.
310            /// </summary>
311            /// <param name="p">The Base process</param>
312            /// <param name="timeout">The maximum waiting time for a graceful process exit.</param>
313            /// <returns></returns>
314            public virtual Task ExitAsync(LauncherBase p, TimeSpan timeout) => Task.FromException(InvalidOperation("exit"));
315
316            /// <summary>
317            /// Handles process kill request.
318            /// </summary>
319            /// <param name="p">The Base process</param>
320            /// <returns></returns>
321            public virtual Task KillAsync(LauncherBase p) => Task.FromException(InvalidOperation("kill"));
322
323            /// <summary>
324            /// Handles wait for process exit request.
325            /// </summary>
326            /// <param name="p">The Base process</param>
327            /// <returns></returns>
328            public virtual Task WaitForExitAsync(LauncherBase p) => p._exitCompletionSource.Task;
329
330            /// <summary>
331            /// Handles disposal of process and temporary user directory
332            /// </summary>
333            /// <param name="p"></param>
334            public virtual void Dispose(LauncherBase p) => Disposed.EnterFrom(p, this);
335
336            /// <inheritdoc />
337            public override string ToString()
338            {
339                string name = GetType().Name;
340                return name.Substring(0, name.Length - "State".Length);
341            }
342
343            private Exception InvalidOperation(string operationName)
344                => new InvalidOperationException($"Cannot {operationName} in state {this}");
345
346            /// <summary>
347            /// Kills process if it is still alive.
348            /// </summary>
349            /// <param name="p"></param>
350            private static void Kill(LauncherBase p)
351            {
352                try
353                {
354                    if (!p.Process.HasExited)
355                    {
356                        p.Process.Kill();
357                    }
358                }
359                catch (InvalidOperationException)
360                {
361                    // Ignore
362                }
363            }
364
365            #endregion
366
367            #region Concrete state classes
368
369            private class InitialState : State
370            {
371                public override Task StartAsync(LauncherBase p) => Starting.EnterFromAsync(p, this);
372
373                public override Task ExitAsync(LauncherBase p, TimeSpan timeout)
374                {
375                    Exited.EnterFrom(p, this);
376                    return Task.CompletedTask;
377                }
378
379                public override Task KillAsync(LauncherBase p)
380                {
381                    Exited.EnterFrom(p, this);
382                    return Task.CompletedTask;
383                }
384
385                public override Task WaitForExitAsync(LauncherBase p) => Task.FromException(InvalidOperation("wait for exit"));
386            }
387
388            private class StartingState : State
389            {
390                public Task EnterFromAsync(LauncherBase p, State fromState)
391                {
392                    if (!TryEnter(p, fromState))
393                    {
394                        // Delegate StartAsync to current state, because it has already changed since
395                        // transition to this state was initiated.
396                        return p._currentState.StartAsync(p);
397                    }
398
399                    return StartCoreAsync(p);
400                }
401
402                public override Task StartAsync(LauncherBase p) => p._startCompletionSource.Task;
403
404                public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => Exiting.EnterFromAsync(p, this, timeout);
405
406                public override Task KillAsync(LauncherBase p) => Killing.EnterFromAsync(p, this);
407
408                public override void Dispose(LauncherBase p)
409                {
410                    p._startCompletionSource.TrySetException(new ObjectDisposedException(p.ToString()));
411                    base.Dispose(p);
412                }
413
414                private async Task StartCoreAsync(LauncherBase p)
415                {
416                    var output = new StringBuilder();
417
418                    void OnProcessDataReceivedWhileStarting(object sender, DataReceivedEventArgs e)
419                    {
420                        if (e.Data != null)
421                        {
422                            output.AppendLine(e.Data);
423                            var match = Regex.Match(e.Data, "^DevTools listening on (ws:\\/\\/.*)");
424                            if (match.Success)
425                            {
426                                p._startCompletionSource.TrySetResult(match.Groups[1].Value);
427                            }
428                        }
429                    }
430
431                    void OnProcessExitedWhileStarting(object sender, EventArgs e)
432                        => p._startCompletionSource.TrySetException(new ProcessException($"Failed to launch Base! {output}"));
433                    void OnProcessExited(object sender, EventArgs e) => Exited.EnterFrom(p, p._currentState);
434
435                    p.Process.ErrorDataReceived += OnProcessDataReceivedWhileStarting;
436                    p.Process.Exited += OnProcessExitedWhileStarting;
437                    p.Process.Exited += OnProcessExited;
438                    CancellationTokenSource cts = null;
439                    try
440                    {
441                        p.Process.Start();
442                        await Started.EnterFromAsync(p, this).ConfigureAwait(false);
443
444                        p.Process.BeginErrorReadLine();
445
446                        int timeout = p._options.Timeout;
447                        if (timeout > 0)
448                        {
449                            cts = new CancellationTokenSource(timeout);
450                            cts.Token.Register(() => p._startCompletionSource.TrySetException(
451                                new ProcessException($"Timed out after {timeout} ms while trying to connect to Base!")));
452                        }
453
454                        try
455                        {
456                            await p._startCompletionSource.Task.ConfigureAwait(false);
457                            await Started.EnterFromAsync(p, this).ConfigureAwait(false);
458                        }
459                        catch
460                        {
461                            await Killing.EnterFromAsync(p, this).ConfigureAwait(false);
462                            throw;
463                        }
464                    }
465                    finally
466                    {
467                        cts?.Dispose();
468                        p.Process.Exited -= OnProcessExitedWhileStarting;
469                        p.Process.ErrorDataReceived -= OnProcessDataReceivedWhileStarting;
470                    }
471                }
472            }
473
474            private class StartedState : State
475            {
476                public Task EnterFromAsync(LauncherBase p, State fromState)
477                {
478                    if (TryEnter(p, fromState))
479                    {
480                    }
481
482                    return Task.CompletedTask;
483                }
484
485                protected override void Leave(LauncherBase p) { }
486
487                public override Task StartAsync(LauncherBase p) => Task.CompletedTask;
488
489                public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => Exiting.EnterFromAsync(p, this, timeout);
490
491                public override Task KillAsync(LauncherBase p) => Killing.EnterFromAsync(p, this);
492            }
493
494            private class ExitingState : State
495            {
496                public Task EnterFromAsync(LauncherBase p, State fromState, TimeSpan timeout)
497                    => !TryEnter(p, fromState) ? p._currentState.ExitAsync(p, timeout) : ExitAsync(p, timeout);
498
499                public override async Task ExitAsync(LauncherBase p, TimeSpan timeout)
500                {
501                    var waitForExitTask = WaitForExitAsync(p);
502                    await waitForExitTask.WithTimeout(
503                        async () =>
504                        {
505                            await Killing.EnterFromAsync(p, this).ConfigureAwait(false);
506                            await waitForExitTask.ConfigureAwait(false);
507                        },
508                        timeout,
509                        CancellationToken.None).ConfigureAwait(false);
510                }
511
512                public override Task KillAsync(LauncherBase p) => Killing.EnterFromAsync(p, this);
513            }
514
515            private class KillingState : State
516            {
517                public async Task EnterFromAsync(LauncherBase p, State fromState)
518                {
519                    if (!TryEnter(p, fromState))
520                    {
521                        // Delegate KillAsync to current state, because it has already changed since
522                        // transition to this state was initiated.
523                        await p._currentState.KillAsync(p).ConfigureAwait(false);
524                    }
525
526                    try
527                    {
528                        if (!p.Process.HasExited)
529                        {
530                            p.Process.Kill();
531                        }
532                    }
533                    catch (InvalidOperationException)
534                    {
535                        // Ignore
536                        return;
537                    }
538
539                    await WaitForExitAsync(p).ConfigureAwait(false);
540                }
541
542                public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => WaitForExitAsync(p);
543
544                public override Task KillAsync(LauncherBase p) => WaitForExitAsync(p);
545            }
546
547            private class ExitedState : State
548            {
549                public void EnterFrom(LauncherBase p, State fromState)
550                {
551                    while (!TryEnter(p, fromState))
552                    {
553                        // Current state has changed since transition to this state was requested.
554                        // Therefore retry transition to this state from the current state. This ensures
555                        // that Leave() operation of current state is properly called.
556                        fromState = p._currentState;
557                        if (fromState == this)
558                        {
559                            return;
560                        }
561                    }
562
563                    p._exitCompletionSource.TrySetResult(true);
564                    p.TempUserDataDir?.Dispose();
565                }
566
567                public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => Task.CompletedTask;
568
569                public override Task KillAsync(LauncherBase p) => Task.CompletedTask;
570
571                public override Task WaitForExitAsync(LauncherBase p) => Task.CompletedTask;
572            }
573
574            private class DisposedState : State
575            {
576                public void EnterFrom(LauncherBase p, State fromState)
577                {
578                    if (!TryEnter(p, fromState))
579                    {
580                        // Delegate Dispose to current state, because it has already changed since
581                        // transition to this state was initiated.
582                        p._currentState.Dispose(p);
583                    }
584                    else if (fromState != Exited)
585                    {
586                        Kill(p);
587
588                        p._exitCompletionSource.TrySetException(new ObjectDisposedException(p.ToString()));
589                        p.TempUserDataDir?.Dispose();
590                    }
591                }
592
593                public override Task StartAsync(LauncherBase p) => throw new ObjectDisposedException(p.ToString());
594
595                public override Task ExitAsync(LauncherBase p, TimeSpan timeout) => throw new ObjectDisposedException(p.ToString());
596
597                public override Task KillAsync(LauncherBase p) => throw new ObjectDisposedException(p.ToString());
598
599                public override void Dispose(LauncherBase p)
600                {
601                    // Nothing to do
602                }
603            }
604
605            #endregion
606        }
607
608        #endregion
609    }
610}
611
Full Screen

ChromiumProcess.cs

Source: ChromiumProcess.cs Github

copy
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Diagnostics;
5using System.Linq;
6using System.Text;
7using System.Text.RegularExpressions;
8using System.Threading;
9using System.Threading.Tasks;
10using Microsoft.Extensions.Logging;
11using PuppeteerSharp.Helpers;
12
13namespace PuppeteerSharp
14{
15    /// <summary>
16    /// Represents a Chromium process and any associated temporary user data directory that have created
17    /// by Puppeteer and therefore must be cleaned up when no longer needed.
18    /// </summary>
19    public class ChromiumProcess : IDisposable
20    {
21        #region Constants
22
23        internal static readonly string[] DefaultArgs = {
24            "--disable-background-networking",
25            "--enable-features=NetworkService,NetworkServiceInProcess",
26            "--disable-background-timer-throttling",
27            "--disable-backgrounding-occluded-windows",
28            "--disable-breakpad",
29            "--disable-client-side-phishing-detection",
30            "--disable-component-extensions-with-background-pages",
31            "--disable-default-apps",
32            "--disable-dev-shm-usage",
33            "--disable-extensions",
34            "--disable-features=TranslateUI,BlinkGenPropertyTrees",
35            "--disable-hang-monitor",
36            "--disable-ipc-flooding-protection",
37            "--disable-popup-blocking",
38            "--disable-prompt-on-repost",
39            "--disable-renderer-backgrounding",
40            "--disable-sync",
41            "--force-color-profile=srgb",
42            "--metrics-recording-only",
43            "--no-first-run",
44            "--enable-automation",
45            "--password-store=basic",
46            "--use-mock-keychain"
47        };
48
49        private const string UserDataDirArgument = "--user-data-dir";
50
51        #endregion
52
53        #region Static fields
54
55        private static int _processCount;
56
57        #endregion
58
59        #region Instance fields
60
61        private readonly LaunchOptions _options;
62        private readonly TempDirectory _tempUserDataDir;
63        private readonly ILogger _logger;
64        private readonly TaskCompletionSource<string> _startCompletionSource = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
65        private readonly TaskCompletionSource<bool> _exitCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
66        private State _currentState = State.Initial;
67
68        #endregion
69
70        #region Constructor
71
72        /// <summary>
73        /// Creates a new <see cref="ChromiumProcess"/> instance.
74        /// </summary>
75        /// <param name="chromiumExecutable">Full path of Chromium executable.</param>
76        /// <param name="options">Options for launching Chromium.</param>
77        /// <param name="loggerFactory">Logger factory</param>
78        public ChromiumProcess(string chromiumExecutable, LaunchOptions options, ILoggerFactory loggerFactory)
79        {
80            _options = options;
81            _logger = options.LogProcess
82                ? loggerFactory.CreateLogger<ChromiumProcess>()
83                : null;
84
85            List<string> chromiumArgs;
86            (chromiumArgs, _tempUserDataDir) = PrepareChromiumArgs(options);
87
88            Process = new Process
89            {
90                EnableRaisingEvents = true
91            };
92            Process.StartInfo.UseShellExecute = false;
93            Process.StartInfo.FileName = chromiumExecutable;
94            Process.StartInfo.Arguments = string.Join(" ", chromiumArgs);
95            Process.StartInfo.RedirectStandardError = true;
96
97            SetEnvVariables(Process.StartInfo.Environment, options.Env, Environment.GetEnvironmentVariables());
98
99            if (options.DumpIO)
100            {
101                Process.ErrorDataReceived += (sender, e) => Console.Error.WriteLine(e.Data);
102            }
103        }
104
105        #endregion
106
107        #region Dispose
108
109        /// <summary>
110        /// Finalizer.
111        /// </summary>
112        ~ChromiumProcess()
113        {
114            Dispose(false);
115        }
116
117        /// <inheritdoc />
118        public void Dispose()
119        {
120            GC.SuppressFinalize(this);
121            Dispose(true);
122        }
123
124        /// <summary>
125        /// Disposes Chromium process and any temporary user directory.
126        /// </summary>
127        /// <param name="disposing">Indicates whether disposal was initiated by <see cref="Dispose()"/> operation.</param>
128        protected virtual void Dispose(bool disposing) => _currentState.Dispose(this);
129
130        #endregion
131
132        #region Properties
133
134        /// <summary>
135        /// Gets Chromium process details.
136        /// </summary>
137        public Process Process { get; }
138
139        /// <summary>
140        /// Gets Chromium endpoint.
141        /// </summary>
142        public string EndPoint => _startCompletionSource.Task.IsCompleted
143            ? _startCompletionSource.Task.Result
144            : null;
145
146        /// <summary>
147        /// Indicates whether Chromium process is exiting. 
148        /// </summary>
149        public bool IsExiting => _currentState.IsExiting;
150
151        /// <summary>
152        /// Indicates whether Chromium process has exited. 
153        /// </summary>
154        public bool HasExited => _currentState.IsExited;
155
156        #endregion
157
158        #region Public methods
159
160        /// <summary>
161        /// Asynchronously starts Chromium process.
162        /// </summary>
163        /// <returns></returns>
164        public Task StartAsync() => _currentState.StartAsync(this);
165
166        /// <summary>
167        /// Asynchronously waits for graceful Chromium process exit within a given timeout period.
168        /// Kills the Chromium process if it has not exited within this period.
169        /// </summary>
170        /// <param name="timeout">The maximum waiting time for a graceful process exit.</param>
171        /// <returns></returns>
172        public Task EnsureExitAsync(TimeSpan? timeout) => timeout.HasValue
173            ? _currentState.ExitAsync(this, timeout.Value)
174            : _currentState.KillAsync(this);
175
176        /// <summary>
177        /// Asynchronously kills Chromium process.
178        /// </summary>
179        /// <returns></returns>
180        public Task KillAsync() => _currentState.KillAsync(this);
181
182        /// <summary>
183        /// Waits for Chromium process exit within a given timeout.
184        /// </summary>
185        /// <param name="timeout">The maximum wait period.</param>
186        /// <returns><c>true</c> if Chromium process has exited within the given <paramref name="timeout"/>,
187        /// or <c>false</c> otherwise.</returns>
188        public async Task<bool> WaitForExitAsync(TimeSpan? timeout)
189        {
190            if (timeout.HasValue)
191            {
192                var taskCompleted = true;
193                await _exitCompletionSource.Task.WithTimeout(() =>
194                {
195                    taskCompleted = false;
196                }, timeout.Value).ConfigureAwait(false);
197                return taskCompleted;
198            }
199
200            await _exitCompletionSource.Task.ConfigureAwait(false);
201            return true;
202        }
203
204        /// <inheritdoc />
205        public override string ToString() => $"Chromium process; EndPoint={EndPoint}; State={_currentState}";
206
207        #endregion
208
209        #region Private methods
210
211        private static (List<string> chromiumArgs, TempDirectory tempUserDataDir) PrepareChromiumArgs(LaunchOptions options)
212        {
213
214            var chromiumArgs = new List<string>();
215
216            if (!options.IgnoreDefaultArgs)
217            {
218                chromiumArgs.AddRange(GetDefaultArgs(options));
219            }
220            else if (options.IgnoredDefaultArgs?.Length > 0)
221            {
222                chromiumArgs.AddRange(GetDefaultArgs(options).Except(options.IgnoredDefaultArgs));
223            }
224            else
225            {
226                chromiumArgs.AddRange(options.Args);
227            }
228
229            TempDirectory tempUserDataDir = null;
230
231            if (!chromiumArgs.Any(argument => argument.StartsWith("--remote-debugging-", StringComparison.Ordinal)))
232            {
233                chromiumArgs.Add("--remote-debugging-port=0");
234            }
235
236            var userDataDirOption = chromiumArgs.FirstOrDefault(i => i.StartsWith(UserDataDirArgument, StringComparison.Ordinal));
237            if (string.IsNullOrEmpty(userDataDirOption))
238            {
239                tempUserDataDir = new TempDirectory();
240                chromiumArgs.Add($"{UserDataDirArgument}={tempUserDataDir.Path.Quote()}");
241            }
242
243            return (chromiumArgs, tempUserDataDir);
244        }
245
246        internal static string[] GetDefaultArgs(LaunchOptions options)
247        {
248            var chromeArguments = new List<string>(DefaultArgs);
249
250            if (!string.IsNullOrEmpty(options.UserDataDir))
251            {
252                chromeArguments.Add($"{UserDataDirArgument}={options.UserDataDir.Quote()}");
253            }
254            if (options.Devtools)
255            {
256                chromeArguments.Add("--auto-open-devtools-for-tabs");
257            }
258            if (options.Headless)
259            {
260                chromeArguments.AddRange(new[]{
261                    "--headless",
262                    "--hide-scrollbars",
263                    "--mute-audio"
264                });
265            }
266
267            if (options.Args.All(arg => arg.StartsWith("-", StringComparison.Ordinal)))
268            {
269                chromeArguments.Add("about:blank");
270            }
271
272            chromeArguments.AddRange(options.Args);
273            return chromeArguments.ToArray();
274        }
275
276        private static void SetEnvVariables(IDictionary<string, string> environment, IDictionary<string, string> customEnv, IDictionary realEnv)
277        {
278            foreach (DictionaryEntry item in realEnv)
279            {
280                environment[item.Key.ToString()] = item.Value.ToString();
281            }
282
283            if (customEnv != null)
284            {
285                foreach (var item in customEnv)
286                {
287                    environment[item.Key] = item.Value;
288                }
289            }
290        }
291
292        #endregion
293
294        #region State machine
295
296        /// <summary>
297        /// Represents state machine for Chromium process instances. The happy path runs along the
298        /// following state transitions: <see cref="Initial"/>
299        /// -> <see cref="Starting"/>
300        /// -> <see cref="Started"/>
301        /// -> <see cref="Exiting"/>
302        /// -> <see cref="Exited"/>.
303        /// -> <see cref="Disposed"/>.
304        /// </summary>
305        /// <remarks>
306        /// <para>
307        /// This state machine implements the following state transitions:
308        /// <code>
309        /// State     Event              Target State Action
310        /// ======== =================== ============ ==========================================================
311        /// Initial  --StartAsync------> Starting     Start process and wait for endpoint
312        /// Initial  --ExitAsync-------> Exited       Cleanup temp user data
313        /// Initial  --KillAsync-------> Exited       Cleanup temp user data
314        /// Initial  --Dispose---------> Disposed     Cleanup temp user data
315        /// Starting --StartAsync------> Starting     -
316        /// Starting --ExitAsync-------> Exiting      Wait for process exit
317        /// Starting --KillAsync-------> Killing      Kill process
318        /// Starting --Dispose---------> Disposed     Kill process; Cleanup temp user data;  throw ObjectDisposedException on outstanding async operations;
319        /// Starting --endpoint ready--> Started      Complete StartAsync successfully; Log process start
320        /// Starting --process exit----> Exited       Complete StartAsync with exception; Cleanup temp user data
321        /// Started  --StartAsync------> Started      -
322        /// Started  --EnsureExitAsync-> Exiting      Start exit timer; Log process exit
323        /// Started  --KillAsync-------> Killing      Kill process; Log process exit
324        /// Started  --Dispose---------> Disposed     Kill process; Log process exit; Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
325        /// Started  --process exit----> Exited       Log process exit; Cleanup temp user data
326        /// Exiting  --StartAsync------> Exiting      - (StartAsync throws InvalidOperationException)
327        /// Exiting  --ExitAsync-------> Exiting      -
328        /// Exiting  --KillAsync-------> Killing      Kill process
329        /// Exiting  --Dispose---------> Disposed     Kill process; Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
330        /// Exiting  --exit timeout----> Killing      Kill process
331        /// Exiting  --process exit----> Exited       Cleanup temp user data; complete outstanding async operations;
332        /// Killing  --StartAsync------> Killing      - (StartAsync throws InvalidOperationException)
333        /// Killing  --KillAsync-------> Killing      -
334        /// Killing  --Dispose---------> Disposed     Cleanup temp user data; throw ObjectDisposedException on outstanding async operations;
335        /// Killing  --process exit----> Exited       Cleanup temp user data; complete outstanding async operations;
336        /// Exited   --StartAsync------> Killing      - (StartAsync throws InvalidOperationException)
337        /// Exited   --KillAsync-------> Exited       -
338        /// Exited   --Dispose---------> Disposed     -
339        /// Disposed --StartAsync------> Disposed     -
340        /// Disposed --KillAsync-------> Disposed     -
341        /// Disposed --Dispose---------> Disposed     -
342        /// </code>
343        /// </para>
344        /// </remarks>
345        private abstract class State
346        {
347            #region Predefined states
348
349            public static readonly State Initial = new InitialState();
350            private static readonly StartingState Starting = new StartingState();
351            private static readonly StartedState Started = new StartedState();
352            private static readonly ExitingState Exiting = new ExitingState();
353            private static readonly KillingState Killing = new KillingState();
354            private static readonly ExitedState Exited = new ExitedState();
355            private static readonly DisposedState Disposed = new DisposedState();
356
357            #endregion
358
359            #region Properties
360
361            public bool IsExiting => this == Killing || this == Exiting;
362            public bool IsExited => this == Exited || this == Disposed;
363
364            #endregion
365
366            #region Methods
367
368            /// <summary>
369            /// Attempts thread-safe transitions from a given state to this state.
370            /// </summary>
371            /// <param name="p">The Chromium process</param>
372            /// <param name="fromState">The state from which state transition takes place</param>
373            /// <returns>Returns <c>true</c> if transition is successful, or <c>false</c> if transition
374            /// cannot be made because current state does not equal <paramref name="fromState"/>.</returns>
375            protected bool TryEnter(ChromiumProcess p, State fromState)
376            {
377                if (Interlocked.CompareExchange(ref p._currentState, this, fromState) == fromState)
378                {
379                    fromState.Leave(p);
380                    return true;
381                }
382
383                return false;
384            }
385
386            /// <summary>
387            /// Notifies that state machine is about to transition to another state.
388            /// </summary>
389            /// <param name="p">The Chromium process</param>
390            protected virtual void Leave(ChromiumProcess p)
391            { }
392
393            /// <summary>
394            /// Handles process start request.
395            /// </summary>
396            /// <param name="p">The Chromium process</param>
397            /// <returns></returns>
398            public virtual Task StartAsync(ChromiumProcess p) => Task.FromException(InvalidOperation("start"));
399
400            /// <summary>
401            /// Handles process exit request.
402            /// </summary>
403            /// <param name="p">The Chromium process</param>
404            /// <param name="timeout">The maximum waiting time for a graceful process exit.</param>
405            /// <returns></returns>
406            public virtual Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => Task.FromException(InvalidOperation("exit"));
407
408            /// <summary>
409            /// Handles process kill request.
410            /// </summary>
411            /// <param name="p">The Chromium process</param>
412            /// <returns></returns>
413            public virtual Task KillAsync(ChromiumProcess p) => Task.FromException(InvalidOperation("kill"));
414
415            /// <summary>
416            /// Handles wait for process exit request.
417            /// </summary>
418            /// <param name="p">The Chromium process</param>
419            /// <returns></returns>
420            public virtual Task WaitForExitAsync(ChromiumProcess p) => p._exitCompletionSource.Task;
421
422            /// <summary>
423            /// Handles disposal of process and temporary user directory
424            /// </summary>
425            /// <param name="p"></param>
426            public virtual void Dispose(ChromiumProcess p) => Disposed.EnterFrom(p, this);
427
428            public override string ToString()
429            {
430                var name = GetType().Name;
431                return name.Substring(0, name.Length - "State".Length);
432            }
433
434            private Exception InvalidOperation(string operationName)
435                => new InvalidOperationException($"Cannot {operationName} in state {this}");
436
437            /// <summary>
438            /// Kills process if it is still alive.
439            /// </summary>
440            /// <param name="p"></param>
441            private static void Kill(ChromiumProcess p)
442            {
443                try
444                {
445                    if (!p.Process.HasExited)
446                    {
447                        p.Process.Kill();
448                    }
449                }
450                catch (InvalidOperationException)
451                {
452                    // Ignore
453                }
454            }
455
456            #endregion
457
458            #region Concrete state classes
459
460            private class InitialState : State
461            {
462                public override Task StartAsync(ChromiumProcess p) => Starting.EnterFromAsync(p, this);
463
464                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout)
465                {
466                    Exited.EnterFrom(p, this);
467                    return Task.CompletedTask;
468                }
469
470                public override Task KillAsync(ChromiumProcess p)
471                {
472                    Exited.EnterFrom(p, this);
473                    return Task.CompletedTask;
474                }
475
476                public override Task WaitForExitAsync(ChromiumProcess p) => Task.FromException(InvalidOperation("wait for exit"));
477            }
478
479            private class StartingState : State
480            {
481                public Task EnterFromAsync(ChromiumProcess p, State fromState)
482                {
483                    if (!TryEnter(p, fromState))
484                    {
485                        // Delegate StartAsync to current state, because it has already changed since
486                        // transition to this state was initiated.
487                        return p._currentState.StartAsync(p);
488                    }
489
490                    return StartCoreAsync(p);
491                }
492
493                public override Task StartAsync(ChromiumProcess p) => p._startCompletionSource.Task;
494
495                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => Exiting.EnterFromAsync(p, this, timeout);
496
497                public override Task KillAsync(ChromiumProcess p) => Killing.EnterFromAsync(p, this);
498
499                public override void Dispose(ChromiumProcess p)
500                {
501                    p._startCompletionSource.TrySetException(new ObjectDisposedException(p.ToString()));
502                    base.Dispose(p);
503                }
504
505                private async Task StartCoreAsync(ChromiumProcess p)
506                {
507                    var output = new StringBuilder();
508
509                    void OnProcessDataReceivedWhileStarting(object sender, DataReceivedEventArgs e)
510                    {
511                        if (e.Data != null)
512                        {
513                            output.AppendLine(e.Data);
514                            var match = Regex.Match(e.Data, "^DevTools listening on (ws:\\/\\/.*)");
515                            if (match.Success)
516                            {
517                                p._startCompletionSource.TrySetResult(match.Groups[1].Value);
518                            }
519                        }
520                    }
521                    void OnProcessExitedWhileStarting(object sender, EventArgs e)
522                        => p._startCompletionSource.TrySetException(new ChromiumProcessException($"Failed to launch Chromium! {output}"));
523                    void OnProcessExited(object sender, EventArgs e) => Exited.EnterFrom(p, p._currentState);
524
525                    p.Process.ErrorDataReceived += OnProcessDataReceivedWhileStarting;
526                    p.Process.Exited += OnProcessExitedWhileStarting;
527                    p.Process.Exited += OnProcessExited;
528                    CancellationTokenSource cts = null;
529                    try
530                    {
531                        p.Process.Start();
532                        await Started.EnterFromAsync(p, this).ConfigureAwait(false);
533
534                        p.Process.BeginErrorReadLine();
535
536                        var timeout = p._options.Timeout;
537                        if (timeout > 0)
538                        {
539                            cts = new CancellationTokenSource(timeout);
540                            cts.Token.Register(() => p._startCompletionSource.TrySetException(
541                                new ChromiumProcessException($"Timed out after {timeout} ms while trying to connect to Chromium!")));
542                        }
543
544                        try
545                        {
546                            await p._startCompletionSource.Task.ConfigureAwait(false);
547                            await Started.EnterFromAsync(p, this).ConfigureAwait(false);
548                        }
549                        catch
550                        {
551                            await Killing.EnterFromAsync(p, this).ConfigureAwait(false);
552                            throw;
553                        }
554                    }
555                    finally
556                    {
557                        cts?.Dispose();
558                        p.Process.Exited -= OnProcessExitedWhileStarting;
559                        p.Process.ErrorDataReceived -= OnProcessDataReceivedWhileStarting;
560                    }
561                }
562            }
563
564            private class StartedState : State
565            {
566                public Task EnterFromAsync(ChromiumProcess p, State fromState)
567                {
568                    if (TryEnter(p, fromState))
569                    {
570                        // Process has not exited or been killed since transition to this state was initiated
571                        LogProcessCount(p, Interlocked.Increment(ref _processCount));
572                    }
573                    return Task.CompletedTask;
574                }
575
576                protected override void Leave(ChromiumProcess p)
577                    => LogProcessCount(p, Interlocked.Decrement(ref _processCount));
578
579                public override Task StartAsync(ChromiumProcess p) => Task.CompletedTask;
580
581                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => Exiting.EnterFromAsync(p, this, timeout);
582
583                public override Task KillAsync(ChromiumProcess p) => Killing.EnterFromAsync(p, this);
584
585                private static void LogProcessCount(ChromiumProcess p, int processCount)
586                {
587                    try
588                    {
589                        p._logger?.LogInformation("Process Count: {ProcessCount}", processCount);
590                    }
591                    catch
592                    {
593                        // Prevent logging exception from causing havoc
594                    }
595                }
596            }
597
598            private class ExitingState : State
599            {
600                public Task EnterFromAsync(ChromiumProcess p, State fromState, TimeSpan timeout)
601                    => !TryEnter(p, fromState) ? p._currentState.ExitAsync(p, timeout) : ExitAsync(p, timeout);
602
603                public override async Task ExitAsync(ChromiumProcess p, TimeSpan timeout)
604                {
605                    var waitForExitTask = WaitForExitAsync(p);
606                    await waitForExitTask.WithTimeout(async () =>
607                    {
608                        await Killing.EnterFromAsync(p, this).ConfigureAwait(false);
609                        await waitForExitTask.ConfigureAwait(false);
610                    }, timeout).ConfigureAwait(false);
611                }
612
613                public override Task KillAsync(ChromiumProcess p) => Killing.EnterFromAsync(p, this);
614            }
615
616            private class KillingState : State
617            {
618                public async Task EnterFromAsync(ChromiumProcess p, State fromState)
619                {
620                    if (!TryEnter(p, fromState))
621                    {
622                        // Delegate KillAsync to current state, because it has already changed since
623                        // transition to this state was initiated.
624                        await p._currentState.KillAsync(p).ConfigureAwait(false);
625                    }
626
627                    try
628                    {
629                        if (!p.Process.HasExited)
630                        {
631                            p.Process.Kill();
632                        }
633                    }
634                    catch (InvalidOperationException)
635                    {
636                        // Ignore
637                        return;
638                    }
639
640                    await WaitForExitAsync(p).ConfigureAwait(false);
641                }
642
643                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => WaitForExitAsync(p);
644
645                public override Task KillAsync(ChromiumProcess p) => WaitForExitAsync(p);
646            }
647
648            private class ExitedState : State
649            {
650                public void EnterFrom(ChromiumProcess p, State fromState)
651                {
652                    while (!TryEnter(p, fromState))
653                    {
654                        // Current state has changed since transition to this state was requested.
655                        // Therefore retry transition to this state from the current state. This ensures
656                        // that Leave() operation of current state is properly called.
657                        fromState = p._currentState;
658                        if (fromState == this)
659                        {
660                            return;
661                        }
662                    }
663
664                    p._exitCompletionSource.TrySetResult(true);
665                    p._tempUserDataDir?.Dispose();
666                }
667
668                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => Task.CompletedTask;
669
670                public override Task KillAsync(ChromiumProcess p) => Task.CompletedTask;
671
672                public override Task WaitForExitAsync(ChromiumProcess p) => Task.CompletedTask;
673            }
674
675            private class DisposedState : State
676            {
677                public void EnterFrom(ChromiumProcess p, State fromState)
678                {
679                    if (!TryEnter(p, fromState))
680                    {
681                        // Delegate Dispose to current state, because it has already changed since
682                        // transition to this state was initiated.
683                        p._currentState.Dispose(p);
684                    }
685                    else if (fromState != Exited)
686                    {
687                        Kill(p);
688
689                        p._exitCompletionSource.TrySetException(new ObjectDisposedException(p.ToString()));
690                        p._tempUserDataDir?.Dispose();
691                    }
692                }
693
694                public override Task StartAsync(ChromiumProcess p) => throw new ObjectDisposedException(p.ToString());
695
696                public override Task ExitAsync(ChromiumProcess p, TimeSpan timeout) => throw new ObjectDisposedException(p.ToString());
697
698                public override Task KillAsync(ChromiumProcess p) => throw new ObjectDisposedException(p.ToString());
699
700                public override void Dispose(ChromiumProcess p)
701                {
702                    // Nothing to do
703                }
704            }
705
706            #endregion
707        }
708
709        #endregion
710    }
711}
712
Full Screen

Accelerate Your Automation Test Cycles With LambdaTest

Leverage LambdaTest’s cloud-based platform to execute your automation tests in parallel and trim down your test execution time significantly. Your first 100 automation testing minutes are on us.

Try LambdaTest

Trigger ExitAsync code on LambdaTest Cloud Grid

Execute automation tests with ExitAsync on a cloud-based Grid of 3000+ real browsers and operating systems for both web and mobile applications.

Test now for Free
LambdaTestX

We use cookies to give you the best experience. Cookies help to provide a more personalized experience and relevant advertising for you, and web analytics for us. Learn More in our Cookies policy, Privacy & Terms of service

Allow Cookie
Sarah

I hope you find the best code examples for your project.

If you want to accelerate automated browser testing, try LambdaTest. Your first 100 automation testing minutes are FREE.

Sarah Elson (Product & Growth Lead)