diff --git a/DDMonorepo.sln b/DDMonorepo.sln index a0e54c47..084a7c6d 100644 --- a/DDMonorepo.sln +++ b/DDMonorepo.sln @@ -74,6 +74,8 @@ Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "DBCleaner", "DBCleaner\DBCl EndProject Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EDMIService.Tests", "EDMIService.Tests\EDMIService.Tests.vbproj", "{E3DA65CA-964D-4896-991A-B5426884E215}" EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "EDMI.API", "..\DDModules\EDMIAPI\EDMI.API.vbproj", "{25017513-0D97-49D3-98D7-BA76D9B251B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -180,6 +182,10 @@ Global {E3DA65CA-964D-4896-991A-B5426884E215}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3DA65CA-964D-4896-991A-B5426884E215}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3DA65CA-964D-4896-991A-B5426884E215}.Release|Any CPU.Build.0 = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25017513-0D97-49D3-98D7-BA76D9B251B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/GUIs.Test.TestGUI/App.config b/GUIs.Test.TestGUI/App.config index 22ec8bdd..7b1c622c 100644 --- a/GUIs.Test.TestGUI/App.config +++ b/GUIs.Test.TestGUI/App.config @@ -33,7 +33,7 @@ - + @@ -75,6 +75,14 @@ + + + + + + + + diff --git a/Service.EDMIService/EDMIService.vb b/Service.EDMIService/EDMIService.vb index 764852d2..b510a553 100644 --- a/Service.EDMIService/EDMIService.vb +++ b/Service.EDMIService/EDMIService.vb @@ -3,8 +3,7 @@ Imports System.ServiceModel Imports System.ServiceModel.Description Imports DigitalData.Modules.Database Imports DigitalData.Modules.Logging -Imports DigitalData.Modules.Language -Imports DigitalData.Modules +'Imports DigitalData.Modules. Imports DigitalData.Modules.EDMI Imports DigitalData.Modules.EDMI.API.Rights Imports DigitalData.Services.EDMIService.Messages @@ -13,6 +12,7 @@ Imports DigitalData.Services.EDMIService.Methods Imports DigitalData.Services.EDMIService.Methods.Database Imports DigitalData.Services.EDMIService.Methods.IDB + Public Class EDMIService Implements IEDMIService @@ -22,7 +22,6 @@ Public Class EDMIService Public Shared MSSQL_IDB As MSSQLServer Public Shared Firebird As Firebird Public Shared AppConfig As Config - Public Shared Filesystem As Filesystem.File Public Shared GlobalState As GlobalState Public Shared Scheduler As Scheduler @@ -586,7 +585,17 @@ Public Class EDMIService Dim oRow As DataRow = oRows.First() Dim oRightAsInt = oRow.Item("ACCESSRIGHT") - oRight = Utils.ToEnum(Of AccessRight)(oRightAsInt) + Select Case oRight + Case 1 + oRight = AccessRight.VIEW_ONLY + Case 2 + oRight = AccessRight.VIEW_EXPORT + Case 4 + oRight = AccessRight.FULL + Case Else + _Logger.Warn("GetAccessRightForObjectId: Unknown access right [{0}] for user [{1}] on object [{2}]", oRightAsInt, UserId, ObjectId) + oRight = AccessRight.VIEW_ONLY + End Select End If _Logger.Debug("Successfully parsed AccessRights for ObjectId [{0}]", ObjectId) diff --git a/Service.EDMIService/EDMIService.vbproj b/Service.EDMIService/EDMIService.vbproj index 1c749f52..2347a3b3 100644 --- a/Service.EDMIService/EDMIService.vbproj +++ b/Service.EDMIService/EDMIService.vbproj @@ -79,7 +79,7 @@ ..\..\DDModules\EDMIAPI\bin\Debug\DigitalData.Modules.EDMI.API.dll - P:\Projekte DIGITAL DATA\DIGITAL DATA - Entwicklung\DLL_Bibliotheken\Digital Data\DigitalData.Modules.Filesystem.dll + ..\..\DDModules\Filesystem\bin\Debug\DigitalData.Modules.Filesystem.dll P:\Projekte DIGITAL DATA\DIGITAL DATA - Entwicklung\DLL_Bibliotheken\Digital Data\DigitalData.Modules.Language.dll diff --git a/Service.EDMIService/GlobalState.vb b/Service.EDMIService/GlobalState.vb index 55a6c6b5..8dbf2988 100644 --- a/Service.EDMIService/GlobalState.vb +++ b/Service.EDMIService/GlobalState.vb @@ -1,7 +1,6 @@ Imports System.Data.SqlClient Imports System.Runtime.Serialization Imports DigitalData.Modules.Database -Imports DigitalData.Modules.Language Imports DigitalData.Modules.Logging Public Class GlobalState @@ -13,8 +12,9 @@ Public Class GlobalState Public Property ObjectStores As New List(Of ObjectStore) Public Property Connections As New List(Of DatabaseConnection) Public Property Doctypes As New List(Of Doctype) + Public Property ClientConfig As ClientConfiguration = New ClientConfiguration() - Public Property ClientConfig As New ClientConfiguration + Public Property JobListener As JobListener Public Property TableStore As New DataSet @@ -123,7 +123,7 @@ Public Class GlobalState Public Sub LoadConnections() _Logger.Info("Loading Database Connections") Try - Dim oSQL As String = "SELECT * FROM TBDD_CONNECTION" + Dim oSQL As String = "SELECT * FROM TBDD_CONNECTION WITH (NOLOCK)" Dim oTable As DataTable = _MSSQL_ECM.GetDatatable(oSQL) _Logger.Info("Found [{0}] Connections", oTable.Rows.Count) @@ -132,14 +132,14 @@ Public Class GlobalState For Each oRow As DataRow In oTable.Rows Dim oConnection As New DatabaseConnection() With { - .Id = oRow.ItemEx(Of Integer)("GUID"), - .Active = oRow.ItemEx(Of Boolean)("AKTIV"), - .Database = oRow.ItemEx(Of String)("DATENBANK"), - .Title = oRow.ItemEx(Of String)("BEZEICHNUNG"), - .Password = oRow.ItemEx(Of String)("PASSWORD"), - .Provider = oRow.ItemEx(Of String)("SQL_PROVIDER"), - .Server = oRow.ItemEx(Of String)("SERVER"), - .Username = oRow.ItemEx(Of String)("USERNAME") + .Id = IIf(IsDBNull(oRow.Item("GUID")), 0, oRow.Item("GUID")), + .Active = IIf(IsDBNull(oRow.Item("AKTIV")), False, oRow.Item("AKTIV")), + .Database = IIf(IsDBNull(oRow.Item("DATENBANK")), String.Empty, oRow.Item("DATENBANK")), + .Title = IIf(IsDBNull(oRow.Item("BEZEICHNUNG")), String.Empty, oRow.Item("BEZEICHNUNG")), + .Password = IIf(IsDBNull(oRow.Item("PASSWORD")), String.Empty, oRow.Item("PASSWORD")), + .Provider = IIf(IsDBNull(oRow.Item("SQL_PROVIDER")), String.Empty, oRow.Item("SQL_PROVIDER")), + .Server = IIf(IsDBNull(oRow.Item("SERVER")), String.Empty, oRow.Item("SERVER")), + .Username = IIf(IsDBNull(oRow.Item("USERNAME")), String.Empty, oRow.Item("USERNAME")) } _Logger.Info("New Connection [{0}]", oConnection.Title) oConnections.Add(oConnection) diff --git a/Service.EDMIService/IDB/Helpers.vb b/Service.EDMIService/IDB/Helpers.vb index 8cf93b57..90519ba0 100644 --- a/Service.EDMIService/IDB/Helpers.vb +++ b/Service.EDMIService/IDB/Helpers.vb @@ -1,6 +1,5 @@ Imports DigitalData.Modules.Database Imports DigitalData.Modules.Database.MSSQLServer -Imports DigitalData.Modules.Language Imports DigitalData.Modules.Logging Imports System.Data.SqlClient Imports DigitalData.Modules.ZooFlow.State @@ -59,8 +58,8 @@ Namespace IDB End If Dim oRow As DataRow = oTable.Rows.Item(0) - Dim oActive = Utils.NotNull(oRow.Item("ACTIVE"), False) - Dim oDeleted = Utils.NotNull(oRow.Item("DELETED"), False) + Dim oActive = IIf(IsDBNull(oRow.Item("ACTIVE")), False, oRow.Item("ACTIVE")) + Dim oDeleted = IIf(IsDBNull(oRow.Item("DELETED")), False, oRow.Item("DELETED")) IsDeleted = oDeleted IsActive = oActive diff --git a/Service.EDMIService/Methods/Database/GetDatatableFromCache/GetDatatableFromCacheMethod.vb b/Service.EDMIService/Methods/Database/GetDatatableFromCache/GetDatatableFromCacheMethod.vb index cd707632..eb88193b 100644 --- a/Service.EDMIService/Methods/Database/GetDatatableFromCache/GetDatatableFromCacheMethod.vb +++ b/Service.EDMIService/Methods/Database/GetDatatableFromCache/GetDatatableFromCacheMethod.vb @@ -14,49 +14,60 @@ Namespace Methods.Database.GetDatatableFromCache Try Logger.Debug($"ReturnDatatableFromCache: Datatable: {pData.DataTable}") - Dim oDataTable As DataTable = Nothing + ' ✅ NEU: Sicherheitsprüfung (sollte nie auftreten, aber defensiv) + If GlobalState.JobListener Is Nothing Then + Logger.Warn("JobListener not initialized - this should not happen! Using direct TableStore access.") - Logger.Debug("ReturnDatatableFromCache: DataSet contains [{0}] datatables", GlobalState.TableStore.Tables.Count) - - If GlobalState.TableStore.Tables.Contains(pData.DataTable) Then - oDataTable = GlobalState.TableStore.Tables.Item(pData.DataTable).Copy() - - ' Apply filter and sorting to data - Dim oFilterExpression As String = Utils.NotNull(pData.FilterExpression, String.Empty) - Dim oSortByColumn As String = Utils.NotNull(pData.SortByColumn, String.Empty) - - Logger.Debug("Applying filter expression: [{0}]", oFilterExpression) - Logger.Debug("Applying sort expression: [{0}]", oSortByColumn) - - Dim oFilteredRows = oDataTable.Select(oFilterExpression, oSortByColumn) - Dim oFilteredTable As DataTable = Nothing - - If oFilteredRows.Count > 0 Then - oFilteredTable = oFilteredRows.CopyToDataTable() - oFilteredTable.TableName = pData.DataTable - Else - ' Produce empty table - oFilteredTable = oDataTable.Clone() - oFilteredTable.TableName = pData.DataTable + ' Fallback: Direkter Zugriff auf TableStore + If Not GlobalState.TableStore.Tables.Contains(pData.DataTable) Then + Return New GetDatatableFromCacheResponse( + New KeyNotFoundException($"Datatable {pData.DataTable} does not exist")) End If - Logger.Debug("ReturnDatatableFromCache: Datatable Stats for [{0}]:", pData.DataTable) - Logger.Debug("Unfiltered: [{0}] rows", oDataTable.Rows.Count) - Logger.Debug("Filtered: [{0}] rows", oFilteredTable.Rows.Count) - - Return New GetDatatableFromCacheResponse(oFilteredTable) - Else - Logger.Warn($"ReturnDatatableFromCache: Datatable {pData.DataTable} does not exist") - - Return New GetDatatableFromCacheResponse(New KeyNotFoundException($"Datatable {pData.DataTable} does not exist")) + Dim oDirectTable = GlobalState.TableStore.Tables(pData.DataTable).Copy() + ' ... Filter/Sort anwenden wie gehabt + Return New GetDatatableFromCacheResponse(ApplyFilterAndSort(oDirectTable, pData)) End If + ' ✅ Normal: Thread-sicheren Zugriff verwenden + Dim oDataTable As DataTable = GlobalState.JobListener.GetTableSafely(pData.DataTable) + + If oDataTable Is Nothing Then + Logger.Warn($"Datatable {pData.DataTable} does not exist") + Return New GetDatatableFromCacheResponse( + New KeyNotFoundException($"Datatable {pData.DataTable} does not exist")) + End If + + Return New GetDatatableFromCacheResponse(ApplyFilterAndSort(oDataTable, pData)) + + Catch ex As Exception Logger.Error(ex) Return New GetDatatableFromCacheResponse(ex) End Try End Function + ' ✅ Hilfsmethode zum Filter/Sort anwenden + Private Function ApplyFilterAndSort(table As DataTable, request As GetDatatableFromCacheRequest) As DataTable + Dim oFilterExpression = IIf(IsDBNull(request.FilterExpression), String.Empty, request.FilterExpression) + Dim oSortByColumn = IIf(IsDBNull(request.SortByColumn), String.Empty, request.SortByColumn) + + Logger.Debug("Filter: [{0}], Sort: [{1}]", oFilterExpression, oSortByColumn) + + Dim oFilteredRows = table.Select(oFilterExpression, oSortByColumn) + Dim oResult As DataTable + + If oFilteredRows.Count > 0 Then + oResult = oFilteredRows.CopyToDataTable() + Else + oResult = table.Clone() + End If + + oResult.TableName = request.DataTable + Logger.Debug("Rows: {0} → {1}", table.Rows.Count, oResult.Rows.Count) + + Return oResult + End Function End Class End Namespace \ No newline at end of file diff --git a/Service.EDMIService/Methods/GlobalIndexer/Loader.vb b/Service.EDMIService/Methods/GlobalIndexer/Loader.vb index 52b5ee0c..4f9e750b 100644 --- a/Service.EDMIService/Methods/GlobalIndexer/Loader.vb +++ b/Service.EDMIService/Methods/GlobalIndexer/Loader.vb @@ -37,13 +37,13 @@ Namespace Methods.GlobalIndexer Dim oRow As DataRow = oProfile.Table.Rows.Item(0) Dim oProfileObject As New Profile With { - .Id = oRow.ItemEx("DOCTYPE_ID", 0), - .Name = oRow.ItemEx("DOCTYPE", "Missing Profile Name"), - .IsActive = oRow.ItemEx("AKTIV", False), - .NameConvention = oRow.ItemEx("NAMENKONVENTION", ""), - .ObjectStore = oRow.ItemEx("OBJECT_STORE", "Work"), - .ShortName = oRow.ItemEx("KURZNAME", ""), - .DynamicPath = oRow.ItemEx("DYNAMIC_FOLDER", "") + .Id = IIf(IsDBNull(oRow.Item("DOCTYPE_ID")), 0, oRow.Item("DOCTYPE_ID")), + .Name = IIf(IsDBNull(oRow.Item("DOCTYPE")), String.Empty, oRow.Item("DOCTYPE")), + .IsActive = IIf(IsDBNull(oRow.Item("AKTIV")), False, oRow.Item("AKTIV")), + .NameConvention = IIf(IsDBNull(oRow.Item("NAMENKONVENTION")), String.Empty, oRow.Item("NAMENKONVENTION")), + .ObjectStore = IIf(IsDBNull(oRow.Item("OBJECT_STORE")), "Work", oRow.Item("OBJECT_STORE")), + .ShortName = IIf(IsDBNull(oRow.Item("KURZNAME")), String.Empty, oRow.Item("KURZNAME")), + .DynamicPath = IIf(IsDBNull(oRow.Item("DYNAMIC_FOLDER")), String.Empty, oRow.Item("DYNAMIC_FOLDER")) } Logger.Debug("Name: [{0}]", oProfileObject.Name) @@ -82,13 +82,13 @@ Namespace Methods.GlobalIndexer For Each oRow As DataRow In oAutomaticIndexes.Table.Rows Dim oAutomaticIndex As New AutomaticIndex With { - .Id = oRow.ItemEx(Of Integer)("GUID"), - .Name = oRow.ItemEx(Of String)("INDEXNAME"), - .ProfileId = oRow.ItemEx(Of Integer)("DOCTYPE_ID"), - .SQLCommand = oRow.ItemEx(Of String)("SQL_RESULT"), - .SQLConnectionId = oRow.ItemEx(Of Integer)("CONNECTION_ID"), - .Sequence = oRow.ItemEx(Of String)("SEQUENCE"), - .Value = oRow.ItemEx(Of String)("VALUE") + .Id = oRow.Item("GUID"), + .Name = IIf(IsDBNull(oRow.Item("INDEXNAME")), String.Empty, oRow.Item("INDEXNAME")), + .ProfileId = IIf(IsDBNull(oRow.Item("DOCTYPE_ID")), 0, oRow.Item("DOCTYPE_ID")), + .SQLCommand = IIf(IsDBNull(oRow.Item("SQL_RESULT")), String.Empty, oRow.Item("SQL_RESULT")), + .SQLConnectionId = IIf(IsDBNull(oRow.Item("CONNECTION_ID")), 0, oRow.Item("CONNECTION_ID")), + .Sequence = IIf(IsDBNull(oRow.Item("SEQUENCE")), String.Empty, oRow.Item("SEQUENCE")), + .Value = IIf(IsDBNull(oRow.Item("VALUE")), String.Empty, oRow.Item("VALUE")) } oIndexes.Add(oAutomaticIndex) @@ -125,15 +125,15 @@ Namespace Methods.GlobalIndexer For Each oRow As DataRow In oManualIndexes.Table.Rows Dim oManualIndex As New ManualIndex With { - .Id = oRow.ItemEx(Of Integer)("GUID"), - .Name = oRow.ItemEx(Of String)("INDEXNAME"), - .ProfileId = oRow.ItemEx(Of Integer)("DOK_ID"), - .IsOptional = oRow.ItemEx(Of Boolean)("OPTIONAL"), - .IsMultiselect = oRow.ItemEx(Of String)("MULTISELECT"), - .SQLCommand = oRow.ItemEx(Of String)("SQL_RESULT"), - .SQLConnectionId = oRow.ItemEx(Of Integer)("CONNECTION_ID"), - .DefaultValue = oRow.ItemEx(Of String)("DEFAULT_VALUE"), - .DataType = oRow.ItemEx(Of String)("DATATYPE") + .Id = oRow.Item("GUID"), + .Name = oRow.Item("INDEXNAME"), + .ProfileId = oRow.Item("DOK_ID"), + .IsOptional = oRow.Item("OPTIONAL"), + .IsMultiselect = oRow.Item("MULTISELECT"), + .SQLCommand = oRow.Item("SQL_RESULT"), + .SQLConnectionId = oRow.Item("CONNECTION_ID"), + .DefaultValue = oRow.Item("DEFAULT_VALUE"), + .DataType = oRow.Item("DATATYPE") } oIndexes.Add(oManualIndex) @@ -179,16 +179,16 @@ Namespace Methods.GlobalIndexer For Each oRow As DataRow In oPostProcessingSteps.Table.Rows Dim oStep As New PostProcessingStep With { - .Id = oRow.ItemEx(Of Integer)("GUID"), - .IndexId = oRow.ItemEx(Of Integer)("IDXMAN_ID"), - .[Variant] = oRow.ItemEx(Of String)("VARIANT"), - .Type = oRow.ItemEx(Of String)("TYPE"), - .Function1 = oRow.ItemEx(Of String)("FUNCTION1"), - .Function2 = oRow.ItemEx(Of String)("FUNCTION2"), - .Text1 = oRow.ItemEx(Of String)("Text1"), - .Text2 = oRow.ItemEx(Of String)("Text2"), - .Text3 = oRow.ItemEx(Of String)("Text3"), - .Sequence = oRow.ItemEx(Of Integer)("SEQUENCE") + .Id = oRow.Item("GUID"), + .IndexId = oRow.Item("IDXMAN_ID"), + .[Variant] = oRow.Item("VARIANT"), + .Type = oRow.Item("TYPE"), + .Function1 = oRow.Item("FUNCTION1"), + .Function2 = oRow.Item("FUNCTION2"), + .Text1 = oRow.Item("Text1"), + .Text2 = oRow.Item("Text2"), + .Text3 = oRow.Item("Text3"), + .Sequence = oRow.Item("SEQUENCE") } oSteps.Add(oStep) diff --git a/Service.EDMIService/Methods/IDB/GetFileObject/GetFileObjectMethod.vb b/Service.EDMIService/Methods/IDB/GetFileObject/GetFileObjectMethod.vb index 613c68c2..382cafcc 100644 --- a/Service.EDMIService/Methods/IDB/GetFileObject/GetFileObjectMethod.vb +++ b/Service.EDMIService/Methods/IDB/GetFileObject/GetFileObjectMethod.vb @@ -25,12 +25,12 @@ Namespace Methods.IDB.GetFileObject LogAndThrow("Error while getting FileObject data!") End If - Dim oRow As DataRow = oTable.First() - Dim oFileHash As String = oRow.ItemEx("FILE_HASH", "") - Dim oFileSize As Long = oRow.ItemEx(Of Long)("FILE_SIZE", 0) - Dim oFileExtension As String = oRow.ItemEx(Of String)("EXTENSION") - Dim oFilePath = oRow.ItemEx("RELPATH", "") - Dim oFileName = oRow.ItemEx("Filename", "") + Dim oRow As DataRow = oTable.Rows(0) + Dim oFileHash As String = oRow.Item("FILE_HASH") + Dim oFileSize As Long = oRow.Item("FILE_SIZE") + Dim oFileExtension As String = oRow.Item("EXTENSION") + Dim oFilePath = oRow.Item("RELPATH") + Dim oFileName = oRow.Item("Filename") Dim oFullFileName = Path.Combine(oFilePath, oFileName) Dim oFileObject As New FileObject With { diff --git a/Service.EDMIService/My Project/AssemblyInfo.vb b/Service.EDMIService/My Project/AssemblyInfo.vb index 6b464f5c..993ffb98 100644 --- a/Service.EDMIService/My Project/AssemblyInfo.vb +++ b/Service.EDMIService/My Project/AssemblyInfo.vb @@ -12,8 +12,8 @@ Imports System.Runtime.InteropServices - - + + @@ -31,5 +31,5 @@ Imports System.Runtime.InteropServices ' übernehmen, indem Sie "*" eingeben: ' - - + + diff --git a/Service.EDMIService/Scheduler/DatatableJob.vb b/Service.EDMIService/Scheduler/DatatableJob.vb index b1510bd0..7f08e9f9 100644 --- a/Service.EDMIService/Scheduler/DatatableJob.vb +++ b/Service.EDMIService/Scheduler/DatatableJob.vb @@ -29,17 +29,17 @@ Public Class DatatableJob Try Dim oMSSQL As MSSQLServer = oJobData.Item("MSSQL") - Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WHERE CRON_ID = {oCronJobId}" + Dim oCronSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {oCronJobId}" Dim oCronDetails As DataTable = oMSSQL.GetDatatable(oCronSQL) For Each oRow As DataRow In oCronDetails.Rows - Dim oDatatableName As String = NotNull(oRow.Item("DT_NAME"), String.Empty) - Dim oParentColumn As String = NotNull(oRow.Item("DT_COLUMN"), String.Empty) - Dim oChildColumn As String = NotNull(oRow.Item("CHILD_DT_COLUMN"), String.Empty) - Dim oConnectionId As Integer = NotNull(oRow.Item("CON_ID"), String.Empty) - Dim oTitle As String = NotNull(oRow.Item("TITLE"), String.Empty) - Dim oSQL As String = NotNull(oRow.Item("COMMAND"), String.Empty) - Dim oIndexColumns As String = NotNull(oRow.Item("INDEX_COLUMNS"), String.Empty) + Dim oDatatableName As String = IIf(IsDBNull(oRow.Item("DT_NAME")), String.Empty, oRow.Item("DT_NAME")) + Dim oParentColumn As String = IIf(IsDBNull(oRow.Item("DT_COLUMN")), String.Empty, oRow.Item("DT_COLUMN")) + Dim oChildColumn As String = IIf(IsDBNull(oRow.Item("CHILD_DT_COLUMN")), String.Empty, oRow.Item("CHILD_DT_COLUMN")) + Dim oConnectionId As Integer = IIf(IsDBNull(oRow.Item("CON_ID")), 0, oRow.Item("CON_ID")) + Dim oTitle As String = IIf(IsDBNull(oRow.Item("TITLE")), String.Empty, oRow.Item("TITLE")) + Dim oSQL As String = IIf(IsDBNull(oRow.Item("COMMAND")), String.Empty, oRow.Item("COMMAND")) + Dim oIndexColumns As String = IIf(IsDBNull(oRow.Item("INDEX_COLUMNS")), String.Empty, oRow.Item("INDEX_COLUMNS")) Try oLogger.Debug("Running Command-Job [{0}]", oTitle) @@ -80,7 +80,7 @@ Public Class DatatableJob .DetailRow = oRow } - Dim oChildTableNAme As String = NotNull(oRow.Item("CHILD_DT_NAME"), String.Empty) + Dim oChildTableNAme As String = IIf(IsDBNull(oRow.Item("CHILD_DT_NAME")), String.Empty, oRow.Item("CHILD_DT_NAME")) If oChildTableNAme <> String.Empty Then oLogger.Debug("Child Datatable [{0}] defined, Relation: Parent [{1}] -> Child [{2}]", oChildTableNAme, oParentColumn, oChildColumn) diff --git a/Service.EDMIService/Scheduler/JobListener.vb b/Service.EDMIService/Scheduler/JobListener.vb index dab2f9b7..52254915 100644 --- a/Service.EDMIService/Scheduler/JobListener.vb +++ b/Service.EDMIService/Scheduler/JobListener.vb @@ -11,7 +11,10 @@ Public Class JobListener Inherits JobListenerSupport Public Overrides ReadOnly Property Name As String = "JobListener" - Public Property Dataset As DataSet + + ' ✅ Verwaltung mit ReaderWriterLockSlim statt Shadow-Copy + Private ReadOnly _Dataset As DataSet + Private Shared ReadOnly _DatasetLock As New ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion) Private ReadOnly _Logger As Logger Private ReadOnly _LogConfig As LogConfig @@ -23,9 +26,44 @@ Public Class JobListener _LogConfig = LogConfig _Logger = LogConfig.GetLogger() _MSSQL = MSSQL - Dataset = ResultDataSet + _Dataset = ResultDataSet + + _Logger.Debug("JobListener initialized with ReaderWriterLockSlim for thread-safe access") End Sub + ' ✅ Thread-sichere Methode zum Abrufen einer Tabelle + Public Function GetTableSafely(tableName As String) As DataTable + Try + _DatasetLock.EnterReadLock() + + If _Dataset.Tables.Contains(tableName) Then + ' Kopie nur von der einzelnen Tabelle, nicht vom ganzen Dataset! + Return _Dataset.Tables(tableName).Copy() + Else + Return Nothing + End If + + Finally + _DatasetLock.ExitReadLock() + End Try + End Function + + ' ✅ Thread-sichere Methode zum Prüfen ob Tabelle existiert + Public Function TableExists(tableName As String) As Boolean + Try + _DatasetLock.EnterReadLock() + Return _Dataset.Tables.Contains(tableName) + Finally + _DatasetLock.ExitReadLock() + End Try + End Function + + ' ✅ Thread-sicherer Zugriff auf Dataset (nur für GetCachedTables) + Public Function GetCurrentDataset() As DataSet + ' Keine Kopie - nur für Metadaten-Zugriff (Tabellennamen) + Return _Dataset + End Function + Public Overrides Function JobWasExecuted(context As IJobExecutionContext, jobException As JobExecutionException, Optional cancellationToken As CancellationToken = Nothing) As Task _Logger.Info("Job [{0}] was executed successfully. Saving Data.", context.JobDetail.Key) @@ -33,10 +71,11 @@ Public Class JobListener For Each oTableResult As JobResult.ResultTable In oResult.Tables Try - _Logger.Debug("Saving Datatable [{0}]", oTableResult.Table) + _Logger.Debug("Saving Datatable [{0}]", oTableResult.Table.TableName) SaveDataTables(oTableResult) Catch ex As Exception + _Logger.Error(ex) _Logger.Error("Error while executing SaveDataTables for {0}", oTableResult.Table.TableName) End Try Next @@ -44,6 +83,86 @@ Public Class JobListener Return MyBase.JobWasExecuted(context, jobException, cancellationToken) End Function + ' ✅ OPTIMIERT: SaveDataTables mit direktem Write-Lock + Public Sub SaveDataTables(Result As JobResult.ResultTable) + Dim oStopWatch As New Stopwatch() + + Try + Dim oTable As DataTable = Result.Table + Dim oName As String = Result.DetailRow.Item("DT_NAME") + Dim oDetailId As Integer = Result.DetailRow.Item("GUID") + + _Logger.Debug("Starting thread-safe update for table [{0}]", oName) + + ' ✅ SCHRITT 1: Write-Lock anfordern (blockiert Leser während Update) + oStopWatch.Start() + _DatasetLock.EnterWriteLock() + _Logger.Debug("[Lock] Write-Lock acquired for table [{0}]", oName) + + Try + ' ✅ SCHRITT 2: Direkte Änderungen im Dataset + If _Dataset.Tables.Contains(oName) Then + _Logger.Debug("Datatable [{0}] already exists, replacing.", oName) + + If Result.ChildTable IsNot Nothing Then + ReplaceExistingTable(oName, oTable, _Dataset, Result.ChildTable) + AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset) + Else + ReplaceExistingTable(oName, oTable, _Dataset) + End If + Else + _Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName) + + AddNewTable(oName, oTable, _Dataset) + + If Result.ChildTable IsNot Nothing Then + AddNewTable(Result.ChildTable.TableName, Result.ChildTable, _Dataset) + AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn, _Dataset) + End If + End If + + _Logger.Info("Dataset successfully updated with table [{0}]", oName) + + Finally + ' ✅ SCHRITT 3: Write-Lock freigeben (so schnell wie möglich!) + _DatasetLock.ExitWriteLock() + oStopWatch.Stop() + ' ✅ Performance-Warning bei langsamen Updates + If oStopWatch.ElapsedMilliseconds > 500 Then + _Logger.Warn("[Performance] Table update took {0}ms (> 500ms threshold)", oStopWatch.ElapsedMilliseconds) + Else + _Logger.Debug("[Performance] Table update completed in {0}ms", oStopWatch.ElapsedMilliseconds) + End If + End Try + + ' ✅ SCHRITT 4: Speichern in Historie (außerhalb des Locks) + Dim oMemoryUsage = GC.GetTotalMemory(True) + _Logger.Debug("Estimated memory usage after Job is [{0}]", FormatBytes(oMemoryUsage)) + + _MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})") + + Catch ex As Exception + _Logger.Warn("Unexpected error in SaveDataTables: {0}", ex.Message) + _Logger.Error(ex) + End Try + End Sub + Private Shared Function FormatBytes(bytes As Long) As String + Const KB As Long = 1024 + Const MB As Long = KB * 1024 + Const GB As Long = MB * 1024 + + Select Case bytes + Case >= GB + Return $"{bytes / GB:N2} GB" + Case >= MB + Return $"{bytes / MB:N2} MB" + Case >= KB + Return $"{bytes / KB:N2} KB" + Case Else + Return $"{bytes} Bytes" + End Select + End Function + Public Sub ReplaceExistingTable(Name As String, Table As DataTable, DataSet As DataSet, Optional ChildTable As DataTable = Nothing) Try Dim oDatatableNameTemp As String = Name & "-TEMP" @@ -103,13 +222,12 @@ Public Class JobListener DataSet.Tables.Item(oDatatableNameTemp).TableName = Name Catch ex As Exception _Logger.Error(ex) - Throw ex + Throw End Try End Sub Public Sub AddNewTable(Name As String, Table As DataTable, DataSet As DataSet) _Logger.Debug("DataTable [{0}] does not exist, adding to DataSet", Name) - DataSet.Tables.Add(Table) End Sub @@ -117,9 +235,9 @@ Public Class JobListener Return $"{ParentTableName}-{ChildTableName}" End Function - Public Sub AddRelation(ParentTableName As String, ParentColumnName As String, ChildTableName As String, ChildColumnName As String) - Dim oChild As DataTable = Dataset.Tables.Item(ChildTableName) - Dim oParent As DataTable = Dataset.Tables.Item(ParentTableName) + Public Sub AddRelation(ParentTableName As String, ParentColumnName As String, ChildTableName As String, ChildColumnName As String, TargetDataSet As DataSet) + Dim oChild As DataTable = TargetDataSet.Tables.Item(ChildTableName) + Dim oParent As DataTable = TargetDataSet.Tables.Item(ParentTableName) Dim oRelationName As String = GetRelationName(ParentTableName, ChildTableName) Dim oParentColumn As DataColumn = oParent.Columns.Item(ParentColumnName) @@ -128,12 +246,13 @@ Public Class JobListener _Logger.Debug("New Relation: {0}/{1} => {2}/{3}", ParentTableName, ParentColumnName, ChildTableName, ChildColumnName) Try _Logger.Debug("Creating Relation [{0}]", oRelationName) - _Logger.Debug("ParentColumn exists: {0}", IsNothing(oParentColumn)) - _Logger.Debug("ChildColumn exists: {0}", IsNothing(oChildColumn)) + _Logger.Debug("ParentColumn exists: {0}", Not IsNothing(oParentColumn)) + _Logger.Debug("ChildColumn exists: {0}", Not IsNothing(oChildColumn)) + Dim oRelation As New DataRelation(oRelationName, oParentColumn, oChildColumn) _Logger.Debug("Adding Relation [{0}]", oRelationName) - Dataset.Relations.Add(oRelation) + TargetDataSet.Relations.Add(oRelation) Catch ex As ArgumentNullException _Logger.Error(ex) @@ -145,7 +264,7 @@ Public Class JobListener Catch ex As InvalidConstraintException _Logger.Error(ex) - _Logger.Warn("DuplicateNameException in AddRelation: {0}", ex.Message) + _Logger.Warn("InvalidConstraintException in AddRelation: {0}", ex.Message) Catch ex As ArgumentException _Logger.Error(ex) @@ -158,55 +277,6 @@ Public Class JobListener End Try End Sub - Public Sub SaveDataTables(Result As JobResult.ResultTable) - Try - Dim oTable As DataTable = Result.Table - - Dim oName As String = Result.DetailRow.Item("DT_NAME") - Dim oDetailId As Integer = Result.DetailRow.Item("GUID") - - ' Used for debugging relations and constraints - '_Logger.Debug("Dataset BEFORE saving datatables") - 'ListTables(Dataset) - - If Dataset.Tables.Contains(oName) Then - _Logger.Debug("Datatable [{0}] already exists, replacing.", oName) - - ' Replace existing table - If Result.ChildTable IsNot Nothing Then - ReplaceExistingTable(oName, oTable, Dataset, Result.ChildTable) - AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn) - Else - ReplaceExistingTable(oName, oTable, Dataset) - End If - Else - _Logger.Debug("Datatable [{0}] does not exist yet, adding.", oName) - - AddNewTable(oName, oTable, Dataset) - - If Result.ChildTable IsNot Nothing Then - AddNewTable(Result.ChildTable.TableName, Result.ChildTable, Dataset) - - AddRelation(oName, Result.TableRelationColumn, Result.ChildTable.TableName, Result.ChildRelationColumn) - End If - End If - - ' Used for debugging relations and constraints - '_Logger.Debug("Dataset AFTER saving datatables") - 'ListTables(Dataset) - - Dim oMemoryUsage = GC.GetTotalMemory(True) - Dim oUsageString = Utils.BytesToString(oMemoryUsage) - - _Logger.Debug("Estimated memory usage after Job is [{0}]", oUsageString) - - _MSSQL.ExecuteNonQuery($"INSERT INTO TBAPPSERV_CRON_DETAIL_HISTORY (DETAIL_ID) VALUES ({oDetailId})") - Catch ex As Exception - _Logger.Warn("Unexpected error in SaveDataTables: {0}", ex.Message) - _Logger.Error(ex) - End Try - End Sub - Private Sub ListTables(dataset As DataSet) Dim oIndex As Integer = 1 @@ -226,4 +296,10 @@ Public Class JobListener oIndex += 1 Next End Sub -End Class + + ' ✅ Cleanup beim Dispose + Protected Overrides Sub Finalize() + _DatasetLock?.Dispose() + MyBase.Finalize() + End Sub +End Class \ No newline at end of file diff --git a/Service.EDMIService/Scheduler/Scheduler.vb b/Service.EDMIService/Scheduler/Scheduler.vb index 1a0209fd..aa3c3ee1 100644 --- a/Service.EDMIService/Scheduler/Scheduler.vb +++ b/Service.EDMIService/Scheduler/Scheduler.vb @@ -13,20 +13,25 @@ Public Class Scheduler Private ReadOnly _MSSQL As MSSQLServer Private _Scheduler As IScheduler Private ReadOnly _JobListener As JobListener + Private ReadOnly _GlobalState As GlobalState ' ✅ NEU Private ReadOnly _Props = New NameValueCollection From { {"quartz.serializer.type", "binary"}, - {"quartz.threadPool.maxConcurrency", 1} + {"quartz.threadPool.maxConcurrency", 3} } Private Const JOB_GROUP As String = "DatatableJobs" - Public Sub New(pLogConfig As LogConfig, pMSSQL_Config As MSSQLServer, pTableStore As DataSet) + Public Sub New(pLogConfig As LogConfig, pMSSQL_Config As MSSQLServer, pTableStore As DataSet, pGlobalState As GlobalState) MyBase.New(pLogConfig) _Factory = New StdSchedulerFactory(_Props) _MSSQL = pMSSQL_Config _JobListener = New JobListener(pLogConfig, _MSSQL, pTableStore) + _GlobalState = pGlobalState + + _GlobalState.JobListener = _JobListener + Logger.Debug("JobListener registered in GlobalState") Logging.LogProvider.SetCurrentLogProvider(New LogProvider(Logger)) End Sub @@ -108,7 +113,7 @@ Public Class Scheduler End Sub Public Async Function GetCronJobs() As Task(Of DataTable) Try - Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WHERE ACTIVE = 1" + Dim oSQL As String = "SELECT * FROM TBAPPSERV_CRON_JOB WITH (NOLOCK) WHERE ACTIVE = 1" Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL) Return oDatatable @@ -119,7 +124,7 @@ Public Class Scheduler Public Async Function GetCronJobDetails(CronJobId As Integer) As Task(Of DataTable) Try - Dim oSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WHERE CRON_ID = {CronJobId}" + Dim oSQL As String = $"SELECT * FROM TBAPPSERV_CRON_DETAIL WITH (NOLOCK) WHERE CRON_ID = {CronJobId}" Dim oDatatable As DataTable = Await _MSSQL.GetDatatableAsync(oSQL) Return oDatatable diff --git a/Service.EDMIService/WindowsService.vb b/Service.EDMIService/WindowsService.vb index 4249c9c5..f5bd3082 100644 --- a/Service.EDMIService/WindowsService.vb +++ b/Service.EDMIService/WindowsService.vb @@ -77,7 +77,7 @@ Public Class WindowsService GlobalState = New GlobalState(LogConfig, MSSQL_IDB, MSSQL_ECM) Dim oMSSQLServer = GetMSSQL_Config(LogConfigScheduler) - Scheduler = New Scheduler(LogConfigScheduler, oMSSQLServer, GlobalState.TableStore) + Scheduler = New Scheduler(LogConfigScheduler, oMSSQLServer, GlobalState.TableStore, GlobalState) Logger.Info("Loading Global Data") GlobalState.LoadObjectStores() @@ -94,7 +94,6 @@ Public Class WindowsService EDMIService.Firebird = Firebird EDMIService.LogConfig = LogConfig EDMIService.AppConfig = Config - EDMIService.Filesystem = Filesystem EDMIService.GlobalState = GlobalState EDMIService.Scheduler = Scheduler @@ -187,8 +186,13 @@ Public Class WindowsService Private Function GetMSSQL_Config(pLogConfig As LogConfig) As MSSQLServer Logger.Debug("Connecting to Config MSSQL") If Config.ConnectionString_Config = String.Empty Then - Logger.Warn("Configuration for ConnectionString_Config is missing.") - Throw New ArgumentNullException("ConnectionString_Config") + If Config.ConnectionString_ECM <> String.Empty Then + Logger.Info("Using ConnectionString_ECM for Config Database connection.") + Return GetMSSQL_ECM(pLogConfig) + Else + Logger.Warn("Configuration for ConnectionString_Config is missing and ConnectionString_ECM is also missing.") + Throw New ArgumentNullException("ConnectionString_Config") + End If End If Dim oMSSQL = New MSSQLServer(pLogConfig, Config.ConnectionString_Config) diff --git a/Service.EDMIService/packages.config b/Service.EDMIService/packages.config index b27d6005..b5d64842 100644 --- a/Service.EDMIService/packages.config +++ b/Service.EDMIService/packages.config @@ -2,7 +2,6 @@ -