Merge branch 'dev'

This commit is contained in:
Benjamin Russell
2016-09-09 17:18:25 -07:00
198 changed files with 15574 additions and 1447 deletions

9
.gitignore vendored
View File

@@ -13,6 +13,7 @@ project.lock.json
*.user
*.userosscache
*.sln.docstates
*.exe
# Build results
[Dd]ebug/
@@ -29,7 +30,13 @@ msbuild.log
msbuild.err
msbuild.wrn
# code coverage artifacts
coverage.xml
node_modules
packages
reports
opencovertests.xml
sqltools.xml
# Cross building rootfs
cross/rootfs/

75
BUILD.md Normal file
View File

@@ -0,0 +1,75 @@
# Usage
Run `build.(ps1|sh)` with the desired set of arguments (see below for options).
The build script itself is `build.cake`, written in C# using the Cake build automation system.
All build related activites should be encapsulated in this file for cross-platform access.
# Arguments
## Primary
`-target=TargetName`: The name of the build task/target to execute (see below for listing and details).
Defaults to `Default`.
`-configuration=(Release|Debug)`: The configuration to build.
Defaults to `Release`.
## Extra
`-test-configuration=(Release|Debug)`: The configuration to use for the unit tests.
Defaults to `Debug`.
`-install-path=Path`: Path used for the **Install** target.
Defaults to `(%USERPROFILE%|$HOME)/.sqltoolsservice/local`
`-archive`: Enable the generation of publishable archives after a build.
# Targets
**Default**: Alias for Local.
**Local**: Full build including testing for the machine-local runtime.
**All**: Same as local, but targeting all runtimes selected by `PopulateRuntimes` in `build.cake`.
Currently configured to also build for a 32-bit Windows runtime on Windows machines.
No additional runtimes are currently selected on non-Windows machines.
**Quick**: Local build which skips all testing.
**Install**: Same as quick, but installs the generated binaries into `install-path`.
**SetPackageVersions**: Updates the dependency versions found within `project.json` files using information from `depversion.json`.
Used for maintainence within the project, not needed for end-users. More information below.
# Configuration files
## build.json
A number of build-related options, including folder names for different entities. Interesting options:
**DotNetInstallScriptURL**: The URL where the .NET SDK install script is located.
Can be used to pin to a specific script version, if a breaking change occurs.
**"DotNetChannel"**: The .NET SDK channel used for retreiving the tools.
**"DotNetVersion"**: The .NET SDK version used for the build. Can be used to pin to a specific version.
Using the string `Latest` will retrieve the latest version.
## depversion.json
A listing of all dependencies (and their desired versions) used by `project.json` files throughout the project.
Allows for quick and automatic updates to the dependency version numbers using the **SetPackageVersions** target.
# Artifacts generated
* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries built for the local machine in `artifacts/publish/Microsoft.SqlTools.ServiceLayer/default/{framework}/`
* Scripts to run Microsoft.SqlTools.ServiceLayer at `scripts/SQLTOOLSSERVICE(.Core)(.cmd)`
* These scripts are updated for every build and every install.
* The scripts point to the installed binary after and install, otherwise just the build folder (reset if a new build occurs without an install).
* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries cross-compiled for other runtimes (if selected in **PopulateRuntimes**) `artifacts/publish/Microsoft.SqlTools.ServiceLayer/{runtime}/{framework}/`
* Test logs in `artifacts/logs`
* Archived binaries in `artifacts/package` (only if `-archive` used on command line)
# Requirements
The build system requires Mono to be installed on non-Windows machines as Cake is not built using .NET Core (yet).

507
build.cake Normal file
View File

@@ -0,0 +1,507 @@
#addin "Newtonsoft.Json"
#load "scripts/runhelpers.cake"
#load "scripts/archiving.cake"
#load "scripts/artifacts.cake"
using System.ComponentModel;
using System.Net;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
// Basic arguments
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
// Optional arguments
var testConfiguration = Argument("test-configuration", "Debug");
var installFolder = Argument("install-path", System.IO.Path.Combine(Environment.GetEnvironmentVariable(IsRunningOnWindows() ? "USERPROFILE" : "HOME"),
".sqltoolsservice", "local"));
var requireArchive = HasArgument("archive");
// Working directory
var workingDirectory = System.IO.Directory.GetCurrentDirectory();
// System specific shell configuration
var shell = IsRunningOnWindows() ? "powershell" : "bash";
var shellArgument = IsRunningOnWindows() ? "-NoProfile /Command" : "-C";
var shellExtension = IsRunningOnWindows() ? "ps1" : "sh";
/// <summary>
/// Class representing build.json
/// </summary>
public class BuildPlan
{
public IDictionary<string, string[]> TestProjects { get; set; }
public string BuildToolsFolder { get; set; }
public string ArtifactsFolder { get; set; }
public bool UseSystemDotNetPath { get; set; }
public string DotNetFolder { get; set; }
public string DotNetInstallScriptURL { get; set; }
public string DotNetChannel { get; set; }
public string DotNetVersion { get; set; }
public string[] Frameworks { get; set; }
public string[] Rids { get; set; }
public string MainProject { get; set; }
}
var buildPlan = JsonConvert.DeserializeObject<BuildPlan>(
System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "build.json")));
// Folders and tools
var dotnetFolder = System.IO.Path.Combine(workingDirectory, buildPlan.DotNetFolder);
var dotnetcli = buildPlan.UseSystemDotNetPath ? "dotnet" : System.IO.Path.Combine(System.IO.Path.GetFullPath(dotnetFolder), "dotnet");
var toolsFolder = System.IO.Path.Combine(workingDirectory, buildPlan.BuildToolsFolder);
var sourceFolder = System.IO.Path.Combine(workingDirectory, "src");
var testFolder = System.IO.Path.Combine(workingDirectory, "test");
var artifactFolder = System.IO.Path.Combine(workingDirectory, buildPlan.ArtifactsFolder);
var publishFolder = System.IO.Path.Combine(artifactFolder, "publish");
var logFolder = System.IO.Path.Combine(artifactFolder, "logs");
var packageFolder = System.IO.Path.Combine(artifactFolder, "package");
var scriptFolder = System.IO.Path.Combine(artifactFolder, "scripts");
/// <summary>
/// Clean artifacts.
/// </summary>
Task("Cleanup")
.Does(() =>
{
if (System.IO.Directory.Exists(artifactFolder))
{
System.IO.Directory.Delete(artifactFolder, true);
}
System.IO.Directory.CreateDirectory(artifactFolder);
System.IO.Directory.CreateDirectory(logFolder);
System.IO.Directory.CreateDirectory(packageFolder);
System.IO.Directory.CreateDirectory(scriptFolder);
});
/// <summary>
/// Pre-build setup tasks.
/// </summary>
Task("Setup")
.IsDependentOn("BuildEnvironment")
.IsDependentOn("PopulateRuntimes")
.Does(() =>
{
});
/// <summary>
/// Populate the RIDs for the specific environment.
/// Use default RID (+ win7-x86 on Windows) for now.
/// </summary>
Task("PopulateRuntimes")
.IsDependentOn("BuildEnvironment")
.Does(() =>
{
buildPlan.Rids = new string[]
{
"default", // To allow testing the published artifact
"win7-x64",
"win7-x86",
"ubuntu.14.04-x64",
"ubuntu.16.04-x64",
"centos.7-x64",
"rhel.7.2-x64",
"debian.8-x64",
"fedora.23-x64",
"opensuse.13.2-x64",
"osx.10.11-x64"
};
});
/// <summary>
/// Install/update build environment.
/// </summary>
Task("BuildEnvironment")
.Does(() =>
{
var installScript = $"dotnet-install.{shellExtension}";
System.IO.Directory.CreateDirectory(dotnetFolder);
var scriptPath = System.IO.Path.Combine(dotnetFolder, installScript);
using (WebClient client = new WebClient())
{
client.DownloadFile($"{buildPlan.DotNetInstallScriptURL}/{installScript}", scriptPath);
}
if (!IsRunningOnWindows())
{
Run("chmod", $"+x '{scriptPath}'");
}
var installArgs = $"-Channel {buildPlan.DotNetChannel}";
if (!String.IsNullOrEmpty(buildPlan.DotNetVersion))
{
installArgs = $"{installArgs} -Version {buildPlan.DotNetVersion}";
}
if (!buildPlan.UseSystemDotNetPath)
{
installArgs = $"{installArgs} -InstallDir {dotnetFolder}";
}
Run(shell, $"{shellArgument} {scriptPath} {installArgs}");
try
{
Run(dotnetcli, "--info");
}
catch (Win32Exception)
{
throw new Exception(".NET CLI binary cannot be found.");
}
System.IO.Directory.CreateDirectory(toolsFolder);
var nugetPath = Environment.GetEnvironmentVariable("NUGET_EXE");
var arguments = $"install xunit.runner.console -ExcludeVersion -NoCache -Prerelease -OutputDirectory \"{toolsFolder}\"";
if (IsRunningOnWindows())
{
Run(nugetPath, arguments);
}
else
{
Run("mono", $"\"{nugetPath}\" {arguments}");
}
});
/// <summary>
/// Restore required NuGet packages.
/// </summary>
Task("Restore")
.IsDependentOn("Setup")
.Does(() =>
{
RunRestore(dotnetcli, "restore", sourceFolder)
.ExceptionOnError("Failed to restore projects under source code folder.");
RunRestore(dotnetcli, "restore --infer-runtimes", testFolder)
.ExceptionOnError("Failed to restore projects under test code folder.");
});
/// <summary>
/// Build Test projects.
/// </summary>
Task("BuildTest")
.IsDependentOn("Setup")
.IsDependentOn("Restore")
.Does(() =>
{
foreach (var pair in buildPlan.TestProjects)
{
foreach (var framework in pair.Value)
{
var project = pair.Key;
var projectFolder = System.IO.Path.Combine(testFolder, project);
var runLog = new List<string>();
Run(dotnetcli, $"build --framework {framework} --configuration {testConfiguration} \"{projectFolder}\"",
new RunOptions
{
StandardOutputListing = runLog
})
.ExceptionOnError($"Building test {project} failed for {framework}.");
System.IO.File.WriteAllLines(System.IO.Path.Combine(logFolder, $"{project}-{framework}-build.log"), runLog.ToArray());
}
}
});
/// <summary>
/// Run all tests for .NET Desktop and .NET Core
/// </summary>
Task("TestAll")
.IsDependentOn("Test")
.IsDependentOn("TestCore")
.Does(() =>{});
/// <summary>
/// Run all tests for Travis CI .NET Desktop and .NET Core
/// </summary>
Task("TravisTestAll")
.IsDependentOn("Cleanup")
.IsDependentOn("TestAll")
.Does(() =>{});
/// <summary>
/// Run tests for .NET Core (using .NET CLI).
/// </summary>
Task("TestCore")
.IsDependentOn("Setup")
.IsDependentOn("Restore")
.Does(() =>
{
var testProjects = buildPlan.TestProjects
.Where(pair => pair.Value.Any(framework => framework.Contains("netcoreapp")))
.Select(pair => pair.Key)
.ToList();
foreach (var testProject in testProjects)
{
var logFile = System.IO.Path.Combine(logFolder, $"{testProject}-core-result.xml");
var testWorkingDir = System.IO.Path.Combine(testFolder, testProject);
Run(dotnetcli, $"test -f netcoreapp1.0 -xml \"{logFile}\" -notrait category=failing", testWorkingDir)
.ExceptionOnError($"Test {testProject} failed for .NET Core.");
}
});
/// <summary>
/// Run tests for other frameworks (using XUnit2).
/// </summary>
Task("Test")
.IsDependentOn("Setup")
.IsDependentOn("BuildTest")
.Does(() =>
{
foreach (var pair in buildPlan.TestProjects)
{
foreach (var framework in pair.Value)
{
// Testing against core happens in TestCore
if (framework.Contains("netcoreapp"))
{
continue;
}
var project = pair.Key;
var frameworkFolder = System.IO.Path.Combine(testFolder, project, "bin", testConfiguration, framework);
var runtime = System.IO.Directory.GetDirectories(frameworkFolder).First();
var instanceFolder = System.IO.Path.Combine(frameworkFolder, runtime);
// Copy xunit executable to test folder to solve path errors
var xunitToolsFolder = System.IO.Path.Combine(toolsFolder, "xunit.runner.console", "tools");
var xunitInstancePath = System.IO.Path.Combine(instanceFolder, "xunit.console.exe");
System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.console.exe"), xunitInstancePath, true);
System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.runner.utility.desktop.dll"), System.IO.Path.Combine(instanceFolder, "xunit.runner.utility.desktop.dll"), true);
var targetPath = System.IO.Path.Combine(instanceFolder, $"{project}.dll");
var logFile = System.IO.Path.Combine(logFolder, $"{project}-{framework}-result.xml");
var arguments = $"\"{targetPath}\" -parallel none -xml \"{logFile}\" -notrait category=failing";
if (IsRunningOnWindows())
{
Run(xunitInstancePath, arguments, instanceFolder)
.ExceptionOnError($"Test {project} failed for {framework}");
}
else
{
Run("mono", $"\"{xunitInstancePath}\" {arguments}", instanceFolder)
.ExceptionOnError($"Test {project} failed for {framework}");
}
}
}
});
/// <summary>
/// Build, publish and package artifacts.
/// Targets all RIDs specified in build.json unless restricted by RestrictToLocalRuntime.
/// No dependencies on other tasks to support quick builds.
/// </summary>
Task("OnlyPublish")
.IsDependentOn("Setup")
.Does(() =>
{
var project = buildPlan.MainProject;
var projectFolder = System.IO.Path.Combine(sourceFolder, project);
foreach (var framework in buildPlan.Frameworks)
{
foreach (var runtime in buildPlan.Rids)
{
var outputFolder = System.IO.Path.Combine(publishFolder, project, runtime, framework);
var publishArguments = "publish";
if (!runtime.Equals("default"))
{
publishArguments = $"{publishArguments} --runtime {runtime}";
}
publishArguments = $"{publishArguments} --framework {framework} --configuration {configuration}";
publishArguments = $"{publishArguments} --output \"{outputFolder}\" \"{projectFolder}\"";
Run(dotnetcli, publishArguments)
.ExceptionOnError($"Failed to publish {project} / {framework}");
if (requireArchive)
{
Package(runtime, framework, outputFolder, packageFolder, buildPlan.MainProject.ToLower());
}
}
}
CreateRunScript(System.IO.Path.Combine(publishFolder, project, "default"), scriptFolder);
});
/// <summary>
/// Alias for OnlyPublish.
/// Targets all RIDs as specified in build.json.
/// </summary>
Task("AllPublish")
.IsDependentOn("Restore")
.IsDependentOn("OnlyPublish")
.Does(() =>
{
});
/// <summary>
/// Restrict the RIDs for the local default.
/// </summary>
Task("RestrictToLocalRuntime")
.IsDependentOn("Setup")
.Does(() =>
{
buildPlan.Rids = new string[] {"default"};
});
/// <summary>
/// Alias for OnlyPublish.
/// Restricts publishing to local RID.
/// </summary>
Task("LocalPublish")
.IsDependentOn("Restore")
.IsDependentOn("RestrictToLocalRuntime")
.IsDependentOn("OnlyPublish")
.Does(() =>
{
});
/// <summary>
/// Test the published binaries if they start up without errors.
/// Uses builds corresponding to local RID.
/// </summary>
Task("TestPublished")
.IsDependentOn("Setup")
.Does(() =>
{
var project = buildPlan.MainProject;
var projectFolder = System.IO.Path.Combine(sourceFolder, project);
var scriptsToTest = new string[] {"SQLTOOLSSERVICE.Core"};//TODO
foreach (var script in scriptsToTest)
{
var scriptPath = System.IO.Path.Combine(scriptFolder, script);
var didNotExitWithError = Run($"{shell}", $"{shellArgument} \"{scriptPath}\" -s \"{projectFolder}\" --stdio",
new RunOptions
{
TimeOut = 10000
})
.DidTimeOut;
if (!didNotExitWithError)
{
throw new Exception($"Failed to run {script}");
}
}
});
/// <summary>
/// Clean install path.
/// </summary>
Task("CleanupInstall")
.Does(() =>
{
if (System.IO.Directory.Exists(installFolder))
{
System.IO.Directory.Delete(installFolder, true);
}
System.IO.Directory.CreateDirectory(installFolder);
});
/// <summary>
/// Quick build.
/// </summary>
Task("Quick")
.IsDependentOn("Cleanup")
.IsDependentOn("LocalPublish")
.Does(() =>
{
});
/// <summary>
/// Quick build + install.
/// </summary>
Task("Install")
.IsDependentOn("Cleanup")
.IsDependentOn("LocalPublish")
.IsDependentOn("CleanupInstall")
.Does(() =>
{
var project = buildPlan.MainProject;
foreach (var framework in buildPlan.Frameworks)
{
var outputFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(publishFolder, project, "default", framework));
var targetFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(installFolder, framework));
// Copy all the folders
foreach (var directory in System.IO.Directory.GetDirectories(outputFolder, "*", SearchOption.AllDirectories))
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(targetFolder, directory.Substring(outputFolder.Length + 1)));
//Copy all the files
foreach (string file in System.IO.Directory.GetFiles(outputFolder, "*", SearchOption.AllDirectories))
System.IO.File.Copy(file, System.IO.Path.Combine(targetFolder, file.Substring(outputFolder.Length + 1)), true);
}
CreateRunScript(installFolder, scriptFolder);
});
/// <summary>
/// Full build targeting all RIDs specified in build.json.
/// </summary>
Task("All")
.IsDependentOn("Cleanup")
.IsDependentOn("Restore")
.IsDependentOn("TestAll")
.IsDependentOn("AllPublish")
//.IsDependentOn("TestPublished")
.Does(() =>
{
});
/// <summary>
/// Full build targeting local RID.
/// </summary>
Task("Local")
.IsDependentOn("Cleanup")
.IsDependentOn("Restore")
.IsDependentOn("TestAll")
.IsDependentOn("LocalPublish")
// .IsDependentOn("TestPublished")
.Does(() =>
{
});
/// <summary>
/// Build centered around producing the final artifacts for Travis
///
/// The tests are run as a different task "TestAll"
/// </summary>
Task("Travis")
.IsDependentOn("Cleanup")
.IsDependentOn("Restore")
.IsDependentOn("AllPublish")
// .IsDependentOn("TestPublished")
.Does(() =>
{
});
/// <summary>
/// Update the package versions within project.json files.
/// Uses depversion.json file as input.
/// </summary>
Task("SetPackageVersions")
.Does(() =>
{
var jDepVersion = JObject.Parse(System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "depversion.json")));
var projects = System.IO.Directory.GetFiles(sourceFolder, "project.json", SearchOption.AllDirectories).ToList();
projects.AddRange(System.IO.Directory.GetFiles(testFolder, "project.json", SearchOption.AllDirectories));
foreach (var project in projects)
{
var jProject = JObject.Parse(System.IO.File.ReadAllText(project));
var dependencies = jProject.SelectTokens("dependencies")
.Union(jProject.SelectTokens("frameworks.*.dependencies"))
.SelectMany(dependencyToken => dependencyToken.Children<JProperty>());
foreach (JProperty dependency in dependencies)
{
if (jDepVersion[dependency.Name] != null)
{
dependency.Value = jDepVersion[dependency.Name];
}
}
System.IO.File.WriteAllText(project, JsonConvert.SerializeObject(jProject, Formatting.Indented));
}
});
/// <summary>
/// Default Task aliases to Local.
/// </summary>
Task("Default")
.IsDependentOn("Local")
.Does(() =>
{
});
/// <summary>
/// Default to Local.
/// </summary>
RunTarget(target);

18
build.json Normal file
View File

@@ -0,0 +1,18 @@
{
"UseSystemDotNetPath": "true",
"DotNetFolder": ".dotnet",
"DotNetInstallScriptURL": "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain",
"DotNetChannel": "preview",
"DotNetVersion": "1.0.0-preview2-003121",
"BuildToolsFolder": ".tools",
"ArtifactsFolder": "artifacts",
"TestProjects": {
"Microsoft.SqlTools.ServiceLayer.Test": [
"netcoreapp1.0"
]
},
"Frameworks": [
"netcoreapp1.0"
],
"MainProject": "Microsoft.SqlTools.ServiceLayer"
}

3
build.ps1 Normal file
View File

@@ -0,0 +1,3 @@
$Env:SQLTOOLSSERVICE_PACKAGE_OSNAME = "win-x64"
.\scripts\cake-bootstrap.ps1 -experimental @args
exit $LASTEXITCODE

12
build.sh Normal file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Handle to many files on osx
if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then
ulimit -n 4096
fi
if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then
export SQLTOOLSSERVICE_PACKAGE_OSNAME=osx-x64
else
export SQLTOOLSSERVICE_PACKAGE_OSNAME=linux-x64
fi
bash ./scripts/cake-bootstrap.sh "$@"

View File

@@ -1,5 +1,8 @@
{
"projects": [ "src", "test" ]
"projects": [ "src", "test" ],
"sdk": {
"version": "1.0.0-preview2-003121"
}
}

6
nuget.config Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration >
<packageSources>
<add key="DataTools Nuget" value="http://dtnuget/api/v2/" />
</packageSources>
</configuration>

104
scripts/archiving.cake Normal file
View File

