diff --git a/.gitignore b/.gitignore
index 4c997e2b..de0fdc5b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/BUILD.md b/BUILD.md
new file mode 100644
index 00000000..603d42b4
--- /dev/null
+++ b/BUILD.md
@@ -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).
diff --git a/build.cake b/build.cake
new file mode 100644
index 00000000..61047b05
--- /dev/null
+++ b/build.cake
@@ -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";
+
+///
+/// Class representing build.json
+///
+public class BuildPlan
+{
+ public IDictionary 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(
+ 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");
+
+///
+/// Clean artifacts.
+///
+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);
+});
+
+///
+/// Pre-build setup tasks.
+///
+Task("Setup")
+ .IsDependentOn("BuildEnvironment")
+ .IsDependentOn("PopulateRuntimes")
+ .Does(() =>
+{
+});
+
+///
+/// Populate the RIDs for the specific environment.
+/// Use default RID (+ win7-x86 on Windows) for now.
+///
+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"
+ };
+});
+
+///
+/// Install/update build environment.
+///
+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}");
+ }
+});
+
+///
+/// Restore required NuGet packages.
+///
+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.");
+});
+
+///
+/// Build Test projects.
+///
+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();
+ 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());
+ }
+ }
+});
+
+///
+/// Run all tests for .NET Desktop and .NET Core
+///
+Task("TestAll")
+ .IsDependentOn("Test")
+ .IsDependentOn("TestCore")
+ .Does(() =>{});
+
+///
+/// Run all tests for Travis CI .NET Desktop and .NET Core
+///
+Task("TravisTestAll")
+ .IsDependentOn("Cleanup")
+ .IsDependentOn("TestAll")
+ .Does(() =>{});
+
+///
+/// Run tests for .NET Core (using .NET CLI).
+///
+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.");
+ }
+});
+
+///
+/// Run tests for other frameworks (using XUnit2).
+///
+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}");
+ }
+ }
+ }
+});
+
+///
+/// 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.
+///
+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);
+});
+
+///
+/// Alias for OnlyPublish.
+/// Targets all RIDs as specified in build.json.
+///
+Task("AllPublish")
+ .IsDependentOn("Restore")
+ .IsDependentOn("OnlyPublish")
+ .Does(() =>
+{
+});
+
+///
+/// Restrict the RIDs for the local default.
+///
+Task("RestrictToLocalRuntime")
+ .IsDependentOn("Setup")
+ .Does(() =>
+{
+ buildPlan.Rids = new string[] {"default"};
+});
+
+///
+/// Alias for OnlyPublish.
+/// Restricts publishing to local RID.
+///
+Task("LocalPublish")
+ .IsDependentOn("Restore")
+ .IsDependentOn("RestrictToLocalRuntime")
+ .IsDependentOn("OnlyPublish")
+ .Does(() =>
+{
+});
+
+///
+/// Test the published binaries if they start up without errors.
+/// Uses builds corresponding to local RID.
+///
+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}");
+ }
+ }
+});
+
+///
+/// Clean install path.
+///
+Task("CleanupInstall")
+ .Does(() =>
+{
+ if (System.IO.Directory.Exists(installFolder))
+ {
+ System.IO.Directory.Delete(installFolder, true);
+ }
+ System.IO.Directory.CreateDirectory(installFolder);
+});
+
+///
+/// Quick build.
+///
+Task("Quick")
+ .IsDependentOn("Cleanup")
+ .IsDependentOn("LocalPublish")
+ .Does(() =>
+{
+});
+
+///
+/// Quick build + install.
+///
+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);
+});
+
+///
+/// Full build targeting all RIDs specified in build.json.
+///
+Task("All")
+ .IsDependentOn("Cleanup")
+ .IsDependentOn("Restore")
+ .IsDependentOn("TestAll")
+ .IsDependentOn("AllPublish")
+ //.IsDependentOn("TestPublished")
+ .Does(() =>
+{
+});
+
+///
+/// Full build targeting local RID.
+///
+Task("Local")
+ .IsDependentOn("Cleanup")
+ .IsDependentOn("Restore")
+ .IsDependentOn("TestAll")
+ .IsDependentOn("LocalPublish")
+ // .IsDependentOn("TestPublished")
+ .Does(() =>
+{
+});
+
+///
+/// Build centered around producing the final artifacts for Travis
+///
+/// The tests are run as a different task "TestAll"
+///
+Task("Travis")
+ .IsDependentOn("Cleanup")
+ .IsDependentOn("Restore")
+ .IsDependentOn("AllPublish")
+ // .IsDependentOn("TestPublished")
+ .Does(() =>
+{
+});
+
+///
+/// Update the package versions within project.json files.
+/// Uses depversion.json file as input.
+///
+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());
+ 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));
+ }
+});
+
+///
+/// Default Task aliases to Local.
+///
+Task("Default")
+ .IsDependentOn("Local")
+ .Does(() =>
+{
+});
+
+///
+/// Default to Local.
+///
+RunTarget(target);
diff --git a/build.json b/build.json
new file mode 100644
index 00000000..a0741723
--- /dev/null
+++ b/build.json
@@ -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"
+}
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 00000000..68dc2c1e
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,3 @@
+$Env:SQLTOOLSSERVICE_PACKAGE_OSNAME = "win-x64"
+.\scripts\cake-bootstrap.ps1 -experimental @args
+exit $LASTEXITCODE
diff --git a/build.sh b/build.sh
new file mode 100644
index 00000000..90332a42
--- /dev/null
+++ b/build.sh
@@ -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 "$@"
diff --git a/global.json b/global.json
index db6ba19b..9ae78d22 100644
--- a/global.json
+++ b/global.json
@@ -1,5 +1,8 @@
{
- "projects": [ "src", "test" ]
+ "projects": [ "src", "test" ],
+ "sdk": {
+ "version": "1.0.0-preview2-003121"
+ }
}
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 00000000..edd564a3
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/scripts/archiving.cake b/scripts/archiving.cake
new file mode 100644
index 00000000..4b012986
--- /dev/null
+++ b/scripts/archiving.cake
@@ -0,0 +1,104 @@
+#load "runhelpers.cake"
+
+using System.IO.Compression;
+using System.Text.RegularExpressions;
+
+///
+/// Generate the build identifier based on the RID and framework identifier.
+/// Special rules when running on Travis (for publishing purposes).
+///
+/// The RID
+/// The framework identifier
+/// The designated build identifier
+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}";
+}
+
+///
+/// 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.
+///
+/// The RID
+/// The folder containing the files to package
+/// The target archive name (without extension)
+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}");
+ }
+ }
+}
+
+///
+/// Package a given output folder using a build identifier generated from the RID and framework identifier.
+///
+/// The RID
+/// The framework identifier
+/// The folder containing the files to package
+/// The destination folder for the archive
+/// The project name
+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}");
+ }
+}
\ No newline at end of file
diff --git a/scripts/artifacts.cake b/scripts/artifacts.cake
new file mode 100644
index 00000000..f448fe3f
--- /dev/null
+++ b/scripts/artifacts.cake
@@ -0,0 +1,43 @@
+#load "runhelpers.cake"
+
+///
+/// Generate the scripts which target the SQLTOOLSSERVICE binaries.
+///
+/// The root folder where the publised (or installed) binaries are located
+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}\"");
+ }
+}
\ No newline at end of file
diff --git a/scripts/cake-bootstrap.ps1 b/scripts/cake-bootstrap.ps1
new file mode 100644
index 00000000..a87c1478
--- /dev/null
+++ b/scripts/cake-bootstrap.ps1
@@ -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
diff --git a/scripts/cake-bootstrap.sh b/scripts/cake-bootstrap.sh
new file mode 100644
index 00000000..abc3ed43
--- /dev/null
+++ b/scripts/cake-bootstrap.sh
@@ -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
diff --git a/scripts/packages.config b/scripts/packages.config
new file mode 100644
index 00000000..c4feb50f
--- /dev/null
+++ b/scripts/packages.config
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/scripts/runhelpers.cake b/scripts/runhelpers.cake
new file mode 100644
index 00000000..03499601
--- /dev/null
+++ b/scripts/runhelpers.cake
@@ -0,0 +1,204 @@
+using System.Collections.Generic;
+using System.Diagnostics;
+
+///
+/// Class encompassing the optional settings for running processes.
+///
+public class RunOptions
+{
+ ///
+ /// The working directory of the process.
+ ///
+ public string WorkingDirectory { get; set; }
+ ///
+ /// Container logging the StandardOutput content.
+ ///
+ public IList StandardOutputListing { get; set; }
+ ///
+ /// Desired maximum time-out for the process
+ ///
+ public int TimeOut { get; set; }
+}
+
+///
+/// Wrapper for the exit code and state.
+/// Used to query the result of an execution with method calls.
+///
+public struct ExitStatus
+{
+ private int _code;
+ private bool _timeOut;
+ ///
+ /// Default constructor when the execution finished.
+ ///
+ /// The exit code
+ public ExitStatus(int code)
+ {
+ this._code = code;
+ this._timeOut = false;
+ }
+ ///
+ /// Default constructor when the execution potentially timed out.
+ ///
+ /// The exit code
+ /// True if the execution timed out
+ public ExitStatus(int code, bool timeOut)
+ {
+ this._code = code;
+ this._timeOut = timeOut;
+ }
+ ///
+ /// Flag signalling that the execution timed out.
+ ///
+ public bool DidTimeOut { get { return _timeOut; } }
+ ///
+ /// Implicit conversion from ExitStatus to the exit code.
+ ///
+ /// The exit status
+ /// The exit code
+ public static implicit operator int(ExitStatus exitStatus)
+ {
+ return exitStatus._code;
+ }
+ ///
+ /// Trigger Exception for non-zero exit code.
+ ///
+ /// The message to use in the Exception
+ /// The exit status for further queries
+ public ExitStatus ExceptionOnError(string errorMessage)
+ {
+ if (this._code != 0)
+ {
+ throw new Exception(errorMessage);
+ }
+ return this;
+ }
+}
+
+///
+/// Run the given executable with the given arguments.
+///
+/// Executable to run
+/// Arguments
+/// The exit status for further queries
+ExitStatus Run(string exec, string args)
+{
+ return Run(exec, args, new RunOptions());
+}
+
+///
+/// Run the given executable with the given arguments.
+///
+/// Executable to run
+/// Arguments
+/// Working directory
+/// The exit status for further queries
+ExitStatus Run(string exec, string args, string workingDirectory)
+{
+ return Run(exec, args,
+ new RunOptions()
+ {
+ WorkingDirectory = workingDirectory
+ });
+}
+
+///
+/// Run the given executable with the given arguments.
+///
+/// Executable to run
+/// Arguments
+/// Optional settings
+/// The exit status for further queries
+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);
+ }
+ }
+}
+
+///
+/// Run restore with the given arguments
+///
+/// Executable to run
+/// Arguments
+/// Optional settings
+/// The exit status for further queries
+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);
+}
+
+///
+/// Kill the given process and all its child processes.
+///
+/// Root process
+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();
+ }
+}
diff --git a/sqltoolsservice.sln b/sqltoolsservice.sln
new file mode 100644
index 00000000..cd55b538
--- /dev/null
+++ b/sqltoolsservice.sln
@@ -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
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs
new file mode 100644
index 00000000..31d0026d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionInfo.cs
@@ -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
+{
+ ///
+ /// Information pertaining to a unique connection instance.
+ ///
+ public class ConnectionInfo
+ {
+ ///
+ /// Constructor
+ ///
+ public ConnectionInfo(ISqlConnectionFactory factory, string ownerUri, ConnectionDetails details)
+ {
+ Factory = factory;
+ OwnerUri = ownerUri;
+ ConnectionDetails = details;
+ ConnectionId = Guid.NewGuid();
+ }
+
+ ///
+ /// Unique Id, helpful to identify a connection info object
+ ///
+ public Guid ConnectionId { get; private set; }
+
+ ///
+ /// URI identifying the owner/user of the connection. Could be a file, service, resource, etc.
+ ///
+ public string OwnerUri { get; private set; }
+
+ ///
+ /// Factory used for creating the SQL connection associated with the connection info.
+ ///
+ public ISqlConnectionFactory Factory {get; private set;}
+
+ ///
+ /// Properties used for creating/opening the SQL connection.
+ ///
+ public ConnectionDetails ConnectionDetails { get; private set; }
+
+ ///
+ /// The connection to the SQL database that commands will be run against.
+ ///
+ public DbConnection SqlConnection { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
new file mode 100644
index 00000000..57a7ba6e
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs
@@ -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
+{
+ ///
+ /// Main class for the Connection Management services
+ ///
+ public class ConnectionService
+ {
+ ///
+ /// Singleton service instance
+ ///
+ private static Lazy instance
+ = new Lazy(() => new ConnectionService());
+
+ ///
+ /// Gets the singleton service instance
+ ///
+ public static ConnectionService Instance
+ {
+ get
+ {
+ return instance.Value;
+ }
+ }
+
+ ///
+ /// The SQL connection factory object
+ ///
+ private ISqlConnectionFactory connectionFactory;
+
+ private Dictionary ownerToConnectionMap = new Dictionary();
+
+ ///
+ /// Service host object for sending/receiving requests/events.
+ /// Internal for testing purposes.
+ ///
+ internal IProtocolEndpoint ServiceHost
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// Default constructor is private since it's a singleton class
+ ///
+ private ConnectionService()
+ {
+ }
+
+ ///
+ /// Callback for onconnection handler
+ ///
+ ///
+ public delegate Task OnConnectionHandler(ConnectionInfo info);
+
+ ///
+ // Callback for ondisconnect handler
+ ///
+ public delegate Task OnDisconnectHandler(ConnectionSummary summary);
+
+ ///
+ /// List of onconnection handlers
+ ///
+ private readonly List onConnectionActivities = new List();
+
+ ///
+ /// List of ondisconnect handlers
+ ///
+ private readonly List onDisconnectActivities = new List();
+
+ ///
+ /// Gets the SQL connection factory instance
+ ///
+ public ISqlConnectionFactory ConnectionFactory
+ {
+ get
+ {
+ if (this.connectionFactory == null)
+ {
+ this.connectionFactory = new SqlConnectionFactory();
+ }
+ return this.connectionFactory;
+ }
+ }
+
+ ///
+ /// Test constructor that injects dependency interfaces
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Open a connection with the specified connection details
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Close a connection with the specified connection details.
+ ///
+ 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;
+ }
+
+ ///
+ /// List all databases on the server specified
+ ///
+ 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 results = new List();
+ 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.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
+ }
+
+ ///
+ /// Add a new method to be called when the onconnection request is submitted
+ ///
+ ///
+ public void RegisterOnConnectionTask(OnConnectionHandler activity)
+ {
+ onConnectionActivities.Add(activity);
+ }
+
+ ///
+ /// Add a new method to be called when the ondisconnect request is submitted
+ ///
+ public void RegisterOnDisconnectTask(OnDisconnectHandler activity)
+ {
+ onDisconnectActivities.Add(activity);
+ }
+
+ ///
+ /// Handle new connection requests
+ ///
+ ///
+ ///
+ ///
+ protected async Task HandleConnectRequest(
+ ConnectParams connectParams,
+ RequestContext 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());
+ }
+ }
+
+ ///
+ /// Handle disconnect requests
+ ///
+ protected async Task HandleDisconnectRequest(
+ DisconnectParams disconnectParams,
+ RequestContext 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());
+ }
+
+ }
+
+ ///
+ /// Handle requests to list databases on the current server
+ ///
+ protected async Task HandleListDatabasesRequest(
+ ListDatabasesParams listDatabasesParams,
+ RequestContext 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);
+ }
+
+ ///
+ /// Build a connection string from a connection details instance
+ ///
+ ///
+ 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();
+ }
+
+ ///
+ /// Change the database context of a connection.
+ ///
+ /// URI of the owner of the connection
+ /// Name of the database to change the connection to
+ 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())
+ );
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParams.cs
new file mode 100644
index 00000000..31dad8c5
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParams.cs
@@ -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
+{
+ ///
+ /// Parameters for the Connect Request.
+ ///
+ public class ConnectParams
+ {
+ ///
+ /// 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.
+ ///
+ public string OwnerUri { get; set; }
+ ///
+ /// 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.
+ ///
+ public ConnectionDetails Connection { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs
new file mode 100644
index 00000000..9f2c7356
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectParamsExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods to ConnectParams
+ ///
+ public static class ConnectParamsExtensions
+ {
+ ///
+ /// Check that the fields in ConnectParams are all valid
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs
new file mode 100644
index 00000000..c325c64f
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectResponse.cs
@@ -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
+{
+ ///
+ /// Message format for the connection result response
+ ///
+ public class ConnectResponse
+ {
+ ///
+ /// A GUID representing a unique connection ID
+ ///
+ public string ConnectionId { get; set; }
+
+ ///
+ /// Gets or sets any connection error messages
+ ///
+ public string Messages { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedNotification.cs
new file mode 100644
index 00000000..c0daee6d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedNotification.cs
@@ -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
+{
+ ///
+ /// ConnectionChanged notification mapping entry
+ ///
+ public class ConnectionChangedNotification
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("connection/connectionchanged");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedParams.cs
new file mode 100644
index 00000000..3db86f34
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionChangedParams.cs
@@ -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
+{
+ ///
+ /// Parameters for the ConnectionChanged Notification.
+ ///
+ public class ConnectionChangedParams
+ {
+ ///
+ /// 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.
+ ///
+ public string OwnerUri { get; set; }
+ ///
+ /// Contains the high-level properties about the connection, for display to the user.
+ ///
+ public ConnectionSummary Connection { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs
new file mode 100644
index 00000000..ce1c6208
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs
@@ -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
+{
+ ///
+ /// Message format for the initial connection request
+ ///
+ ///
+ /// If this contract is ever changed, be sure to update ConnectionDetailsExtensions methods.
+ ///
+ public class ConnectionDetails : ConnectionSummary
+ {
+ ///
+ /// Gets or sets the connection password
+ ///
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Gets or sets the authentication to use.
+ ///
+ public string AuthenticationType { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool? Encrypt { get; set; }
+
+ ///
+ /// Gets or sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust.
+ ///
+ public bool? TrustServerCertificate { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool? PersistSecurityInfo { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public int? ConnectTimeout { get; set; }
+
+ ///
+ /// The number of reconnections attempted after identifying that there was an idle connection failure.
+ ///
+ public int? ConnectRetryCount { get; set; }
+
+ ///
+ /// Amount of time (in seconds) between each reconnection attempt after identifying that there was an idle connection failure.
+ ///
+ public int? ConnectRetryInterval { get; set; }
+
+ ///
+ /// Gets or sets the name of the application associated with the connection string.
+ ///
+ public string ApplicationName { get; set; }
+
+ ///
+ /// Gets or sets the name of the workstation connecting to SQL Server.
+ ///
+ public string WorkstationId { get; set; }
+
+ ///
+ /// Declares the application workload type when connecting to a database in an SQL Server Availability Group.
+ ///
+ public string ApplicationIntent { get; set; }
+
+ ///
+ /// Gets or sets the SQL Server Language record name.
+ ///
+ public string CurrentLanguage { get; set; }
+
+ ///
+ /// Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested.
+ ///
+ public bool? Pooling { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string.
+ ///
+ public int? MaxPoolSize { get; set; }
+
+ ///
+ /// Gets or sets the minimum number of connections allowed in the connection pool for this specific connection string.
+ ///
+ public int? MinPoolSize { get; set; }
+
+ ///
+ /// Gets or sets the minimum time, in seconds, for the connection to live in the connection pool before being destroyed.
+ ///
+ public int? LoadBalanceTimeout { get; set; }
+
+ ///
+ /// Gets or sets a Boolean value that indicates whether replication is supported using the connection.
+ ///
+ public bool? Replication { get; set; }
+
+ ///
+ /// Gets or sets a string that contains the name of the primary data file. This includes the full path name of an attachable database.
+ ///
+ public string AttachDbFilename { get; set; }
+
+ ///
+ /// Gets or sets the name or address of the partner server to connect to if the primary server is down.
+ ///
+ public string FailoverPartner { get; set; }
+
+ ///
+ /// 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.
+ ///
+ public bool? MultiSubnetFailover { get; set; }
+
+ ///
+ /// When true, an application can maintain multiple active result sets (MARS).
+ ///
+ public bool? MultipleActiveResultSets { get; set; }
+
+ ///
+ /// Gets or sets the size in bytes of the network packets used to communicate with an instance of SQL Server.
+ ///
+ public int? PacketSize { get; set; }
+
+ ///
+ /// Gets or sets a string value that indicates the type system the application expects.
+ ///
+ public string TypeSystemVersion { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetailsExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetailsExtensions.cs
new file mode 100644
index 00000000..106fa06e
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetailsExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods for the ConnectionDetails contract class
+ ///
+ public static class ConnectionDetailsExtensions
+ {
+ ///
+ /// Create a copy of a connection details object.
+ ///
+ 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
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs
new file mode 100644
index 00000000..50251e12
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionRequest.cs
@@ -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
+{
+ ///
+ /// Connect request mapping entry
+ ///
+ public class ConnectionRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("connection/connect");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummary.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummary.cs
new file mode 100644
index 00000000..11549e85
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummary.cs
@@ -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
+{
+ ///
+ /// Provides high level information about a connection.
+ ///
+ public class ConnectionSummary
+ {
+ ///
+ /// Gets or sets the connection server name
+ ///
+ public string ServerName { get; set; }
+
+ ///
+ /// Gets or sets the connection database name
+ ///
+ public string DatabaseName { get; set; }
+
+ ///
+ /// Gets or sets the connection user name
+ ///
+ public string UserName { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryComparer.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryComparer.cs
new file mode 100644
index 00000000..dfeb0ab4
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryComparer.cs
@@ -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
+{
+
+ ///
+ /// Treats connections as the same if their server, db and usernames all match
+ ///
+ public class ConnectionSummaryComparer : IEqualityComparer
+ {
+ 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;
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryExtensions.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryExtensions.cs
new file mode 100644
index 00000000..02bc7623
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionSummaryExtensions.cs
@@ -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
+{
+ ///
+ /// Extension methods to ConnectionSummary
+ ///
+ public static class ConnectionSummaryExtensions
+ {
+ ///
+ /// Create a copy of a ConnectionSummary object
+ ///
+ public static ConnectionSummary Clone(this ConnectionSummary summary)
+ {
+ return new ConnectionSummary()
+ {
+ ServerName = summary.ServerName,
+ DatabaseName = summary.DatabaseName,
+ UserName = summary.UserName
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectParams.cs
new file mode 100644
index 00000000..91bc7faf
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectParams.cs
@@ -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
+{
+ ///
+ /// Parameters for the Disconnect Request.
+ ///
+ public class DisconnectParams
+ {
+ ///
+ /// 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.
+ ///
+ public string OwnerUri { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectRequest.cs
new file mode 100644
index 00000000..cbf67ef2
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/DisconnectRequest.cs
@@ -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
+{
+ ///
+ /// Disconnect request mapping entry
+ ///
+ public class DisconnectRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("connection/disconnect");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesParams.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesParams.cs
new file mode 100644
index 00000000..fa607e75
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesParams.cs
@@ -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
+{
+ ///
+ /// Parameters for the List Databases Request.
+ ///
+ public class ListDatabasesParams
+ {
+ ///
+ /// URI of the owner of the connection requesting the list of databases.
+ ///
+ public string OwnerUri { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesRequest.cs
new file mode 100644
index 00000000..01c12a45
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesRequest.cs
@@ -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
+{
+ ///
+ /// List databases request mapping entry
+ ///
+ public class ListDatabasesRequest
+ {
+ public static readonly
+ RequestType Type =
+ RequestType.Create("connection/listdatabases");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesResponse.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesResponse.cs
new file mode 100644
index 00000000..68610803
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ListDatabasesResponse.cs
@@ -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
+{
+ ///
+ /// Message format for the list databases response
+ ///
+ public class ListDatabasesResponse
+ {
+ ///
+ /// Gets or sets the list of database names.
+ ///
+ public string[] DatabaseNames { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ISqlConnectionFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ISqlConnectionFactory.cs
new file mode 100644
index 00000000..ed0cc01b
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ISqlConnectionFactory.cs
@@ -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
+{
+ ///
+ /// Interface for the SQL Connection factory
+ ///
+ public interface ISqlConnectionFactory
+ {
+ ///
+ /// Create a new SQL Connection object
+ ///
+ DbConnection CreateSqlConnection(string connectionString);
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs
new file mode 100644
index 00000000..cffb690d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/SqlConnectionFactory.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ public class SqlConnectionFactory : ISqlConnectionFactory
+ {
+ ///
+ /// Creates a new SqlConnection object
+ ///
+ public DbConnection CreateSqlConnection(string connectionString)
+ {
+ return new SqlConnection(connectionString);
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Contracts/Credential.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Contracts/Credential.cs
new file mode 100644
index 00000000..be595ec8
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Contracts/Credential.cs
@@ -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
+{
+ ///
+ /// A Credential containing information needed to log into a resource. This is primarily
+ /// defined as a unique with an associated
+ /// that's linked to it.
+ ///
+ public class Credential
+ {
+ ///
+ /// A unique ID to identify the credential being saved.
+ ///
+ public string CredentialId { get; set; }
+
+ ///
+ /// The Password stored for this credential.
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Default Constructor
+ ///
+ public Credential()
+ {
+ }
+
+ ///
+ /// Constructor used when only is known
+ ///
+ ///
+ public Credential(string credentialId)
+ : this(credentialId, null)
+ {
+
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Validates the credential has all the properties needed to look up the password
+ ///
+ public static void ValidateForLookup(Credential credential)
+ {
+ Validate.IsNotNull("credential", credential);
+ Validate.IsNotNullOrEmptyString("credential.CredentialId", credential.CredentialId);
+ }
+
+
+ ///
+ /// Validates the credential has all the properties needed to save a password
+ ///
+ public static void ValidateForSave(Credential credential)
+ {
+ ValidateForLookup(credential);
+ Validate.IsNotNullOrEmptyString("credential.Password", credential.Password);
+ }
+ }
+
+ ///
+ /// Read Credential request mapping entry. Expects a Credential with CredentialId,
+ /// and responds with the filled in if found
+ ///
+ public class ReadCredentialRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("credential/read");
+ }
+
+ ///
+ /// Save Credential request mapping entry
+ ///
+ public class SaveCredentialRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("credential/save");
+ }
+
+ ///
+ /// Delete Credential request mapping entry
+ ///
+ public class DeleteCredentialRequest
+ {
+ ///
+ /// Request definition
+ ///
+ public static readonly
+ RequestType Type =
+ RequestType.Create("credential/delete");
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/CredentialService.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/CredentialService.cs
new file mode 100644
index 00000000..f1a80807
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/CredentialService.cs
@@ -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
+{
+ ///
+ /// Service responsible for securing credentials in a platform-neutral manner. This provides
+ /// a generic API for read, save and delete credentials
+ ///
+ public class CredentialService
+ {
+ internal static string DefaultSecretsFolder = ".sqlsecrets";
+ internal const string DefaultSecretsFile = "sqlsecrets.json";
+
+
+ ///
+ /// Singleton service instance
+ ///
+ private static Lazy instance
+ = new Lazy(() => new CredentialService());
+
+ ///
+ /// Gets the singleton service instance
+ ///
+ public static CredentialService Instance
+ {
+ get
+ {
+ return instance.Value;
+ }
+ }
+
+ private ICredentialStore credStore;
+
+ ///
+ /// Default constructor is private since it's a singleton class
+ ///
+ private CredentialService()
+ : this(null, new LinuxCredentialStore.StoreConfig()
+ { CredentialFolder = DefaultSecretsFolder, CredentialFile = DefaultSecretsFile, IsRelativeToUserHomeDir = true})
+ {
+ }
+
+ ///
+ /// Internal for testing purposes only
+ ///
+ internal CredentialService(ICredentialStore store, LinuxCredentialStore.StoreConfig config)
+ {
+ this.credStore = store != null ? store : GetStoreForOS(config);
+ }
+
+ ///
+ /// Internal for testing purposes only
+ ///
+ 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 requestContext)
+ {
+ Func 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 requestContext)
+ {
+ Func doSave = () =>
+ {
+ Credential.ValidateForSave(credential);
+ return credStore.Save(credential);
+ };
+ await HandleRequest(doSave, requestContext, "HandleSaveCredentialRequest");
+ }
+
+ public async Task HandleDeleteCredentialRequest(Credential credential, RequestContext requestContext)
+ {
+ Func doDelete = () =>
+ {
+ Credential.ValidateForLookup(credential);
+ return credStore.DeletePassword(credential.CredentialId);
+ };
+ await HandleRequest(doDelete, requestContext, "HandleDeleteCredentialRequest");
+ }
+
+ private async Task HandleRequest(Func handler, RequestContext requestContext, string requestType)
+ {
+ Logger.Write(LogLevel.Verbose, requestType);
+
+ try
+ {
+ T result = handler();
+ await requestContext.SendResult(result);
+ }
+ catch (Exception ex)
+ {
+ await requestContext.SendError(ex.ToString());
+ }
+ }
+
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/ICredentialStore.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/ICredentialStore.cs
new file mode 100644
index 00000000..0fa51cdd
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/ICredentialStore.cs
@@ -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
+{
+ ///
+ /// An support securely saving and retrieving passwords
+ ///
+ public interface ICredentialStore
+ {
+ ///
+ /// Saves a Password linked to a given Credential
+ ///
+ ///
+ /// A to be saved.
+ /// and are required
+ ///
+ /// True if successful, false otherwise
+ bool Save(Credential credential);
+
+ ///
+ /// Gets a Password and sets it into a object
+ ///
+ /// The name of the credential to find the password for. This is required
+ /// Out value
+ /// true if password was found, false otherwise
+ bool TryGetPassword(string credentialId, out string password);
+
+ ///
+ /// Deletes a password linked to a given credential
+ ///
+ /// The name of the credential to find the password for. This is required
+ /// True if password existed and was deleted, false otherwise
+ bool DeletePassword(string credentialId);
+
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/InteropUtils.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/InteropUtils.cs
new file mode 100644
index 00000000..fdb5343e
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/InteropUtils.cs
@@ -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
+ {
+
+ ///
+ /// Gets the length in bytes for a Unicode string, for use in interop where length must be defined
+ ///
+ 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);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/CredentialsWrapper.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/CredentialsWrapper.cs
new file mode 100644
index 00000000..3deab819
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/CredentialsWrapper.cs
@@ -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
+{
+ ///
+ /// Simplified class to enable writing a set of credentials to/from disk
+ ///
+ public class CredentialsWrapper
+ {
+ public List Credentials { get; set; }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/FileTokenStorage.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/FileTokenStorage.cs
new file mode 100644
index 00000000..ef2c2a67
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/FileTokenStorage.cs
@@ -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 newEntries, IEnumerable existingEntries)
+ {
+ var allEntries = existingEntries.Concat(newEntries);
+ this.SaveEntries(allEntries);
+ }
+
+ public void Clear()
+ {
+ this.SaveEntries(new List());
+ }
+
+ public IEnumerable LoadEntries()
+ {
+ if(!File.Exists(this.fileName))
+ {
+ return Enumerable.Empty();
+ }
+
+ string serializedCreds;
+ lock (lockObject)
+ {
+ serializedCreds = File.ReadAllText(this.fileName);
+ }
+
+ CredentialsWrapper creds = JsonConvert.DeserializeObject(serializedCreds, Constants.JsonSerializerSettings);
+ if(creds != null)
+ {
+ return creds.Credentials;
+ }
+ return Enumerable.Empty();
+ }
+
+ public void SaveEntries(IEnumerable 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);
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Errors.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Errors.cs
new file mode 100644
index 00000000..f3b1d5f5
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Errors.cs
@@ -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
+ {
+ /// Common Unix errno error codes.
+ 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);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Sys.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Sys.cs
new file mode 100644
index 00000000..8777ab0c
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/Interop.Sys.cs
@@ -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";
+ }
+ }
+
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/LinuxCredentialStore.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/LinuxCredentialStore.cs
new file mode 100644
index 00000000..6d6b5908
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Linux/LinuxCredentialStore.cs
@@ -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
+{
+ ///
+ /// Linux implementation of the credential store.
+ ///
+ ///
+ /// This entire implementation may need to be revised to support encryption of
+ /// passwords and protection of them when loaded into memory.
+ ///
+ ///
+ 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 creds;
+ if (LoadCredentialsAndFilterById(credentialId, out creds))
+ {
+ storage.SaveEntries(creds);
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Gets filtered credentials with a specific ID filtered out
+ ///
+ /// True if the credential to filter was removed, false if it was not found
+ private bool LoadCredentialsAndFilterById(string idToFilter, out IEnumerable 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 creds;
+ LoadCredentialsAndFilterById(credential.CredentialId, out creds);
+ storage.SaveEntries(creds.Append(credential));
+
+ return true;
+ }
+
+
+ ///
+ /// Internal for testing purposes only
+ ///
+ internal string CredentialFolderPath
+ {
+ get { return this.credentialFolderPath; }
+ }
+
+ ///
+ /// Concatenates a directory to the user home directory's path
+ ///
+ internal static string GetUserScopedDirectory(string userPath)
+ {
+ string homeDir = GetHomeDirectory() ?? string.Empty;
+ return Path.Combine(homeDir, userPath);
+ }
+
+
+ /// Gets the current user's home directory.
+ /// The path to the home directory, or null if it could not be determined.
+ 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;
+ }
+ }
+ }
+
+ /// Wrapper for getpwuid_r.
+ /// The length of the buffer to use when storing the password result.
+ /// The resulting path; null if the user didn't have an entry.
+ /// true if the call was successful (path may still be null); false is a larger buffer is needed.
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.CoreFoundation.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.CoreFoundation.cs
new file mode 100644
index 00000000..140dfc63
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.CoreFoundation.cs
@@ -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
+ {
+ ///
+ /// Tells the OS what encoding the passed in String is in. These come from the CFString.h header file in the CoreFoundation framework.
+ ///
+ 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
+ }
+
+ ///
+ /// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
+ ///
+ /// Should be IntPtr.Zero
+ /// The string to get a CFStringRef for
+ /// The encoding of the str variable. This should be UTF 8 for OS X
+ /// Returns a pointer to a CFString on success; otherwise, returns IntPtr.Zero
+ /// For *nix systems, the CLR maps ANSI to UTF-8, so be explicit about that
+ [DllImport(Interop.Libraries.CoreFoundationLibrary, CharSet = CharSet.Ansi)]
+ private static extern SafeCreateHandle CFStringCreateWithCString(
+ IntPtr allocator,
+ string str,
+ CFStringBuiltInEncodings encoding);
+
+ ///
+ /// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
+ ///
+ /// The string to get a CFStringRef for
+ /// Returns a valid SafeCreateHandle to a CFString on success; otherwise, returns an invalid SafeCreateHandle
+ internal static SafeCreateHandle CFStringCreateWithCString(string str)
+ {
+ return CFStringCreateWithCString(IntPtr.Zero, str, CFStringBuiltInEncodings.kCFStringEncodingUTF8);
+ }
+
+ ///
+ /// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
+ ///
+ /// Should be IntPtr.Zero
+ /// The values to put in the array
+ /// The number of values in the array
+ /// Should be IntPtr.Zero
+ /// Returns a pointer to a CFArray on success; otherwise, returns IntPtr.Zero
+ [DllImport(Interop.Libraries.CoreFoundationLibrary)]
+ private static extern SafeCreateHandle CFArrayCreate(
+ IntPtr allocator,
+ [MarshalAs(UnmanagedType.LPArray)]
+ IntPtr[] values,
+ ulong numValues,
+ IntPtr callbacks);
+
+ ///
+ /// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
+ ///
+ /// The values to put in the array
+ /// The number of values in the array
+ /// Returns a valid SafeCreateHandle to a CFArray on success; otherwise, returns an invalid SafeCreateHandle
+ internal static SafeCreateHandle CFArrayCreate(IntPtr[] values, ulong numValues)
+ {
+ return CFArrayCreate(IntPtr.Zero, values, numValues, IntPtr.Zero);
+ }
+
+ ///
+ /// 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
+ ///
+ /// The CFType object to retain. This value must not be NULL
+ /// The input value
+ [DllImport(Interop.Libraries.CoreFoundationLibrary)]
+ internal extern static IntPtr CFRetain(IntPtr ptr);
+
+ ///
+ /// Decrements the reference count on the specified object and, if the ref count hits 0, cleans up the object.
+ ///
+ /// The pointer on which to decrement the reference count.
+ [DllImport(Interop.Libraries.CoreFoundationLibrary)]
+ internal extern static void CFRelease(IntPtr ptr);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Libraries.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Libraries.cs
new file mode 100644
index 00000000..7ad5b639
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Libraries.cs
@@ -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";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Security.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Security.cs
new file mode 100644
index 00000000..0a6209e8
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/Interop.Security.cs
@@ -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);
+
+ ///
+ /// Find a generic password based on the attributes passed
+ ///
+ ///
+ /// A reference to an array of keychains to search, a single keychain, or NULL to search the user's default keychain search list.
+ ///
+ /// The length of the buffer pointed to by serviceName.
+ /// A pointer to a string containing the service name.
+ /// The length of the buffer pointed to by accountName.
+ /// A pointer to a string containing the account name.
+ /// On return, the length of the buffer pointed to by passwordData.
+ ///
+ /// 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.
+ ///
+ /// On return, a reference to the keychain item which was found.
+ /// A result code that should be in
+ ///
+ /// 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.
+ ///
+ [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);
+
+ ///
+ /// Releases the memory used by the keychain attribute list and the keychain data retrieved in a previous call to SecKeychainItemCopyContent.
+ ///
+ /// A pointer to the attribute list to release. Pass NULL to ignore this parameter.
+ /// A pointer to the data buffer to release. Pass NULL to ignore this parameter.
+ /// A result code that should be in
+ [DllImport(Libraries.SecurityLibrary, SetLastError = true)]
+ internal static extern OSStatus SecKeychainItemFreeContent([In] IntPtr attrList, [In] IntPtr data);
+
+ ///
+ /// Deletes a keychain item from the default keychain's permanent data store.
+ ///
+ /// A keychain item reference of the item to delete.
+ /// A result code that should be in
+ ///
+ /// 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.
+ ///
+ [DllImport(Libraries.SecurityLibrary, SetLastError = true)]
+ internal static extern OSStatus SecKeychainItemDelete(SafeHandle itemRef);
+
+ #region OSStatus Codes
+ /// Common Unix errno error codes.
+ 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
+ }
+ }
+}
+
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/OSXCredentialStore.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/OSXCredentialStore.cs
new file mode 100644
index 00000000..dc868040
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/OSXCredentialStore.cs
@@ -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
+{
+ ///
+ /// OSX implementation of the credential store
+ ///
+ 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;
+ }
+
+ ///
+ /// Finds the first password matching this credential
+ ///
+ 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;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/SafeCreateHandle.OSX.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/SafeCreateHandle.OSX.cs
new file mode 100644
index 00000000..5beaaf26
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/OSX/SafeCreateHandle.OSX.cs
@@ -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
+
+{
+ ///
+ /// 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.
+ ///
+ [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;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/SecureStringHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/SecureStringHelper.cs
new file mode 100644
index 00000000..070e0b20
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/SecureStringHelper.cs
@@ -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);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialResources.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialResources.cs
new file mode 100644
index 00000000..d3834c42
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialResources.cs
@@ -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.";
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialSet.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialSet.cs
new file mode 100644
index 00000000..f94a0f57
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialSet.cs
@@ -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, 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 credentialHandles =
+ ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
+
+ IEnumerable 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);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialType.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialType.cs
new file mode 100644
index 00000000..edc16d0d
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/CredentialType.cs
@@ -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
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/GlobalSuppressions.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/GlobalSuppressions.cs
new file mode 100644
index 00000000..3ee40dc8
Binary files /dev/null and b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/GlobalSuppressions.cs differ
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/NativeMethods.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/NativeMethods.cs
new file mode 100644
index 00000000..1e43205c
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/NativeMethods.cs
@@ -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(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;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/PersistanceType.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/PersistanceType.cs
new file mode 100644
index 00000000..b08eff08
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/PersistanceType.cs
@@ -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
+ }
+}
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32Credential.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32Credential.cs
new file mode 100644
index 00000000..21c1c8b9
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32Credential.cs
@@ -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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32CredentialStore.cs b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32CredentialStore.cs
new file mode 100644
index 00000000..8a219854
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Credentials/Win32/Win32CredentialStore.cs
@@ -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
+{
+ ///
+ /// Win32 implementation of the credential store
+ ///
+ 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();
+ }
+
+ }
+ }
+
+}
diff --git a/src/ServiceHost/LanguageServer/ClientCapabilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ClientCapabilities.cs
similarity index 85%
rename from src/ServiceHost/LanguageServer/ClientCapabilities.cs
rename to src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ClientCapabilities.cs
index 70e2d068..397deceb 100644
--- a/src/ServiceHost/LanguageServer/ClientCapabilities.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ClientCapabilities.cs
@@ -4,7 +4,7 @@
//
-namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
+namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
{
///
/// Defines a class that describes the capabilities of a language
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/HostingErrorEvent.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/HostingErrorEvent.cs
new file mode 100644
index 00000000..d6e65801
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/HostingErrorEvent.cs
@@ -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
+{
+ ///
+ /// Parameters to be used for reporting hosting-level errors, such as protocol violations
+ ///
+ public class HostingErrorParams
+ {
+ ///
+ /// The message of the error
+ ///
+ public string Message { get; set; }
+ }
+
+ public class HostingErrorEvent
+ {
+ public static readonly
+ EventType Type =
+ EventType.Create("hosting/error");
+
+ }
+}
diff --git a/src/ServiceHost/LanguageServer/Initialize.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Initialize.cs
similarity index 91%
rename from src/ServiceHost/LanguageServer/Initialize.cs
rename to src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Initialize.cs
index 7551835e..215edf87 100644
--- a/src/ServiceHost/LanguageServer/Initialize.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Initialize.cs
@@ -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
{
diff --git a/src/ServiceHost/LanguageServer/ServerCapabilities.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ServerCapabilities.cs
similarity index 96%
rename from src/ServiceHost/LanguageServer/ServerCapabilities.cs
rename to src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ServerCapabilities.cs
index 2f7404d9..32f0e736 100644
--- a/src/ServiceHost/LanguageServer/ServerCapabilities.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/ServerCapabilities.cs
@@ -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
{
diff --git a/src/ServiceHost/LanguageServer/Shutdown.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Shutdown.cs
similarity index 86%
rename from src/ServiceHost/LanguageServer/Shutdown.cs
rename to src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Shutdown.cs
index f0a7bbd2..1ccb9cfc 100644
--- a/src/ServiceHost/LanguageServer/Shutdown.cs
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/Shutdown.cs
@@ -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
{
///
/// Defines a message that is sent from the client to request
diff --git a/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/VersionRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/VersionRequest.cs
new file mode 100644
index 00000000..ed7ab358
--- /dev/null
+++ b/src/Microsoft.SqlTools.ServiceLayer/Hosting/Contracts/VersionRequest.cs
@@ -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
+{
+ ///
+ /// Defines a message that is sent from the client to request
+ /// the version of the server.
+ ///
+ public class VersionRequest
+ {
+ public static readonly
+ RequestType