Skip to content
2 changes: 1 addition & 1 deletion ossdbtoolsservice/admin/admin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def register(self, service_provider: ServiceProvider):
def _handle_get_database_info_request(self, request_context: RequestContext, params: GetDatabaseInfoParameters) -> None:
# Retrieve the connection from the connection service
connection_service = self._service_provider[constants.CONNECTION_SERVICE_NAME]
connection: ServerConnection = connection_service.get_connection(params.owner_uri, ConnectionType.DEFAULT)
connection: ServerConnection = connection_service.get_connection(params.owner_uri, ConnectionType.DEFAULT, request_context)

# Get database owner
owner_result = connection.get_database_owner()
Expand Down
34 changes: 23 additions & 11 deletions ossdbtoolsservice/connection/connection_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
)

from ossdbtoolsservice.hosting import RequestContext, ServiceProvider
from ossdbtoolsservice.utils import constants
from ossdbtoolsservice.utils import constants as utils_constants
from ossdbtoolsservice.utils.telemetry import send_error_telemetry_notification
from ossdbtoolsservice.exception import constants as error_constants
from ossdbtoolsservice.utils.cancellation import CancellationToken
from ossdbtoolsservice.driver import ServerConnection, ConnectionManager

Expand Down Expand Up @@ -90,7 +92,7 @@ def register(self, service_provider: ServiceProvider):
self._service_provider.server.set_request_handler(GET_CONNECTION_STRING_REQUEST, self.handle_get_connection_string_request)

# PUBLIC METHODS #######################################################
def connect(self, params: ConnectRequestParams) -> Optional[ConnectionCompleteParams]:
def connect(self, params: ConnectRequestParams, request_context: RequestContext = None) -> Optional[ConnectionCompleteParams]:
"""
Open a connection using the given connection information.

Expand Down Expand Up @@ -121,12 +123,12 @@ def connect(self, params: ConnectRequestParams) -> Optional[ConnectionCompletePa

# Get the type of server and config
provider_name = self._service_provider.provider
config = self._service_provider[constants.WORKSPACE_SERVICE_NAME].configuration
config = self._service_provider[utils_constants.WORKSPACE_SERVICE_NAME].configuration
try:
# Get connection to DB server using the provided connection params
connection: ServerConnection = ConnectionManager(provider_name, config, params.connection.options).get_connection()
except Exception as err:
return _build_connection_response_error(connection_info, params.type, err)
return _build_connection_response_error(connection_info, params.type, request_context, err)
finally:
# Remove this thread's cancellation token if needed
with self._cancellation_lock:
Expand All @@ -141,7 +143,7 @@ def connect(self, params: ConnectRequestParams) -> Optional[ConnectionCompletePa

# The connection was not canceled, so add the connection and respond
connection_info.add_connection(params.type, connection)
self._notify_on_connect(params.type, connection_info)
self._notify_on_connect(params.type, connection_info, request_context)
return _build_connection_response(connection_info, params.type)

def disconnect(self, owner_uri: str, connection_type: Optional[ConnectionType]) -> bool:
Expand All @@ -157,7 +159,7 @@ def disconnect(self, owner_uri: str, connection_type: Optional[ConnectionType])
connection_info = self.owner_to_connection_map.get(owner_uri)
return self._close_connections(connection_info, connection_type) if connection_info is not None else False

def get_connection(self, owner_uri: str, connection_type: ConnectionType) -> Optional[ServerConnection]:
def get_connection(self, owner_uri: str, connection_type: ConnectionType, request_context: RequestContext = None) -> Optional[ServerConnection]:
"""
Get a connection for the given owner URI and connection type

Expand All @@ -168,7 +170,7 @@ def get_connection(self, owner_uri: str, connection_type: ConnectionType) -> Opt
raise ValueError('No connection associated with given owner URI')

if not connection_info.has_connection(connection_type) or not connection_info.get_connection(connection_type).open:
self.connect(ConnectRequestParams(connection_info.details, owner_uri, connection_type))
self.connect(ConnectRequestParams(connection_info.details, owner_uri, connection_type), request_context)
return connection_info.get_connection(connection_type)