@@ -0,0 +1,104 @@
#load "runhelpers.cake"
using System.IO.Compression;
using System.Text.RegularExpressions;
/// <summary>
/// Generate the build identifier based on the RID and framework identifier.
/// Special rules when running on Travis (for publishing purposes).
/// </summary>
/// <param name="runtime">The RID</param>
/// <param name="framework">The framework identifier</param>
/// <returns>The designated build identifier</returns>
string GetBuildIdentifier(string runtime, string framework)
{
var runtimeShort = "";
// Default RID uses package name set in build script
if (runtime.Equals("default"))
{
runtimeShort = Environment.GetEnvironmentVariable("SQLTOOLSSERVICE_PACKAGE_OSNAME");
}
else
{
// Remove version number. Note: because there are separate versions for Ubuntu 14 and 16,
// we treat Ubuntu as a special case.
if (runtime.StartsWith("ubuntu.14"))
{
runtimeShort = "ubuntu14-x64";
}
else if (runtime.StartsWith("ubuntu.16"))
{
runtimeShort = "ubuntu16-x64";
}
else
{
runtimeShort = Regex.Replace(runtime, "(\\d|\\.)*-", "-");
}
}
return $"{runtimeShort}-{framework}";
}
/// <summary>
/// Generate an archive out of the given published folder.
/// Use ZIP for Windows runtimes.
/// Use TAR.GZ for non-Windows runtimes.
/// Use 7z to generate TAR.GZ on Windows if available.
/// </summary>
/// <param name="runtime">The RID</param>
/// <param name="contentFolder">The folder containing the files to package</param>
/// <param name="archiveName">The target archive name (without extension)</param>
void DoArchive(string runtime, string contentFolder, string archiveName)
{
// On all platforms use ZIP for Windows runtimes
if (runtime.Contains("win") || (runtime.Equals("default") && IsRunningOnWindows()))
{
var zipFile = $"{archiveName}.zip";
Zip(contentFolder, zipFile);
}
// On all platforms use TAR.GZ for Unix runtimes
else
{
var tarFile = $"{archiveName}.tar.gz";
// Use 7z to create TAR.GZ on Windows
if (IsRunningOnWindows())
{
var tempFile = $"{archiveName}.tar";
try
{
Run("7z", $"a \"{tempFile}\"", contentFolder)
.ExceptionOnError($"Tar-ing failed for {contentFolder} {archiveName}");
Run("7z", $"a \"{tarFile}\" \"{tempFile}\"", contentFolder)
.ExceptionOnError($"Compression failed for {contentFolder} {archiveName}");
System.IO.File.Delete(tempFile);
}
catch(Win32Exception)
{
Information("Warning: 7z not available on PATH to pack tar.gz results");
}
}
// Use tar to create TAR.GZ on Unix
else
{
Run("tar", $"czf \"{tarFile}\" .", contentFolder)
.ExceptionOnError($"Compression failed for {contentFolder} {archiveName}");
}
}
}
/// <summary>
/// Package a given output folder using a build identifier generated from the RID and framework identifier.
/// </summary>
/// <param name="runtime">The RID</param>
/// <param name="framework">The framework identifier</param>
/// <param name="contentFolder">The folder containing the files to package</param>
/// <param name="packageFolder">The destination folder for the archive</param>
/// <param name="projectName">The project name</param>
void Package(string runtime, string framework, string contentFolder, string packageFolder, string projectName)
{
var buildIdentifier = GetBuildIdentifier(runtime, framework);
if (buildIdentifier != null)
{
DoArchive(runtime, contentFolder, $"{packageFolder}/{projectName}-{buildIdentifier}");
}
}

43
scripts/artifacts.cake Normal file
View File

@@ -0,0 +1,43 @@
#load "runhelpers.cake"
/// <summary>
/// Generate the scripts which target the SQLTOOLSSERVICE binaries.
/// </summary>
/// <param name="outputRoot">The root folder where the publised (or installed) binaries are located</param>
void CreateRunScript(string outputRoot, string scriptFolder)
{
if (IsRunningOnWindows())
{
var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core.cmd");
var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{0}", "SQLTOOLSSERVICE");
var content = new string[] {
"SETLOCAL",
"",
$"\"{sqlToolsServicePath}\" %*"
};
if (System.IO.File.Exists(coreScript))
{
System.IO.File.Delete(coreScript);
}
content[2] = String.Format(content[2], "netcoreapp1.0");
System.IO.File.WriteAllLines(coreScript, content);
}
else
{
var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core");
var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{1}", "SQLTOOLSSERVICE");
var content = new string[] {
"#!/bin/bash",
"",
$"{{0}} \"{sqlToolsServicePath}{{2}}\" \"$@\""
};
if (System.IO.File.Exists(coreScript))
{
System.IO.File.Delete(coreScript);
}
content[2] = String.Format(content[2], "", "netcoreapp1.0", "");
System.IO.File.WriteAllLines(coreScript, content);
Run("chmod", $"+x \"{coreScript}\"");
}
}

110
scripts/cake-bootstrap.ps1 Normal file
View File

@@ -0,0 +1,110 @@
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Script
The build script to execute.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
Tells Cake to use the latest Roslyn release.
.PARAMETER WhatIf
Performs a dry run of the build script.
No tasks will be executed.
.PARAMETER Mono
Tells Cake to use the Mono scripting engine.
.LINK
http://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Script = "build.cake",
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[Alias("DryRun","Noop")]
[switch]$WhatIf,
[switch]$Mono,
[switch]$SkipToolPackageRestore,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
Write-Host "Preparing to run build script..."
$PS_SCRIPT_ROOT = split-path -parent $MyInvocation.MyCommand.Definition;
$TOOLS_DIR = Join-Path $PSScriptRoot "..\.tools"
$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe"
$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
$PACKAGES_CONFIG = Join-Path $PS_SCRIPT_ROOT "packages.config"
# Should we use mono?
$UseMono = "";
if($Mono.IsPresent) {
Write-Verbose -Message "Using the Mono based scripting engine."
$UseMono = "-mono"
}
# Is this a dry run?
$UseDryRun = "";
if($WhatIf.IsPresent) {
$UseDryRun = "-dryrun"
}
# Make sure tools folder exists
if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
Write-Verbose -Message "Creating tools directory..."
New-Item -Path $TOOLS_DIR -Type directory | out-null
}
# Try download NuGet.exe if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Downloading NuGet.exe..."
try {
(New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
} catch {
Throw "Could not download NuGet.exe."
}
}
# Save nuget.exe path to environment to be available to child processed
$ENV:NUGET_EXE = $NUGET_EXE
# Restore tools from NuGet?
if(-Not $SkipToolPackageRestore.IsPresent)
{
# Restore packages from NuGet.
Push-Location
Set-Location $TOOLS_DIR
Write-Verbose -Message "Restoring tools from NuGet..."
$NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install $PACKAGES_CONFIG -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
Write-Verbose -Message ($NuGetOutput | out-string)
Pop-Location
if ($LASTEXITCODE -ne 0)
{
exit $LASTEXITCODE
}
}
# Make sure that Cake has been installed.
if (!(Test-Path $CAKE_EXE)) {
Throw "Could not find Cake.exe at $CAKE_EXE"
}
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $ScriptArgs"
exit $LASTEXITCODE

69
scripts/cake-bootstrap.sh Normal file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
###############################################################
# This is the Cake bootstrapper script that is responsible for
# downloading Cake and all specified tools from NuGet.
###############################################################
# Define directories.
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/../.tools
export NUGET_EXE=$TOOLS_DIR/nuget.exe
CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe
PACKAGES_CONFIG=$SCRIPT_DIR/packages.config
# Define default arguments.
SCRIPT="build.cake"
VERBOSITY="verbose"
DRYRUN=
SHOW_VERSION=false
SCRIPT_ARGUMENTS=()
# Parse arguments.
for i in "$@"; do
case $1 in
-s|--script) SCRIPT="$2"; shift ;;
-v|--verbosity) VERBOSITY="$2"; shift ;;
-d|--dryrun) DRYRUN="-dryrun" ;;
--version) SHOW_VERSION=true ;;
--) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
*) SCRIPT_ARGUMENTS+=("$1") ;;
esac
shift
done
# Make sure the tools folder exist.
if [ ! -d "$TOOLS_DIR" ]; then
mkdir "$TOOLS_DIR"
fi
# Download NuGet if it does not exist.
if [ ! -f "$NUGET_EXE" ]; then
echo "Downloading NuGet..."
curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe
if [ $? -ne 0 ]; then
echo "An error occured while downloading nuget.exe."
exit 1
fi
fi
# Restore tools from NuGet.
pushd "$TOOLS_DIR" >/dev/null
mono "$NUGET_EXE" install "$PACKAGES_CONFIG" -ExcludeVersion -OutputDirectory "$TOOLS_DIR"
if [ $? -ne 0 ]; then
echo "Could not restore NuGet packages."
exit 1
fi
popd >/dev/null
# Make sure that Cake has been installed.
if [ ! -f "$CAKE_EXE" ]; then
echo "Could not find Cake.exe at '$CAKE_EXE'."
exit 1
fi
# Start Cake
if $SHOW_VERSION; then
exec mono "$CAKE_EXE" -version
else
exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
fi

5
scripts/packages.config Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Cake" version="0.10.1" />
<package id="Newtonsoft.Json" version="8.0.3" />
</packages>

204
scripts/runhelpers.cake Normal file
View File

@@ -0,0 +1,204 @@
using System.Collections.Generic;
using System.Diagnostics;
/// <summary>
/// Class encompassing the optional settings for running processes.
/// </summary>
public class RunOptions
{
/// <summary>
/// The working directory of the process.
/// </summary>
public string WorkingDirectory { get; set; }
/// <summary>
/// Container logging the StandardOutput content.
/// </summary>
public IList<string> StandardOutputListing { get; set; }
/// <summary>
/// Desired maximum time-out for the process
/// </summary>
public int TimeOut { get; set; }
}
/// <summary>
/// Wrapper for the exit code and state.
/// Used to query the result of an execution with method calls.
/// </summary>
public struct ExitStatus
{
private int _code;
private bool _timeOut;
/// <summary>
/// Default constructor when the execution finished.
/// </summary>
/// <param name="code">The exit code</param>
public ExitStatus(int code)
{
this._code = code;
this._timeOut = false;
}
/// <summary>
/// Default constructor when the execution potentially timed out.
/// </summary>
/// <param name="code">The exit code</param>
/// <param name="timeOut">True if the execution timed out</param>
public ExitStatus(int code, bool timeOut)
{
this._code = code;
this._timeOut = timeOut;
}
/// <summary>
/// Flag signalling that the execution timed out.
/// </summary>
public bool DidTimeOut { get { return _timeOut; } }
/// <summary>
/// Implicit conversion from ExitStatus to the exit code.
/// </summary>
/// <param name="exitStatus">The exit status</param>
/// <returns>The exit code</returns>
public static implicit operator int(ExitStatus exitStatus)
{
return exitStatus._code;
}
/// <summary>
/// Trigger Exception for non-zero exit code.
/// </summary>
/// <param name="errorMessage">The message to use in the Exception</param>
/// <returns>The exit status for further queries</returns>
public ExitStatus ExceptionOnError(string errorMessage)
{
if (this._code != 0)
{
throw new Exception(errorMessage);
}
return this;
}
}
/// <summary>
/// Run the given executable with the given arguments.
/// </summary>
/// <param name="exec">Executable to run</param>
/// <param name="args">Arguments</param>
/// <returns>The exit status for further queries</returns>
ExitStatus Run(string exec, string args)
{
return Run(exec, args, new RunOptions());
}
/// <summary>
/// Run the given executable with the given arguments.
/// </summary>
/// <param name="exec">Executable to run</param>
/// <param name="args">Arguments</param>
/// <param name="workingDirectory">Working directory</param>
/// <returns>The exit status for further queries</returns>
ExitStatus Run(string exec, string args, string workingDirectory)
{
return Run(exec, args,
new RunOptions()
{
WorkingDirectory = workingDirectory
});
}
/// <summary>
/// Run the given executable with the given arguments.
/// </summary>
/// <param name="exec">Executable to run</param>
/// <param name="args">Arguments</param>
/// <param name="runOptions">Optional settings</param>
/// <returns>The exit status for further queries</returns>
ExitStatus Run(string exec, string args, RunOptions runOptions)
{
var workingDirectory = runOptions.WorkingDirectory ?? System.IO.Directory.GetCurrentDirectory();
var process = System.Diagnostics.Process.Start(
new ProcessStartInfo(exec, args)
{
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardOutput = runOptions.StandardOutputListing != null
});
if (runOptions.StandardOutputListing != null)
{
process.OutputDataReceived += (s, e) =>
{
if (e.Data != null)
{
runOptions.StandardOutputListing.Add(e.Data);
}
};
process.BeginOutputReadLine();
}
if (runOptions.TimeOut == 0)
{
process.WaitForExit();
return new ExitStatus(process.ExitCode);
}
else
{
bool finished = process.WaitForExit(runOptions.TimeOut);
if (finished)
{
return new ExitStatus(process.ExitCode);
}
else
{
KillProcessTree(process);
return new ExitStatus(0, true);
}
}
}
/// <summary>
/// Run restore with the given arguments
/// </summary>
/// <param name="exec">Executable to run</param>
/// <param name="args">Arguments</param>
/// <param name="runOptions">Optional settings</param>
/// <returns>The exit status for further queries</returns>
ExitStatus RunRestore(string exec, string args, string workingDirectory)
{
Information("Restoring packages....");
var p = StartAndReturnProcess(exec,
new ProcessSettings
{
Arguments = args,
RedirectStandardOutput = true,
WorkingDirectory = workingDirectory
});
p.WaitForExit();
var exitCode = p.GetExitCode();
if (exitCode == 0)
{
Information("Package restore successful!");
}
else
{
Error(string.Join("\n", p.GetStandardOutput()));
}
return new ExitStatus(exitCode);
}
/// <summary>
/// Kill the given process and all its child processes.
/// </summary>
/// <param name="process">Root process</param>
public void KillProcessTree(Process process)
{
// Child processes are not killed on Windows by default
// Use TASKKILL to kill the process hierarchy rooted in the process
if (IsRunningOnWindows())
{
StartProcess($"TASKKILL",
new ProcessSettings
{
Arguments = $"/PID {process.Id} /T /F",
});
}
else
{
process.Kill();
}
}

43
sqltoolsservice.sln Normal file
View File

