diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml index ac5bc8aa..dcd630c1 100644 --- a/Lite/Controls/ServerTab.xaml +++ b/Lite/Controls/ServerTab.xaml @@ -1203,11 +1203,16 @@ + - + diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 3f0ca82e..bcefae42 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -105,6 +105,7 @@ public partial class ServerTab : UserControl }; public int UtcOffsetMinutes { get; } + private readonly bool _hasMsdbAccess; /// /// Raised after each data refresh with alert counts for tab badge display. @@ -113,7 +114,7 @@ public partial class ServerTab : UserControl public event Action? ApplyTimeRangeRequested; /* selectedIndex */ public event Func? ManualRefreshRequested; - public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialService credentialService, int utcOffsetMinutes = 0) + public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialService credentialService, int utcOffsetMinutes = 0, bool hasMsdbAccess = true) { InitializeComponent(); @@ -122,6 +123,7 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe _serverId = RemoteCollectorService.GetDeterministicHashCode(RemoteCollectorService.GetServerNameForStorage(server)); _credentialService = credentialService; UtcOffsetMinutes = utcOffsetMinutes; + _hasMsdbAccess = hasMsdbAccess; ServerTimeHelper.UtcOffsetMinutes = utcOffsetMinutes; ServerNameText.Text = server.ReadOnlyIntent ? $"{server.DisplayName} (Read-Only)" : server.DisplayName; @@ -158,6 +160,12 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe }; _refreshTimer.Start(); + /* Show warning on Running Jobs tab if login lacks msdb access */ + if (!_hasMsdbAccess) + { + RunningJobsMsdbWarning.Visibility = System.Windows.Visibility.Visible; + } + /* Initialize time picker ComboBoxes */ InitializeTimeComboBoxes(); diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index 270fcd0a..860b835a 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -507,7 +507,7 @@ private async void ConnectToServer(ServerConnection server) } var utcOffset = status.UtcOffsetMinutes ?? 0; - var serverTab = new ServerTab(server, _databaseInitializer, _serverManager.CredentialService, utcOffset); + var serverTab = new ServerTab(server, _databaseInitializer, _serverManager.CredentialService, utcOffset, status.HasMsdbAccess); var tabHeader = CreateTabHeader(server); var tabItem = new TabItem { diff --git a/Lite/Models/ServerConnectionStatus.cs b/Lite/Models/ServerConnectionStatus.cs index 3bb56012..b786f70a 100644 --- a/Lite/Models/ServerConnectionStatus.cs +++ b/Lite/Models/ServerConnectionStatus.cs @@ -81,6 +81,12 @@ public class ServerConnectionStatus /// public bool IsAwsRds { get; set; } + /// + /// Whether the connected login has access to msdb. + /// Used for gating collectors that query msdb system tables (e.g., running jobs). + /// + public bool HasMsdbAccess { get; set; } = true; + /// /// The server's UTC offset in minutes, queried via DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()). /// Used to convert UTC collection_time values to server-local time for display. diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs index 9e307177..bd2f3c95 100644 --- a/Lite/Services/RemoteCollectorService.cs +++ b/Lite/Services/RemoteCollectorService.cs @@ -317,8 +317,9 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam var majorVersion = serverStatus.SqlMajorVersion; var engineEdition = serverStatus.SqlEngineEdition; var isAwsRds = serverStatus.IsAwsRds; + var hasMsdbAccess = serverStatus.HasMsdbAccess; - if (!IsCollectorSupported(collectorName, majorVersion, engineEdition, isAwsRds)) + if (!IsCollectorSupported(collectorName, majorVersion, engineEdition, isAwsRds, hasMsdbAccess)) { AppLogger.Info("Collector", $" [{server.DisplayName}] {collectorName} SKIPPED (version {majorVersion}, edition {engineEdition})"); return; @@ -740,7 +741,7 @@ internal static int GetDeterministicHashCode(string value) /// Version 13 = SQL Server 2016, 14 = 2017, 15 = 2019, 16 = 2022, 17 = 2025. /// Engine edition 5 = Azure SQL DB, 8 = Azure MI. /// - private static bool IsCollectorSupported(string collectorName, int majorVersion, int engineEdition, bool isAwsRds = false) + private static bool IsCollectorSupported(string collectorName, int majorVersion, int engineEdition, bool isAwsRds = false, bool hasMsdbAccess = true) { bool isAzureSqlDb = engineEdition == 5; bool isAzureMi = engineEdition == 8; @@ -781,6 +782,16 @@ private static bool IsCollectorSupported(string collectorName, int majorVersion, } } + /* msdb access gate — login may not have access to msdb on any edition */ + if (!hasMsdbAccess) + { + switch (collectorName) + { + case "running_jobs": /* requires msdb.dbo.sysjobs, sysjobactivity, etc. */ + return false; + } + } + return true; } } diff --git a/Lite/Services/ServerManager.cs b/Lite/Services/ServerManager.cs index 167d9576..70daa653 100644 --- a/Lite/Services/ServerManager.cs +++ b/Lite/Services/ServerManager.cs @@ -347,7 +347,8 @@ public async Task CheckConnectionAsync(string serverId, CONVERT(integer, SERVERPROPERTY('ProductMajorVersion')) AS major_version, DATEDIFF(MINUTE, GETUTCDATE(), GETDATE()) AS utc_offset_minutes, CONVERT(integer, SERVERPROPERTY('EngineEdition')) AS engine_edition, - CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds + CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds, + HAS_DBACCESS(N'msdb') AS has_msdb_access FROM sys.dm_os_sys_info", connection); command.CommandTimeout = ConnectionCheckTimeoutSeconds; @@ -366,6 +367,8 @@ CASE WHEN DB_ID('rdsadmin') IS NOT NULL THEN 1 ELSE 0 END AS is_aws_rds status.SqlEngineEdition = Convert.ToInt32(reader.GetValue(4)); if (!reader.IsDBNull(5)) status.IsAwsRds = Convert.ToInt32(reader.GetValue(5)) == 1; + if (!reader.IsDBNull(6)) + status.HasMsdbAccess = Convert.ToInt32(reader.GetValue(6)) == 1; } } catch (SqlException metaEx)