def register_on_connect_callback(self, task: Callable[[ConnectionInfo], None]) -> None:
Expand Down Expand Up @@ -199,8 +201,13 @@ def handle_list_databases(self, request_context: RequestContext, params: ListDat
"""List all databases on the server that the given URI has a connection to"""
connection = None
try:
connection = self.get_connection(params.owner_uri, ConnectionType.DEFAULT)
connection = self.get_connection(params.owner_uri, ConnectionType.DEFAULT, request_context)
except ValueError as err:
send_error_telemetry_notification(
request_context,
error_constants.CONNECTION,
error_constants.LIST_DATABASES_CONNECTION_VALUE_ERROR,
error_constants.LIST_DATABASE_GET_CONNECTION_VALUE_ERROR)
request_context.send_error(str(err))
return

Expand Down Expand Up @@ -261,14 +268,14 @@ def _connect_and_respond(self, request_context: RequestContext, params: ConnectR
if response is not None:
request_context.send_notification(CONNECTION_COMPLETE_METHOD, response)

def _notify_on_connect(self, conn_type: ConnectionType, info: ConnectionInfo) -> None:
def _notify_on_connect(self, conn_type: ConnectionType, info: ConnectionInfo, request_context: RequestContext = None) -> None:
"""
Sends a notification to any listeners that a new connection has been established.
Only sent if the connection is a new, defalt connection
"""
if (conn_type == ConnectionType.DEFAULT):
for callback in self._on_connect_callbacks:
callback(info)
callback(info, request_context)

@staticmethod
def _close_connections(connection_info: ConnectionInfo, connection_type=None):
Expand Down Expand Up @@ -318,7 +325,7 @@ def _build_connection_response(connection_info: ConnectionInfo, connection_type:
return response


def _build_connection_response_error(connection_info: ConnectionInfo, connection_type: ConnectionType, err)\
def _build_connection_response_error(connection_info: ConnectionInfo, connection_type: ConnectionType, request_context: RequestContext, err: Exception)\
-> ConnectionCompleteParams:
"""Build a connection complete response object"""
response: ConnectionCompleteParams = ConnectionCompleteParams()
Expand All @@ -342,6 +349,11 @@ def _build_connection_response_error(connection_info: ConnectionInfo, connection
response.messages = errorMessage
response.error_message = errorMessage

send_error_telemetry_notification(
request_context, error_constants.CONNECTION,
error_constants.BUILD_CONNECTION_ERROR,
error_constants.BUILD_CONNECTION_ERROR_CODE
)
return response


Expand Down
10 changes: 5 additions & 5 deletions ossdbtoolsservice/driver/types/psycopg_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,14 @@ def get_error_message(self, error) -> str:
"""
Get the message from DatabaseError instance
"""
# If error.diag.message_primary is not None, return it.
if error.diag and error.diag.message_primary:
return error.diag.message_primary

# If error.args exists and has at least one element, return the first element as the error message.
elif hasattr(error, 'args') and error.args and len(error.args) > 0:
if hasattr(error, 'args') and error.args and len(error.args) > 0:
return error.args[0]

# If error.diag.message_primary is not None, return it.
elif error.diag and error.diag.message_primary:
return error.diag.message_primary

# If neither is available, return a generic error message.
else:
return "An unspecified database error occurred."
Expand Down
4 changes: 4 additions & 0 deletions ossdbtoolsservice/exception/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
70 changes: 70 additions & 0 deletions ossdbtoolsservice/exception/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# This module holds the error resource constants for ossdb tools service errors
# According to the PostgreSQL standard, the first two characters of an error code denote a class of errors, while the
# last three characters indicate a specific condition within that class
# Read more here: https://www.postgresql.org/docs/current/errcodes-appendix.html

# INTERNAL ERROR CODES
REQUEST_METHOD_PROCESSING_UNHANDLED_EXCEPTION = 'AD004'
EXECUTE_QUERY_GET_CONNECTION_ERROR = 'AD005'
EXECUTE_DEPLOY_GET_CONNECTION_ERROR = 'AD006'
CANCEL_QUERY_ERROR = 'AD007'
DISPOSE_QUERY_REQUEST_ERROR = 'AD008'
UNSUPPORTED_REQUEST_METHOD = 'AD009'
LIST_DATABASE_GET_CONNECTION_VALUE_ERROR = 'AD010'
LIST_DATABASE_ERROR = 'AD011'
EDIT_DATA_CUSTOM_QUERY_UNSUPPORTED_ERROR = 'AD012'
EDIT_DATA_COMMIT_FAILURE = 'AD013'
EDIT_DATA_SESSION_NOT_FOUND = 'AD014'
EDIT_DATA_SESSION_OPERATION_FAILURE = 'AD015'
GET_METADATA_FAILURE = 'AD016'
OBJECT_EXPLORER_CREATE_SESSION_ERROR = 'AD017'
OBJECT_EXPLORER_CLOSE_SESSION_ERROR = 'AD018'
OBJECT_EXPLORER_EXPAND_NODE_ERROR = 'AD019'
ANOTHER_QUERY_EXECUTING_ERROR = 'AD020'
DISPOSE_REQUEST_NO_QUERY_ERROR = 'AD021'
SAVE_QUERY_RESULT_ERROR = 'AD022'
SCRIPTAS_REQUEST_ERROR = 'AD023'
UNKNOWN_CONNECTION_ERROR = 'AD024'
PSYCOPG_DRIVER_UNKNOWN_ERROR_CODE = 'AD025'
NEW_DATABASE_GET_CHARSETS_ERROR_CODE = 'AD026'
NEW_DATABASE_GET_COLLATIONS_ERROR_CODE = 'AD027'
NEW_DATABASE_CREATE_ERROR_CODE = 'AD028'
BUILD_CONNECTION_ERROR_CODE = 'AD029'
LIST_METADATA_FAILURE_ERROR_CODE = 'AD030'

# ErrorTelemetryViews
CONNECTION = 'Connection'
EDIT_DATA = 'Edit Data'
JSON_RPC = 'Json Rpc'
METADATA = 'Metadata'
OBJECT_EXPLORER = 'Object Explorer'
QUERY_EXECUTION = 'Query Execution'
SCRIPTING = 'Scripting'

# ErrorTelmetryNames
LIST_DATABASES_CONNECTION_VALUE_ERROR = 'List Databases Connection Value Error'
LIST_DATABASES_ERROR = 'List Databases Error'
BUILD_CONNECTION_ERROR = 'Build Connection Error'
EDIT_DATA_CUSTOM_QUERY = 'Edit Data Custom Query Unsupported'
EDIT_DATA_COMMIT = 'Edit Data Commit Failure'
EDIT_DATA_SESSION_NOT_FOUND = 'Edit Data Session Not Found'
EDIT_DATA_SESSION_OPERATION = 'Edit Data Session Operation Failure'
UNSUPPORTED_REQUEST = 'Unsupported Request Method'
REQUEST_METHOD_PROCESSING = 'Request Method Processing Unhandled Exception'
LIST_METADATA_FAILURE = 'List Metadata Failure'
OBJECT_EXPLORER_CREATE_SESSION = 'Object Explorer Create Session Error'
OBJECT_EXPLORER_CLOSE_SESSION = 'Object Explorer Close Session Error'
OBJECT_EXPLORER_EXPAND_NODE = 'Object Explorer Expand Node Error'
EXECUTE_QUERY_GET_CONNECTION = 'Execute Query Get Connection Error'
EXECUTE_DEPLOY_GET_CONNECTION = 'Execute Deploy Get Connection Error'
ANOTHER_QUERY_EXECUTING = 'Another Query Executing Error'
CANCEL_QUERY = 'Cancel Query'
DISPOSE_QUERY_NO_QUERY = 'Dispose Query No Query Error'
DISPOSE_QUERY_REQUEST = 'Dispose Query Request Error'
SAVE_QUERY_RESULT = 'Save Query Result Error'
SCRIPT_AS_REQUEST = 'Script As Request Error'
12 changes: 12 additions & 0 deletions ossdbtoolsservice/hosting/json_rpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
from ossdbtoolsservice.hosting.json_message import JSONRPCMessage, JSONRPCMessageType
from ossdbtoolsservice.hosting.json_reader import JSONRPCReader
from ossdbtoolsservice.hosting.json_writer import JSONRPCWriter
from ossdbtoolsservice.utils.telemetry import send_error_telemetry_notification
from ossdbtoolsservice.exception import constants as error_constants


class JSONRPCServer:
Expand Down Expand Up @@ -267,6 +269,11 @@ def _dispatch_message(self, message):
# Make sure we got a handler for the request
if handler is None:
# TODO: Localize?
send_error_telemetry_notification(
request_context,
error_constants.JSON_RPC,
error_constants.UNSUPPORTED_REQUEST,
error_constants.UNSUPPORTED_REQUEST_METHOD)
request_context.send_error(f'Requested method is unsupported: {message.message_method}')
if self._logger is not None:
self._logger.warn('Requested method is unsupported: %s', message.message_method)
Expand All @@ -285,6 +292,11 @@ def _dispatch_message(self, message):
error_message = f'Unhandled exception while handling request method {message.message_method}: "{e}"' # TODO: Localize
if self._logger is not None:
self._logger.exception(error_message)
send_error_telemetry_notification(
request_context,
error_constants.JSON_RPC,
error_constants.REQUEST_METHOD_PROCESSING,
error_constants.REQUEST_METHOD_PROCESSING_UNHANDLED_EXCEPTION)
request_context.send_error(error_message, code=-32603)
elif message.message_type is JSONRPCMessageType.Notification:
if self._logger is not None:
Expand Down
11 changes: 6 additions & 5 deletions ossdbtoolsservice/language/language_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,9 @@ def do_send_default_empty_response():
do_send_default_empty_response()

# SERVICE NOTIFICATION HANDLERS #####################################################
def on_connect(self, conn_info: ConnectionInfo) -> threading.Thread:
def on_connect(self, conn_info: ConnectionInfo, request_context: RequestContext) -> threading.Thread:
"""Set up intellisense cache on connection to a new database"""
return utils.thread.run_as_thread(self._build_intellisense_cache_thread, conn_info)
return utils.thread.run_as_thread(self._build_intellisense_cache_thread, conn_info, request_context)

# PROPERTIES ###########################################################
@property
Expand Down Expand Up @@ -278,12 +278,12 @@ def is_valid_uri(self, uri: str) -> bool:
"""
return uri in self._valid_uri

def _build_intellisense_cache_thread(self, conn_info: ConnectionInfo) -> None:
def _build_intellisense_cache_thread(self, conn_info: ConnectionInfo, request_context: RequestContext) -> None:
# TODO build the cache. For now, sending intellisense ready as a test
scriptparseinfo: ScriptParseInfo = self.get_script_parse_info(conn_info.owner_uri, create_if_not_exists=True)
if scriptparseinfo is not None:
# This is a connection for an actual script in the workspace. Build the intellisense cache for it
connection_context: ConnectionContext = self.operations_queue.add_connection_context(conn_info, False)
connection_context: ConnectionContext = self.operations_queue.add_connection_context(conn_info, False, request_context)
# Wait until the intellisense is completed before sending back the message and caching the key
connection_context.intellisense_complete.wait()
scriptparseinfo.connection_key = connection_context.key
Expand Down Expand Up @@ -366,7 +366,8 @@ def send_definition_using_connected_completions(self, request_context: RequestCo
matching_completion = next(completion for completion in completions if completion.display == word_under_cursor)
if matching_completion:
connection = self._connection_service.get_connection(params.text_document.uri,
ConnectionType.QUERY)
ConnectionType.QUERY,
request_context)
scripter_instance = scripter.Scripter(connection)
object_metadata = ObjectMetadata(None, None, matching_completion.display_meta,
matching_completion.display,
Expand Down
12 changes: 6 additions & 6 deletions ossdbtoolsservice/language/operations_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import ossdbtoolsservice.utils as utils
from ossdbtoolsservice.connection import ConnectionInfo, ConnectionService
from ossdbtoolsservice.connection.contracts import ConnectRequestParams, ConnectionType
from ossdbtoolsservice.hosting import ServiceProvider
from ossdbtoolsservice.hosting import ServiceProvider, RequestContext
from ossdbtoolsservice.language.completion_refresher import CompletionRefresher
from ossdbtoolsservice.driver import ServerConnection

Expand Down Expand Up @@ -117,7 +117,7 @@ def has_connection_context(self, conn_info: ConnectionInfo) -> bool:
key: str = OperationsQueue.create_key(conn_info)
return key in self._context_map

def add_connection_context(self, conn_info: ConnectionInfo, overwrite=False) -> ConnectionContext:
def add_connection_context(self, conn_info: ConnectionInfo, request_context: RequestContext, overwrite=False) -> ConnectionContext:
"""
Adds a connection context and returns the notification event.
If a connection queue exists alread, will overwrite if necesary
Expand All @@ -135,7 +135,7 @@ def add_connection_context(self, conn_info: ConnectionInfo, overwrite=False) ->
return context
# Create the context and start refresh
context = ConnectionContext(key, logger)
conn = self._create_connection(key, conn_info)
conn = self._create_connection(key, conn_info, request_context)
context.refresh_metadata(conn)
self._context_map[key] = context
return context
Expand All @@ -162,15 +162,15 @@ def create_key(cls, conn_info: ConnectionInfo) -> str:
"""
return '{0}|{1}|{2}'.format(conn_info.details.server_name, conn_info.details.database_name, conn_info.details.user_name)

def _create_connection(self, connection_key: str, conn_info: ConnectionInfo) -> Optional[ServerConnection]:
def _create_connection(self, connection_key: str, conn_info: ConnectionInfo, request_context: RequestContext) -> Optional[ServerConnection]:
conn_service = self._connection_service
key_uri = INTELLISENSE_URI + connection_key
connect_request = ConnectRequestParams(conn_info.details, key_uri, ConnectionType.INTELLISENSE)
connect_result = conn_service.connect(connect_request)
connect_result = conn_service.connect(connect_request, request_context)
if connect_result.error_message is not None:
raise RuntimeError(connect_result.error_message)

connection = conn_service.get_connection(key_uri, ConnectionType.INTELLISENSE)
connection = conn_service.get_connection(key_uri, ConnectionType.INTELLISENSE, request_context)
return connection

def _process_operations(self):
Expand Down
Loading