@@ -0,0 +1,43 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2BBD7364-054F-4693-97CD-1C395E3E84A9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{32DC973E-9EEA-4694-B1C2-B031167AB945}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
global.json = global.json
nuget.config = nuget.config
README.md = README.md
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.SqlTools.ServiceLayer", "src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.xproj", "{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.SqlTools.ServiceLayer.Test", "test\Microsoft.SqlTools.ServiceLayer.Test\Microsoft.SqlTools.ServiceLayer.Test.xproj", "{2D771D16-9D85-4053-9F79-E2034737DEEF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Release|Any CPU.Build.0 = Release|Any CPU
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9} = {2BBD7364-054F-4693-97CD-1C395E3E84A9}
{2D771D16-9D85-4053-9F79-E2034737DEEF} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,53 @@
//
// 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.Data.Common;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection
{
/// <summary>
/// Information pertaining to a unique connection instance.
/// </summary>
public class ConnectionInfo
{
/// <summary>
/// Constructor
/// </summary>
public ConnectionInfo(ISqlConnectionFactory factory, string ownerUri, ConnectionDetails details)
{
Factory = factory;
OwnerUri = ownerUri;
ConnectionDetails = details;
ConnectionId = Guid.NewGuid();
}
/// <summary>
/// Unique Id, helpful to identify a connection info object
/// </summary>
public Guid ConnectionId { get; private set; }
/// <summary>
/// URI identifying the owner/user of the connection. Could be a file, service, resource, etc.
/// </summary>
public string OwnerUri { get; private set; }
/// <summary>
/// Factory used for creating the SQL connection associated with the connection info.
/// </summary>
public ISqlConnectionFactory Factory {get; private set;}
/// <summary>
/// Properties used for creating/opening the SQL connection.
/// </summary>
public ConnectionDetails ConnectionDetails { get; private set; }
/// <summary>
/// The connection to the SQL database that commands will be run against.
/// </summary>
public DbConnection SqlConnection { get; set; }
}
}

View File

@@ -0,0 +1,533 @@
//
// 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.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
namespace Microsoft.SqlTools.ServiceLayer.Connection
{
/// <summary>
/// Main class for the Connection Management services
/// </summary>
public class ConnectionService
{
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<ConnectionService> instance
= new Lazy<ConnectionService>(() => new ConnectionService());
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static ConnectionService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// The SQL connection factory object
/// </summary>
private ISqlConnectionFactory connectionFactory;
private Dictionary<string, ConnectionInfo> ownerToConnectionMap = new Dictionary<string, ConnectionInfo>();
/// <summary>
/// Service host object for sending/receiving requests/events.
/// Internal for testing purposes.
/// </summary>
internal IProtocolEndpoint ServiceHost
{
get;
set;
}
/// <summary>
/// Default constructor is private since it's a singleton class
/// </summary>
private ConnectionService()
{
}
/// <summary>
/// Callback for onconnection handler
/// </summary>
/// <param name="sqlConnection"></param>
public delegate Task OnConnectionHandler(ConnectionInfo info);
/// <summary>
// Callback for ondisconnect handler
/// </summary>
public delegate Task OnDisconnectHandler(ConnectionSummary summary);
/// <summary>
/// List of onconnection handlers
/// </summary>
private readonly List<OnConnectionHandler> onConnectionActivities = new List<OnConnectionHandler>();
/// <summary>
/// List of ondisconnect handlers
/// </summary>
private readonly List<OnDisconnectHandler> onDisconnectActivities = new List<OnDisconnectHandler>();
/// <summary>
/// Gets the SQL connection factory instance
/// </summary>
public ISqlConnectionFactory ConnectionFactory
{
get
{
if (this.connectionFactory == null)
{
this.connectionFactory = new SqlConnectionFactory();
}
return this.connectionFactory;
}
}
/// <summary>
/// Test constructor that injects dependency interfaces
/// </summary>
/// <param name="testFactory"></param>
public ConnectionService(ISqlConnectionFactory testFactory)
{
this.connectionFactory = testFactory;
}
// Attempts to link a URI to an actively used connection for this URI
public bool TryFindConnection(string ownerUri, out ConnectionInfo connectionInfo)
{
return this.ownerToConnectionMap.TryGetValue(ownerUri, out connectionInfo);
}
/// <summary>
/// Open a connection with the specified connection details
/// </summary>
/// <param name="connectionParams"></param>
public ConnectResponse Connect(ConnectParams connectionParams)
{
// Validate parameters
string paramValidationErrorMessage;
if (connectionParams == null)
{
return new ConnectResponse()
{
Messages = "Error: Connection parameters cannot be null."
};
}
else if (!connectionParams.IsValid(out paramValidationErrorMessage))
{
return new ConnectResponse()
{
Messages = paramValidationErrorMessage
};
}
// Resolve if it is an existing connection
// Disconnect active connection if the URI is already connected
ConnectionInfo connectionInfo;
if (ownerToConnectionMap.TryGetValue(connectionParams.OwnerUri, out connectionInfo) )
{
var disconnectParams = new DisconnectParams()
{
OwnerUri = connectionParams.OwnerUri
};
Disconnect(disconnectParams);
}
connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection);
// try to connect
var response = new ConnectResponse();
try
{
// build the connection string from the input parameters
string connectionString = ConnectionService.BuildConnectionString(connectionInfo.ConnectionDetails);
// create a sql connection instance
connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString);
connectionInfo.SqlConnection.Open();
}
catch(Exception ex)
{
response.Messages = ex.ToString();
return response;
}
ownerToConnectionMap[connectionParams.OwnerUri] = connectionInfo;
// invoke callback notifications
foreach (var activity in this.onConnectionActivities)
{
activity(connectionInfo);
}
// return the connection result
response.ConnectionId = connectionInfo.ConnectionId.ToString();
return response;
}
/// <summary>
/// Close a connection with the specified connection details.
/// </summary>
public bool Disconnect(DisconnectParams disconnectParams)
{
// Validate parameters
if (disconnectParams == null || string.IsNullOrEmpty(disconnectParams.OwnerUri))
{
return false;
}
// Lookup the connection owned by the URI
ConnectionInfo info;
if (!ownerToConnectionMap.TryGetValue(disconnectParams.OwnerUri, out info))
{
return false;
}
// Close the connection
info.SqlConnection.Close();
// Remove URI mapping
ownerToConnectionMap.Remove(disconnectParams.OwnerUri);
// Invoke callback notifications
foreach (var activity in this.onDisconnectActivities)
{
activity(info.ConnectionDetails);
}
// Success
return true;
}
/// <summary>
/// List all databases on the server specified
/// </summary>
public ListDatabasesResponse ListDatabases(ListDatabasesParams listDatabasesParams)
{
// Verify parameters
var owner = listDatabasesParams.OwnerUri;
if (string.IsNullOrEmpty(owner))
{
throw new ArgumentException("OwnerUri cannot be null or empty");
}
// Use the existing connection as a base for the search
ConnectionInfo info;
if (!TryFindConnection(owner, out info))
{
throw new Exception("Specified OwnerUri \"" + owner + "\" does not have an existing connection");
}
ConnectionDetails connectionDetails = info.ConnectionDetails.Clone();
// Connect to master and query sys.databases
connectionDetails.DatabaseName = "master";
var connection = this.ConnectionFactory.CreateSqlConnection(BuildConnectionString(connectionDetails));
connection.Open();
DbCommand command = connection.CreateCommand();
command.CommandText = "SELECT name FROM sys.databases";
command.CommandTimeout = 15;
command.CommandType = CommandType.Text;
var reader = command.ExecuteReader();
List<string> results = new List<string>();
while (reader.Read())
{
results.Add(reader[0].ToString());
}
connection.Close();
ListDatabasesResponse response = new ListDatabasesResponse();
response.DatabaseNames = results.ToArray();
return response;
}
public void InitializeService(IProtocolEndpoint serviceHost)
{
this.ServiceHost = serviceHost;
// Register request and event handlers with the Service Host
serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest);
serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest);
serviceHost.SetRequestHandler(ListDatabasesRequest.Type, HandleListDatabasesRequest);
// Register the configuration update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
}
/// <summary>
/// Add a new method to be called when the onconnection request is submitted
/// </summary>
/// <param name="activity"></param>
public void RegisterOnConnectionTask(OnConnectionHandler activity)
{
onConnectionActivities.Add(activity);
}
/// <summary>
/// Add a new method to be called when the ondisconnect request is submitted
/// </summary>
public void RegisterOnDisconnectTask(OnDisconnectHandler activity)
{
onDisconnectActivities.Add(activity);
}
/// <summary>
/// Handle new connection requests
/// </summary>
/// <param name="connectionDetails"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
protected async Task HandleConnectRequest(
ConnectParams connectParams,
RequestContext<ConnectResponse> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleConnectRequest");
try
{
// open connection base on request details
ConnectResponse result = ConnectionService.Instance.Connect(connectParams);
await requestContext.SendResult(result);
}
catch(Exception ex)
{
await requestContext.SendError(ex.ToString());
}
}
/// <summary>
/// Handle disconnect requests
/// </summary>
protected async Task HandleDisconnectRequest(
DisconnectParams disconnectParams,
RequestContext<bool> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDisconnectRequest");
try
{
bool result = ConnectionService.Instance.Disconnect(disconnectParams);
await requestContext.SendResult(result);
}
catch(Exception ex)
{
await requestContext.SendError(ex.ToString());
}
}
/// <summary>
/// Handle requests to list databases on the current server
/// </summary>
protected async Task HandleListDatabasesRequest(
ListDatabasesParams listDatabasesParams,
RequestContext<ListDatabasesResponse> requestContext)
{
Logger.Write(LogLevel.Verbose, "ListDatabasesRequest");
try
{
ListDatabasesResponse result = ConnectionService.Instance.ListDatabases(listDatabasesParams);
await requestContext.SendResult(result);
}
catch(Exception ex)
{
await requestContext.SendError(ex.ToString());
}
}
public Task HandleDidChangeConfigurationNotification(
SqlToolsSettings newSettings,
SqlToolsSettings oldSettings,
EventContext eventContext)
{
return Task.FromResult(true);
}
/// <summary>
/// Build a connection string from a connection details instance
/// </summary>
/// <param name="connectionDetails"></param>
public static string BuildConnectionString(ConnectionDetails connectionDetails)
{
SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder();
connectionBuilder["Data Source"] = connectionDetails.ServerName;
connectionBuilder["User Id"] = connectionDetails.UserName;
connectionBuilder["Password"] = connectionDetails.Password;
// Check for any optional parameters
if (!string.IsNullOrEmpty(connectionDetails.DatabaseName))
{
connectionBuilder["Initial Catalog"] = connectionDetails.DatabaseName;
}
if (!string.IsNullOrEmpty(connectionDetails.AuthenticationType))
{
switch(connectionDetails.AuthenticationType)
{
case "Integrated":
connectionBuilder.IntegratedSecurity = true;
break;
case "SqlLogin":
break;
default:
throw new ArgumentException(string.Format("Invalid value \"{0}\" for AuthenticationType. Valid values are \"Integrated\" and \"SqlLogin\".", connectionDetails.AuthenticationType));
}
}
if (connectionDetails.Encrypt.HasValue)
{
connectionBuilder.Encrypt = connectionDetails.Encrypt.Value;
}
if (connectionDetails.TrustServerCertificate.HasValue)
{
connectionBuilder.TrustServerCertificate = connectionDetails.TrustServerCertificate.Value;
}
if (connectionDetails.PersistSecurityInfo.HasValue)
{
connectionBuilder.PersistSecurityInfo = connectionDetails.PersistSecurityInfo.Value;
}
if (connectionDetails.ConnectTimeout.HasValue)
{
connectionBuilder.ConnectTimeout = connectionDetails.ConnectTimeout.Value;
}
if (connectionDetails.ConnectRetryCount.HasValue)
{
connectionBuilder.ConnectRetryCount = connectionDetails.ConnectRetryCount.Value;
}
if (connectionDetails.ConnectRetryInterval.HasValue)
{
connectionBuilder.ConnectRetryInterval = connectionDetails.ConnectRetryInterval.Value;
}
if (!string.IsNullOrEmpty(connectionDetails.ApplicationName))
{
connectionBuilder.ApplicationName = connectionDetails.ApplicationName;
}
if (!string.IsNullOrEmpty(connectionDetails.WorkstationId))
{
connectionBuilder.WorkstationID = connectionDetails.WorkstationId;
}
if (!string.IsNullOrEmpty(connectionDetails.ApplicationIntent))
{
ApplicationIntent intent;
switch (connectionDetails.ApplicationIntent)
{
case "ReadOnly":
intent = ApplicationIntent.ReadOnly;
break;
case "ReadWrite":
intent = ApplicationIntent.ReadWrite;
break;
default:
throw new ArgumentException(string.Format("Invalid value \"{0}\" for ApplicationIntent. Valid values are \"ReadWrite\" and \"ReadOnly\".", connectionDetails.ApplicationIntent));
}
connectionBuilder.ApplicationIntent = intent;
}
if (!string.IsNullOrEmpty(connectionDetails.CurrentLanguage))
{
connectionBuilder.CurrentLanguage = connectionDetails.CurrentLanguage;
}
if (connectionDetails.Pooling.HasValue)
{
connectionBuilder.Pooling = connectionDetails.Pooling.Value;
}
if (connectionDetails.MaxPoolSize.HasValue)
{
connectionBuilder.MaxPoolSize = connectionDetails.MaxPoolSize.Value;
}
if (connectionDetails.MinPoolSize.HasValue)
{
connectionBuilder.MinPoolSize = connectionDetails.MinPoolSize.Value;
}
if (connectionDetails.LoadBalanceTimeout.HasValue)
{
connectionBuilder.LoadBalanceTimeout = connectionDetails.LoadBalanceTimeout.Value;
}
if (connectionDetails.Replication.HasValue)
{
connectionBuilder.Replication = connectionDetails.Replication.Value;
}
if (!string.IsNullOrEmpty(connectionDetails.AttachDbFilename))
{
connectionBuilder.AttachDBFilename = connectionDetails.AttachDbFilename;
}
if (!string.IsNullOrEmpty(connectionDetails.FailoverPartner))
{
connectionBuilder.FailoverPartner = connectionDetails.FailoverPartner;
}
if (connectionDetails.MultiSubnetFailover.HasValue)
{
connectionBuilder.MultiSubnetFailover = connectionDetails.MultiSubnetFailover.Value;
}
if (connectionDetails.MultipleActiveResultSets.HasValue)
{
connectionBuilder.MultipleActiveResultSets = connectionDetails.MultipleActiveResultSets.Value;
}
if (connectionDetails.PacketSize.HasValue)
{
connectionBuilder.PacketSize = connectionDetails.PacketSize.Value;
}
if (!string.IsNullOrEmpty(connectionDetails.TypeSystemVersion))
{
connectionBuilder.TypeSystemVersion = connectionDetails.TypeSystemVersion;
}
return connectionBuilder.ToString();
}
/// <summary>
/// Change the database context of a connection.
/// </summary>
/// <param name="ownerUri">URI of the owner of the connection</param>
/// <param name="newDatabaseName">Name of the database to change the connection to</param>
public void ChangeConnectionDatabaseContext(string ownerUri, string newDatabaseName)
{
ConnectionInfo info;
if (TryFindConnection(ownerUri, out info))
{
try
{
if (info.SqlConnection.State == ConnectionState.Open)
{
info.SqlConnection.ChangeDatabase(newDatabaseName);
}
info.ConnectionDetails.DatabaseName = newDatabaseName;
// Fire a connection changed event
ConnectionChangedParams parameters = new ConnectionChangedParams();
ConnectionSummary summary = (ConnectionSummary)(info.ConnectionDetails);
parameters.Connection = summary.Clone();
parameters.OwnerUri = ownerUri;
ServiceHost.SendEvent(ConnectionChangedNotification.Type, parameters);
}
catch (Exception e)
{
Logger.Write(
LogLevel.Error,
string.Format(
"Exception caught while trying to change database context to [{0}] for OwnerUri [{1}]. Exception:{2}",
newDatabaseName,
ownerUri,
e.ToString())
);
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the Connect Request.
/// </summary>
public class ConnectParams
{
/// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
/// or a virtual file representing an object in a database.
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Contains the required parameters to initialize a connection to a database.
/// A connection will identified by its server name, database name and user name.
/// This may be changed in the future to support multiple connections with different
/// connection properties to the same database.
/// </summary>
public ConnectionDetails Connection { get; set; }
}
}

View File

@@ -0,0 +1,56 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Extension methods to ConnectParams
/// </summary>
public static class ConnectParamsExtensions
{
/// <summary>
/// Check that the fields in ConnectParams are all valid
/// </summary>
public static bool IsValid(this ConnectParams parameters, out string errorMessage)
{
errorMessage = string.Empty;
if (string.IsNullOrEmpty(parameters.OwnerUri))
{
errorMessage = "Error: OwnerUri cannot be null or empty.";
}
else if (parameters.Connection == null)
{
errorMessage = "Error: Connection details object cannot be null.";
}
else if (string.IsNullOrEmpty(parameters.Connection.ServerName))
{
errorMessage = "Error: ServerName cannot be null or empty.";
}
else if (string.IsNullOrEmpty(parameters.Connection.AuthenticationType) || parameters.Connection.AuthenticationType == "SqlLogin")
{
// For SqlLogin, username/password cannot be empty
if (string.IsNullOrEmpty(parameters.Connection.UserName))
{
errorMessage = "Error: UserName cannot be null or empty when using SqlLogin authentication.";
}
else if( string.IsNullOrEmpty(parameters.Connection.Password))
{
errorMessage = "Error: Password cannot be null or empty when using SqlLogin authentication.";
}
}
if (string.IsNullOrEmpty(errorMessage))
{
return true;
}
else
{
return false;
}
}
}
}

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Message format for the connection result response
/// </summary>
public class ConnectResponse
{
/// <summary>
/// A GUID representing a unique connection ID
/// </summary>
public string ConnectionId { get; set; }
/// <summary>
/// Gets or sets any connection error messages
/// </summary>
public string Messages { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// ConnectionChanged notification mapping entry
/// </summary>
public class ConnectionChangedNotification
{
public static readonly
EventType<ConnectionChangedParams> Type =
EventType<ConnectionChangedParams>.Create("connection/connectionchanged");
}
}

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the ConnectionChanged Notification.
/// </summary>
public class ConnectionChangedParams
{
/// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
/// or a virtual file representing an object in a database.
/// </summary>
public string OwnerUri { get; set; }
/// <summary>
/// Contains the high-level properties about the connection, for display to the user.
/// </summary>
public ConnectionSummary Connection { get; set; }
}
}

View File

@@ -0,0 +1,132 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Message format for the initial connection request
/// </summary>
/// <remarks>
/// If this contract is ever changed, be sure to update ConnectionDetailsExtensions methods.
/// </remarks>
public class ConnectionDetails : ConnectionSummary
{
/// <summary>
/// Gets or sets the connection password
/// </summary>
/// <returns></returns>
public string Password { get; set; }
/// <summary>
/// Gets or sets the authentication to use.
/// </summary>
public string AuthenticationType { get; set; }
/// <summary>
/// Gets or sets a Boolean value that indicates whether SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed.
/// </summary>
public bool? Encrypt { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust.
/// </summary>
public bool? TrustServerCertificate { get; set; }
/// <summary>
/// Gets or sets a Boolean value that indicates if security-sensitive information, such as the password, is not returned as part of the connection if the connection is open or has ever been in an open state.
/// </summary>
public bool? PersistSecurityInfo { get; set; }
/// <summary>
/// Gets or sets the length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error.
/// </summary>
public int? ConnectTimeout { get; set; }
/// <summary>
/// The number of reconnections attempted after identifying that there was an idle connection failure.
/// </summary>
public int? ConnectRetryCount { get; set; }
/// <summary>
/// Amount of time (in seconds) between each reconnection attempt after identifying that there was an idle connection failure.
/// </summary>
public int? ConnectRetryInterval { get; set; }
/// <summary>
/// Gets or sets the name of the application associated with the connection string.
/// </summary>
public string ApplicationName { get; set; }
/// <summary>
/// Gets or sets the name of the workstation connecting to SQL Server.
/// </summary>
public string WorkstationId { get; set; }
/// <summary>
/// Declares the application workload type when connecting to a database in an SQL Server Availability Group.
/// </summary>
public string ApplicationIntent { get; set; }
/// <summary>
/// Gets or sets the SQL Server Language record name.
/// </summary>
public string CurrentLanguage { get; set; }
/// <summary>
/// Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested.
/// </summary>
public bool? Pooling { get; set; }
/// <summary>
/// Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string.
/// </summary>
public int? MaxPoolSize { get; set; }
/// <summary>
/// Gets or sets the minimum number of connections allowed in the connection pool for this specific connection string.
/// </summary>
public int? MinPoolSize { get; set; }
/// <summary>
/// Gets or sets the minimum time, in seconds, for the connection to live in the connection pool before being destroyed.
/// </summary>
public int? LoadBalanceTimeout { get; set; }
/// <summary>
/// Gets or sets a Boolean value that indicates whether replication is supported using the connection.
/// </summary>
public bool? Replication { get; set; }
/// <summary>
/// Gets or sets a string that contains the name of the primary data file. This includes the full path name of an attachable database.
/// </summary>
public string AttachDbFilename { get; set; }
/// <summary>
/// Gets or sets the name or address of the partner server to connect to if the primary server is down.
/// </summary>
public string FailoverPartner { get; set; }
/// <summary>
/// If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting MultiSubnetFailover=true provides faster detection of and connection to the (currently) active server.
/// </summary>
public bool? MultiSubnetFailover { get; set; }
/// <summary>
/// When true, an application can maintain multiple active result sets (MARS).
/// </summary>
public bool? MultipleActiveResultSets { get; set; }
/// <summary>
/// Gets or sets the size in bytes of the network packets used to communicate with an instance of SQL Server.
/// </summary>
public int? PacketSize { get; set; }
/// <summary>
/// Gets or sets a string value that indicates the type system the application expects.
/// </summary>
public string TypeSystemVersion { get; set; }
}
}

View File

@@ -0,0 +1,49 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Extension methods for the ConnectionDetails contract class
/// </summary>
public static class ConnectionDetailsExtensions
{
/// <summary>
/// Create a copy of a connection details object.
/// </summary>
public static ConnectionDetails Clone(this ConnectionDetails details)
{
return new ConnectionDetails()
{
ServerName = details.ServerName,
DatabaseName = details.DatabaseName,
UserName = details.UserName,
Password = details.Password,
AuthenticationType = details.AuthenticationType,
Encrypt = details.Encrypt,
TrustServerCertificate = details.TrustServerCertificate,
PersistSecurityInfo = details.PersistSecurityInfo,
ConnectTimeout = details.ConnectTimeout,
ConnectRetryCount = details.ConnectRetryCount,
ConnectRetryInterval = details.ConnectRetryInterval,
ApplicationName = details.ApplicationName,
WorkstationId = details.WorkstationId,
ApplicationIntent = details.ApplicationIntent,
CurrentLanguage = details.CurrentLanguage,
Pooling = details.Pooling,
MaxPoolSize = details.MaxPoolSize,
MinPoolSize = details.MinPoolSize,
LoadBalanceTimeout = details.LoadBalanceTimeout,
Replication = details.Replication,
AttachDbFilename = details.AttachDbFilename,
FailoverPartner = details.FailoverPartner,
MultiSubnetFailover = details.MultiSubnetFailover,
MultipleActiveResultSets = details.MultipleActiveResultSets,
PacketSize = details.PacketSize,
TypeSystemVersion = details.TypeSystemVersion
};
}
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Connect request mapping entry
/// </summary>
public class ConnectionRequest
{
public static readonly
RequestType<ConnectParams, ConnectResponse> Type =
RequestType<ConnectParams, ConnectResponse>.Create("connection/connect");
}
}

View File

@@ -0,0 +1,28 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Provides high level information about a connection.
/// </summary>
public class ConnectionSummary
{
/// <summary>
/// Gets or sets the connection server name
/// </summary>
public string ServerName { get; set; }
/// <summary>
/// Gets or sets the connection database name
/// </summary>
public string DatabaseName { get; set; }
/// <summary>
/// Gets or sets the connection user name
/// </summary>
public string UserName { get; set; }
}
}

View File

@@ -0,0 +1,53 @@
//
// 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;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Treats connections as the same if their server, db and usernames all match
/// </summary>
public class ConnectionSummaryComparer : IEqualityComparer<ConnectionSummary>
{
public bool Equals(ConnectionSummary x, ConnectionSummary y)
{
if(x == y) { return true; }
else if(x != null)
{
if(y == null) { return false; }
// Compare server, db, username. Note: server is case-insensitive in the driver
return string.Compare(x.ServerName, y.ServerName, StringComparison.OrdinalIgnoreCase) == 0
&& string.Compare(x.DatabaseName, y.DatabaseName, StringComparison.Ordinal) == 0
&& string.Compare(x.UserName, y.UserName, StringComparison.Ordinal) == 0;
}
return false;
}
public int GetHashCode(ConnectionSummary obj)
{
int hashcode = 31;
if(obj != null)
{
if(obj.ServerName != null)
{
hashcode ^= obj.ServerName.GetHashCode();
}
if (obj.DatabaseName != null)
{
hashcode ^= obj.DatabaseName.GetHashCode();
}
if (obj.UserName != null)
{
hashcode ^= obj.UserName.GetHashCode();
}
}
return hashcode;
}
}
}

View File

