openlawlibrary / pygls

A pythonic generic language server
https://pygls.readthedocs.io/en/latest/
Apache License 2.0
585 stars 104 forks source link

Startup hook #180

Closed suzizecat closed 3 years ago

suzizecat commented 3 years ago

Hi !

A the moment, it seems that there is no reliable way to trigger something (in example asking for the client configuration) after the server startup and initialization.

I tried to use the INITIALIZED handler with

@diplomat_server.feature(INITIALIZED)
def on_initialized(ls : DiplomatLanguageServer,params : InitializedParams) :
    get_client_config(ls)
    ls.show_message_log("Diplomat server is initialized.")
    return None

where get_client_config is (simplified) :

@diplomat_server.thread()
@diplomat_server.command(DiplomatLanguageServer.CMD_GET_CONFIGURATION)
def get_client_config(ls: DiplomatLanguageServer, *args):
    logger.debug("Refresh configuration")
    ls.show_message_log("Configuration requested")

    config = ls.get_configuration(ConfigurationParams(items=[
        ConfigurationItem(
            scope_uri='',
            section=DiplomatLanguageServer.CONFIGURATION_SECTION)
    ])).result(2)[0]
    logger.debug("Got configuration back")

this function works well in stand alone (or from a didConfigurationChanged handler) but when used in initialized handler, I get

Configuration requested
Failed to handle user defined notification "initialized": (InitializedParams(),)
Traceback (most recent call last):
  File "/dpt/adh/flow/digital/apps/python/3.6.8/lib/python3.6/site-packages/pygls/protocol.py", line 68, in decorator
    self._execute_notification(user_func, *args, **kwargs)
  File "/dpt/adh/flow/digital/apps/python/3.6.8/lib/python3.6/site-packages/pygls/protocol.py", line 238, in _execute_notification
    handler(*params)
  File "/home/jfaucher/local-apps/diplomat-ls/main.py", line 97, in on_initialized
    _update_config(ls)
  File "/home/jfaucher/local-apps/diplomat-ls/main.py", line 157, in _update_config
    ])).result(2)[0]
  File "/dpt/adh/flow/digital/apps/python/3.6.8/lib/python3.6/concurrent/futures/_base.py", line 434, in result
    raise TimeoutError()
concurrent.futures._base.TimeoutError

Am I doing it wrong ? If I'm not supposed to use INITIALIZED, it would be nice to have a word about that in the documentation and "hook" for "the server is done with initialization step and is ready to work with the client"

Best regards, Julien FAUCHER

danixeee commented 3 years ago

Could you try to get the configuration with async/await, and let me know about the results?

suzizecat commented 3 years ago

So, with :

@diplomat_server.feature(INITIALIZED)
async def on_initialized(ls : DiplomatLanguageServer,params : InitializedParams) :
    await get_client_config(ls)
    ls.show_message_log("Diplomat server is initialized.")

@diplomat_server.command(DiplomatLanguageServer.CMD_GET_CONFIGURATION)
async def get_client_config(ls: DiplomatLanguageServer, *args):
    logger.debug("Refresh configuration")
    ls.show_message("Configuration requested")

    config = await ls.get_configuration(ConfigurationParams(items=[
        ConfigurationItem(
            scope_uri='',
            section=DiplomatLanguageServer.CONFIGURATION_SECTION)
    ])).result(2)[0]
    ls.show_message_log("Got configuration back")

I have the "Configuration requested" notification in VSCode as expected, however the "Got configuration back" never appears. Instead, the log window shows :

Exception occurred in notification: "{'code': -32602, 'message': 'None: None', 'data': "{'traceback': []}"}"
NoneType: None

And if I try to run any command (here the manual call to refresh configuration) I have :

Traceback (most recent call last):
  File "xxx/python3.6/site-packages/pygls/protocol.py", line 278, in _execute_request_callback
    method_name, method_type, msg_id, result=future.result())
  File "xxx/python3.6/site-packages/pygls/feature_manager.py", line 68, in wrapped
    return await f(server, *args, **kwargs)
  File "/home/jfaucher/local-apps/diplomat-ls/main.py", line 155, in get_client_config
    ])).result(2)[0]
  File "xxx/python3.6/concurrent/futures/_base.py", line 434, in result
    raise TimeoutError()
concurrent.futures._base.TimeoutError
[Error - 4:12:02 PM] Request workspace/executeCommand failed.
  Message: concurrent.futures._base.TimeoutError
  Code: -32602 
alcarney commented 3 years ago

@suzizecat I've been playing around with something similar and managed to get the following to work for me

    @server.feature(INITIALIZED)
    async def on_initialized(rst: RstLanguageServer, params):
        rst.logger.debug(INITIALIZED)

        config_params = ConfigurationParams(
            items=[ConfigurationItem(section="esbonio.sphinx")]
        )
        config = await rst.get_configuration_async(config_params)
        rst.logger.debug(config)

It looks like there are "normal" and "async" versions of the get_configuration function

danixeee commented 3 years ago

@suzizecat Have a look at the example, there are all three ways of getting the configuration from the client, or look at @alcarney's answer.

Probably increasing the wait time (.result(2) -> .result(4)) will fix your "sync implementation", but I would go with async/await. If you need to be sure that configuration is set before doing anything else, then maybe set some flag once configuration is received in the initialize hook.

suzizecat commented 3 years ago

@suzizecat Have a look at the example, there are all three ways of getting the configuration from the client, or look at @alcarney's answer.

I saw those three versions and have no issue with them. I don't really have any issue with getting the configuration through a command (as done in the example). My issue is rather on how to automatically trigger something, upon server initialization, on a server initiative.

danixeee commented 3 years ago

@suzizecat Does this work for you?

suzizecat commented 3 years ago

@danixeee : @alcarney solution seems to work just fine... Also, when actually replicating the get_client_config content within the INITIALIZED hook, it seems to works. Maybe there is an issue when calling a function registered with the thread() decorator from a non-threaded function ?

Anyway, it works ! Thanks !

danixeee commented 3 years ago

@suzizecat It could be because of @diplomat_server.command(DiplomatLanguageServer.CMD_GET_CONFIGURATION) and async decorated function.

Extract the code for getting the configuration into a separate function, and call the function on multiple places:

@diplomat_server.feature(INITIALIZED)
async def on_initialized(ls : DiplomatLanguageServer,params : InitializedParams) :
    config = await get_client_config(ls)

@diplomat_server.command(DiplomatLanguageServer.CMD_GET_CONFIGURATION)
async def on_get_configuration(ls: DiplomatLanguageServer, *args):
    config = await get_client_config(ls)

async def get_client_config(ls : DiplomatLanguageServer):
    config_params = ConfigurationParams(items=[
        ConfigurationItem(
            scope_uri='',
            section=DiplomatLanguageServer.CONFIGURATION_SECTION)
    ])
    return await ls.get_configuration_async(config_params)