diff --git a/src/libs/SoloX.CodeQuality.Playwright/IPlaywrightTest.cs b/src/libs/SoloX.CodeQuality.Playwright/IPlaywrightTest.cs
index e7406f5..64b7f4c 100644
--- a/src/libs/SoloX.CodeQuality.Playwright/IPlaywrightTest.cs
+++ b/src/libs/SoloX.CodeQuality.Playwright/IPlaywrightTest.cs
@@ -24,5 +24,12 @@ public interface IPlaywrightTest : IAsyncDisposable
/// Page setup handler to initialize page environment before it is display (or null).
/// async task.
Task GotoPageAsync(string relativePath, Func testHandler, string? traceName = null, Func? pageSetupHandler = null);
+
+ ///
+ /// Get Url used to bind Playwright and host to test.
+ ///
+#pragma warning disable CA1056 // URI-like properties should not be strings
+ string Url { get; }
+#pragma warning restore CA1056 // URI-like properties should not be strings
}
}
diff --git a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs
index d8f07da..dd6444d 100644
--- a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs
+++ b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightDriver.cs
@@ -20,7 +20,8 @@ public class PlaywrightDriver : IAsyncDisposable
private static readonly string[] INSTALL_ARGUMENTS = new[] { "install" };
- private static readonly object InstallLock = new object();
+ private static readonly object LockSync = new object();
+ private static bool playwrightInstalled;
private readonly BrowserNewContextOptions? browserNewContextOptions;
private readonly TracingStartOptions? tracingStartOptions;
@@ -86,31 +87,78 @@ public async Task InitializeAsync(BrowserTypeLaunchOptions? browserTypeLaunchOpt
///
private static void InstallPlaywright()
{
- InstallPlaywrightDeps();
- InstallPlaywrightBin();
+ lock (LockSync)
+ {
+ if (playwrightInstalled)
+ {
+ return;
+ }
+
+ var localApplicationData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PlaywrightDriver");
+
+ if (!Directory.Exists(localApplicationData))
+ {
+ Directory.CreateDirectory(localApplicationData);
+ }
+
+ var lockFile = Path.Combine(localApplicationData, "PlaywrightInstallLock");
+
+ var timeout = 60;
+
+ while (!TryWriteLockFile(lockFile))
+ {
+ Thread.Sleep(1000);
+ timeout--;
+ if (timeout < 0)
+ {
+ throw new PlaywrightException($"Unable to lock for Playwright install (Timeout)");
+ }
+ }
+
+ try
+ {
+ InstallPlaywrightDeps();
+ InstallPlaywrightBin();
+ }
+ finally
+ {
+ playwrightInstalled = true;
+ File.Delete(lockFile);
+ }
+ }
+ }
+
+ private static bool TryWriteLockFile(string lockFile)
+ {
+ try
+ {
+ using var file = new FileStream(lockFile, FileMode.CreateNew, FileAccess.Write);
+ file.WriteByte(0);
+ file.Flush();
+ file.Close();
+ }
+ catch (IOException)
+ {
+ return false;
+ }
+ return true;
}
private static void InstallPlaywrightDeps()
{
- lock (InstallLock)
+ var exitCode = Microsoft.Playwright.Program.Main(INSTALL_DEPS_ARGUMENTS);
+ if (exitCode != 0)
{
- var exitCode = Microsoft.Playwright.Program.Main(INSTALL_DEPS_ARGUMENTS);
- if (exitCode != 0)
- {
- throw new PlaywrightException($"Playwright exited with code {exitCode} on install-deps");
- }
+ throw new PlaywrightException($"Playwright exited with code {exitCode} on install-deps");
}
}
private static void InstallPlaywrightBin()
{
- lock (InstallLock)
+ var exitCode = Microsoft.Playwright.Program.Main(INSTALL_ARGUMENTS);
+ if (exitCode != 0)
{
- var exitCode2 = Microsoft.Playwright.Program.Main(INSTALL_ARGUMENTS);
- if (exitCode2 != 0)
- {
- throw new PlaywrightException($"Playwright exited with code {exitCode2} on install");
- }
+ throw new PlaywrightException($"Playwright exited with code {exitCode} on install");
}
}
@@ -140,6 +188,9 @@ public async Task GotoPageAsync(string url, Func testHandler, Brows
catch (PlaywrightException)
{
retry--;
+
+ await Task.Delay(1000).ConfigureAwait(false);
+
if (retry == 0)
{
throw;
diff --git a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs
index 68ac04d..de7365d 100644
--- a/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs
+++ b/src/libs/SoloX.CodeQuality.Playwright/PlaywrightTestBuilder.cs
@@ -254,6 +254,8 @@ private sealed class PlaywrightTest : IPlaywrightTest
private bool isDisposed;
+ public string Url => this.url;
+
internal PlaywrightTest(
Browser browser,
string url,
@@ -358,28 +360,6 @@ private sealed class PortStore
{
private readonly HashSet usedPorts = [];
- ///
- /// Setup port store and prob all TCP port already in use.
- ///
- public PortStore()
- {
- // Get used port with Netstat like command.
- var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
- var tcpListeners = ipGlobalProperties.GetActiveTcpListeners();
-
- foreach (var tcpEndPoint in tcpListeners)
- {
- this.usedPorts.Add(tcpEndPoint.Port);
- }
-
- var tcpConnections = ipGlobalProperties.GetActiveTcpConnections();
-
- foreach (var tcpConnection in tcpConnections)
- {
- this.usedPorts.Add(tcpConnection.LocalEndPoint.Port);
- }
- }
-
public int GetPort(PortRange portRange)
{
#pragma warning disable CA5394 // Do not use insecure randomness
@@ -388,7 +368,11 @@ public int GetPort(PortRange portRange)
lock (this.usedPorts)
{
- while (!this.usedPorts.Add(port))
+ var systemPort = ProbUsedPorts();
+
+ var allUsedPorts = new HashSet(systemPort.Union(this.usedPorts));
+
+ while (allUsedPorts.Contains(port))
{
port++;
if (port >= portRange.EndPort)
@@ -396,6 +380,8 @@ public int GetPort(PortRange portRange)
port = portRange.StartPort;
}
}
+
+ this.usedPorts.Add(port);
}
return port;
}
@@ -407,6 +393,29 @@ public void Release(int port)
this.usedPorts.Remove(port);
}
}
+
+ public static HashSet ProbUsedPorts()
+ {
+ HashSet usedSystemPorts = [];
+
+ // Get used port with Netstat like command.
+ var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
+ var tcpListeners = ipGlobalProperties.GetActiveTcpListeners();
+
+ foreach (var tcpEndPoint in tcpListeners)
+ {
+ usedSystemPorts.Add(tcpEndPoint.Port);
+ }
+
+ var tcpConnections = ipGlobalProperties.GetActiveTcpConnections();
+
+ foreach (var tcpConnection in tcpConnections)
+ {
+ usedSystemPorts.Add(tcpConnection.LocalEndPoint.Port);
+ }
+
+ return usedSystemPorts;
+ }
}
}
}
diff --git a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs
index c7c88c7..d8b70bc 100644
--- a/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs
+++ b/src/tests/SoloX.CodeQuality.Playwright.E2ETest/Resources/PackageNugetTestProject/PackageNugetTestProject/PlaywrightTestBuilderLocalTest.cs
@@ -15,7 +15,7 @@ public PlaywrightTestBuilderLocalTest()
.WithLocalHost(localHostBuilder =>
{
localHostBuilder
- .UsePortRange(new PortRange(5000, 6000))
+ .UsePortRange(new PortRange(6000, 7000))
.UseWebHostWithWwwRoot(path, "home.html")
.UseWebHostBuilder(builder =>
{