@@ -0,0 +1,26 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Extension methods to ConnectionSummary
/// </summary>
public static class ConnectionSummaryExtensions
{
/// <summary>
/// Create a copy of a ConnectionSummary object
/// </summary>
public static ConnectionSummary Clone(this ConnectionSummary summary)
{
return new ConnectionSummary()
{
ServerName = summary.ServerName,
DatabaseName = summary.DatabaseName,
UserName = summary.UserName
};
}
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the Disconnect Request.
/// </summary>
public class DisconnectParams
{
/// <summary>
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
/// or a virtual file representing an object in a database.
/// </summary>
public string OwnerUri { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Disconnect request mapping entry
/// </summary>
public class DisconnectRequest
{
public static readonly
RequestType<DisconnectParams, bool> Type =
RequestType<DisconnectParams, bool>.Create("connection/disconnect");
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Parameters for the List Databases Request.
/// </summary>
public class ListDatabasesParams
{
/// <summary>
/// URI of the owner of the connection requesting the list of databases.
/// </summary>
public string OwnerUri { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// List databases request mapping entry
/// </summary>
public class ListDatabasesRequest
{
public static readonly
RequestType<ListDatabasesParams, ListDatabasesResponse> Type =
RequestType<ListDatabasesParams, ListDatabasesResponse>.Create("connection/listdatabases");
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
{
/// <summary>
/// Message format for the list databases response
/// </summary>
public class ListDatabasesResponse
{
/// <summary>
/// Gets or sets the list of database names.
/// </summary>
public string[] DatabaseNames { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Data.Common;
namespace Microsoft.SqlTools.ServiceLayer.Connection
{
/// <summary>
/// Interface for the SQL Connection factory
/// </summary>
public interface ISqlConnectionFactory
{
/// <summary>
/// Create a new SQL Connection object
/// </summary>
DbConnection CreateSqlConnection(string connectionString);
}
}

View File

@@ -0,0 +1,26 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Data.Common;
using System.Data.SqlClient;
namespace Microsoft.SqlTools.ServiceLayer.Connection
{
/// <summary>
/// Factory class to create SqlClientConnections
/// The purpose of the factory is to make it easier to mock out the database
/// in 'offline' unit test scenarios.
/// </summary>
public class SqlConnectionFactory : ISqlConnectionFactory
{
/// <summary>
/// Creates a new SqlConnection object
/// </summary>
public DbConnection CreateSqlConnection(string connectionString)
{
return new SqlConnection(connectionString);
}
}
}

View File

@@ -0,0 +1,124 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Contracts
{
/// <summary>
/// A Credential containing information needed to log into a resource. This is primarily
/// defined as a unique <see cref="CredentialId"/> with an associated <see cref="Password"/>
/// that's linked to it.
/// </summary>
public class Credential
{
/// <summary>
/// A unique ID to identify the credential being saved.
/// </summary>
public string CredentialId { get; set; }
/// <summary>
/// The Password stored for this credential.
/// </summary>
public string Password { get; set; }
/// <summary>
/// Default Constructor
/// </summary>
public Credential()
{
}
/// <summary>
/// Constructor used when only <paramref name="credentialId"/> is known
/// </summary>
/// <param name="credentialId"><see cref="CredentialId"/></param>
public Credential(string credentialId)
: this(credentialId, null)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="credentialId"><see cref="CredentialId"/></param>
/// <param name="password"><see cref="Password"/></param>
public Credential(string credentialId, string password)
{
CredentialId = credentialId;
Password = password;
}
internal static Credential Copy(Credential credential)
{
return new Credential
{
CredentialId = credential.CredentialId,
Password = credential.Password
};
}
/// <summary>
/// Validates the credential has all the properties needed to look up the password
/// </summary>
public static void ValidateForLookup(Credential credential)
{
Validate.IsNotNull("credential", credential);
Validate.IsNotNullOrEmptyString("credential.CredentialId", credential.CredentialId);
}
/// <summary>
/// Validates the credential has all the properties needed to save a password
/// </summary>
public static void ValidateForSave(Credential credential)
{
ValidateForLookup(credential);
Validate.IsNotNullOrEmptyString("credential.Password", credential.Password);
}
}
/// <summary>
/// Read Credential request mapping entry. Expects a Credential with CredentialId,
/// and responds with the <see cref="Credential.Password"/> filled in if found
/// </summary>
public class ReadCredentialRequest
{
/// <summary>
/// Request definition
/// </summary>
public static readonly
RequestType<Credential, Credential> Type =
RequestType<Credential, Credential>.Create("credential/read");
}
/// <summary>
/// Save Credential request mapping entry
/// </summary>
public class SaveCredentialRequest
{
/// <summary>
/// Request definition
/// </summary>
public static readonly
RequestType<Credential, bool> Type =
RequestType<Credential, bool>.Create("credential/save");
}
/// <summary>
/// Delete Credential request mapping entry
/// </summary>
public class DeleteCredentialRequest
{
/// <summary>
/// Request definition
/// </summary>
public static readonly
RequestType<Credential, bool> Type =
RequestType<Credential, bool>.Create("credential/delete");
}
}

View File

@@ -0,0 +1,151 @@
//
// 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.Runtime.InteropServices;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
using Microsoft.SqlTools.ServiceLayer.Credentials.Linux;
using Microsoft.SqlTools.ServiceLayer.Credentials.OSX;
using Microsoft.SqlTools.ServiceLayer.Credentials.Win32;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
/// <summary>
/// Service responsible for securing credentials in a platform-neutral manner. This provides
/// a generic API for read, save and delete credentials
/// </summary>
public class CredentialService
{
internal static string DefaultSecretsFolder = ".sqlsecrets";
internal const string DefaultSecretsFile = "sqlsecrets.json";
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<CredentialService> instance
= new Lazy<CredentialService>(() => new CredentialService());
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static CredentialService Instance
{
get
{
return instance.Value;
}
}
private ICredentialStore credStore;
/// <summary>
/// Default constructor is private since it's a singleton class
/// </summary>
private CredentialService()
: this(null, new LinuxCredentialStore.StoreConfig()
{ CredentialFolder = DefaultSecretsFolder, CredentialFile = DefaultSecretsFile, IsRelativeToUserHomeDir = true})
{
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal CredentialService(ICredentialStore store, LinuxCredentialStore.StoreConfig config)
{
this.credStore = store != null ? store : GetStoreForOS(config);
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal static ICredentialStore GetStoreForOS(LinuxCredentialStore.StoreConfig config)
{
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return new Win32CredentialStore();
}
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return new OSXCredentialStore();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return new LinuxCredentialStore(config);
}
throw new InvalidOperationException("Platform not currently supported");
}
public void InitializeService(IProtocolEndpoint serviceHost)
{
// Register request and event handlers with the Service Host
serviceHost.SetRequestHandler(ReadCredentialRequest.Type, HandleReadCredentialRequest);
serviceHost.SetRequestHandler(SaveCredentialRequest.Type, HandleSaveCredentialRequest);
serviceHost.SetRequestHandler(DeleteCredentialRequest.Type, HandleDeleteCredentialRequest);
}
public async Task HandleReadCredentialRequest(Credential credential, RequestContext<Credential> requestContext)
{
Func<Credential> doRead = () =>
{
return ReadCredential(credential);
};
await HandleRequest(doRead, requestContext, "HandleReadCredentialRequest");
}
private Credential ReadCredential(Credential credential)
{
Credential.ValidateForLookup(credential);
Credential result = Credential.Copy(credential);
string password;
if (credStore.TryGetPassword(credential.CredentialId, out password))
{
result.Password = password;
}
return result;
}
public async Task HandleSaveCredentialRequest(Credential credential, RequestContext<bool> requestContext)
{
Func<bool> doSave = () =>
{
Credential.ValidateForSave(credential);
return credStore.Save(credential);
};
await HandleRequest(doSave, requestContext, "HandleSaveCredentialRequest");
}
public async Task HandleDeleteCredentialRequest(Credential credential, RequestContext<bool> requestContext)
{
Func<bool> doDelete = () =>
{
Credential.ValidateForLookup(credential);
return credStore.DeletePassword(credential.CredentialId);
};
await HandleRequest(doDelete, requestContext, "HandleDeleteCredentialRequest");
}
private async Task HandleRequest<T>(Func<T> handler, RequestContext<T> requestContext, string requestType)
{
Logger.Write(LogLevel.Verbose, requestType);
try
{
T result = handler();
await requestContext.SendResult(result);
}
catch (Exception ex)
{
await requestContext.SendError(ex.ToString());
}
}
}
}

View File

@@ -0,0 +1,41 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
/// <summary>
/// An <see cref="ICredentialStore"/> support securely saving and retrieving passwords
/// </summary>
public interface ICredentialStore
{
/// <summary>
/// Saves a Password linked to a given Credential
/// </summary>
/// <param name="credential">
/// A <see cref="Credential"/> to be saved.
/// <see cref="Credential.CredentialId"/> and <see cref="Credential.Password"/> are required
/// </param>
/// <returns>True if successful, false otherwise</returns>
bool Save(Credential credential);
/// <summary>
/// Gets a Password and sets it into a <see cref="Credential"/> object
/// </summary>
/// <param name="credentialId">The name of the credential to find the password for. This is required</param>
/// <param name="password">Out value</param>
/// <returns>true if password was found, false otherwise</returns>
bool TryGetPassword(string credentialId, out string password);
/// <summary>
/// Deletes a password linked to a given credential
/// </summary>
/// <param name="credentialId">The name of the credential to find the password for. This is required</param>
/// <returns>True if password existed and was deleted, false otherwise</returns>
bool DeletePassword(string credentialId);
}
}

View File

@@ -0,0 +1,36 @@
//
// 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.Runtime.InteropServices;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal static class InteropUtils
{
/// <summary>
/// Gets the length in bytes for a Unicode string, for use in interop where length must be defined
/// </summary>
public static UInt32 GetLengthInBytes(string value)
{
return Convert.ToUInt32( (value != null ? Encoding.Unicode.GetByteCount(value) : 0) );
}
public static string CopyToString(IntPtr ptr, int length)
{
if (ptr == IntPtr.Zero || length == 0)
{
return null;
}
byte[] pwdBytes = new byte[length];
Marshal.Copy(ptr, pwdBytes, 0, (int)length);
return Encoding.Unicode.GetString(pwdBytes, 0, (int)length);
}
}
}

View File

@@ -0,0 +1,18 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
{
/// <summary>
/// Simplified class to enable writing a set of credentials to/from disk
/// </summary>
public class CredentialsWrapper
{
public List<Credential> Credentials { get; set; }
}
}

View File

@@ -0,0 +1,87 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Newtonsoft.Json;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
{
public class FileTokenStorage
{
private const int OwnerAccessMode = 384; // Permission 0600 - owner read/write, nobody else has access
private object lockObject = new object();
private string fileName;
public FileTokenStorage(string fileName)
{
Validate.IsNotNullOrEmptyString("fileName", fileName);
this.fileName = fileName;
}
public void AddEntries(IEnumerable<Credential> newEntries, IEnumerable<Credential> existingEntries)
{
var allEntries = existingEntries.Concat(newEntries);
this.SaveEntries(allEntries);
}
public void Clear()
{
this.SaveEntries(new List<Credential>());
}
public IEnumerable<Credential> LoadEntries()
{
if(!File.Exists(this.fileName))
{
return Enumerable.Empty<Credential>();
}
string serializedCreds;
lock (lockObject)
{
serializedCreds = File.ReadAllText(this.fileName);
}
CredentialsWrapper creds = JsonConvert.DeserializeObject<CredentialsWrapper>(serializedCreds, Constants.JsonSerializerSettings);
if(creds != null)
{
return creds.Credentials;
}
return Enumerable.Empty<Credential>();
}
public void SaveEntries(IEnumerable<Credential> entries)
{
CredentialsWrapper credentials = new CredentialsWrapper() { Credentials = entries.ToList() };
string serializedCreds = JsonConvert.SerializeObject(credentials, Constants.JsonSerializerSettings);
lock(lockObject)
{
WriteToFile(this.fileName, serializedCreds);
}
}
private static void WriteToFile(string filePath, string fileContents)
{
string dir = Path.GetDirectoryName(filePath);
if(!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
// Overwrite file, then use ChMod to ensure we have
File.WriteAllText(filePath, fileContents);
// set appropriate permissions so only current user can read/write
Interop.Sys.ChMod(filePath, OwnerAccessMode);
}
}
}

View File

@@ -0,0 +1,221 @@
//
// 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.Runtime.InteropServices;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal static partial class Interop
{
/// <summary>Common Unix errno error codes.</summary>
internal enum Error
{
// These values were defined in src/Native/System.Native/fxerrno.h
//
// They compare against values obtained via Interop.Sys.GetLastError() not Marshal.GetLastWin32Error()
// which obtains the raw errno that varies between unixes. The strong typing as an enum is meant to
// prevent confusing the two. Casting to or from int is suspect. Use GetLastErrorInfo() if you need to
// correlate these to the underlying platform values or obtain the corresponding error message.
//
SUCCESS = 0,
E2BIG = 0x10001, // Argument list too long.
EACCES = 0x10002, // Permission denied.
EADDRINUSE = 0x10003, // Address in use.
EADDRNOTAVAIL = 0x10004, // Address not available.
EAFNOSUPPORT = 0x10005, // Address family not supported.
EAGAIN = 0x10006, // Resource unavailable, try again (same value as EWOULDBLOCK),
EALREADY = 0x10007, // Connection already in progress.
EBADF = 0x10008, // Bad file descriptor.
EBADMSG = 0x10009, // Bad message.
EBUSY = 0x1000A, // Device or resource busy.
ECANCELED = 0x1000B, // Operation canceled.
ECHILD = 0x1000C, // No child processes.
ECONNABORTED = 0x1000D, // Connection aborted.
ECONNREFUSED = 0x1000E, // Connection refused.
ECONNRESET = 0x1000F, // Connection reset.
EDEADLK = 0x10010, // Resource deadlock would occur.
EDESTADDRREQ = 0x10011, // Destination address required.
EDOM = 0x10012, // Mathematics argument out of domain of function.
EDQUOT = 0x10013, // Reserved.
EEXIST = 0x10014, // File exists.
EFAULT = 0x10015, // Bad address.
EFBIG = 0x10016, // File too large.
EHOSTUNREACH = 0x10017, // Host is unreachable.
EIDRM = 0x10018, // Identifier removed.
EILSEQ = 0x10019, // Illegal byte sequence.
EINPROGRESS = 0x1001A, // Operation in progress.
EINTR = 0x1001B, // Interrupted function.
EINVAL = 0x1001C, // Invalid argument.
EIO = 0x1001D, // I/O error.
EISCONN = 0x1001E, // Socket is connected.
EISDIR = 0x1001F, // Is a directory.
ELOOP = 0x10020, // Too many levels of symbolic links.
EMFILE = 0x10021, // File descriptor value too large.
EMLINK = 0x10022, // Too many links.
EMSGSIZE = 0x10023, // Message too large.
EMULTIHOP = 0x10024, // Reserved.
ENAMETOOLONG = 0x10025, // Filename too long.
ENETDOWN = 0x10026, // Network is down.
ENETRESET = 0x10027, // Connection aborted by network.
ENETUNREACH = 0x10028, // Network unreachable.
ENFILE = 0x10029, // Too many files open in system.
ENOBUFS = 0x1002A, // No buffer space available.
ENODEV = 0x1002C, // No such device.
ENOENT = 0x1002D, // No such file or directory.
ENOEXEC = 0x1002E, // Executable file format error.
ENOLCK = 0x1002F, // No locks available.
ENOLINK = 0x10030, // Reserved.
ENOMEM = 0x10031, // Not enough space.
ENOMSG = 0x10032, // No message of the desired type.
ENOPROTOOPT = 0x10033, // Protocol not available.
ENOSPC = 0x10034, // No space left on device.
ENOSYS = 0x10037, // Function not supported.
ENOTCONN = 0x10038, // The socket is not connected.
ENOTDIR = 0x10039, // Not a directory or a symbolic link to a directory.
ENOTEMPTY = 0x1003A, // Directory not empty.
ENOTSOCK = 0x1003C, // Not a socket.
ENOTSUP = 0x1003D, // Not supported (same value as EOPNOTSUP).
ENOTTY = 0x1003E, // Inappropriate I/O control operation.
ENXIO = 0x1003F, // No such device or address.
EOVERFLOW = 0x10040, // Value too large to be stored in data type.
EPERM = 0x10042, // Operation not permitted.
EPIPE = 0x10043, // Broken pipe.
EPROTO = 0x10044, // Protocol error.
EPROTONOSUPPORT = 0x10045, // Protocol not supported.
EPROTOTYPE = 0x10046, // Protocol wrong type for socket.
ERANGE = 0x10047, // Result too large.
EROFS = 0x10048, // Read-only file system.
ESPIPE = 0x10049, // Invalid seek.
ESRCH = 0x1004A, // No such process.
ESTALE = 0x1004B, // Reserved.
ETIMEDOUT = 0x1004D, // Connection timed out.
ETXTBSY = 0x1004E, // Text file busy.
EXDEV = 0x1004F, // Cross-device link.
ESOCKTNOSUPPORT = 0x1005E, // Socket type not supported.
EPFNOSUPPORT = 0x10060, // Protocol family not supported.
ESHUTDOWN = 0x1006C, // Socket shutdown.
EHOSTDOWN = 0x10070, // Host is down.
ENODATA = 0x10071, // No data available.
// POSIX permits these to have the same value and we make them always equal so
// that CoreFX cannot introduce a dependency on distinguishing between them that
// would not work on all platforms.
EOPNOTSUPP = ENOTSUP, // Operation not supported on socket.
EWOULDBLOCK = EAGAIN, // Operation would block.
}
// Represents a platform-agnostic Error and underlying platform-specific errno
internal struct ErrorInfo
{
private Error _error;
private int _rawErrno;
internal ErrorInfo(int errno)
{
_error = Interop.Sys.ConvertErrorPlatformToPal(errno);
_rawErrno = errno;
}
internal ErrorInfo(Error error)
{
_error = error;
_rawErrno = -1;
}
internal Error Error
{
get { return _error; }
}
internal int RawErrno
{
get { return _rawErrno == -1 ? (_rawErrno = Interop.Sys.ConvertErrorPalToPlatform(_error)) : _rawErrno; }
}
internal string GetErrorMessage()
{
return Interop.Sys.StrError(RawErrno);
}
public override string ToString()
{
return string.Format(
"RawErrno: {0} Error: {1} GetErrorMessage: {2}", // No localization required; text is member names used for debugging purposes
RawErrno, Error, GetErrorMessage());
}
}
internal partial class Sys
{
internal static Error GetLastError()
{
return ConvertErrorPlatformToPal(Marshal.GetLastWin32Error());
}
internal static ErrorInfo GetLastErrorInfo()
{
return new ErrorInfo(Marshal.GetLastWin32Error());
}
internal static string StrError(int platformErrno)
{
int maxBufferLength = 1024; // should be long enough for most any UNIX error
IntPtr buffer = Marshal.AllocHGlobal(maxBufferLength);
try
{
IntPtr message = StrErrorR(platformErrno, buffer, maxBufferLength);
if (message == IntPtr.Zero)
{
// This means the buffer was not large enough, but still contains
// as much of the error message as possible and is guaranteed to
// be null-terminated. We're not currently resizing/retrying because
// maxBufferLength is large enough in practice, but we could do
// so here in the future if necessary.
message = buffer;
}
string returnMsg = Marshal.PtrToStringAnsi(message);
return returnMsg;
}
finally
{
// Deallocate the buffer we created
Marshal.FreeHGlobal(buffer);
}
}
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPlatformToPal")]
internal static extern Error ConvertErrorPlatformToPal(int platformErrno);
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPalToPlatform")]
internal static extern int ConvertErrorPalToPlatform(Error error);
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_StrErrorR")]
private static extern IntPtr StrErrorR(int platformErrno, IntPtr buffer, int bufferSize);
}
}
// NOTE: extension method can't be nested inside Interop class.
internal static class InteropErrorExtensions
{
// Intended usage is e.g. Interop.Error.EFAIL.Info() for brevity
// vs. new Interop.ErrorInfo(Interop.Error.EFAIL) for synthesizing
// errors. Errors originated from the system should be obtained
// via GetLastErrorInfo(), not GetLastError().Info() as that will
// convert twice, which is not only inefficient but also lossy if
// we ever encounter a raw errno that no equivalent in the Error
// enum.
public static Interop.ErrorInfo Info(this Interop.Error error)
{
return new Interop.ErrorInfo(error);
}
}
}

View File

@@ -0,0 +1,42 @@
//
// 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.Runtime.InteropServices;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal static partial class Interop
{
internal static partial class Sys
{
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ChMod", SetLastError = true)]
internal static extern int ChMod(string path, int mode);
internal struct Passwd
{
internal IntPtr Name; // char*
internal IntPtr Password; // char*
internal uint UserId;
internal uint GroupId;
internal IntPtr UserInfo; // char*
internal IntPtr HomeDirectory; // char*
internal IntPtr Shell; // char*
};
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetPwUidR", SetLastError = false)]
internal static extern int GetPwUidR(uint uid, out Passwd pwd, IntPtr buf, int bufLen);
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetEUid")]
internal static extern uint GetEUid();
private static partial class Libraries
{
internal const string SystemNative = "System.Native";
}
}
}
}

View File

@@ -0,0 +1,231 @@
//
// 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.Runtime.InteropServices;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
{
/// <summary>
/// Linux implementation of the credential store.
///
/// <remarks>
/// This entire implementation may need to be revised to support encryption of
/// passwords and protection of them when loaded into memory.
/// </remarks>
/// </summary>
internal class LinuxCredentialStore : ICredentialStore
{
internal struct StoreConfig
{
public string CredentialFolder { get; set; }
public string CredentialFile { get; set; }
public bool IsRelativeToUserHomeDir { get; set; }
}
private string credentialFolderPath;
private string credentialFileName;
private FileTokenStorage storage;
public LinuxCredentialStore(StoreConfig config)
{
Validate.IsNotNull("config", config);
Validate.IsNotNullOrEmptyString("credentialFolder", config.CredentialFolder);
Validate.IsNotNullOrEmptyString("credentialFileName", config.CredentialFile);
this.credentialFolderPath = config.IsRelativeToUserHomeDir ? GetUserScopedDirectory(config.CredentialFolder) : config.CredentialFolder;
this.credentialFileName = config.CredentialFile;
string combinedPath = Path.Combine(this.credentialFolderPath, this.credentialFileName);
storage = new FileTokenStorage(combinedPath);
}
public bool DeletePassword(string credentialId)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
IEnumerable<Credential> creds;
if (LoadCredentialsAndFilterById(credentialId, out creds))
{
storage.SaveEntries(creds);
return true;
}
return false;
}
/// <summary>
/// Gets filtered credentials with a specific ID filtered out
/// </summary>
/// <returns>True if the credential to filter was removed, false if it was not found</returns>
private bool LoadCredentialsAndFilterById(string idToFilter, out IEnumerable<Credential> creds)
{
bool didRemove = false;
creds = storage.LoadEntries().Where(cred =>
{
if (IsCredentialMatch(idToFilter, cred))
{
didRemove = true;
return false; // filter this out
}
return true;
}).ToList(); // Call ToList ensures Where clause is executed so didRemove can be evaluated
return didRemove;
}
private static bool IsCredentialMatch(string credentialId, Credential cred)
{
return string.Equals(credentialId, cred.CredentialId, StringComparison.Ordinal);
}
public bool TryGetPassword(string credentialId, out string password)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
Credential cred = storage.LoadEntries().FirstOrDefault(c => IsCredentialMatch(credentialId, c));
if (cred != null)
{
password = cred.Password;
return true;
}
// Else this was not found in the list
password = null;
return false;
}
public bool Save(Credential credential)
{
Credential.ValidateForSave(credential);
// Load the credentials, removing the existing Cred for this
IEnumerable<Credential> creds;
LoadCredentialsAndFilterById(credential.CredentialId, out creds);
storage.SaveEntries(creds.Append(credential));
return true;
}
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal string CredentialFolderPath
{
get { return this.credentialFolderPath; }
}
/// <summary>
/// Concatenates a directory to the user home directory's path
/// </summary>
internal static string GetUserScopedDirectory(string userPath)
{
string homeDir = GetHomeDirectory() ?? string.Empty;
return Path.Combine(homeDir, userPath);
}
/// <summary>Gets the current user's home directory.</summary>
/// <returns>The path to the home directory, or null if it could not be determined.</returns>
internal static string GetHomeDirectory()
{
// First try to get the user's home directory from the HOME environment variable.
// This should work in most cases.
string userHomeDirectory = Environment.GetEnvironmentVariable("HOME");
if (!string.IsNullOrEmpty(userHomeDirectory))
{
return userHomeDirectory;
}
// In initialization conditions, however, the "HOME" environment variable may
// not yet be set. For such cases, consult with the password entry.
// First try with a buffer that should suffice for 99% of cases.
// Note that, theoretically, userHomeDirectory may be null in the success case
// if we simply couldn't find a home directory for the current user.
// In that case, we pass back the null value and let the caller decide
// what to do.
return GetHomeDirectoryFromPw();
}
internal static string GetHomeDirectoryFromPw()
{
string userHomeDirectory = null;
const int BufLen = 1024;
if (TryGetHomeDirectoryFromPasswd(BufLen, out userHomeDirectory))
{
return userHomeDirectory;
}
// Fallback to heap allocations if necessary, growing the buffer until
// we succeed. TryGetHomeDirectory will throw if there's an unexpected error.
int lastBufLen = BufLen;
while (true)
{
lastBufLen *= 2;
if (TryGetHomeDirectoryFromPasswd(lastBufLen, out userHomeDirectory))
{
return userHomeDirectory;
}
}
}
/// <summary>Wrapper for getpwuid_r.</summary>
/// <param name="bufLen">The length of the buffer to use when storing the password result.</param>
/// <param name="path">The resulting path; null if the user didn't have an entry.</param>
/// <returns>true if the call was successful (path may still be null); false is a larger buffer is needed.</returns>
private static bool TryGetHomeDirectoryFromPasswd(int bufLen, out string path)
{
// Call getpwuid_r to get the passwd struct
Interop.Sys.Passwd passwd;
IntPtr buffer = Marshal.AllocHGlobal(bufLen);
try
{
int error = Interop.Sys.GetPwUidR(Interop.Sys.GetEUid(), out passwd, buffer, bufLen);
// If the call succeeds, give back the home directory path retrieved
if (error == 0)
{
Debug.Assert(passwd.HomeDirectory != IntPtr.Zero);
path = Marshal.PtrToStringAnsi(passwd.HomeDirectory);
return true;
}
// If the current user's entry could not be found, give back null
// path, but still return true as false indicates the buffer was
// too small.
if (error == -1)
{
path = null;
return true;
}
var errorInfo = new Interop.ErrorInfo(error);
// If the call failed because the buffer was too small, return false to
// indicate the caller should try again with a larger buffer.
if (errorInfo.Error == Interop.Error.ERANGE)
{
path = null;
return false;
}
// Otherwise, fail.
throw new IOException(errorInfo.GetErrorMessage(), errorInfo.RawErrno);
}
finally
{
// Deallocate the buffer we created
Marshal.FreeHGlobal(buffer);
}
}
}
}

View File

@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal static partial class Interop
{
internal static partial class CoreFoundation
{
/// <summary>
/// Tells the OS what encoding the passed in String is in. These come from the CFString.h header file in the CoreFoundation framework.
/// </summary>
private enum CFStringBuiltInEncodings : uint
{
kCFStringEncodingMacRoman = 0,
kCFStringEncodingWindowsLatin1 = 0x0500,
kCFStringEncodingISOLatin1 = 0x0201,
kCFStringEncodingNextStepLatin = 0x0B01,
kCFStringEncodingASCII = 0x0600,
kCFStringEncodingUnicode = 0x0100,
kCFStringEncodingUTF8 = 0x08000100,
kCFStringEncodingNonLossyASCII = 0x0BFF,
kCFStringEncodingUTF16 = 0x0100,
kCFStringEncodingUTF16BE = 0x10000100,
kCFStringEncodingUTF16LE = 0x14000100,
kCFStringEncodingUTF32 = 0x0c000100,
kCFStringEncodingUTF32BE = 0x18000100,
kCFStringEncodingUTF32LE = 0x1c000100
}
/// <summary>
/// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
/// </summary>
/// <param name="allocator">Should be IntPtr.Zero</param>
/// <param name="str">The string to get a CFStringRef for</param>
/// <param name="encoding">The encoding of the str variable. This should be UTF 8 for OS X</param>
/// <returns>Returns a pointer to a CFString on success; otherwise, returns IntPtr.Zero</returns>
/// <remarks>For *nix systems, the CLR maps ANSI to UTF-8, so be explicit about that</remarks>
[DllImport(Interop.Libraries.CoreFoundationLibrary, CharSet = CharSet.Ansi)]
private static extern SafeCreateHandle CFStringCreateWithCString(
IntPtr allocator,
string str,
CFStringBuiltInEncodings encoding);
/// <summary>
/// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
/// </summary>
/// <param name="str">The string to get a CFStringRef for</param>
/// <returns>Returns a valid SafeCreateHandle to a CFString on success; otherwise, returns an invalid SafeCreateHandle</returns>
internal static SafeCreateHandle CFStringCreateWithCString(string str)
{
return CFStringCreateWithCString(IntPtr.Zero, str, CFStringBuiltInEncodings.kCFStringEncodingUTF8);
}
/// <summary>
/// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
/// </summary>
/// <param name="allocator">Should be IntPtr.Zero</param>
/// <param name="values">The values to put in the array</param>
/// <param name="numValues">The number of values in the array</param>
/// <param name="callbacks">Should be IntPtr.Zero</param>
/// <returns>Returns a pointer to a CFArray on success; otherwise, returns IntPtr.Zero</returns>
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
private static extern SafeCreateHandle CFArrayCreate(
IntPtr allocator,
[MarshalAs(UnmanagedType.LPArray)]
IntPtr[] values,
ulong numValues,
IntPtr callbacks);
/// <summary>
/// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
/// </summary>
/// <param name="values">The values to put in the array</param>
/// <param name="numValues">The number of values in the array</param>
/// <returns>Returns a valid SafeCreateHandle to a CFArray on success; otherwise, returns an invalid SafeCreateHandle</returns>
internal static SafeCreateHandle CFArrayCreate(IntPtr[] values, ulong numValues)
{
return CFArrayCreate(IntPtr.Zero, values, numValues, IntPtr.Zero);
}
/// <summary>
/// You should retain a Core Foundation object when you receive it from elsewhere
/// (that is, you did not create or copy it) and you want it to persist. If you
/// retain a Core Foundation object you are responsible for releasing it
/// </summary>
/// <param name="ptr">The CFType object to retain. This value must not be NULL</param>
/// <returns>The input value</param>
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
internal extern static IntPtr CFRetain(IntPtr ptr);
/// <summary>
/// Decrements the reference count on the specified object and, if the ref count hits 0, cleans up the object.
/// </summary>
/// <param name="ptr">The pointer on which to decrement the reference count.</param>
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
internal extern static void CFRelease(IntPtr ptr);
}
}
}

View File

@@ -0,0 +1,17 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal static partial class Interop
{
private static partial class Libraries
{
internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
internal const string CoreServicesLibrary = "/System/Library/Frameworks/CoreServices.framework/CoreServices";
internal const string SecurityLibrary = "/System/Library/Frameworks/Security.framework/Security";
}
}
}

View File

@@ -0,0 +1,459 @@
//
// 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.Runtime.InteropServices;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
internal partial class Interop
{
internal partial class Security
{
[DllImport(Libraries.SecurityLibrary, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern OSStatus SecKeychainAddGenericPassword(IntPtr keyChainRef, UInt32 serviceNameLength, string serviceName,
UInt32 accountNameLength, string accountName, UInt32 passwordLength, IntPtr password, [Out] IntPtr itemRef);
/// <summary>
/// Find a generic password based on the attributes passed
/// </summary>
/// <param name="keyChainRef">
/// A reference to an array of keychains to search, a single keychain, or NULL to search the user's default keychain search list.
/// </param>
/// <param name="serviceNameLength">The length of the buffer pointed to by serviceName.</param>
/// <param name="serviceName">A pointer to a string containing the service name.</param>
/// <param name="accountNameLength">The length of the buffer pointed to by accountName.</param>
/// <param name="accountName">A pointer to a string containing the account name.</param>
/// <param name="passwordLength">On return, the length of the buffer pointed to by passwordData.</param>
/// <param name="password">
/// On return, a pointer to a data buffer containing the password.
/// Your application must call SecKeychainItemFreeContent(NULL, passwordData)
/// to release this data buffer when it is no longer needed.Pass NULL if you are not interested in retrieving the password data at
/// this time, but simply want to find the item reference.
/// </param>
/// <param name="itemRef">On return, a reference to the keychain item which was found.</param>
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
/// <remarks>
/// The SecKeychainFindGenericPassword function finds the first generic password item which matches the attributes you provide.
/// Most attributes are optional; you should pass only as many as you need to narrow the search sufficiently for your application's intended use.
/// SecKeychainFindGenericPassword optionally returns a reference to the found item.
/// </remarks>
[DllImport(Libraries.SecurityLibrary, CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern OSStatus SecKeychainFindGenericPassword(IntPtr keyChainRef, UInt32 serviceNameLength, string serviceName,
UInt32 accountNameLength, string accountName, out UInt32 passwordLength, out IntPtr password, out IntPtr itemRef);
/// <summary>
/// Releases the memory used by the keychain attribute list and the keychain data retrieved in a previous call to SecKeychainItemCopyContent.
/// </summary>
/// <param name="attrList">A pointer to the attribute list to release. Pass NULL to ignore this parameter.</param>
/// <param name="data">A pointer to the data buffer to release. Pass NULL to ignore this parameter.</param>
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
[DllImport(Libraries.SecurityLibrary, SetLastError = true)]
internal static extern OSStatus SecKeychainItemFreeContent([In] IntPtr attrList, [In] IntPtr data);
/// <summary>
/// Deletes a keychain item from the default keychain's permanent data store.
/// </summary>
/// <param name="itemRef">A keychain item reference of the item to delete.</param>
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
/// <remarks>
/// If itemRef has not previously been added to the keychain, SecKeychainItemDelete does nothing and returns ErrSecSuccess.
/// IMPORTANT: SecKeychainItemDelete does not dispose the memory occupied by the item reference itself;
/// use the CFRelease function when you are completely * * finished with an item.
/// </remarks>
[DllImport(Libraries.SecurityLibrary, SetLastError = true)]
internal static extern OSStatus SecKeychainItemDelete(SafeHandle itemRef);
#region OSStatus Codes
/// <summary>Common Unix errno error codes.</summary>
internal enum OSStatus
{
ErrSecSuccess = 0, /* No error. */
ErrSecUnimplemented = -4, /* Function or operation not implemented. */
ErrSecDskFull = -34,
ErrSecIO = -36, /*I/O error (bummers)*/
ErrSecParam = -50, /* One or more parameters passed to a function were not valid. */
ErrSecWrPerm = -61, /* write permissions error*/
ErrSecAllocate = -108, /* Failed to allocate memory. */
ErrSecUserCanceled = -128, /* User canceled the operation. */
ErrSecBadReq = -909, /* Bad parameter or invalid state for operation. */
ErrSecInternalComponent = -2070,
ErrSecCoreFoundationUnknown = -4960,
ErrSecNotAvailable = -25291, /* No keychain is available. You may need to restart your computer. */
ErrSecReadOnly = -25292, /* This keychain cannot be modified. */
ErrSecAuthFailed = -25293, /* The user name or passphrase you entered is not correct. */
ErrSecNoSuchKeychain = -25294, /* The specified keychain could not be found. */
ErrSecInvalidKeychain = -25295, /* The specified keychain is not a valid keychain file. */
ErrSecDuplicateKeychain = -25296, /* A keychain with the same name already exists. */
ErrSecDuplicateCallback = -25297, /* The specified callback function is already installed. */
ErrSecInvalidCallback = -25298, /* The specified callback function is not valid. */
ErrSecDuplicateItem = -25299, /* The specified item already exists in the keychain. */
ErrSecItemNotFound = -25300, /* The specified item could not be found in the keychain. */
ErrSecBufferTooSmall = -25301, /* There is not enough memory available to use the specified item. */
ErrSecDataTooLarge = -25302, /* This item contains information which is too large or in a format that cannot be displayed. */
ErrSecNoSuchAttr = -25303, /* The specified attribute does not exist. */
ErrSecInvalidItemRef = -25304, /* The specified item is no longer valid. It may have been deleted from the keychain. */
ErrSecInvalidSearchRef = -25305, /* Unable to search the current keychain. */
ErrSecNoSuchClass = -25306, /* The specified item does not appear to be a valid keychain item. */
ErrSecNoDefaultKeychain = -25307, /* A default keychain could not be found. */
ErrSecInteractionNotAllowed = -25308, /* User interaction is not allowed. */
ErrSecReadOnlyAttr = -25309, /* The specified attribute could not be modified. */
ErrSecWrongSecVersion = -25310, /* This keychain was created by a different version of the system software and cannot be opened. */
ErrSecKeySizeNotAllowed = -25311, /* This item specifies a key size which is too large. */
ErrSecNoStorageModule = -25312, /* A required component (data storage module) could not be loaded. You may need to restart your computer. */
ErrSecNoCertificateModule = -25313, /* A required component (certificate module) could not be loaded. You may need to restart your computer. */
ErrSecNoPolicyModule = -25314, /* A required component (policy module) could not be loaded. You may need to restart your computer. */
ErrSecInteractionRequired = -25315, /* User interaction is required, but is currently not allowed. */
ErrSecDataNotAvailable = -25316, /* The contents of this item cannot be retrieved. */
ErrSecDataNotModifiable = -25317, /* The contents of this item cannot be modified. */
ErrSecCreateChainFailed = -25318, /* One or more certificates required to validate this certificate cannot be found. */
ErrSecInvalidPrefsDomain = -25319, /* The specified preferences domain is not valid. */
ErrSecInDarkWake = -25320, /* In dark wake, no UI possible */
ErrSecACLNotSimple = -25240, /* The specified access control list is not in standard (simple) form. */
ErrSecPolicyNotFound = -25241, /* The specified policy cannot be found. */
ErrSecInvalidTrustSetting = -25242, /* The specified trust setting is invalid. */
ErrSecNoAccessForItem = -25243, /* The specified item has no access control. */
ErrSecInvalidOwnerEdit = -25244, /* Invalid attempt to change the owner of this item. */
ErrSecTrustNotAvailable = -25245, /* No trust results are available. */
ErrSecUnsupportedFormat = -25256, /* Import/Export format unsupported. */
ErrSecUnknownFormat = -25257, /* Unknown format in import. */
ErrSecKeyIsSensitive = -25258, /* Key material must be wrapped for export. */
ErrSecMultiplePrivKeys = -25259, /* An attempt was made to import multiple private keys. */
ErrSecPassphraseRequired = -25260, /* Passphrase is required for import/export. */
ErrSecInvalidPasswordRef = -25261, /* The password reference was invalid. */
ErrSecInvalidTrustSettings = -25262, /* The Trust Settings Record was corrupted. */
ErrSecNoTrustSettings = -25263, /* No Trust Settings were found. */
ErrSecPkcs12VerifyFailure = -25264, /* MAC verification failed during PKCS12 import (wrong password?) */
ErrSecNotSigner = -26267, /* A certificate was not signed by its proposed parent. */
ErrSecDecode = -26275, /* Unable to decode the provided data. */
ErrSecServiceNotAvailable = -67585, /* The required service is not available. */
ErrSecInsufficientClientID = -67586, /* The client ID is not correct. */
ErrSecDeviceReset = -67587, /* A device reset has occurred. */
ErrSecDeviceFailed = -67588, /* A device failure has occurred. */
ErrSecAppleAddAppACLSubject = -67589, /* Adding an application ACL subject failed. */
ErrSecApplePublicKeyIncomplete = -67590, /* The public key is incomplete. */
ErrSecAppleSignatureMismatch = -67591, /* A signature mismatch has occurred. */
ErrSecAppleInvalidKeyStartDate = -67592, /* The specified key has an invalid start date. */
ErrSecAppleInvalidKeyEndDate = -67593, /* The specified key has an invalid end date. */
ErrSecConversionError = -67594, /* A conversion error has occurred. */
ErrSecAppleSSLv2Rollback = -67595, /* A SSLv2 rollback error has occurred. */
ErrSecDiskFull = -34, /* The disk is full. */
ErrSecQuotaExceeded = -67596, /* The quota was exceeded. */
ErrSecFileTooBig = -67597, /* The file is too big. */
ErrSecInvalidDatabaseBlob = -67598, /* The specified database has an invalid blob. */
ErrSecInvalidKeyBlob = -67599, /* The specified database has an invalid key blob. */
ErrSecIncompatibleDatabaseBlob = -67600, /* The specified database has an incompatible blob. */
ErrSecIncompatibleKeyBlob = -67601, /* The specified database has an incompatible key blob. */
ErrSecHostNameMismatch = -67602, /* A host name mismatch has occurred. */
ErrSecUnknownCriticalExtensionFlag = -67603, /* There is an unknown critical extension flag. */
ErrSecNoBasicConstraints = -67604, /* No basic constraints were found. */
ErrSecNoBasicConstraintsCA = -67605, /* No basic CA constraints were found. */
ErrSecInvalidAuthorityKeyID = -67606, /* The authority key ID is not valid. */
ErrSecInvalidSubjectKeyID = -67607, /* The subject key ID is not valid. */
ErrSecInvalidKeyUsageForPolicy = -67608, /* The key usage is not valid for the specified policy. */
ErrSecInvalidExtendedKeyUsage = -67609, /* The extended key usage is not valid. */
ErrSecInvalidIDLinkage = -67610, /* The ID linkage is not valid. */
ErrSecPathLengthConstraintExceeded = -67611, /* The path length constraint was exceeded. */
ErrSecInvalidRoot = -67612, /* The root or anchor certificate is not valid. */
ErrSecCRLExpired = -67613, /* The CRL has expired. */
ErrSecCRLNotValidYet = -67614, /* The CRL is not yet valid. */
ErrSecCRLNotFound = -67615, /* The CRL was not found. */
ErrSecCRLServerDown = -67616, /* The CRL server is down. */
ErrSecCRLBadURI = -67617, /* The CRL has a bad Uniform Resource Identifier. */
ErrSecUnknownCertExtension = -67618, /* An unknown certificate extension was encountered. */
ErrSecUnknownCRLExtension = -67619, /* An unknown CRL extension was encountered. */
ErrSecCRLNotTrusted = -67620, /* The CRL is not trusted. */
ErrSecCRLPolicyFailed = -67621, /* The CRL policy failed. */
ErrSecIDPFailure = -67622, /* The issuing distribution point was not valid. */
ErrSecSMIMEEmailAddressesNotFound = -67623, /* An email address mismatch was encountered. */
ErrSecSMIMEBadExtendedKeyUsage = -67624, /* The appropriate extended key usage for SMIME was not found. */
ErrSecSMIMEBadKeyUsage = -67625, /* The key usage is not compatible with SMIME. */
ErrSecSMIMEKeyUsageNotCritical = -67626, /* The key usage extension is not marked as critical. */
ErrSecSMIMENoEmailAddress = -67627, /* No email address was found in the certificate. */
ErrSecSMIMESubjAltNameNotCritical = -67628, /* The subject alternative name extension is not marked as critical. */
ErrSecSSLBadExtendedKeyUsage = -67629, /* The appropriate extended key usage for SSL was not found. */
ErrSecOCSPBadResponse = -67630, /* The OCSP response was incorrect or could not be parsed. */
ErrSecOCSPBadRequest = -67631, /* The OCSP request was incorrect or could not be parsed. */
ErrSecOCSPUnavailable = -67632, /* OCSP service is unavailable. */
ErrSecOCSPStatusUnrecognized = -67633, /* The OCSP server did not recognize this certificate. */
ErrSecEndOfData = -67634, /* An end-of-data was detected. */
ErrSecIncompleteCertRevocationCheck = -67635, /* An incomplete certificate revocation check occurred. */
ErrSecNetworkFailure = -67636, /* A network failure occurred. */
ErrSecOCSPNotTrustedToAnchor = -67637, /* The OCSP response was not trusted to a root or anchor certificate. */
ErrSecRecordModified = -67638, /* The record was modified. */
ErrSecOCSPSignatureError = -67639, /* The OCSP response had an invalid signature. */
ErrSecOCSPNoSigner = -67640, /* The OCSP response had no signer. */
ErrSecOCSPResponderMalformedReq = -67641, /* The OCSP responder was given a malformed request. */
ErrSecOCSPResponderInternalError = -67642, /* The OCSP responder encountered an internal error. */
ErrSecOCSPResponderTryLater = -67643, /* The OCSP responder is busy, try again later. */
ErrSecOCSPResponderSignatureRequired = -67644, /* The OCSP responder requires a signature. */
ErrSecOCSPResponderUnauthorized = -67645, /* The OCSP responder rejected this request as unauthorized. */
ErrSecOCSPResponseNonceMismatch = -67646, /* The OCSP response nonce did not match the request. */
ErrSecCodeSigningBadCertChainLength = -67647, /* Code signing encountered an incorrect certificate chain length. */
ErrSecCodeSigningNoBasicConstraints = -67648, /* Code signing found no basic constraints. */
ErrSecCodeSigningBadPathLengthConstraint= -67649, /* Code signing encountered an incorrect path length constraint. */
ErrSecCodeSigningNoExtendedKeyUsage = -67650, /* Code signing found no extended key usage. */
ErrSecCodeSigningDevelopment = -67651, /* Code signing indicated use of a development-only certificate. */
ErrSecResourceSignBadCertChainLength = -67652, /* Resource signing has encountered an incorrect certificate chain length. */
ErrSecResourceSignBadExtKeyUsage = -67653, /* Resource signing has encountered an error in the extended key usage. */
ErrSecTrustSettingDeny = -67654, /* The trust setting for this policy was set to Deny. */
ErrSecInvalidSubjectName = -67655, /* An invalid certificate subject name was encountered. */
ErrSecUnknownQualifiedCertStatement = -67656, /* An unknown qualified certificate statement was encountered. */
ErrSecMobileMeRequestQueued = -67657, /* The MobileMe request will be sent during the next connection. */
ErrSecMobileMeRequestRedirected = -67658, /* The MobileMe request was redirected. */
ErrSecMobileMeServerError = -67659, /* A MobileMe server error occurred. */
ErrSecMobileMeServerNotAvailable = -67660, /* The MobileMe server is not available. */
ErrSecMobileMeServerAlreadyExists = -67661, /* The MobileMe server reported that the item already exists. */
ErrSecMobileMeServerServiceErr = -67662, /* A MobileMe service error has occurred. */
ErrSecMobileMeRequestAlreadyPending = -67663, /* A MobileMe request is already pending. */
ErrSecMobileMeNoRequestPending = -67664, /* MobileMe has no request pending. */
ErrSecMobileMeCSRVerifyFailure = -67665, /* A MobileMe CSR verification failure has occurred. */
ErrSecMobileMeFailedConsistencyCheck = -67666, /* MobileMe has found a failed consistency check. */
ErrSecNotInitialized = -67667, /* A function was called without initializing CSSM. */
ErrSecInvalidHandleUsage = -67668, /* The CSSM handle does not match with the service type. */
ErrSecPVCReferentNotFound = -67669, /* A reference to the calling module was not found in the list of authorized callers. */
ErrSecFunctionIntegrityFail = -67670, /* A function address was not within the verified module. */
ErrSecInternalError = -67671, /* An internal error has occurred. */
ErrSecMemoryError = -67672, /* A memory error has occurred. */
ErrSecInvalidData = -67673, /* Invalid data was encountered. */
ErrSecMDSError = -67674, /* A Module Directory Service error has occurred. */
ErrSecInvalidPointer = -67675, /* An invalid pointer was encountered. */
ErrSecSelfCheckFailed = -67676, /* Self-check has failed. */
ErrSecFunctionFailed = -67677, /* A function has failed. */
ErrSecModuleManifestVerifyFailed = -67678, /* A module manifest verification failure has occurred. */
ErrSecInvalidGUID = -67679, /* An invalid GUID was encountered. */
ErrSecInvalidHandle = -67680, /* An invalid handle was encountered. */
ErrSecInvalidDBList = -67681, /* An invalid DB list was encountered. */
ErrSecInvalidPassthroughID = -67682, /* An invalid passthrough ID was encountered. */
ErrSecInvalidNetworkAddress = -67683, /* An invalid network address was encountered. */
ErrSecCRLAlreadySigned = -67684, /* The certificate revocation list is already signed. */
ErrSecInvalidNumberOfFields = -67685, /* An invalid number of fields were encountered. */
ErrSecVerificationFailure = -67686, /* A verification failure occurred. */
ErrSecUnknownTag = -67687, /* An unknown tag was encountered. */
ErrSecInvalidSignature = -67688, /* An invalid signature was encountered. */
ErrSecInvalidName = -67689, /* An invalid name was encountered. */
ErrSecInvalidCertificateRef = -67690, /* An invalid certificate reference was encountered. */
ErrSecInvalidCertificateGroup = -67691, /* An invalid certificate group was encountered. */
ErrSecTagNotFound = -67692, /* The specified tag was not found. */
ErrSecInvalidQuery = -67693, /* The specified query was not valid. */
ErrSecInvalidValue = -67694, /* An invalid value was detected. */
ErrSecCallbackFailed = -67695, /* A callback has failed. */
ErrSecACLDeleteFailed = -67696, /* An ACL delete operation has failed. */
ErrSecACLReplaceFailed = -67697, /* An ACL replace operation has failed. */
ErrSecACLAddFailed = -67698, /* An ACL add operation has failed. */
ErrSecACLChangeFailed = -67699, /* An ACL change operation has failed. */
ErrSecInvalidAccessCredentials = -67700, /* Invalid access credentials were encountered. */
ErrSecInvalidRecord = -67701, /* An invalid record was encountered. */
ErrSecInvalidACL = -67702, /* An invalid ACL was encountered. */
ErrSecInvalidSampleValue = -67703, /* An invalid sample value was encountered. */
ErrSecIncompatibleVersion = -67704, /* An incompatible version was encountered. */
ErrSecPrivilegeNotGranted = -67705, /* The privilege was not granted. */
ErrSecInvalidScope = -67706, /* An invalid scope was encountered. */
ErrSecPVCAlreadyConfigured = -67707, /* The PVC is already configured. */
ErrSecInvalidPVC = -67708, /* An invalid PVC was encountered. */
ErrSecEMMLoadFailed = -67709, /* The EMM load has failed. */
ErrSecEMMUnloadFailed = -67710, /* The EMM unload has failed. */
ErrSecAddinLoadFailed = -67711, /* The add-in load operation has failed. */
ErrSecInvalidKeyRef = -67712, /* An invalid key was encountered. */
ErrSecInvalidKeyHierarchy = -67713, /* An invalid key hierarchy was encountered. */
ErrSecAddinUnloadFailed = -67714, /* The add-in unload operation has failed. */
ErrSecLibraryReferenceNotFound = -67715, /* A library reference was not found. */
ErrSecInvalidAddinFunctionTable = -67716, /* An invalid add-in function table was encountered. */
ErrSecInvalidServiceMask = -67717, /* An invalid service mask was encountered. */
ErrSecModuleNotLoaded = -67718, /* A module was not loaded. */
ErrSecInvalidSubServiceID = -67719, /* An invalid subservice ID was encountered. */
ErrSecAttributeNotInContext = -67720, /* An attribute was not in the context. */
ErrSecModuleManagerInitializeFailed = -67721, /* A module failed to initialize. */
ErrSecModuleManagerNotFound = -67722, /* A module was not found. */
ErrSecEventNotificationCallbackNotFound = -67723, /* An event notification callback was not found. */
ErrSecInputLengthError = -67724, /* An input length error was encountered. */
ErrSecOutputLengthError = -67725, /* An output length error was encountered. */
ErrSecPrivilegeNotSupported = -67726, /* The privilege is not supported. */
ErrSecDeviceError = -67727, /* A device error was encountered. */
ErrSecAttachHandleBusy = -67728, /* The CSP handle was busy. */
ErrSecNotLoggedIn = -67729, /* You are not logged in. */
ErrSecAlgorithmMismatch = -67730, /* An algorithm mismatch was encountered. */
ErrSecKeyUsageIncorrect = -67731, /* The key usage is incorrect. */
ErrSecKeyBlobTypeIncorrect = -67732, /* The key blob type is incorrect. */
ErrSecKeyHeaderInconsistent = -67733, /* The key header is inconsistent. */
ErrSecUnsupportedKeyFormat = -67734, /* The key header format is not supported. */
ErrSecUnsupportedKeySize = -67735, /* The key size is not supported. */
ErrSecInvalidKeyUsageMask = -67736, /* The key usage mask is not valid. */
ErrSecUnsupportedKeyUsageMask = -67737, /* The key usage mask is not supported. */
ErrSecInvalidKeyAttributeMask = -67738, /* The key attribute mask is not valid. */
ErrSecUnsupportedKeyAttributeMask = -67739, /* The key attribute mask is not supported. */
ErrSecInvalidKeyLabel = -67740, /* The key label is not valid. */
ErrSecUnsupportedKeyLabel = -67741, /* The key label is not supported. */
ErrSecInvalidKeyFormat = -67742, /* The key format is not valid. */
ErrSecUnsupportedVectorOfBuffers = -67743, /* The vector of buffers is not supported. */
ErrSecInvalidInputVector = -67744, /* The input vector is not valid. */
ErrSecInvalidOutputVector = -67745, /* The output vector is not valid. */
ErrSecInvalidContext = -67746, /* An invalid context was encountered. */
ErrSecInvalidAlgorithm = -67747, /* An invalid algorithm was encountered. */
ErrSecInvalidAttributeKey = -67748, /* A key attribute was not valid. */
ErrSecMissingAttributeKey = -67749, /* A key attribute was missing. */
ErrSecInvalidAttributeInitVector = -67750, /* An init vector attribute was not valid. */
ErrSecMissingAttributeInitVector = -67751, /* An init vector attribute was missing. */
ErrSecInvalidAttributeSalt = -67752, /* A salt attribute was not valid. */
ErrSecMissingAttributeSalt = -67753, /* A salt attribute was missing. */
ErrSecInvalidAttributePadding = -67754, /* A padding attribute was not valid. */
ErrSecMissingAttributePadding = -67755, /* A padding attribute was missing. */
ErrSecInvalidAttributeRandom = -67756, /* A random number attribute was not valid. */
ErrSecMissingAttributeRandom = -67757, /* A random number attribute was missing. */
ErrSecInvalidAttributeSeed = -67758, /* A seed attribute was not valid. */
ErrSecMissingAttributeSeed = -67759, /* A seed attribute was missing. */
ErrSecInvalidAttributePassphrase = -67760, /* A passphrase attribute was not valid. */
ErrSecMissingAttributePassphrase = -67761, /* A passphrase attribute was missing. */
ErrSecInvalidAttributeKeyLength = -67762, /* A key length attribute was not valid. */
ErrSecMissingAttributeKeyLength = -67763, /* A key length attribute was missing. */
ErrSecInvalidAttributeBlockSize = -67764, /* A block size attribute was not valid. */
ErrSecMissingAttributeBlockSize = -67765, /* A block size attribute was missing. */
ErrSecInvalidAttributeOutputSize = -67766, /* An output size attribute was not valid. */
ErrSecMissingAttributeOutputSize = -67767, /* An output size attribute was missing. */
ErrSecInvalidAttributeRounds = -67768, /* The number of rounds attribute was not valid. */
ErrSecMissingAttributeRounds = -67769, /* The number of rounds attribute was missing. */
ErrSecInvalidAlgorithmParms = -67770, /* An algorithm parameters attribute was not valid. */
ErrSecMissingAlgorithmParms = -67771, /* An algorithm parameters attribute was missing. */
ErrSecInvalidAttributeLabel = -67772, /* A label attribute was not valid. */
ErrSecMissingAttributeLabel = -67773, /* A label attribute was missing. */
ErrSecInvalidAttributeKeyType = -67774, /* A key type attribute was not valid. */
ErrSecMissingAttributeKeyType = -67775, /* A key type attribute was missing. */
ErrSecInvalidAttributeMode = -67776, /* A mode attribute was not valid. */
ErrSecMissingAttributeMode = -67777, /* A mode attribute was missing. */
ErrSecInvalidAttributeEffectiveBits = -67778, /* An effective bits attribute was not valid. */
ErrSecMissingAttributeEffectiveBits = -67779, /* An effective bits attribute was missing. */
ErrSecInvalidAttributeStartDate = -67780, /* A start date attribute was not valid. */
ErrSecMissingAttributeStartDate = -67781, /* A start date attribute was missing. */
ErrSecInvalidAttributeEndDate = -67782, /* An end date attribute was not valid. */
ErrSecMissingAttributeEndDate = -67783, /* An end date attribute was missing. */
ErrSecInvalidAttributeVersion = -67784, /* A version attribute was not valid. */
ErrSecMissingAttributeVersion = -67785, /* A version attribute was missing. */
ErrSecInvalidAttributePrime = -67786, /* A prime attribute was not valid. */
ErrSecMissingAttributePrime = -67787, /* A prime attribute was missing. */
ErrSecInvalidAttributeBase = -67788, /* A base attribute was not valid. */
ErrSecMissingAttributeBase = -67789, /* A base attribute was missing. */
ErrSecInvalidAttributeSubprime = -67790, /* A subprime attribute was not valid. */
ErrSecMissingAttributeSubprime = -67791, /* A subprime attribute was missing. */
ErrSecInvalidAttributeIterationCount = -67792, /* An iteration count attribute was not valid. */
ErrSecMissingAttributeIterationCount = -67793, /* An iteration count attribute was missing. */
ErrSecInvalidAttributeDLDBHandle = -67794, /* A database handle attribute was not valid. */
ErrSecMissingAttributeDLDBHandle = -67795, /* A database handle attribute was missing. */
ErrSecInvalidAttributeAccessCredentials = -67796, /* An access credentials attribute was not valid. */
ErrSecMissingAttributeAccessCredentials = -67797, /* An access credentials attribute was missing. */
ErrSecInvalidAttributePublicKeyFormat = -67798, /* A public key format attribute was not valid. */
ErrSecMissingAttributePublicKeyFormat = -67799, /* A public key format attribute was missing. */
ErrSecInvalidAttributePrivateKeyFormat = -67800, /* A private key format attribute was not valid. */
ErrSecMissingAttributePrivateKeyFormat = -67801, /* A private key format attribute was missing. */
ErrSecInvalidAttributeSymmetricKeyFormat = -67802, /* A symmetric key format attribute was not valid. */
ErrSecMissingAttributeSymmetricKeyFormat = -67803, /* A symmetric key format attribute was missing. */
ErrSecInvalidAttributeWrappedKeyFormat = -67804, /* A wrapped key format attribute was not valid. */
ErrSecMissingAttributeWrappedKeyFormat = -67805, /* A wrapped key format attribute was missing. */
ErrSecStagedOperationInProgress = -67806, /* A staged operation is in progress. */
ErrSecStagedOperationNotStarted = -67807, /* A staged operation was not started. */
ErrSecVerifyFailed = -67808, /* A cryptographic verification failure has occurred. */
ErrSecQuerySizeUnknown = -67809, /* The query size is unknown. */
ErrSecBlockSizeMismatch = -67810, /* A block size mismatch occurred. */
ErrSecPublicKeyInconsistent = -67811, /* The public key was inconsistent. */
ErrSecDeviceVerifyFailed = -67812, /* A device verification failure has occurred. */
ErrSecInvalidLoginName = -67813, /* An invalid login name was detected. */
ErrSecAlreadyLoggedIn = -67814, /* The user is already logged in. */
ErrSecInvalidDigestAlgorithm = -67815, /* An invalid digest algorithm was detected. */
ErrSecInvalidCRLGroup = -67816, /* An invalid CRL group was detected. */
ErrSecCertificateCannotOperate = -67817, /* The certificate cannot operate. */
ErrSecCertificateExpired = -67818, /* An expired certificate was detected. */
ErrSecCertificateNotValidYet = -67819, /* The certificate is not yet valid. */
ErrSecCertificateRevoked = -67820, /* The certificate was revoked. */
ErrSecCertificateSuspended = -67821, /* The certificate was suspended. */
ErrSecInsufficientCredentials = -67822, /* Insufficient credentials were detected. */
ErrSecInvalidAction = -67823, /* The action was not valid. */
ErrSecInvalidAuthority = -67824, /* The authority was not valid. */
ErrSecVerifyActionFailed = -67825, /* A verify action has failed. */
ErrSecInvalidCertAuthority = -67826, /* The certificate authority was not valid. */
ErrSecInvaldCRLAuthority = -67827, /* The CRL authority was not valid. */
ErrSecInvalidCRLEncoding = -67828, /* The CRL encoding was not valid. */
ErrSecInvalidCRLType = -67829, /* The CRL type was not valid. */
ErrSecInvalidCRL = -67830, /* The CRL was not valid. */
ErrSecInvalidFormType = -67831, /* The form type was not valid. */
ErrSecInvalidID = -67832, /* The ID was not valid. */
ErrSecInvalidIdentifier = -67833, /* The identifier was not valid. */
ErrSecInvalidIndex = -67834, /* The index was not valid. */
ErrSecInvalidPolicyIdentifiers = -67835, /* The policy identifiers are not valid. */
ErrSecInvalidTimeString = -67836, /* The time specified was not valid. */
ErrSecInvalidReason = -67837, /* The trust policy reason was not valid. */
ErrSecInvalidRequestInputs = -67838, /* The request inputs are not valid. */
ErrSecInvalidResponseVector = -67839, /* The response vector was not valid. */
ErrSecInvalidStopOnPolicy = -67840, /* The stop-on policy was not valid. */
ErrSecInvalidTuple = -67841, /* The tuple was not valid. */
ErrSecMultipleValuesUnsupported = -67842, /* Multiple values are not supported. */
ErrSecNotTrusted = -67843, /* The trust policy was not trusted. */
ErrSecNoDefaultAuthority = -67844, /* No default authority was detected. */
ErrSecRejectedForm = -67845, /* The trust policy had a rejected form. */
ErrSecRequestLost = -67846, /* The request was lost. */
ErrSecRequestRejected = -67847, /* The request was rejected. */
ErrSecUnsupportedAddressType = -67848, /* The address type is not supported. */
ErrSecUnsupportedService = -67849, /* The service is not supported. */
ErrSecInvalidTupleGroup = -67850, /* The tuple group was not valid. */
ErrSecInvalidBaseACLs = -67851, /* The base ACLs are not valid. */
ErrSecInvalidTupleCredendtials = -67852, /* The tuple credentials are not valid. */
ErrSecInvalidEncoding = -67853, /* The encoding was not valid. */
ErrSecInvalidValidityPeriod = -67854, /* The validity period was not valid. */
ErrSecInvalidRequestor = -67855, /* The requestor was not valid. */
ErrSecRequestDescriptor = -67856, /* The request descriptor was not valid. */
ErrSecInvalidBundleInfo = -67857, /* The bundle information was not valid. */
ErrSecInvalidCRLIndex = -67858, /* The CRL index was not valid. */
ErrSecNoFieldValues = -67859, /* No field values were detected. */
ErrSecUnsupportedFieldFormat = -67860, /* The field format is not supported. */
ErrSecUnsupportedIndexInfo = -67861, /* The index information is not supported. */
ErrSecUnsupportedLocality = -67862, /* The locality is not supported. */
ErrSecUnsupportedNumAttributes = -67863, /* The number of attributes is not supported. */
ErrSecUnsupportedNumIndexes = -67864, /* The number of indexes is not supported. */
ErrSecUnsupportedNumRecordTypes = -67865, /* The number of record types is not supported. */
ErrSecFieldSpecifiedMultiple = -67866, /* Too many fields were specified. */
ErrSecIncompatibleFieldFormat = -67867, /* The field format was incompatible. */
ErrSecInvalidParsingModule = -67868, /* The parsing module was not valid. */
ErrSecDatabaseLocked = -67869, /* The database is locked. */
ErrSecDatastoreIsOpen = -67870, /* The data store is open. */
ErrSecMissingValue = -67871, /* A missing value was detected. */
ErrSecUnsupportedQueryLimits = -67872, /* The query limits are not supported. */
ErrSecUnsupportedNumSelectionPreds = -67873, /* The number of selection predicates is not supported. */
ErrSecUnsupportedOperator = -67874, /* The operator is not supported. */
ErrSecInvalidDBLocation = -67875, /* The database location is not valid. */
ErrSecInvalidAccessRequest = -67876, /* The access request is not valid. */
ErrSecInvalidIndexInfo = -67877, /* The index information is not valid. */
ErrSecInvalidNewOwner = -67878, /* The new owner is not valid. */
ErrSecInvalidModifyMode = -67879, /* The modify mode is not valid. */
ErrSecMissingRequiredExtension = -67880, /* A required certificate extension is missing. */
ErrSecExtendedKeyUsageNotCritical = -67881, /* The extended key usage extension was not marked critical. */
ErrSecTimestampMissing = -67882, /* A timestamp was expected but was not found. */
ErrSecTimestampInvalid = -67883, /* The timestamp was not valid. */
ErrSecTimestampNotTrusted = -67884, /* The timestamp was not trusted. */
ErrSecTimestampServiceNotAvailable = -67885, /* The timestamp service is not available. */
ErrSecTimestampBadAlg = -67886, /* An unrecognized or unsupported Algorithm Identifier in timestamp. */
ErrSecTimestampBadRequest = -67887, /* The timestamp transaction is not permitted or supported. */
ErrSecTimestampBadDataFormat = -67888, /* The timestamp data submitted has the wrong format. */
ErrSecTimestampTimeNotAvailable = -67889, /* The time source for the Timestamp Authority is not available. */
ErrSecTimestampUnacceptedPolicy = -67890, /* The requested policy is not supported by the Timestamp Authority. */
ErrSecTimestampUnacceptedExtension = -67891, /* The requested extension is not supported by the Timestamp Authority. */
ErrSecTimestampAddInfoNotAvailable = -67892, /* The additional information requested is not available. */
ErrSecTimestampSystemFailure = -67893, /* The timestamp request cannot be handled due to system failure. */
ErrSecSigningTimeMissing = -67894, /* A signing time was expected but was not found. */
ErrSecTimestampRejection = -67895, /* A timestamp transaction was rejected. */
ErrSecTimestampWaiting = -67896, /* A timestamp transaction is waiting. */
ErrSecTimestampRevocationWarning = -67897, /* A timestamp authority revocation warning was issued. */
ErrSecTimestampRevocationNotification = -67898, /* A timestamp authority revocation notification was issued. */
}
#endregion
}
}
}

View File

@@ -0,0 +1,158 @@
//
// 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.Runtime.InteropServices;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.OSX
{
/// <summary>
/// OSX implementation of the credential store
/// </summary>
internal class OSXCredentialStore : ICredentialStore
{
public bool DeletePassword(string credentialId)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
return DeletePasswordImpl(credentialId);
}
public bool TryGetPassword(string credentialId, out string password)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
return FindPassword(credentialId, out password);
}
public bool Save(Credential credential)
{
Credential.ValidateForSave(credential);
bool result = false;
// Note: OSX blocks AddPassword if the credential
// already exists, so for now we delete the password if already present since we're updating
// the value. In the future, we could consider updating but it's low value to solve this
DeletePasswordImpl(credential.CredentialId);
// Now add the password
result = AddGenericPassword(credential);
return result;
}
private bool AddGenericPassword(Credential credential)
{
IntPtr passwordPtr = Marshal.StringToCoTaskMemUni(credential.Password);
Interop.Security.OSStatus status = Interop.Security.SecKeychainAddGenericPassword(
IntPtr.Zero,
InteropUtils.GetLengthInBytes(credential.CredentialId),
credential.CredentialId,
0,
null,
InteropUtils.GetLengthInBytes(credential.Password),
passwordPtr,
IntPtr.Zero);
return status == Interop.Security.OSStatus.ErrSecSuccess;
}
/// <summary>
/// Finds the first password matching this credential
/// </summary>
private bool FindPassword(string credentialId, out string password)
{
password = null;
using (KeyChainItemHandle handle = LookupKeyChainItem(credentialId))
{
if( handle == null)
{
return false;
}
password = handle.Password;
}
return true;
}
private KeyChainItemHandle LookupKeyChainItem(string credentialId)
{
UInt32 passwordLength;
IntPtr passwordPtr;
IntPtr item;
Interop.Security.OSStatus status = Interop.Security.SecKeychainFindGenericPassword(
IntPtr.Zero,
InteropUtils.GetLengthInBytes(credentialId),
credentialId,
0,
null,
out passwordLength,
out passwordPtr,
out item);
if(status == Interop.Security.OSStatus.ErrSecSuccess)
{
return new KeyChainItemHandle(item, passwordPtr, passwordLength);
}
return null;
}
private bool DeletePasswordImpl(string credentialId)
{
// Find password, then Delete, then cleanup
using (KeyChainItemHandle handle = LookupKeyChainItem(credentialId))
{
if (handle == null)
{
return false;
}
Interop.Security.OSStatus status = Interop.Security.SecKeychainItemDelete(handle);
return status == Interop.Security.OSStatus.ErrSecSuccess;
}
}
private class KeyChainItemHandle : SafeCreateHandle
{
private IntPtr passwordPtr;
private int passwordLength;
public KeyChainItemHandle() : base()
{
}
public KeyChainItemHandle(IntPtr itemPtr) : this(itemPtr, IntPtr.Zero, 0)
{
}
public KeyChainItemHandle(IntPtr itemPtr, IntPtr passwordPtr, UInt32 passwordLength)
: base(itemPtr)
{
this.passwordPtr = passwordPtr;
this.passwordLength = (int) passwordLength;
}
public string Password
{
get {
if (IsInvalid)
{
return null;
}
return InteropUtils.CopyToString(passwordPtr, passwordLength);
}
}
protected override bool ReleaseHandle()
{
if (passwordPtr != IntPtr.Zero)
{
Interop.Security.SecKeychainItemFreeContent(IntPtr.Zero, passwordPtr);
}
base.ReleaseHandle();
return true;
}
}
}
}

View File

@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.InteropServices;
namespace Microsoft.SqlTools.ServiceLayer.Credentials
{
/// <summary>
/// This class is a wrapper around the Create pattern in OS X where
/// if a Create* function is called, the caller must also CFRelease
/// on the same pointer in order to correctly free the memory.
/// </summary>
[System.Security.SecurityCritical]
internal partial class SafeCreateHandle : SafeHandle
{
internal SafeCreateHandle() : base(IntPtr.Zero, true) { }
internal SafeCreateHandle(IntPtr ptr) : base(IntPtr.Zero, true)
{
this.SetHandle(ptr);
}
[System.Security.SecurityCritical]
protected override bool ReleaseHandle()
{
Interop.CoreFoundation.CFRelease(handle);
return true;
}
public override bool IsInvalid
{
[System.Security.SecurityCritical]
get
{
return handle == IntPtr.Zero;
}
}
}
}

View File

@@ -0,0 +1,42 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Runtime.InteropServices;
using System.Security;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
internal static class SecureStringHelper
{
// Methods
internal static SecureString CreateSecureString(string plainString)
{
SecureString str = new SecureString();
if (!string.IsNullOrEmpty(plainString))
{
foreach (char c in plainString)
{
str.AppendChar(c);
}
}
str.MakeReadOnly();
return str;
}
internal static string CreateString(SecureString value)
{
IntPtr ptr = SecureStringMarshal.SecureStringToGlobalAllocUnicode(value);
try
{
return Marshal.PtrToStringUni(ptr);
}
finally
{
Marshal.ZeroFreeGlobalAllocUnicode(ptr);
}
}
}
}

View File

@@ -0,0 +1,16 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
// TODO Replace this strings class with a resx file
internal class CredentialResources
{
public const string PasswordLengthExceeded = "The password has exceeded 512 bytes.";
public const string TargetRequiredForDelete = "Target must be specified to delete a credential.";
public const string TargetRequiredForLookup = "Target must be specified to check existance of a credential.";
public const string CredentialDisposed = "Win32Credential object is already disposed.";
}
}

View File

@@ -0,0 +1,113 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.SqlTools.EditorServices.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public class CredentialSet: List<Win32Credential>, IDisposable
{
bool _disposed;
public CredentialSet()
{
}
public CredentialSet(string target)
: this()
{
if (string.IsNullOrEmpty(target))
{
throw new ArgumentNullException("target");
}
Target = target;
}
public string Target { get; set; }
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
~CredentialSet()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (Count > 0)
{
ForEach(cred => cred.Dispose());
}
}
}
_disposed = true;
}
public CredentialSet Load()
{
LoadInternal();
return this;
}
private void LoadInternal()
{
uint count;
IntPtr pCredentials = IntPtr.Zero;
bool result = NativeMethods.CredEnumerateW(Target, 0, out count, out pCredentials);
if (!result)
{
Logger.Write(LogLevel.Error, string.Format("Win32Exception: {0}", new Win32Exception(Marshal.GetLastWin32Error()).ToString()));
return;
}
// Read in all of the pointers first
IntPtr[] ptrCredList = new IntPtr[count];
for (int i = 0; i < count; i++)
{
ptrCredList[i] = Marshal.ReadIntPtr(pCredentials, IntPtr.Size*i);
}
// Now let's go through all of the pointers in the list
// and create our Credential object(s)
List<NativeMethods.CriticalCredentialHandle> credentialHandles =
ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
IEnumerable<Win32Credential> existingCredentials = credentialHandles
.Select(handle => handle.GetCredential())
.Select(nativeCredential =>
{
Win32Credential credential = new Win32Credential();
credential.LoadInternal(nativeCredential);
return credential;
});
AddRange(existingCredentials);
// The individual credentials should not be free'd
credentialHandles.ForEach(handle => handle.SetHandleAsInvalid());
// Clean up memory to the Enumeration pointer
NativeMethods.CredFree(pCredentials);
}
}
}

