From 2e4c3045d41ee2976c52c0d2aeb7e4909f599f76 Mon Sep 17 00:00:00 2001 From: dummy Date: Tue, 25 Aug 2026 16:48:58 -0500 Subject: [PATCH 1/5] Integrate nitlsconfig client changes: Call hook-in to add client connection audit logs and improve error reporting --- CHANGELOG.md | 16 ++++++++-------- build/templates/_grpc_stub_interpreter.py.mako | 7 ++++++- build/templates/session.py.mako | 11 +++++++++++ build/templates/setup.py.mako | 2 +- .../nidcpower/_grpc_stub_interpreter.py | 7 ++++++- generated/nidcpower/nidcpower/session.py | 9 ++++++++- generated/nidcpower/setup.py | 2 +- .../nidigital/_grpc_stub_interpreter.py | 7 ++++++- generated/nidigital/nidigital/session.py | 9 ++++++++- generated/nidigital/setup.py | 2 +- generated/nidmm/nidmm/_grpc_stub_interpreter.py | 7 ++++++- generated/nidmm/nidmm/session.py | 9 ++++++++- generated/nidmm/setup.py | 2 +- .../nifake/nifake/_grpc_stub_interpreter.py | 7 ++++++- generated/nifake/nifake/session.py | 9 ++++++++- generated/nifake/setup.py | 2 +- .../nifgen/nifgen/_grpc_stub_interpreter.py | 7 ++++++- generated/nifgen/nifgen/session.py | 9 ++++++++- generated/nifgen/setup.py | 2 +- .../nirfsa/nirfsa/_grpc_stub_interpreter.py | 7 ++++++- generated/nirfsa/nirfsa/session.py | 9 ++++++++- generated/nirfsa/setup.py | 2 +- .../nirfsg/nirfsg/_grpc_stub_interpreter.py | 7 ++++++- generated/nirfsg/nirfsg/session.py | 9 ++++++++- generated/nirfsg/setup.py | 2 +- .../niscope/niscope/_grpc_stub_interpreter.py | 7 ++++++- generated/niscope/niscope/session.py | 9 ++++++++- generated/niscope/setup.py | 2 +- .../niswitch/niswitch/_grpc_stub_interpreter.py | 7 ++++++- generated/niswitch/niswitch/session.py | 9 ++++++++- generated/niswitch/setup.py | 2 +- tox.ini | 1 + 32 files changed, 162 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e7187c5cd..de3f196dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ #### [nidcpower] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -556,7 +556,7 @@ #### [nidigital] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -798,7 +798,7 @@ #### [nidmm] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1122,7 +1122,7 @@ #### [nifgen] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1736,7 +1736,7 @@ #### [nirfsa] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1754,7 +1754,7 @@ #### [nirfsg] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -1893,7 +1893,7 @@ #### [niscope] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed @@ -2505,7 +2505,7 @@ #### [niswitch] Unreleased - Added - - (Common) Added `nitlsconfig[grpc]>=1.0.0a1` to the optional gRPC dependencies. + - (Common) Added `nitlsconfig[grpc]>=1.0.0a2` to the optional gRPC dependencies. - Changed - Removed diff --git a/build/templates/_grpc_stub_interpreter.py.mako b/build/templates/_grpc_stub_interpreter.py.mako index 5af49bf86f..b5ce4d98a1 100644 --- a/build/templates/_grpc_stub_interpreter.py.mako +++ b/build/templates/_grpc_stub_interpreter.py.mako @@ -13,6 +13,7 @@ are_complex_parameters_used = helper.are_complex_parameters_used(functions) import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -69,7 +70,11 @@ class GrpcStubInterpreter(object): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/build/templates/session.py.mako b/build/templates/session.py.mako index df755677b3..b3a19c5998 100644 --- a/build/templates/session.py.mako +++ b/build/templates/session.py.mako @@ -276,7 +276,18 @@ if grpc_supported: # if ${init_function['python_name']} fails, the error handler can reference it. # And then here, once ${init_function['python_name']} succeeds, we call set_session_handle # with the actual session handle. +% if grpc_supported: + connected = False + try: + self._interpreter.set_session_handle(self.${init_function['python_name']}(${init_call_params})) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('${config['driver_name']}', grpc_options.grpc_channel, connected) +% else: self._interpreter.set_session_handle(self.${init_function['python_name']}(${init_call_params})) +% endif % if config['uses_nitclk']: % if grpc_supported: diff --git a/build/templates/setup.py.mako b/build/templates/setup.py.mako index 6ae7c2aef3..7251e26c2c 100644 --- a/build/templates/setup.py.mako +++ b/build/templates/setup.py.mako @@ -54,7 +54,7 @@ setup( 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, % endif diff --git a/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py b/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py index 6b479e4b2c..353b856a1d 100644 --- a/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py +++ b/generated/nidcpower/nidcpower/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -54,7 +55,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidcpower/nidcpower/session.py b/generated/nidcpower/nidcpower/session.py index b4dc5a8288..3a9ff00714 100644 --- a/generated/nidcpower/nidcpower/session.py +++ b/generated/nidcpower/nidcpower/session.py @@ -7681,7 +7681,14 @@ def __init__(self, resource_name, channels=None, reset=False, options={}, indepe # if _fancy_initialize fails, the error handler can reference it. # And then here, once _fancy_initialize succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._fancy_initialize(resource_name, channels, reset, options, independent_channels)) + connected = False + try: + self._interpreter.set_session_handle(self._fancy_initialize(resource_name, channels, reset, options, independent_channels)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-DCPower', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/nidcpower/setup.py b/generated/nidcpower/setup.py index 8c6e7aab04..b2d01ae8cd 100644 --- a/generated/nidcpower/setup.py +++ b/generated/nidcpower/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nidigital/nidigital/_grpc_stub_interpreter.py b/generated/nidigital/nidigital/_grpc_stub_interpreter.py index 49589cda1d..d733b9e7ff 100644 --- a/generated/nidigital/nidigital/_grpc_stub_interpreter.py +++ b/generated/nidigital/nidigital/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -52,7 +53,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidigital/nidigital/session.py b/generated/nidigital/nidigital/session.py index 3678e5dc3f..858375095f 100644 --- a/generated/nidigital/nidigital/session.py +++ b/generated/nidigital/nidigital/session.py @@ -3280,7 +3280,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-Digital Pattern Driver', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nidigital/setup.py b/generated/nidigital/setup.py index 35586e8c57..ec3377e3c2 100644 --- a/generated/nidigital/setup.py +++ b/generated/nidigital/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nidmm/nidmm/_grpc_stub_interpreter.py b/generated/nidmm/nidmm/_grpc_stub_interpreter.py index 25b22ab85c..91bf0fa8b6 100644 --- a/generated/nidmm/nidmm/_grpc_stub_interpreter.py +++ b/generated/nidmm/nidmm/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nidmm/nidmm/session.py b/generated/nidmm/nidmm/session.py index 2d04e204e2..8f52477dbe 100644 --- a/generated/nidmm/nidmm/session.py +++ b/generated/nidmm/nidmm/session.py @@ -1049,7 +1049,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-DMM', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/nidmm/setup.py b/generated/nidmm/setup.py index 0d0b67ec41..0a6113944a 100644 --- a/generated/nidmm/setup.py +++ b/generated/nidmm/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nifake/nifake/_grpc_stub_interpreter.py b/generated/nifake/nifake/_grpc_stub_interpreter.py index 8991074a0b..a3e34085b2 100644 --- a/generated/nifake/nifake/_grpc_stub_interpreter.py +++ b/generated/nifake/nifake/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -57,7 +58,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nifake/nifake/session.py b/generated/nifake/nifake/session.py index 2fcfb3755c..66741bcab8 100644 --- a/generated/nifake/nifake/session.py +++ b/generated/nifake/nifake/session.py @@ -728,7 +728,14 @@ def __init__(self, resource_name, options={}, id_query=False, reset_device=False # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, options, id_query, reset_device)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, options, id_query, reset_device)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-FAKE', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nifake/setup.py b/generated/nifake/setup.py index 6790a9e3e0..5fec3f14d3 100644 --- a/generated/nifake/setup.py +++ b/generated/nifake/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nifgen/nifgen/_grpc_stub_interpreter.py b/generated/nifgen/nifgen/_grpc_stub_interpreter.py index 19b06e3360..21f50c24da 100644 --- a/generated/nifgen/nifgen/_grpc_stub_interpreter.py +++ b/generated/nifgen/nifgen/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nifgen/nifgen/session.py b/generated/nifgen/nifgen/session.py index 0edd4dec42..0429bfcb3f 100644 --- a/generated/nifgen/nifgen/session.py +++ b/generated/nifgen/nifgen/session.py @@ -3166,7 +3166,14 @@ def __init__(self, resource_name, channel_name=None, reset_device=False, options # if _initialize_with_channels fails, the error handler can reference it. # And then here, once _initialize_with_channels succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._initialize_with_channels(resource_name, channel_name, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._initialize_with_channels(resource_name, channel_name, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-FGEN', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nifgen/setup.py b/generated/nifgen/setup.py index 39c7bc4261..76ee778cc1 100644 --- a/generated/nifgen/setup.py +++ b/generated/nifgen/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py b/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py index d5dc23c2ae..e23c12a58a 100644 --- a/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py +++ b/generated/nirfsa/nirfsa/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -57,7 +58,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nirfsa/nirfsa/session.py b/generated/nirfsa/nirfsa/session.py index 363cd4862c..b241e59f86 100644 --- a/generated/nirfsa/nirfsa/session.py +++ b/generated/nirfsa/nirfsa/session.py @@ -6777,7 +6777,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-RFSA', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nirfsa/setup.py b/generated/nirfsa/setup.py index 0c552d39bf..6252a35e79 100644 --- a/generated/nirfsa/setup.py +++ b/generated/nirfsa/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py b/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py index 015f85fd22..a2e2ff3a3c 100644 --- a/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py +++ b/generated/nirfsg/nirfsg/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -51,7 +52,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/nirfsg/nirfsg/session.py b/generated/nirfsg/nirfsg/session.py index 83ec53bd2c..210f8fbc18 100644 --- a/generated/nirfsg/nirfsg/session.py +++ b/generated/nirfsg/nirfsg/session.py @@ -5713,7 +5713,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-RFSG', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/nirfsg/setup.py b/generated/nirfsg/setup.py index 63eb43951b..baa52e289d 100644 --- a/generated/nirfsg/setup.py +++ b/generated/nirfsg/setup.py @@ -40,7 +40,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/niscope/niscope/_grpc_stub_interpreter.py b/generated/niscope/niscope/_grpc_stub_interpreter.py index cf6cd35617..93d12cedba 100644 --- a/generated/niscope/niscope/_grpc_stub_interpreter.py +++ b/generated/niscope/niscope/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -54,7 +55,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/niscope/niscope/session.py b/generated/niscope/niscope/session.py index b17b43b900..c89ddc81b0 100644 --- a/generated/niscope/niscope/session.py +++ b/generated/niscope/niscope/session.py @@ -4035,7 +4035,14 @@ def __init__(self, resource_name, id_query=False, reset_device=False, options={} # if _init_with_options fails, the error handler can reference it. # And then here, once _init_with_options succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_options(resource_name, id_query, reset_device, options)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-SCOPE', grpc_options.grpc_channel, connected) # NI-TClk does not work over NI gRPC Device Server if not grpc_options: diff --git a/generated/niscope/setup.py b/generated/niscope/setup.py index 2658d0c76c..dc67785cc0 100644 --- a/generated/niscope/setup.py +++ b/generated/niscope/setup.py @@ -39,7 +39,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/generated/niswitch/niswitch/_grpc_stub_interpreter.py b/generated/niswitch/niswitch/_grpc_stub_interpreter.py index f25ce95632..26d382e948 100644 --- a/generated/niswitch/niswitch/_grpc_stub_interpreter.py +++ b/generated/niswitch/niswitch/_grpc_stub_interpreter.py @@ -3,6 +3,7 @@ import grpc import hightime # noqa: F401 +import nitlsconfig import session_pb2 as session_grpc_types import threading import warnings @@ -50,7 +51,11 @@ def _invoke(self, func, request, metadata=None): elif grpc_error == grpc.StatusCode.INVALID_ARGUMENT: raise ValueError(error_message) from None elif grpc_error == grpc.StatusCode.UNAVAILABLE: - error_message = 'Failed to connect to server' + # gRPC reports a rejected TLS handshake and an unreachable server with the + # same code, so ask NI-TLS whether it built this channel and can say more. + error_message = nitlsconfig.get_tls_connection_error_elaboration( + self._grpc_options.grpc_channel + ) or 'Failed to connect to server' elif grpc_error == grpc.StatusCode.UNIMPLEMENTED: error_message = ( 'This operation is not supported by the NI gRPC Device Server being used. Upgrade NI gRPC Device Server.' diff --git a/generated/niswitch/niswitch/session.py b/generated/niswitch/niswitch/session.py index ad4063da63..c7ab4eb439 100644 --- a/generated/niswitch/niswitch/session.py +++ b/generated/niswitch/niswitch/session.py @@ -1374,7 +1374,14 @@ def __init__(self, resource_name, topology="Configured Topology", simulate=False # if _init_with_topology fails, the error handler can reference it. # And then here, once _init_with_topology succeeds, we call set_session_handle # with the actual session handle. - self._interpreter.set_session_handle(self._init_with_topology(resource_name, topology, simulate, reset_device)) + connected = False + try: + self._interpreter.set_session_handle(self._init_with_topology(resource_name, topology, simulate, reset_device)) + connected = True + finally: + if grpc_options: + import nitlsconfig + nitlsconfig.audit_session_connect('NI-SWITCH', grpc_options.grpc_channel, connected) # Store the parameter list for later printing in __repr__ param_list = [] diff --git a/generated/niswitch/setup.py b/generated/niswitch/setup.py index ee9e6e92b4..8a81e95772 100644 --- a/generated/niswitch/setup.py +++ b/generated/niswitch/setup.py @@ -38,7 +38,7 @@ def read_contents(file_to_read): 'grpcio>=1.59.0,<2.0', 'protobuf>=4.21.6', 'ni.grpcdevice.v1.proto>=1.0.0', - 'nitlsconfig[grpc]>=1.0.0a1', + 'nitlsconfig[grpc]>=1.0.0a2', ], }, classifiers=[ diff --git a/tox.ini b/tox.ini index 41333b29e9..ddb9381198 100644 --- a/tox.ini +++ b/tox.ini @@ -150,6 +150,7 @@ deps = test: grpcio == 1.75.1 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: protobuf == 5.27.2 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: ni.grpcdevice.v1.proto >= 1.0.0 # Provides session_pb2 package at runtime since we no longer generate it per-driver + test: nitlsconfig >= 1.0.0a2 # Pinned while the API is still moving through alphas build_test: pytest build_test: coverage build_test: mako From b961f22e8df26e83256393d94ecd6b0e85da8043 Mon Sep 17 00:00:00 2001 From: dummy Date: Tue, 25 Aug 2026 16:53:23 -0500 Subject: [PATCH 2/5] Fix stale comment on tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ddb9381198..b42df19fdd 100644 --- a/tox.ini +++ b/tox.ini @@ -150,7 +150,7 @@ deps = test: grpcio == 1.75.1 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: protobuf == 5.27.2 # Compatible with Python 3.14; should be backwards compatible with grpcio-tools 1.59.0 test: ni.grpcdevice.v1.proto >= 1.0.0 # Provides session_pb2 package at runtime since we no longer generate it per-driver - test: nitlsconfig >= 1.0.0a2 # Pinned while the API is still moving through alphas + test: nitlsconfig >= 1.0.0a2 build_test: pytest build_test: coverage build_test: mako From eb5ae69b6a3ae36f4b26c96ffadc420b150bd4d6 Mon Sep 17 00:00:00 2001 From: dummy Date: Wed, 26 Aug 2026 17:12:38 -0500 Subject: [PATCH 3/5] Add unit testing for the new nitlsconfig integration tests using a fake channel that is tagged as an nitlsconfigtest channel for checking audit and error behavior --- .../nifake/nifake/unit_tests/conftest.py | 16 ++++++++++++++++ .../nifake/nifake/unit_tests/test_grpc.py | 19 +++++++++++++++++++ .../nifake/nifake/unit_tests/test_session.py | 19 +++++++++++++++++++ src/nifake/unit_tests/conftest.py | 19 +++++++++++++++++++ src/nifake/unit_tests/test_grpc.py | 19 +++++++++++++++++++ src/nifake/unit_tests/test_session.py | 19 +++++++++++++++++++ 6 files changed, 111 insertions(+) create mode 100644 generated/nifake/nifake/unit_tests/conftest.py create mode 100644 src/nifake/unit_tests/conftest.py diff --git a/generated/nifake/nifake/unit_tests/conftest.py b/generated/nifake/nifake/unit_tests/conftest.py new file mode 100644 index 0000000000..d4dab6bfd3 --- /dev/null +++ b/generated/nifake/nifake/unit_tests/conftest.py @@ -0,0 +1,16 @@ +import grpc +import nitlsconfig.channel_tag +import pytest + + +@pytest.fixture +def nitls_tagged_channel(): + """A real gRPC channel tagged the way nitlsconfig.create_grpc_device_channel tags one. + + The factory itself shells out to the NI-installed nitlsconfig CLI, so it cannot run here. + """ + target = 'localhost:31763' + with grpc.insecure_channel(target) as channel: + nitlsconfig.channel_tag.tag_channel_target(channel, target) + assert nitlsconfig.channel_tag.is_nitls_channel(channel), 'nitlsconfig no longer recognizes a channel it tagged' + yield channel diff --git a/generated/nifake/nifake/unit_tests/test_grpc.py b/generated/nifake/nifake/unit_tests/test_grpc.py index bd47be561d..f47e7e72e5 100644 --- a/generated/nifake/nifake/unit_tests/test_grpc.py +++ b/generated/nifake/nifake/unit_tests/test_grpc.py @@ -4,6 +4,7 @@ import math import nifake import nifake.errors +import nitlsconfig import numpy import pytest import session_pb2 @@ -184,6 +185,24 @@ def test_server_unavailable(self): assert e.description == expected_error_message assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_server_unavailable_with_tls_elaboration(self, nitls_tagged_channel): + library_func = 'InitWithOptions' + grpc_error = grpc.StatusCode.UNAVAILABLE + self._set_side_effect(library_func, side_effect=MyRpcError(None, '', grpc_error=grpc_error)) + # The real elaboration, so this fails if nitlsconfig stops recognizing our channel. + expected_error_message = nitlsconfig.get_tls_connection_error_elaboration(nitls_tagged_channel) + assert expected_error_message is not None + assert expected_error_message != 'Failed to connect to server' + grpc_options = nifake.GrpcSessionOptions(nitls_tagged_channel, '', initialization_behavior=nifake.SessionInitializationBehavior.AUTO) + interpreter = nifake._grpc_stub_interpreter.GrpcStubInterpreter(grpc_options) + try: + interpreter.init_with_options('dev1', False, False, '') + assert False + except nifake.Error as e: + assert e.rpc_code == grpc_error + assert e.description == expected_error_message + assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_function_not_implemented(self): library_func = 'PoorlyNamedSimpleFunction' grpc_error = grpc.StatusCode.UNIMPLEMENTED diff --git a/generated/nifake/nifake/unit_tests/test_session.py b/generated/nifake/nifake/unit_tests/test_session.py index 36e45fb06f..5166d14a5a 100644 --- a/generated/nifake/nifake/unit_tests/test_session.py +++ b/generated/nifake/nifake/unit_tests/test_session.py @@ -68,6 +68,12 @@ def test_init_with_options_nondefault_and_close(self): session.close() self.patched_library_interpreter.close.assert_called_once_with() + def test_init_without_grpc_options_does_not_audit(self): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1'): + pass + patched_audit.assert_not_called() + def test_close(self): session = nifake.Session('dev1') assert session._interpreter._vi == SESSION_NUM_FOR_TEST @@ -966,6 +972,19 @@ def test_init_with_options_and_close(self): session.close() self.patched_grpc_interpreter.close.assert_called_once_with() + def test_init_audits_successful_connect(self, nitls_tagged_channel): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')): + pass + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, True) + + def test_init_audits_failed_connect(self, nitls_tagged_channel): + self.patched_grpc_interpreter.init_with_options.side_effect = nifake.Error('Fake init failure') + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with pytest.raises(nifake.Error): + nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')) + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, False) + # Session locking def test_lock_session_none(self): diff --git a/src/nifake/unit_tests/conftest.py b/src/nifake/unit_tests/conftest.py new file mode 100644 index 0000000000..a4aeca5503 --- /dev/null +++ b/src/nifake/unit_tests/conftest.py @@ -0,0 +1,19 @@ +import grpc +import nitlsconfig.channel_tag +import pytest + + +@pytest.fixture +def nitls_tagged_channel(): + """A real gRPC channel tagged the way nitlsconfig.create_grpc_device_channel tags one. + + nitlsconfig.create_grpc_device_channel itself requires the nitlsconfig library to be + installed on the system in order to access the CLI. As a result, we are unable to directly + leverage it in our unit tests. But, this can at least allow us to unit-test our code + that relies on channels being created from nitlsconfig.create_grpc_device_channel. + """ + target = 'localhost:31763' + with grpc.insecure_channel(target) as channel: + nitlsconfig.channel_tag.tag_channel_target(channel, target) + assert nitlsconfig.channel_tag.is_nitls_channel(channel), 'nitlsconfig no longer recognizes a channel it tagged' + yield channel diff --git a/src/nifake/unit_tests/test_grpc.py b/src/nifake/unit_tests/test_grpc.py index bd47be561d..f47e7e72e5 100644 --- a/src/nifake/unit_tests/test_grpc.py +++ b/src/nifake/unit_tests/test_grpc.py @@ -4,6 +4,7 @@ import math import nifake import nifake.errors +import nitlsconfig import numpy import pytest import session_pb2 @@ -184,6 +185,24 @@ def test_server_unavailable(self): assert e.description == expected_error_message assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_server_unavailable_with_tls_elaboration(self, nitls_tagged_channel): + library_func = 'InitWithOptions' + grpc_error = grpc.StatusCode.UNAVAILABLE + self._set_side_effect(library_func, side_effect=MyRpcError(None, '', grpc_error=grpc_error)) + # The real elaboration, so this fails if nitlsconfig stops recognizing our channel. + expected_error_message = nitlsconfig.get_tls_connection_error_elaboration(nitls_tagged_channel) + assert expected_error_message is not None + assert expected_error_message != 'Failed to connect to server' + grpc_options = nifake.GrpcSessionOptions(nitls_tagged_channel, '', initialization_behavior=nifake.SessionInitializationBehavior.AUTO) + interpreter = nifake._grpc_stub_interpreter.GrpcStubInterpreter(grpc_options) + try: + interpreter.init_with_options('dev1', False, False, '') + assert False + except nifake.Error as e: + assert e.rpc_code == grpc_error + assert e.description == expected_error_message + assert str(e) == f'StatusCode.UNAVAILABLE: {expected_error_message}' + def test_function_not_implemented(self): library_func = 'PoorlyNamedSimpleFunction' grpc_error = grpc.StatusCode.UNIMPLEMENTED diff --git a/src/nifake/unit_tests/test_session.py b/src/nifake/unit_tests/test_session.py index 36e45fb06f..5166d14a5a 100644 --- a/src/nifake/unit_tests/test_session.py +++ b/src/nifake/unit_tests/test_session.py @@ -68,6 +68,12 @@ def test_init_with_options_nondefault_and_close(self): session.close() self.patched_library_interpreter.close.assert_called_once_with() + def test_init_without_grpc_options_does_not_audit(self): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1'): + pass + patched_audit.assert_not_called() + def test_close(self): session = nifake.Session('dev1') assert session._interpreter._vi == SESSION_NUM_FOR_TEST @@ -966,6 +972,19 @@ def test_init_with_options_and_close(self): session.close() self.patched_grpc_interpreter.close.assert_called_once_with() + def test_init_audits_successful_connect(self, nitls_tagged_channel): + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')): + pass + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, True) + + def test_init_audits_failed_connect(self, nitls_tagged_channel): + self.patched_grpc_interpreter.init_with_options.side_effect = nifake.Error('Fake init failure') + with patch('nitlsconfig.audit_session_connect', autospec=True) as patched_audit: + with pytest.raises(nifake.Error): + nifake.Session('dev1', grpc_options=nifake.GrpcSessionOptions(nitls_tagged_channel, '')) + patched_audit.assert_called_once_with('NI-FAKE', nitls_tagged_channel, False) + # Session locking def test_lock_session_none(self): From 6a7c19e1452eeed21aa26eeec74e66dc4a9e50a8 Mon Sep 17 00:00:00 2001 From: dummy Date: Wed, 26 Aug 2026 17:14:48 -0500 Subject: [PATCH 4/5] Update codegen to also include the updated comment I missed --- generated/nifake/nifake/unit_tests/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/generated/nifake/nifake/unit_tests/conftest.py b/generated/nifake/nifake/unit_tests/conftest.py index d4dab6bfd3..a4aeca5503 100644 --- a/generated/nifake/nifake/unit_tests/conftest.py +++ b/generated/nifake/nifake/unit_tests/conftest.py @@ -7,7 +7,10 @@ def nitls_tagged_channel(): """A real gRPC channel tagged the way nitlsconfig.create_grpc_device_channel tags one. - The factory itself shells out to the NI-installed nitlsconfig CLI, so it cannot run here. + nitlsconfig.create_grpc_device_channel itself requires the nitlsconfig library to be + installed on the system in order to access the CLI. As a result, we are unable to directly + leverage it in our unit tests. But, this can at least allow us to unit-test our code + that relies on channels being created from nitlsconfig.create_grpc_device_channel. """ target = 'localhost:31763' with grpc.insecure_channel(target) as channel: From 7ff791eb8c80a835abbe3b87aff5337dbfb76fd9 Mon Sep 17 00:00:00 2001 From: dummy Date: Wed, 26 Aug 2026 19:29:29 -0500 Subject: [PATCH 5/5] Trigger CI