e483bf73e7
Co-authored-by: Copilot <copilot@github.com>
194 lines
6.1 KiB
C#
194 lines
6.1 KiB
C#
using Microsoft.Playwright;
|
|
|
|
const string defaultSelector = "button:has-text('Click to')";
|
|
|
|
var targetUrl = PromptForTargetUrl();
|
|
var options = StressOptions.Parse(args, defaultSelector, targetUrl);
|
|
|
|
Console.WriteLine($"URL: {options.TargetUrl}");
|
|
Console.WriteLine($"Instances: {options.InstanceCount}");
|
|
Console.WriteLine($"Interval: {options.IntervalMs}ms");
|
|
Console.WriteLine($"Selector: {options.ButtonSelector}");
|
|
Console.WriteLine($"Headless: {options.Headless}");
|
|
Console.WriteLine("Press Ctrl+C to stop.");
|
|
|
|
using var cts = new CancellationTokenSource();
|
|
Console.CancelKeyPress += (_, eventArgs) =>
|
|
{
|
|
eventArgs.Cancel = true;
|
|
cts.Cancel();
|
|
};
|
|
|
|
var workers = Enumerable.Range(1, options.InstanceCount)
|
|
.Select(instanceId => RunWorkerAsync(instanceId, options, cts.Token))
|
|
.ToArray();
|
|
|
|
await Task.WhenAll(workers);
|
|
|
|
static string PromptForTargetUrl()
|
|
{
|
|
while (true)
|
|
{
|
|
Console.Write("Enter target URL: ");
|
|
var input = Console.ReadLine();
|
|
|
|
if (!string.IsNullOrWhiteSpace(input)
|
|
&& Uri.TryCreate(input, UriKind.Absolute, out var uri)
|
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
|
{
|
|
return uri.ToString();
|
|
}
|
|
|
|
Console.WriteLine("Invalid URL. Please enter a full http/https URL.");
|
|
}
|
|
}
|
|
|
|
static async Task RunWorkerAsync(int instanceId, StressOptions options, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
using var playwright = await Playwright.CreateAsync();
|
|
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
|
|
{
|
|
Headless = options.Headless
|
|
});
|
|
|
|
var page = await browser.NewPageAsync();
|
|
await page.GotoAsync(options.TargetUrl, new PageGotoOptions
|
|
{
|
|
WaitUntil = WaitUntilState.DOMContentLoaded,
|
|
Timeout = 30000
|
|
});
|
|
|
|
Console.WriteLine($"[{instanceId}] connected");
|
|
var clickCount = 0;
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
// HTMX can replace the button after each request, so resolve it fresh every loop.
|
|
var button = page.Locator(options.ButtonSelector).First;
|
|
await button.WaitForAsync(new LocatorWaitForOptions
|
|
{
|
|
State = WaitForSelectorState.Visible,
|
|
Timeout = 5000
|
|
});
|
|
await button.ClickAsync(new LocatorClickOptions
|
|
{
|
|
Timeout = 5000,
|
|
Force = true
|
|
});
|
|
|
|
clickCount += 1;
|
|
if (clickCount % 25 == 0)
|
|
{
|
|
Console.WriteLine($"[{instanceId}] clicks sent: {clickCount}");
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
Console.WriteLine($"[{instanceId}] click timeout");
|
|
}
|
|
catch (PlaywrightException ex)
|
|
{
|
|
Console.WriteLine($"[{instanceId}] click error: {ex.Message}");
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.Delay(options.IntervalMs, cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
await page.CloseAsync();
|
|
Console.WriteLine($"[{instanceId}] stopped");
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Console.WriteLine($"[{instanceId}] canceled");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[{instanceId}] fatal: {ex.Message}");
|
|
if (ex.Message.Contains("Executable doesn't exist", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine("Install browser binaries first:");
|
|
Console.WriteLine("./bin/Debug/net10.0/.playwright/node/linux-x64/node ./bin/Debug/net10.0/.playwright/package/cli.js install chromium");
|
|
}
|
|
}
|
|
}
|
|
|
|
internal sealed record StressOptions(
|
|
string TargetUrl,
|
|
int InstanceCount,
|
|
int IntervalMs,
|
|
string ButtonSelector,
|
|
bool Headless)
|
|
{
|
|
public static StressOptions Parse(string[] args, string defaultSelector, string targetUrl)
|
|
{
|
|
var instanceCount = ParsePositiveInt(Environment.GetEnvironmentVariable("INSTANCE_COUNT"), 20);
|
|
var intervalMs = ParsePositiveInt(Environment.GetEnvironmentVariable("CLICK_INTERVAL_MS"), 200);
|
|
var buttonSelector = Environment.GetEnvironmentVariable("BUTTON_SELECTOR") ?? defaultSelector;
|
|
var headless = ParseBool(Environment.GetEnvironmentVariable("HEADLESS"), true);
|
|
|
|
foreach (var arg in args)
|
|
{
|
|
var split = arg.Split('=', 2, StringSplitOptions.TrimEntries);
|
|
if (split.Length != 2)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var key = split[0].TrimStart('-', '/').ToLowerInvariant();
|
|
var value = split[1];
|
|
|
|
switch (key)
|
|
{
|
|
case "instances":
|
|
case "instancecount":
|
|
instanceCount = ParsePositiveInt(value, instanceCount);
|
|
break;
|
|
case "interval":
|
|
case "intervalms":
|
|
intervalMs = ParsePositiveInt(value, intervalMs);
|
|
break;
|
|
case "selector":
|
|
case "buttonselector":
|
|
buttonSelector = value;
|
|
break;
|
|
case "headless":
|
|
headless = ParseBool(value, headless);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new StressOptions(targetUrl, instanceCount, intervalMs, buttonSelector, headless);
|
|
}
|
|
|
|
private static int ParsePositiveInt(string? rawValue, int fallback)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(rawValue) && int.TryParse(rawValue, out var parsed) && parsed > 0)
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
private static bool ParseBool(string? rawValue, bool fallback)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(rawValue) && bool.TryParse(rawValue, out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
}
|