View File

@@ -0,0 +1,16 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public enum CredentialType: uint
{
None = 0,
Generic = 1,
DomainPassword = 2,
DomainCertificate = 3,
DomainVisiblePassword = 4
}
}

View File

@@ -0,0 +1,109 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
internal class NativeMethods
{
[StructLayout(LayoutKind.Sequential)]
internal struct CREDENTIAL
{
public int Flags;
public int Type;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetName;
[MarshalAs(UnmanagedType.LPWStr)]
public string Comment;
public long LastWritten;
public int CredentialBlobSize;
public IntPtr CredentialBlob;
public int Persist;
public int AttributeCount;
public IntPtr Attributes;
[MarshalAs(UnmanagedType.LPWStr)]
public string TargetAlias;
[MarshalAs(UnmanagedType.LPWStr)]
public string UserName;
}
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
internal static extern bool CredFree([In] IntPtr cred);
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
internal static extern bool CredDelete(StringBuilder target, CredentialType type, int flags);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool CredEnumerateW(string filter, int flag, out uint count, out IntPtr pCredentials);
[DllImport("ole32.dll")]
internal static extern void CoTaskMemFree(IntPtr ptr);
internal abstract class CriticalHandleZeroOrMinusOneIsInvalid : CriticalHandle
{
protected CriticalHandleZeroOrMinusOneIsInvalid() : base(IntPtr.Zero)
{
}
public override bool IsInvalid
{
get { return handle == new IntPtr(0) || handle == new IntPtr(-1); }
}
}
internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
{
// Set the handle.
internal CriticalCredentialHandle(IntPtr preexistingHandle)
{
SetHandle(preexistingHandle);
}
internal CREDENTIAL GetCredential()
{
if (!IsInvalid)
{
// Get the Credential from the mem location
return (CREDENTIAL)Marshal.PtrToStructure<CREDENTIAL>(handle);
}
else
{
throw new InvalidOperationException("Invalid CriticalHandle!");
}
}
// Perform any specific actions to release the handle in the ReleaseHandle method.
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
override protected bool ReleaseHandle()
{
// If the handle was set, free it. Return success.
if (!IsInvalid)
{
// NOTE: We should also ZERO out the memory allocated to the handle, before free'ing it
// so there are no traces of the sensitive data left in memory.
CredFree(handle);
// Mark the handle as invalid for future users.
SetHandleAsInvalid();
return true;
}
// Return false.
return false;
}
}
}
}

