diff --git a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs index 8ba521a..4c5cdbb 100644 --- a/src/MxGateway.Contracts/Generated/MxaccessGateway.cs +++ b/src/MxGateway.Contracts/Generated/MxaccessGateway.cs @@ -322,163 +322,166 @@ namespace MxGateway.Contracts.Proto { "b29nbGUucHJvdG9idWYuVGltZXN0YW1wEhUKDW9wZXJhdG9yX3VzZXIYCiAB", "KAkSGAoQb3BlcmF0b3JfY29tbWVudBgLIAEoCRIzCg1jdXJyZW50X3ZhbHVl", "GAwgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEjEKC2xpbWl0", - "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIpIB", - "ChdBY2tub3dsZWRnZUFsYXJtUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJ", - "Eh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgCIAEoCRIcChRhbGFybV9mdWxs", - "X3JlZmVyZW5jZRgDIAEoCRIPCgdjb21tZW50GAQgASgJEhUKDW9wZXJhdG9y", - "X3VzZXIYBSABKAki8wEKFUFja25vd2xlZGdlQWxhcm1SZXBseRISCgpzZXNz", - "aW9uX2lkGAEgASgJEhYKDmNvcnJlbGF0aW9uX2lkGAIgASgJEjwKD3Byb3Rv", - "Y29sX3N0YXR1cxgDIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9j", - "b2xTdGF0dXMSFAoHaHJlc3VsdBgEIAEoBUgAiAEBEjIKBnN0YXR1cxgFIAEo", - "CzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQcm94eRIaChJkaWFn", - "bm9zdGljX21lc3NhZ2UYBiABKAlCCgoIX2hyZXN1bHQiagoYUXVlcnlBY3Rp", - "dmVBbGFybXNSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50", - "X2NvcnJlbGF0aW9uX2lkGAIgASgJEhsKE2FsYXJtX2ZpbHRlcl9wcmVmaXgY", - "AyABKAki6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3Cghj", - "YXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXND", - "YXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3", - "YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19j", - "YXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlh", - "Z25vc3RpY190ZXh0GAcgASgJIqcDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgB", - "IAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJp", - "YW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25v", - "c3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFs", - "dWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3Zh", - "bHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVf", - "dmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1l", - "c3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1w", - "SAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYx", - "Lk14QXJyYXlIABITCglyYXdfdmFsdWUYEiABKAxIAEIGCgRraW5kIv4ECgdN", - "eEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIS", - "CgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0K", - "FXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgK", - "IAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50", - "MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFy", - "cmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdh", - "eS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhh", - "Y2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVl", - "cxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5", - "Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5T", - "dHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFj", - "Y2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVl", - "cxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2", - "YWx1ZXMiGwoJQm9vbEFycmF5Eg4KBnZhbHVlcxgBIAMoCCIcCgpJbnQzMkFy", - "cmF5Eg4KBnZhbHVlcxgBIAMoBSIcCgpJbnQ2NEFycmF5Eg4KBnZhbHVlcxgB", - "IAMoAyIcCgpGbG9hdEFycmF5Eg4KBnZhbHVlcxgBIAMoAiIdCgtEb3VibGVB", - "cnJheRIOCgZ2YWx1ZXMYASADKAEiHQoLU3RyaW5nQXJyYXkSDgoGdmFsdWVz", - "GAEgAygJIjwKDlRpbWVzdGFtcEFycmF5EioKBnZhbHVlcxgBIAMoCzIaLmdv", - "b2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiGgoIUmF3QXJyYXkSDgoGdmFsdWVz", - "GAEgAygMIlgKDlByb3RvY29sU3RhdHVzEjUKBGNvZGUYASABKA4yJy5teGFj", - "Y2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVzQ29kZRIPCgdtZXNzYWdl", - "GAIgASgJKp8LCg1NeENvbW1hbmRLaW5kEh8KG01YX0NPTU1BTkRfS0lORF9V", - "TlNQRUNJRklFRBAAEhwKGE1YX0NPTU1BTkRfS0lORF9SRUdJU1RFUhABEh4K", - "Gk1YX0NPTU1BTkRfS0lORF9VTlJFR0lTVEVSEAISHAoYTVhfQ09NTUFORF9L", - "SU5EX0FERF9JVEVNEAMSHQoZTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNMhAE", - "Eh8KG01YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTRAFEhoKFk1YX0NPTU1B", - "TkRfS0lORF9BRFZJU0UQBhIdChlNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNF", - "EAcSJgoiTVhfQ09NTUFORF9LSU5EX0FEVklTRV9TVVBFUlZJU09SWRAIEiUK", - "IU1YX0NPTU1BTkRfS0lORF9BRERfQlVGRkVSRURfSVRFTRAJEjAKLE1YX0NP", - "TU1BTkRfS0lORF9TRVRfQlVGRkVSRURfVVBEQVRFX0lOVEVSVkFMEAoSGwoX", - "TVhfQ09NTUFORF9LSU5EX1NVU1BFTkQQCxIcChhNWF9DT01NQU5EX0tJTkRf", - "QUNUSVZBVEUQDBIZChVNWF9DT01NQU5EX0tJTkRfV1JJVEUQDRIaChZNWF9D", - "T01NQU5EX0tJTkRfV1JJVEUyEA4SIQodTVhfQ09NTUFORF9LSU5EX1dSSVRF", - "X1NFQ1VSRUQQDxIiCh5NWF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDIQ", - "EBIlCiFNWF9DT01NQU5EX0tJTkRfQVVUSEVOVElDQVRFX1VTRVIQERIoCiRN", - "WF9DT01NQU5EX0tJTkRfQVJDSEVTVFJBX1VTRVJfVE9fSUQQEhIhCh1NWF9D", - "T01NQU5EX0tJTkRfQUREX0lURU1fQlVMSxATEiQKIE1YX0NPTU1BTkRfS0lO", - "RF9BRFZJU0VfSVRFTV9CVUxLEBQSJAogTVhfQ09NTUFORF9LSU5EX1JFTU9W", - "RV9JVEVNX0JVTEsQFRInCiNNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFX0lU", - "RU1fQlVMSxAWEiIKHk1YX0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQlVMSxAX", - "EiQKIE1YX0NPTU1BTkRfS0lORF9VTlNVQlNDUklCRV9CVUxLEBgSJAogTVhf", - "Q09NTUFORF9LSU5EX1NVQlNDUklCRV9BTEFSTVMQGRImCiJNWF9DT01NQU5E", - "X0tJTkRfVU5TVUJTQ1JJQkVfQUxBUk1TEBoSJQohTVhfQ09NTUFORF9LSU5E", - "X0FDS05PV0xFREdFX0FMQVJNEBsSJwojTVhfQ09NTUFORF9LSU5EX1FVRVJZ", - "X0FDVElWRV9BTEFSTVMQHBItCilNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVE", - "R0VfQUxBUk1fQllfTkFNRRAdEh4KGk1YX0NPTU1BTkRfS0lORF9XUklURV9C", - "VUxLEB4SHwobTVhfQ09NTUFORF9LSU5EX1dSSVRFMl9CVUxLEB8SJgoiTVhf", - "Q09NTUFORF9LSU5EX1dSSVRFX1NFQ1VSRURfQlVMSxAgEicKI01YX0NPTU1B", - "TkRfS0lORF9XUklURV9TRUNVUkVEMl9CVUxLECESHQoZTVhfQ09NTUFORF9L", - "SU5EX1JFQURfQlVMSxAiEhgKFE1YX0NPTU1BTkRfS0lORF9QSU5HEGQSJQoh", - "TVhfQ09NTUFORF9LSU5EX0dFVF9TRVNTSU9OX1NUQVRFEGUSIwofTVhfQ09N", - "TUFORF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBmEiAKHE1YX0NPTU1BTkRfS0lO", - "RF9EUkFJTl9FVkVOVFMQZxIjCh9NWF9DT01NQU5EX0tJTkRfU0hVVERPV05f", - "V09SS0VSEGgq+QEKDU14RXZlbnRGYW1pbHkSHwobTVhfRVZFTlRfRkFNSUxZ", - "X1VOU1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRfRkFNSUxZX09OX0RBVEFfQ0hB", - "TkdFEAESJQohTVhfRVZFTlRfRkFNSUxZX09OX1dSSVRFX0NPTVBMRVRFEAIS", - "JgoiTVhfRVZFTlRfRkFNSUxZX09QRVJBVElPTl9DT01QTEVURRADEisKJ01Y", - "X0VWRU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9EQVRBX0NIQU5HRRAEEicKI01Y", - "X0VWRU5UX0ZBTUlMWV9PTl9BTEFSTV9UUkFOU0lUSU9OEAUqygEKE0FsYXJt", - "VHJhbnNpdGlvbktpbmQSJQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BF", - "Q0lGSUVEEAASHwobQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQoh", - "QUxBUk1fVFJBTlNJVElPTl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1f", - "VFJBTlNJVElPTl9LSU5EX0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9L", - "SU5EX1JFVFJJR0dFUhAEKqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFM", - "QVJNX0NPTkRJVElPTl9TVEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NP", - "TkRJVElPTl9TVEFURV9BQ1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RB", - "VEVfQUNUSVZFX0FDS0VEEAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lO", - "QUNUSVZFEAMqpQMKEE14U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NB", - "VEVHT1JZX1VOU1BFQ0lGSUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VO", - "S05PV04QARIZChVNWF9TVEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFU", - "VVNfQ0FURUdPUllfUEVORElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9X", - "QVJOSU5HEAQSKgomTVhfU1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05f", - "RVJST1IQBRIqCiZNWF9TVEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9F", - "UlJPUhAGEigKJE1YX1NUQVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJP", - "UhAHEiUKIU1YX1NUQVRVU19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUK", - "IU1YX1NUQVRVU19DQVRFR09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NU", - "QVRVU19DQVRFR09SWV9PVEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJj", - "ZRIgChxNWF9TVEFUVVNfU09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RB", - "VFVTX1NPVVJDRV9VTktOT1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFV", - "RVNUSU5HX0xNWBACEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19M", - "TVgQAxIjCh9NWF9TVEFUVVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwof", - "TVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRV", - "U19TT1VSQ0VfUkVRVUVTVElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1Y", - "X1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAH", - "Kt0ECgpNeERhdGFUeXBlEhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAA", - "EhgKFE1YX0RBVEFfVFlQRV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05P", - "X0RBVEEQAhIYChRNWF9EQVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFf", - "VFlQRV9JTlRFR0VSEAQSFgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhf", - "REFUQV9UWVBFX0RPVUJMRRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIV", - "ChFNWF9EQVRBX1RZUEVfVElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VE", - "X1RJTUUQCRIfChtNWF9EQVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhN", - "WF9EQVRBX1RZUEVfU1RBVFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5V", - "TRAMEi0KKU1YX0RBVEFfVFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9F", - "TlVNEA0SIgoeTVhfREFUQV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwob", - "TVhfREFUQV9UWVBFX1FVQUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBF", - "X1FVQUxJRklFRF9TVFJVQ1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJ", - "T05BTElaRURfU1RSSU5HEBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQ", - "EhIUChBNWF9EQVRBX1RZUEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29k", - "ZRIkCiBQUk9UT0NPTF9TVEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BS", - "T1RPQ09MX1NUQVRVU19DT0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NP", - "REVfSU5WQUxJRF9SRVFVRVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVf", - "U0VTU0lPTl9OT1RfRk9VTkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9T", - "RVNTSU9OX05PVF9SRUFEWRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dP", - "UktFUl9VTkFWQUlMQUJMRRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJ", - "TUVPVVQQBhIhCh1QUk9UT0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisK", - "J1BST1RPQ09MX1NUQVRVU19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikK", - "JVBST1RPQ09MX1NUQVRVU19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoM", - "U2Vzc2lvblN0YXRlEh0KGVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIa", - "ChZTRVNTSU9OX1NUQVRFX0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9T", - "VEFSVElOR19XT1JLRVIQAhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9S", - "X1BJUEUQAxIdChlTRVNTSU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VT", - "U0lPTl9TVEFURV9JTklUSUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9T", - "VEFURV9SRUFEWRAGEhkKFVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNF", - "U1NJT05fU1RBVEVfQ0xPU0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVE", - "EAky4AQKD014QWNjZXNzR2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNj", - "ZXNzX2dhdGV3YXkudjEuT3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3Nf", - "Z2F0ZXdheS52MS5PcGVuU2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIo", - "Lm14YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14", - "YWNjZXNzX2dhdGV3YXkudjEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tl", - "EiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhh", - "Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVu", - "dHMSKC5teGFjY2Vzc19nYXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3Qa", - "HC5teGFjY2Vzc19nYXRld2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRn", - "ZUFsYXJtEiwubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJt", - "UmVxdWVzdBoqLm14YWNjZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFy", - "bVJlcGx5Em4KEVF1ZXJ5QWN0aXZlQWxhcm1zEi0ubXhhY2Nlc3NfZ2F0ZXdh", - "eS52MS5RdWVyeUFjdGl2ZUFsYXJtc1JlcXVlc3QaKC5teGFjY2Vzc19nYXRl", - "d2F5LnYxLkFjdGl2ZUFsYXJtU25hcHNob3QwAUIcqgIZTXhHYXRld2F5LkNv", - "bnRyYWN0cy5Qcm90b2IGcHJvdG8z")); + "X3ZhbHVlGA0gASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlIpAB", + "ChdBY2tub3dsZWRnZUFsYXJtUmVxdWVzdBIdChVjbGllbnRfY29ycmVsYXRp", + "b25faWQYAiABKAkSHAoUYWxhcm1fZnVsbF9yZWZlcmVuY2UYAyABKAkSDwoH", + "Y29tbWVudBgEIAEoCRIVCg1vcGVyYXRvcl91c2VyGAUgASgJSgQIARACUgpz", + "ZXNzaW9uX2lkIvEBChVBY2tub3dsZWRnZUFsYXJtUmVwbHkSFgoOY29ycmVs", + "YXRpb25faWQYAiABKAkSPAoPcHJvdG9jb2xfc3RhdHVzGAMgASgLMiMubXhh", + "Y2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0GAQg", + "ASgFSACIAQESMgoGc3RhdHVzGAUgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5NeFN0YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVzc2FnZRgGIAEoCUIK", + "CghfaHJlc3VsdEoECAEQAlIKc2Vzc2lvbl9pZCJRChNTdHJlYW1BbGFybXNS", + "ZXF1ZXN0Eh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgBIAEoCRIbChNhbGFy", + "bV9maWx0ZXJfcHJlZml4GAIgASgJIr8BChBBbGFybUZlZWRNZXNzYWdlEkAK", + "DGFjdGl2ZV9hbGFybRgBIAEoCzIoLm14YWNjZXNzX2dhdGV3YXkudjEuQWN0", + "aXZlQWxhcm1TbmFwc2hvdEgAEhsKEXNuYXBzaG90X2NvbXBsZXRlGAIgASgI", + "SAASQQoKdHJhbnNpdGlvbhgDIAEoCzIrLm14YWNjZXNzX2dhdGV3YXkudjEu", + "T25BbGFybVRyYW5zaXRpb25FdmVudEgAQgkKB3BheWxvYWQi6wEKDU14U3Rh", + "dHVzUHJveHkSDwoHc3VjY2VzcxgBIAEoBRI3CghjYXRlZ29yeRgCIAEoDjIl", + "Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRl", + "Y3RlZF9ieRgDIAEoDjIjLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNT", + "b3VyY2USDgoGZGV0YWlsGAQgASgFEhQKDHJhd19jYXRlZ29yeRgFIAEoBRIX", + "Cg9yYXdfZGV0ZWN0ZWRfYnkYBiABKAUSFwoPZGlhZ25vc3RpY190ZXh0GAcg", + "ASgJIqcDCgdNeFZhbHVlEjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNz", + "X2dhdGV3YXkudjEuTXhEYXRhVHlwZRIUCgx2YXJpYW50X3R5cGUYAiABKAkS", + "DwoHaXNfbnVsbBgDIAEoCBIWCg5yYXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1y", + "YXdfZGF0YV90eXBlGAUgASgFEhQKCmJvb2xfdmFsdWUYCiABKAhIABIVCgtp", + "bnQzMl92YWx1ZRgLIAEoBUgAEhUKC2ludDY0X3ZhbHVlGAwgASgDSAASFQoL", + "ZmxvYXRfdmFsdWUYDSABKAJIABIWCgxkb3VibGVfdmFsdWUYDiABKAFIABIW", + "CgxzdHJpbmdfdmFsdWUYDyABKAlIABI1Cg90aW1lc3RhbXBfdmFsdWUYECAB", + "KAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAASMwoLYXJyYXlfdmFs", + "dWUYESABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJyYXlIABITCgly", + "YXdfdmFsdWUYEiABKAxIAEIGCgRraW5kIv4ECgdNeEFycmF5EjoKEWVsZW1l", + "bnRfZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERh", + "dGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRISCgpkaW1lbnNpb25zGAMg", + "AygNEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEh0KFXJhd19lbGVtZW50X2Rh", + "dGFfdHlwZRgFIAEoBRI1Cgtib29sX3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNz", + "X2dhdGV3YXkudjEuQm9vbEFycmF5SAASNwoMaW50MzJfdmFsdWVzGAsgASgL", + "Mh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQzMkFycmF5SAASNwoMaW50NjRf", + "dmFsdWVzGAwgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5", + "SAASNwoMZmxvYXRfdmFsdWVzGA0gASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5GbG9hdEFycmF5SAASOQoNZG91YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuRG91YmxlQXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVz", + "GA8gASgLMiAubXhhY2Nlc3NfZ2F0ZXdheS52MS5TdHJpbmdBcnJheUgAEj8K", + "EHRpbWVzdGFtcF92YWx1ZXMYECABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYx", + "LlRpbWVzdGFtcEFycmF5SAASMwoKcmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuUmF3QXJyYXlIAEIICgZ2YWx1ZXMiGwoJQm9vbEFy", + "cmF5Eg4KBnZhbHVlcxgBIAMoCCIcCgpJbnQzMkFycmF5Eg4KBnZhbHVlcxgB", + "IAMoBSIcCgpJbnQ2NEFycmF5Eg4KBnZhbHVlcxgBIAMoAyIcCgpGbG9hdEFy", + "cmF5Eg4KBnZhbHVlcxgBIAMoAiIdCgtEb3VibGVBcnJheRIOCgZ2YWx1ZXMY", + "ASADKAEiHQoLU3RyaW5nQXJyYXkSDgoGdmFsdWVzGAEgAygJIjwKDlRpbWVz", + "dGFtcEFycmF5EioKBnZhbHVlcxgBIAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5U", + "aW1lc3RhbXAiGgoIUmF3QXJyYXkSDgoGdmFsdWVzGAEgAygMIlgKDlByb3Rv", + "Y29sU3RhdHVzEjUKBGNvZGUYASABKA4yJy5teGFjY2Vzc19nYXRld2F5LnYx", + "LlByb3RvY29sU3RhdHVzQ29kZRIPCgdtZXNzYWdlGAIgASgJKp8LCg1NeENv", + "bW1hbmRLaW5kEh8KG01YX0NPTU1BTkRfS0lORF9VTlNQRUNJRklFRBAAEhwK", + "GE1YX0NPTU1BTkRfS0lORF9SRUdJU1RFUhABEh4KGk1YX0NPTU1BTkRfS0lO", + "RF9VTlJFR0lTVEVSEAISHAoYTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNEAMS", + "HQoZTVhfQ09NTUFORF9LSU5EX0FERF9JVEVNMhAEEh8KG01YX0NPTU1BTkRf", + "S0lORF9SRU1PVkVfSVRFTRAFEhoKFk1YX0NPTU1BTkRfS0lORF9BRFZJU0UQ", + "BhIdChlNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFEAcSJgoiTVhfQ09NTUFO", + "RF9LSU5EX0FEVklTRV9TVVBFUlZJU09SWRAIEiUKIU1YX0NPTU1BTkRfS0lO", + "RF9BRERfQlVGRkVSRURfSVRFTRAJEjAKLE1YX0NPTU1BTkRfS0lORF9TRVRf", + "QlVGRkVSRURfVVBEQVRFX0lOVEVSVkFMEAoSGwoXTVhfQ09NTUFORF9LSU5E", + "X1NVU1BFTkQQCxIcChhNWF9DT01NQU5EX0tJTkRfQUNUSVZBVEUQDBIZChVN", + "WF9DT01NQU5EX0tJTkRfV1JJVEUQDRIaChZNWF9DT01NQU5EX0tJTkRfV1JJ", + "VEUyEA4SIQodTVhfQ09NTUFORF9LSU5EX1dSSVRFX1NFQ1VSRUQQDxIiCh5N", + "WF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRDIQEBIlCiFNWF9DT01NQU5E", + "X0tJTkRfQVVUSEVOVElDQVRFX1VTRVIQERIoCiRNWF9DT01NQU5EX0tJTkRf", + "QVJDSEVTVFJBX1VTRVJfVE9fSUQQEhIhCh1NWF9DT01NQU5EX0tJTkRfQURE", + "X0lURU1fQlVMSxATEiQKIE1YX0NPTU1BTkRfS0lORF9BRFZJU0VfSVRFTV9C", + "VUxLEBQSJAogTVhfQ09NTUFORF9LSU5EX1JFTU9WRV9JVEVNX0JVTEsQFRIn", + "CiNNWF9DT01NQU5EX0tJTkRfVU5fQURWSVNFX0lURU1fQlVMSxAWEiIKHk1Y", + "X0NPTU1BTkRfS0lORF9TVUJTQ1JJQkVfQlVMSxAXEiQKIE1YX0NPTU1BTkRf", + "S0lORF9VTlNVQlNDUklCRV9CVUxLEBgSJAogTVhfQ09NTUFORF9LSU5EX1NV", + "QlNDUklCRV9BTEFSTVMQGRImCiJNWF9DT01NQU5EX0tJTkRfVU5TVUJTQ1JJ", + "QkVfQUxBUk1TEBoSJQohTVhfQ09NTUFORF9LSU5EX0FDS05PV0xFREdFX0FM", + "QVJNEBsSJwojTVhfQ09NTUFORF9LSU5EX1FVRVJZX0FDVElWRV9BTEFSTVMQ", + "HBItCilNWF9DT01NQU5EX0tJTkRfQUNLTk9XTEVER0VfQUxBUk1fQllfTkFN", + "RRAdEh4KGk1YX0NPTU1BTkRfS0lORF9XUklURV9CVUxLEB4SHwobTVhfQ09N", + "TUFORF9LSU5EX1dSSVRFMl9CVUxLEB8SJgoiTVhfQ09NTUFORF9LSU5EX1dS", + "SVRFX1NFQ1VSRURfQlVMSxAgEicKI01YX0NPTU1BTkRfS0lORF9XUklURV9T", + "RUNVUkVEMl9CVUxLECESHQoZTVhfQ09NTUFORF9LSU5EX1JFQURfQlVMSxAi", + "EhgKFE1YX0NPTU1BTkRfS0lORF9QSU5HEGQSJQohTVhfQ09NTUFORF9LSU5E", + "X0dFVF9TRVNTSU9OX1NUQVRFEGUSIwofTVhfQ09NTUFORF9LSU5EX0dFVF9X", + "T1JLRVJfSU5GTxBmEiAKHE1YX0NPTU1BTkRfS0lORF9EUkFJTl9FVkVOVFMQ", + "ZxIjCh9NWF9DT01NQU5EX0tJTkRfU0hVVERPV05fV09SS0VSEGgq+QEKDU14", + "RXZlbnRGYW1pbHkSHwobTVhfRVZFTlRfRkFNSUxZX1VOU1BFQ0lGSUVEEAAS", + "IgoeTVhfRVZFTlRfRkFNSUxZX09OX0RBVEFfQ0hBTkdFEAESJQohTVhfRVZF", + "TlRfRkFNSUxZX09OX1dSSVRFX0NPTVBMRVRFEAISJgoiTVhfRVZFTlRfRkFN", + "SUxZX09QRVJBVElPTl9DT01QTEVURRADEisKJ01YX0VWRU5UX0ZBTUlMWV9P", + "Tl9CVUZGRVJFRF9EQVRBX0NIQU5HRRAEEicKI01YX0VWRU5UX0ZBTUlMWV9P", + "Tl9BTEFSTV9UUkFOU0lUSU9OEAUqygEKE0FsYXJtVHJhbnNpdGlvbktpbmQS", + "JQohQUxBUk1fVFJBTlNJVElPTl9LSU5EX1VOU1BFQ0lGSUVEEAASHwobQUxB", + "Uk1fVFJBTlNJVElPTl9LSU5EX1JBSVNFEAESJQohQUxBUk1fVFJBTlNJVElP", + "Tl9LSU5EX0FDS05PV0xFREdFEAISHwobQUxBUk1fVFJBTlNJVElPTl9LSU5E", + "X0NMRUFSEAMSIwofQUxBUk1fVFJBTlNJVElPTl9LSU5EX1JFVFJJR0dFUhAE", + "KqoBChNBbGFybUNvbmRpdGlvblN0YXRlEiUKIUFMQVJNX0NPTkRJVElPTl9T", + "VEFURV9VTlNQRUNJRklFRBAAEiAKHEFMQVJNX0NPTkRJVElPTl9TVEFURV9B", + "Q1RJVkUQARImCiJBTEFSTV9DT05ESVRJT05fU1RBVEVfQUNUSVZFX0FDS0VE", + "EAISIgoeQUxBUk1fQ09ORElUSU9OX1NUQVRFX0lOQUNUSVZFEAMqpQMKEE14", + "U3RhdHVzQ2F0ZWdvcnkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX1VOU1BFQ0lG", + "SUVEEAASHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1VOS05PV04QARIZChVNWF9T", + "VEFUVVNfQ0FURUdPUllfT0sQAhIeChpNWF9TVEFUVVNfQ0FURUdPUllfUEVO", + "RElORxADEh4KGk1YX1NUQVRVU19DQVRFR09SWV9XQVJOSU5HEAQSKgomTVhf", + "U1RBVFVTX0NBVEVHT1JZX0NPTU1VTklDQVRJT05fRVJST1IQBRIqCiZNWF9T", + "VEFUVVNfQ0FURUdPUllfQ09ORklHVVJBVElPTl9FUlJPUhAGEigKJE1YX1NU", + "QVRVU19DQVRFR09SWV9PUEVSQVRJT05BTF9FUlJPUhAHEiUKIU1YX1NUQVRV", + "U19DQVRFR09SWV9TRUNVUklUWV9FUlJPUhAIEiUKIU1YX1NUQVRVU19DQVRF", + "R09SWV9TT0ZUV0FSRV9FUlJPUhAJEiIKHk1YX1NUQVRVU19DQVRFR09SWV9P", + "VEhFUl9FUlJPUhAKKsoCCg5NeFN0YXR1c1NvdXJjZRIgChxNWF9TVEFUVVNf", + "U09VUkNFX1VOU1BFQ0lGSUVEEAASHAoYTVhfU1RBVFVTX1NPVVJDRV9VTktO", + "T1dOEAESIwofTVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0xNWBACEiMK", + "H01YX1NUQVRVU19TT1VSQ0VfUkVTUE9ORElOR19MTVgQAxIjCh9NWF9TVEFU", + "VVNfU09VUkNFX1JFUVVFU1RJTkdfTk1YEAQSIwofTVhfU1RBVFVTX1NPVVJD", + "RV9SRVNQT05ESU5HX05NWBAFEjEKLU1YX1NUQVRVU19TT1VSQ0VfUkVRVUVT", + "VElOR19BVVRPTUFUSU9OX09CSkVDVBAGEjEKLU1YX1NUQVRVU19TT1VSQ0Vf", + "UkVTUE9ORElOR19BVVRPTUFUSU9OX09CSkVDVBAHKt0ECgpNeERhdGFUeXBl", + "EhwKGE1YX0RBVEFfVFlQRV9VTlNQRUNJRklFRBAAEhgKFE1YX0RBVEFfVFlQ", + "RV9VTktOT1dOEAESGAoUTVhfREFUQV9UWVBFX05PX0RBVEEQAhIYChRNWF9E", + "QVRBX1RZUEVfQk9PTEVBThADEhgKFE1YX0RBVEFfVFlQRV9JTlRFR0VSEAQS", + "FgoSTVhfREFUQV9UWVBFX0ZMT0FUEAUSFwoTTVhfREFUQV9UWVBFX0RPVUJM", + "RRAGEhcKE01YX0RBVEFfVFlQRV9TVFJJTkcQBxIVChFNWF9EQVRBX1RZUEVf", + "VElNRRAIEh0KGU1YX0RBVEFfVFlQRV9FTEFQU0VEX1RJTUUQCRIfChtNWF9E", + "QVRBX1RZUEVfUkVGRVJFTkNFX1RZUEUQChIcChhNWF9EQVRBX1RZUEVfU1RB", + "VFVTX1RZUEUQCxIVChFNWF9EQVRBX1RZUEVfRU5VTRAMEi0KKU1YX0RBVEFf", + "VFlQRV9TRUNVUklUWV9DTEFTU0lGSUNBVElPTl9FTlVNEA0SIgoeTVhfREFU", + "QV9UWVBFX0RBVEFfUVVBTElUWV9UWVBFEA4SHwobTVhfREFUQV9UWVBFX1FV", + "QUxJRklFRF9FTlVNEA8SIQodTVhfREFUQV9UWVBFX1FVQUxJRklFRF9TVFJV", + "Q1QQEBIpCiVNWF9EQVRBX1RZUEVfSU5URVJOQVRJT05BTElaRURfU1RSSU5H", + "EBESGwoXTVhfREFUQV9UWVBFX0JJR19TVFJJTkcQEhIUChBNWF9EQVRBX1RZ", + "UEVfRU5EEBMqowMKElByb3RvY29sU3RhdHVzQ29kZRIkCiBQUk9UT0NPTF9T", + "VEFUVVNfQ09ERV9VTlNQRUNJRklFRBAAEhsKF1BST1RPQ09MX1NUQVRVU19D", + "T0RFX09LEAESKAokUFJPVE9DT0xfU1RBVFVTX0NPREVfSU5WQUxJRF9SRVFV", + "RVNUEAISKgomUFJPVE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfRk9V", + "TkQQAxIqCiZQUk9UT0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9SRUFE", + "WRAEEisKJ1BST1RPQ09MX1NUQVRVU19DT0RFX1dPUktFUl9VTkFWQUlMQUJM", + "RRAFEiAKHFBST1RPQ09MX1NUQVRVU19DT0RFX1RJTUVPVVQQBhIhCh1QUk9U", + "T0NPTF9TVEFUVVNfQ09ERV9DQU5DRUxFRBAHEisKJ1BST1RPQ09MX1NUQVRV", + "U19DT0RFX1BST1RPQ09MX1ZJT0xBVElPThAIEikKJVBST1RPQ09MX1NUQVRV", + "U19DT0RFX01YQUNDRVNTX0ZBSUxVUkUQCSq/AgoMU2Vzc2lvblN0YXRlEh0K", + "GVNFU1NJT05fU1RBVEVfVU5TUEVDSUZJRUQQABIaChZTRVNTSU9OX1NUQVRF", + "X0NSRUFUSU5HEAESIQodU0VTU0lPTl9TVEFURV9TVEFSVElOR19XT1JLRVIQ", + "AhIiCh5TRVNTSU9OX1NUQVRFX1dBSVRJTkdfRk9SX1BJUEUQAxIdChlTRVNT", + "SU9OX1NUQVRFX0hBTkRTSEFLSU5HEAQSJQohU0VTU0lPTl9TVEFURV9JTklU", + "SUFMSVpJTkdfV09SS0VSEAUSFwoTU0VTU0lPTl9TVEFURV9SRUFEWRAGEhkK", + "FVNFU1NJT05fU1RBVEVfQ0xPU0lORxAHEhgKFFNFU1NJT05fU1RBVEVfQ0xP", + "U0VEEAgSGQoVU0VTU0lPTl9TVEFURV9GQVVMVEVEEAky0wQKD014QWNjZXNz", + "R2F0ZXdheRJdCgtPcGVuU2Vzc2lvbhInLm14YWNjZXNzX2dhdGV3YXkudjEu", + "T3BlblNlc3Npb25SZXF1ZXN0GiUubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVu", + "U2Vzc2lvblJlcGx5EmAKDENsb3NlU2Vzc2lvbhIoLm14YWNjZXNzX2dhdGV3", + "YXkudjEuQ2xvc2VTZXNzaW9uUmVxdWVzdBomLm14YWNjZXNzX2dhdGV3YXku", + "djEuQ2xvc2VTZXNzaW9uUmVwbHkSVAoGSW52b2tlEiUubXhhY2Nlc3NfZ2F0", + "ZXdheS52MS5NeENvbW1hbmRSZXF1ZXN0GiMubXhhY2Nlc3NfZ2F0ZXdheS52", + "MS5NeENvbW1hbmRSZXBseRJYCgxTdHJlYW1FdmVudHMSKC5teGFjY2Vzc19n", + "YXRld2F5LnYxLlN0cmVhbUV2ZW50c1JlcXVlc3QaHC5teGFjY2Vzc19nYXRl", + "d2F5LnYxLk14RXZlbnQwARJsChBBY2tub3dsZWRnZUFsYXJtEiwubXhhY2Nl", + "c3NfZ2F0ZXdheS52MS5BY2tub3dsZWRnZUFsYXJtUmVxdWVzdBoqLm14YWNj", + "ZXNzX2dhdGV3YXkudjEuQWNrbm93bGVkZ2VBbGFybVJlcGx5EmEKDFN0cmVh", + "bUFsYXJtcxIoLm14YWNjZXNzX2dhdGV3YXkudjEuU3RyZWFtQWxhcm1zUmVx", + "dWVzdBolLm14YWNjZXNzX2dhdGV3YXkudjEuQWxhcm1GZWVkTWVzc2FnZTAB", + "QhyqAhlNeEdhdGV3YXkuQ29udHJhY3RzLlByb3RvYgZwcm90bzM=")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.AlarmTransitionKind), typeof(global::MxGateway.Contracts.Proto.AlarmConditionState), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] { @@ -559,9 +562,10 @@ namespace MxGateway.Contracts.Proto { new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnBufferedDataChangeEvent), global::MxGateway.Contracts.Proto.OnBufferedDataChangeEvent.Parser, new[]{ "DataType", "QualityValues", "TimestampValues", "RawDataType" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent), global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent.Parser, new[]{ "AlarmFullReference", "SourceObjectReference", "AlarmTypeName", "TransitionKind", "Severity", "OriginalRaiseTimestamp", "TransitionTimestamp", "OperatorUser", "OperatorComment", "Category", "Description", "CurrentValue", "LimitValue" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot), global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser, new[]{ "AlarmFullReference", "SourceObjectReference", "AlarmTypeName", "Severity", "OriginalRaiseTimestamp", "CurrentState", "Category", "Description", "LastTransitionTimestamp", "OperatorUser", "OperatorComment", "CurrentValue", "LimitValue" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "SessionId", "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest), global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest.Parser, new[]{ "SessionId", "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest), global::MxGateway.Contracts.Proto.AcknowledgeAlarmRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFullReference", "Comment", "OperatorUser" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply), global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser, new[]{ "CorrelationId", "ProtocolStatus", "Hresult", "Status", "DiagnosticMessage" }, new[]{ "Hresult" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamAlarmsRequest), global::MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser, new[]{ "ClientCorrelationId", "AlarmFilterPrefix" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.AlarmFeedMessage), global::MxGateway.Contracts.Proto.AlarmFeedMessage.Parser, new[]{ "ActiveAlarm", "SnapshotComplete", "Transition" }, new[]{ "Payload" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxStatusProxy), global::MxGateway.Contracts.Proto.MxStatusProxy.Parser, new[]{ "Success", "Category", "DetectedBy", "Detail", "RawCategory", "RawDetectedBy", "DiagnosticText" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxValue), global::MxGateway.Contracts.Proto.MxValue.Parser, new[]{ "DataType", "VariantType", "IsNull", "RawDiagnostic", "RawDataType", "BoolValue", "Int32Value", "Int64Value", "FloatValue", "DoubleValue", "StringValue", "TimestampValue", "ArrayValue", "RawValue" }, new[]{ "Kind" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.MxArray), global::MxGateway.Contracts.Proto.MxArray.Parser, new[]{ "ElementDataType", "VariantType", "Dimensions", "RawDiagnostic", "RawElementDataType", "BoolValues", "Int32Values", "Int64Values", "FloatValues", "DoubleValues", "StringValues", "TimestampValues", "RawValues" }, new[]{ "Values" }, null, null, null), @@ -25428,7 +25432,6 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public AcknowledgeAlarmRequest(AcknowledgeAlarmRequest other) : this() { - sessionId_ = other.sessionId_; clientCorrelationId_ = other.clientCorrelationId_; alarmFullReference_ = other.alarmFullReference_; comment_ = other.comment_; @@ -25442,18 +25445,6 @@ namespace MxGateway.Contracts.Proto { return new AcknowledgeAlarmRequest(this); } - /// Field number for the "session_id" field. - public const int SessionIdFieldNumber = 1; - private string sessionId_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string SessionId { - get { return sessionId_; } - set { - sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - /// Field number for the "client_correlation_id" field. public const int ClientCorrelationIdFieldNumber = 2; private string clientCorrelationId_ = ""; @@ -25527,7 +25518,6 @@ namespace MxGateway.Contracts.Proto { if (ReferenceEquals(other, this)) { return true; } - if (SessionId != other.SessionId) return false; if (ClientCorrelationId != other.ClientCorrelationId) return false; if (AlarmFullReference != other.AlarmFullReference) return false; if (Comment != other.Comment) return false; @@ -25539,7 +25529,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (SessionId.Length != 0) hash ^= SessionId.GetHashCode(); if (ClientCorrelationId.Length != 0) hash ^= ClientCorrelationId.GetHashCode(); if (AlarmFullReference.Length != 0) hash ^= AlarmFullReference.GetHashCode(); if (Comment.Length != 0) hash ^= Comment.GetHashCode(); @@ -25562,10 +25551,6 @@ namespace MxGateway.Contracts.Proto { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (ClientCorrelationId.Length != 0) { output.WriteRawTag(18); output.WriteString(ClientCorrelationId); @@ -25592,10 +25577,6 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (ClientCorrelationId.Length != 0) { output.WriteRawTag(18); output.WriteString(ClientCorrelationId); @@ -25622,9 +25603,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (SessionId.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId); - } if (ClientCorrelationId.Length != 0) { size += 1 + pb::CodedOutputStream.ComputeStringSize(ClientCorrelationId); } @@ -25649,9 +25627,6 @@ namespace MxGateway.Contracts.Proto { if (other == null) { return; } - if (other.SessionId.Length != 0) { - SessionId = other.SessionId; - } if (other.ClientCorrelationId.Length != 0) { ClientCorrelationId = other.ClientCorrelationId; } @@ -25683,10 +25658,6 @@ namespace MxGateway.Contracts.Proto { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; - case 10: { - SessionId = input.ReadString(); - break; - } case 18: { ClientCorrelationId = input.ReadString(); break; @@ -25722,10 +25693,6 @@ namespace MxGateway.Contracts.Proto { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; - case 10: { - SessionId = input.ReadString(); - break; - } case 18: { ClientCorrelationId = input.ReadString(); break; @@ -25786,7 +25753,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public AcknowledgeAlarmReply(AcknowledgeAlarmReply other) : this() { _hasBits0 = other._hasBits0; - sessionId_ = other.sessionId_; correlationId_ = other.correlationId_; protocolStatus_ = other.protocolStatus_ != null ? other.protocolStatus_.Clone() : null; hresult_ = other.hresult_; @@ -25801,18 +25767,6 @@ namespace MxGateway.Contracts.Proto { return new AcknowledgeAlarmReply(this); } - /// Field number for the "session_id" field. - public const int SessionIdFieldNumber = 1; - private string sessionId_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string SessionId { - get { return sessionId_; } - set { - sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } - } - /// Field number for the "correlation_id" field. public const int CorrelationIdFieldNumber = 2; private string correlationId_ = ""; @@ -25918,7 +25872,6 @@ namespace MxGateway.Contracts.Proto { if (ReferenceEquals(other, this)) { return true; } - if (SessionId != other.SessionId) return false; if (CorrelationId != other.CorrelationId) return false; if (!object.Equals(ProtocolStatus, other.ProtocolStatus)) return false; if (Hresult != other.Hresult) return false; @@ -25931,7 +25884,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (SessionId.Length != 0) hash ^= SessionId.GetHashCode(); if (CorrelationId.Length != 0) hash ^= CorrelationId.GetHashCode(); if (protocolStatus_ != null) hash ^= ProtocolStatus.GetHashCode(); if (HasHresult) hash ^= Hresult.GetHashCode(); @@ -25955,10 +25907,6 @@ namespace MxGateway.Contracts.Proto { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (CorrelationId.Length != 0) { output.WriteRawTag(18); output.WriteString(CorrelationId); @@ -25989,10 +25937,6 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (CorrelationId.Length != 0) { output.WriteRawTag(18); output.WriteString(CorrelationId); @@ -26023,9 +25967,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (SessionId.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId); - } if (CorrelationId.Length != 0) { size += 1 + pb::CodedOutputStream.ComputeStringSize(CorrelationId); } @@ -26053,9 +25994,6 @@ namespace MxGateway.Contracts.Proto { if (other == null) { return; } - if (other.SessionId.Length != 0) { - SessionId = other.SessionId; - } if (other.CorrelationId.Length != 0) { CorrelationId = other.CorrelationId; } @@ -26096,10 +26034,6 @@ namespace MxGateway.Contracts.Proto { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; - case 10: { - SessionId = input.ReadString(); - break; - } case 18: { CorrelationId = input.ReadString(); break; @@ -26145,10 +26079,6 @@ namespace MxGateway.Contracts.Proto { default: _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; - case 10: { - SessionId = input.ReadString(); - break; - } case 18: { CorrelationId = input.ReadString(); break; @@ -26182,17 +26112,20 @@ namespace MxGateway.Contracts.Proto { } + /// + /// Request to attach to the gateway's central alarm feed (StreamAlarms). + /// [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] - public sealed partial class QueryActiveAlarmsRequest : pb::IMessage + public sealed partial class StreamAlarmsRequest : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE , pb::IBufferMessage #endif { - private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new QueryActiveAlarmsRequest()); + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new StreamAlarmsRequest()); private pb::UnknownFieldSet _unknownFields; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public static pb::MessageParser Parser { get { return _parser; } } + public static pb::MessageParser Parser { get { return _parser; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -26208,7 +26141,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public QueryActiveAlarmsRequest() { + public StreamAlarmsRequest() { OnConstruction(); } @@ -26216,8 +26149,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public QueryActiveAlarmsRequest(QueryActiveAlarmsRequest other) : this() { - sessionId_ = other.sessionId_; + public StreamAlarmsRequest(StreamAlarmsRequest other) : this() { clientCorrelationId_ = other.clientCorrelationId_; alarmFilterPrefix_ = other.alarmFilterPrefix_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); @@ -26225,24 +26157,12 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public QueryActiveAlarmsRequest Clone() { - return new QueryActiveAlarmsRequest(this); - } - - /// Field number for the "session_id" field. - public const int SessionIdFieldNumber = 1; - private string sessionId_ = ""; - [global::System.Diagnostics.DebuggerNonUserCodeAttribute] - [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public string SessionId { - get { return sessionId_; } - set { - sessionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); - } + public StreamAlarmsRequest Clone() { + return new StreamAlarmsRequest(this); } /// Field number for the "client_correlation_id" field. - public const int ClientCorrelationIdFieldNumber = 2; + public const int ClientCorrelationIdFieldNumber = 1; private string clientCorrelationId_ = ""; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -26254,11 +26174,11 @@ namespace MxGateway.Contracts.Proto { } /// Field number for the "alarm_filter_prefix" field. - public const int AlarmFilterPrefixFieldNumber = 3; + public const int AlarmFilterPrefixFieldNumber = 2; private string alarmFilterPrefix_ = ""; /// - /// Optional alarm-reference prefix used to scope a partial ConditionRefresh - /// (e.g. equipment sub-tree). Empty means full refresh. + /// Optional alarm-reference prefix scoping the feed to an equipment + /// sub-tree. Empty streams every active alarm. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] @@ -26272,19 +26192,18 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { - return Equals(other as QueryActiveAlarmsRequest); + return Equals(other as StreamAlarmsRequest); } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public bool Equals(QueryActiveAlarmsRequest other) { + public bool Equals(StreamAlarmsRequest other) { if (ReferenceEquals(other, null)) { return false; } if (ReferenceEquals(other, this)) { return true; } - if (SessionId != other.SessionId) return false; if (ClientCorrelationId != other.ClientCorrelationId) return false; if (AlarmFilterPrefix != other.AlarmFilterPrefix) return false; return Equals(_unknownFields, other._unknownFields); @@ -26294,7 +26213,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (SessionId.Length != 0) hash ^= SessionId.GetHashCode(); if (ClientCorrelationId.Length != 0) hash ^= ClientCorrelationId.GetHashCode(); if (AlarmFilterPrefix.Length != 0) hash ^= AlarmFilterPrefix.GetHashCode(); if (_unknownFields != null) { @@ -26315,16 +26233,12 @@ namespace MxGateway.Contracts.Proto { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (ClientCorrelationId.Length != 0) { - output.WriteRawTag(18); + output.WriteRawTag(10); output.WriteString(ClientCorrelationId); } if (AlarmFilterPrefix.Length != 0) { - output.WriteRawTag(26); + output.WriteRawTag(18); output.WriteString(AlarmFilterPrefix); } if (_unknownFields != null) { @@ -26337,16 +26251,12 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (SessionId.Length != 0) { - output.WriteRawTag(10); - output.WriteString(SessionId); - } if (ClientCorrelationId.Length != 0) { - output.WriteRawTag(18); + output.WriteRawTag(10); output.WriteString(ClientCorrelationId); } if (AlarmFilterPrefix.Length != 0) { - output.WriteRawTag(26); + output.WriteRawTag(18); output.WriteString(AlarmFilterPrefix); } if (_unknownFields != null) { @@ -26359,9 +26269,6 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (SessionId.Length != 0) { - size += 1 + pb::CodedOutputStream.ComputeStringSize(SessionId); - } if (ClientCorrelationId.Length != 0) { size += 1 + pb::CodedOutputStream.ComputeStringSize(ClientCorrelationId); } @@ -26376,13 +26283,10 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public void MergeFrom(QueryActiveAlarmsRequest other) { + public void MergeFrom(StreamAlarmsRequest other) { if (other == null) { return; } - if (other.SessionId.Length != 0) { - SessionId = other.SessionId; - } if (other.ClientCorrelationId.Length != 0) { ClientCorrelationId = other.ClientCorrelationId; } @@ -26409,14 +26313,10 @@ namespace MxGateway.Contracts.Proto { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); break; case 10: { - SessionId = input.ReadString(); - break; - } - case 18: { ClientCorrelationId = input.ReadString(); break; } - case 26: { + case 18: { AlarmFilterPrefix = input.ReadString(); break; } @@ -26440,14 +26340,10 @@ namespace MxGateway.Contracts.Proto { _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); break; case 10: { - SessionId = input.ReadString(); - break; - } - case 18: { ClientCorrelationId = input.ReadString(); break; } - case 26: { + case 18: { AlarmFilterPrefix = input.ReadString(); break; } @@ -26458,6 +26354,369 @@ namespace MxGateway.Contracts.Proto { } + /// + /// One message on the StreamAlarms feed. The stream opens with one + /// `active_alarm` per currently-active alarm, then a single + /// `snapshot_complete`, then a `transition` for every subsequent change. + /// + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class AlarmFeedMessage : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new AlarmFeedMessage()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[80]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AlarmFeedMessage() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AlarmFeedMessage(AlarmFeedMessage other) : this() { + switch (other.PayloadCase) { + case PayloadOneofCase.ActiveAlarm: + ActiveAlarm = other.ActiveAlarm.Clone(); + break; + case PayloadOneofCase.SnapshotComplete: + SnapshotComplete = other.SnapshotComplete; + break; + case PayloadOneofCase.Transition: + Transition = other.Transition.Clone(); + break; + } + + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public AlarmFeedMessage Clone() { + return new AlarmFeedMessage(this); + } + + /// Field number for the "active_alarm" field. + public const int ActiveAlarmFieldNumber = 1; + /// + /// Part of the initial active-alarm snapshot (ConditionRefresh). + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot ActiveAlarm { + get { return payloadCase_ == PayloadOneofCase.ActiveAlarm ? (global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.ActiveAlarm; + } + } + + /// Field number for the "snapshot_complete" field. + public const int SnapshotCompleteFieldNumber = 2; + /// + /// Sentinel: the initial snapshot is fully delivered and `transition` + /// messages follow. Always true when present. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool SnapshotComplete { + get { return HasSnapshotComplete ? (bool) payload_ : false; } + set { + payload_ = value; + payloadCase_ = PayloadOneofCase.SnapshotComplete; + } + } + /// Gets whether the "snapshot_complete" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasSnapshotComplete { + get { return payloadCase_ == PayloadOneofCase.SnapshotComplete; } + } + /// Clears the value of the oneof if it's currently set to "snapshot_complete" + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearSnapshotComplete() { + if (HasSnapshotComplete) { + ClearPayload(); + } + } + + /// Field number for the "transition" field. + public const int TransitionFieldNumber = 3; + /// + /// A live alarm state change (raise / acknowledge / clear). + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent Transition { + get { return payloadCase_ == PayloadOneofCase.Transition ? (global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent) payload_ : null; } + set { + payload_ = value; + payloadCase_ = value == null ? PayloadOneofCase.None : PayloadOneofCase.Transition; + } + } + + private object payload_; + /// Enum of possible cases for the "payload" oneof. + public enum PayloadOneofCase { + None = 0, + ActiveAlarm = 1, + SnapshotComplete = 2, + Transition = 3, + } + private PayloadOneofCase payloadCase_ = PayloadOneofCase.None; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public PayloadOneofCase PayloadCase { + get { return payloadCase_; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearPayload() { + payloadCase_ = PayloadOneofCase.None; + payload_ = null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as AlarmFeedMessage); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(AlarmFeedMessage other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (!object.Equals(ActiveAlarm, other.ActiveAlarm)) return false; + if (SnapshotComplete != other.SnapshotComplete) return false; + if (!object.Equals(Transition, other.Transition)) return false; + if (PayloadCase != other.PayloadCase) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) hash ^= ActiveAlarm.GetHashCode(); + if (HasSnapshotComplete) hash ^= SnapshotComplete.GetHashCode(); + if (payloadCase_ == PayloadOneofCase.Transition) hash ^= Transition.GetHashCode(); + hash ^= (int) payloadCase_; + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) { + output.WriteRawTag(10); + output.WriteMessage(ActiveAlarm); + } + if (HasSnapshotComplete) { + output.WriteRawTag(16); + output.WriteBool(SnapshotComplete); + } + if (payloadCase_ == PayloadOneofCase.Transition) { + output.WriteRawTag(26); + output.WriteMessage(Transition); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) { + output.WriteRawTag(10); + output.WriteMessage(ActiveAlarm); + } + if (HasSnapshotComplete) { + output.WriteRawTag(16); + output.WriteBool(SnapshotComplete); + } + if (payloadCase_ == PayloadOneofCase.Transition) { + output.WriteRawTag(26); + output.WriteMessage(Transition); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(ActiveAlarm); + } + if (HasSnapshotComplete) { + size += 1 + 1; + } + if (payloadCase_ == PayloadOneofCase.Transition) { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Transition); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(AlarmFeedMessage other) { + if (other == null) { + return; + } + switch (other.PayloadCase) { + case PayloadOneofCase.ActiveAlarm: + if (ActiveAlarm == null) { + ActiveAlarm = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot(); + } + ActiveAlarm.MergeFrom(other.ActiveAlarm); + break; + case PayloadOneofCase.SnapshotComplete: + SnapshotComplete = other.SnapshotComplete; + break; + case PayloadOneofCase.Transition: + if (Transition == null) { + Transition = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent(); + } + Transition.MergeFrom(other.Transition); + break; + } + + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot subBuilder = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot(); + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) { + subBuilder.MergeFrom(ActiveAlarm); + } + input.ReadMessage(subBuilder); + ActiveAlarm = subBuilder; + break; + } + case 16: { + SnapshotComplete = input.ReadBool(); + break; + } + case 26: { + global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent subBuilder = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent(); + if (payloadCase_ == PayloadOneofCase.Transition) { + subBuilder.MergeFrom(Transition); + } + input.ReadMessage(subBuilder); + Transition = subBuilder; + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot subBuilder = new global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot(); + if (payloadCase_ == PayloadOneofCase.ActiveAlarm) { + subBuilder.MergeFrom(ActiveAlarm); + } + input.ReadMessage(subBuilder); + ActiveAlarm = subBuilder; + break; + } + case 16: { + SnapshotComplete = input.ReadBool(); + break; + } + case 26: { + global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent subBuilder = new global::MxGateway.Contracts.Proto.OnAlarmTransitionEvent(); + if (payloadCase_ == PayloadOneofCase.Transition) { + subBuilder.MergeFrom(Transition); + } + input.ReadMessage(subBuilder); + Transition = subBuilder; + break; + } + } + } + } + #endif + + } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class MxStatusProxy : pb::IMessage #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE @@ -26473,7 +26732,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[80]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[81]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -26904,7 +27163,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[81]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[82]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -27761,7 +28020,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[82]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[83]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -28549,7 +28808,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[83]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[84]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -28738,7 +28997,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[84]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[85]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -28927,7 +29186,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[85]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[86]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -29116,7 +29375,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[86]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -29305,7 +29564,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[87]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -29494,7 +29753,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[88]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -29681,7 +29940,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[89]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -29868,7 +30127,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[90]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] @@ -30055,7 +30314,7 @@ namespace MxGateway.Contracts.Proto { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pbr::MessageDescriptor Descriptor { - get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[91]; } + get { return global::MxGateway.Contracts.Proto.MxaccessGatewayReflection.Descriptor.MessageTypes[92]; } } [global::System.Diagnostics.DebuggerNonUserCodeAttribute] diff --git a/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs b/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs index 55606d7..349bfac 100644 --- a/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs +++ b/src/MxGateway.Contracts/Generated/MxaccessGatewayGrpc.cs @@ -69,9 +69,9 @@ namespace MxGateway.Contracts.Proto { [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AcknowledgeAlarmReply.Parser)); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest.Parser)); + static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.StreamAlarmsRequest.Parser)); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.ActiveAlarmSnapshot.Parser)); + static readonly grpc::Marshaller __Marshaller_mxaccess_gateway_v1_AlarmFeedMessage = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::MxGateway.Contracts.Proto.AlarmFeedMessage.Parser)); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] static readonly grpc::Method __Method_OpenSession = new grpc::Method( @@ -114,12 +114,12 @@ namespace MxGateway.Contracts.Proto { __Marshaller_mxaccess_gateway_v1_AcknowledgeAlarmReply); [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - static readonly grpc::Method __Method_QueryActiveAlarms = new grpc::Method( + static readonly grpc::Method __Method_StreamAlarms = new grpc::Method( grpc::MethodType.ServerStreaming, __ServiceName, - "QueryActiveAlarms", - __Marshaller_mxaccess_gateway_v1_QueryActiveAlarmsRequest, - __Marshaller_mxaccess_gateway_v1_ActiveAlarmSnapshot); + "StreamAlarms", + __Marshaller_mxaccess_gateway_v1_StreamAlarmsRequest, + __Marshaller_mxaccess_gateway_v1_AlarmFeedMessage); /// Service descriptor public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor @@ -161,8 +161,19 @@ namespace MxGateway.Contracts.Proto { throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); } + /// + /// Session-less central alarm feed. The stream opens with the current + /// active-alarm snapshot (one `active_alarm` per alarm), then a single + /// `snapshot_complete`, then a `transition` for every subsequent change. + /// Served by the gateway's always-on alarm monitor; any number of clients + /// fan out from the single monitor without opening a worker session. + /// + /// The request received from the client. + /// Used for sending responses back to the client. + /// The context of the server-side call handler being invoked. + /// A task indicating completion of the handler. [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - public virtual global::System.Threading.Tasks.Task QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::IServerStreamWriter responseStream, grpc::ServerCallContext context) + public virtual global::System.Threading.Tasks.Task StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::IServerStreamWriter responseStream, grpc::ServerCallContext context) { throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); } @@ -286,15 +297,37 @@ namespace MxGateway.Contracts.Proto { { return CallInvoker.AsyncUnaryCall(__Method_AcknowledgeAlarm, null, options, request); } + /// + /// Session-less central alarm feed. The stream opens with the current + /// active-alarm snapshot (one `active_alarm` per alarm), then a single + /// `snapshot_complete`, then a `transition` for every subsequent change. + /// Served by the gateway's always-on alarm monitor; any number of clients + /// fan out from the single monitor without opening a worker session. + /// + /// The request to send to the server. + /// The initial metadata to send with the call. This parameter is optional. + /// An optional deadline for the call. The call will be cancelled if deadline is hit. + /// An optional token for canceling the call. + /// The call object. [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - public virtual grpc::AsyncServerStreamingCall QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) + public virtual grpc::AsyncServerStreamingCall StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) { - return QueryActiveAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken)); + return StreamAlarms(request, new grpc::CallOptions(headers, deadline, cancellationToken)); } + /// + /// Session-less central alarm feed. The stream opens with the current + /// active-alarm snapshot (one `active_alarm` per alarm), then a single + /// `snapshot_complete`, then a `transition` for every subsequent change. + /// Served by the gateway's always-on alarm monitor; any number of clients + /// fan out from the single monitor without opening a worker session. + /// + /// The request to send to the server. + /// The options for the call. + /// The call object. [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] - public virtual grpc::AsyncServerStreamingCall QueryActiveAlarms(global::MxGateway.Contracts.Proto.QueryActiveAlarmsRequest request, grpc::CallOptions options) + public virtual grpc::AsyncServerStreamingCall StreamAlarms(global::MxGateway.Contracts.Proto.StreamAlarmsRequest request, grpc::CallOptions options) { - return CallInvoker.AsyncServerStreamingCall(__Method_QueryActiveAlarms, null, options, request); + return CallInvoker.AsyncServerStreamingCall(__Method_StreamAlarms, null, options, request); } /// Creates a new instance of client from given ClientBaseConfiguration. [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] @@ -315,7 +348,7 @@ namespace MxGateway.Contracts.Proto { .AddMethod(__Method_Invoke, serviceImpl.Invoke) .AddMethod(__Method_StreamEvents, serviceImpl.StreamEvents) .AddMethod(__Method_AcknowledgeAlarm, serviceImpl.AcknowledgeAlarm) - .AddMethod(__Method_QueryActiveAlarms, serviceImpl.QueryActiveAlarms).Build(); + .AddMethod(__Method_StreamAlarms, serviceImpl.StreamAlarms).Build(); } /// Register service method with a service binder with or without implementation. Useful when customizing the service binding logic. @@ -330,7 +363,7 @@ namespace MxGateway.Contracts.Proto { serviceBinder.AddMethod(__Method_Invoke, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.Invoke)); serviceBinder.AddMethod(__Method_StreamEvents, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.StreamEvents)); serviceBinder.AddMethod(__Method_AcknowledgeAlarm, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.AcknowledgeAlarm)); - serviceBinder.AddMethod(__Method_QueryActiveAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.QueryActiveAlarms)); + serviceBinder.AddMethod(__Method_StreamAlarms, serviceImpl == null ? null : new grpc::ServerStreamingServerMethod(serviceImpl.StreamAlarms)); } } diff --git a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto index 9a74078..4496acd 100644 --- a/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto +++ b/src/MxGateway.Contracts/Protos/mxaccess_gateway.proto @@ -11,8 +11,7 @@ import "google/protobuf/timestamp.proto"; // additively only. Never renumber or repurpose an existing field number or // enum value. When a field or enum value is removed, add a `reserved` range // (and `reserved` name) covering it in the same change so a future editor -// cannot accidentally reuse the retired tag. There are no `reserved` -// declarations today because no field or enum value has ever been removed. +// cannot accidentally reuse the retired tag. // Public client API for MXAccess sessions hosted by the gateway. service MxAccessGateway { @@ -21,7 +20,12 @@ service MxAccessGateway { rpc Invoke(MxCommandRequest) returns (MxCommandReply); rpc StreamEvents(StreamEventsRequest) returns (stream MxEvent); rpc AcknowledgeAlarm(AcknowledgeAlarmRequest) returns (AcknowledgeAlarmReply); - rpc QueryActiveAlarms(QueryActiveAlarmsRequest) returns (stream ActiveAlarmSnapshot); + // Session-less central alarm feed. The stream opens with the current + // active-alarm snapshot (one `active_alarm` per alarm), then a single + // `snapshot_complete`, then a `transition` for every subsequent change. + // Served by the gateway's always-on alarm monitor; any number of clients + // fan out from the single monitor without opening a worker session. + rpc StreamAlarms(StreamAlarmsRequest) returns (stream AlarmFeedMessage); } message OpenSessionRequest { @@ -785,7 +789,10 @@ enum AlarmConditionState { } message AcknowledgeAlarmRequest { - string session_id = 1; + // Retired: acknowledgement is session-less — it routes to the gateway's + // central alarm monitor, not a client worker session. + reserved 1; + reserved "session_id"; string client_correlation_id = 2; // Fully-qualified alarm reference matching OnAlarmTransitionEvent.alarm_full_reference. string alarm_full_reference = 3; @@ -797,7 +804,9 @@ message AcknowledgeAlarmRequest { } message AcknowledgeAlarmReply { - string session_id = 1; + // Retired: see AcknowledgeAlarmRequest — acknowledgement is session-less. + reserved 1; + reserved "session_id"; string correlation_id = 2; ProtocolStatus protocol_status = 3; // Native ack return code echoed from the worker. The worker carries the @@ -816,12 +825,27 @@ message AcknowledgeAlarmReply { string diagnostic_message = 6; } -message QueryActiveAlarmsRequest { - string session_id = 1; - string client_correlation_id = 2; - // Optional alarm-reference prefix used to scope a partial ConditionRefresh - // (e.g. equipment sub-tree). Empty means full refresh. - string alarm_filter_prefix = 3; +// Request to attach to the gateway's central alarm feed (StreamAlarms). +message StreamAlarmsRequest { + string client_correlation_id = 1; + // Optional alarm-reference prefix scoping the feed to an equipment + // sub-tree. Empty streams every active alarm. + string alarm_filter_prefix = 2; +} + +// One message on the StreamAlarms feed. The stream opens with one +// `active_alarm` per currently-active alarm, then a single +// `snapshot_complete`, then a `transition` for every subsequent change. +message AlarmFeedMessage { + oneof payload { + // Part of the initial active-alarm snapshot (ConditionRefresh). + ActiveAlarmSnapshot active_alarm = 1; + // Sentinel: the initial snapshot is fully delivered and `transition` + // messages follow. Always true when present. + bool snapshot_complete = 2; + // A live alarm state change (raise / acknowledge / clear). + OnAlarmTransitionEvent transition = 3; + } } message MxStatusProxy { diff --git a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs index b0b1311..686dd0a 100644 --- a/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs +++ b/src/MxGateway.IntegrationTests/WorkerLiveMxAccessSmokeTests.cs @@ -1084,7 +1084,11 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output) mapper, eventStreamService, _metrics, - _loggerFactory.CreateLogger()); + _loggerFactory.CreateLogger(), + new MxGateway.Server.Alarms.GatewayAlarmMonitor( + sessionManager, + options, + _loggerFactory.CreateLogger())); } /// diff --git a/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs b/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs new file mode 100644 index 0000000..b88b5c6 --- /dev/null +++ b/src/MxGateway.Server/Alarms/AlarmsServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +namespace MxGateway.Server.Alarms; + +/// Service-collection wiring for the gateway's central alarm monitor. +public static class AlarmsServiceCollectionExtensions +{ + /// + /// Registers the always-on as both + /// the singleton and a hosted + /// service, so it starts with the gateway host and is shared by the + /// gRPC alarm surface and the dashboard. + /// + /// Service collection to register services in. + /// The service collection for chaining. + public static IServiceCollection AddGatewayAlarms(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService()); + services.AddHostedService(provider => provider.GetRequiredService()); + + return services; + } +} diff --git a/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs b/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs new file mode 100644 index 0000000..fa334b4 --- /dev/null +++ b/src/MxGateway.Server/Alarms/GatewayAlarmMonitor.cs @@ -0,0 +1,693 @@ +using System.Threading.Channels; +using Microsoft.Extensions.Options; +using MxGateway.Contracts.Proto; +using MxGateway.Server.Configuration; +using MxGateway.Server.Sessions; + +namespace MxGateway.Server.Alarms; + +/// +/// The gateway's always-on alarm monitor and broker. It owns one +/// gateway-managed worker session dedicated to alarms, keeps an in-process +/// cache of the active-alarm set fed by that session's transition events +/// (reconciled periodically against the worker's snapshot), and fans the +/// feed out to any number of subscribers. +/// The session is re-opened transparently if the worker faults. +/// +public sealed class GatewayAlarmMonitor : BackgroundService, IGatewayAlarmService +{ + private const string MonitorClientName = "gateway-alarm-monitor"; + private const string BackendName = "Galaxy"; + private const int SubscriberQueueCapacity = 2048; + private static readonly TimeSpan RestartBackoff = TimeSpan.FromSeconds(5); + private static readonly TimeSpan StartupGrace = TimeSpan.FromSeconds(2); + + private readonly ISessionManager _sessionManager; + private readonly AlarmsOptions _options; + private readonly ILogger _logger; + + private readonly object _sync = new(); + private readonly Dictionary _alarms = new(StringComparer.Ordinal); + private readonly List _subscribers = []; + + private volatile GatewayAlarmMonitorState _state = GatewayAlarmMonitorState.Disabled; + private volatile string? _lastError; + private GatewaySession? _session; + + /// Initializes the gateway alarm monitor. + /// Gateway session manager. + /// Gateway options carrying the alarm configuration. + /// Diagnostic logger. + public GatewayAlarmMonitor( + ISessionManager sessionManager, + IOptions options, + ILogger logger) + { + _sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager)); + _options = (options ?? throw new ArgumentNullException(nameof(options))).Value.Alarms; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public GatewayAlarmMonitorState State => _state; + + /// + public string? LastError => _lastError; + + /// + public int? WorkerProcessId + { + get { lock (_sync) { return _session?.WorkerProcessId; } } + } + + /// + public IReadOnlyList CurrentAlarms + { + get + { + lock (_sync) + { + return _alarms.Values.Select(alarm => alarm.Clone()).ToArray(); + } + } + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!_options.Enabled) + { + _state = GatewayAlarmMonitorState.Disabled; + _logger.LogInformation("Gateway alarm monitor disabled (MxGateway:Alarms:Enabled is false)."); + return; + } + + string subscription = ResolveSubscription(); + if (string.IsNullOrWhiteSpace(subscription)) + { + _state = GatewayAlarmMonitorState.Faulted; + _lastError = "MxGateway:Alarms is enabled but no SubscriptionExpression / DefaultArea is configured."; + _logger.LogError("{Diagnostic}", _lastError); + return; + } + + // Brief grace so worker-process launching and startup orphan cleanup + // settle before the monitor opens its own session. + try + { + await Task.Delay(StartupGrace, stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await RunMonitorAsync(subscription, stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + break; + } + catch (Exception exception) + { + _state = GatewayAlarmMonitorState.Faulted; + _lastError = exception.Message; + _logger.LogWarning( + exception, + "Gateway alarm monitor lifecycle faulted; restarting in {Backoff}.", + RestartBackoff); + try + { + await Task.Delay(RestartBackoff, stoppingToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + } + } + + _state = GatewayAlarmMonitorState.Disabled; + } + + // One monitoring lifecycle: open a session, subscribe alarms, reconcile, + // then consume transition events until the session ends or is cancelled. + private async Task RunMonitorAsync(string subscription, CancellationToken stoppingToken) + { + _state = GatewayAlarmMonitorState.Starting; + GatewaySession session = await _sessionManager.OpenSessionAsync( + new SessionOpenRequest(BackendName, MonitorClientName, Guid.NewGuid().ToString("N"), CommandTimeout: null), + MonitorClientName, + stoppingToken) + .ConfigureAwait(false); + lock (_sync) { _session = session; } + + try + { + await SubscribeAlarmsAsync(session.SessionId, subscription, stoppingToken).ConfigureAwait(false); + await ReconcileAsync(session.SessionId, stoppingToken).ConfigureAwait(false); + + _state = GatewayAlarmMonitorState.Monitoring; + _lastError = null; + _logger.LogInformation( + "Gateway alarm monitor active on {Subscription} (session {SessionId}, worker pid {WorkerPid}).", + subscription, + session.SessionId, + session.WorkerProcessId); + + using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + Task reconcileLoop = ReconcileLoopAsync(session.SessionId, linked.Token); + try + { + await foreach (WorkerEvent workerEvent in _sessionManager + .ReadEventsAsync(session.SessionId, linked.Token) + .ConfigureAwait(false)) + { + MxEvent? mxEvent = workerEvent.Event; + if (mxEvent is { BodyCase: MxEvent.BodyOneofCase.OnAlarmTransition } + && mxEvent.OnAlarmTransition is not null) + { + ApplyTransition(mxEvent.OnAlarmTransition); + } + } + } + finally + { + await linked.CancelAsync().ConfigureAwait(false); + try + { + await reconcileLoop.ConfigureAwait(false); + } + catch + { + // Reconcile-loop teardown errors are not actionable here. + } + } + + // The event stream ended without cancellation — the worker session + // closed or faulted. Surface it so the supervisor loop restarts. + throw new InvalidOperationException("Alarm monitor worker event stream ended."); + } + finally + { + lock (_sync) { _session = null; } + ClearCache(); + try + { + await _sessionManager.CloseSessionAsync(session.SessionId, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception exception) + { + _logger.LogDebug(exception, "Closing alarm monitor session {SessionId} failed.", session.SessionId); + } + } + } + + private async Task SubscribeAlarmsAsync(string sessionId, string subscription, CancellationToken cancellationToken) + { + WorkerCommandReply reply = await _sessionManager.InvokeAsync( + sessionId, + new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.SubscribeAlarms, + SubscribeAlarms = new SubscribeAlarmsCommand { SubscriptionExpression = subscription }, + }, + }, + cancellationToken) + .ConfigureAwait(false); + + ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code; + if (code != ProtocolStatusCode.Ok) + { + string diagnostic = reply.Reply?.DiagnosticMessage + ?? reply.Reply?.ProtocolStatus?.Message + ?? $"status {code}"; + throw new InvalidOperationException($"Worker rejected SubscribeAlarms: {diagnostic}"); + } + } + + private async Task ReconcileLoopAsync(string sessionId, CancellationToken cancellationToken) + { + try + { + int seconds = Math.Max(5, _options.ReconcileIntervalSeconds); + using PeriodicTimer timer = new(TimeSpan.FromSeconds(seconds)); + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + try + { + await ReconcileAsync(sessionId, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + _logger.LogDebug(exception, "Alarm reconcile pass failed; keeping the current cache."); + } + } + } + catch (OperationCanceledException) + { + } + } + + private async Task ReconcileAsync(string sessionId, CancellationToken cancellationToken) + { + WorkerCommandReply reply = await _sessionManager.InvokeAsync( + sessionId, + new WorkerCommand + { + Command = new MxCommand + { + Kind = MxCommandKind.QueryActiveAlarms, + QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand { AlarmFilterPrefix = string.Empty }, + }, + }, + cancellationToken) + .ConfigureAwait(false); + + if (reply.Reply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok) + { + return; + } + + QueryActiveAlarmsReplyPayload? payload = reply.Reply.QueryActiveAlarms; + if (payload is not null) + { + ApplyReconcile(payload.Snapshots); + } + } + + // Applies a live transition to the cache and broadcasts it to subscribers. + private void ApplyTransition(OnAlarmTransitionEvent transition) + { + string reference = transition.AlarmFullReference ?? string.Empty; + if (reference.Length == 0) + { + return; + } + + lock (_sync) + { + if (transition.TransitionKind == AlarmTransitionKind.Clear) + { + _alarms.Remove(reference); + } + else + { + _alarms[reference] = SnapshotFromTransition(transition); + } + + Broadcast(new AlarmFeedMessage { Transition = transition }, reference); + } + } + + // Replaces the cache with the worker's authoritative snapshot, broadcasting + // a synthetic transition for any alarm the live stream missed. + private void ApplyReconcile(IEnumerable snapshots) + { + Dictionary next = new(StringComparer.Ordinal); + foreach (ActiveAlarmSnapshot snapshot in snapshots) + { + if (!string.IsNullOrEmpty(snapshot.AlarmFullReference)) + { + next[snapshot.AlarmFullReference] = snapshot; + } + } + + lock (_sync) + { + foreach (KeyValuePair existing in _alarms) + { + if (!next.ContainsKey(existing.Key)) + { + Broadcast( + new AlarmFeedMessage { Transition = TransitionFromSnapshot(existing.Value, AlarmTransitionKind.Clear) }, + existing.Key); + } + } + + foreach (KeyValuePair incoming in next) + { + if (!_alarms.ContainsKey(incoming.Key)) + { + Broadcast( + new AlarmFeedMessage { Transition = TransitionFromSnapshot(incoming.Value, AlarmTransitionKind.Raise) }, + incoming.Key); + } + } + + _alarms.Clear(); + foreach (KeyValuePair incoming in next) + { + _alarms[incoming.Key] = incoming.Value; + } + } + } + + // Caller holds _sync. Pushes a feed message to every matching subscriber; + // a subscriber that has fallen behind is completed with an error and dropped. + private void Broadcast(AlarmFeedMessage message, string reference) + { + for (int index = _subscribers.Count - 1; index >= 0; index--) + { + Subscriber subscriber = _subscribers[index]; + if (!subscriber.Matches(reference)) + { + continue; + } + + if (!subscriber.Channel.Writer.TryWrite(message)) + { + subscriber.Channel.Writer.TryComplete(new InvalidOperationException( + "Alarm feed subscriber fell behind and was dropped; reconnect to re-snapshot.")); + _subscribers.RemoveAt(index); + } + } + } + + private void ClearCache() + { + lock (_sync) + { + _alarms.Clear(); + } + } + + /// + public async IAsyncEnumerable StreamAsync( + string? alarmFilterPrefix, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + string prefix = alarmFilterPrefix ?? string.Empty; + Channel channel = Channel.CreateBounded( + new BoundedChannelOptions(SubscriberQueueCapacity) + { + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false, + }); + Subscriber subscriber = new(channel, prefix); + + ActiveAlarmSnapshot[] snapshot; + lock (_sync) + { + // Register before snapshotting under the same lock so no transition + // can slip between the snapshot and the live stream. + _subscribers.Add(subscriber); + snapshot = _alarms.Values + .Where(alarm => prefix.Length == 0 + || alarm.AlarmFullReference.StartsWith(prefix, StringComparison.Ordinal)) + .Select(alarm => alarm.Clone()) + .ToArray(); + } + + try + { + foreach (ActiveAlarmSnapshot alarm in snapshot) + { + yield return new AlarmFeedMessage { ActiveAlarm = alarm }; + } + + yield return new AlarmFeedMessage { SnapshotComplete = true }; + + await foreach (AlarmFeedMessage message in channel.Reader + .ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return message; + } + } + finally + { + lock (_sync) { _subscribers.Remove(subscriber); } + channel.Writer.TryComplete(); + } + } + + /// + public async Task AcknowledgeAsync( + AcknowledgeAlarmRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + string? sessionId; + lock (_sync) { sessionId = _session?.SessionId; } + if (sessionId is null || _state != GatewayAlarmMonitorState.Monitoring) + { + return new AcknowledgeAlarmReply + { + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.WorkerUnavailable, + Message = "Gateway alarm monitor is not currently active.", + }, + DiagnosticMessage = _lastError ?? "Alarm monitor is not running.", + }; + } + + MxCommand? command = BuildAcknowledgeCommand(request, out string? parseError); + if (command is null) + { + return new AcknowledgeAlarmReply + { + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.InvalidRequest, + Message = parseError ?? "Invalid acknowledge request.", + }, + DiagnosticMessage = parseError ?? "Invalid acknowledge request.", + }; + } + + WorkerCommandReply workerReply = await _sessionManager + .InvokeAsync(sessionId, new WorkerCommand { Command = command }, cancellationToken) + .ConfigureAwait(false); + + MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply + { + ProtocolStatus = new ProtocolStatus + { + Code = ProtocolStatusCode.ProtocolViolation, + Message = "Worker reply did not include an MxCommandReply.", + }, + }; + + AcknowledgeAlarmReply reply = new() + { + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = mxReply.ProtocolStatus ?? new ProtocolStatus { Code = ProtocolStatusCode.Ok }, + DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty, + }; + if (mxReply.HasHresult) + { + reply.Hresult = mxReply.Hresult; + } + + return reply; + } + + private string ResolveSubscription() + { + if (!string.IsNullOrWhiteSpace(_options.SubscriptionExpression)) + { + return _options.SubscriptionExpression; + } + + if (!string.IsNullOrWhiteSpace(_options.DefaultArea)) + { + return $@"\\{Environment.MachineName}\Galaxy!{_options.DefaultArea}"; + } + + return string.Empty; + } + + private static MxCommand? BuildAcknowledgeCommand(AcknowledgeAlarmRequest request, out string? parseError) + { + parseError = null; + if (string.IsNullOrWhiteSpace(request.AlarmFullReference)) + { + parseError = "alarm_full_reference is required."; + return null; + } + + string comment = request.Comment ?? string.Empty; + string operatorUser = request.OperatorUser ?? string.Empty; + + if (Guid.TryParse(request.AlarmFullReference, out Guid guid)) + { + return new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarm, + AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand + { + AlarmGuid = guid.ToString(), + Comment = comment, + OperatorUser = operatorUser, + OperatorNode = string.Empty, + OperatorDomain = string.Empty, + OperatorFullName = string.Empty, + }, + }; + } + + if (TryParseAlarmReference(request.AlarmFullReference, out string provider, out string group, out string alarm)) + { + return new MxCommand + { + Kind = MxCommandKind.AcknowledgeAlarmByName, + AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand + { + AlarmName = alarm, + ProviderName = provider, + GroupName = group, + Comment = comment, + OperatorUser = operatorUser, + OperatorNode = string.Empty, + OperatorDomain = string.Empty, + OperatorFullName = string.Empty, + }, + }; + } + + parseError = "alarm_full_reference must be a canonical GUID or 'Provider!Group.Tag' format."; + return null; + } + + /// + /// Parses an alarm reference of the form Provider!Group.Tag: the + /// first ! splits provider from Group.Tag; the first + /// . after the ! splits group from tag. + /// + /// The full alarm reference. + /// The parsed provider. + /// The parsed group/area. + /// The parsed tag/alarm name. + /// true on a well-formed reference; otherwise false. + public static bool TryParseAlarmReference( + string? reference, + out string providerName, + out string groupName, + out string alarmName) + { + providerName = string.Empty; + groupName = string.Empty; + alarmName = string.Empty; + if (string.IsNullOrWhiteSpace(reference)) + { + return false; + } + + int bang = reference!.IndexOf('!', StringComparison.Ordinal); + if (bang <= 0 || bang == reference.Length - 1) + { + return false; + } + + string left = reference[..bang]; + string right = reference[(bang + 1)..]; + int dot = right.IndexOf('.', StringComparison.Ordinal); + if (dot <= 0 || dot == right.Length - 1) + { + return false; + } + + providerName = left; + groupName = right[..dot]; + alarmName = right[(dot + 1)..]; + return true; + } + + private static ActiveAlarmSnapshot SnapshotFromTransition(OnAlarmTransitionEvent transition) + { + ActiveAlarmSnapshot snapshot = new() + { + AlarmFullReference = transition.AlarmFullReference, + SourceObjectReference = transition.SourceObjectReference, + AlarmTypeName = transition.AlarmTypeName, + Severity = transition.Severity, + CurrentState = transition.TransitionKind == AlarmTransitionKind.Acknowledge + ? AlarmConditionState.ActiveAcked + : AlarmConditionState.Active, + Category = transition.Category, + Description = transition.Description, + OperatorUser = transition.OperatorUser, + OperatorComment = transition.OperatorComment, + }; + if (transition.OriginalRaiseTimestamp is not null) + { + snapshot.OriginalRaiseTimestamp = transition.OriginalRaiseTimestamp; + } + if (transition.TransitionTimestamp is not null) + { + snapshot.LastTransitionTimestamp = transition.TransitionTimestamp; + } + if (transition.CurrentValue is not null) + { + snapshot.CurrentValue = transition.CurrentValue; + } + if (transition.LimitValue is not null) + { + snapshot.LimitValue = transition.LimitValue; + } + + return snapshot; + } + + private static OnAlarmTransitionEvent TransitionFromSnapshot( + ActiveAlarmSnapshot snapshot, + AlarmTransitionKind kind) + { + OnAlarmTransitionEvent transition = new() + { + AlarmFullReference = snapshot.AlarmFullReference, + SourceObjectReference = snapshot.SourceObjectReference, + AlarmTypeName = snapshot.AlarmTypeName, + TransitionKind = kind, + Severity = snapshot.Severity, + Category = snapshot.Category, + Description = snapshot.Description, + OperatorUser = snapshot.OperatorUser, + OperatorComment = snapshot.OperatorComment, + }; + if (snapshot.OriginalRaiseTimestamp is not null) + { + transition.OriginalRaiseTimestamp = snapshot.OriginalRaiseTimestamp; + } + if (snapshot.LastTransitionTimestamp is not null) + { + transition.TransitionTimestamp = snapshot.LastTransitionTimestamp; + } + if (snapshot.CurrentValue is not null) + { + transition.CurrentValue = snapshot.CurrentValue; + } + if (snapshot.LimitValue is not null) + { + transition.LimitValue = snapshot.LimitValue; + } + + return transition; + } + + private sealed class Subscriber(Channel channel, string prefix) + { + public Channel Channel { get; } = channel; + + public bool Matches(string reference) + { + return prefix.Length == 0 || reference.StartsWith(prefix, StringComparison.Ordinal); + } + } +} diff --git a/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs b/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs new file mode 100644 index 0000000..281d61b --- /dev/null +++ b/src/MxGateway.Server/Alarms/IGatewayAlarmService.cs @@ -0,0 +1,63 @@ +using MxGateway.Contracts.Proto; + +namespace MxGateway.Server.Alarms; + +/// Lifecycle state of the gateway's central alarm monitor. +public enum GatewayAlarmMonitorState +{ + /// Alarm monitoring is switched off (MxGateway:Alarms:Enabled is false). + Disabled, + + /// The monitor is opening or re-opening its worker session. + Starting, + + /// The monitor is connected and tracking the active-alarm set. + Monitoring, + + /// The monitor's last lifecycle attempt failed; a restart is pending. + Faulted, +} + +/// +/// The gateway's always-on alarm broker. A single gateway-owned worker +/// session monitors the AVEVA alarm provider; this service caches the +/// current active-alarm set and fans it out to any number of clients — +/// no client needs to open its own worker session to see alarms. +/// +public interface IGatewayAlarmService +{ + /// Current monitor lifecycle state. + GatewayAlarmMonitorState State { get; } + + /// Diagnostic message from the most recent fault, or null. + string? LastError { get; } + + /// Process id of the worker backing the monitor, when one is attached. + int? WorkerProcessId { get; } + + /// A point-in-time copy of the current active-alarm set. + IReadOnlyList CurrentAlarms { get; } + + /// + /// Attaches to the central alarm feed. The returned stream yields one + /// per currently-active alarm, then a + /// single snapshot_complete sentinel, then a transition + /// for every subsequent change. + /// + /// Optional alarm-reference prefix scoping the feed. + /// Token that ends the subscription. + IAsyncEnumerable StreamAsync( + string? alarmFilterPrefix, + CancellationToken cancellationToken); + + /// + /// Acknowledges an alarm through the monitor's worker session. Never + /// throws — transport and monitor-state failures surface in the + /// reply's . + /// + /// The acknowledge request. + /// Token to cancel the call. + Task AcknowledgeAsync( + AcknowledgeAlarmRequest request, + CancellationToken cancellationToken); +} diff --git a/src/MxGateway.Server/Configuration/AlarmsOptions.cs b/src/MxGateway.Server/Configuration/AlarmsOptions.cs index 2722fac..dd47d78 100644 --- a/src/MxGateway.Server/Configuration/AlarmsOptions.cs +++ b/src/MxGateway.Server/Configuration/AlarmsOptions.cs @@ -1,32 +1,32 @@ namespace MxGateway.Server.Configuration; /// -/// Per-gateway alarm-subsystem configuration. Drives the auto-subscribe -/// hook in : when -/// is true and a session reaches Ready, the -/// manager issues a SubscribeAlarmsCommand to the worker with -/// the configured . +/// Configuration for the gateway's always-on central alarm monitor +/// (). When +/// is true the gateway opens one gateway-owned worker session dedicated to +/// alarms, caches the active-alarm set, and fans it out to every client +/// through the StreamAlarms RPC — no client opens its own session +/// to see alarms. /// /// -/// Defaults preserve current behaviour (alarms disabled). Operators -/// opt in by setting MxGateway:Alarms:Enabled = true and -/// supplying a canonical -/// \\<machine>\Galaxy!<area> subscription -/// expression. The literal "Galaxy" provider is correct regardless of -/// the configured Galaxy database name (the wnwrap consumer doesn't -/// accept the database name as the provider). +/// Defaults preserve current behaviour (alarm monitoring disabled). +/// Operators opt in by setting MxGateway:Alarms:Enabled = true and +/// supplying a canonical \\<machine>\Galaxy!<area> +/// subscription expression. The literal "Galaxy" provider is correct +/// regardless of the configured Galaxy database name (the wnwrap consumer +/// does not accept the database name as the provider). /// public sealed class AlarmsOptions { - /// Gate the auto-subscribe hook on session open. Default false. + /// Gate the gateway's always-on central alarm monitor. Default false. public bool Enabled { get; init; } /// - /// AVEVA alarm-subscription expression. When empty and - /// is true, the gateway falls back to - /// \\$(MachineName)\Galaxy!$(DefaultArea) if - /// is set; otherwise the session open - /// fails with a configuration diagnostic. + /// AVEVA alarm-subscription expression the monitor subscribes on + /// startup. When empty and is true, the gateway + /// falls back to \\$(MachineName)\Galaxy!$(DefaultArea) if + /// is set; otherwise the monitor faults with + /// a configuration diagnostic. /// public string SubscriptionExpression { get; init; } = string.Empty; @@ -39,10 +39,10 @@ public sealed class AlarmsOptions public string DefaultArea { get; init; } = string.Empty; /// - /// If true, an auto-subscribe failure faults the session. If false - /// (default), the failure is logged and the session remains Ready — - /// alarm-side commands return "not subscribed" but data subscriptions - /// work normally. + /// How often the monitor reconciles its in-process alarm cache against + /// the worker's authoritative active-alarm snapshot, catching any + /// transitions the live poll-and-diff feed missed. Default 30 seconds; + /// the monitor floors it at 5 seconds. /// - public bool RequireSubscribeOnOpen { get; init; } + public int ReconcileIntervalSeconds { get; init; } = 30; } diff --git a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs index 3421607..77a0288 100644 --- a/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs +++ b/src/MxGateway.Server/Configuration/GatewayOptionsValidator.cs @@ -236,11 +236,10 @@ public sealed class GatewayOptionsValidator : IValidateOptions return; } - // When the alarm auto-subscribe hook is enabled, the gateway needs either a - // canonical SubscriptionExpression or a DefaultArea to compose one from. Both - // empty is the configuration mistake SessionManager.TryAutoSubscribeAlarmsAsync - // currently surfaces per-session — pulling it up to startup validation makes - // the misconfiguration fail-fast at boot, in line with every other section. + // When the central alarm monitor is enabled, it needs either a canonical + // SubscriptionExpression or a DefaultArea to compose one from. Validating + // it at startup makes the misconfiguration fail-fast at boot, in line + // with every other section. if (string.IsNullOrWhiteSpace(options.SubscriptionExpression) && string.IsNullOrWhiteSpace(options.DefaultArea)) { diff --git a/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs b/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs index 6efe838..a43e773 100644 --- a/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs +++ b/src/MxGateway.Server/Dashboard/DashboardLiveDataService.cs @@ -1,4 +1,5 @@ using MxGateway.Contracts.Proto; +using MxGateway.Server.Alarms; using MxGateway.Server.Sessions; namespace MxGateway.Server.Dashboard; @@ -17,7 +18,7 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5); private readonly ISessionManager _sessionManager; - private readonly IAlarmRpcDispatcher _alarmDispatcher; + private readonly IGatewayAlarmService _alarmService; private readonly ILogger _logger; private readonly SemaphoreSlim _gate = new(1, 1); private readonly HashSet _subscribed = new(StringComparer.OrdinalIgnoreCase); @@ -28,15 +29,15 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync /// Initializes the live-data service. /// Gateway session manager. - /// Active-alarm query dispatcher. + /// Gateway central alarm service. /// Diagnostic logger. public DashboardLiveDataService( ISessionManager sessionManager, - IAlarmRpcDispatcher alarmDispatcher, + IGatewayAlarmService alarmService, ILogger logger) { _sessionManager = sessionManager ?? throw new ArgumentNullException(nameof(sessionManager)); - _alarmDispatcher = alarmDispatcher ?? throw new ArgumentNullException(nameof(alarmDispatcher)); + _alarmService = alarmService ?? throw new ArgumentNullException(nameof(alarmService)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -90,39 +91,20 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync } /// - public async Task QueryAlarmsAsync(CancellationToken cancellationToken) + public Task QueryAlarmsAsync(CancellationToken cancellationToken) { - await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - (GatewaySession session, _) = await EnsureReadyAsync(cancellationToken).ConfigureAwait(false); + // Alarms come from the gateway's always-on central monitor; the + // dashboard reads its in-process cache directly — no session needed. + DashboardActiveAlarm[] alarms = _alarmService.CurrentAlarms + .Select(DashboardActiveAlarm.FromSnapshot) + .ToArray(); - QueryActiveAlarmsRequest request = new() - { - SessionId = session.SessionId, - ClientCorrelationId = Guid.NewGuid().ToString("N"), - }; + string? error = _alarmService.State is GatewayAlarmMonitorState.Monitoring + or GatewayAlarmMonitorState.Disabled + ? null + : _alarmService.LastError ?? $"Alarm monitor is {_alarmService.State}."; - List alarms = []; - await foreach (ActiveAlarmSnapshot snapshot in _alarmDispatcher - .QueryActiveAlarmsAsync(request, cancellationToken) - .ConfigureAwait(false)) - { - alarms.Add(DashboardActiveAlarm.FromSnapshot(snapshot)); - } - - return new DashboardAlarmQueryResult(alarms, null, session.WorkerProcessId); - } - catch (Exception exception) when (exception is not OperationCanceledException) - { - InvalidateSession(); - _logger.LogWarning(exception, "Dashboard alarm query failed; the dashboard session will be re-opened."); - return new DashboardAlarmQueryResult([], exception.Message, null); - } - finally - { - _gate.Release(); - } + return Task.FromResult(new DashboardAlarmQueryResult(alarms, error, _alarmService.WorkerProcessId)); } // Returns a Ready session + its Register server handle, opening a fresh diff --git a/src/MxGateway.Server/GatewayApplication.cs b/src/MxGateway.Server/GatewayApplication.cs index bb19b90..c450065 100644 --- a/src/MxGateway.Server/GatewayApplication.cs +++ b/src/MxGateway.Server/GatewayApplication.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting.StaticWebAssets; using MxGateway.Contracts; +using MxGateway.Server.Alarms; using MxGateway.Server.Configuration; using MxGateway.Server.Dashboard; using MxGateway.Server.Diagnostics; @@ -64,6 +65,7 @@ public static class GatewayApplication builder.Services.AddSingleton(); builder.Services.AddWorkerProcessLauncher(); builder.Services.AddGatewaySessions(); + builder.Services.AddGatewayAlarms(); builder.Services.AddGatewayDashboard(); builder.Services.AddGalaxyRepository(); diff --git a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs index c59bde7..e143881 100644 --- a/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs +++ b/src/MxGateway.Server/Grpc/MxAccessGatewayService.cs @@ -3,6 +3,7 @@ using Grpc.Core; using Google.Protobuf.WellKnownTypes; using MxGateway.Contracts; using MxGateway.Contracts.Proto; +using MxGateway.Server.Alarms; using MxGateway.Server.Metrics; using MxGateway.Server.Security.Authentication; using MxGateway.Server.Security.Authorization; @@ -21,9 +22,8 @@ public sealed class MxAccessGatewayService( IEventStreamService eventStreamService, GatewayMetrics metrics, ILogger logger, - IAlarmRpcDispatcher? alarmRpcDispatcher = null) : MxAccessGateway.MxAccessGatewayBase + IGatewayAlarmService alarmService) : MxAccessGateway.MxAccessGatewayBase { - private readonly IAlarmRpcDispatcher alarmRpcDispatcher = alarmRpcDispatcher ?? new NotWiredAlarmRpcDispatcher(); /// public override async Task OpenSession( OpenSessionRequest request, @@ -163,14 +163,13 @@ public sealed class MxAccessGatewayService( /// /// - /// Surfaces the public AcknowledgeAlarm RPC. The gateway validates the request, - /// resolves the session, and delegates to the registered - /// . DI binds the production - /// , which routes - /// the ack through the worker pipe IPC: an alarm_full_reference that parses - /// as a canonical GUID forwards to AcknowledgeAlarmCommand; a - /// Provider!Group.Tag reference forwards to AcknowledgeAlarmByNameCommand; - /// anything else returns an InvalidRequest diagnostic. + /// Surfaces the public AcknowledgeAlarm RPC. Acknowledgement is + /// session-less: the gateway routes it through the always-on + /// monitor session. An + /// alarm_full_reference that parses as a canonical GUID forwards + /// to AcknowledgeAlarmCommand; a Provider!Group.Tag + /// reference forwards to AcknowledgeAlarmByNameCommand; anything + /// else returns an InvalidRequest diagnostic in the reply. /// public override async Task AcknowledgeAlarm( AcknowledgeAlarmRequest request, @@ -179,25 +178,12 @@ public sealed class MxAccessGatewayService( try { ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrEmpty(request.SessionId)) - { - throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required.")); - } if (string.IsNullOrEmpty(request.AlarmFullReference)) { throw new RpcException(new Status(StatusCode.InvalidArgument, "alarm_full_reference is required.")); } - // Validate the session exists. Throws SessionManagerException → mapped to - // gRPC NotFound by the caller's MapException. - _ = ResolveSession(request.SessionId); - - // Delegate to the registered alarm dispatcher. DI binds the production - // WorkerAlarmRpcDispatcher, which routes the ack over the worker IPC by - // GUID (AcknowledgeAlarmCommand) or by Provider!Group.Tag reference - // (AcknowledgeAlarmByNameCommand). NotWiredAlarmRpcDispatcher is only the - // null fallback used when no dispatcher is registered. - return await alarmRpcDispatcher.AcknowledgeAsync(request, context.CancellationToken) + return await alarmService.AcknowledgeAsync(request, context.CancellationToken) .ConfigureAwait(false); } catch (Exception exception) when (exception is not RpcException) @@ -208,38 +194,27 @@ public sealed class MxAccessGatewayService( /// /// - /// Surfaces the public QueryActiveAlarms RPC. The gateway validates the request, - /// resolves the session, and delegates to the registered - /// . DI binds the production - /// , which issues a - /// QueryActiveAlarmsCommand over the worker pipe IPC and streams each - /// ActiveAlarmSnapshot from the worker reply. + /// Surfaces the public StreamAlarms RPC — the session-less central + /// alarm feed. The stream opens with one active_alarm per + /// currently-active alarm, then a single snapshot_complete, then + /// a transition for every subsequent change. Served by the + /// gateway's always-on monitor; any + /// number of clients fan out from the single monitor. /// - public override async Task QueryActiveAlarms( - QueryActiveAlarmsRequest request, - IServerStreamWriter responseStream, + public override async Task StreamAlarms( + StreamAlarmsRequest request, + IServerStreamWriter responseStream, ServerCallContext context) { try { ArgumentNullException.ThrowIfNull(request); - if (string.IsNullOrEmpty(request.SessionId)) - { - throw new RpcException(new Status(StatusCode.InvalidArgument, "session_id is required.")); - } - _ = ResolveSession(request.SessionId); - - // Delegate to the registered alarm dispatcher. DI binds the production - // WorkerAlarmRpcDispatcher, which issues a QueryActiveAlarmsCommand over the - // worker IPC and streams each ActiveAlarmSnapshot from the worker reply. - // NotWiredAlarmRpcDispatcher is only the null fallback used when no - // dispatcher is registered. - await foreach (ActiveAlarmSnapshot snapshot in alarmRpcDispatcher - .QueryActiveAlarmsAsync(request, context.CancellationToken) + await foreach (AlarmFeedMessage message in alarmService + .StreamAsync(request.AlarmFilterPrefix, context.CancellationToken) .WithCancellation(context.CancellationToken) .ConfigureAwait(false)) { - await responseStream.WriteAsync(snapshot).ConfigureAwait(false); + await responseStream.WriteAsync(message).ConfigureAwait(false); } } catch (Exception exception) when (exception is not RpcException) diff --git a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs index bd9428a..d134065 100644 --- a/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs +++ b/src/MxGateway.Server/Security/Authorization/GatewayGrpcScopeResolver.cs @@ -19,7 +19,7 @@ public sealed class GatewayGrpcScopeResolver StreamEventsRequest => GatewayScopes.EventsRead, MxCommandRequest commandRequest => ResolveCommandScope(commandRequest.Command?.Kind ?? MxCommandKind.Unspecified), AcknowledgeAlarmRequest => GatewayScopes.InvokeWrite, - QueryActiveAlarmsRequest => GatewayScopes.EventsRead, + StreamAlarmsRequest => GatewayScopes.EventsRead, TestConnectionRequest or GetLastDeployTimeRequest or DiscoverHierarchyRequest or diff --git a/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs deleted file mode 100644 index 41170f3..0000000 --- a/src/MxGateway.Server/Sessions/IAlarmRpcDispatcher.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using MxGateway.Contracts.Proto; - -namespace MxGateway.Server.Sessions; - -/// -/// Gateway-side dispatcher seam for the alarm-RPC surface. Bridges the -/// public AcknowledgeAlarm + QueryActiveAlarms gRPC handlers -/// to the worker process that hosts IMxAccessAlarmConsumer. -/// -/// -/// -/// DI binds the production by -/// default; it routes calls through the existing worker-pipe IPC. -/// NotWiredAlarmRpcDispatcher is only the null fallback used -/// when no dispatcher is registered (DI omission / standalone tests). -/// Other tests inject a fake to exercise the gateway handler shape -/// without spinning up a worker process. -/// -/// -/// The dispatcher is session-scoped: every call resolves the -/// session and forwards to that session's worker. The handler -/// constructs the / -/// stream from the dispatcher's -/// output without further translation. -/// -/// -public interface IAlarmRpcDispatcher -{ - /// Forward an Acknowledge to the worker that owns the session. - Task AcknowledgeAsync( - AcknowledgeAlarmRequest request, - CancellationToken cancellationToken); - - /// Walk active alarms on the worker that owns the session. - IAsyncEnumerable QueryActiveAlarmsAsync( - QueryActiveAlarmsRequest request, - CancellationToken cancellationToken); -} diff --git a/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs deleted file mode 100644 index 2e5306d..0000000 --- a/src/MxGateway.Server/Sessions/NotWiredAlarmRpcDispatcher.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using MxGateway.Contracts.Proto; -using MxGateway.Server.Grpc; - -namespace MxGateway.Server.Sessions; - -/// -/// Null fallback used when no dispatcher -/// is registered in the DI container (DI omission or standalone tests). -/// Acknowledges with a structured "alarm dispatcher not registered" -/// diagnostic and yields an empty active-alarm stream. -/// -/// -/// -/// Production wires as the -/// default via -/// SessionServiceCollectionExtensions.AddGatewaySessions, so -/// clients that hit this fallback are running against an -/// intentionally minimal service composition rather than the full -/// gateway. -/// -/// -public sealed class NotWiredAlarmRpcDispatcher : IAlarmRpcDispatcher -{ - /// - public Task AcknowledgeAsync( - AcknowledgeAlarmRequest request, - CancellationToken cancellationToken) - { - return Task.FromResult(new AcknowledgeAlarmReply - { - SessionId = request.SessionId, - CorrelationId = request.ClientCorrelationId, - ProtocolStatus = MxAccessGrpcMapper.Ok("AcknowledgeAlarm accepted; alarm dispatcher is not registered."), - DiagnosticMessage = "Alarm dispatcher is not registered.", - }); - } - - /// -#pragma warning disable CS1998 // Async method lacks 'await' operators — empty stream is intentional. - public async IAsyncEnumerable QueryActiveAlarmsAsync( - QueryActiveAlarmsRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - yield break; - } -#pragma warning restore CS1998 -} diff --git a/src/MxGateway.Server/Sessions/SessionManager.cs b/src/MxGateway.Server/Sessions/SessionManager.cs index 488c2f8..4a0ee21 100644 --- a/src/MxGateway.Server/Sessions/SessionManager.cs +++ b/src/MxGateway.Server/Sessions/SessionManager.cs @@ -90,8 +90,6 @@ public sealed class SessionManager : ISessionManager _metrics.SessionOpened(); sessionOpenedRecorded = true; - await TryAutoSubscribeAlarmsAsync(session, cancellationToken).ConfigureAwait(false); - return session; } catch (Exception exception) @@ -410,100 +408,4 @@ public sealed class SessionManager : ISessionManager return Convert.ToBase64String(bytes); } - /// - /// If Alarms.Enabled is configured, issue a - /// SubscribeAlarmsCommand on the freshly-Ready session so the - /// worker's wnwrap consumer starts polling. Failure handling is - /// governed by Alarms.RequireSubscribeOnOpen: - /// - /// true — propagate the failure to fault the session. - /// false (default) — log a warning and let the session continue serving data subscriptions. - /// - /// - private async Task TryAutoSubscribeAlarmsAsync( - GatewaySession session, - CancellationToken cancellationToken) - { - AlarmsOptions alarms = _options.Alarms; - if (!alarms.Enabled) return; - - string subscription = ResolveAlarmSubscription(alarms); - if (string.IsNullOrWhiteSpace(subscription)) - { - const string diagnostic = - "Alarms.Enabled is true but no SubscriptionExpression / DefaultArea is configured."; - if (alarms.RequireSubscribeOnOpen) - { - throw new SessionManagerException( - SessionManagerErrorCode.OpenFailed, diagnostic); - } - _logger.LogWarning( - "Auto-subscribe skipped for session {SessionId}: {Diagnostic}", - session.SessionId, diagnostic); - return; - } - - WorkerCommand command = new WorkerCommand - { - Command = new MxCommand - { - Kind = MxCommandKind.SubscribeAlarms, - SubscribeAlarms = new SubscribeAlarmsCommand - { - SubscriptionExpression = subscription, - }, - }, - EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()), - }; - - try - { - WorkerCommandReply reply = await session.InvokeAsync(command, cancellationToken) - .ConfigureAwait(false); - ProtocolStatusCode? code = reply.Reply?.ProtocolStatus?.Code; - if (code != ProtocolStatusCode.Ok) - { - string diagnostic = reply.Reply?.DiagnosticMessage - ?? reply.Reply?.ProtocolStatus?.Message - ?? "Worker rejected SubscribeAlarms."; - if (alarms.RequireSubscribeOnOpen) - { - throw new SessionManagerException( - SessionManagerErrorCode.OpenFailed, - $"Auto-subscribe failed for session {session.SessionId}: {diagnostic}"); - } - _logger.LogWarning( - "Auto-subscribe failed for session {SessionId} (status {StatusCode}): {Diagnostic}", - session.SessionId, code, diagnostic); - return; - } - _logger.LogInformation( - "Alarm auto-subscribe succeeded for session {SessionId} on {Subscription}.", - session.SessionId, subscription); - } - catch (SessionManagerException) - { - throw; - } - catch (Exception ex) when (!alarms.RequireSubscribeOnOpen) - { - _logger.LogWarning( - ex, - "Auto-subscribe threw for session {SessionId} on {Subscription}; alarm path remains inactive.", - session.SessionId, subscription); - } - } - - private static string ResolveAlarmSubscription(AlarmsOptions alarms) - { - if (!string.IsNullOrWhiteSpace(alarms.SubscriptionExpression)) - { - return alarms.SubscriptionExpression; - } - if (!string.IsNullOrWhiteSpace(alarms.DefaultArea)) - { - return $@"\\{Environment.MachineName}\Galaxy!{alarms.DefaultArea}"; - } - return string.Empty; - } } diff --git a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs index 1f1e67b..5b7eae4 100644 --- a/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs +++ b/src/MxGateway.Server/Sessions/SessionServiceCollectionExtensions.cs @@ -11,7 +11,6 @@ public static class SessionServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); diff --git a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs b/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs deleted file mode 100644 index ef19f52..0000000 --- a/src/MxGateway.Server/Sessions/WorkerAlarmRpcDispatcher.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System.Runtime.CompilerServices; -using Google.Protobuf.WellKnownTypes; -using MxGateway.Contracts.Proto; -using MxGateway.Server.Grpc; - -namespace MxGateway.Server.Sessions; - -/// -/// Production that routes the public -/// AcknowledgeAlarm + QueryActiveAlarms RPCs through the -/// worker pipe IPC. DI binds this dispatcher; -/// is only the null fallback used when no dispatcher is registered. -/// -/// -/// -/// QueryActiveAlarms issues a -/// over the pipe and yields -/// each from the -/// . -/// -/// -/// AcknowledgeAlarm accepts either form of -/// : a canonical -/// GUID forwards as an ; a -/// Provider!Group.Tag reference is parsed by -/// and forwarded as an -/// . Any other reference -/// returns an InvalidRequest diagnostic. -/// -/// -public sealed class WorkerAlarmRpcDispatcher( - ISessionRegistry sessionRegistry, - TimeProvider? timeProvider = null) : IAlarmRpcDispatcher -{ - private readonly ISessionRegistry sessionRegistry = sessionRegistry - ?? throw new ArgumentNullException(nameof(sessionRegistry)); - private readonly TimeProvider timeProvider = timeProvider ?? TimeProvider.System; - - /// - /// Parse a full alarm reference of the form Provider!Group.Tag - /// into its components. Convention: the first ! separates - /// provider from Group.Tag; the first . after the - /// ! separates group from tag (the tag itself may contain - /// more dots — e.g. TestMachine_001.TestAlarm001). - /// - /// true on a well-formed reference; false otherwise. - public static bool TryParseAlarmReference( - string? reference, - out string providerName, - out string groupName, - out string alarmName) - { - providerName = string.Empty; - groupName = string.Empty; - alarmName = string.Empty; - if (string.IsNullOrWhiteSpace(reference)) return false; - - int bang = reference!.IndexOf('!'); - if (bang <= 0 || bang == reference.Length - 1) return false; - - string left = reference[..bang]; - string right = reference[(bang + 1)..]; - int dot = right.IndexOf('.'); - if (dot <= 0 || dot == right.Length - 1) return false; - - providerName = left; - groupName = right[..dot]; - alarmName = right[(dot + 1)..]; - return true; - } - - /// - public async Task AcknowledgeAsync( - AcknowledgeAlarmRequest request, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(request); - - if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession? session) || session is null) - { - return new AcknowledgeAlarmReply - { - SessionId = request.SessionId, - CorrelationId = request.ClientCorrelationId, - ProtocolStatus = MxAccessGrpcMapper.SessionNotFound( - $"Session '{request.SessionId}' not found."), - DiagnosticMessage = "AcknowledgeAlarm: session not found.", - }; - } - - WorkerCommand workerCommand; - if (Guid.TryParse(request.AlarmFullReference, out Guid guid)) - { - workerCommand = new WorkerCommand - { - Command = new MxCommand - { - Kind = MxCommandKind.AcknowledgeAlarm, - AcknowledgeAlarmCommand = new AcknowledgeAlarmCommand - { - AlarmGuid = guid.ToString(), - Comment = request.Comment ?? string.Empty, - OperatorUser = request.OperatorUser ?? string.Empty, - // Operator node/domain/full-name are not on the public - // RPC surface today; pass empty strings so the worker - // honours the existing AcknowledgeAlarmCommand schema. - OperatorNode = string.Empty, - OperatorDomain = string.Empty, - OperatorFullName = string.Empty, - }, - }, - EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), - }; - } - else if (TryParseAlarmReference( - request.AlarmFullReference, - out string providerName, - out string groupName, - out string alarmName)) - { - workerCommand = new WorkerCommand - { - Command = new MxCommand - { - Kind = MxCommandKind.AcknowledgeAlarmByName, - AcknowledgeAlarmByNameCommand = new AcknowledgeAlarmByNameCommand - { - AlarmName = alarmName, - ProviderName = providerName, - GroupName = groupName, - Comment = request.Comment ?? string.Empty, - OperatorUser = request.OperatorUser ?? string.Empty, - OperatorNode = string.Empty, - OperatorDomain = string.Empty, - OperatorFullName = string.Empty, - }, - }, - EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), - }; - } - else - { - return new AcknowledgeAlarmReply - { - SessionId = request.SessionId, - CorrelationId = request.ClientCorrelationId, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.InvalidRequest, - Message = "AlarmFullReference must be a canonical GUID or 'Provider!Group.Tag' format.", - }, - DiagnosticMessage = $"AcknowledgeAlarm received unrecognized reference '{request.AlarmFullReference}'.", - }; - } - - WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken) - .ConfigureAwait(false); - - MxCommandReply mxReply = workerReply.Reply ?? new MxCommandReply - { - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.ProtocolViolation, - Message = "Worker reply did not include an MxCommandReply.", - }, - }; - - AcknowledgeAlarmReply reply = new AcknowledgeAlarmReply - { - SessionId = request.SessionId, - CorrelationId = request.ClientCorrelationId, - ProtocolStatus = mxReply.ProtocolStatus ?? MxAccessGrpcMapper.Ok(), - DiagnosticMessage = mxReply.DiagnosticMessage ?? string.Empty, - }; - if (mxReply.HasHresult) - { - reply.Hresult = mxReply.Hresult; - } - return reply; - } - - /// - public async IAsyncEnumerable QueryActiveAlarmsAsync( - QueryActiveAlarmsRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(request); - - if (!sessionRegistry.TryGet(request.SessionId, out GatewaySession? session) || session is null) - { - // Server-019: align with AcknowledgeAsync's missing-session handling and - // surface a SessionNotFound error rather than yielding an empty stream. - // QueryActiveAlarms is server-streaming, so a thrown exception is the - // cleaner fit than an in-band ProtocolStatus; MxAccessGatewayService maps - // SessionManagerException(SessionNotFound) to gRPC NotFound. - throw new SessionManagerException( - SessionManagerErrorCode.SessionNotFound, - $"Session '{request.SessionId}' not found."); - } - - WorkerCommand workerCommand = new WorkerCommand - { - Command = new MxCommand - { - Kind = MxCommandKind.QueryActiveAlarms, - QueryActiveAlarmsCommand = new QueryActiveAlarmsCommand - { - AlarmFilterPrefix = request.AlarmFilterPrefix ?? string.Empty, - }, - }, - EnqueueTimestamp = Timestamp.FromDateTimeOffset(timeProvider.GetUtcNow()), - }; - - WorkerCommandReply workerReply = await session.InvokeAsync(workerCommand, cancellationToken) - .ConfigureAwait(false); - - MxCommandReply? mxReply = workerReply.Reply; - if (mxReply?.ProtocolStatus?.Code != ProtocolStatusCode.Ok) yield break; - - QueryActiveAlarmsReplyPayload? payload = mxReply.QueryActiveAlarms; - if (payload is null) yield break; - - foreach (ActiveAlarmSnapshot snapshot in payload.Snapshots) - { - cancellationToken.ThrowIfCancellationRequested(); - yield return snapshot; - } - } -} diff --git a/src/MxGateway.Server/appsettings.json b/src/MxGateway.Server/appsettings.json index 2c5a91b..4d4da73 100644 --- a/src/MxGateway.Server/appsettings.json +++ b/src/MxGateway.Server/appsettings.json @@ -71,7 +71,7 @@ "Enabled": true, "SubscriptionExpression": "\\\\DESKTOP-6JL3KKO\\Galaxy!DEV", "DefaultArea": "", - "RequireSubscribeOnOpen": false + "ReconcileIntervalSeconds": 30 } } } diff --git a/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs b/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs index 0ad0edd..b36d59a 100644 --- a/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs +++ b/src/MxGateway.Tests/Contracts/ProtobufContractRoundTripTests.cs @@ -21,7 +21,7 @@ public sealed class ProtobufContractRoundTripTests Assert.Contains(service.Methods, method => method.Name == "Invoke"); Assert.Contains(service.Methods, method => method.Name == "StreamEvents"); Assert.Contains(service.Methods, method => method.Name == "AcknowledgeAlarm"); - Assert.Contains(service.Methods, method => method.Name == "QueryActiveAlarms"); + Assert.Contains(service.Methods, method => method.Name == "StreamAlarms"); } /// Verifies that worker envelope descriptor contains required correlation fields. @@ -306,7 +306,6 @@ public sealed class ProtobufContractRoundTripTests { var original = new AcknowledgeAlarmRequest { - SessionId = "session-1", ClientCorrelationId = "client-correlation-7", AlarmFullReference = "Tank01.Level.HiHi", Comment = "shift handover", @@ -324,7 +323,6 @@ public sealed class ProtobufContractRoundTripTests { var original = new AcknowledgeAlarmReply { - SessionId = "session-1", CorrelationId = "gateway-correlation-7", ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, Hresult = 0, @@ -420,25 +418,23 @@ public sealed class ProtobufContractRoundTripTests Assert.Equal(AlarmConditionState.ActiveAcked, parsed.CurrentState); } - /// Verifies that QueryActiveAlarmsRequest round-trips empty filter prefix. + /// Verifies that StreamAlarmsRequest round-trips with and without a filter prefix. [Fact] - public void QueryActiveAlarmsRequest_RoundTripsWithAndWithoutFilter() + public void StreamAlarmsRequest_RoundTripsWithAndWithoutFilter() { - var withoutFilter = new QueryActiveAlarmsRequest + var withoutFilter = new StreamAlarmsRequest { - SessionId = "session-1", ClientCorrelationId = "client-correlation-8", }; - var withFilter = new QueryActiveAlarmsRequest + var withFilter = new StreamAlarmsRequest { - SessionId = "session-1", ClientCorrelationId = "client-correlation-9", AlarmFilterPrefix = "Tank01.", }; - Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray())); - Assert.Equal(withFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray())); + Assert.Equal(withoutFilter, StreamAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray())); + Assert.Equal(withFilter, StreamAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray())); } /// Verifies that an MxValue carrying a raw_value bytes payload round-trips. diff --git a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs index 29ae84d..0c06f56 100644 --- a/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs +++ b/src/MxGateway.Tests/Gateway/GatewayEndToEndFakeWorkerSmokeTests.cs @@ -188,7 +188,8 @@ public sealed class GatewayEndToEndFakeWorkerSmokeTests mapper, eventStreamService, _metrics, - NullLogger.Instance); + NullLogger.Instance, + new FakeGatewayAlarmService()); } /// diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs index fcb276e..fe04d10 100644 --- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs +++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceConstraintTests.cs @@ -612,7 +612,8 @@ public sealed class MxAccessGatewayServiceConstraintTests new MxAccessGrpcMapper(), new FakeEventStreamService(sessionManager), new GatewayMetrics(), - NullLogger.Instance); + NullLogger.Instance, + new FakeGatewayAlarmService()); } private static FakeSessionManager CreateSessionManagerWithSeed() diff --git a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs index 97be331..7aeef0b 100644 --- a/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs +++ b/src/MxGateway.Tests/Gateway/Grpc/MxAccessGatewayServiceTests.cs @@ -92,54 +92,6 @@ public sealed class MxAccessGatewayServiceTests Assert.Equal(1, sessionManager.InvokeCount); } - /// - /// Verifies that AcknowledgeAlarm maps a genuinely missing session to NotFound via - /// the service's own ResolveSession lookup rather than an injected exception. - /// - [Fact] - public async Task AcknowledgeAlarm_WhenSessionMissing_ThrowsNotFound() - { - FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true }; - MxAccessGatewayService service = CreateService(sessionManager); - - RpcException exception = await Assert.ThrowsAsync( - async () => await service.AcknowledgeAlarm( - new AcknowledgeAlarmRequest - { - SessionId = "session-missing", - AlarmFullReference = "Tank01.Level.HiHi", - OperatorUser = "alice", - }, - new TestServerCallContext())); - - Assert.Equal(StatusCode.NotFound, exception.StatusCode); - Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal); - } - - /// - /// Verifies that QueryActiveAlarms maps a genuinely missing session to NotFound via - /// the service's own ResolveSession lookup rather than an injected exception. - /// - [Fact] - public async Task QueryActiveAlarms_WhenSessionMissing_ThrowsNotFound() - { - FakeSessionManager sessionManager = new() { ResolveOnlySeededSessions = true }; - MxAccessGatewayService service = CreateService(sessionManager); - - RpcException exception = await Assert.ThrowsAsync( - async () => await service.QueryActiveAlarms( - new QueryActiveAlarmsRequest - { - SessionId = "session-missing", - AlarmFilterPrefix = "Tank01.", - }, - new RecordingServerStreamWriter(), - new TestServerCallContext())); - - Assert.Equal(StatusCode.NotFound, exception.StatusCode); - Assert.Contains("session-missing", exception.Status.Detail, StringComparison.Ordinal); - } - /// Verifies that Invoke throws InvalidArgument and does not invoke the session manager when payload is mismatched. [Fact] public async Task Invoke_WithMismatchedPayload_ThrowsInvalidArgumentAndDoesNotCallSessionManager() @@ -301,32 +253,13 @@ public sealed class MxAccessGatewayServiceTests Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); } - // ===== PR A.4 — AcknowledgeAlarm + QueryActiveAlarms handler contract ===== + // ===== AcknowledgeAlarm + StreamAlarms handler contract ===== // - // Worker-side dispatch (translating AcknowledgeAlarm to MxAccess Acknowledge, - // walking the active-alarm collection for QueryActiveAlarms) is gated on PR - // A.2's dev-rig validation. These tests pin the public surface so the worker - // wiring lands without changing observable behaviour for clients. + // AcknowledgeAlarm validates alarm_full_reference then delegates to the + // session-less IGatewayAlarmService; StreamAlarms forwards the central + // alarm feed. CreateService injects FakeGatewayAlarmService. - /// Verifies AcknowledgeAlarm rejects empty session_id. - [Fact] - public async Task AcknowledgeAlarm_WithMissingSessionId_ThrowsInvalidArgument() - { - MxAccessGatewayService service = CreateService(new FakeSessionManager()); - - RpcException exception = await Assert.ThrowsAsync( - async () => await service.AcknowledgeAlarm( - new AcknowledgeAlarmRequest - { - AlarmFullReference = "Tank01.Level.HiHi", - OperatorUser = "alice", - }, - new TestServerCallContext())); - - Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); - } - - /// Verifies AcknowledgeAlarm rejects empty alarm_full_reference. + /// Verifies AcknowledgeAlarm rejects an empty alarm_full_reference. [Fact] public async Task AcknowledgeAlarm_WithMissingAlarmReference_ThrowsInvalidArgument() { @@ -334,71 +267,47 @@ public sealed class MxAccessGatewayServiceTests RpcException exception = await Assert.ThrowsAsync( async () => await service.AcknowledgeAlarm( - new AcknowledgeAlarmRequest - { - SessionId = "session-1", - OperatorUser = "alice", - }, + new AcknowledgeAlarmRequest { OperatorUser = "alice" }, new TestServerCallContext())); Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); } - /// Verifies AcknowledgeAlarm returns OK with a "dispatcher not registered" diagnostic when DI omits the dispatcher. + /// Verifies AcknowledgeAlarm delegates a valid request to the alarm service. [Fact] - public async Task AcknowledgeAlarm_WithValidRequest_ReturnsOkWithNotRegisteredDiagnostic() + public async Task AcknowledgeAlarm_WithValidRequest_DelegatesToAlarmService() { MxAccessGatewayService service = CreateService(new FakeSessionManager()); AcknowledgeAlarmReply reply = await service.AcknowledgeAlarm( new AcknowledgeAlarmRequest { - SessionId = "session-1", ClientCorrelationId = "corr-1", - AlarmFullReference = "Tank01.Level.HiHi", + AlarmFullReference = "Galaxy!Area.Tank01.Level.HiHi", Comment = "investigating", OperatorUser = "alice", }, new TestServerCallContext()); Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); - Assert.Equal("session-1", reply.SessionId); Assert.Equal("corr-1", reply.CorrelationId); - Assert.Contains("not registered", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase); } - /// Verifies QueryActiveAlarms rejects empty session_id. + /// Verifies StreamAlarms forwards the central alarm feed, ending with snapshot_complete. [Fact] - public async Task QueryActiveAlarms_WithMissingSessionId_ThrowsInvalidArgument() + public async Task StreamAlarms_ForwardsTheCentralAlarmFeed() { MxAccessGatewayService service = CreateService(new FakeSessionManager()); + RecordingServerStreamWriter sink = new(); - RpcException exception = await Assert.ThrowsAsync( - async () => await service.QueryActiveAlarms( - new QueryActiveAlarmsRequest(), - new RecordingServerStreamWriter(), - new TestServerCallContext())); - - Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); - } - - /// Verifies QueryActiveAlarms streams zero snapshots until PR A.2 wires the worker walk. - [Fact] - public async Task QueryActiveAlarms_WithValidRequest_StreamsZeroSnapshots() - { - MxAccessGatewayService service = CreateService(new FakeSessionManager()); - RecordingServerStreamWriter sink = new(); - - await service.QueryActiveAlarms( - new QueryActiveAlarmsRequest - { - SessionId = "session-1", - AlarmFilterPrefix = "Tank01.", - }, + await service.StreamAlarms( + new StreamAlarmsRequest(), sink, new TestServerCallContext()); - Assert.Empty(sink.Messages); + Assert.Contains( + sink.Messages, + message => message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.SnapshotComplete); } /// Verifies OpenSession advertises the alarm RPC capability strings. @@ -433,7 +342,8 @@ public sealed class MxAccessGatewayServiceTests new MxAccessGrpcMapper(), new FakeEventStreamService(sessionManager), metrics ?? new GatewayMetrics(), - NullLogger.Instance); + NullLogger.Instance, + new FakeGatewayAlarmService()); } private static ApiKeyIdentity CreateIdentity() diff --git a/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs deleted file mode 100644 index 4b5f2e8..0000000 --- a/src/MxGateway.Tests/Gateway/Sessions/NotWiredAlarmRpcDispatcherTests.cs +++ /dev/null @@ -1,52 +0,0 @@ -using MxGateway.Contracts.Proto; -using MxGateway.Server.Sessions; - -namespace MxGateway.Tests.Gateway.Sessions; - -/// -/// Pins the null-fallback dispatcher's behaviour: AcknowledgeAsync -/// returns OK with a "dispatcher not registered" diagnostic and -/// QueryActiveAlarmsAsync yields an empty stream. Production binds -/// WorkerAlarmRpcDispatcher in DI; this fallback is only used -/// when no dispatcher is registered (DI omission / standalone tests). -/// -public sealed class NotWiredAlarmRpcDispatcherTests -{ - [Fact] - public async Task AcknowledgeAsync_WhenNotWired_ReturnsOkWithNotRegisteredDiagnostic() - { - IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher(); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "session-1", - ClientCorrelationId = "corr-1", - AlarmFullReference = "Tank01.Level.HiHi", - Comment = "investigating", - OperatorUser = "alice", - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); - Assert.Equal("session-1", reply.SessionId); - Assert.Equal("corr-1", reply.CorrelationId); - Assert.Contains("not registered", reply.DiagnosticMessage, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task QueryActiveAlarmsAsync_WhenNotWired_YieldsNoSnapshots() - { - IAlarmRpcDispatcher dispatcher = new NotWiredAlarmRpcDispatcher(); - - int count = 0; - await foreach (ActiveAlarmSnapshot _ in dispatcher.QueryActiveAlarmsAsync( - new QueryActiveAlarmsRequest { SessionId = "session-1" }, - CancellationToken.None)) - { - count++; - } - - Assert.Equal(0, count); - } -} diff --git a/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs b/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs deleted file mode 100644 index e4c3175..0000000 --- a/src/MxGateway.Tests/Gateway/Sessions/SessionManagerAlarmAutoSubscribeTests.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System.Runtime.CompilerServices; -using Google.Protobuf.WellKnownTypes; -using Microsoft.Extensions.Options; -using MxGateway.Contracts.Proto; -using MxGateway.Server.Configuration; -using MxGateway.Server.Metrics; -using MxGateway.Server.Sessions; -using MxGateway.Server.Workers; - -namespace MxGateway.Tests.Gateway.Sessions; - -/// -/// Pins the alarm auto-subscribe hook on session open. Runs in -/// its own file because the cases are orthogonal to -/// (alarms-disabled vs. -/// alarms-enabled lanes), and the fake worker client below verifies -/// the issued SubscribeAlarms command shape directly. -/// -public sealed class SessionManagerAlarmAutoSubscribeTests -{ - [Fact] - public async Task OpenSessionAsync_DoesNotAutoSubscribe_WhenAlarmsDisabled() - { - AlarmAutoSubscribeWorkerClient worker = new(); - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions { Enabled = false }); - - await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None); - - Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); - } - - [Fact] - public async Task OpenSessionAsync_AutoSubscribes_WhenEnabledWithExpression() - { - AlarmAutoSubscribeWorkerClient worker = new(); - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - SubscriptionExpression = @"\\HOST\Galaxy!Area1", - }); - - GatewaySession session = await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None); - - Assert.Equal(SessionState.Ready, session.State); - Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); - Assert.Equal(@"\\HOST\Galaxy!Area1", - worker.LastSubscribeAlarmsCommand!.SubscriptionExpression); - } - - [Fact] - public async Task OpenSessionAsync_FallsBackToDefaultArea_WhenExpressionEmpty() - { - AlarmAutoSubscribeWorkerClient worker = new(); - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - DefaultArea = "DEV", - }); - - await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", CancellationToken.None); - - Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); - Assert.Contains(@"\Galaxy!DEV", - worker.LastSubscribeAlarmsCommand!.SubscriptionExpression); - } - - [Fact] - public async Task OpenSessionAsync_Succeeds_WhenAutoSubscribeFailsWithRequireOff() - { - // Worker rejects the SubscribeAlarms command. With RequireSubscribeOnOpen=false - // (the default), the session still opens — alarm-side commands later return - // "not subscribed", but data subscriptions work. - AlarmAutoSubscribeWorkerClient worker = new() - { - SubscribeAlarmsReplyFactory = _ => new MxCommandReply - { - Kind = MxCommandKind.SubscribeAlarms, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.MxaccessFailure, - Message = "wnwrap subscribe failed", - }, - DiagnosticMessage = "alarm provider unavailable", - }, - }; - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - SubscriptionExpression = @"\\HOST\Galaxy!Area1", - RequireSubscribeOnOpen = false, - }); - - GatewaySession session = await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None); - - Assert.Equal(SessionState.Ready, session.State); - Assert.Equal(1, worker.SubscribeAlarmsInvokeCount); - } - - [Fact] - public async Task OpenSessionAsync_Throws_WhenAutoSubscribeFailsWithRequireOn() - { - AlarmAutoSubscribeWorkerClient worker = new() - { - SubscribeAlarmsReplyFactory = _ => new MxCommandReply - { - Kind = MxCommandKind.SubscribeAlarms, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.MxaccessFailure, - Message = "wnwrap subscribe failed", - }, - }, - }; - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - SubscriptionExpression = @"\\HOST\Galaxy!Area1", - RequireSubscribeOnOpen = true, - }); - - await Assert.ThrowsAsync( - async () => await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None)); - } - - /// - /// Server-006 regression: when auto-subscribe throws after - /// SessionOpened() incremented the open-session gauge, the failed - /// open must not leave mxgateway.sessions.open over-counted. - /// - [Fact] - public async Task OpenSessionAsync_DoesNotLeakOpenSessionGauge_WhenAutoSubscribeFailsWithRequireOn() - { - AlarmAutoSubscribeWorkerClient worker = new() - { - SubscribeAlarmsReplyFactory = _ => new MxCommandReply - { - Kind = MxCommandKind.SubscribeAlarms, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.MxaccessFailure, - Message = "wnwrap subscribe failed", - }, - }, - }; - using GatewayMetrics metrics = new(); - SessionManager manager = NewManager( - worker, - alarms: new AlarmsOptions - { - Enabled = true, - SubscriptionExpression = @"\\HOST\Galaxy!Area1", - RequireSubscribeOnOpen = true, - }, - metrics: metrics); - - await Assert.ThrowsAsync( - async () => await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None)); - - Assert.Equal(0, metrics.GetSnapshot().OpenSessions); - } - - [Fact] - public async Task OpenSessionAsync_Throws_WhenEnabledButNoExpressionAndRequireOn() - { - AlarmAutoSubscribeWorkerClient worker = new(); - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - // No SubscriptionExpression and no DefaultArea. - RequireSubscribeOnOpen = true, - }); - - await Assert.ThrowsAsync( - async () => await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None)); - Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); - } - - [Fact] - public async Task OpenSessionAsync_Succeeds_WhenEnabledButNoExpressionAndRequireOff() - { - AlarmAutoSubscribeWorkerClient worker = new(); - SessionManager manager = NewManager(worker, alarms: new AlarmsOptions - { - Enabled = true, - // No SubscriptionExpression and no DefaultArea — default require=false. - }); - - GatewaySession session = await manager.OpenSessionAsync( - CreateOpenRequest(), "client-1", CancellationToken.None); - - Assert.Equal(SessionState.Ready, session.State); - Assert.Equal(0, worker.SubscribeAlarmsInvokeCount); - } - - private static SessionManager NewManager( - AlarmAutoSubscribeWorkerClient worker, - AlarmsOptions alarms, - GatewayMetrics? metrics = null) - { - FakeSessionWorkerClientFactory factory = new(worker); - GatewayOptions options = new GatewayOptions - { - Sessions = new SessionOptions - { - DefaultCommandTimeoutSeconds = 30, - MaxSessions = 64, - DefaultLeaseSeconds = 1800, - }, - Worker = new WorkerOptions - { - StartupTimeoutSeconds = 30, - ShutdownTimeoutSeconds = 10, - }, - Alarms = alarms, - }; - return new SessionManager( - new SessionRegistry(), - factory, - Options.Create(options), - metrics ?? new GatewayMetrics()); - } - - private static SessionOpenRequest CreateOpenRequest() - { - return new SessionOpenRequest( - RequestedBackend: null, - ClientSessionName: "test-session", - ClientCorrelationId: "client-correlation-1", - CommandTimeout: Duration.FromTimeSpan(TimeSpan.FromSeconds(5))); - } - - private sealed class FakeSessionWorkerClientFactory(IWorkerClient client) : ISessionWorkerClientFactory - { - public Task CreateAsync( - GatewaySession session, - CancellationToken cancellationToken) - { - return Task.FromResult(client); - } - } - - private sealed class AlarmAutoSubscribeWorkerClient : IWorkerClient - { - public string SessionId { get; } = "session-1"; - public int? ProcessId { get; } = 1234; - public WorkerClientState State { get; set; } = WorkerClientState.Ready; - public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow; - - public int SubscribeAlarmsInvokeCount { get; private set; } - public SubscribeAlarmsCommand? LastSubscribeAlarmsCommand { get; private set; } - public Func? SubscribeAlarmsReplyFactory { get; init; } - - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public Task InvokeAsync( - WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken) - { - if (command.Command?.Kind == MxCommandKind.SubscribeAlarms) - { - SubscribeAlarmsInvokeCount++; - LastSubscribeAlarmsCommand = command.Command.SubscribeAlarms; - MxCommandReply reply = SubscribeAlarmsReplyFactory?.Invoke(command) - ?? new MxCommandReply - { - Kind = MxCommandKind.SubscribeAlarms, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.Ok, - Message = "OK", - }, - }; - return Task.FromResult(new WorkerCommandReply { Reply = reply }); - } - return Task.FromResult(new WorkerCommandReply - { - Reply = new MxCommandReply - { - Kind = command.Command?.Kind ?? MxCommandKind.Unspecified, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.Ok, - Message = "OK", - }, - }, - }); - } - - public async IAsyncEnumerable ReadEventsAsync( - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await Task.CompletedTask; - yield break; - } - - public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) - => Task.CompletedTask; - public void Kill(string reason) { } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs b/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs deleted file mode 100644 index d6c7367..0000000 --- a/src/MxGateway.Tests/Gateway/Sessions/WorkerAlarmRpcDispatcherTests.cs +++ /dev/null @@ -1,393 +0,0 @@ -using System.Runtime.CompilerServices; -using MxGateway.Contracts.Proto; -using MxGateway.Server.Sessions; -using MxGateway.Server.Workers; - -namespace MxGateway.Tests.Gateway.Sessions; - -/// -/// Pins the production 's behaviour: -/// resolves the session by id, issues the matching MxCommand over the -/// worker pipe, and unwraps the reply into AcknowledgeAlarmReply or the -/// ActiveAlarmSnapshot stream. -/// -public sealed class WorkerAlarmRpcDispatcherTests -{ - [Fact] - public async Task AcknowledgeAsync_WhenSessionMissing_ReturnsSessionNotFound() - { - SessionRegistry registry = new(); - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "missing", - ClientCorrelationId = "c1", - AlarmFullReference = Guid.NewGuid().ToString(), - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.SessionNotFound, reply.ProtocolStatus.Code); - } - - [Fact] - public async Task AcknowledgeAsync_WithGuidReference_ForwardsGuidAndReturnsNativeStatus() - { - SessionRegistry registry = new(); - Guid alarmGuid = Guid.NewGuid(); - FakeAlarmWorkerClient worker = new() - { - ReplyFactory = command => - { - Assert.Equal(MxCommandKind.AcknowledgeAlarm, command.Command.Kind); - Assert.Equal(alarmGuid.ToString(), command.Command.AcknowledgeAlarmCommand.AlarmGuid); - Assert.Equal("ack", command.Command.AcknowledgeAlarmCommand.Comment); - Assert.Equal("alice", command.Command.AcknowledgeAlarmCommand.OperatorUser); - return new MxCommandReply - { - Kind = MxCommandKind.AcknowledgeAlarm, - ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, - Hresult = 0, - AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 }, - }; - }, - }; - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "s1", - ClientCorrelationId = "c1", - AlarmFullReference = alarmGuid.ToString(), - Comment = "ack", - OperatorUser = "alice", - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); - Assert.Equal(0, reply.Hresult); - Assert.Equal("s1", reply.SessionId); - Assert.Equal("c1", reply.CorrelationId); - Assert.Equal(1, worker.InvokeCount); - } - - [Fact] - public async Task AcknowledgeAsync_WhenWorkerFails_PropagatesWorkerDiagnostic() - { - SessionRegistry registry = new(); - FakeAlarmWorkerClient worker = new() - { - ReplyFactory = _ => new MxCommandReply - { - Kind = MxCommandKind.AcknowledgeAlarm, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.MxaccessFailure, - Message = "AVEVA Acknowledge failed.", - }, - Hresult = -123, - DiagnosticMessage = "AVEVA AlarmAckByGUID returned non-zero status -123.", - }, - }; - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "s1", - AlarmFullReference = Guid.NewGuid().ToString(), - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); - Assert.Equal(-123, reply.Hresult); - Assert.Contains("-123", reply.DiagnosticMessage); - } - - [Theory] - [InlineData("Galaxy!TestArea.TestMachine_001.TestAlarm001", "Galaxy", "TestArea", "TestMachine_001.TestAlarm001")] - [InlineData("Galaxy!Area.Tag", "Galaxy", "Area", "Tag")] - [InlineData("Provider!Group.Tag.With.Dots", "Provider", "Group", "Tag.With.Dots")] - public void TryParseAlarmReference_WithProviderGroupTag_DecomposesParts( - string reference, string expectedProvider, string expectedGroup, string expectedName) - { - Assert.True(WorkerAlarmRpcDispatcher.TryParseAlarmReference( - reference, out string provider, out string group, out string name)); - Assert.Equal(expectedProvider, provider); - Assert.Equal(expectedGroup, group); - Assert.Equal(expectedName, name); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData(null)] - [InlineData("no-bang-here")] - [InlineData("!Group.Tag")] // empty provider - [InlineData("Galaxy!")] // bang at end - [InlineData("Galaxy!Group")] // missing dot - [InlineData("Galaxy!.Tag")] // empty group - [InlineData("Galaxy!Group.")] // empty tag - public void TryParseAlarmReference_WithMalformedReference_ReturnsFalse(string? reference) - { - Assert.False(WorkerAlarmRpcDispatcher.TryParseAlarmReference( - reference, out _, out _, out _)); - } - - [Fact] - public async Task AcknowledgeAsync_WithProviderGroupTagReference_RoutesViaAckByName() - { - SessionRegistry registry = new(); - AcknowledgeAlarmByNameCommand? observed = null; - FakeAlarmWorkerClient worker = new() - { - ReplyFactory = command => - { - Assert.Equal(MxCommandKind.AcknowledgeAlarmByName, command.Command.Kind); - observed = command.Command.AcknowledgeAlarmByNameCommand; - return new MxCommandReply - { - Kind = MxCommandKind.AcknowledgeAlarmByName, - ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, - Hresult = 0, - AcknowledgeAlarm = new AcknowledgeAlarmReplyPayload { NativeStatus = 0 }, - }; - }, - }; - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "s1", - ClientCorrelationId = "c1", - AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001", - Comment = "ack-by-name", - OperatorUser = "bob", - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); - Assert.NotNull(observed); - Assert.Equal("TestMachine_001.TestAlarm001", observed!.AlarmName); - Assert.Equal("Galaxy", observed.ProviderName); - Assert.Equal("TestArea", observed.GroupName); - Assert.Equal("bob", observed.OperatorUser); - Assert.Equal("ack-by-name", observed.Comment); - } - - [Fact] - public async Task AcknowledgeAsync_WithUnparseableReference_ReturnsInvalidRequest() - { - SessionRegistry registry = new(); - FakeAlarmWorkerClient worker = new(); - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - AcknowledgeAlarmReply reply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "s1", - AlarmFullReference = "no-bang-no-dot", - }, - CancellationToken.None); - - Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); - Assert.Equal(0, worker.InvokeCount); - } - - [Fact] - public async Task QueryActiveAlarmsAsync_WithPayloadSnapshots_YieldsEachSnapshot() - { - SessionRegistry registry = new(); - FakeAlarmWorkerClient worker = new() - { - ReplyFactory = command => - { - Assert.Equal(MxCommandKind.QueryActiveAlarms, command.Command.Kind); - QueryActiveAlarmsReplyPayload payload = new QueryActiveAlarmsReplyPayload(); - payload.Snapshots.Add(new ActiveAlarmSnapshot - { - AlarmFullReference = "Galaxy!A.T1", - CurrentState = AlarmConditionState.Active, - Severity = 500, - }); - payload.Snapshots.Add(new ActiveAlarmSnapshot - { - AlarmFullReference = "Galaxy!A.T2", - CurrentState = AlarmConditionState.ActiveAcked, - Severity = 100, - }); - return new MxCommandReply - { - Kind = MxCommandKind.QueryActiveAlarms, - ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = "OK" }, - QueryActiveAlarms = payload, - }; - }, - }; - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - List collected = new(); - await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync( - new QueryActiveAlarmsRequest { SessionId = "s1" }, - CancellationToken.None)) - { - collected.Add(snap); - } - - Assert.Equal(2, collected.Count); - Assert.Equal("Galaxy!A.T1", collected[0].AlarmFullReference); - Assert.Equal("Galaxy!A.T2", collected[1].AlarmFullReference); - } - - /// - /// Server-019 regression: QueryActiveAlarmsAsync used to silently - /// yield break when the session id was not in the registry, while the - /// peer AcknowledgeAsync returned SessionNotFound. Both methods - /// now signal a missing session — QueryActiveAlarms throws a - /// with - /// (the gateway gRPC - /// layer maps it to gRPC NotFound), aligning the dispatcher's - /// missing-session contract across the two RPCs. - /// - [Fact] - public async Task QueryActiveAlarmsAsync_WhenSessionMissing_ThrowsSessionNotFound() - { - SessionRegistry registry = new(); - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - SessionManagerException exception = await Assert.ThrowsAsync(async () => - { - await foreach (ActiveAlarmSnapshot _ in dispatcher.QueryActiveAlarmsAsync( - new QueryActiveAlarmsRequest { SessionId = "missing" }, - CancellationToken.None)) - { - // No yield expected — the throw happens before the first iteration. - } - }); - - Assert.Equal(SessionManagerErrorCode.SessionNotFound, exception.ErrorCode); - - // Peer-method parity: AcknowledgeAsync still signals SessionNotFound (as an - // in-band ProtocolStatus, since it's a unary RPC). The two methods now agree - // that a missing session is an error, not an empty success. - AcknowledgeAlarmReply ackReply = await dispatcher.AcknowledgeAsync( - new AcknowledgeAlarmRequest - { - SessionId = "missing", - AlarmFullReference = Guid.NewGuid().ToString(), - }, - CancellationToken.None); - Assert.Equal(ProtocolStatusCode.SessionNotFound, ackReply.ProtocolStatus.Code); - } - - [Fact] - public async Task QueryActiveAlarmsAsync_WhenWorkerFails_YieldsEmpty() - { - SessionRegistry registry = new(); - FakeAlarmWorkerClient worker = new() - { - ReplyFactory = _ => new MxCommandReply - { - Kind = MxCommandKind.QueryActiveAlarms, - ProtocolStatus = new ProtocolStatus - { - Code = ProtocolStatusCode.MxaccessFailure, - Message = "alarm consumer not subscribed", - }, - }, - }; - GatewaySession session = NewSession("s1"); - session.AttachWorkerClient(worker); - session.MarkReady(); - registry.TryAdd(session); - - WorkerAlarmRpcDispatcher dispatcher = new(registry); - - List collected = new(); - await foreach (ActiveAlarmSnapshot snap in dispatcher.QueryActiveAlarmsAsync( - new QueryActiveAlarmsRequest { SessionId = "s1" }, - CancellationToken.None)) - { - collected.Add(snap); - } - - Assert.Empty(collected); - } - - private static GatewaySession NewSession(string sessionId) - { - return new( - sessionId, - "mxaccess", - $"mxaccess-gateway-1-{sessionId}", - "nonce", - "client-1", - "test-session", - "client-correlation-1", - commandTimeout: TimeSpan.FromSeconds(30), - startupTimeout: TimeSpan.FromSeconds(5), - shutdownTimeout: TimeSpan.FromSeconds(5), - leaseDuration: TimeSpan.FromMinutes(30), - openedAt: DateTimeOffset.UtcNow); - } - - private sealed class FakeAlarmWorkerClient : IWorkerClient - { - public string SessionId { get; } = "session-1"; - public int? ProcessId { get; } = 1; - public WorkerClientState State { get; } = WorkerClientState.Ready; - public DateTimeOffset LastHeartbeatAt { get; } = DateTimeOffset.UtcNow; - - public Func? ReplyFactory { get; set; } - public int InvokeCount { get; private set; } - - public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; - - public Task InvokeAsync( - WorkerCommand command, TimeSpan timeout, CancellationToken cancellationToken) - { - InvokeCount++; - MxCommandReply reply = ReplyFactory?.Invoke(command) ?? new MxCommandReply(); - return Task.FromResult(new WorkerCommandReply { Reply = reply }); - } - - public async IAsyncEnumerable ReadEventsAsync( - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await Task.CompletedTask; - yield break; - } - - public Task ShutdownAsync(TimeSpan timeout, CancellationToken cancellationToken) => Task.CompletedTask; - public void Kill(string reason) { } - public ValueTask DisposeAsync() => ValueTask.CompletedTask; - } -} diff --git a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs index b741803..55e927c 100644 --- a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs +++ b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs @@ -266,7 +266,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests RpcException exception = await Assert.ThrowsAsync( () => interceptor.UnaryServerHandler( - new AcknowledgeAlarmRequest { SessionId = "session-1", AlarmFullReference = "ref" }, + new AcknowledgeAlarmRequest { AlarmFullReference = "ref" }, ContextWithAuthorization("Bearer mxgw_operator01_secret"), (_, _) => Task.FromResult(new AcknowledgeAlarmReply()))); @@ -284,7 +284,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests bool handlerRan = false; AcknowledgeAlarmReply reply = await interceptor.UnaryServerHandler( - new AcknowledgeAlarmRequest { SessionId = "session-1", AlarmFullReference = "ref" }, + new AcknowledgeAlarmRequest { AlarmFullReference = "ref" }, ContextWithAuthorization("Bearer mxgw_operator01_secret"), (_, _) => { @@ -310,7 +310,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests RpcException exception = await Assert.ThrowsAsync( () => interceptor.ServerStreamingServerHandler( - new QueryActiveAlarmsRequest { SessionId = "session-1" }, + new StreamAlarmsRequest(), new RecordingServerStreamWriter(), ContextWithAuthorization("Bearer mxgw_operator01_secret"), (_, _, _) => Task.CompletedTask)); @@ -329,7 +329,7 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests RecordingServerStreamWriter streamWriter = new(); await interceptor.ServerStreamingServerHandler( - new QueryActiveAlarmsRequest { SessionId = "session-1" }, + new StreamAlarmsRequest(), streamWriter, ContextWithAuthorization("Bearer mxgw_operator01_secret"), async (_, writer, _) => @@ -352,7 +352,8 @@ public sealed class GatewayGrpcAuthorizationInterceptorTests new MxAccessGrpcMapper(), new NoOpEventStreamService(), new GatewayMetrics(), - NullLogger.Instance); + NullLogger.Instance, + new FakeGatewayAlarmService()); } private static GatewayGrpcAuthorizationInterceptor CreateInterceptor( diff --git a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs index 137f146..d2ad3ad 100644 --- a/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs +++ b/src/MxGateway.Tests/Security/Authorization/GatewayGrpcScopeResolverTests.cs @@ -14,7 +14,7 @@ public sealed class GatewayGrpcScopeResolverTests [InlineData(typeof(CloseSessionRequest), GatewayScopes.SessionClose)] [InlineData(typeof(StreamEventsRequest), GatewayScopes.EventsRead)] [InlineData(typeof(AcknowledgeAlarmRequest), GatewayScopes.InvokeWrite)] - [InlineData(typeof(QueryActiveAlarmsRequest), GatewayScopes.EventsRead)] + [InlineData(typeof(StreamAlarmsRequest), GatewayScopes.EventsRead)] [InlineData(typeof(TestConnectionRequest), GatewayScopes.MetadataRead)] [InlineData(typeof(GetLastDeployTimeRequest), GatewayScopes.MetadataRead)] [InlineData(typeof(DiscoverHierarchyRequest), GatewayScopes.MetadataRead)] diff --git a/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs b/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs new file mode 100644 index 0000000..bf32ada --- /dev/null +++ b/src/MxGateway.Tests/TestSupport/FakeGatewayAlarmService.cs @@ -0,0 +1,54 @@ +using System.Runtime.CompilerServices; +using MxGateway.Contracts.Proto; +using MxGateway.Server.Alarms; + +namespace MxGateway.Tests.TestSupport; + +/// +/// test double — serves a scripted +/// active-alarm set and acknowledges every request with an OK status, +/// so gRPC service tests can exercise the alarm handlers without the +/// real gateway alarm monitor or a worker. +/// +public sealed class FakeGatewayAlarmService : IGatewayAlarmService +{ + /// + public GatewayAlarmMonitorState State { get; set; } = GatewayAlarmMonitorState.Monitoring; + + /// + public string? LastError { get; set; } + + /// + public int? WorkerProcessId { get; set; } + + /// + public IReadOnlyList CurrentAlarms { get; set; } = []; + + /// + public async IAsyncEnumerable StreamAsync( + string? alarmFilterPrefix, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + foreach (ActiveAlarmSnapshot alarm in CurrentAlarms) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new AlarmFeedMessage { ActiveAlarm = alarm }; + } + + yield return new AlarmFeedMessage { SnapshotComplete = true }; + await Task.CompletedTask.ConfigureAwait(false); + } + + /// + public Task AcknowledgeAsync( + AcknowledgeAlarmRequest request, + CancellationToken cancellationToken) + { + return Task.FromResult(new AcknowledgeAlarmReply + { + CorrelationId = request.ClientCorrelationId, + ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok }, + DiagnosticMessage = string.Empty, + }); + } +}