From b949dcaf4e16f0c60fdffbca91fbde1f452233e1 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:33:13 +0300 Subject: [PATCH 1/5] [dotnet] Possibility to output internal log messages to file (#13249) * INitial implementation * With context * Update HttpCommandExecutor.cs * Nullable handlers * Don't capture logger * Log message issuer * Simplify things * Continue * Update nunit adapter to work with dotnet 7 and be more friendly with windows * Rename to LogEventLevel * Typo * Introduce LogContextManager * Typo again * Rename to Timestamp * Make ILogger as static field * Support hierarchical contexts * Rename to EmitMessage * Do not emit message to parent context * Deep copy of loggers and handlers per context * Make fields private * Static works with current log context * Create context with minimum level * Set minimum level for context * Rename to WithHandler * Set minimum level per issuer * Simplify getting internal logger * Use DateTimeOffset * Docs for log event level * Docs for ILogger * Docs for Logger * Docs for others * Make ILogger interface as internal * Revert "Make ILogger interface as internal" This reverts commit 3cf6e489dc5aaaa7dc78a54d22d97230ff6b0b11. * First test * Update LogTest.cs * Fix build error * Info minimum log level by default * Remove unnecessary log call in ChromeDriver * Adjust log levels in console output * Make it length fixed * Make webdriver assembly internals visible to tests * Make ILogger hidden from user * More tests for log context * Init * Rename back to AddHandler * Make format script happy? * Make format script happy? * Rename back to SetLevel * Console handler by default * Output logs to stderr * New api to mange log handlers * Use logging in DriverFactory * Revert "Use logging in DriverFactory" This reverts commit e3255a6217851882ef772165fb01bff1df8c244f. * Verbose driver creation in tests * Search driver type in loaded assemblies * Decalare internals visible to in csproj to not conflict with bazel * Clean specific assembly name for driver type * Old school using to make bazel happy * Fix targeting packs for test targets * It works * Small clean up * Lock in ctor * Dispose at process exit * Remove redundant Clone for log handlers * Dispose log handlers when context finishes * Lock writing to the disk globally * Fix new list of log handlers for context * Don't lock in ctor * Add docs * Change format of datetime in file log * Thread safe disposing * Add finilizer * Docs for finilizer * Add tests * Recreating missing dirs --- .../Internal/Logging/ConsoleLogHandler.cs | 9 -- .../Internal/Logging/FileLogHandler.cs | 94 +++++++++++++++++++ .../webdriver/Internal/Logging/ILogHandler.cs | 6 -- .../webdriver/Internal/Logging/LogContext.cs | 26 +++-- .../Internal/Logging/FileLogHandlerTest.cs | 43 +++++++++ 5 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs create mode 100644 dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs diff --git a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs index 83759ac09dee6..30f63dd625673 100644 --- a/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs +++ b/dotnet/src/webdriver/Internal/Logging/ConsoleLogHandler.cs @@ -36,14 +36,5 @@ public void Handle(LogEvent logEvent) { Console.Error.WriteLine($"{logEvent.Timestamp:HH:mm:ss.fff} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}"); } - - /// - /// Creates a new instance of the class. - /// - /// A new instance of the class. - public ILogHandler Clone() - { - return this; - } } } diff --git a/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs new file mode 100644 index 0000000000000..915ea43870863 --- /dev/null +++ b/dotnet/src/webdriver/Internal/Logging/FileLogHandler.cs @@ -0,0 +1,94 @@ +using System; +using System.IO; + +namespace OpenQA.Selenium.Internal.Logging +{ + /// + /// Represents a log handler that writes log events to a file. + /// + public class FileLogHandler : ILogHandler, IDisposable + { + // performance trick to avoid expensive Enum.ToString() with fixed length + private static readonly string[] _levels = { "TRACE", "DEBUG", " INFO", " WARN", "ERROR" }; + + private FileStream _fileStream; + private StreamWriter _streamWriter; + + private readonly object _lockObj = new object(); + private bool _isDisposed; + + /// + /// Initializes a new instance of the class with the specified file path. + /// + /// The path of the log file. + public FileLogHandler(string path) + { + if (string.IsNullOrEmpty(path)) throw new ArgumentException("File log path cannot be null or empty.", nameof(path)); + + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + _fileStream = File.Open(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + _fileStream.Seek(0, SeekOrigin.End); + _streamWriter = new StreamWriter(_fileStream, System.Text.Encoding.UTF8) + { + AutoFlush = true + }; + } + + /// + /// Handles a log event by writing it to the log file. + /// + /// The log event to handle. + public void Handle(LogEvent logEvent) + { + lock (_lockObj) + { + _streamWriter.WriteLine($"{logEvent.Timestamp:r} {_levels[(int)logEvent.Level]} {logEvent.IssuedBy.Name}: {logEvent.Message}"); + } + } + + /// + /// Disposes the file log handler and releases any resources used. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Finalizes the file log handler instance. + /// + ~FileLogHandler() + { + Dispose(false); + } + + /// + /// Disposes the file log handler and releases any resources used. + /// + /// A flag indicating whether to dispose managed resources. + protected virtual void Dispose(bool disposing) + { + lock (_lockObj) + { + if (!_isDisposed) + { + if (disposing) + { + _streamWriter?.Dispose(); + _streamWriter = null; + _fileStream?.Dispose(); + _fileStream = null; + } + + _isDisposed = true; + } + } + } + } +} diff --git a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs index 44c3ae67f6377..9c0365e0881e4 100644 --- a/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs +++ b/dotnet/src/webdriver/Internal/Logging/ILogHandler.cs @@ -28,11 +28,5 @@ public interface ILogHandler /// /// The log event to handle. void Handle(LogEvent logEvent); - - /// - /// Creates a clone of the log handler. - /// - /// A clone of the log handler. - ILogHandler Clone(); } } diff --git a/dotnet/src/webdriver/Internal/Logging/LogContext.cs b/dotnet/src/webdriver/Internal/Logging/LogContext.cs index 6b19059b5cd5a..57713c23edc53 100644 --- a/dotnet/src/webdriver/Internal/Logging/LogContext.cs +++ b/dotnet/src/webdriver/Internal/Logging/LogContext.cs @@ -60,19 +60,16 @@ public ILogContext CreateContext(LogEventLevel minimumLevel) loggers = new ConcurrentDictionary(_loggers.Select(l => new KeyValuePair(l.Key, new Logger(l.Value.Issuer, minimumLevel)))); } - IList handlers = null; + var context = new LogContext(minimumLevel, this, loggers, null); if (Handlers != null) { - handlers = new List(Handlers.Select(h => h.Clone())); - } - else - { - handlers = new List(); + foreach (var handler in Handlers) + { + context.Handlers.Add(handler); + } } - var context = new LogContext(minimumLevel, this, loggers, Handlers); - Log.CurrentContext = context; return context; @@ -137,6 +134,19 @@ public ILogContext SetLevel(Type issuer, LogEventLevel level) public void Dispose() { + // Dispose log handlers associated with this context + // if they are hot handled by parent context + if (Handlers != null && _parentLogContext != null && _parentLogContext.Handlers != null) + { + foreach (var logHandler in Handlers) + { + if (!_parentLogContext.Handlers.Contains(logHandler)) + { + (logHandler as IDisposable)?.Dispose(); + } + } + } + Log.CurrentContext = _parentLogContext; } } diff --git a/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs new file mode 100644 index 0000000000000..072582723b154 --- /dev/null +++ b/dotnet/test/common/Internal/Logging/FileLogHandlerTest.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using System; +using System.IO; + +namespace OpenQA.Selenium.Internal.Logging +{ + public class FileLogHandlerTest + { + [Test] + [TestCase(null)] + [TestCase("")] + public void ShouldNotAcceptIncorrectPath(string path) + { + var act = () => new FileLogHandler(path); + + Assert.That(act, Throws.ArgumentException); + } + + [Test] + public void ShouldHandleLogEvent() + { + var tempFile = Path.GetTempFileName(); + + try + { + using (var fileLogHandler = new FileLogHandler(tempFile)) + { + fileLogHandler.Handle(new LogEvent(typeof(FileLogHandlerTest), DateTimeOffset.Now, LogEventLevel.Info, "test message")); + } + + Assert.That(File.ReadAllText(tempFile), Does.Contain("test message")); + } + catch (Exception) + { + throw; + } + finally + { + File.Delete(tempFile); + } + } + } +} From 8e75d5dfc42be1ebfc9ed30d0e3103efeed1f969 Mon Sep 17 00:00:00 2001 From: Nikolay Borisenko <22616990+nvborisenko@users.noreply.github.com> Date: Thu, 7 Dec 2023 23:36:57 +0300 Subject: [PATCH 2/5] [dotnet] Change a list of downloadable files to IReadOnlyList (#13265) --- dotnet/src/webdriver/IHasDownloads.cs | 4 ++-- dotnet/src/webdriver/Remote/RemoteWebDriver.cs | 6 +++--- dotnet/test/common/DownloadsTest.cs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dotnet/src/webdriver/IHasDownloads.cs b/dotnet/src/webdriver/IHasDownloads.cs index 19a2f9982dfb6..e857621495462 100644 --- a/dotnet/src/webdriver/IHasDownloads.cs +++ b/dotnet/src/webdriver/IHasDownloads.cs @@ -28,8 +28,8 @@ public interface IHasDownloads /// /// Retrieves the downloadable files. /// - /// A list of file names available for download. - List GetDownloadableFiles(); + /// A read-only list of file names available for download. + IReadOnlyList GetDownloadableFiles(); /// /// Downloads a file with the specified file name and returns a dictionary containing the downloaded file's data. diff --git a/dotnet/src/webdriver/Remote/RemoteWebDriver.cs b/dotnet/src/webdriver/Remote/RemoteWebDriver.cs index cc8ae20bdaca7..35d0b62ebf3cd 100644 --- a/dotnet/src/webdriver/Remote/RemoteWebDriver.cs +++ b/dotnet/src/webdriver/Remote/RemoteWebDriver.cs @@ -471,10 +471,10 @@ public DevToolsSession GetDevToolsSession(int protocolVersion) } /// - /// Retrieves the downloadable files as a map of file names and their corresponding URLs. + /// Retrieves the downloadable files. /// - /// A list containing file names as keys and URLs as values. - public List GetDownloadableFiles() + /// A read-only list of file names available for download. + public IReadOnlyList GetDownloadableFiles() { var enableDownloads = this.Capabilities.GetCapability(CapabilityType.EnableDownloads); if (enableDownloads == null || !(bool) enableDownloads) { diff --git a/dotnet/test/common/DownloadsTest.cs b/dotnet/test/common/DownloadsTest.cs index 5acecdd7d434e..2597e0a65e3fe 100644 --- a/dotnet/test/common/DownloadsTest.cs +++ b/dotnet/test/common/DownloadsTest.cs @@ -41,7 +41,7 @@ public void CanListDownloadableFiles() { DownloadWithBrowser(); - List names = ((RemoteWebDriver) driver).GetDownloadableFiles(); + IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles(); Assert.That(names, Contains.Item("file_1.txt")); Assert.That(names, Contains.Item("file_2.jpg")); } @@ -52,7 +52,7 @@ public void CanDownloadFile() { DownloadWithBrowser(); - List names = ((RemoteWebDriver) driver).GetDownloadableFiles(); + IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles(); string fileName = names[0]; string targetDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); @@ -72,7 +72,7 @@ public void CanDeleteFiles() ((RemoteWebDriver)driver).DeleteDownloadableFiles(); - List names = ((RemoteWebDriver) driver).GetDownloadableFiles(); + IReadOnlyList names = ((RemoteWebDriver) driver).GetDownloadableFiles(); Assert.IsEmpty(names, "The names list should be empty."); } From 7647b5f343330b24bb29092bbbd07cb964014ea3 Mon Sep 17 00:00:00 2001 From: titusfortner Date: Thu, 7 Dec 2023 15:32:43 -0600 Subject: [PATCH 3/5] [build] create prepare_release task that builds packages for all languages --- Rakefile | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 182d064b25478..073a8e1984b7f 100644 --- a/Rakefile +++ b/Rakefile @@ -133,7 +133,7 @@ task all: [ :"selenium-java", '//java/test/org/openqa/selenium/environment:webserver' ] -task all_zip: [:'java-release-zip'] +task all_zip: [:'java-release-zip', :'dotnet-release-zip'] task tests: [ '//java/test/org/openqa/selenium/htmlunit:htmlunit', '//java/test/org/openqa/selenium/firefox:test-synthesized', @@ -403,6 +403,26 @@ def read_m2_user_pass return [user, pass] end +task :prepare_release do + RELEASE_TARGETS = [ + '//java/src/org/openqa/selenium:client-zip', + '//java/src/org/openqa/selenium/grid:server-zip', + '//java/src/org/openqa/selenium/grid:executable-grid', + '//dotnet/src/webdriver:webdriver-pack', + '//dotnet/src/webdriver:webdriver-strongnamed-pack', + '//dotnet/src/support:support-pack', + '//dotnet/src/support:support-strongnamed-pack', + '//javascript/node/selenium-webdriver:selenium-webdriver', + '//py:selenium-wheel', + '//py:selenium-sdist', + ] + + RELEASE_TARGETS.each do |target| + Bazel::execute('build', ['--config', 'release'], target) + end + Bazel::execute('build', ['--stamp'], '//rb:selenium-webdriver') +end + task 'publish-maven': JAVA_RELEASE_TARGETS do creds = read_m2_user_pass JAVA_RELEASE_TARGETS.each do |p| From 150625680769af147f0d69e7dc06bf1dd0010280 Mon Sep 17 00:00:00 2001 From: titusfortner Date: Thu, 7 Dec 2023 15:58:24 -0600 Subject: [PATCH 4/5] [build] create publish-nuget task --- Rakefile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Rakefile b/Rakefile index 073a8e1984b7f..7fb18c0ed65a7 100644 --- a/Rakefile +++ b/Rakefile @@ -430,6 +430,17 @@ task 'publish-maven': JAVA_RELEASE_TARGETS do end end +NUGET_RELEASE_ASSETS = [ + "./bazel-bin/dotnet/src/webdriver/Selenium.WebDriver.#{dotnet_version}.nupkg", + "./bazel-bin/dotnet/src/webdriver/Selenium.Support.#{dotnet_version}.nupkg" +] + +task 'publish-nuget' do + NUGET_RELEASE_ASSETS.each do |asset| + sh "dotnet nuget push #{asset} --api-key #{ENV[:NUGET_API_KEY]} --source https://api.nuget.org/v3/index.json" + end +end + task 'publish-maven-snapshot': JAVA_RELEASE_TARGETS do creds = read_m2_user_pass if java_version.end_with?('-SNAPSHOT') From 6620bce4e8e9da1fee3ec5a5547afa7dece3f80e Mon Sep 17 00:00:00 2001 From: titusfortner Date: Thu, 7 Dec 2023 16:09:07 -0600 Subject: [PATCH 5/5] [build] create publish-pypi task --- Rakefile | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Rakefile b/Rakefile index 7fb18c0ed65a7..0867c2c1de1cb 100644 --- a/Rakefile +++ b/Rakefile @@ -62,6 +62,12 @@ def dotnet_version end end +def python_version + File.foreach('py/BUILD.bazel') do |line| + return line.split('=').last.strip.tr('"', '') if line.include?('SE_VERSION') + end +end + # The build system used by webdriver is layered on top of rake, and we call it # "crazy fun" for no readily apparent reason. @@ -423,6 +429,17 @@ task :prepare_release do Bazel::execute('build', ['--stamp'], '//rb:selenium-webdriver') end +PYPI_ASSETS = [ + "bazel-bin/py/selenium-#{python_version}-py3-none-any.whl", + "bazel-bin/py/selenium-#{python_version}.tar.gz" +] + +task 'publish-pypi' do + PYPI_ASSETS.each do |asset| + sh "python3 -m twine upload #{asset}" + end +end + task 'publish-maven': JAVA_RELEASE_TARGETS do creds = read_m2_user_pass JAVA_RELEASE_TARGETS.each do |p|