View File

@@ -0,0 +1,14 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public enum PersistanceType : uint
{
Session = 1,
LocalComputer = 2,
Enterprise = 3
}
}

View File

@@ -0,0 +1,290 @@
//
// Code originally from http://credentialmanagement.codeplex.com/,
// Licensed under the Apache License 2.0
//
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
public class Win32Credential: IDisposable
{
bool disposed;
CredentialType type;
string target;
SecureString password;
string username;
string description;
DateTime lastWriteTime;
PersistanceType persistanceType;
public Win32Credential()
: this(null)
{
}
public Win32Credential(string username)
: this(username, null)
{
}
public Win32Credential(string username, string password)
: this(username, password, null)
{
}
public Win32Credential(string username, string password, string target)
: this(username, password, target, CredentialType.Generic)
{
}
public Win32Credential(string username, string password, string target, CredentialType type)
{
Username = username;
Password = password;
Target = target;
Type = type;
PersistanceType = PersistanceType.Session;
lastWriteTime = DateTime.MinValue;
}
public void Dispose()
{
Dispose(true);
// Prevent GC Collection since we have already disposed of this object
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
SecurePassword.Clear();
SecurePassword.Dispose();
}
}
disposed = true;
}
private void CheckNotDisposed()
{
if (disposed)
{
throw new ObjectDisposedException(CredentialResources.CredentialDisposed);
}
}
public string Username {
get
{
CheckNotDisposed();
return username;
}
set
{
CheckNotDisposed();
username = value;
}
}
public string Password
{
get
{
return SecureStringHelper.CreateString(SecurePassword);
}
set
{
CheckNotDisposed();
SecurePassword = SecureStringHelper.CreateSecureString(string.IsNullOrEmpty(value) ? string.Empty : value);
}
}
public SecureString SecurePassword
{
get
{
CheckNotDisposed();
return null == password ? new SecureString() : password.Copy();
}
set
{
CheckNotDisposed();
if (null != password)
{
password.Clear();
password.Dispose();
}
password = null == value ? new SecureString() : value.Copy();
}
}
public string Target
{
get
{
CheckNotDisposed();
return target;
}
set
{
CheckNotDisposed();
target = value;
}
}
public string Description
{
get
{
CheckNotDisposed();
return description;
}
set
{
CheckNotDisposed();
description = value;
}
}
public DateTime LastWriteTime
{
get
{
return LastWriteTimeUtc.ToLocalTime();
}
}
public DateTime LastWriteTimeUtc
{
get
{
CheckNotDisposed();
return lastWriteTime;
}
private set { lastWriteTime = value; }
}
public CredentialType Type
{
get
{
CheckNotDisposed();
return type;
}
set
{
CheckNotDisposed();
type = value;
}
}
public PersistanceType PersistanceType
{
get
{
CheckNotDisposed();
return persistanceType;
}
set
{
CheckNotDisposed();
persistanceType = value;
}
}
public bool Save()
{
CheckNotDisposed();
byte[] passwordBytes = Encoding.Unicode.GetBytes(Password);
if (Password.Length > (512))
{
throw new ArgumentOutOfRangeException(CredentialResources.PasswordLengthExceeded);
}
NativeMethods.CREDENTIAL credential = new NativeMethods.CREDENTIAL();
credential.TargetName = Target;
credential.UserName = Username;
credential.CredentialBlob = Marshal.StringToCoTaskMemUni(Password);
credential.CredentialBlobSize = passwordBytes.Length;
credential.Comment = Description;
credential.Type = (int)Type;
credential.Persist = (int) PersistanceType;
bool result = NativeMethods.CredWrite(ref credential, 0);
if (!result)
{
return false;
}
LastWriteTimeUtc = DateTime.UtcNow;
return true;
}
public bool Delete()
{
CheckNotDisposed();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException(CredentialResources.TargetRequiredForDelete);
}
StringBuilder target = string.IsNullOrEmpty(Target) ? new StringBuilder() : new StringBuilder(Target);
bool result = NativeMethods.CredDelete(target, Type, 0);
return result;
}
public bool Load()
{
CheckNotDisposed();
IntPtr credPointer;
bool result = NativeMethods.CredRead(Target, Type, 0, out credPointer);
if (!result)
{
return false;
}
using (NativeMethods.CriticalCredentialHandle credentialHandle = new NativeMethods.CriticalCredentialHandle(credPointer))
{
LoadInternal(credentialHandle.GetCredential());
}
return true;
}
public bool Exists()
{
CheckNotDisposed();
if (string.IsNullOrEmpty(Target))
{
throw new InvalidOperationException(CredentialResources.TargetRequiredForLookup);
}
using (Win32Credential existing = new Win32Credential { Target = Target, Type = Type })
{
return existing.Load();
}
}
internal void LoadInternal(NativeMethods.CREDENTIAL credential)
{
Username = credential.UserName;
if (credential.CredentialBlobSize > 0)
{
Password = Marshal.PtrToStringUni(credential.CredentialBlob, credential.CredentialBlobSize / 2);
}
Target = credential.TargetName;
Type = (CredentialType)credential.Type;
PersistanceType = (PersistanceType)credential.Persist;
Description = credential.Comment;
LastWriteTimeUtc = DateTime.FromFileTimeUtc(credential.LastWritten);
}
}
}

