// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting.Contracts; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol.Channel; using Microsoft.SqlTools.ServiceLayer.SqlContext; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.Extensibility { public class ExtensionServiceHost : ServiceHostBase { private ExtensibleServiceHostOptions options; public ExtensionServiceProvider serviceProvider; private List initializedServices = new List(); public ExtensionServiceHost( ExtensibleServiceHostOptions options ) : base(new StdioServerChannel()) { this.options = options; this.Initialize(); // Start the service only after all request handlers are setup. This is vital // as otherwise the Initialize event can be lost - it's processed and discarded before the handler // is hooked up to receive the message this.Start().GetAwaiter().GetResult(); } private void Initialize() { base.Initialize(); this.serviceProvider = ExtensionServiceProvider.CreateFromAssembliesInDirectory(options.ExtensionServiceAssemblyDirectory, options.ExtensionServiceAssemblyDllFileNames); var hostDetails = new HostDetails( name: options.HostName, profileId: options.HostProfileId, version: options.HostVersion); SqlToolsContext sqlToolsContext = new SqlToolsContext(hostDetails); serviceProvider.RegisterSingleService(sqlToolsContext); serviceProvider.RegisterSingleService(this); this.InitializeHostedServices(); this.InitializeRequestHandlers(); } private void InitializeRequestHandlers() { // Register the requests that this service host will handle this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest); this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest); this.SetRequestHandler(VersionRequest.Type, this.HandleVersionRequest); } private void InitializeHostedServices() { // Pre-register all services before initializing. This ensures that if one service wishes to reference // another one during initialization, it will be able to safely do so foreach (IHostedService service in this.serviceProvider.GetServices()) { if (IsServiceInitialized(service)) { continue; } Logger.Verbose("Registering service: " + service.GetType()); this.RegisterService(service); } foreach (IHostedService service in this.serviceProvider.GetServices()) { if (IsServiceInitialized(service)) { continue; } Logger.Verbose("Initializing service: " + service.GetType()); // Initialize all hosted services, and register them in the service provider for their requested // service type. This ensures that when searching for the ConnectionService you can get it without // searching for an IHostedService of type ConnectionService this.InitializeService(service); } } private bool IsServiceInitialized(IHostedService service) { return this.initializedServices.Any(s => s.GetType() == service.GetType()); } /// /// Delegate definition for the host shutdown event /// public delegate Task ShutdownCallback(object shutdownParams, RequestContext shutdownRequestContext); /// /// Delegate definition for the host initialization event /// public delegate Task InitializeCallback(InitializeRequest startupParams, RequestContext requestContext); private readonly List shutdownCallbacks = new List(); private readonly List initializeCallbacks = new List(); private readonly Version serviceVersion = Assembly.GetEntryAssembly().GetName().Version; /// /// Adds a new callback to be called when the shutdown request is submitted /// /// Callback to perform when a shutdown request is submitted public void RegisterShutdownTask(ShutdownCallback callback) { shutdownCallbacks.Add(callback); } /// /// Add a new method to be called when the initialize request is submitted /// /// Callback to perform when an initialize request is submitted public void RegisterInitializeTask(InitializeCallback callback) { initializeCallbacks.Add(callback); } /// /// Handles the shutdown event for the Language Server /// private async Task HandleShutdownRequest(object shutdownParams, RequestContext requestContext) { Logger.Write(TraceEventType.Information, "Service host is shutting down..."); // Call all the shutdown methods provided by the service components Task[] shutdownTasks = shutdownCallbacks.Select(t => t(shutdownParams, requestContext)).ToArray(); TimeSpan shutdownTimeout = TimeSpan.FromSeconds(options.ShutdownTimeoutInSeconds); // shut down once all tasks are completed, or after the timeout expires, whichever comes first. await Task.WhenAny(Task.WhenAll(shutdownTasks), Task.Delay(shutdownTimeout)).ContinueWith(t => Environment.Exit(0)); } /// /// Handles the initialization request /// private async Task HandleInitializeRequest(InitializeRequest initializeParams, RequestContext requestContext) { try { // Call all tasks that registered on the initialize request var initializeTasks = initializeCallbacks.Select(t => t(initializeParams, requestContext)); await Task.WhenAll(initializeTasks); // Send back what this server can do await requestContext.SendResult( new InitializeResult { Capabilities = options.ServerCapabilities }); } catch (Exception e) { await requestContext.SendError(e.Message); } } /// /// Handles the version request. Sends back the server version as result. /// private async Task HandleVersionRequest(object versionRequestParams, RequestContext requestContext) { await requestContext.SendResult(serviceVersion.ToString()); } /// /// Loads and initializes the services from the given assemblies /// /// path of the dll files public void LoadAndIntializeServicesFromAssesmblies(string[] assemblyPaths) { this.serviceProvider.AddAssemblies(options.ExtensionServiceAssemblyDirectory, assemblyPaths); this.InitializeHostedServices(); } /// /// Registers and initializes the given service /// /// service to be initialized protected void RegisterService(IHostedService service) { this.serviceProvider.RegisterSingleService(service.GetType(), service); } protected void InitializeService(IHostedService service) { service.InitializeService(this); this.initializedServices.Add(service); if (this.options.InitializeServiceCallback != null) { this.options.InitializeServiceCallback(this, service); } } /// /// Registers and initializes the given services /// /// services to be initalized public void RegisterAndInitializedServices(IEnumerable services) { foreach (IHostedService service in services) { this.RegisterService(service); this.InitializeService(service); } } /// /// Register and initializes the given service /// /// service to be initialized public void RegisterAndInitializeService(IHostedService service) { this.RegisterService(service); this.InitializeService(service); } } public class ExtensibleServiceHostOptions { /// /// The folder where the extension service assemblies are located. By default it is /// the folder where the current server assembly is located. /// public string ExtensionServiceAssemblyDirectory { get; set; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); /// /// The dlls that contain the extension services. /// public string[] ExtensionServiceAssemblyDllFileNames { get; set; } = new string[0]; /// /// Host name for the services. /// public string HostName { get; set; } = HostDetails.DefaultHostName; /// /// Gets the profile ID of the host, used to determine the /// host-specific profile path. /// public string HostProfileId { get; set; } = HostDetails.DefaultHostProfileId; /// /// Gets the version of the host. /// public Version HostVersion { get; set; } = HostDetails.DefaultHostVersion; /// /// Data protocol capabilities that the server supports. /// public ServerCapabilities ServerCapabilities { get; set; } = new ServerCapabilities { DefinitionProvider = false, ReferencesProvider = false, DocumentFormattingProvider = false, DocumentRangeFormattingProvider = false, DocumentHighlightProvider = false, HoverProvider = false }; /// /// Timeout in seconds for the shutdown request. Default is 120 seconds. /// public int ShutdownTimeoutInSeconds { get; set; } = 120; public delegate void InitializeService(ExtensionServiceHost serviceHost, IHostedService service); /// /// Service initialization callback. The caller must define this callback to initialize the service. /// /// public InitializeService InitializeServiceCallback { get; set; } } }