From 3ac687c258025b58301259c3fd74cbddbc7ff33f Mon Sep 17 00:00:00 2001 From: "victor.delarosa" <1938685+vdlr@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:19:38 +0100 Subject: [PATCH 01/20] fix play button xygeni/product-backlog#4192 --- UI/Control/XygeniExplorerControl.xaml | 2 +- UI/Control/XygeniExplorerControl.xaml.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/UI/Control/XygeniExplorerControl.xaml b/UI/Control/XygeniExplorerControl.xaml index e2b49da..db73ab6 100644 --- a/UI/Control/XygeniExplorerControl.xaml +++ b/UI/Control/XygeniExplorerControl.xaml @@ -74,7 +74,7 @@ diff --git a/UI/Control/XygeniExplorerControl.xaml.cs b/UI/Control/XygeniExplorerControl.xaml.cs index 106a703..398694e 100644 --- a/UI/Control/XygeniExplorerControl.xaml.cs +++ b/UI/Control/XygeniExplorerControl.xaml.cs @@ -12,6 +12,7 @@ using vs2026_plugin.Models; using vs2026_plugin.Commands; using System.Reflection; +using System.Windows.Media.Imaging; namespace vs2026_plugin.UI.Control @@ -46,6 +47,7 @@ public partial class XygeniExplorerControl : UserControl public XygeniExplorerControl() { InitializeComponent(); + SetRunButtonIcon(); var issueService = XygeniIssueService.GetInstance(); var scannerService = XygeniScannerService.GetInstance(); @@ -59,6 +61,17 @@ public XygeniExplorerControl() } + private void SetRunButtonIcon() + { + string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string iconPath = Path.Combine(baseDir, "media", "icons", "play.png"); + + if (File.Exists(iconPath)) + { + RunScanIcon.Source = new BitmapImage(new Uri(iconPath, UriKind.Absolute)); + } + } + private async void OnIssuesChanged(object sender, EventArgs e) { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); From 7c690b44990eeb80ae1aaabee25b3f533b59dac5 Mon Sep 17 00:00:00 2001 From: "victor.delarosa" <1938685+vdlr@users.noreply.github.com> Date: Wed, 18 Feb 2026 09:27:53 +0100 Subject: [PATCH 02/20] fix xygeni explorer tree refreshing xygeni/product-backlog#2192 --- Services/XygeniIssueService.cs | 31 +++++++++++++++++++++--- UI/Control/XygeniExplorerControl.xaml.cs | 19 +++++++++------ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Services/XygeniIssueService.cs b/Services/XygeniIssueService.cs index 14a9cb5..33ff2d2 100644 --- a/Services/XygeniIssueService.cs +++ b/Services/XygeniIssueService.cs @@ -19,20 +19,38 @@ public class XygeniIssueService { private static XygeniIssueService _instance; private readonly ILogger _logger; + private readonly XygeniScannerService _scannerService; private List _issues = new List(); private bool _isReadingIssues = false; + private bool _pendingReadIssues = false; public event EventHandler IssuesChanged; private XygeniIssueService(ILogger logger) { _logger = logger; - XygeniScannerService.GetInstance().Changed += (s, e) => + _scannerService = XygeniScannerService.GetInstance(); + _scannerService.Changed += (s, e) => { - ReadIssuesAsync(); + OnScannerChanged(); }; } + private void OnScannerChanged() + { + var latestScan = _scannerService.GetScans() + .OrderByDescending(s => s.Timestamp) + .FirstOrDefault(); + + // Refresh issues only when a scan has ended. A "running" update is emitted at scan start. + if (latestScan != null && string.Equals(latestScan.Status, "running", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + _ = ReadIssuesAsync(); + } + @@ -78,7 +96,8 @@ public async Task ReadIssuesAsync() string suffix = XygeniCommands.ReportSuffix; if (_isReadingIssues) { - _logger.Log(" Issues are already being read, skipping..."); + _pendingReadIssues = true; + _logger.Log(" Issues are already being read, scheduling a refresh..."); return; } _logger.Log(" Issues report directory: " + workingDir); @@ -100,6 +119,12 @@ public async Task ReadIssuesAsync() { _isReadingIssues = false; _logger.Log("=================================================="); + + if (_pendingReadIssues) + { + _pendingReadIssues = false; + _ = ReadIssuesAsync(); + } } } diff --git a/UI/Control/XygeniExplorerControl.xaml.cs b/UI/Control/XygeniExplorerControl.xaml.cs index 398694e..f652a23 100644 --- a/UI/Control/XygeniExplorerControl.xaml.cs +++ b/UI/Control/XygeniExplorerControl.xaml.cs @@ -40,7 +40,6 @@ public partial class XygeniExplorerControl : UserControl { private readonly XygeniIssueService _issueService; private readonly XygeniScannerService _scannerService; - private readonly XygeniInstallerService _installerService; private readonly XygeniExplorerViewModel _vm; @@ -49,16 +48,18 @@ public XygeniExplorerControl() InitializeComponent(); SetRunButtonIcon(); - var issueService = XygeniIssueService.GetInstance(); - var scannerService = XygeniScannerService.GetInstance(); + _issueService = XygeniIssueService.GetInstance(); + _scannerService = XygeniScannerService.GetInstance(); - _vm = new XygeniExplorerViewModel(issueService, scannerService); + _vm = new XygeniExplorerViewModel(_issueService, _scannerService); DataContext = _vm; _vm.IssueSelected += OnIssueSelected; - issueService.IssuesChanged += OnIssuesChanged; - + _issueService.IssuesChanged += OnIssuesChanged; + _scannerService.Changed += OnScannerChanged; + + _vm.Refresh(); } private void SetRunButtonIcon() @@ -78,7 +79,11 @@ private async void OnIssuesChanged(object sender, EventArgs e) _vm.Refresh(); } - + private async void OnScannerChanged(object sender, EventArgs e) + { + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + _vm.Refresh(); + } From 172a09b93c630c887db12396bf336342a3266655 Mon Sep 17 00:00:00 2001 From: "victor.delarosa" <1938685+vdlr@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:33:52 +0100 Subject: [PATCH 03/20] add proxy setting xygeni/product-backlog#4192 --- Models/ProxySettings.cs | 13 +++ Services/XygeniConfigurationService.cs | 73 ++++++++++++-- Services/XygeniInstallerService.cs | 81 ++++++++++++++-- Services/XygeniScannerService.cs | 51 +++++++++- UI/Control/XygeniConfigurationControl.xaml | 95 +++++++++++-------- UI/Control/XygeniConfigurationControl.xaml.cs | 43 ++++++++- 6 files changed, 301 insertions(+), 55 deletions(-) create mode 100644 Models/ProxySettings.cs diff --git a/Models/ProxySettings.cs b/Models/ProxySettings.cs new file mode 100644 index 0000000..34b503c --- /dev/null +++ b/Models/ProxySettings.cs @@ -0,0 +1,13 @@ +namespace vs2026_plugin.Models +{ + public class ProxySettings + { + public string Protocol { get; set; } + public string Host { get; set; } + public int? Port { get; set; } + public string Authentication { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string NonProxyHosts { get; set; } + } +} diff --git a/Services/XygeniConfigurationService.cs b/Services/XygeniConfigurationService.cs index 4b38b43..9b0121a 100644 --- a/Services/XygeniConfigurationService.cs +++ b/Services/XygeniConfigurationService.cs @@ -9,6 +9,7 @@ using EnvDTE80; using EnvDTE; using System.Collections.Generic; +using vs2026_plugin.Models; namespace vs2026_plugin.Services { @@ -17,6 +18,13 @@ public class XygeniConfigurationService private const string CollectionPath = "XygeniConfiguration"; private const string ApiUrlKey = "ApiUrl"; private const string TokenKey = "ApiToken"; + private const string ProxyProtocolKey = "ProxyProtocol"; + private const string ProxyHostKey = "ProxyHost"; + private const string ProxyPortKey = "ProxyPort"; + private const string ProxyAuthenticationKey = "ProxyAuthentication"; + private const string ProxyUsernameKey = "ProxyUsername"; + private const string ProxyPasswordKey = "ProxyPassword"; + private const string ProxyNonProxyHostsKey = "ProxyNonProxyHosts"; private const string MetadataFolderKey = ".xygenidata"; private readonly SettingsManager _settingsManager; @@ -77,22 +85,55 @@ public string GetToken() public void SaveUrl(string url) { - var store = _settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); - if (!store.CollectionExists(CollectionPath)) - { - store.CreateCollection(CollectionPath); - } + var store = GetWritableStore(); store.SetString(CollectionPath, ApiUrlKey, url); } public void SaveToken(string token) { - var store = _settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); + var store = GetWritableStore(); + store.SetString(CollectionPath, TokenKey, token); + } + + public ProxySettings GetProxySettings() + { + var store = _settingsManager.GetReadOnlySettingsStore(SettingsScope.UserSettings); if (!store.CollectionExists(CollectionPath)) { - store.CreateCollection(CollectionPath); + return new ProxySettings(); } - store.SetString(CollectionPath, TokenKey, token); + + var proxySettings = new ProxySettings + { + Protocol = store.GetString(CollectionPath, ProxyProtocolKey, string.Empty), + Host = store.GetString(CollectionPath, ProxyHostKey, string.Empty), + Authentication = store.GetString(CollectionPath, ProxyAuthenticationKey, string.Empty), + Username = store.GetString(CollectionPath, ProxyUsernameKey, string.Empty), + Password = store.GetString(CollectionPath, ProxyPasswordKey, string.Empty), + NonProxyHosts = store.GetString(CollectionPath, ProxyNonProxyHostsKey, string.Empty) + }; + + var proxyPort = store.GetString(CollectionPath, ProxyPortKey, string.Empty); + if (int.TryParse(proxyPort, out int parsedPort)) + { + proxySettings.Port = parsedPort; + } + + return proxySettings; + } + + public void SaveProxySettings(ProxySettings proxySettings) + { + var settings = proxySettings ?? new ProxySettings(); + var store = GetWritableStore(); + + store.SetString(CollectionPath, ProxyProtocolKey, NormalizeSetting(settings.Protocol)); + store.SetString(CollectionPath, ProxyHostKey, NormalizeSetting(settings.Host)); + store.SetString(CollectionPath, ProxyPortKey, settings.Port.HasValue ? settings.Port.Value.ToString() : string.Empty); + store.SetString(CollectionPath, ProxyAuthenticationKey, NormalizeSetting(settings.Authentication)); + store.SetString(CollectionPath, ProxyUsernameKey, NormalizeSetting(settings.Username)); + store.SetString(CollectionPath, ProxyPasswordKey, settings.Password ?? string.Empty); + store.SetString(CollectionPath, ProxyNonProxyHostsKey, NormalizeSetting(settings.NonProxyHosts)); } public void ClearCache() { @@ -360,6 +401,22 @@ private async Task IsSolutionWithProjectsAsync(Solution solution, Projects return !string.IsNullOrEmpty(solution.FullName) && !solution.IsDirty && projects.Count > 0; } + private WritableSettingsStore GetWritableStore() + { + var store = _settingsManager.GetWritableSettingsStore(SettingsScope.UserSettings); + if (!store.CollectionExists(CollectionPath)) + { + store.CreateCollection(CollectionPath); + } + + return store; + } + + private string NormalizeSetting(string value) + { + return string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim(); + } + diff --git a/Services/XygeniInstallerService.cs b/Services/XygeniInstallerService.cs index 889aa2a..f899710 100644 --- a/Services/XygeniInstallerService.cs +++ b/Services/XygeniInstallerService.cs @@ -2,11 +2,13 @@ using System.IO; using System.IO.Compression; using System.Net.Http; +using System.Net; using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using vs2026_plugin.Services; +using vs2026_plugin.Models; namespace vs2026_plugin.Services { @@ -15,7 +17,6 @@ public class XygeniInstallerService private static XygeniInstallerService _instance; private readonly string _extensionPath; private readonly ILogger _logger; - private readonly HttpClient _httpClient; private const string XygeniGetScannerUrl = "https://get.xygeni.io/latest/scanner/"; private const string XygeniScannerZipName = "xygeni_scanner.zip"; @@ -35,7 +36,6 @@ private XygeniInstallerService(string extensionPath, ILogger logger) { _extensionPath = extensionPath; _logger = logger; - _httpClient = new HttpClient(); CheckScannerInstallation(); } @@ -201,13 +201,14 @@ public async Task IsValidTokenAsync(string xygeniApiUrl, string xygeniToke string testApiUrl = $"{xygeniApiUrl.TrimEnd('/')}/language"; try { + using (var httpClient = CreateHttpClient()) using (var request = new HttpRequestMessage(HttpMethod.Get, testApiUrl)) { if (!string.IsNullOrEmpty(xygeniToken)) { request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {xygeniToken}"); } - var response = await _httpClient.SendAsync(request); + var response = await httpClient.SendAsync(request); return response.StatusCode == System.Net.HttpStatusCode.OK; } } @@ -223,8 +224,11 @@ public async Task IsValidApiUrlAsync(string xygeniApiUrl) string pingUrl = $"{xygeniApiUrl.TrimEnd('/')}/ping"; try { - var response = await _httpClient.GetAsync(pingUrl); - return response.StatusCode == System.Net.HttpStatusCode.OK; + using (var httpClient = CreateHttpClient()) + { + var response = await httpClient.GetAsync(pingUrl); + return response.StatusCode == System.Net.HttpStatusCode.OK; + } } catch (Exception ex) { @@ -235,7 +239,8 @@ public async Task IsValidApiUrlAsync(string xygeniApiUrl) private async Task DownloadFileAsync(string url, string destinationPath) { - using (var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) + using (var httpClient = CreateHttpClient()) + using (var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); using (var stream = await response.Content.ReadAsStreamAsync()) @@ -281,6 +286,70 @@ private async Task ReadAllTextAsync(string path) } } + private HttpClient CreateHttpClient() + { + var handler = new HttpClientHandler(); + var proxySettings = GetProxySettingsSafe(); + var webProxy = BuildWebProxy(proxySettings); + + if (webProxy != null) + { + handler.Proxy = webProxy; + handler.UseProxy = true; + } + else + { + handler.UseProxy = false; + } + + return new HttpClient(handler, disposeHandler: true); + } + + private ProxySettings GetProxySettingsSafe() + { + try + { + return XygeniConfigurationService.GetInstance().GetProxySettings(); + } + catch + { + return null; + } + } + + private IWebProxy BuildWebProxy(ProxySettings proxySettings) + { + if (proxySettings == null || string.IsNullOrWhiteSpace(proxySettings.Host)) + { + return null; + } + + string protocol = string.IsNullOrWhiteSpace(proxySettings.Protocol) ? "http" : proxySettings.Protocol.Trim(); + string host = proxySettings.Host.Trim(); + string proxyUri = proxySettings.Port.HasValue + ? $"{protocol}://{host}:{proxySettings.Port.Value}" + : $"{protocol}://{host}"; + + var webProxy = new WebProxy(proxyUri); + + if (!string.IsNullOrWhiteSpace(proxySettings.Username)) + { + webProxy.Credentials = new NetworkCredential(proxySettings.Username.Trim(), proxySettings.Password ?? string.Empty); + } + else if (string.Equals(proxySettings.Authentication, "default", StringComparison.OrdinalIgnoreCase)) + { + webProxy.Credentials = CredentialCache.DefaultCredentials; + } + + if (!string.IsNullOrWhiteSpace(proxySettings.NonProxyHosts)) + { + webProxy.BypassList = proxySettings.NonProxyHosts + .Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries); + } + + return webProxy; + } + private void OnChanged() { Changed?.Invoke(this, EventArgs.Empty); diff --git a/Services/XygeniScannerService.cs b/Services/XygeniScannerService.cs index c45b853..e979b05 100644 --- a/Services/XygeniScannerService.cs +++ b/Services/XygeniScannerService.cs @@ -9,6 +9,7 @@ using Microsoft.VisualStudio.Shell.Settings; using vs2026_plugin.Services; using vs2026_plugin.Commands; +using vs2026_plugin.Models; namespace vs2026_plugin.Services { @@ -292,7 +293,55 @@ private async Task GetEnvVariables(Dictionary env) env["XYGENI_TOKEN"] = token; } - // Note: Proxy settings are not yet implemented in C# side + ApplyProxyEnvironmentVariables(env, _configurationService.GetProxySettings()); + } + + private void ApplyProxyEnvironmentVariables(Dictionary env, ProxySettings proxySettings) + { + if (proxySettings == null || string.IsNullOrWhiteSpace(proxySettings.Host)) + { + return; + } + + string protocol = string.IsNullOrWhiteSpace(proxySettings.Protocol) ? "http" : proxySettings.Protocol.Trim(); + string host = proxySettings.Host.Trim(); + string portPart = proxySettings.Port.HasValue ? $":{proxySettings.Port.Value}" : string.Empty; + string credentials = string.Empty; + + if (!string.IsNullOrWhiteSpace(proxySettings.Username)) + { + string username = Uri.EscapeDataString(proxySettings.Username.Trim()); + string password = Uri.EscapeDataString(proxySettings.Password ?? string.Empty); + credentials = $"{username}:{password}@"; + } + + string proxyUrl = $"{protocol}://{credentials}{host}{portPart}"; + + env["HTTP_PROXY"] = proxyUrl; + env["HTTPS_PROXY"] = proxyUrl; + + if (!string.IsNullOrWhiteSpace(proxySettings.NonProxyHosts)) + { + env["NO_PROXY"] = proxySettings.NonProxyHosts.Trim(); + } + + SetEnvIfPresent(env, "PROXY_PROTOCOL", proxySettings.Protocol); + SetEnvIfPresent(env, "PROXY_HOST", proxySettings.Host); + SetEnvIfPresent(env, "PROXY_AUTH", proxySettings.Authentication); + SetEnvIfPresent(env, "PROXY_USERNAME", proxySettings.Username); + SetEnvIfPresent(env, "PROXY_PASSWORD", proxySettings.Password); + if (proxySettings.Port.HasValue) + { + env["PROXY_PORT"] = proxySettings.Port.Value.ToString(); + } + } + + private void SetEnvIfPresent(Dictionary env, string key, string value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + env[key] = value.Trim(); + } } private string GetScannerScriptPath(string xygeniScannerPath) diff --git a/UI/Control/XygeniConfigurationControl.xaml b/UI/Control/XygeniConfigurationControl.xaml index 99092a0..35c745b 100755 --- a/UI/Control/XygeniConfigurationControl.xaml +++ b/UI/Control/XygeniConfigurationControl.xaml @@ -5,10 +5,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vsshell="clr-namespace:Microsoft.VisualStudio.PlatformUI;assembly=Microsoft.VisualStudio.Shell.15.0" mc:Ignorable="d" - d:DesignHeight="300" d:DesignWidth="300"> - + d:DesignHeight="420" d:DesignWidth="350"> + - - - - - - - - - - - - - - + + + + - - + + - - - - - - -