View File

@@ -0,0 +1,63 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
{
/// <summary>
/// Win32 implementation of the credential store
/// </summary>
internal class Win32CredentialStore : ICredentialStore
{
private const string AnyUsername = "*";
public bool DeletePassword(string credentialId)
{
using (Win32Credential cred = new Win32Credential() { Target = credentialId, Username = AnyUsername })
{
return cred.Delete();
}
}
public bool TryGetPassword(string credentialId, out string password)
{
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
password = null;
using (CredentialSet set = new CredentialSet(credentialId).Load())
{
// Note: Credentials are disposed on disposal of the set
Win32Credential foundCred = null;
if (set.Count > 0)
{
foundCred = set[0];
}
if (foundCred != null)
{
password = foundCred.Password;
return true;
}
return false;
}
}
public bool Save(Credential credential)
{
Credential.ValidateForSave(credential);
using (Win32Credential cred =
new Win32Credential(AnyUsername, credential.Password, credential.CredentialId, CredentialType.Generic)
{ PersistanceType = PersistanceType.LocalComputer })
{
return cred.Save();
}
}
}
}

View File

@@ -4,7 +4,7 @@
//
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
/// <summary>
/// Defines a class that describes the capabilities of a language

View File

@@ -0,0 +1,28 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
/// <summary>
/// Parameters to be used for reporting hosting-level errors, such as protocol violations
/// </summary>
public class HostingErrorParams
{
/// <summary>
/// The message of the error
/// </summary>
public string Message { get; set; }
}
public class HostingErrorEvent
{
public static readonly
EventType<HostingErrorParams> Type =
EventType<HostingErrorParams>.Create("hosting/error");
}
}

View File

@@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
public class InitializeRequest
{

View File

@@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
public class ServerCapabilities
{

View File

@@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
/// <summary>
/// Defines a message that is sent from the client to request

View File

@@ -0,0 +1,20 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
/// <summary>
/// Defines a message that is sent from the client to request
/// the version of the server.
/// </summary>
public class VersionRequest
{
public static readonly
RequestType<object, string> Type =
RequestType<object, string>.Create("version");
}
}

View File

@@ -3,10 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
{
/// <summary>
/// Defines a base implementation for servers and their clients over a

View File

@@ -7,8 +7,9 @@ using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
{
/// <summary>
/// Provides a client implementation for the standard I/O channel.

View File

@@ -6,8 +6,9 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
{
/// <summary>
/// Provides a server implementation for the standard I/O channel.

View File

@@ -6,7 +6,7 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public static class Constants
{

View File

@@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
{
/// <summary>
/// Defines an event type with a particular method name.

View File

@@ -6,7 +6,7 @@
using System.Diagnostics;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
{
/// <summary>
/// Defines all possible message types.

View File

@@ -5,7 +5,7 @@
using System.Diagnostics;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
{
[DebuggerDisplay("RequestType MethodName = {MethodName}")]
public class RequestType<TParams, TResult>

View File

@@ -4,8 +4,9 @@
//
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
/// <summary>
/// Provides context for a received event so that handlers

View File

@@ -4,10 +4,11 @@
//
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
internal interface IMessageSender
public interface IMessageSender
{
Task SendEvent<TParams>(
EventType<TParams> eventType,

View File

@@ -0,0 +1,32 @@
//
// 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.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
/// <summary>
/// A ProtocolEndpoint is used for inter-process communication. Services can register to
/// respond to requests and events, send their own requests, and listen for notifications
/// sent by the other side of the endpoint
/// </summary>
public interface IProtocolEndpoint : IMessageSender
{
void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler);
void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler);
void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler,
bool overrideExisting);
}
}

View File

@@ -3,15 +3,17 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
using Microsoft.SqlTools.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.EditorServices.Utility;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public class MessageDispatcher
{
@@ -197,10 +199,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
this.SynchronizationContext = SynchronizationContext.Current;
// Run the message loop
bool isRunning = true;
while (isRunning && !cancellationToken.IsCancellationRequested)
while (!cancellationToken.IsCancellationRequested)
{
Message newMessage = null;
Message newMessage;
try
{
@@ -209,12 +210,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
}
catch (MessageParseException e)
{
// TODO: Write an error response
Logger.Write(
LogLevel.Error,
"Could not parse a message that was received:\r\n\r\n" +
e.ToString());
string message = string.Format("Exception occurred while parsing message: {0}", e.Message);
Logger.Write(LogLevel.Error, message);
await MessageWriter.WriteEvent(HostingErrorEvent.Type, new HostingErrorParams
{
Message = message
});
// Continue the loop
continue;
@@ -226,18 +227,29 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
}
catch (Exception e)
{
var b = e.Message;
newMessage = null;
// Log the error and send an error event to the client
string message = string.Format("Exception occurred while receiving message: {0}", e.Message);
Logger.Write(LogLevel.Error, message);
await MessageWriter.WriteEvent(HostingErrorEvent.Type, new HostingErrorParams
{
Message = message
});
// Continue the loop
continue;
}
// The message could be null if there was an error parsing the
// previous message. In this case, do not try to dispatch it.
if (newMessage != null)
{
// Verbose logging
string logMessage = string.Format("Received message of type[{0}] and method[{1}]",
newMessage.MessageType, newMessage.Method);
Logger.Write(LogLevel.Verbose, logMessage);
// Process the message
await this.DispatchMessage(
newMessage,
this.MessageWriter);
await this.DispatchMessage(newMessage, this.MessageWriter);
}
}
}

View File

@@ -5,7 +5,7 @@
using System;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public class MessageParseException : Exception
{

View File

@@ -3,7 +3,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
/// <summary>
/// Defines the possible message protocol types.

View File

@@ -3,16 +3,17 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public class MessageReader
{
@@ -23,22 +24,22 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
private const int CR = 0x0D;
private const int LF = 0x0A;
private static string[] NewLineDelimiters = new string[] { Environment.NewLine };
private static readonly string[] NewLineDelimiters = { Environment.NewLine };
private Stream inputStream;
private IMessageSerializer messageSerializer;
private Encoding messageEncoding;
private readonly Stream inputStream;
private readonly IMessageSerializer messageSerializer;
private readonly Encoding messageEncoding;
private ReadState readState;
private bool needsMoreData = true;
private int readOffset;
private int bufferEndOffset;
private byte[] messageBuffer = new byte[DefaultBufferSize];
private byte[] messageBuffer;
private int expectedContentLength;
private Dictionary<string, string> messageHeaders;
enum ReadState
private enum ReadState
{
Headers,
Content
@@ -83,7 +84,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
this.needsMoreData = false;
// Do we need to look for message headers?
if (this.readState == ReadState.Headers &&
if (this.readState == ReadState.Headers &&
!this.TryReadMessageHeaders())
{
// If we don't have enough data to read headers yet, keep reading
@@ -92,7 +93,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
}
// Do we need to look for message content?
if (this.readState == ReadState.Content &&
if (this.readState == ReadState.Content &&
!this.TryReadMessageContent(out messageContent))
{
// If we don't have enough data yet to construct the content, keep reading
@@ -104,16 +105,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
break;
}
// Now that we have a message, reset the buffer's state
ShiftBufferBytesAndShrink(readOffset);
// Get the JObject for the JSON content
JObject messageObject = JObject.Parse(messageContent);
// Load the message
Logger.Write(
LogLevel.Verbose,
string.Format(
"READ MESSAGE:\r\n\r\n{0}",
messageObject.ToString(Formatting.Indented)));
// Return the parsed message
return this.messageSerializer.DeserializeMessage(messageObject);
}
@@ -160,8 +157,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
{
int scanOffset = this.readOffset;
// Scan for the final double-newline that marks the
// end of the header lines
// Scan for the final double-newline that marks the end of the header lines
while (scanOffset + 3 < this.bufferEndOffset &&
(this.messageBuffer[scanOffset] != CR ||
this.messageBuffer[scanOffset + 1] != LF ||
@@ -171,45 +167,51 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
scanOffset++;
}
// No header or body separator found (e.g CRLFCRLF)
// Make sure we haven't reached the end of the buffer without finding a separator (e.g CRLFCRLF)
if (scanOffset + 3 >= this.bufferEndOffset)
{
return false;
}
this.messageHeaders = new Dictionary<string, string>();
// Convert the header block into a array of lines
var headers = Encoding.ASCII.GetString(this.messageBuffer, this.readOffset, scanOffset)
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
var headers =
Encoding.ASCII
.GetString(this.messageBuffer, this.readOffset, scanOffset)
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
// Read each header and store it in the dictionary
foreach (var header in headers)
try
{
int currentLength = header.IndexOf(':');
if (currentLength == -1)
// Read each header and store it in the dictionary
this.messageHeaders = new Dictionary<string, string>();
foreach (var header in headers)
{
throw new ArgumentException("Message header must separate key and value using :");
int currentLength = header.IndexOf(':');
if (currentLength == -1)
{
throw new ArgumentException("Message header must separate key and value using :");
}
var key = header.Substring(0, currentLength);
var value = header.Substring(currentLength + 1).Trim();
this.messageHeaders[key] = value;
}
var key = header.Substring(0, currentLength);
var value = header.Substring(currentLength + 1).Trim();
this.messageHeaders[key] = value;
}
// Parse out the content length as an int
string contentLengthString;
if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
{
throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
}
// Make sure a Content-Length header was present, otherwise it
// is a fatal error
string contentLengthString = null;
if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
{
throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
// Parse the content length to an integer
if (!int.TryParse(contentLengthString, out this.expectedContentLength))
{
throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
}
}
// Parse the content length to an integer
if (!int.TryParse(contentLengthString, out this.expectedContentLength))
catch (Exception)
{
throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
// The content length was invalid or missing. Trash the buffer we've read
ShiftBufferBytesAndShrink(scanOffset + 4);
throw;
}
// Skip past the headers plus the newline characters
@@ -232,31 +234,40 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
}
// Convert the message contents to a string using the specified encoding
messageContent =
this.messageEncoding.GetString(
this.messageBuffer,
this.readOffset,
this.expectedContentLength);
messageContent = this.messageEncoding.GetString(
this.messageBuffer,
this.readOffset,
this.expectedContentLength);
// Move the remaining bytes to the front of the buffer for the next message
var remainingByteCount = this.bufferEndOffset - (this.expectedContentLength + this.readOffset);
Buffer.BlockCopy(
this.messageBuffer,
this.expectedContentLength + this.readOffset,
this.messageBuffer,
0,
remainingByteCount);
readOffset += expectedContentLength;
// Reset the offsets for the next read
this.readOffset = 0;
this.bufferEndOffset = remainingByteCount;
// Done reading content, now look for headers
// Done reading content, now look for headers for the next message
this.readState = ReadState.Headers;
return true;
}
private void ShiftBufferBytesAndShrink(int bytesToRemove)
{
// Create a new buffer that is shrunken by the number of bytes to remove
// Note: by using Max, we can guarantee a buffer of at least default buffer size
byte[] newBuffer = new byte[Math.Max(messageBuffer.Length - bytesToRemove, DefaultBufferSize)];
// If we need to do shifting, do the shifting
if (bytesToRemove <= messageBuffer.Length)
{
// Copy the existing buffer starting at the offset to remove
Buffer.BlockCopy(messageBuffer, bytesToRemove, newBuffer, 0, bufferEndOffset - bytesToRemove);
}
// Make the new buffer the message buffer
messageBuffer = newBuffer;
// Reset the read offset and the end offset
readOffset = 0;
bufferEndOffset -= bytesToRemove;
}
#endregion
}
}

View File

@@ -3,14 +3,16 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public class MessageWriter
{

View File

@@ -3,19 +3,20 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
/// <summary>
/// Provides behavior for a client or server endpoint that
/// communicates using the specified protocol.
/// </summary>
public class ProtocolEndpoint : IMessageSender
public class ProtocolEndpoint : IMessageSender, IProtocolEndpoint
{
private bool isStarted;
private int currentMessageId;

View File

@@ -3,10 +3,11 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
{
public class RequestContext<TResult>
{
@@ -19,7 +20,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
this.messageWriter = messageWriter;
}
public async Task SendResult(TResult resultDetails)
public RequestContext() { }
public virtual async Task SendResult(TResult resultDetails)
{
await this.messageWriter.WriteResponse<TResult>(
resultDetails,
@@ -27,14 +30,14 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
requestMessage.Id);
}
public async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
public virtual async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
{
await this.messageWriter.WriteEvent(
eventType,
eventParams);
}
public async Task SendError(object errorDetails)
public virtual async Task SendError(object errorDetails)
{
await this.messageWriter.WriteMessage(
Message.ResponseError(

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
{
/// <summary>
/// Defines a common interface for message serializers.

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Newtonsoft.Json.Linq;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
{
/// <summary>
/// Serializes messages in the JSON RPC format. Used primarily

View File

@@ -5,8 +5,9 @@
using Newtonsoft.Json.Linq;
using System;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
{
/// <summary>
/// Serializes messages in the V8 format. Used primarily for debug adapters.

View File

@@ -0,0 +1,168 @@
//
// 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.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
using System.Reflection;
namespace Microsoft.SqlTools.ServiceLayer.Hosting
{
/// <summary>
/// SQL Tools VS Code Language Server request handler. Provides the entire JSON RPC
/// implementation for sending/receiving JSON requests and dispatching the requests to
/// handlers that are registered prior to startup.
/// </summary>
public sealed class ServiceHost : ServiceHostBase
{
#region Singleton Instance Code
/// <summary>
/// Singleton instance of the service host for internal storage
/// </summary>
private static readonly Lazy<ServiceHost> instance = new Lazy<ServiceHost>(() => new ServiceHost());
/// <summary>
/// Current instance of the ServiceHost
/// </summary>
public static ServiceHost Instance
{
get { return instance.Value; }
}
/// <summary>
/// Constructs new instance of ServiceHost using the host and profile details provided.
/// Access is private to ensure only one instance exists at a time.
/// </summary>
private ServiceHost() : base(new StdioServerChannel())
{
// Initialize the shutdown activities
shutdownCallbacks = new List<ShutdownCallback>();
initializeCallbacks = new List<InitializeCallback>();
}
/// <summary>
/// Provide initialization that must occur after the service host is started
/// </summary>
public void Initialize()
{
// 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, HandleVersionRequest);
}
#endregion
#region Member Variables
public delegate Task ShutdownCallback(object shutdownParams, RequestContext<object> shutdownRequestContext);
public delegate Task InitializeCallback(InitializeRequest startupParams, RequestContext<InitializeResult> requestContext);
private readonly List<ShutdownCallback> shutdownCallbacks;
private readonly List<InitializeCallback> initializeCallbacks;
private static readonly Version serviceVersion = Assembly.GetEntryAssembly().GetName().Version;
#endregion
#region Public Methods
/// <summary>
/// Adds a new callback to be called when the shutdown request is submitted
/// </summary>
/// <param name="callback">Callback to perform when a shutdown request is submitted</param>
public void RegisterShutdownTask(ShutdownCallback callback)
{
shutdownCallbacks.Add(callback);
}
/// <summary>
/// Add a new method to be called when the initialize request is submitted
/// </summary>
/// <param name="callback">Callback to perform when an initialize request is submitted</param>
public void RegisterInitializeTask(InitializeCallback callback)
{
initializeCallbacks.Add(callback);
}
#endregion
#region Request Handlers
/// <summary>
/// Handles the shutdown event for the Language Server
/// </summary>
private async Task HandleShutdownRequest(object shutdownParams, RequestContext<object> requestContext)
{
Logger.Write(LogLevel.Normal, "Service host is shutting down...");
// Call all the shutdown methods provided by the service components
Task[] shutdownTasks = shutdownCallbacks.Select(t => t(shutdownParams, requestContext)).ToArray();
await Task.WhenAll(shutdownTasks);
}
/// <summary>
/// Handles the initialization request
/// </summary>
/// <param name="initializeParams"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private async Task HandleInitializeRequest(InitializeRequest initializeParams, RequestContext<InitializeResult> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleInitializationRequest");
// Call all tasks that registered on the initialize request
var initializeTasks = initializeCallbacks.Select(t => t(initializeParams, requestContext));
await Task.WhenAll(initializeTasks);
// TODO: Figure out where this needs to go to be agnostic of the language
// Send back what this server can do
await requestContext.SendResult(
new InitializeResult
{
Capabilities = new ServerCapabilities
{
TextDocumentSync = TextDocumentSyncKind.Incremental,
DefinitionProvider = true,
ReferencesProvider = true,
DocumentHighlightProvider = true,
DocumentSymbolProvider = true,
WorkspaceSymbolProvider = true,
CompletionProvider = new CompletionOptions
{
ResolveProvider = true,
TriggerCharacters = new string[] { ".", "-", ":", "\\" }
},
SignatureHelpProvider = new SignatureHelpOptions
{
TriggerCharacters = new string[] { " " } // TODO: Other characters here?
}
}
});
}
/// <summary>
/// Handles the version request. Sends back the server version as result.
/// </summary>
private static async Task HandleVersionRequest(
object versionRequestParams,
RequestContext<string> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleVersionRequest");
await requestContext.SendResult(serviceVersion.ToString());
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Threading.Tasks;
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
namespace Microsoft.SqlTools.ServiceLayer.Hosting
{
public abstract class ServiceHostBase : ProtocolEndpoint
{
private bool isStarted;
private TaskCompletionSource<bool> serverExitedTask;
protected ServiceHostBase(ChannelBase serverChannel) :
base(serverChannel, MessageProtocolType.LanguageServer)
{
}
protected override Task OnStart()
{
// Register handlers for server lifetime messages
this.SetEventHandler(ExitNotification.Type, this.HandleExitNotification);
return Task.FromResult(true);
}
private async Task HandleExitNotification(
object exitParams,
EventContext eventContext)
{
// Stop the server channel
await this.Stop();
// Notify any waiter that the server has exited
if (this.serverExitedTask != null)
{
this.serverExitedTask.SetResult(true);
}
}
}
}

View File

@@ -0,0 +1,323 @@
//
// 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.Data.SqlClient;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SmoMetadataProvider;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for Autocomplete functionality
/// </summary>
public class AutoCompleteService
{
#region Singleton Instance Implementation
/// <summary>
/// Singleton service instance
/// </summary>
private static Lazy<AutoCompleteService> instance
= new Lazy<AutoCompleteService>(() => new AutoCompleteService());
/// <summary>
/// Gets the singleton service instance
/// </summary>
public static AutoCompleteService Instance
{
get
{
return instance.Value;
}
}
/// <summary>
/// Default, parameterless constructor.
/// Internal constructor for use in test cases only
/// </summary>
internal AutoCompleteService()
{
}
#endregion
private ConnectionService connectionService = null;
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal ConnectionService ConnectionServiceInstance
{
get
{
if(connectionService == null)
{
connectionService = ConnectionService.Instance;
}
return connectionService;
}
set
{
connectionService = value;
}
}
public void InitializeService(ServiceHost serviceHost)
{
// Register auto-complete request handler
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
// Register a callback for when a connection is created
ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
// Register a callback for when a connection is closed
ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
}
/// <summary>
/// Auto-complete completion provider request callback
/// </summary>
/// <param name="textDocumentPosition"></param>
/// <param name="requestContext"></param>
/// <returns></returns>
private static async Task HandleCompletionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<CompletionItem[]> requestContext)
{
// get the current list of completion items and return to client
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(
textDocumentPosition.TextDocument.Uri);
ConnectionInfo connInfo;
ConnectionService.Instance.TryFindConnection(
scriptFile.ClientFilePath,
out connInfo);
var completionItems = Instance.GetCompletionItems(
textDocumentPosition, scriptFile, connInfo);
await requestContext.SendResult(completionItems);
}
/// <summary>
/// Remove a reference to an autocomplete cache from a URI. If
/// it is the last URI connected to a particular connection,
/// then remove the cache.
/// </summary>
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
{
// currently this method is disabled, but we need to reimplement now that the
// implementation of the 'cache' has changed.
await Task.FromResult(0);
}
/// <summary>
/// Update the cached autocomplete candidate list when the user connects to a database
/// </summary>
/// <param name="info"></param>
public async Task UpdateAutoCompleteCache(ConnectionInfo info)
{
await Task.Run( () =>
{
if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(info.OwnerUri))
{
var sqlConn = info.SqlConnection as SqlConnection;
if (sqlConn != null)
{
var srvConn = new ServerConnection(sqlConn);
var displayInfoProvider = new MetadataDisplayInfoProvider();
var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
var binder = BinderProvider.CreateBinder(metadataProvider);
LanguageService.Instance.ScriptParseInfoMap.Add(info.OwnerUri,
new ScriptParseInfo()
{
Binder = binder,
MetadataProvider = metadataProvider,
MetadataDisplayInfoProvider = displayInfoProvider
});
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
LanguageService.Instance.ParseAndBind(scriptFile, info);
}
}
});
}
/// <summary>
/// Find the position of the previous delimeter for autocomplete token replacement.
/// SQL Parser may have similar functionality in which case we'll delete this method.
/// </summary>
/// <param name="sql"></param>
/// <param name="startRow"></param>
/// <param name="startColumn"></param>
/// <returns></returns>
private int PositionOfPrevDelimeter(string sql, int startRow, int startColumn)
{
if (string.IsNullOrWhiteSpace(sql))
{
return 1;
}
int prevLineColumns = 0;
for (int i = 0; i < startRow; ++i)
{
while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length)
{
++prevLineColumns;
}
++prevLineColumns;
}
startColumn += prevLineColumns;
if (startColumn - 1 < sql.Length)
{
while (--startColumn >= prevLineColumns)
{
if (sql[startColumn] == ' '
|| sql[startColumn] == '\t'
|| sql[startColumn] == '\n'
|| sql[startColumn] == '.'
|| sql[startColumn] == '+'
|| sql[startColumn] == '-'
|| sql[startColumn] == '*'
|| sql[startColumn] == '>'
|| sql[startColumn] == '<'
|| sql[startColumn] == '='
|| sql[startColumn] == '/'
|| sql[startColumn] == '%')
{
break;
}
}
}
return startColumn + 1 - prevLineColumns;
}
/// <summary>
/// Determines whether a reparse and bind is required to provide autocomplete
/// </summary>
/// <param name="info"></param>
/// <returns>TEMP: Currently hard-coded to false for perf</returns>
private bool RequiresReparse(ScriptParseInfo info)
{
return false;
}
/// <summary>
/// Converts a list of Declaration objects to CompletionItem objects
/// since VS Code expects CompletionItems but SQL Parser works with Declarations
/// </summary>
/// <param name="suggestions"></param>
/// <param name="cursorRow"></param>
/// <param name="cursorColumn"></param>
/// <returns></returns>
private CompletionItem[] ConvertDeclarationsToCompletionItems(
IEnumerable<Declaration> suggestions,
int row,
int startColumn,
int endColumn)
{
List<CompletionItem> completions = new List<CompletionItem>();
foreach (var autoCompleteItem in suggestions)
{
// convert the completion item candidates into CompletionItems
completions.Add(new CompletionItem()
{
Label = autoCompleteItem.Title,
Kind = CompletionItemKind.Keyword,
Detail = autoCompleteItem.Title,
Documentation = autoCompleteItem.Description,
TextEdit = new TextEdit
{
NewText = autoCompleteItem.Title,
Range = new Range
{
Start = new Position
{
Line = row,
Character = startColumn
},
End = new Position
{
Line = row,
Character = endColumn
}
}
}
});
}
return completions.ToArray();
}
/// <summary>
/// Return the completion item list for the current text position.
/// This method does not await cache builds since it expects to return quickly
/// </summary>
/// <param name="textDocumentPosition"></param>
public CompletionItem[] GetCompletionItems(
TextDocumentPosition textDocumentPosition,
ScriptFile scriptFile,
ConnectionInfo connInfo)
{
string filePath = textDocumentPosition.TextDocument.Uri;
// Take a reference to the list at a point in time in case we update and replace the list
if (connInfo == null
|| !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri))
{
return new CompletionItem[0];
}
// reparse and bind the SQL statement if needed
var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri];
if (RequiresReparse(scriptParseInfo))
{
LanguageService.Instance.ParseAndBind(scriptFile, connInfo);
}
if (scriptParseInfo.ParseResult == null)
{
return new CompletionItem[0];
}
// get the completion list from SQL Parser
var suggestions = Resolver.FindCompletions(
scriptParseInfo.ParseResult,
textDocumentPosition.Position.Line + 1,
textDocumentPosition.Position.Character + 1,
scriptParseInfo.MetadataDisplayInfoProvider);
// convert the suggestion list to the VS Code format
return ConvertDeclarationsToCompletionItems(
suggestions,
textDocumentPosition.Position.Line,
PositionOfPrevDelimeter(
scriptFile.Contents,
textDocumentPosition.Position.Line,
textDocumentPosition.Position.Character),
textDocumentPosition.Position.Character);
}
}
}

View File

@@ -4,9 +4,10 @@
//
using System.Diagnostics;
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class CompletionRequest
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class DefinitionRequest
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class PublishDiagnosticsNotification
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public enum DocumentHighlightKind
{

View File

@@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ExpandAliasRequest
{

View File

@@ -3,10 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using System.Collections.Generic;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class FindModuleRequest
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class MarkedString
{

View File

@@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
class InstallModuleRequest
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ReferencesRequest
{

View File

@@ -3,9 +3,9 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class ShowOnlineHelpRequest
{

View File

@@ -3,9 +3,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
{
public class SignatureHelpRequest
{

View File

@@ -0,0 +1,538 @@
//
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.EditorServices.Utility;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
using System.Linq;
using Microsoft.SqlServer.Management.SqlParser.Parser;
using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlServer.Management.SqlParser.Binder;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.SqlParser;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
/// <summary>
/// Main class for Language Service functionality including anything that reqires knowledge of
/// the language to perfom, such as definitions, intellisense, etc.
/// </summary>
public sealed class LanguageService
{
#region Singleton Instance Implementation
private static readonly Lazy<LanguageService> instance = new Lazy<LanguageService>(() => new LanguageService());
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>());
internal Dictionary<string, ScriptParseInfo> ScriptParseInfoMap
{
get
{
return this.scriptParseInfoMap.Value;
}
}
public static LanguageService Instance
{
get { return instance.Value; }
}
/// <summary>
/// Default, parameterless constructor.
/// </summary>
internal LanguageService()
{
}
#endregion
#region Properties
private static CancellationTokenSource ExistingRequestCancellation { get; set; }
private SqlToolsSettings CurrentSettings
{
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
}
private Workspace.Workspace CurrentWorkspace
{
get { return WorkspaceService<SqlToolsSettings>.Instance.Workspace; }
}
/// <summary>
/// Gets or sets the current SQL Tools context
/// </summary>
/// <returns></returns>
private SqlToolsContext Context { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Initializes the Language Service instance
/// </summary>
/// <param name="serviceHost"></param>
/// <param name="context"></param>
public void InitializeService(ServiceHost serviceHost, SqlToolsContext context)
{
// Register the requests that this service will handle
serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
serviceHost.SetRequestHandler(DocumentSymbolRequest.Type, HandleDocumentSymbolRequest);
serviceHost.SetRequestHandler(WorkspaceSymbolRequest.Type, HandleWorkspaceSymbolRequest);
// Register a no-op shutdown task for validation of the shutdown logic
serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) =>
{
Logger.Write(LogLevel.Verbose, "Shutting down language service");
await Task.FromResult(0);
});
// Register the configuration update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
// Register the file change update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification);
// Register the file open update handler
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
// Store the SqlToolsContext for future use
Context = context;
}
/// <summary>
/// Parses the SQL text and binds it to the SMO metadata provider if connected
/// </summary>
/// <param name="filePath"></param>
/// <param name="sqlText"></param>
/// <returns></returns>
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
{
ScriptParseInfo parseInfo = null;
if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath))
{
parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath];
}
// parse current SQL file contents to retrieve a list of errors
ParseOptions parseOptions = new ParseOptions();
ParseResult parseResult = Parser.IncrementalParse(
scriptFile.Contents,
parseInfo != null ? parseInfo.ParseResult : null,
parseOptions);
// save previous result for next incremental parse
if (parseInfo != null)
{
parseInfo.ParseResult = parseResult;
}
if (connInfo != null)
{
try
{
List<ParseResult> parseResults = new List<ParseResult>();
parseResults.Add(parseResult);
parseInfo.Binder.Bind(
parseResults,
connInfo.ConnectionDetails.DatabaseName,
BindMode.Batch);
}
catch (ConnectionException)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
catch (SqlParserInternalBinderError)
{
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
}
}
return parseResult;
}
/// <summary>
/// Gets a list of semantic diagnostic marks for the provided script file
/// </summary>
/// <param name="scriptFile"></param>
public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile)
{
ConnectionInfo connInfo;
ConnectionService.Instance.TryFindConnection(
scriptFile.ClientFilePath,
out connInfo);
var parseResult = ParseAndBind(scriptFile, connInfo);
// build a list of SQL script file markers from the errors
List<ScriptFileMarker> markers = new List<ScriptFileMarker>();
foreach (var error in parseResult.Errors)
{
markers.Add(new ScriptFileMarker()
{
Message = error.Message,
Level = ScriptFileMarkerLevel.Error,
ScriptRegion = new ScriptRegion()
{
File = scriptFile.FilePath,
StartLineNumber = error.Start.LineNumber,
StartColumnNumber = error.Start.ColumnNumber,
StartOffset = 0,
EndLineNumber = error.End.LineNumber,
EndColumnNumber = error.End.ColumnNumber,
EndOffset = 0
}
});
}
return markers.ToArray();
}
#endregion
#region Request Handlers
private static async Task HandleDefinitionRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<Location[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest");
await Task.FromResult(true);
}
private static async Task HandleReferencesRequest(
ReferencesParams referencesParams,
RequestContext<Location[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleReferencesRequest");
await Task.FromResult(true);
}
private static async Task HandleCompletionResolveRequest(
CompletionItem completionItem,
RequestContext<CompletionItem> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
await Task.FromResult(true);
}
private static async Task HandleSignatureHelpRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<SignatureHelp> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentHighlightRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<DocumentHighlight[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest");
await Task.FromResult(true);
}
private static async Task HandleHoverRequest(
TextDocumentPosition textDocumentPosition,
RequestContext<Hover> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleHoverRequest");
await Task.FromResult(true);
}
private static async Task HandleDocumentSymbolRequest(
DocumentSymbolParams documentSymbolParams,
RequestContext<SymbolInformation[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
await Task.FromResult(true);
}
private static async Task HandleWorkspaceSymbolRequest(
WorkspaceSymbolParams workspaceSymbolParams,
RequestContext<SymbolInformation[]> requestContext)
{
Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest");
await Task.FromResult(true);
}
#endregion
#region Handlers for Events from Other Services
/// <summary>
/// Handle the file open notification
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
public async Task HandleDidOpenTextDocumentNotification(
ScriptFile scriptFile,
EventContext eventContext)
{
await this.RunScriptDiagnostics(
new ScriptFile[] { scriptFile },
eventContext);
await Task.FromResult(true);
}
/// <summary>
/// Handles text document change events
/// </summary>
/// <param name="textChangeParams"></param>
/// <param name="eventContext"></param>
/// <returns></returns>
public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext)
{
await this.RunScriptDiagnostics(
changedFiles.ToArray(),
eventContext);
await Task.FromResult(true);
}
/// <summary>
/// Handle the file configuration change notification
/// </summary>
/// <param name="newSettings"></param>
/// <param name="oldSettings"></param>
/// <param name="eventContext"></param>
public async Task HandleDidChangeConfigurationNotification(
SqlToolsSettings newSettings,
SqlToolsSettings oldSettings,
EventContext eventContext)
{
// If script analysis settings have changed we need to clear & possibly update the current diagnostic records.
bool oldScriptAnalysisEnabled = oldSettings.ScriptAnalysis.Enable.HasValue;
if ((oldScriptAnalysisEnabled != newSettings.ScriptAnalysis.Enable))
{
// If the user just turned off script analysis or changed the settings path, send a diagnostics
// event to clear the analysis markers that they already have.
if (!newSettings.ScriptAnalysis.Enable.Value)
{
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
foreach (var scriptFile in WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetOpenedFiles())
{
await PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext);
}
}
else
{
await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
}
}
// Update the settings in the current
CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading;
CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath);
}
#endregion
#region Private Helpers
/// <summary>
/// Runs script diagnostics on changed files
/// </summary>
/// <param name="filesToAnalyze"></param>
/// <param name="eventContext"></param>
private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext)
{
if (!CurrentSettings.ScriptAnalysis.Enable.Value)
{
// If the user has disabled script analysis, skip it entirely
return Task.FromResult(true);
}
// If there's an existing task, attempt to cancel it
try
{
if (ExistingRequestCancellation != null)
{
// Try to cancel the request
ExistingRequestCancellation.Cancel();
// If cancellation didn't throw an exception,
// clean up the existing token
ExistingRequestCancellation.Dispose();
ExistingRequestCancellation = null;
}
}
catch (Exception e)
{
Logger.Write(
LogLevel.Error,
string.Format(
"Exception while cancelling analysis task:\n\n{0}",
e.ToString()));
TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>();
cancelTask.SetCanceled();
return cancelTask.Task;
}
// Create a fresh cancellation token and then start the task.
// We create this on a different TaskScheduler so that we
// don't block the main message loop thread.
ExistingRequestCancellation = new CancellationTokenSource();
Task.Factory.StartNew(
() =>
DelayThenInvokeDiagnostics(
750,
filesToAnalyze,
eventContext,
ExistingRequestCancellation.Token),
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
return Task.FromResult(true);
}
/// <summary>
/// Actually run the script diagnostics after waiting for some small delay
/// </summary>
/// <param name="delayMilliseconds"></param>
/// <param name="filesToAnalyze"></param>
/// <param name="eventContext"></param>
/// <param name="cancellationToken"></param>
private async Task DelayThenInvokeDiagnostics(
int delayMilliseconds,
ScriptFile[] filesToAnalyze,
EventContext eventContext,
CancellationToken cancellationToken)
{
// First of all, wait for the desired delay period before
// analyzing the provided list of files
try
{
await Task.Delay(delayMilliseconds, cancellationToken);
}
catch (TaskCanceledException)
{
// If the task is cancelled, exit directly
return;
}
// If we've made it past the delay period then we don't care
// about the cancellation token anymore. This could happen
// when the user stops typing for long enough that the delay
// period ends but then starts typing while analysis is going
// on. It makes sense to send back the results from the first
// delay period while the second one is ticking away.
// Get the requested files
foreach (ScriptFile scriptFile in filesToAnalyze)
{
Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath);
ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile);
Logger.Write(LogLevel.Verbose, "Analysis complete.");
await PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext);
}
}
/// <summary>
/// Send the diagnostic results back to the host application
/// </summary>
/// <param name="scriptFile"></param>
/// <param name="semanticMarkers"></param>
/// <param name="eventContext"></param>
private static async Task PublishScriptDiagnostics(
ScriptFile scriptFile,
ScriptFileMarker[] semanticMarkers,
EventContext eventContext)
{
var allMarkers = scriptFile.SyntaxMarkers != null
? scriptFile.SyntaxMarkers.Concat(semanticMarkers)
: semanticMarkers;
// Always send syntax and semantic errors. We want to
// make sure no out-of-date markers are being displayed.
await eventContext.SendEvent(
PublishDiagnosticsNotification.Type,
new PublishDiagnosticsNotification
{
Uri = scriptFile.ClientFilePath,
Diagnostics =
allMarkers
.Select(GetDiagnosticFromMarker)
.ToArray()
});
}
/// <summary>
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
/// </summary>
/// <param name="scriptFileMarker"></param>
/// <returns></returns>
private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker)
{
return new Diagnostic
{
Severity = MapDiagnosticSeverity(scriptFileMarker.Level),
Message = scriptFileMarker.Message,
Range = new Range
{
Start = new Position
{
Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1,
Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1
},
End = new Position
{
Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1,
Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1
}
}
};
}
/// <summary>
/// Map ScriptFileMarker severity to Diagnostic severity
/// </summary>
/// <param name="markerLevel"></param>
private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel)
{
switch (markerLevel)
{
case ScriptFileMarkerLevel.Error:
return DiagnosticSeverity.Error;
case ScriptFileMarkerLevel.Warning:
return DiagnosticSeverity.Warning;
case ScriptFileMarkerLevel.Information:
return DiagnosticSeverity.Information;
default:
return DiagnosticSeverity.Error;
}
}
#endregion
}
}

Some files were not shown because too many files have changed in this diff Show More