mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-03 09:35:39 -05:00
Merge branch 'dev'
This commit is contained in:
9
.gitignore
vendored
9
.gitignore
vendored
@@ -13,6 +13,7 @@ project.lock.json
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.exe
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
@@ -29,7 +30,13 @@ msbuild.log
|
||||
msbuild.err
|
||||
msbuild.wrn
|
||||
|
||||
|
||||
# code coverage artifacts
|
||||
coverage.xml
|
||||
node_modules
|
||||
packages
|
||||
reports
|
||||
opencovertests.xml
|
||||
sqltools.xml
|
||||
|
||||
# Cross building rootfs
|
||||
cross/rootfs/
|
||||
|
||||
75
BUILD.md
Normal file
75
BUILD.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Usage
|
||||
|
||||
Run `build.(ps1|sh)` with the desired set of arguments (see below for options).
|
||||
The build script itself is `build.cake`, written in C# using the Cake build automation system.
|
||||
All build related activites should be encapsulated in this file for cross-platform access.
|
||||
|
||||
# Arguments
|
||||
|
||||
## Primary
|
||||
|
||||
`-target=TargetName`: The name of the build task/target to execute (see below for listing and details).
|
||||
Defaults to `Default`.
|
||||
|
||||
`-configuration=(Release|Debug)`: The configuration to build.
|
||||
Defaults to `Release`.
|
||||
|
||||
## Extra
|
||||
|
||||
`-test-configuration=(Release|Debug)`: The configuration to use for the unit tests.
|
||||
Defaults to `Debug`.
|
||||
|
||||
`-install-path=Path`: Path used for the **Install** target.
|
||||
Defaults to `(%USERPROFILE%|$HOME)/.sqltoolsservice/local`
|
||||
|
||||
`-archive`: Enable the generation of publishable archives after a build.
|
||||
|
||||
# Targets
|
||||
|
||||
**Default**: Alias for Local.
|
||||
|
||||
**Local**: Full build including testing for the machine-local runtime.
|
||||
|
||||
**All**: Same as local, but targeting all runtimes selected by `PopulateRuntimes` in `build.cake`.
|
||||
Currently configured to also build for a 32-bit Windows runtime on Windows machines.
|
||||
No additional runtimes are currently selected on non-Windows machines.
|
||||
|
||||
**Quick**: Local build which skips all testing.
|
||||
|
||||
**Install**: Same as quick, but installs the generated binaries into `install-path`.
|
||||
|
||||
**SetPackageVersions**: Updates the dependency versions found within `project.json` files using information from `depversion.json`.
|
||||
Used for maintainence within the project, not needed for end-users. More information below.
|
||||
|
||||
# Configuration files
|
||||
|
||||
## build.json
|
||||
|
||||
A number of build-related options, including folder names for different entities. Interesting options:
|
||||
|
||||
**DotNetInstallScriptURL**: The URL where the .NET SDK install script is located.
|
||||
Can be used to pin to a specific script version, if a breaking change occurs.
|
||||
|
||||
**"DotNetChannel"**: The .NET SDK channel used for retreiving the tools.
|
||||
|
||||
**"DotNetVersion"**: The .NET SDK version used for the build. Can be used to pin to a specific version.
|
||||
Using the string `Latest` will retrieve the latest version.
|
||||
|
||||
## depversion.json
|
||||
|
||||
A listing of all dependencies (and their desired versions) used by `project.json` files throughout the project.
|
||||
Allows for quick and automatic updates to the dependency version numbers using the **SetPackageVersions** target.
|
||||
|
||||
# Artifacts generated
|
||||
|
||||
* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries built for the local machine in `artifacts/publish/Microsoft.SqlTools.ServiceLayer/default/{framework}/`
|
||||
* Scripts to run Microsoft.SqlTools.ServiceLayer at `scripts/SQLTOOLSSERVICE(.Core)(.cmd)`
|
||||
* These scripts are updated for every build and every install.
|
||||
* The scripts point to the installed binary after and install, otherwise just the build folder (reset if a new build occurs without an install).
|
||||
* Binaries of Microsoft.SqlTools.ServiceLayer and its libraries cross-compiled for other runtimes (if selected in **PopulateRuntimes**) `artifacts/publish/Microsoft.SqlTools.ServiceLayer/{runtime}/{framework}/`
|
||||
* Test logs in `artifacts/logs`
|
||||
* Archived binaries in `artifacts/package` (only if `-archive` used on command line)
|
||||
|
||||
# Requirements
|
||||
|
||||
The build system requires Mono to be installed on non-Windows machines as Cake is not built using .NET Core (yet).
|
||||
507
build.cake
Normal file
507
build.cake
Normal file
@@ -0,0 +1,507 @@
|
||||
#addin "Newtonsoft.Json"
|
||||
|
||||
#load "scripts/runhelpers.cake"
|
||||
#load "scripts/archiving.cake"
|
||||
#load "scripts/artifacts.cake"
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
// Basic arguments
|
||||
var target = Argument("target", "Default");
|
||||
var configuration = Argument("configuration", "Release");
|
||||
// Optional arguments
|
||||
var testConfiguration = Argument("test-configuration", "Debug");
|
||||
var installFolder = Argument("install-path", System.IO.Path.Combine(Environment.GetEnvironmentVariable(IsRunningOnWindows() ? "USERPROFILE" : "HOME"),
|
||||
".sqltoolsservice", "local"));
|
||||
var requireArchive = HasArgument("archive");
|
||||
|
||||
// Working directory
|
||||
var workingDirectory = System.IO.Directory.GetCurrentDirectory();
|
||||
|
||||
// System specific shell configuration
|
||||
var shell = IsRunningOnWindows() ? "powershell" : "bash";
|
||||
var shellArgument = IsRunningOnWindows() ? "-NoProfile /Command" : "-C";
|
||||
var shellExtension = IsRunningOnWindows() ? "ps1" : "sh";
|
||||
|
||||
/// <summary>
|
||||
/// Class representing build.json
|
||||
/// </summary>
|
||||
public class BuildPlan
|
||||
{
|
||||
public IDictionary<string, string[]> TestProjects { get; set; }
|
||||
public string BuildToolsFolder { get; set; }
|
||||
public string ArtifactsFolder { get; set; }
|
||||
public bool UseSystemDotNetPath { get; set; }
|
||||
public string DotNetFolder { get; set; }
|
||||
public string DotNetInstallScriptURL { get; set; }
|
||||
public string DotNetChannel { get; set; }
|
||||
public string DotNetVersion { get; set; }
|
||||
public string[] Frameworks { get; set; }
|
||||
public string[] Rids { get; set; }
|
||||
public string MainProject { get; set; }
|
||||
}
|
||||
|
||||
var buildPlan = JsonConvert.DeserializeObject<BuildPlan>(
|
||||
System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "build.json")));
|
||||
|
||||
// Folders and tools
|
||||
var dotnetFolder = System.IO.Path.Combine(workingDirectory, buildPlan.DotNetFolder);
|
||||
var dotnetcli = buildPlan.UseSystemDotNetPath ? "dotnet" : System.IO.Path.Combine(System.IO.Path.GetFullPath(dotnetFolder), "dotnet");
|
||||
var toolsFolder = System.IO.Path.Combine(workingDirectory, buildPlan.BuildToolsFolder);
|
||||
|
||||
var sourceFolder = System.IO.Path.Combine(workingDirectory, "src");
|
||||
var testFolder = System.IO.Path.Combine(workingDirectory, "test");
|
||||
|
||||
var artifactFolder = System.IO.Path.Combine(workingDirectory, buildPlan.ArtifactsFolder);
|
||||
var publishFolder = System.IO.Path.Combine(artifactFolder, "publish");
|
||||
var logFolder = System.IO.Path.Combine(artifactFolder, "logs");
|
||||
var packageFolder = System.IO.Path.Combine(artifactFolder, "package");
|
||||
var scriptFolder = System.IO.Path.Combine(artifactFolder, "scripts");
|
||||
|
||||
/// <summary>
|
||||
/// Clean artifacts.
|
||||
/// </summary>
|
||||
Task("Cleanup")
|
||||
.Does(() =>
|
||||
{
|
||||
if (System.IO.Directory.Exists(artifactFolder))
|
||||
{
|
||||
System.IO.Directory.Delete(artifactFolder, true);
|
||||
}
|
||||
System.IO.Directory.CreateDirectory(artifactFolder);
|
||||
System.IO.Directory.CreateDirectory(logFolder);
|
||||
System.IO.Directory.CreateDirectory(packageFolder);
|
||||
System.IO.Directory.CreateDirectory(scriptFolder);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Pre-build setup tasks.
|
||||
/// </summary>
|
||||
Task("Setup")
|
||||
.IsDependentOn("BuildEnvironment")
|
||||
.IsDependentOn("PopulateRuntimes")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Populate the RIDs for the specific environment.
|
||||
/// Use default RID (+ win7-x86 on Windows) for now.
|
||||
/// </summary>
|
||||
Task("PopulateRuntimes")
|
||||
.IsDependentOn("BuildEnvironment")
|
||||
.Does(() =>
|
||||
{
|
||||
buildPlan.Rids = new string[]
|
||||
{
|
||||
"default", // To allow testing the published artifact
|
||||
"win7-x64",
|
||||
"win7-x86",
|
||||
"ubuntu.14.04-x64",
|
||||
"ubuntu.16.04-x64",
|
||||
"centos.7-x64",
|
||||
"rhel.7.2-x64",
|
||||
"debian.8-x64",
|
||||
"fedora.23-x64",
|
||||
"opensuse.13.2-x64",
|
||||
"osx.10.11-x64"
|
||||
};
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Install/update build environment.
|
||||
/// </summary>
|
||||
Task("BuildEnvironment")
|
||||
.Does(() =>
|
||||
{
|
||||
var installScript = $"dotnet-install.{shellExtension}";
|
||||
System.IO.Directory.CreateDirectory(dotnetFolder);
|
||||
var scriptPath = System.IO.Path.Combine(dotnetFolder, installScript);
|
||||
using (WebClient client = new WebClient())
|
||||
{
|
||||
client.DownloadFile($"{buildPlan.DotNetInstallScriptURL}/{installScript}", scriptPath);
|
||||
}
|
||||
if (!IsRunningOnWindows())
|
||||
{
|
||||
Run("chmod", $"+x '{scriptPath}'");
|
||||
}
|
||||
var installArgs = $"-Channel {buildPlan.DotNetChannel}";
|
||||
if (!String.IsNullOrEmpty(buildPlan.DotNetVersion))
|
||||
{
|
||||
installArgs = $"{installArgs} -Version {buildPlan.DotNetVersion}";
|
||||
}
|
||||
if (!buildPlan.UseSystemDotNetPath)
|
||||
{
|
||||
installArgs = $"{installArgs} -InstallDir {dotnetFolder}";
|
||||
}
|
||||
Run(shell, $"{shellArgument} {scriptPath} {installArgs}");
|
||||
try
|
||||
{
|
||||
Run(dotnetcli, "--info");
|
||||
}
|
||||
catch (Win32Exception)
|
||||
{
|
||||
throw new Exception(".NET CLI binary cannot be found.");
|
||||
}
|
||||
|
||||
System.IO.Directory.CreateDirectory(toolsFolder);
|
||||
|
||||
var nugetPath = Environment.GetEnvironmentVariable("NUGET_EXE");
|
||||
var arguments = $"install xunit.runner.console -ExcludeVersion -NoCache -Prerelease -OutputDirectory \"{toolsFolder}\"";
|
||||
if (IsRunningOnWindows())
|
||||
{
|
||||
Run(nugetPath, arguments);
|
||||
}
|
||||
else
|
||||
{
|
||||
Run("mono", $"\"{nugetPath}\" {arguments}");
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Restore required NuGet packages.
|
||||
/// </summary>
|
||||
Task("Restore")
|
||||
.IsDependentOn("Setup")
|
||||
.Does(() =>
|
||||
{
|
||||
RunRestore(dotnetcli, "restore", sourceFolder)
|
||||
.ExceptionOnError("Failed to restore projects under source code folder.");
|
||||
RunRestore(dotnetcli, "restore --infer-runtimes", testFolder)
|
||||
.ExceptionOnError("Failed to restore projects under test code folder.");
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Build Test projects.
|
||||
/// </summary>
|
||||
Task("BuildTest")
|
||||
.IsDependentOn("Setup")
|
||||
.IsDependentOn("Restore")
|
||||
.Does(() =>
|
||||
{
|
||||
foreach (var pair in buildPlan.TestProjects)
|
||||
{
|
||||
foreach (var framework in pair.Value)
|
||||
{
|
||||
var project = pair.Key;
|
||||
var projectFolder = System.IO.Path.Combine(testFolder, project);
|
||||
var runLog = new List<string>();
|
||||
Run(dotnetcli, $"build --framework {framework} --configuration {testConfiguration} \"{projectFolder}\"",
|
||||
new RunOptions
|
||||
{
|
||||
StandardOutputListing = runLog
|
||||
})
|
||||
.ExceptionOnError($"Building test {project} failed for {framework}.");
|
||||
System.IO.File.WriteAllLines(System.IO.Path.Combine(logFolder, $"{project}-{framework}-build.log"), runLog.ToArray());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Run all tests for .NET Desktop and .NET Core
|
||||
/// </summary>
|
||||
Task("TestAll")
|
||||
.IsDependentOn("Test")
|
||||
.IsDependentOn("TestCore")
|
||||
.Does(() =>{});
|
||||
|
||||
/// <summary>
|
||||
/// Run all tests for Travis CI .NET Desktop and .NET Core
|
||||
/// </summary>
|
||||
Task("TravisTestAll")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("TestAll")
|
||||
.Does(() =>{});
|
||||
|
||||
/// <summary>
|
||||
/// Run tests for .NET Core (using .NET CLI).
|
||||
/// </summary>
|
||||
Task("TestCore")
|
||||
.IsDependentOn("Setup")
|
||||
.IsDependentOn("Restore")
|
||||
.Does(() =>
|
||||
{
|
||||
var testProjects = buildPlan.TestProjects
|
||||
.Where(pair => pair.Value.Any(framework => framework.Contains("netcoreapp")))
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var testProject in testProjects)
|
||||
{
|
||||
var logFile = System.IO.Path.Combine(logFolder, $"{testProject}-core-result.xml");
|
||||
var testWorkingDir = System.IO.Path.Combine(testFolder, testProject);
|
||||
Run(dotnetcli, $"test -f netcoreapp1.0 -xml \"{logFile}\" -notrait category=failing", testWorkingDir)
|
||||
.ExceptionOnError($"Test {testProject} failed for .NET Core.");
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Run tests for other frameworks (using XUnit2).
|
||||
/// </summary>
|
||||
Task("Test")
|
||||
.IsDependentOn("Setup")
|
||||
.IsDependentOn("BuildTest")
|
||||
.Does(() =>
|
||||
{
|
||||
foreach (var pair in buildPlan.TestProjects)
|
||||
{
|
||||
foreach (var framework in pair.Value)
|
||||
{
|
||||
// Testing against core happens in TestCore
|
||||
if (framework.Contains("netcoreapp"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var project = pair.Key;
|
||||
var frameworkFolder = System.IO.Path.Combine(testFolder, project, "bin", testConfiguration, framework);
|
||||
var runtime = System.IO.Directory.GetDirectories(frameworkFolder).First();
|
||||
var instanceFolder = System.IO.Path.Combine(frameworkFolder, runtime);
|
||||
|
||||
// Copy xunit executable to test folder to solve path errors
|
||||
var xunitToolsFolder = System.IO.Path.Combine(toolsFolder, "xunit.runner.console", "tools");
|
||||
var xunitInstancePath = System.IO.Path.Combine(instanceFolder, "xunit.console.exe");
|
||||
System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.console.exe"), xunitInstancePath, true);
|
||||
System.IO.File.Copy(System.IO.Path.Combine(xunitToolsFolder, "xunit.runner.utility.desktop.dll"), System.IO.Path.Combine(instanceFolder, "xunit.runner.utility.desktop.dll"), true);
|
||||
var targetPath = System.IO.Path.Combine(instanceFolder, $"{project}.dll");
|
||||
var logFile = System.IO.Path.Combine(logFolder, $"{project}-{framework}-result.xml");
|
||||
var arguments = $"\"{targetPath}\" -parallel none -xml \"{logFile}\" -notrait category=failing";
|
||||
if (IsRunningOnWindows())
|
||||
{
|
||||
Run(xunitInstancePath, arguments, instanceFolder)
|
||||
.ExceptionOnError($"Test {project} failed for {framework}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Run("mono", $"\"{xunitInstancePath}\" {arguments}", instanceFolder)
|
||||
.ExceptionOnError($"Test {project} failed for {framework}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Build, publish and package artifacts.
|
||||
/// Targets all RIDs specified in build.json unless restricted by RestrictToLocalRuntime.
|
||||
/// No dependencies on other tasks to support quick builds.
|
||||
/// </summary>
|
||||
Task("OnlyPublish")
|
||||
.IsDependentOn("Setup")
|
||||
.Does(() =>
|
||||
{
|
||||
var project = buildPlan.MainProject;
|
||||
var projectFolder = System.IO.Path.Combine(sourceFolder, project);
|
||||
foreach (var framework in buildPlan.Frameworks)
|
||||
{
|
||||
foreach (var runtime in buildPlan.Rids)
|
||||
{
|
||||
var outputFolder = System.IO.Path.Combine(publishFolder, project, runtime, framework);
|
||||
var publishArguments = "publish";
|
||||
if (!runtime.Equals("default"))
|
||||
{
|
||||
publishArguments = $"{publishArguments} --runtime {runtime}";
|
||||
}
|
||||
publishArguments = $"{publishArguments} --framework {framework} --configuration {configuration}";
|
||||
publishArguments = $"{publishArguments} --output \"{outputFolder}\" \"{projectFolder}\"";
|
||||
Run(dotnetcli, publishArguments)
|
||||
.ExceptionOnError($"Failed to publish {project} / {framework}");
|
||||
|
||||
if (requireArchive)
|
||||
{
|
||||
Package(runtime, framework, outputFolder, packageFolder, buildPlan.MainProject.ToLower());
|
||||
}
|
||||
}
|
||||
}
|
||||
CreateRunScript(System.IO.Path.Combine(publishFolder, project, "default"), scriptFolder);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Alias for OnlyPublish.
|
||||
/// Targets all RIDs as specified in build.json.
|
||||
/// </summary>
|
||||
Task("AllPublish")
|
||||
.IsDependentOn("Restore")
|
||||
.IsDependentOn("OnlyPublish")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Restrict the RIDs for the local default.
|
||||
/// </summary>
|
||||
Task("RestrictToLocalRuntime")
|
||||
.IsDependentOn("Setup")
|
||||
.Does(() =>
|
||||
{
|
||||
buildPlan.Rids = new string[] {"default"};
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Alias for OnlyPublish.
|
||||
/// Restricts publishing to local RID.
|
||||
/// </summary>
|
||||
Task("LocalPublish")
|
||||
.IsDependentOn("Restore")
|
||||
.IsDependentOn("RestrictToLocalRuntime")
|
||||
.IsDependentOn("OnlyPublish")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Test the published binaries if they start up without errors.
|
||||
/// Uses builds corresponding to local RID.
|
||||
/// </summary>
|
||||
Task("TestPublished")
|
||||
.IsDependentOn("Setup")
|
||||
.Does(() =>
|
||||
{
|
||||
var project = buildPlan.MainProject;
|
||||
var projectFolder = System.IO.Path.Combine(sourceFolder, project);
|
||||
var scriptsToTest = new string[] {"SQLTOOLSSERVICE.Core"};//TODO
|
||||
foreach (var script in scriptsToTest)
|
||||
{
|
||||
var scriptPath = System.IO.Path.Combine(scriptFolder, script);
|
||||
var didNotExitWithError = Run($"{shell}", $"{shellArgument} \"{scriptPath}\" -s \"{projectFolder}\" --stdio",
|
||||
new RunOptions
|
||||
{
|
||||
TimeOut = 10000
|
||||
})
|
||||
.DidTimeOut;
|
||||
if (!didNotExitWithError)
|
||||
{
|
||||
throw new Exception($"Failed to run {script}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Clean install path.
|
||||
/// </summary>
|
||||
Task("CleanupInstall")
|
||||
.Does(() =>
|
||||
{
|
||||
if (System.IO.Directory.Exists(installFolder))
|
||||
{
|
||||
System.IO.Directory.Delete(installFolder, true);
|
||||
}
|
||||
System.IO.Directory.CreateDirectory(installFolder);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Quick build.
|
||||
/// </summary>
|
||||
Task("Quick")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("LocalPublish")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Quick build + install.
|
||||
/// </summary>
|
||||
Task("Install")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("LocalPublish")
|
||||
.IsDependentOn("CleanupInstall")
|
||||
.Does(() =>
|
||||
{
|
||||
var project = buildPlan.MainProject;
|
||||
foreach (var framework in buildPlan.Frameworks)
|
||||
{
|
||||
var outputFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(publishFolder, project, "default", framework));
|
||||
var targetFolder = System.IO.Path.GetFullPath(System.IO.Path.Combine(installFolder, framework));
|
||||
// Copy all the folders
|
||||
foreach (var directory in System.IO.Directory.GetDirectories(outputFolder, "*", SearchOption.AllDirectories))
|
||||
System.IO.Directory.CreateDirectory(System.IO.Path.Combine(targetFolder, directory.Substring(outputFolder.Length + 1)));
|
||||
//Copy all the files
|
||||
foreach (string file in System.IO.Directory.GetFiles(outputFolder, "*", SearchOption.AllDirectories))
|
||||
System.IO.File.Copy(file, System.IO.Path.Combine(targetFolder, file.Substring(outputFolder.Length + 1)), true);
|
||||
}
|
||||
CreateRunScript(installFolder, scriptFolder);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Full build targeting all RIDs specified in build.json.
|
||||
/// </summary>
|
||||
Task("All")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("Restore")
|
||||
.IsDependentOn("TestAll")
|
||||
.IsDependentOn("AllPublish")
|
||||
//.IsDependentOn("TestPublished")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Full build targeting local RID.
|
||||
/// </summary>
|
||||
Task("Local")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("Restore")
|
||||
.IsDependentOn("TestAll")
|
||||
.IsDependentOn("LocalPublish")
|
||||
// .IsDependentOn("TestPublished")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Build centered around producing the final artifacts for Travis
|
||||
///
|
||||
/// The tests are run as a different task "TestAll"
|
||||
/// </summary>
|
||||
Task("Travis")
|
||||
.IsDependentOn("Cleanup")
|
||||
.IsDependentOn("Restore")
|
||||
.IsDependentOn("AllPublish")
|
||||
// .IsDependentOn("TestPublished")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Update the package versions within project.json files.
|
||||
/// Uses depversion.json file as input.
|
||||
/// </summary>
|
||||
Task("SetPackageVersions")
|
||||
.Does(() =>
|
||||
{
|
||||
var jDepVersion = JObject.Parse(System.IO.File.ReadAllText(System.IO.Path.Combine(workingDirectory, "depversion.json")));
|
||||
var projects = System.IO.Directory.GetFiles(sourceFolder, "project.json", SearchOption.AllDirectories).ToList();
|
||||
projects.AddRange(System.IO.Directory.GetFiles(testFolder, "project.json", SearchOption.AllDirectories));
|
||||
foreach (var project in projects)
|
||||
{
|
||||
var jProject = JObject.Parse(System.IO.File.ReadAllText(project));
|
||||
var dependencies = jProject.SelectTokens("dependencies")
|
||||
.Union(jProject.SelectTokens("frameworks.*.dependencies"))
|
||||
.SelectMany(dependencyToken => dependencyToken.Children<JProperty>());
|
||||
foreach (JProperty dependency in dependencies)
|
||||
{
|
||||
if (jDepVersion[dependency.Name] != null)
|
||||
{
|
||||
dependency.Value = jDepVersion[dependency.Name];
|
||||
}
|
||||
}
|
||||
System.IO.File.WriteAllText(project, JsonConvert.SerializeObject(jProject, Formatting.Indented));
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Default Task aliases to Local.
|
||||
/// </summary>
|
||||
Task("Default")
|
||||
.IsDependentOn("Local")
|
||||
.Does(() =>
|
||||
{
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Default to Local.
|
||||
/// </summary>
|
||||
RunTarget(target);
|
||||
18
build.json
Normal file
18
build.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"UseSystemDotNetPath": "true",
|
||||
"DotNetFolder": ".dotnet",
|
||||
"DotNetInstallScriptURL": "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview2/scripts/obtain",
|
||||
"DotNetChannel": "preview",
|
||||
"DotNetVersion": "1.0.0-preview2-003121",
|
||||
"BuildToolsFolder": ".tools",
|
||||
"ArtifactsFolder": "artifacts",
|
||||
"TestProjects": {
|
||||
"Microsoft.SqlTools.ServiceLayer.Test": [
|
||||
"netcoreapp1.0"
|
||||
]
|
||||
},
|
||||
"Frameworks": [
|
||||
"netcoreapp1.0"
|
||||
],
|
||||
"MainProject": "Microsoft.SqlTools.ServiceLayer"
|
||||
}
|
||||
3
build.ps1
Normal file
3
build.ps1
Normal file
@@ -0,0 +1,3 @@
|
||||
$Env:SQLTOOLSSERVICE_PACKAGE_OSNAME = "win-x64"
|
||||
.\scripts\cake-bootstrap.ps1 -experimental @args
|
||||
exit $LASTEXITCODE
|
||||
12
build.sh
Normal file
12
build.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Handle to many files on osx
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then
|
||||
ulimit -n 4096
|
||||
fi
|
||||
|
||||
if [ "$TRAVIS_OS_NAME" == "osx" ] || [ `uname` == "Darwin" ]; then
|
||||
export SQLTOOLSSERVICE_PACKAGE_OSNAME=osx-x64
|
||||
else
|
||||
export SQLTOOLSSERVICE_PACKAGE_OSNAME=linux-x64
|
||||
fi
|
||||
bash ./scripts/cake-bootstrap.sh "$@"
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"projects": [ "src", "test" ]
|
||||
"projects": [ "src", "test" ],
|
||||
"sdk": {
|
||||
"version": "1.0.0-preview2-003121"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
6
nuget.config
Normal file
6
nuget.config
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration >
|
||||
<packageSources>
|
||||
<add key="DataTools Nuget" value="http://dtnuget/api/v2/" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
104
scripts/archiving.cake
Normal file
104
scripts/archiving.cake
Normal file
@@ -0,0 +1,104 @@
|
||||
#load "runhelpers.cake"
|
||||
|
||||
using System.IO.Compression;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generate the build identifier based on the RID and framework identifier.
|
||||
/// Special rules when running on Travis (for publishing purposes).
|
||||
/// </summary>
|
||||
/// <param name="runtime">The RID</param>
|
||||
/// <param name="framework">The framework identifier</param>
|
||||
/// <returns>The designated build identifier</returns>
|
||||
string GetBuildIdentifier(string runtime, string framework)
|
||||
{
|
||||
var runtimeShort = "";
|
||||
// Default RID uses package name set in build script
|
||||
if (runtime.Equals("default"))
|
||||
{
|
||||
runtimeShort = Environment.GetEnvironmentVariable("SQLTOOLSSERVICE_PACKAGE_OSNAME");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove version number. Note: because there are separate versions for Ubuntu 14 and 16,
|
||||
// we treat Ubuntu as a special case.
|
||||
if (runtime.StartsWith("ubuntu.14"))
|
||||
{
|
||||
runtimeShort = "ubuntu14-x64";
|
||||
}
|
||||
else if (runtime.StartsWith("ubuntu.16"))
|
||||
{
|
||||
runtimeShort = "ubuntu16-x64";
|
||||
}
|
||||
else
|
||||
{
|
||||
runtimeShort = Regex.Replace(runtime, "(\\d|\\.)*-", "-");
|
||||
}
|
||||
}
|
||||
|
||||
return $"{runtimeShort}-{framework}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate an archive out of the given published folder.
|
||||
/// Use ZIP for Windows runtimes.
|
||||
/// Use TAR.GZ for non-Windows runtimes.
|
||||
/// Use 7z to generate TAR.GZ on Windows if available.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The RID</param>
|
||||
/// <param name="contentFolder">The folder containing the files to package</param>
|
||||
/// <param name="archiveName">The target archive name (without extension)</param>
|
||||
void DoArchive(string runtime, string contentFolder, string archiveName)
|
||||
{
|
||||
// On all platforms use ZIP for Windows runtimes
|
||||
if (runtime.Contains("win") || (runtime.Equals("default") && IsRunningOnWindows()))
|
||||
{
|
||||
var zipFile = $"{archiveName}.zip";
|
||||
Zip(contentFolder, zipFile);
|
||||
}
|
||||
// On all platforms use TAR.GZ for Unix runtimes
|
||||
else
|
||||
{
|
||||
var tarFile = $"{archiveName}.tar.gz";
|
||||
// Use 7z to create TAR.GZ on Windows
|
||||
if (IsRunningOnWindows())
|
||||
{
|
||||
var tempFile = $"{archiveName}.tar";
|
||||
try
|
||||
{
|
||||
Run("7z", $"a \"{tempFile}\"", contentFolder)
|
||||
.ExceptionOnError($"Tar-ing failed for {contentFolder} {archiveName}");
|
||||
Run("7z", $"a \"{tarFile}\" \"{tempFile}\"", contentFolder)
|
||||
.ExceptionOnError($"Compression failed for {contentFolder} {archiveName}");
|
||||
System.IO.File.Delete(tempFile);
|
||||
}
|
||||
catch(Win32Exception)
|
||||
{
|
||||
Information("Warning: 7z not available on PATH to pack tar.gz results");
|
||||
}
|
||||
}
|
||||
// Use tar to create TAR.GZ on Unix
|
||||
else
|
||||
{
|
||||
Run("tar", $"czf \"{tarFile}\" .", contentFolder)
|
||||
.ExceptionOnError($"Compression failed for {contentFolder} {archiveName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Package a given output folder using a build identifier generated from the RID and framework identifier.
|
||||
/// </summary>
|
||||
/// <param name="runtime">The RID</param>
|
||||
/// <param name="framework">The framework identifier</param>
|
||||
/// <param name="contentFolder">The folder containing the files to package</param>
|
||||
/// <param name="packageFolder">The destination folder for the archive</param>
|
||||
/// <param name="projectName">The project name</param>
|
||||
void Package(string runtime, string framework, string contentFolder, string packageFolder, string projectName)
|
||||
{
|
||||
var buildIdentifier = GetBuildIdentifier(runtime, framework);
|
||||
if (buildIdentifier != null)
|
||||
{
|
||||
DoArchive(runtime, contentFolder, $"{packageFolder}/{projectName}-{buildIdentifier}");
|
||||
}
|
||||
}
|
||||
43
scripts/artifacts.cake
Normal file
43
scripts/artifacts.cake
Normal file
@@ -0,0 +1,43 @@
|
||||
#load "runhelpers.cake"
|
||||
|
||||
/// <summary>
|
||||
/// Generate the scripts which target the SQLTOOLSSERVICE binaries.
|
||||
/// </summary>
|
||||
/// <param name="outputRoot">The root folder where the publised (or installed) binaries are located</param>
|
||||
void CreateRunScript(string outputRoot, string scriptFolder)
|
||||
{
|
||||
if (IsRunningOnWindows())
|
||||
{
|
||||
var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core.cmd");
|
||||
var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{0}", "SQLTOOLSSERVICE");
|
||||
var content = new string[] {
|
||||
"SETLOCAL",
|
||||
"",
|
||||
$"\"{sqlToolsServicePath}\" %*"
|
||||
};
|
||||
if (System.IO.File.Exists(coreScript))
|
||||
{
|
||||
System.IO.File.Delete(coreScript);
|
||||
}
|
||||
content[2] = String.Format(content[2], "netcoreapp1.0");
|
||||
System.IO.File.WriteAllLines(coreScript, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
var coreScript = System.IO.Path.Combine(scriptFolder, "SQLTOOLSSERVICE.Core");
|
||||
var sqlToolsServicePath = System.IO.Path.Combine(System.IO.Path.GetFullPath(outputRoot), "{1}", "SQLTOOLSSERVICE");
|
||||
var content = new string[] {
|
||||
"#!/bin/bash",
|
||||
"",
|
||||
$"{{0}} \"{sqlToolsServicePath}{{2}}\" \"$@\""
|
||||
};
|
||||
|
||||
if (System.IO.File.Exists(coreScript))
|
||||
{
|
||||
System.IO.File.Delete(coreScript);
|
||||
}
|
||||
content[2] = String.Format(content[2], "", "netcoreapp1.0", "");
|
||||
System.IO.File.WriteAllLines(coreScript, content);
|
||||
Run("chmod", $"+x \"{coreScript}\"");
|
||||
}
|
||||
}
|
||||
110
scripts/cake-bootstrap.ps1
Normal file
110
scripts/cake-bootstrap.ps1
Normal file
@@ -0,0 +1,110 @@
|
||||
<#
|
||||
|
||||
.SYNOPSIS
|
||||
This is a Powershell script to bootstrap a Cake build.
|
||||
|
||||
.DESCRIPTION
|
||||
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
|
||||
and execute your Cake build script with the parameters you provide.
|
||||
|
||||
.PARAMETER Script
|
||||
The build script to execute.
|
||||
.PARAMETER Target
|
||||
The build script target to run.
|
||||
.PARAMETER Configuration
|
||||
The build configuration to use.
|
||||
.PARAMETER Verbosity
|
||||
Specifies the amount of information to be displayed.
|
||||
Tells Cake to use the latest Roslyn release.
|
||||
.PARAMETER WhatIf
|
||||
Performs a dry run of the build script.
|
||||
No tasks will be executed.
|
||||
.PARAMETER Mono
|
||||
Tells Cake to use the Mono scripting engine.
|
||||
|
||||
.LINK
|
||||
http://cakebuild.net
|
||||
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[string]$Script = "build.cake",
|
||||
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
|
||||
[string]$Verbosity = "Verbose",
|
||||
[Alias("DryRun","Noop")]
|
||||
[switch]$WhatIf,
|
||||
[switch]$Mono,
|
||||
[switch]$SkipToolPackageRestore,
|
||||
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
|
||||
[string[]]$ScriptArgs
|
||||
)
|
||||
|
||||
Write-Host "Preparing to run build script..."
|
||||
|
||||
$PS_SCRIPT_ROOT = split-path -parent $MyInvocation.MyCommand.Definition;
|
||||
$TOOLS_DIR = Join-Path $PSScriptRoot "..\.tools"
|
||||
$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
|
||||
$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe"
|
||||
$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
|
||||
$PACKAGES_CONFIG = Join-Path $PS_SCRIPT_ROOT "packages.config"
|
||||
|
||||
# Should we use mono?
|
||||
$UseMono = "";
|
||||
if($Mono.IsPresent) {
|
||||
Write-Verbose -Message "Using the Mono based scripting engine."
|
||||
$UseMono = "-mono"
|
||||
}
|
||||
|
||||
# Is this a dry run?
|
||||
$UseDryRun = "";
|
||||
if($WhatIf.IsPresent) {
|
||||
$UseDryRun = "-dryrun"
|
||||
}
|
||||
|
||||
# Make sure tools folder exists
|
||||
if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
|
||||
Write-Verbose -Message "Creating tools directory..."
|
||||
New-Item -Path $TOOLS_DIR -Type directory | out-null
|
||||
}
|
||||
|
||||
# Try download NuGet.exe if not exists
|
||||
if (!(Test-Path $NUGET_EXE)) {
|
||||
Write-Verbose -Message "Downloading NuGet.exe..."
|
||||
try {
|
||||
(New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
|
||||
} catch {
|
||||
Throw "Could not download NuGet.exe."
|
||||
}
|
||||
}
|
||||
|
||||
# Save nuget.exe path to environment to be available to child processed
|
||||
$ENV:NUGET_EXE = $NUGET_EXE
|
||||
|
||||
# Restore tools from NuGet?
|
||||
if(-Not $SkipToolPackageRestore.IsPresent)
|
||||
{
|
||||
# Restore packages from NuGet.
|
||||
Push-Location
|
||||
Set-Location $TOOLS_DIR
|
||||
|
||||
Write-Verbose -Message "Restoring tools from NuGet..."
|
||||
$NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install $PACKAGES_CONFIG -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
|
||||
Write-Verbose -Message ($NuGetOutput | out-string)
|
||||
|
||||
Pop-Location
|
||||
if ($LASTEXITCODE -ne 0)
|
||||
{
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
# Make sure that Cake has been installed.
|
||||
if (!(Test-Path $CAKE_EXE)) {
|
||||
Throw "Could not find Cake.exe at $CAKE_EXE"
|
||||
}
|
||||
|
||||
# Start Cake
|
||||
Write-Host "Running build script..."
|
||||
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $ScriptArgs"
|
||||
exit $LASTEXITCODE
|
||||
69
scripts/cake-bootstrap.sh
Normal file
69
scripts/cake-bootstrap.sh
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
###############################################################
|
||||
# This is the Cake bootstrapper script that is responsible for
|
||||
# downloading Cake and all specified tools from NuGet.
|
||||
###############################################################
|
||||
|
||||
# Define directories.
|
||||
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
TOOLS_DIR=$SCRIPT_DIR/../.tools
|
||||
export NUGET_EXE=$TOOLS_DIR/nuget.exe
|
||||
CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe
|
||||
PACKAGES_CONFIG=$SCRIPT_DIR/packages.config
|
||||
|
||||
# Define default arguments.
|
||||
SCRIPT="build.cake"
|
||||
VERBOSITY="verbose"
|
||||
DRYRUN=
|
||||
SHOW_VERSION=false
|
||||
SCRIPT_ARGUMENTS=()
|
||||
|
||||
# Parse arguments.
|
||||
for i in "$@"; do
|
||||
case $1 in
|
||||
-s|--script) SCRIPT="$2"; shift ;;
|
||||
-v|--verbosity) VERBOSITY="$2"; shift ;;
|
||||
-d|--dryrun) DRYRUN="-dryrun" ;;
|
||||
--version) SHOW_VERSION=true ;;
|
||||
--) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
|
||||
*) SCRIPT_ARGUMENTS+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# Make sure the tools folder exist.
|
||||
if [ ! -d "$TOOLS_DIR" ]; then
|
||||
mkdir "$TOOLS_DIR"
|
||||
fi
|
||||
|
||||
# Download NuGet if it does not exist.
|
||||
if [ ! -f "$NUGET_EXE" ]; then
|
||||
echo "Downloading NuGet..."
|
||||
curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/v3.3.0/nuget.exe
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "An error occured while downloading nuget.exe."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Restore tools from NuGet.
|
||||
pushd "$TOOLS_DIR" >/dev/null
|
||||
mono "$NUGET_EXE" install "$PACKAGES_CONFIG" -ExcludeVersion -OutputDirectory "$TOOLS_DIR"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Could not restore NuGet packages."
|
||||
exit 1
|
||||
fi
|
||||
popd >/dev/null
|
||||
|
||||
# Make sure that Cake has been installed.
|
||||
if [ ! -f "$CAKE_EXE" ]; then
|
||||
echo "Could not find Cake.exe at '$CAKE_EXE'."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Start Cake
|
||||
if $SHOW_VERSION; then
|
||||
exec mono "$CAKE_EXE" -version
|
||||
else
|
||||
exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
|
||||
fi
|
||||
5
scripts/packages.config
Normal file
5
scripts/packages.config
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Cake" version="0.10.1" />
|
||||
<package id="Newtonsoft.Json" version="8.0.3" />
|
||||
</packages>
|
||||
204
scripts/runhelpers.cake
Normal file
204
scripts/runhelpers.cake
Normal file
@@ -0,0 +1,204 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Class encompassing the optional settings for running processes.
|
||||
/// </summary>
|
||||
public class RunOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// The working directory of the process.
|
||||
/// </summary>
|
||||
public string WorkingDirectory { get; set; }
|
||||
/// <summary>
|
||||
/// Container logging the StandardOutput content.
|
||||
/// </summary>
|
||||
public IList<string> StandardOutputListing { get; set; }
|
||||
/// <summary>
|
||||
/// Desired maximum time-out for the process
|
||||
/// </summary>
|
||||
public int TimeOut { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for the exit code and state.
|
||||
/// Used to query the result of an execution with method calls.
|
||||
/// </summary>
|
||||
public struct ExitStatus
|
||||
{
|
||||
private int _code;
|
||||
private bool _timeOut;
|
||||
/// <summary>
|
||||
/// Default constructor when the execution finished.
|
||||
/// </summary>
|
||||
/// <param name="code">The exit code</param>
|
||||
public ExitStatus(int code)
|
||||
{
|
||||
this._code = code;
|
||||
this._timeOut = false;
|
||||
}
|
||||
/// <summary>
|
||||
/// Default constructor when the execution potentially timed out.
|
||||
/// </summary>
|
||||
/// <param name="code">The exit code</param>
|
||||
/// <param name="timeOut">True if the execution timed out</param>
|
||||
public ExitStatus(int code, bool timeOut)
|
||||
{
|
||||
this._code = code;
|
||||
this._timeOut = timeOut;
|
||||
}
|
||||
/// <summary>
|
||||
/// Flag signalling that the execution timed out.
|
||||
/// </summary>
|
||||
public bool DidTimeOut { get { return _timeOut; } }
|
||||
/// <summary>
|
||||
/// Implicit conversion from ExitStatus to the exit code.
|
||||
/// </summary>
|
||||
/// <param name="exitStatus">The exit status</param>
|
||||
/// <returns>The exit code</returns>
|
||||
public static implicit operator int(ExitStatus exitStatus)
|
||||
{
|
||||
return exitStatus._code;
|
||||
}
|
||||
/// <summary>
|
||||
/// Trigger Exception for non-zero exit code.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The message to use in the Exception</param>
|
||||
/// <returns>The exit status for further queries</returns>
|
||||
public ExitStatus ExceptionOnError(string errorMessage)
|
||||
{
|
||||
if (this._code != 0)
|
||||
{
|
||||
throw new Exception(errorMessage);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the given executable with the given arguments.
|
||||
/// </summary>
|
||||
/// <param name="exec">Executable to run</param>
|
||||
/// <param name="args">Arguments</param>
|
||||
/// <returns>The exit status for further queries</returns>
|
||||
ExitStatus Run(string exec, string args)
|
||||
{
|
||||
return Run(exec, args, new RunOptions());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the given executable with the given arguments.
|
||||
/// </summary>
|
||||
/// <param name="exec">Executable to run</param>
|
||||
/// <param name="args">Arguments</param>
|
||||
/// <param name="workingDirectory">Working directory</param>
|
||||
/// <returns>The exit status for further queries</returns>
|
||||
ExitStatus Run(string exec, string args, string workingDirectory)
|
||||
{
|
||||
return Run(exec, args,
|
||||
new RunOptions()
|
||||
{
|
||||
WorkingDirectory = workingDirectory
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the given executable with the given arguments.
|
||||
/// </summary>
|
||||
/// <param name="exec">Executable to run</param>
|
||||
/// <param name="args">Arguments</param>
|
||||
/// <param name="runOptions">Optional settings</param>
|
||||
/// <returns>The exit status for further queries</returns>
|
||||
ExitStatus Run(string exec, string args, RunOptions runOptions)
|
||||
{
|
||||
var workingDirectory = runOptions.WorkingDirectory ?? System.IO.Directory.GetCurrentDirectory();
|
||||
var process = System.Diagnostics.Process.Start(
|
||||
new ProcessStartInfo(exec, args)
|
||||
{
|
||||
WorkingDirectory = workingDirectory,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = runOptions.StandardOutputListing != null
|
||||
});
|
||||
if (runOptions.StandardOutputListing != null)
|
||||
{
|
||||
process.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
runOptions.StandardOutputListing.Add(e.Data);
|
||||
}
|
||||
};
|
||||
process.BeginOutputReadLine();
|
||||
}
|
||||
if (runOptions.TimeOut == 0)
|
||||
{
|
||||
process.WaitForExit();
|
||||
return new ExitStatus(process.ExitCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool finished = process.WaitForExit(runOptions.TimeOut);
|
||||
if (finished)
|
||||
{
|
||||
return new ExitStatus(process.ExitCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
KillProcessTree(process);
|
||||
return new ExitStatus(0, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run restore with the given arguments
|
||||
/// </summary>
|
||||
/// <param name="exec">Executable to run</param>
|
||||
/// <param name="args">Arguments</param>
|
||||
/// <param name="runOptions">Optional settings</param>
|
||||
/// <returns>The exit status for further queries</returns>
|
||||
ExitStatus RunRestore(string exec, string args, string workingDirectory)
|
||||
{
|
||||
Information("Restoring packages....");
|
||||
var p = StartAndReturnProcess(exec,
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = args,
|
||||
RedirectStandardOutput = true,
|
||||
WorkingDirectory = workingDirectory
|
||||
});
|
||||
p.WaitForExit();
|
||||
var exitCode = p.GetExitCode();
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Information("Package restore successful!");
|
||||
}
|
||||
else
|
||||
{
|
||||
Error(string.Join("\n", p.GetStandardOutput()));
|
||||
}
|
||||
return new ExitStatus(exitCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kill the given process and all its child processes.
|
||||
/// </summary>
|
||||
/// <param name="process">Root process</param>
|
||||
public void KillProcessTree(Process process)
|
||||
{
|
||||
// Child processes are not killed on Windows by default
|
||||
// Use TASKKILL to kill the process hierarchy rooted in the process
|
||||
if (IsRunningOnWindows())
|
||||
{
|
||||
StartProcess($"TASKKILL",
|
||||
new ProcessSettings
|
||||
{
|
||||
Arguments = $"/PID {process.Id} /T /F",
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
43
sqltoolsservice.sln
Normal file
43
sqltoolsservice.sln
Normal file
@@ -0,0 +1,43 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.25420.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2BBD7364-054F-4693-97CD-1C395E3E84A9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{32DC973E-9EEA-4694-B1C2-B031167AB945}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitignore = .gitignore
|
||||
global.json = global.json
|
||||
nuget.config = nuget.config
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.SqlTools.ServiceLayer", "src\Microsoft.SqlTools.ServiceLayer\Microsoft.SqlTools.ServiceLayer.xproj", "{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.SqlTools.ServiceLayer.Test", "test\Microsoft.SqlTools.ServiceLayer.Test\Microsoft.SqlTools.ServiceLayer.Test.xproj", "{2D771D16-9D85-4053-9F79-E2034737DEEF}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2D771D16-9D85-4053-9F79-E2034737DEEF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{0D61DC2B-DA66-441D-B9D0-F76C98F780F9} = {2BBD7364-054F-4693-97CD-1C395E3E84A9}
|
||||
{2D771D16-9D85-4053-9F79-E2034737DEEF} = {AB9CA2B8-6F70-431C-8A1D-67479D8A7BE4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Data.Common;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// Information pertaining to a unique connection instance.
|
||||
/// </summary>
|
||||
public class ConnectionInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public ConnectionInfo(ISqlConnectionFactory factory, string ownerUri, ConnectionDetails details)
|
||||
{
|
||||
Factory = factory;
|
||||
OwnerUri = ownerUri;
|
||||
ConnectionDetails = details;
|
||||
ConnectionId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unique Id, helpful to identify a connection info object
|
||||
/// </summary>
|
||||
public Guid ConnectionId { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// URI identifying the owner/user of the connection. Could be a file, service, resource, etc.
|
||||
/// </summary>
|
||||
public string OwnerUri { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Factory used for creating the SQL connection associated with the connection info.
|
||||
/// </summary>
|
||||
public ISqlConnectionFactory Factory {get; private set;}
|
||||
|
||||
/// <summary>
|
||||
/// Properties used for creating/opening the SQL connection.
|
||||
/// </summary>
|
||||
public ConnectionDetails ConnectionDetails { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The connection to the SQL database that commands will be run against.
|
||||
/// </summary>
|
||||
public DbConnection SqlConnection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// Main class for the Connection Management services
|
||||
/// </summary>
|
||||
public class ConnectionService
|
||||
{
|
||||
/// <summary>
|
||||
/// Singleton service instance
|
||||
/// </summary>
|
||||
private static Lazy<ConnectionService> instance
|
||||
= new Lazy<ConnectionService>(() => new ConnectionService());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton service instance
|
||||
/// </summary>
|
||||
public static ConnectionService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return instance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The SQL connection factory object
|
||||
/// </summary>
|
||||
private ISqlConnectionFactory connectionFactory;
|
||||
|
||||
private Dictionary<string, ConnectionInfo> ownerToConnectionMap = new Dictionary<string, ConnectionInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// Service host object for sending/receiving requests/events.
|
||||
/// Internal for testing purposes.
|
||||
/// </summary>
|
||||
internal IProtocolEndpoint ServiceHost
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor is private since it's a singleton class
|
||||
/// </summary>
|
||||
private ConnectionService()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback for onconnection handler
|
||||
/// </summary>
|
||||
/// <param name="sqlConnection"></param>
|
||||
public delegate Task OnConnectionHandler(ConnectionInfo info);
|
||||
|
||||
/// <summary>
|
||||
// Callback for ondisconnect handler
|
||||
/// </summary>
|
||||
public delegate Task OnDisconnectHandler(ConnectionSummary summary);
|
||||
|
||||
/// <summary>
|
||||
/// List of onconnection handlers
|
||||
/// </summary>
|
||||
private readonly List<OnConnectionHandler> onConnectionActivities = new List<OnConnectionHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// List of ondisconnect handlers
|
||||
/// </summary>
|
||||
private readonly List<OnDisconnectHandler> onDisconnectActivities = new List<OnDisconnectHandler>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the SQL connection factory instance
|
||||
/// </summary>
|
||||
public ISqlConnectionFactory ConnectionFactory
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.connectionFactory == null)
|
||||
{
|
||||
this.connectionFactory = new SqlConnectionFactory();
|
||||
}
|
||||
return this.connectionFactory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test constructor that injects dependency interfaces
|
||||
/// </summary>
|
||||
/// <param name="testFactory"></param>
|
||||
public ConnectionService(ISqlConnectionFactory testFactory)
|
||||
{
|
||||
this.connectionFactory = testFactory;
|
||||
}
|
||||
|
||||
// Attempts to link a URI to an actively used connection for this URI
|
||||
public bool TryFindConnection(string ownerUri, out ConnectionInfo connectionInfo)
|
||||
{
|
||||
return this.ownerToConnectionMap.TryGetValue(ownerUri, out connectionInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a connection with the specified connection details
|
||||
/// </summary>
|
||||
/// <param name="connectionParams"></param>
|
||||
public ConnectResponse Connect(ConnectParams connectionParams)
|
||||
{
|
||||
// Validate parameters
|
||||
string paramValidationErrorMessage;
|
||||
if (connectionParams == null)
|
||||
{
|
||||
return new ConnectResponse()
|
||||
{
|
||||
Messages = "Error: Connection parameters cannot be null."
|
||||
};
|
||||
}
|
||||
else if (!connectionParams.IsValid(out paramValidationErrorMessage))
|
||||
{
|
||||
return new ConnectResponse()
|
||||
{
|
||||
Messages = paramValidationErrorMessage
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve if it is an existing connection
|
||||
// Disconnect active connection if the URI is already connected
|
||||
ConnectionInfo connectionInfo;
|
||||
if (ownerToConnectionMap.TryGetValue(connectionParams.OwnerUri, out connectionInfo) )
|
||||
{
|
||||
var disconnectParams = new DisconnectParams()
|
||||
{
|
||||
OwnerUri = connectionParams.OwnerUri
|
||||
};
|
||||
Disconnect(disconnectParams);
|
||||
}
|
||||
connectionInfo = new ConnectionInfo(ConnectionFactory, connectionParams.OwnerUri, connectionParams.Connection);
|
||||
|
||||
// try to connect
|
||||
var response = new ConnectResponse();
|
||||
try
|
||||
{
|
||||
// build the connection string from the input parameters
|
||||
string connectionString = ConnectionService.BuildConnectionString(connectionInfo.ConnectionDetails);
|
||||
|
||||
// create a sql connection instance
|
||||
connectionInfo.SqlConnection = connectionInfo.Factory.CreateSqlConnection(connectionString);
|
||||
connectionInfo.SqlConnection.Open();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
response.Messages = ex.ToString();
|
||||
return response;
|
||||
}
|
||||
|
||||
ownerToConnectionMap[connectionParams.OwnerUri] = connectionInfo;
|
||||
|
||||
// invoke callback notifications
|
||||
foreach (var activity in this.onConnectionActivities)
|
||||
{
|
||||
activity(connectionInfo);
|
||||
}
|
||||
|
||||
// return the connection result
|
||||
response.ConnectionId = connectionInfo.ConnectionId.ToString();
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close a connection with the specified connection details.
|
||||
/// </summary>
|
||||
public bool Disconnect(DisconnectParams disconnectParams)
|
||||
{
|
||||
// Validate parameters
|
||||
if (disconnectParams == null || string.IsNullOrEmpty(disconnectParams.OwnerUri))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Lookup the connection owned by the URI
|
||||
ConnectionInfo info;
|
||||
if (!ownerToConnectionMap.TryGetValue(disconnectParams.OwnerUri, out info))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Close the connection
|
||||
info.SqlConnection.Close();
|
||||
|
||||
// Remove URI mapping
|
||||
ownerToConnectionMap.Remove(disconnectParams.OwnerUri);
|
||||
|
||||
// Invoke callback notifications
|
||||
foreach (var activity in this.onDisconnectActivities)
|
||||
{
|
||||
activity(info.ConnectionDetails);
|
||||
}
|
||||
|
||||
// Success
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List all databases on the server specified
|
||||
/// </summary>
|
||||
public ListDatabasesResponse ListDatabases(ListDatabasesParams listDatabasesParams)
|
||||
{
|
||||
// Verify parameters
|
||||
var owner = listDatabasesParams.OwnerUri;
|
||||
if (string.IsNullOrEmpty(owner))
|
||||
{
|
||||
throw new ArgumentException("OwnerUri cannot be null or empty");
|
||||
}
|
||||
|
||||
// Use the existing connection as a base for the search
|
||||
ConnectionInfo info;
|
||||
if (!TryFindConnection(owner, out info))
|
||||
{
|
||||
throw new Exception("Specified OwnerUri \"" + owner + "\" does not have an existing connection");
|
||||
}
|
||||
ConnectionDetails connectionDetails = info.ConnectionDetails.Clone();
|
||||
|
||||
// Connect to master and query sys.databases
|
||||
connectionDetails.DatabaseName = "master";
|
||||
var connection = this.ConnectionFactory.CreateSqlConnection(BuildConnectionString(connectionDetails));
|
||||
connection.Open();
|
||||
|
||||
DbCommand command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT name FROM sys.databases";
|
||||
command.CommandTimeout = 15;
|
||||
command.CommandType = CommandType.Text;
|
||||
var reader = command.ExecuteReader();
|
||||
|
||||
List<string> results = new List<string>();
|
||||
while (reader.Read())
|
||||
{
|
||||
results.Add(reader[0].ToString());
|
||||
}
|
||||
|
||||
connection.Close();
|
||||
|
||||
ListDatabasesResponse response = new ListDatabasesResponse();
|
||||
response.DatabaseNames = results.ToArray();
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void InitializeService(IProtocolEndpoint serviceHost)
|
||||
{
|
||||
this.ServiceHost = serviceHost;
|
||||
|
||||
// Register request and event handlers with the Service Host
|
||||
serviceHost.SetRequestHandler(ConnectionRequest.Type, HandleConnectRequest);
|
||||
serviceHost.SetRequestHandler(DisconnectRequest.Type, HandleDisconnectRequest);
|
||||
serviceHost.SetRequestHandler(ListDatabasesRequest.Type, HandleListDatabasesRequest);
|
||||
|
||||
// Register the configuration update handler
|
||||
WorkspaceService<SqlToolsSettings>.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new method to be called when the onconnection request is submitted
|
||||
/// </summary>
|
||||
/// <param name="activity"></param>
|
||||
public void RegisterOnConnectionTask(OnConnectionHandler activity)
|
||||
{
|
||||
onConnectionActivities.Add(activity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new method to be called when the ondisconnect request is submitted
|
||||
/// </summary>
|
||||
public void RegisterOnDisconnectTask(OnDisconnectHandler activity)
|
||||
{
|
||||
onDisconnectActivities.Add(activity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle new connection requests
|
||||
/// </summary>
|
||||
/// <param name="connectionDetails"></param>
|
||||
/// <param name="requestContext"></param>
|
||||
/// <returns></returns>
|
||||
protected async Task HandleConnectRequest(
|
||||
ConnectParams connectParams,
|
||||
RequestContext<ConnectResponse> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleConnectRequest");
|
||||
|
||||
try
|
||||
{
|
||||
// open connection base on request details
|
||||
ConnectResponse result = ConnectionService.Instance.Connect(connectParams);
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
await requestContext.SendError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle disconnect requests
|
||||
/// </summary>
|
||||
protected async Task HandleDisconnectRequest(
|
||||
DisconnectParams disconnectParams,
|
||||
RequestContext<bool> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDisconnectRequest");
|
||||
|
||||
try
|
||||
{
|
||||
bool result = ConnectionService.Instance.Disconnect(disconnectParams);
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
await requestContext.SendError(ex.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle requests to list databases on the current server
|
||||
/// </summary>
|
||||
protected async Task HandleListDatabasesRequest(
|
||||
ListDatabasesParams listDatabasesParams,
|
||||
RequestContext<ListDatabasesResponse> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "ListDatabasesRequest");
|
||||
|
||||
try
|
||||
{
|
||||
ListDatabasesResponse result = ConnectionService.Instance.ListDatabases(listDatabasesParams);
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
await requestContext.SendError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public Task HandleDidChangeConfigurationNotification(
|
||||
SqlToolsSettings newSettings,
|
||||
SqlToolsSettings oldSettings,
|
||||
EventContext eventContext)
|
||||
{
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a connection string from a connection details instance
|
||||
/// </summary>
|
||||
/// <param name="connectionDetails"></param>
|
||||
public static string BuildConnectionString(ConnectionDetails connectionDetails)
|
||||
{
|
||||
SqlConnectionStringBuilder connectionBuilder = new SqlConnectionStringBuilder();
|
||||
connectionBuilder["Data Source"] = connectionDetails.ServerName;
|
||||
connectionBuilder["User Id"] = connectionDetails.UserName;
|
||||
connectionBuilder["Password"] = connectionDetails.Password;
|
||||
|
||||
// Check for any optional parameters
|
||||
if (!string.IsNullOrEmpty(connectionDetails.DatabaseName))
|
||||
{
|
||||
connectionBuilder["Initial Catalog"] = connectionDetails.DatabaseName;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.AuthenticationType))
|
||||
{
|
||||
switch(connectionDetails.AuthenticationType)
|
||||
{
|
||||
case "Integrated":
|
||||
connectionBuilder.IntegratedSecurity = true;
|
||||
break;
|
||||
case "SqlLogin":
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(string.Format("Invalid value \"{0}\" for AuthenticationType. Valid values are \"Integrated\" and \"SqlLogin\".", connectionDetails.AuthenticationType));
|
||||
}
|
||||
}
|
||||
if (connectionDetails.Encrypt.HasValue)
|
||||
{
|
||||
connectionBuilder.Encrypt = connectionDetails.Encrypt.Value;
|
||||
}
|
||||
if (connectionDetails.TrustServerCertificate.HasValue)
|
||||
{
|
||||
connectionBuilder.TrustServerCertificate = connectionDetails.TrustServerCertificate.Value;
|
||||
}
|
||||
if (connectionDetails.PersistSecurityInfo.HasValue)
|
||||
{
|
||||
connectionBuilder.PersistSecurityInfo = connectionDetails.PersistSecurityInfo.Value;
|
||||
}
|
||||
if (connectionDetails.ConnectTimeout.HasValue)
|
||||
{
|
||||
connectionBuilder.ConnectTimeout = connectionDetails.ConnectTimeout.Value;
|
||||
}
|
||||
if (connectionDetails.ConnectRetryCount.HasValue)
|
||||
{
|
||||
connectionBuilder.ConnectRetryCount = connectionDetails.ConnectRetryCount.Value;
|
||||
}
|
||||
if (connectionDetails.ConnectRetryInterval.HasValue)
|
||||
{
|
||||
connectionBuilder.ConnectRetryInterval = connectionDetails.ConnectRetryInterval.Value;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.ApplicationName))
|
||||
{
|
||||
connectionBuilder.ApplicationName = connectionDetails.ApplicationName;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.WorkstationId))
|
||||
{
|
||||
connectionBuilder.WorkstationID = connectionDetails.WorkstationId;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.ApplicationIntent))
|
||||
{
|
||||
ApplicationIntent intent;
|
||||
switch (connectionDetails.ApplicationIntent)
|
||||
{
|
||||
case "ReadOnly":
|
||||
intent = ApplicationIntent.ReadOnly;
|
||||
break;
|
||||
case "ReadWrite":
|
||||
intent = ApplicationIntent.ReadWrite;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(string.Format("Invalid value \"{0}\" for ApplicationIntent. Valid values are \"ReadWrite\" and \"ReadOnly\".", connectionDetails.ApplicationIntent));
|
||||
}
|
||||
connectionBuilder.ApplicationIntent = intent;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.CurrentLanguage))
|
||||
{
|
||||
connectionBuilder.CurrentLanguage = connectionDetails.CurrentLanguage;
|
||||
}
|
||||
if (connectionDetails.Pooling.HasValue)
|
||||
{
|
||||
connectionBuilder.Pooling = connectionDetails.Pooling.Value;
|
||||
}
|
||||
if (connectionDetails.MaxPoolSize.HasValue)
|
||||
{
|
||||
connectionBuilder.MaxPoolSize = connectionDetails.MaxPoolSize.Value;
|
||||
}
|
||||
if (connectionDetails.MinPoolSize.HasValue)
|
||||
{
|
||||
connectionBuilder.MinPoolSize = connectionDetails.MinPoolSize.Value;
|
||||
}
|
||||
if (connectionDetails.LoadBalanceTimeout.HasValue)
|
||||
{
|
||||
connectionBuilder.LoadBalanceTimeout = connectionDetails.LoadBalanceTimeout.Value;
|
||||
}
|
||||
if (connectionDetails.Replication.HasValue)
|
||||
{
|
||||
connectionBuilder.Replication = connectionDetails.Replication.Value;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.AttachDbFilename))
|
||||
{
|
||||
connectionBuilder.AttachDBFilename = connectionDetails.AttachDbFilename;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.FailoverPartner))
|
||||
{
|
||||
connectionBuilder.FailoverPartner = connectionDetails.FailoverPartner;
|
||||
}
|
||||
if (connectionDetails.MultiSubnetFailover.HasValue)
|
||||
{
|
||||
connectionBuilder.MultiSubnetFailover = connectionDetails.MultiSubnetFailover.Value;
|
||||
}
|
||||
if (connectionDetails.MultipleActiveResultSets.HasValue)
|
||||
{
|
||||
connectionBuilder.MultipleActiveResultSets = connectionDetails.MultipleActiveResultSets.Value;
|
||||
}
|
||||
if (connectionDetails.PacketSize.HasValue)
|
||||
{
|
||||
connectionBuilder.PacketSize = connectionDetails.PacketSize.Value;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(connectionDetails.TypeSystemVersion))
|
||||
{
|
||||
connectionBuilder.TypeSystemVersion = connectionDetails.TypeSystemVersion;
|
||||
}
|
||||
|
||||
return connectionBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the database context of a connection.
|
||||
/// </summary>
|
||||
/// <param name="ownerUri">URI of the owner of the connection</param>
|
||||
/// <param name="newDatabaseName">Name of the database to change the connection to</param>
|
||||
public void ChangeConnectionDatabaseContext(string ownerUri, string newDatabaseName)
|
||||
{
|
||||
ConnectionInfo info;
|
||||
if (TryFindConnection(ownerUri, out info))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (info.SqlConnection.State == ConnectionState.Open)
|
||||
{
|
||||
info.SqlConnection.ChangeDatabase(newDatabaseName);
|
||||
}
|
||||
info.ConnectionDetails.DatabaseName = newDatabaseName;
|
||||
|
||||
// Fire a connection changed event
|
||||
ConnectionChangedParams parameters = new ConnectionChangedParams();
|
||||
ConnectionSummary summary = (ConnectionSummary)(info.ConnectionDetails);
|
||||
parameters.Connection = summary.Clone();
|
||||
parameters.OwnerUri = ownerUri;
|
||||
ServiceHost.SendEvent(ConnectionChangedNotification.Type, parameters);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(
|
||||
LogLevel.Error,
|
||||
string.Format(
|
||||
"Exception caught while trying to change database context to [{0}] for OwnerUri [{1}]. Exception:{2}",
|
||||
newDatabaseName,
|
||||
ownerUri,
|
||||
e.ToString())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the Connect Request.
|
||||
/// </summary>
|
||||
public class ConnectParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
|
||||
/// or a virtual file representing an object in a database.
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
/// <summary>
|
||||
/// Contains the required parameters to initialize a connection to a database.
|
||||
/// A connection will identified by its server name, database name and user name.
|
||||
/// This may be changed in the future to support multiple connections with different
|
||||
/// connection properties to the same database.
|
||||
/// </summary>
|
||||
public ConnectionDetails Connection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods to ConnectParams
|
||||
/// </summary>
|
||||
public static class ConnectParamsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that the fields in ConnectParams are all valid
|
||||
/// </summary>
|
||||
public static bool IsValid(this ConnectParams parameters, out string errorMessage)
|
||||
{
|
||||
errorMessage = string.Empty;
|
||||
if (string.IsNullOrEmpty(parameters.OwnerUri))
|
||||
{
|
||||
errorMessage = "Error: OwnerUri cannot be null or empty.";
|
||||
}
|
||||
else if (parameters.Connection == null)
|
||||
{
|
||||
errorMessage = "Error: Connection details object cannot be null.";
|
||||
}
|
||||
else if (string.IsNullOrEmpty(parameters.Connection.ServerName))
|
||||
{
|
||||
errorMessage = "Error: ServerName cannot be null or empty.";
|
||||
}
|
||||
else if (string.IsNullOrEmpty(parameters.Connection.AuthenticationType) || parameters.Connection.AuthenticationType == "SqlLogin")
|
||||
{
|
||||
// For SqlLogin, username/password cannot be empty
|
||||
if (string.IsNullOrEmpty(parameters.Connection.UserName))
|
||||
{
|
||||
errorMessage = "Error: UserName cannot be null or empty when using SqlLogin authentication.";
|
||||
}
|
||||
else if( string.IsNullOrEmpty(parameters.Connection.Password))
|
||||
{
|
||||
errorMessage = "Error: Password cannot be null or empty when using SqlLogin authentication.";
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Message format for the connection result response
|
||||
/// </summary>
|
||||
public class ConnectResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// A GUID representing a unique connection ID
|
||||
/// </summary>
|
||||
public string ConnectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets any connection error messages
|
||||
/// </summary>
|
||||
public string Messages { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// ConnectionChanged notification mapping entry
|
||||
/// </summary>
|
||||
public class ConnectionChangedNotification
|
||||
{
|
||||
public static readonly
|
||||
EventType<ConnectionChangedParams> Type =
|
||||
EventType<ConnectionChangedParams>.Create("connection/connectionchanged");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the ConnectionChanged Notification.
|
||||
/// </summary>
|
||||
public class ConnectionChangedParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
|
||||
/// or a virtual file representing an object in a database.
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
/// <summary>
|
||||
/// Contains the high-level properties about the connection, for display to the user.
|
||||
/// </summary>
|
||||
public ConnectionSummary Connection { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Message format for the initial connection request
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this contract is ever changed, be sure to update ConnectionDetailsExtensions methods.
|
||||
/// </remarks>
|
||||
public class ConnectionDetails : ConnectionSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the connection password
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the authentication to use.
|
||||
/// </summary>
|
||||
public string AuthenticationType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a Boolean value that indicates whether SQL Server uses SSL encryption for all data sent between the client and server if the server has a certificate installed.
|
||||
/// </summary>
|
||||
public bool? Encrypt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether the channel will be encrypted while bypassing walking the certificate chain to validate trust.
|
||||
/// </summary>
|
||||
public bool? TrustServerCertificate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a Boolean value that indicates if security-sensitive information, such as the password, is not returned as part of the connection if the connection is open or has ever been in an open state.
|
||||
/// </summary>
|
||||
public bool? PersistSecurityInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the length of time (in seconds) to wait for a connection to the server before terminating the attempt and generating an error.
|
||||
/// </summary>
|
||||
public int? ConnectTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of reconnections attempted after identifying that there was an idle connection failure.
|
||||
/// </summary>
|
||||
public int? ConnectRetryCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount of time (in seconds) between each reconnection attempt after identifying that there was an idle connection failure.
|
||||
/// </summary>
|
||||
public int? ConnectRetryInterval { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the application associated with the connection string.
|
||||
/// </summary>
|
||||
public string ApplicationName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the workstation connecting to SQL Server.
|
||||
/// </summary>
|
||||
public string WorkstationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Declares the application workload type when connecting to a database in an SQL Server Availability Group.
|
||||
/// </summary>
|
||||
public string ApplicationIntent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SQL Server Language record name.
|
||||
/// </summary>
|
||||
public string CurrentLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a Boolean value that indicates whether the connection will be pooled or explicitly opened every time that the connection is requested.
|
||||
/// </summary>
|
||||
public bool? Pooling { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of connections allowed in the connection pool for this specific connection string.
|
||||
/// </summary>
|
||||
public int? MaxPoolSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum number of connections allowed in the connection pool for this specific connection string.
|
||||
/// </summary>
|
||||
public int? MinPoolSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum time, in seconds, for the connection to live in the connection pool before being destroyed.
|
||||
/// </summary>
|
||||
public int? LoadBalanceTimeout { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a Boolean value that indicates whether replication is supported using the connection.
|
||||
/// </summary>
|
||||
public bool? Replication { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a string that contains the name of the primary data file. This includes the full path name of an attachable database.
|
||||
/// </summary>
|
||||
public string AttachDbFilename { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name or address of the partner server to connect to if the primary server is down.
|
||||
/// </summary>
|
||||
public string FailoverPartner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If your application is connecting to an AlwaysOn availability group (AG) on different subnets, setting MultiSubnetFailover=true provides faster detection of and connection to the (currently) active server.
|
||||
/// </summary>
|
||||
public bool? MultiSubnetFailover { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, an application can maintain multiple active result sets (MARS).
|
||||
/// </summary>
|
||||
public bool? MultipleActiveResultSets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size in bytes of the network packets used to communicate with an instance of SQL Server.
|
||||
/// </summary>
|
||||
public int? PacketSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a string value that indicates the type system the application expects.
|
||||
/// </summary>
|
||||
public string TypeSystemVersion { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the ConnectionDetails contract class
|
||||
/// </summary>
|
||||
public static class ConnectionDetailsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a copy of a connection details object.
|
||||
/// </summary>
|
||||
public static ConnectionDetails Clone(this ConnectionDetails details)
|
||||
{
|
||||
return new ConnectionDetails()
|
||||
{
|
||||
ServerName = details.ServerName,
|
||||
DatabaseName = details.DatabaseName,
|
||||
UserName = details.UserName,
|
||||
Password = details.Password,
|
||||
AuthenticationType = details.AuthenticationType,
|
||||
Encrypt = details.Encrypt,
|
||||
TrustServerCertificate = details.TrustServerCertificate,
|
||||
PersistSecurityInfo = details.PersistSecurityInfo,
|
||||
ConnectTimeout = details.ConnectTimeout,
|
||||
ConnectRetryCount = details.ConnectRetryCount,
|
||||
ConnectRetryInterval = details.ConnectRetryInterval,
|
||||
ApplicationName = details.ApplicationName,
|
||||
WorkstationId = details.WorkstationId,
|
||||
ApplicationIntent = details.ApplicationIntent,
|
||||
CurrentLanguage = details.CurrentLanguage,
|
||||
Pooling = details.Pooling,
|
||||
MaxPoolSize = details.MaxPoolSize,
|
||||
MinPoolSize = details.MinPoolSize,
|
||||
LoadBalanceTimeout = details.LoadBalanceTimeout,
|
||||
Replication = details.Replication,
|
||||
AttachDbFilename = details.AttachDbFilename,
|
||||
FailoverPartner = details.FailoverPartner,
|
||||
MultiSubnetFailover = details.MultiSubnetFailover,
|
||||
MultipleActiveResultSets = details.MultipleActiveResultSets,
|
||||
PacketSize = details.PacketSize,
|
||||
TypeSystemVersion = details.TypeSystemVersion
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Connect request mapping entry
|
||||
/// </summary>
|
||||
public class ConnectionRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<ConnectParams, ConnectResponse> Type =
|
||||
RequestType<ConnectParams, ConnectResponse>.Create("connection/connect");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides high level information about a connection.
|
||||
/// </summary>
|
||||
public class ConnectionSummary
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the connection server name
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection database name
|
||||
/// </summary>
|
||||
public string DatabaseName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the connection user name
|
||||
/// </summary>
|
||||
public string UserName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Treats connections as the same if their server, db and usernames all match
|
||||
/// </summary>
|
||||
public class ConnectionSummaryComparer : IEqualityComparer<ConnectionSummary>
|
||||
{
|
||||
public bool Equals(ConnectionSummary x, ConnectionSummary y)
|
||||
{
|
||||
if(x == y) { return true; }
|
||||
else if(x != null)
|
||||
{
|
||||
if(y == null) { return false; }
|
||||
|
||||
// Compare server, db, username. Note: server is case-insensitive in the driver
|
||||
return string.Compare(x.ServerName, y.ServerName, StringComparison.OrdinalIgnoreCase) == 0
|
||||
&& string.Compare(x.DatabaseName, y.DatabaseName, StringComparison.Ordinal) == 0
|
||||
&& string.Compare(x.UserName, y.UserName, StringComparison.Ordinal) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetHashCode(ConnectionSummary obj)
|
||||
{
|
||||
int hashcode = 31;
|
||||
if(obj != null)
|
||||
{
|
||||
if(obj.ServerName != null)
|
||||
{
|
||||
hashcode ^= obj.ServerName.GetHashCode();
|
||||
}
|
||||
if (obj.DatabaseName != null)
|
||||
{
|
||||
hashcode ^= obj.DatabaseName.GetHashCode();
|
||||
}
|
||||
if (obj.UserName != null)
|
||||
{
|
||||
hashcode ^= obj.UserName.GetHashCode();
|
||||
}
|
||||
}
|
||||
return hashcode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods to ConnectionSummary
|
||||
/// </summary>
|
||||
public static class ConnectionSummaryExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a copy of a ConnectionSummary object
|
||||
/// </summary>
|
||||
public static ConnectionSummary Clone(this ConnectionSummary summary)
|
||||
{
|
||||
return new ConnectionSummary()
|
||||
{
|
||||
ServerName = summary.ServerName,
|
||||
DatabaseName = summary.DatabaseName,
|
||||
UserName = summary.UserName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the Disconnect Request.
|
||||
/// </summary>
|
||||
public class DisconnectParams
|
||||
{
|
||||
/// <summary>
|
||||
/// A URI identifying the owner of the connection. This will most commonly be a file in the workspace
|
||||
/// or a virtual file representing an object in a database.
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Disconnect request mapping entry
|
||||
/// </summary>
|
||||
public class DisconnectRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<DisconnectParams, bool> Type =
|
||||
RequestType<DisconnectParams, bool>.Create("connection/disconnect");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters for the List Databases Request.
|
||||
/// </summary>
|
||||
public class ListDatabasesParams
|
||||
{
|
||||
/// <summary>
|
||||
/// URI of the owner of the connection requesting the list of databases.
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// List databases request mapping entry
|
||||
/// </summary>
|
||||
public class ListDatabasesRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<ListDatabasesParams, ListDatabasesResponse> Type =
|
||||
RequestType<ListDatabasesParams, ListDatabasesResponse>.Create("connection/listdatabases");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Message format for the list databases response
|
||||
/// </summary>
|
||||
public class ListDatabasesResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of database names.
|
||||
/// </summary>
|
||||
public string[] DatabaseNames { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Data.Common;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for the SQL Connection factory
|
||||
/// </summary>
|
||||
public interface ISqlConnectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a new SQL Connection object
|
||||
/// </summary>
|
||||
DbConnection CreateSqlConnection(string connectionString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Connection
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory class to create SqlClientConnections
|
||||
/// The purpose of the factory is to make it easier to mock out the database
|
||||
/// in 'offline' unit test scenarios.
|
||||
/// </summary>
|
||||
public class SqlConnectionFactory : ISqlConnectionFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new SqlConnection object
|
||||
/// </summary>
|
||||
public DbConnection CreateSqlConnection(string connectionString)
|
||||
{
|
||||
return new SqlConnection(connectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// A Credential containing information needed to log into a resource. This is primarily
|
||||
/// defined as a unique <see cref="CredentialId"/> with an associated <see cref="Password"/>
|
||||
/// that's linked to it.
|
||||
/// </summary>
|
||||
public class Credential
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique ID to identify the credential being saved.
|
||||
/// </summary>
|
||||
public string CredentialId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Password stored for this credential.
|
||||
/// </summary>
|
||||
public string Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default Constructor
|
||||
/// </summary>
|
||||
public Credential()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor used when only <paramref name="credentialId"/> is known
|
||||
/// </summary>
|
||||
/// <param name="credentialId"><see cref="CredentialId"/></param>
|
||||
public Credential(string credentialId)
|
||||
: this(credentialId, null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="credentialId"><see cref="CredentialId"/></param>
|
||||
/// <param name="password"><see cref="Password"/></param>
|
||||
public Credential(string credentialId, string password)
|
||||
{
|
||||
CredentialId = credentialId;
|
||||
Password = password;
|
||||
}
|
||||
|
||||
internal static Credential Copy(Credential credential)
|
||||
{
|
||||
return new Credential
|
||||
{
|
||||
CredentialId = credential.CredentialId,
|
||||
Password = credential.Password
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the credential has all the properties needed to look up the password
|
||||
/// </summary>
|
||||
public static void ValidateForLookup(Credential credential)
|
||||
{
|
||||
Validate.IsNotNull("credential", credential);
|
||||
Validate.IsNotNullOrEmptyString("credential.CredentialId", credential.CredentialId);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Validates the credential has all the properties needed to save a password
|
||||
/// </summary>
|
||||
public static void ValidateForSave(Credential credential)
|
||||
{
|
||||
ValidateForLookup(credential);
|
||||
Validate.IsNotNullOrEmptyString("credential.Password", credential.Password);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read Credential request mapping entry. Expects a Credential with CredentialId,
|
||||
/// and responds with the <see cref="Credential.Password"/> filled in if found
|
||||
/// </summary>
|
||||
public class ReadCredentialRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Request definition
|
||||
/// </summary>
|
||||
public static readonly
|
||||
RequestType<Credential, Credential> Type =
|
||||
RequestType<Credential, Credential>.Create("credential/read");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save Credential request mapping entry
|
||||
/// </summary>
|
||||
public class SaveCredentialRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Request definition
|
||||
/// </summary>
|
||||
public static readonly
|
||||
RequestType<Credential, bool> Type =
|
||||
RequestType<Credential, bool>.Create("credential/save");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete Credential request mapping entry
|
||||
/// </summary>
|
||||
public class DeleteCredentialRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Request definition
|
||||
/// </summary>
|
||||
public static readonly
|
||||
RequestType<Credential, bool> Type =
|
||||
RequestType<Credential, bool>.Create("credential/delete");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Linux;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.OSX;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Win32;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
/// <summary>
|
||||
/// Service responsible for securing credentials in a platform-neutral manner. This provides
|
||||
/// a generic API for read, save and delete credentials
|
||||
/// </summary>
|
||||
public class CredentialService
|
||||
{
|
||||
internal static string DefaultSecretsFolder = ".sqlsecrets";
|
||||
internal const string DefaultSecretsFile = "sqlsecrets.json";
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Singleton service instance
|
||||
/// </summary>
|
||||
private static Lazy<CredentialService> instance
|
||||
= new Lazy<CredentialService>(() => new CredentialService());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton service instance
|
||||
/// </summary>
|
||||
public static CredentialService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return instance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private ICredentialStore credStore;
|
||||
|
||||
/// <summary>
|
||||
/// Default constructor is private since it's a singleton class
|
||||
/// </summary>
|
||||
private CredentialService()
|
||||
: this(null, new LinuxCredentialStore.StoreConfig()
|
||||
{ CredentialFolder = DefaultSecretsFolder, CredentialFile = DefaultSecretsFile, IsRelativeToUserHomeDir = true})
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing purposes only
|
||||
/// </summary>
|
||||
internal CredentialService(ICredentialStore store, LinuxCredentialStore.StoreConfig config)
|
||||
{
|
||||
this.credStore = store != null ? store : GetStoreForOS(config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing purposes only
|
||||
/// </summary>
|
||||
internal static ICredentialStore GetStoreForOS(LinuxCredentialStore.StoreConfig config)
|
||||
{
|
||||
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new Win32CredentialStore();
|
||||
}
|
||||
else if(RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return new OSXCredentialStore();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return new LinuxCredentialStore(config);
|
||||
}
|
||||
throw new InvalidOperationException("Platform not currently supported");
|
||||
}
|
||||
|
||||
public void InitializeService(IProtocolEndpoint serviceHost)
|
||||
{
|
||||
// Register request and event handlers with the Service Host
|
||||
serviceHost.SetRequestHandler(ReadCredentialRequest.Type, HandleReadCredentialRequest);
|
||||
serviceHost.SetRequestHandler(SaveCredentialRequest.Type, HandleSaveCredentialRequest);
|
||||
serviceHost.SetRequestHandler(DeleteCredentialRequest.Type, HandleDeleteCredentialRequest);
|
||||
}
|
||||
|
||||
public async Task HandleReadCredentialRequest(Credential credential, RequestContext<Credential> requestContext)
|
||||
{
|
||||
Func<Credential> doRead = () =>
|
||||
{
|
||||
return ReadCredential(credential);
|
||||
};
|
||||
await HandleRequest(doRead, requestContext, "HandleReadCredentialRequest");
|
||||
}
|
||||
|
||||
|
||||
private Credential ReadCredential(Credential credential)
|
||||
{
|
||||
Credential.ValidateForLookup(credential);
|
||||
|
||||
Credential result = Credential.Copy(credential);
|
||||
string password;
|
||||
if (credStore.TryGetPassword(credential.CredentialId, out password))
|
||||
{
|
||||
result.Password = password;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task HandleSaveCredentialRequest(Credential credential, RequestContext<bool> requestContext)
|
||||
{
|
||||
Func<bool> doSave = () =>
|
||||
{
|
||||
Credential.ValidateForSave(credential);
|
||||
return credStore.Save(credential);
|
||||
};
|
||||
await HandleRequest(doSave, requestContext, "HandleSaveCredentialRequest");
|
||||
}
|
||||
|
||||
public async Task HandleDeleteCredentialRequest(Credential credential, RequestContext<bool> requestContext)
|
||||
{
|
||||
Func<bool> doDelete = () =>
|
||||
{
|
||||
Credential.ValidateForLookup(credential);
|
||||
return credStore.DeletePassword(credential.CredentialId);
|
||||
};
|
||||
await HandleRequest(doDelete, requestContext, "HandleDeleteCredentialRequest");
|
||||
}
|
||||
|
||||
private async Task HandleRequest<T>(Func<T> handler, RequestContext<T> requestContext, string requestType)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, requestType);
|
||||
|
||||
try
|
||||
{
|
||||
T result = handler();
|
||||
await requestContext.SendResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await requestContext.SendError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ICredentialStore"/> support securely saving and retrieving passwords
|
||||
/// </summary>
|
||||
public interface ICredentialStore
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a Password linked to a given Credential
|
||||
/// </summary>
|
||||
/// <param name="credential">
|
||||
/// A <see cref="Credential"/> to be saved.
|
||||
/// <see cref="Credential.CredentialId"/> and <see cref="Credential.Password"/> are required
|
||||
/// </param>
|
||||
/// <returns>True if successful, false otherwise</returns>
|
||||
bool Save(Credential credential);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Password and sets it into a <see cref="Credential"/> object
|
||||
/// </summary>
|
||||
/// <param name="credentialId">The name of the credential to find the password for. This is required</param>
|
||||
/// <param name="password">Out value</param>
|
||||
/// <returns>true if password was found, false otherwise</returns>
|
||||
bool TryGetPassword(string credentialId, out string password);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a password linked to a given credential
|
||||
/// </summary>
|
||||
/// <param name="credentialId">The name of the credential to find the password for. This is required</param>
|
||||
/// <returns>True if password existed and was deleted, false otherwise</returns>
|
||||
bool DeletePassword(string credentialId);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
internal static class InteropUtils
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length in bytes for a Unicode string, for use in interop where length must be defined
|
||||
/// </summary>
|
||||
public static UInt32 GetLengthInBytes(string value)
|
||||
{
|
||||
|
||||
return Convert.ToUInt32( (value != null ? Encoding.Unicode.GetByteCount(value) : 0) );
|
||||
}
|
||||
|
||||
public static string CopyToString(IntPtr ptr, int length)
|
||||
{
|
||||
if (ptr == IntPtr.Zero || length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
byte[] pwdBytes = new byte[length];
|
||||
Marshal.Copy(ptr, pwdBytes, 0, (int)length);
|
||||
return Encoding.Unicode.GetString(pwdBytes, 0, (int)length);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
|
||||
{
|
||||
/// <summary>
|
||||
/// Simplified class to enable writing a set of credentials to/from disk
|
||||
/// </summary>
|
||||
public class CredentialsWrapper
|
||||
{
|
||||
public List<Credential> Credentials { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
|
||||
{
|
||||
public class FileTokenStorage
|
||||
{
|
||||
private const int OwnerAccessMode = 384; // Permission 0600 - owner read/write, nobody else has access
|
||||
|
||||
private object lockObject = new object();
|
||||
|
||||
private string fileName;
|
||||
|
||||
public FileTokenStorage(string fileName)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("fileName", fileName);
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public void AddEntries(IEnumerable<Credential> newEntries, IEnumerable<Credential> existingEntries)
|
||||
{
|
||||
var allEntries = existingEntries.Concat(newEntries);
|
||||
this.SaveEntries(allEntries);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.SaveEntries(new List<Credential>());
|
||||
}
|
||||
|
||||
public IEnumerable<Credential> LoadEntries()
|
||||
{
|
||||
if(!File.Exists(this.fileName))
|
||||
{
|
||||
return Enumerable.Empty<Credential>();
|
||||
}
|
||||
|
||||
string serializedCreds;
|
||||
lock (lockObject)
|
||||
{
|
||||
serializedCreds = File.ReadAllText(this.fileName);
|
||||
}
|
||||
|
||||
CredentialsWrapper creds = JsonConvert.DeserializeObject<CredentialsWrapper>(serializedCreds, Constants.JsonSerializerSettings);
|
||||
if(creds != null)
|
||||
{
|
||||
return creds.Credentials;
|
||||
}
|
||||
return Enumerable.Empty<Credential>();
|
||||
}
|
||||
|
||||
public void SaveEntries(IEnumerable<Credential> entries)
|
||||
{
|
||||
CredentialsWrapper credentials = new CredentialsWrapper() { Credentials = entries.ToList() };
|
||||
string serializedCreds = JsonConvert.SerializeObject(credentials, Constants.JsonSerializerSettings);
|
||||
|
||||
lock(lockObject)
|
||||
{
|
||||
WriteToFile(this.fileName, serializedCreds);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteToFile(string filePath, string fileContents)
|
||||
{
|
||||
string dir = Path.GetDirectoryName(filePath);
|
||||
if(!Directory.Exists(dir))
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
|
||||
// Overwrite file, then use ChMod to ensure we have
|
||||
File.WriteAllText(filePath, fileContents);
|
||||
// set appropriate permissions so only current user can read/write
|
||||
Interop.Sys.ChMod(filePath, OwnerAccessMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
internal static partial class Interop
|
||||
{
|
||||
/// <summary>Common Unix errno error codes.</summary>
|
||||
internal enum Error
|
||||
{
|
||||
// These values were defined in src/Native/System.Native/fxerrno.h
|
||||
//
|
||||
// They compare against values obtained via Interop.Sys.GetLastError() not Marshal.GetLastWin32Error()
|
||||
// which obtains the raw errno that varies between unixes. The strong typing as an enum is meant to
|
||||
// prevent confusing the two. Casting to or from int is suspect. Use GetLastErrorInfo() if you need to
|
||||
// correlate these to the underlying platform values or obtain the corresponding error message.
|
||||
//
|
||||
|
||||
SUCCESS = 0,
|
||||
|
||||
E2BIG = 0x10001, // Argument list too long.
|
||||
EACCES = 0x10002, // Permission denied.
|
||||
EADDRINUSE = 0x10003, // Address in use.
|
||||
EADDRNOTAVAIL = 0x10004, // Address not available.
|
||||
EAFNOSUPPORT = 0x10005, // Address family not supported.
|
||||
EAGAIN = 0x10006, // Resource unavailable, try again (same value as EWOULDBLOCK),
|
||||
EALREADY = 0x10007, // Connection already in progress.
|
||||
EBADF = 0x10008, // Bad file descriptor.
|
||||
EBADMSG = 0x10009, // Bad message.
|
||||
EBUSY = 0x1000A, // Device or resource busy.
|
||||
ECANCELED = 0x1000B, // Operation canceled.
|
||||
ECHILD = 0x1000C, // No child processes.
|
||||
ECONNABORTED = 0x1000D, // Connection aborted.
|
||||
ECONNREFUSED = 0x1000E, // Connection refused.
|
||||
ECONNRESET = 0x1000F, // Connection reset.
|
||||
EDEADLK = 0x10010, // Resource deadlock would occur.
|
||||
EDESTADDRREQ = 0x10011, // Destination address required.
|
||||
EDOM = 0x10012, // Mathematics argument out of domain of function.
|
||||
EDQUOT = 0x10013, // Reserved.
|
||||
EEXIST = 0x10014, // File exists.
|
||||
EFAULT = 0x10015, // Bad address.
|
||||
EFBIG = 0x10016, // File too large.
|
||||
EHOSTUNREACH = 0x10017, // Host is unreachable.
|
||||
EIDRM = 0x10018, // Identifier removed.
|
||||
EILSEQ = 0x10019, // Illegal byte sequence.
|
||||
EINPROGRESS = 0x1001A, // Operation in progress.
|
||||
EINTR = 0x1001B, // Interrupted function.
|
||||
EINVAL = 0x1001C, // Invalid argument.
|
||||
EIO = 0x1001D, // I/O error.
|
||||
EISCONN = 0x1001E, // Socket is connected.
|
||||
EISDIR = 0x1001F, // Is a directory.
|
||||
ELOOP = 0x10020, // Too many levels of symbolic links.
|
||||
EMFILE = 0x10021, // File descriptor value too large.
|
||||
EMLINK = 0x10022, // Too many links.
|
||||
EMSGSIZE = 0x10023, // Message too large.
|
||||
EMULTIHOP = 0x10024, // Reserved.
|
||||
ENAMETOOLONG = 0x10025, // Filename too long.
|
||||
ENETDOWN = 0x10026, // Network is down.
|
||||
ENETRESET = 0x10027, // Connection aborted by network.
|
||||
ENETUNREACH = 0x10028, // Network unreachable.
|
||||
ENFILE = 0x10029, // Too many files open in system.
|
||||
ENOBUFS = 0x1002A, // No buffer space available.
|
||||
ENODEV = 0x1002C, // No such device.
|
||||
ENOENT = 0x1002D, // No such file or directory.
|
||||
ENOEXEC = 0x1002E, // Executable file format error.
|
||||
ENOLCK = 0x1002F, // No locks available.
|
||||
ENOLINK = 0x10030, // Reserved.
|
||||
ENOMEM = 0x10031, // Not enough space.
|
||||
ENOMSG = 0x10032, // No message of the desired type.
|
||||
ENOPROTOOPT = 0x10033, // Protocol not available.
|
||||
ENOSPC = 0x10034, // No space left on device.
|
||||
ENOSYS = 0x10037, // Function not supported.
|
||||
ENOTCONN = 0x10038, // The socket is not connected.
|
||||
ENOTDIR = 0x10039, // Not a directory or a symbolic link to a directory.
|
||||
ENOTEMPTY = 0x1003A, // Directory not empty.
|
||||
ENOTSOCK = 0x1003C, // Not a socket.
|
||||
ENOTSUP = 0x1003D, // Not supported (same value as EOPNOTSUP).
|
||||
ENOTTY = 0x1003E, // Inappropriate I/O control operation.
|
||||
ENXIO = 0x1003F, // No such device or address.
|
||||
EOVERFLOW = 0x10040, // Value too large to be stored in data type.
|
||||
EPERM = 0x10042, // Operation not permitted.
|
||||
EPIPE = 0x10043, // Broken pipe.
|
||||
EPROTO = 0x10044, // Protocol error.
|
||||
EPROTONOSUPPORT = 0x10045, // Protocol not supported.
|
||||
EPROTOTYPE = 0x10046, // Protocol wrong type for socket.
|
||||
ERANGE = 0x10047, // Result too large.
|
||||
EROFS = 0x10048, // Read-only file system.
|
||||
ESPIPE = 0x10049, // Invalid seek.
|
||||
ESRCH = 0x1004A, // No such process.
|
||||
ESTALE = 0x1004B, // Reserved.
|
||||
ETIMEDOUT = 0x1004D, // Connection timed out.
|
||||
ETXTBSY = 0x1004E, // Text file busy.
|
||||
EXDEV = 0x1004F, // Cross-device link.
|
||||
ESOCKTNOSUPPORT = 0x1005E, // Socket type not supported.
|
||||
EPFNOSUPPORT = 0x10060, // Protocol family not supported.
|
||||
ESHUTDOWN = 0x1006C, // Socket shutdown.
|
||||
EHOSTDOWN = 0x10070, // Host is down.
|
||||
ENODATA = 0x10071, // No data available.
|
||||
|
||||
// POSIX permits these to have the same value and we make them always equal so
|
||||
// that CoreFX cannot introduce a dependency on distinguishing between them that
|
||||
// would not work on all platforms.
|
||||
EOPNOTSUPP = ENOTSUP, // Operation not supported on socket.
|
||||
EWOULDBLOCK = EAGAIN, // Operation would block.
|
||||
}
|
||||
|
||||
|
||||
// Represents a platform-agnostic Error and underlying platform-specific errno
|
||||
internal struct ErrorInfo
|
||||
{
|
||||
private Error _error;
|
||||
private int _rawErrno;
|
||||
|
||||
internal ErrorInfo(int errno)
|
||||
{
|
||||
_error = Interop.Sys.ConvertErrorPlatformToPal(errno);
|
||||
_rawErrno = errno;
|
||||
}
|
||||
|
||||
internal ErrorInfo(Error error)
|
||||
{
|
||||
_error = error;
|
||||
_rawErrno = -1;
|
||||
}
|
||||
|
||||
internal Error Error
|
||||
{
|
||||
get { return _error; }
|
||||
}
|
||||
|
||||
internal int RawErrno
|
||||
{
|
||||
get { return _rawErrno == -1 ? (_rawErrno = Interop.Sys.ConvertErrorPalToPlatform(_error)) : _rawErrno; }
|
||||
}
|
||||
|
||||
internal string GetErrorMessage()
|
||||
{
|
||||
return Interop.Sys.StrError(RawErrno);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format(
|
||||
"RawErrno: {0} Error: {1} GetErrorMessage: {2}", // No localization required; text is member names used for debugging purposes
|
||||
RawErrno, Error, GetErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
internal partial class Sys
|
||||
{
|
||||
internal static Error GetLastError()
|
||||
{
|
||||
return ConvertErrorPlatformToPal(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
internal static ErrorInfo GetLastErrorInfo()
|
||||
{
|
||||
return new ErrorInfo(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
internal static string StrError(int platformErrno)
|
||||
{
|
||||
int maxBufferLength = 1024; // should be long enough for most any UNIX error
|
||||
IntPtr buffer = Marshal.AllocHGlobal(maxBufferLength);
|
||||
try
|
||||
{
|
||||
IntPtr message = StrErrorR(platformErrno, buffer, maxBufferLength);
|
||||
|
||||
if (message == IntPtr.Zero)
|
||||
{
|
||||
// This means the buffer was not large enough, but still contains
|
||||
// as much of the error message as possible and is guaranteed to
|
||||
// be null-terminated. We're not currently resizing/retrying because
|
||||
// maxBufferLength is large enough in practice, but we could do
|
||||
// so here in the future if necessary.
|
||||
message = buffer;
|
||||
}
|
||||
|
||||
string returnMsg = Marshal.PtrToStringAnsi(message);
|
||||
return returnMsg;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Deallocate the buffer we created
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPlatformToPal")]
|
||||
internal static extern Error ConvertErrorPlatformToPal(int platformErrno);
|
||||
|
||||
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_ConvertErrorPalToPlatform")]
|
||||
internal static extern int ConvertErrorPalToPlatform(Error error);
|
||||
|
||||
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_StrErrorR")]
|
||||
private static extern IntPtr StrErrorR(int platformErrno, IntPtr buffer, int bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: extension method can't be nested inside Interop class.
|
||||
internal static class InteropErrorExtensions
|
||||
{
|
||||
// Intended usage is e.g. Interop.Error.EFAIL.Info() for brevity
|
||||
// vs. new Interop.ErrorInfo(Interop.Error.EFAIL) for synthesizing
|
||||
// errors. Errors originated from the system should be obtained
|
||||
// via GetLastErrorInfo(), not GetLastError().Info() as that will
|
||||
// convert twice, which is not only inefficient but also lossy if
|
||||
// we ever encounter a raw errno that no equivalent in the Error
|
||||
// enum.
|
||||
public static Interop.ErrorInfo Info(this Interop.Error error)
|
||||
{
|
||||
return new Interop.ErrorInfo(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Linux
|
||||
{
|
||||
/// <summary>
|
||||
/// Linux implementation of the credential store.
|
||||
///
|
||||
/// <remarks>
|
||||
/// This entire implementation may need to be revised to support encryption of
|
||||
/// passwords and protection of them when loaded into memory.
|
||||
/// </remarks>
|
||||
/// </summary>
|
||||
internal class LinuxCredentialStore : ICredentialStore
|
||||
{
|
||||
internal struct StoreConfig
|
||||
{
|
||||
public string CredentialFolder { get; set; }
|
||||
public string CredentialFile { get; set; }
|
||||
public bool IsRelativeToUserHomeDir { get; set; }
|
||||
}
|
||||
|
||||
private string credentialFolderPath;
|
||||
private string credentialFileName;
|
||||
private FileTokenStorage storage;
|
||||
|
||||
public LinuxCredentialStore(StoreConfig config)
|
||||
{
|
||||
Validate.IsNotNull("config", config);
|
||||
Validate.IsNotNullOrEmptyString("credentialFolder", config.CredentialFolder);
|
||||
Validate.IsNotNullOrEmptyString("credentialFileName", config.CredentialFile);
|
||||
|
||||
this.credentialFolderPath = config.IsRelativeToUserHomeDir ? GetUserScopedDirectory(config.CredentialFolder) : config.CredentialFolder;
|
||||
this.credentialFileName = config.CredentialFile;
|
||||
|
||||
|
||||
string combinedPath = Path.Combine(this.credentialFolderPath, this.credentialFileName);
|
||||
storage = new FileTokenStorage(combinedPath);
|
||||
}
|
||||
|
||||
public bool DeletePassword(string credentialId)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
|
||||
IEnumerable<Credential> creds;
|
||||
if (LoadCredentialsAndFilterById(credentialId, out creds))
|
||||
{
|
||||
storage.SaveEntries(creds);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets filtered credentials with a specific ID filtered out
|
||||
/// </summary>
|
||||
/// <returns>True if the credential to filter was removed, false if it was not found</returns>
|
||||
private bool LoadCredentialsAndFilterById(string idToFilter, out IEnumerable<Credential> creds)
|
||||
{
|
||||
bool didRemove = false;
|
||||
creds = storage.LoadEntries().Where(cred =>
|
||||
{
|
||||
if (IsCredentialMatch(idToFilter, cred))
|
||||
{
|
||||
didRemove = true;
|
||||
return false; // filter this out
|
||||
}
|
||||
return true;
|
||||
}).ToList(); // Call ToList ensures Where clause is executed so didRemove can be evaluated
|
||||
|
||||
return didRemove;
|
||||
}
|
||||
|
||||
private static bool IsCredentialMatch(string credentialId, Credential cred)
|
||||
{
|
||||
return string.Equals(credentialId, cred.CredentialId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public bool TryGetPassword(string credentialId, out string password)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
|
||||
Credential cred = storage.LoadEntries().FirstOrDefault(c => IsCredentialMatch(credentialId, c));
|
||||
if (cred != null)
|
||||
{
|
||||
password = cred.Password;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Else this was not found in the list
|
||||
password = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Save(Credential credential)
|
||||
{
|
||||
Credential.ValidateForSave(credential);
|
||||
|
||||
// Load the credentials, removing the existing Cred for this
|
||||
IEnumerable<Credential> creds;
|
||||
LoadCredentialsAndFilterById(credential.CredentialId, out creds);
|
||||
storage.SaveEntries(creds.Append(credential));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing purposes only
|
||||
/// </summary>
|
||||
internal string CredentialFolderPath
|
||||
{
|
||||
get { return this.credentialFolderPath; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Concatenates a directory to the user home directory's path
|
||||
/// </summary>
|
||||
internal static string GetUserScopedDirectory(string userPath)
|
||||
{
|
||||
string homeDir = GetHomeDirectory() ?? string.Empty;
|
||||
return Path.Combine(homeDir, userPath);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Gets the current user's home directory.</summary>
|
||||
/// <returns>The path to the home directory, or null if it could not be determined.</returns>
|
||||
internal static string GetHomeDirectory()
|
||||
{
|
||||
// First try to get the user's home directory from the HOME environment variable.
|
||||
// This should work in most cases.
|
||||
string userHomeDirectory = Environment.GetEnvironmentVariable("HOME");
|
||||
if (!string.IsNullOrEmpty(userHomeDirectory))
|
||||
{
|
||||
return userHomeDirectory;
|
||||
}
|
||||
|
||||
// In initialization conditions, however, the "HOME" environment variable may
|
||||
// not yet be set. For such cases, consult with the password entry.
|
||||
|
||||
// First try with a buffer that should suffice for 99% of cases.
|
||||
// Note that, theoretically, userHomeDirectory may be null in the success case
|
||||
// if we simply couldn't find a home directory for the current user.
|
||||
// In that case, we pass back the null value and let the caller decide
|
||||
// what to do.
|
||||
return GetHomeDirectoryFromPw();
|
||||
}
|
||||
|
||||
internal static string GetHomeDirectoryFromPw()
|
||||
{
|
||||
string userHomeDirectory = null;
|
||||
const int BufLen = 1024;
|
||||
if (TryGetHomeDirectoryFromPasswd(BufLen, out userHomeDirectory))
|
||||
{
|
||||
return userHomeDirectory;
|
||||
}
|
||||
// Fallback to heap allocations if necessary, growing the buffer until
|
||||
// we succeed. TryGetHomeDirectory will throw if there's an unexpected error.
|
||||
int lastBufLen = BufLen;
|
||||
while (true)
|
||||
{
|
||||
lastBufLen *= 2;
|
||||
if (TryGetHomeDirectoryFromPasswd(lastBufLen, out userHomeDirectory))
|
||||
{
|
||||
return userHomeDirectory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wrapper for getpwuid_r.</summary>
|
||||
/// <param name="bufLen">The length of the buffer to use when storing the password result.</param>
|
||||
/// <param name="path">The resulting path; null if the user didn't have an entry.</param>
|
||||
/// <returns>true if the call was successful (path may still be null); false is a larger buffer is needed.</returns>
|
||||
private static bool TryGetHomeDirectoryFromPasswd(int bufLen, out string path)
|
||||
{
|
||||
// Call getpwuid_r to get the passwd struct
|
||||
Interop.Sys.Passwd passwd;
|
||||
IntPtr buffer = Marshal.AllocHGlobal(bufLen);
|
||||
try
|
||||
{
|
||||
int error = Interop.Sys.GetPwUidR(Interop.Sys.GetEUid(), out passwd, buffer, bufLen);
|
||||
|
||||
// If the call succeeds, give back the home directory path retrieved
|
||||
if (error == 0)
|
||||
{
|
||||
Debug.Assert(passwd.HomeDirectory != IntPtr.Zero);
|
||||
path = Marshal.PtrToStringAnsi(passwd.HomeDirectory);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the current user's entry could not be found, give back null
|
||||
// path, but still return true as false indicates the buffer was
|
||||
// too small.
|
||||
if (error == -1)
|
||||
{
|
||||
path = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var errorInfo = new Interop.ErrorInfo(error);
|
||||
|
||||
// If the call failed because the buffer was too small, return false to
|
||||
// indicate the caller should try again with a larger buffer.
|
||||
if (errorInfo.Error == Interop.Error.ERANGE)
|
||||
{
|
||||
path = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, fail.
|
||||
throw new IOException(errorInfo.GetErrorMessage(), errorInfo.RawErrno);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Deallocate the buffer we created
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
internal static partial class Interop
|
||||
{
|
||||
internal static partial class CoreFoundation
|
||||
{
|
||||
/// <summary>
|
||||
/// Tells the OS what encoding the passed in String is in. These come from the CFString.h header file in the CoreFoundation framework.
|
||||
/// </summary>
|
||||
private enum CFStringBuiltInEncodings : uint
|
||||
{
|
||||
kCFStringEncodingMacRoman = 0,
|
||||
kCFStringEncodingWindowsLatin1 = 0x0500,
|
||||
kCFStringEncodingISOLatin1 = 0x0201,
|
||||
kCFStringEncodingNextStepLatin = 0x0B01,
|
||||
kCFStringEncodingASCII = 0x0600,
|
||||
kCFStringEncodingUnicode = 0x0100,
|
||||
kCFStringEncodingUTF8 = 0x08000100,
|
||||
kCFStringEncodingNonLossyASCII = 0x0BFF,
|
||||
|
||||
kCFStringEncodingUTF16 = 0x0100,
|
||||
kCFStringEncodingUTF16BE = 0x10000100,
|
||||
kCFStringEncodingUTF16LE = 0x14000100,
|
||||
kCFStringEncodingUTF32 = 0x0c000100,
|
||||
kCFStringEncodingUTF32BE = 0x18000100,
|
||||
kCFStringEncodingUTF32LE = 0x1c000100
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
|
||||
/// </summary>
|
||||
/// <param name="allocator">Should be IntPtr.Zero</param>
|
||||
/// <param name="str">The string to get a CFStringRef for</param>
|
||||
/// <param name="encoding">The encoding of the str variable. This should be UTF 8 for OS X</param>
|
||||
/// <returns>Returns a pointer to a CFString on success; otherwise, returns IntPtr.Zero</returns>
|
||||
/// <remarks>For *nix systems, the CLR maps ANSI to UTF-8, so be explicit about that</remarks>
|
||||
[DllImport(Interop.Libraries.CoreFoundationLibrary, CharSet = CharSet.Ansi)]
|
||||
private static extern SafeCreateHandle CFStringCreateWithCString(
|
||||
IntPtr allocator,
|
||||
string str,
|
||||
CFStringBuiltInEncodings encoding);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it.
|
||||
/// </summary>
|
||||
/// <param name="str">The string to get a CFStringRef for</param>
|
||||
/// <returns>Returns a valid SafeCreateHandle to a CFString on success; otherwise, returns an invalid SafeCreateHandle</returns>
|
||||
internal static SafeCreateHandle CFStringCreateWithCString(string str)
|
||||
{
|
||||
return CFStringCreateWithCString(IntPtr.Zero, str, CFStringBuiltInEncodings.kCFStringEncodingUTF8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
|
||||
/// </summary>
|
||||
/// <param name="allocator">Should be IntPtr.Zero</param>
|
||||
/// <param name="values">The values to put in the array</param>
|
||||
/// <param name="numValues">The number of values in the array</param>
|
||||
/// <param name="callbacks">Should be IntPtr.Zero</param>
|
||||
/// <returns>Returns a pointer to a CFArray on success; otherwise, returns IntPtr.Zero</returns>
|
||||
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
|
||||
private static extern SafeCreateHandle CFArrayCreate(
|
||||
IntPtr allocator,
|
||||
[MarshalAs(UnmanagedType.LPArray)]
|
||||
IntPtr[] values,
|
||||
ulong numValues,
|
||||
IntPtr callbacks);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it.
|
||||
/// </summary>
|
||||
/// <param name="values">The values to put in the array</param>
|
||||
/// <param name="numValues">The number of values in the array</param>
|
||||
/// <returns>Returns a valid SafeCreateHandle to a CFArray on success; otherwise, returns an invalid SafeCreateHandle</returns>
|
||||
internal static SafeCreateHandle CFArrayCreate(IntPtr[] values, ulong numValues)
|
||||
{
|
||||
return CFArrayCreate(IntPtr.Zero, values, numValues, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// You should retain a Core Foundation object when you receive it from elsewhere
|
||||
/// (that is, you did not create or copy it) and you want it to persist. If you
|
||||
/// retain a Core Foundation object you are responsible for releasing it
|
||||
/// </summary>
|
||||
/// <param name="ptr">The CFType object to retain. This value must not be NULL</param>
|
||||
/// <returns>The input value</param>
|
||||
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
|
||||
internal extern static IntPtr CFRetain(IntPtr ptr);
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the reference count on the specified object and, if the ref count hits 0, cleans up the object.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer on which to decrement the reference count.</param>
|
||||
[DllImport(Interop.Libraries.CoreFoundationLibrary)]
|
||||
internal extern static void CFRelease(IntPtr ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
{
|
||||
internal partial class Interop
|
||||
{
|
||||
internal partial class Security
|
||||
{
|
||||
|
||||
[DllImport(Libraries.SecurityLibrary, CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern OSStatus SecKeychainAddGenericPassword(IntPtr keyChainRef, UInt32 serviceNameLength, string serviceName,
|
||||
UInt32 accountNameLength, string accountName, UInt32 passwordLength, IntPtr password, [Out] IntPtr itemRef);
|
||||
|
||||
/// <summary>
|
||||
/// Find a generic password based on the attributes passed
|
||||
/// </summary>
|
||||
/// <param name="keyChainRef">
|
||||
/// A reference to an array of keychains to search, a single keychain, or NULL to search the user's default keychain search list.
|
||||
/// </param>
|
||||
/// <param name="serviceNameLength">The length of the buffer pointed to by serviceName.</param>
|
||||
/// <param name="serviceName">A pointer to a string containing the service name.</param>
|
||||
/// <param name="accountNameLength">The length of the buffer pointed to by accountName.</param>
|
||||
/// <param name="accountName">A pointer to a string containing the account name.</param>
|
||||
/// <param name="passwordLength">On return, the length of the buffer pointed to by passwordData.</param>
|
||||
/// <param name="password">
|
||||
/// On return, a pointer to a data buffer containing the password.
|
||||
/// Your application must call SecKeychainItemFreeContent(NULL, passwordData)
|
||||
/// to release this data buffer when it is no longer needed.Pass NULL if you are not interested in retrieving the password data at
|
||||
/// this time, but simply want to find the item reference.
|
||||
/// </param>
|
||||
/// <param name="itemRef">On return, a reference to the keychain item which was found.</param>
|
||||
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
|
||||
/// <remarks>
|
||||
/// The SecKeychainFindGenericPassword function finds the first generic password item which matches the attributes you provide.
|
||||
/// Most attributes are optional; you should pass only as many as you need to narrow the search sufficiently for your application's intended use.
|
||||
/// SecKeychainFindGenericPassword optionally returns a reference to the found item.
|
||||
/// </remarks>
|
||||
[DllImport(Libraries.SecurityLibrary, CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern OSStatus SecKeychainFindGenericPassword(IntPtr keyChainRef, UInt32 serviceNameLength, string serviceName,
|
||||
UInt32 accountNameLength, string accountName, out UInt32 passwordLength, out IntPtr password, out IntPtr itemRef);
|
||||
|
||||
/// <summary>
|
||||
/// Releases the memory used by the keychain attribute list and the keychain data retrieved in a previous call to SecKeychainItemCopyContent.
|
||||
/// </summary>
|
||||
/// <param name="attrList">A pointer to the attribute list to release. Pass NULL to ignore this parameter.</param>
|
||||
/// <param name="data">A pointer to the data buffer to release. Pass NULL to ignore this parameter.</param>
|
||||
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
|
||||
[DllImport(Libraries.SecurityLibrary, SetLastError = true)]
|
||||
internal static extern OSStatus SecKeychainItemFreeContent([In] IntPtr attrList, [In] IntPtr data);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a keychain item from the default keychain's permanent data store.
|
||||
/// </summary>
|
||||
/// <param name="itemRef">A keychain item reference of the item to delete.</param>
|
||||
/// <returns>A result code that should be in <see cref="OSStatus"/></returns>
|
||||
/// <remarks>
|
||||
/// If itemRef has not previously been added to the keychain, SecKeychainItemDelete does nothing and returns ErrSecSuccess.
|
||||
/// IMPORTANT: SecKeychainItemDelete does not dispose the memory occupied by the item reference itself;
|
||||
/// use the CFRelease function when you are completely * * finished with an item.
|
||||
/// </remarks>
|
||||
[DllImport(Libraries.SecurityLibrary, SetLastError = true)]
|
||||
internal static extern OSStatus SecKeychainItemDelete(SafeHandle itemRef);
|
||||
|
||||
#region OSStatus Codes
|
||||
/// <summary>Common Unix errno error codes.</summary>
|
||||
internal enum OSStatus
|
||||
{
|
||||
ErrSecSuccess = 0, /* No error. */
|
||||
ErrSecUnimplemented = -4, /* Function or operation not implemented. */
|
||||
ErrSecDskFull = -34,
|
||||
ErrSecIO = -36, /*I/O error (bummers)*/
|
||||
|
||||
ErrSecParam = -50, /* One or more parameters passed to a function were not valid. */
|
||||
ErrSecWrPerm = -61, /* write permissions error*/
|
||||
ErrSecAllocate = -108, /* Failed to allocate memory. */
|
||||
ErrSecUserCanceled = -128, /* User canceled the operation. */
|
||||
ErrSecBadReq = -909, /* Bad parameter or invalid state for operation. */
|
||||
|
||||
ErrSecInternalComponent = -2070,
|
||||
ErrSecCoreFoundationUnknown = -4960,
|
||||
|
||||
ErrSecNotAvailable = -25291, /* No keychain is available. You may need to restart your computer. */
|
||||
ErrSecReadOnly = -25292, /* This keychain cannot be modified. */
|
||||
ErrSecAuthFailed = -25293, /* The user name or passphrase you entered is not correct. */
|
||||
ErrSecNoSuchKeychain = -25294, /* The specified keychain could not be found. */
|
||||
ErrSecInvalidKeychain = -25295, /* The specified keychain is not a valid keychain file. */
|
||||
ErrSecDuplicateKeychain = -25296, /* A keychain with the same name already exists. */
|
||||
ErrSecDuplicateCallback = -25297, /* The specified callback function is already installed. */
|
||||
ErrSecInvalidCallback = -25298, /* The specified callback function is not valid. */
|
||||
ErrSecDuplicateItem = -25299, /* The specified item already exists in the keychain. */
|
||||
ErrSecItemNotFound = -25300, /* The specified item could not be found in the keychain. */
|
||||
ErrSecBufferTooSmall = -25301, /* There is not enough memory available to use the specified item. */
|
||||
ErrSecDataTooLarge = -25302, /* This item contains information which is too large or in a format that cannot be displayed. */
|
||||
ErrSecNoSuchAttr = -25303, /* The specified attribute does not exist. */
|
||||
ErrSecInvalidItemRef = -25304, /* The specified item is no longer valid. It may have been deleted from the keychain. */
|
||||
ErrSecInvalidSearchRef = -25305, /* Unable to search the current keychain. */
|
||||
ErrSecNoSuchClass = -25306, /* The specified item does not appear to be a valid keychain item. */
|
||||
ErrSecNoDefaultKeychain = -25307, /* A default keychain could not be found. */
|
||||
ErrSecInteractionNotAllowed = -25308, /* User interaction is not allowed. */
|
||||
ErrSecReadOnlyAttr = -25309, /* The specified attribute could not be modified. */
|
||||
ErrSecWrongSecVersion = -25310, /* This keychain was created by a different version of the system software and cannot be opened. */
|
||||
ErrSecKeySizeNotAllowed = -25311, /* This item specifies a key size which is too large. */
|
||||
ErrSecNoStorageModule = -25312, /* A required component (data storage module) could not be loaded. You may need to restart your computer. */
|
||||
ErrSecNoCertificateModule = -25313, /* A required component (certificate module) could not be loaded. You may need to restart your computer. */
|
||||
ErrSecNoPolicyModule = -25314, /* A required component (policy module) could not be loaded. You may need to restart your computer. */
|
||||
ErrSecInteractionRequired = -25315, /* User interaction is required, but is currently not allowed. */
|
||||
ErrSecDataNotAvailable = -25316, /* The contents of this item cannot be retrieved. */
|
||||
ErrSecDataNotModifiable = -25317, /* The contents of this item cannot be modified. */
|
||||
ErrSecCreateChainFailed = -25318, /* One or more certificates required to validate this certificate cannot be found. */
|
||||
ErrSecInvalidPrefsDomain = -25319, /* The specified preferences domain is not valid. */
|
||||
ErrSecInDarkWake = -25320, /* In dark wake, no UI possible */
|
||||
|
||||
ErrSecACLNotSimple = -25240, /* The specified access control list is not in standard (simple) form. */
|
||||
ErrSecPolicyNotFound = -25241, /* The specified policy cannot be found. */
|
||||
ErrSecInvalidTrustSetting = -25242, /* The specified trust setting is invalid. */
|
||||
ErrSecNoAccessForItem = -25243, /* The specified item has no access control. */
|
||||
ErrSecInvalidOwnerEdit = -25244, /* Invalid attempt to change the owner of this item. */
|
||||
ErrSecTrustNotAvailable = -25245, /* No trust results are available. */
|
||||
ErrSecUnsupportedFormat = -25256, /* Import/Export format unsupported. */
|
||||
ErrSecUnknownFormat = -25257, /* Unknown format in import. */
|
||||
ErrSecKeyIsSensitive = -25258, /* Key material must be wrapped for export. */
|
||||
ErrSecMultiplePrivKeys = -25259, /* An attempt was made to import multiple private keys. */
|
||||
ErrSecPassphraseRequired = -25260, /* Passphrase is required for import/export. */
|
||||
ErrSecInvalidPasswordRef = -25261, /* The password reference was invalid. */
|
||||
ErrSecInvalidTrustSettings = -25262, /* The Trust Settings Record was corrupted. */
|
||||
ErrSecNoTrustSettings = -25263, /* No Trust Settings were found. */
|
||||
ErrSecPkcs12VerifyFailure = -25264, /* MAC verification failed during PKCS12 import (wrong password?) */
|
||||
ErrSecNotSigner = -26267, /* A certificate was not signed by its proposed parent. */
|
||||
|
||||
ErrSecDecode = -26275, /* Unable to decode the provided data. */
|
||||
|
||||
ErrSecServiceNotAvailable = -67585, /* The required service is not available. */
|
||||
ErrSecInsufficientClientID = -67586, /* The client ID is not correct. */
|
||||
ErrSecDeviceReset = -67587, /* A device reset has occurred. */
|
||||
ErrSecDeviceFailed = -67588, /* A device failure has occurred. */
|
||||
ErrSecAppleAddAppACLSubject = -67589, /* Adding an application ACL subject failed. */
|
||||
ErrSecApplePublicKeyIncomplete = -67590, /* The public key is incomplete. */
|
||||
ErrSecAppleSignatureMismatch = -67591, /* A signature mismatch has occurred. */
|
||||
ErrSecAppleInvalidKeyStartDate = -67592, /* The specified key has an invalid start date. */
|
||||
ErrSecAppleInvalidKeyEndDate = -67593, /* The specified key has an invalid end date. */
|
||||
ErrSecConversionError = -67594, /* A conversion error has occurred. */
|
||||
ErrSecAppleSSLv2Rollback = -67595, /* A SSLv2 rollback error has occurred. */
|
||||
ErrSecDiskFull = -34, /* The disk is full. */
|
||||
ErrSecQuotaExceeded = -67596, /* The quota was exceeded. */
|
||||
ErrSecFileTooBig = -67597, /* The file is too big. */
|
||||
ErrSecInvalidDatabaseBlob = -67598, /* The specified database has an invalid blob. */
|
||||
ErrSecInvalidKeyBlob = -67599, /* The specified database has an invalid key blob. */
|
||||
ErrSecIncompatibleDatabaseBlob = -67600, /* The specified database has an incompatible blob. */
|
||||
ErrSecIncompatibleKeyBlob = -67601, /* The specified database has an incompatible key blob. */
|
||||
ErrSecHostNameMismatch = -67602, /* A host name mismatch has occurred. */
|
||||
ErrSecUnknownCriticalExtensionFlag = -67603, /* There is an unknown critical extension flag. */
|
||||
ErrSecNoBasicConstraints = -67604, /* No basic constraints were found. */
|
||||
ErrSecNoBasicConstraintsCA = -67605, /* No basic CA constraints were found. */
|
||||
ErrSecInvalidAuthorityKeyID = -67606, /* The authority key ID is not valid. */
|
||||
ErrSecInvalidSubjectKeyID = -67607, /* The subject key ID is not valid. */
|
||||
ErrSecInvalidKeyUsageForPolicy = -67608, /* The key usage is not valid for the specified policy. */
|
||||
ErrSecInvalidExtendedKeyUsage = -67609, /* The extended key usage is not valid. */
|
||||
ErrSecInvalidIDLinkage = -67610, /* The ID linkage is not valid. */
|
||||
ErrSecPathLengthConstraintExceeded = -67611, /* The path length constraint was exceeded. */
|
||||
ErrSecInvalidRoot = -67612, /* The root or anchor certificate is not valid. */
|
||||
ErrSecCRLExpired = -67613, /* The CRL has expired. */
|
||||
ErrSecCRLNotValidYet = -67614, /* The CRL is not yet valid. */
|
||||
ErrSecCRLNotFound = -67615, /* The CRL was not found. */
|
||||
ErrSecCRLServerDown = -67616, /* The CRL server is down. */
|
||||
ErrSecCRLBadURI = -67617, /* The CRL has a bad Uniform Resource Identifier. */
|
||||
ErrSecUnknownCertExtension = -67618, /* An unknown certificate extension was encountered. */
|
||||
ErrSecUnknownCRLExtension = -67619, /* An unknown CRL extension was encountered. */
|
||||
ErrSecCRLNotTrusted = -67620, /* The CRL is not trusted. */
|
||||
ErrSecCRLPolicyFailed = -67621, /* The CRL policy failed. */
|
||||
ErrSecIDPFailure = -67622, /* The issuing distribution point was not valid. */
|
||||
ErrSecSMIMEEmailAddressesNotFound = -67623, /* An email address mismatch was encountered. */
|
||||
ErrSecSMIMEBadExtendedKeyUsage = -67624, /* The appropriate extended key usage for SMIME was not found. */
|
||||
ErrSecSMIMEBadKeyUsage = -67625, /* The key usage is not compatible with SMIME. */
|
||||
ErrSecSMIMEKeyUsageNotCritical = -67626, /* The key usage extension is not marked as critical. */
|
||||
ErrSecSMIMENoEmailAddress = -67627, /* No email address was found in the certificate. */
|
||||
ErrSecSMIMESubjAltNameNotCritical = -67628, /* The subject alternative name extension is not marked as critical. */
|
||||
ErrSecSSLBadExtendedKeyUsage = -67629, /* The appropriate extended key usage for SSL was not found. */
|
||||
ErrSecOCSPBadResponse = -67630, /* The OCSP response was incorrect or could not be parsed. */
|
||||
ErrSecOCSPBadRequest = -67631, /* The OCSP request was incorrect or could not be parsed. */
|
||||
ErrSecOCSPUnavailable = -67632, /* OCSP service is unavailable. */
|
||||
ErrSecOCSPStatusUnrecognized = -67633, /* The OCSP server did not recognize this certificate. */
|
||||
ErrSecEndOfData = -67634, /* An end-of-data was detected. */
|
||||
ErrSecIncompleteCertRevocationCheck = -67635, /* An incomplete certificate revocation check occurred. */
|
||||
ErrSecNetworkFailure = -67636, /* A network failure occurred. */
|
||||
ErrSecOCSPNotTrustedToAnchor = -67637, /* The OCSP response was not trusted to a root or anchor certificate. */
|
||||
ErrSecRecordModified = -67638, /* The record was modified. */
|
||||
ErrSecOCSPSignatureError = -67639, /* The OCSP response had an invalid signature. */
|
||||
ErrSecOCSPNoSigner = -67640, /* The OCSP response had no signer. */
|
||||
ErrSecOCSPResponderMalformedReq = -67641, /* The OCSP responder was given a malformed request. */
|
||||
ErrSecOCSPResponderInternalError = -67642, /* The OCSP responder encountered an internal error. */
|
||||
ErrSecOCSPResponderTryLater = -67643, /* The OCSP responder is busy, try again later. */
|
||||
ErrSecOCSPResponderSignatureRequired = -67644, /* The OCSP responder requires a signature. */
|
||||
ErrSecOCSPResponderUnauthorized = -67645, /* The OCSP responder rejected this request as unauthorized. */
|
||||
ErrSecOCSPResponseNonceMismatch = -67646, /* The OCSP response nonce did not match the request. */
|
||||
ErrSecCodeSigningBadCertChainLength = -67647, /* Code signing encountered an incorrect certificate chain length. */
|
||||
ErrSecCodeSigningNoBasicConstraints = -67648, /* Code signing found no basic constraints. */
|
||||
ErrSecCodeSigningBadPathLengthConstraint= -67649, /* Code signing encountered an incorrect path length constraint. */
|
||||
ErrSecCodeSigningNoExtendedKeyUsage = -67650, /* Code signing found no extended key usage. */
|
||||
ErrSecCodeSigningDevelopment = -67651, /* Code signing indicated use of a development-only certificate. */
|
||||
ErrSecResourceSignBadCertChainLength = -67652, /* Resource signing has encountered an incorrect certificate chain length. */
|
||||
ErrSecResourceSignBadExtKeyUsage = -67653, /* Resource signing has encountered an error in the extended key usage. */
|
||||
ErrSecTrustSettingDeny = -67654, /* The trust setting for this policy was set to Deny. */
|
||||
ErrSecInvalidSubjectName = -67655, /* An invalid certificate subject name was encountered. */
|
||||
ErrSecUnknownQualifiedCertStatement = -67656, /* An unknown qualified certificate statement was encountered. */
|
||||
ErrSecMobileMeRequestQueued = -67657, /* The MobileMe request will be sent during the next connection. */
|
||||
ErrSecMobileMeRequestRedirected = -67658, /* The MobileMe request was redirected. */
|
||||
ErrSecMobileMeServerError = -67659, /* A MobileMe server error occurred. */
|
||||
ErrSecMobileMeServerNotAvailable = -67660, /* The MobileMe server is not available. */
|
||||
ErrSecMobileMeServerAlreadyExists = -67661, /* The MobileMe server reported that the item already exists. */
|
||||
ErrSecMobileMeServerServiceErr = -67662, /* A MobileMe service error has occurred. */
|
||||
ErrSecMobileMeRequestAlreadyPending = -67663, /* A MobileMe request is already pending. */
|
||||
ErrSecMobileMeNoRequestPending = -67664, /* MobileMe has no request pending. */
|
||||
ErrSecMobileMeCSRVerifyFailure = -67665, /* A MobileMe CSR verification failure has occurred. */
|
||||
ErrSecMobileMeFailedConsistencyCheck = -67666, /* MobileMe has found a failed consistency check. */
|
||||
ErrSecNotInitialized = -67667, /* A function was called without initializing CSSM. */
|
||||
ErrSecInvalidHandleUsage = -67668, /* The CSSM handle does not match with the service type. */
|
||||
ErrSecPVCReferentNotFound = -67669, /* A reference to the calling module was not found in the list of authorized callers. */
|
||||
ErrSecFunctionIntegrityFail = -67670, /* A function address was not within the verified module. */
|
||||
ErrSecInternalError = -67671, /* An internal error has occurred. */
|
||||
ErrSecMemoryError = -67672, /* A memory error has occurred. */
|
||||
ErrSecInvalidData = -67673, /* Invalid data was encountered. */
|
||||
ErrSecMDSError = -67674, /* A Module Directory Service error has occurred. */
|
||||
ErrSecInvalidPointer = -67675, /* An invalid pointer was encountered. */
|
||||
ErrSecSelfCheckFailed = -67676, /* Self-check has failed. */
|
||||
ErrSecFunctionFailed = -67677, /* A function has failed. */
|
||||
ErrSecModuleManifestVerifyFailed = -67678, /* A module manifest verification failure has occurred. */
|
||||
ErrSecInvalidGUID = -67679, /* An invalid GUID was encountered. */
|
||||
ErrSecInvalidHandle = -67680, /* An invalid handle was encountered. */
|
||||
ErrSecInvalidDBList = -67681, /* An invalid DB list was encountered. */
|
||||
ErrSecInvalidPassthroughID = -67682, /* An invalid passthrough ID was encountered. */
|
||||
ErrSecInvalidNetworkAddress = -67683, /* An invalid network address was encountered. */
|
||||
ErrSecCRLAlreadySigned = -67684, /* The certificate revocation list is already signed. */
|
||||
ErrSecInvalidNumberOfFields = -67685, /* An invalid number of fields were encountered. */
|
||||
ErrSecVerificationFailure = -67686, /* A verification failure occurred. */
|
||||
ErrSecUnknownTag = -67687, /* An unknown tag was encountered. */
|
||||
ErrSecInvalidSignature = -67688, /* An invalid signature was encountered. */
|
||||
ErrSecInvalidName = -67689, /* An invalid name was encountered. */
|
||||
ErrSecInvalidCertificateRef = -67690, /* An invalid certificate reference was encountered. */
|
||||
ErrSecInvalidCertificateGroup = -67691, /* An invalid certificate group was encountered. */
|
||||
ErrSecTagNotFound = -67692, /* The specified tag was not found. */
|
||||
ErrSecInvalidQuery = -67693, /* The specified query was not valid. */
|
||||
ErrSecInvalidValue = -67694, /* An invalid value was detected. */
|
||||
ErrSecCallbackFailed = -67695, /* A callback has failed. */
|
||||
ErrSecACLDeleteFailed = -67696, /* An ACL delete operation has failed. */
|
||||
ErrSecACLReplaceFailed = -67697, /* An ACL replace operation has failed. */
|
||||
ErrSecACLAddFailed = -67698, /* An ACL add operation has failed. */
|
||||
ErrSecACLChangeFailed = -67699, /* An ACL change operation has failed. */
|
||||
ErrSecInvalidAccessCredentials = -67700, /* Invalid access credentials were encountered. */
|
||||
ErrSecInvalidRecord = -67701, /* An invalid record was encountered. */
|
||||
ErrSecInvalidACL = -67702, /* An invalid ACL was encountered. */
|
||||
ErrSecInvalidSampleValue = -67703, /* An invalid sample value was encountered. */
|
||||
ErrSecIncompatibleVersion = -67704, /* An incompatible version was encountered. */
|
||||
ErrSecPrivilegeNotGranted = -67705, /* The privilege was not granted. */
|
||||
ErrSecInvalidScope = -67706, /* An invalid scope was encountered. */
|
||||
ErrSecPVCAlreadyConfigured = -67707, /* The PVC is already configured. */
|
||||
ErrSecInvalidPVC = -67708, /* An invalid PVC was encountered. */
|
||||
ErrSecEMMLoadFailed = -67709, /* The EMM load has failed. */
|
||||
ErrSecEMMUnloadFailed = -67710, /* The EMM unload has failed. */
|
||||
ErrSecAddinLoadFailed = -67711, /* The add-in load operation has failed. */
|
||||
ErrSecInvalidKeyRef = -67712, /* An invalid key was encountered. */
|
||||
ErrSecInvalidKeyHierarchy = -67713, /* An invalid key hierarchy was encountered. */
|
||||
ErrSecAddinUnloadFailed = -67714, /* The add-in unload operation has failed. */
|
||||
ErrSecLibraryReferenceNotFound = -67715, /* A library reference was not found. */
|
||||
ErrSecInvalidAddinFunctionTable = -67716, /* An invalid add-in function table was encountered. */
|
||||
ErrSecInvalidServiceMask = -67717, /* An invalid service mask was encountered. */
|
||||
ErrSecModuleNotLoaded = -67718, /* A module was not loaded. */
|
||||
ErrSecInvalidSubServiceID = -67719, /* An invalid subservice ID was encountered. */
|
||||
ErrSecAttributeNotInContext = -67720, /* An attribute was not in the context. */
|
||||
ErrSecModuleManagerInitializeFailed = -67721, /* A module failed to initialize. */
|
||||
ErrSecModuleManagerNotFound = -67722, /* A module was not found. */
|
||||
ErrSecEventNotificationCallbackNotFound = -67723, /* An event notification callback was not found. */
|
||||
ErrSecInputLengthError = -67724, /* An input length error was encountered. */
|
||||
ErrSecOutputLengthError = -67725, /* An output length error was encountered. */
|
||||
ErrSecPrivilegeNotSupported = -67726, /* The privilege is not supported. */
|
||||
ErrSecDeviceError = -67727, /* A device error was encountered. */
|
||||
ErrSecAttachHandleBusy = -67728, /* The CSP handle was busy. */
|
||||
ErrSecNotLoggedIn = -67729, /* You are not logged in. */
|
||||
ErrSecAlgorithmMismatch = -67730, /* An algorithm mismatch was encountered. */
|
||||
ErrSecKeyUsageIncorrect = -67731, /* The key usage is incorrect. */
|
||||
ErrSecKeyBlobTypeIncorrect = -67732, /* The key blob type is incorrect. */
|
||||
ErrSecKeyHeaderInconsistent = -67733, /* The key header is inconsistent. */
|
||||
ErrSecUnsupportedKeyFormat = -67734, /* The key header format is not supported. */
|
||||
ErrSecUnsupportedKeySize = -67735, /* The key size is not supported. */
|
||||
ErrSecInvalidKeyUsageMask = -67736, /* The key usage mask is not valid. */
|
||||
ErrSecUnsupportedKeyUsageMask = -67737, /* The key usage mask is not supported. */
|
||||
ErrSecInvalidKeyAttributeMask = -67738, /* The key attribute mask is not valid. */
|
||||
ErrSecUnsupportedKeyAttributeMask = -67739, /* The key attribute mask is not supported. */
|
||||
ErrSecInvalidKeyLabel = -67740, /* The key label is not valid. */
|
||||
ErrSecUnsupportedKeyLabel = -67741, /* The key label is not supported. */
|
||||
ErrSecInvalidKeyFormat = -67742, /* The key format is not valid. */
|
||||
ErrSecUnsupportedVectorOfBuffers = -67743, /* The vector of buffers is not supported. */
|
||||
ErrSecInvalidInputVector = -67744, /* The input vector is not valid. */
|
||||
ErrSecInvalidOutputVector = -67745, /* The output vector is not valid. */
|
||||
ErrSecInvalidContext = -67746, /* An invalid context was encountered. */
|
||||
ErrSecInvalidAlgorithm = -67747, /* An invalid algorithm was encountered. */
|
||||
ErrSecInvalidAttributeKey = -67748, /* A key attribute was not valid. */
|
||||
ErrSecMissingAttributeKey = -67749, /* A key attribute was missing. */
|
||||
ErrSecInvalidAttributeInitVector = -67750, /* An init vector attribute was not valid. */
|
||||
ErrSecMissingAttributeInitVector = -67751, /* An init vector attribute was missing. */
|
||||
ErrSecInvalidAttributeSalt = -67752, /* A salt attribute was not valid. */
|
||||
ErrSecMissingAttributeSalt = -67753, /* A salt attribute was missing. */
|
||||
ErrSecInvalidAttributePadding = -67754, /* A padding attribute was not valid. */
|
||||
ErrSecMissingAttributePadding = -67755, /* A padding attribute was missing. */
|
||||
ErrSecInvalidAttributeRandom = -67756, /* A random number attribute was not valid. */
|
||||
ErrSecMissingAttributeRandom = -67757, /* A random number attribute was missing. */
|
||||
ErrSecInvalidAttributeSeed = -67758, /* A seed attribute was not valid. */
|
||||
ErrSecMissingAttributeSeed = -67759, /* A seed attribute was missing. */
|
||||
ErrSecInvalidAttributePassphrase = -67760, /* A passphrase attribute was not valid. */
|
||||
ErrSecMissingAttributePassphrase = -67761, /* A passphrase attribute was missing. */
|
||||
ErrSecInvalidAttributeKeyLength = -67762, /* A key length attribute was not valid. */
|
||||
ErrSecMissingAttributeKeyLength = -67763, /* A key length attribute was missing. */
|
||||
ErrSecInvalidAttributeBlockSize = -67764, /* A block size attribute was not valid. */
|
||||
ErrSecMissingAttributeBlockSize = -67765, /* A block size attribute was missing. */
|
||||
ErrSecInvalidAttributeOutputSize = -67766, /* An output size attribute was not valid. */
|
||||
ErrSecMissingAttributeOutputSize = -67767, /* An output size attribute was missing. */
|
||||
ErrSecInvalidAttributeRounds = -67768, /* The number of rounds attribute was not valid. */
|
||||
ErrSecMissingAttributeRounds = -67769, /* The number of rounds attribute was missing. */
|
||||
ErrSecInvalidAlgorithmParms = -67770, /* An algorithm parameters attribute was not valid. */
|
||||
ErrSecMissingAlgorithmParms = -67771, /* An algorithm parameters attribute was missing. */
|
||||
ErrSecInvalidAttributeLabel = -67772, /* A label attribute was not valid. */
|
||||
ErrSecMissingAttributeLabel = -67773, /* A label attribute was missing. */
|
||||
ErrSecInvalidAttributeKeyType = -67774, /* A key type attribute was not valid. */
|
||||
ErrSecMissingAttributeKeyType = -67775, /* A key type attribute was missing. */
|
||||
ErrSecInvalidAttributeMode = -67776, /* A mode attribute was not valid. */
|
||||
ErrSecMissingAttributeMode = -67777, /* A mode attribute was missing. */
|
||||
ErrSecInvalidAttributeEffectiveBits = -67778, /* An effective bits attribute was not valid. */
|
||||
ErrSecMissingAttributeEffectiveBits = -67779, /* An effective bits attribute was missing. */
|
||||
ErrSecInvalidAttributeStartDate = -67780, /* A start date attribute was not valid. */
|
||||
ErrSecMissingAttributeStartDate = -67781, /* A start date attribute was missing. */
|
||||
ErrSecInvalidAttributeEndDate = -67782, /* An end date attribute was not valid. */
|
||||
ErrSecMissingAttributeEndDate = -67783, /* An end date attribute was missing. */
|
||||
ErrSecInvalidAttributeVersion = -67784, /* A version attribute was not valid. */
|
||||
ErrSecMissingAttributeVersion = -67785, /* A version attribute was missing. */
|
||||
ErrSecInvalidAttributePrime = -67786, /* A prime attribute was not valid. */
|
||||
ErrSecMissingAttributePrime = -67787, /* A prime attribute was missing. */
|
||||
ErrSecInvalidAttributeBase = -67788, /* A base attribute was not valid. */
|
||||
ErrSecMissingAttributeBase = -67789, /* A base attribute was missing. */
|
||||
ErrSecInvalidAttributeSubprime = -67790, /* A subprime attribute was not valid. */
|
||||
ErrSecMissingAttributeSubprime = -67791, /* A subprime attribute was missing. */
|
||||
ErrSecInvalidAttributeIterationCount = -67792, /* An iteration count attribute was not valid. */
|
||||
ErrSecMissingAttributeIterationCount = -67793, /* An iteration count attribute was missing. */
|
||||
ErrSecInvalidAttributeDLDBHandle = -67794, /* A database handle attribute was not valid. */
|
||||
ErrSecMissingAttributeDLDBHandle = -67795, /* A database handle attribute was missing. */
|
||||
ErrSecInvalidAttributeAccessCredentials = -67796, /* An access credentials attribute was not valid. */
|
||||
ErrSecMissingAttributeAccessCredentials = -67797, /* An access credentials attribute was missing. */
|
||||
ErrSecInvalidAttributePublicKeyFormat = -67798, /* A public key format attribute was not valid. */
|
||||
ErrSecMissingAttributePublicKeyFormat = -67799, /* A public key format attribute was missing. */
|
||||
ErrSecInvalidAttributePrivateKeyFormat = -67800, /* A private key format attribute was not valid. */
|
||||
ErrSecMissingAttributePrivateKeyFormat = -67801, /* A private key format attribute was missing. */
|
||||
ErrSecInvalidAttributeSymmetricKeyFormat = -67802, /* A symmetric key format attribute was not valid. */
|
||||
ErrSecMissingAttributeSymmetricKeyFormat = -67803, /* A symmetric key format attribute was missing. */
|
||||
ErrSecInvalidAttributeWrappedKeyFormat = -67804, /* A wrapped key format attribute was not valid. */
|
||||
ErrSecMissingAttributeWrappedKeyFormat = -67805, /* A wrapped key format attribute was missing. */
|
||||
ErrSecStagedOperationInProgress = -67806, /* A staged operation is in progress. */
|
||||
ErrSecStagedOperationNotStarted = -67807, /* A staged operation was not started. */
|
||||
ErrSecVerifyFailed = -67808, /* A cryptographic verification failure has occurred. */
|
||||
ErrSecQuerySizeUnknown = -67809, /* The query size is unknown. */
|
||||
ErrSecBlockSizeMismatch = -67810, /* A block size mismatch occurred. */
|
||||
ErrSecPublicKeyInconsistent = -67811, /* The public key was inconsistent. */
|
||||
ErrSecDeviceVerifyFailed = -67812, /* A device verification failure has occurred. */
|
||||
ErrSecInvalidLoginName = -67813, /* An invalid login name was detected. */
|
||||
ErrSecAlreadyLoggedIn = -67814, /* The user is already logged in. */
|
||||
ErrSecInvalidDigestAlgorithm = -67815, /* An invalid digest algorithm was detected. */
|
||||
ErrSecInvalidCRLGroup = -67816, /* An invalid CRL group was detected. */
|
||||
ErrSecCertificateCannotOperate = -67817, /* The certificate cannot operate. */
|
||||
ErrSecCertificateExpired = -67818, /* An expired certificate was detected. */
|
||||
ErrSecCertificateNotValidYet = -67819, /* The certificate is not yet valid. */
|
||||
ErrSecCertificateRevoked = -67820, /* The certificate was revoked. */
|
||||
ErrSecCertificateSuspended = -67821, /* The certificate was suspended. */
|
||||
ErrSecInsufficientCredentials = -67822, /* Insufficient credentials were detected. */
|
||||
ErrSecInvalidAction = -67823, /* The action was not valid. */
|
||||
ErrSecInvalidAuthority = -67824, /* The authority was not valid. */
|
||||
ErrSecVerifyActionFailed = -67825, /* A verify action has failed. */
|
||||
ErrSecInvalidCertAuthority = -67826, /* The certificate authority was not valid. */
|
||||
ErrSecInvaldCRLAuthority = -67827, /* The CRL authority was not valid. */
|
||||
ErrSecInvalidCRLEncoding = -67828, /* The CRL encoding was not valid. */
|
||||
ErrSecInvalidCRLType = -67829, /* The CRL type was not valid. */
|
||||
ErrSecInvalidCRL = -67830, /* The CRL was not valid. */
|
||||
ErrSecInvalidFormType = -67831, /* The form type was not valid. */
|
||||
ErrSecInvalidID = -67832, /* The ID was not valid. */
|
||||
ErrSecInvalidIdentifier = -67833, /* The identifier was not valid. */
|
||||
ErrSecInvalidIndex = -67834, /* The index was not valid. */
|
||||
ErrSecInvalidPolicyIdentifiers = -67835, /* The policy identifiers are not valid. */
|
||||
ErrSecInvalidTimeString = -67836, /* The time specified was not valid. */
|
||||
ErrSecInvalidReason = -67837, /* The trust policy reason was not valid. */
|
||||
ErrSecInvalidRequestInputs = -67838, /* The request inputs are not valid. */
|
||||
ErrSecInvalidResponseVector = -67839, /* The response vector was not valid. */
|
||||
ErrSecInvalidStopOnPolicy = -67840, /* The stop-on policy was not valid. */
|
||||
ErrSecInvalidTuple = -67841, /* The tuple was not valid. */
|
||||
ErrSecMultipleValuesUnsupported = -67842, /* Multiple values are not supported. */
|
||||
ErrSecNotTrusted = -67843, /* The trust policy was not trusted. */
|
||||
ErrSecNoDefaultAuthority = -67844, /* No default authority was detected. */
|
||||
ErrSecRejectedForm = -67845, /* The trust policy had a rejected form. */
|
||||
ErrSecRequestLost = -67846, /* The request was lost. */
|
||||
ErrSecRequestRejected = -67847, /* The request was rejected. */
|
||||
ErrSecUnsupportedAddressType = -67848, /* The address type is not supported. */
|
||||
ErrSecUnsupportedService = -67849, /* The service is not supported. */
|
||||
ErrSecInvalidTupleGroup = -67850, /* The tuple group was not valid. */
|
||||
ErrSecInvalidBaseACLs = -67851, /* The base ACLs are not valid. */
|
||||
ErrSecInvalidTupleCredendtials = -67852, /* The tuple credentials are not valid. */
|
||||
ErrSecInvalidEncoding = -67853, /* The encoding was not valid. */
|
||||
ErrSecInvalidValidityPeriod = -67854, /* The validity period was not valid. */
|
||||
ErrSecInvalidRequestor = -67855, /* The requestor was not valid. */
|
||||
ErrSecRequestDescriptor = -67856, /* The request descriptor was not valid. */
|
||||
ErrSecInvalidBundleInfo = -67857, /* The bundle information was not valid. */
|
||||
ErrSecInvalidCRLIndex = -67858, /* The CRL index was not valid. */
|
||||
ErrSecNoFieldValues = -67859, /* No field values were detected. */
|
||||
ErrSecUnsupportedFieldFormat = -67860, /* The field format is not supported. */
|
||||
ErrSecUnsupportedIndexInfo = -67861, /* The index information is not supported. */
|
||||
ErrSecUnsupportedLocality = -67862, /* The locality is not supported. */
|
||||
ErrSecUnsupportedNumAttributes = -67863, /* The number of attributes is not supported. */
|
||||
ErrSecUnsupportedNumIndexes = -67864, /* The number of indexes is not supported. */
|
||||
ErrSecUnsupportedNumRecordTypes = -67865, /* The number of record types is not supported. */
|
||||
ErrSecFieldSpecifiedMultiple = -67866, /* Too many fields were specified. */
|
||||
ErrSecIncompatibleFieldFormat = -67867, /* The field format was incompatible. */
|
||||
ErrSecInvalidParsingModule = -67868, /* The parsing module was not valid. */
|
||||
ErrSecDatabaseLocked = -67869, /* The database is locked. */
|
||||
ErrSecDatastoreIsOpen = -67870, /* The data store is open. */
|
||||
ErrSecMissingValue = -67871, /* A missing value was detected. */
|
||||
ErrSecUnsupportedQueryLimits = -67872, /* The query limits are not supported. */
|
||||
ErrSecUnsupportedNumSelectionPreds = -67873, /* The number of selection predicates is not supported. */
|
||||
ErrSecUnsupportedOperator = -67874, /* The operator is not supported. */
|
||||
ErrSecInvalidDBLocation = -67875, /* The database location is not valid. */
|
||||
ErrSecInvalidAccessRequest = -67876, /* The access request is not valid. */
|
||||
ErrSecInvalidIndexInfo = -67877, /* The index information is not valid. */
|
||||
ErrSecInvalidNewOwner = -67878, /* The new owner is not valid. */
|
||||
ErrSecInvalidModifyMode = -67879, /* The modify mode is not valid. */
|
||||
ErrSecMissingRequiredExtension = -67880, /* A required certificate extension is missing. */
|
||||
ErrSecExtendedKeyUsageNotCritical = -67881, /* The extended key usage extension was not marked critical. */
|
||||
ErrSecTimestampMissing = -67882, /* A timestamp was expected but was not found. */
|
||||
ErrSecTimestampInvalid = -67883, /* The timestamp was not valid. */
|
||||
ErrSecTimestampNotTrusted = -67884, /* The timestamp was not trusted. */
|
||||
ErrSecTimestampServiceNotAvailable = -67885, /* The timestamp service is not available. */
|
||||
ErrSecTimestampBadAlg = -67886, /* An unrecognized or unsupported Algorithm Identifier in timestamp. */
|
||||
ErrSecTimestampBadRequest = -67887, /* The timestamp transaction is not permitted or supported. */
|
||||
ErrSecTimestampBadDataFormat = -67888, /* The timestamp data submitted has the wrong format. */
|
||||
ErrSecTimestampTimeNotAvailable = -67889, /* The time source for the Timestamp Authority is not available. */
|
||||
ErrSecTimestampUnacceptedPolicy = -67890, /* The requested policy is not supported by the Timestamp Authority. */
|
||||
ErrSecTimestampUnacceptedExtension = -67891, /* The requested extension is not supported by the Timestamp Authority. */
|
||||
ErrSecTimestampAddInfoNotAvailable = -67892, /* The additional information requested is not available. */
|
||||
ErrSecTimestampSystemFailure = -67893, /* The timestamp request cannot be handled due to system failure. */
|
||||
ErrSecSigningTimeMissing = -67894, /* A signing time was expected but was not found. */
|
||||
ErrSecTimestampRejection = -67895, /* A timestamp transaction was rejected. */
|
||||
ErrSecTimestampWaiting = -67896, /* A timestamp transaction is waiting. */
|
||||
ErrSecTimestampRevocationWarning = -67897, /* A timestamp authority revocation warning was issued. */
|
||||
ErrSecTimestampRevocationNotification = -67898, /* A timestamp authority revocation notification was issued. */
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.OSX
|
||||
{
|
||||
/// <summary>
|
||||
/// OSX implementation of the credential store
|
||||
/// </summary>
|
||||
internal class OSXCredentialStore : ICredentialStore
|
||||
{
|
||||
public bool DeletePassword(string credentialId)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
|
||||
return DeletePasswordImpl(credentialId);
|
||||
}
|
||||
|
||||
public bool TryGetPassword(string credentialId, out string password)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
|
||||
return FindPassword(credentialId, out password);
|
||||
}
|
||||
|
||||
public bool Save(Credential credential)
|
||||
{
|
||||
Credential.ValidateForSave(credential);
|
||||
bool result = false;
|
||||
|
||||
// Note: OSX blocks AddPassword if the credential
|
||||
// already exists, so for now we delete the password if already present since we're updating
|
||||
// the value. In the future, we could consider updating but it's low value to solve this
|
||||
DeletePasswordImpl(credential.CredentialId);
|
||||
|
||||
// Now add the password
|
||||
result = AddGenericPassword(credential);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool AddGenericPassword(Credential credential)
|
||||
{
|
||||
IntPtr passwordPtr = Marshal.StringToCoTaskMemUni(credential.Password);
|
||||
Interop.Security.OSStatus status = Interop.Security.SecKeychainAddGenericPassword(
|
||||
IntPtr.Zero,
|
||||
InteropUtils.GetLengthInBytes(credential.CredentialId),
|
||||
credential.CredentialId,
|
||||
0,
|
||||
null,
|
||||
InteropUtils.GetLengthInBytes(credential.Password),
|
||||
passwordPtr,
|
||||
IntPtr.Zero);
|
||||
|
||||
return status == Interop.Security.OSStatus.ErrSecSuccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first password matching this credential
|
||||
/// </summary>
|
||||
private bool FindPassword(string credentialId, out string password)
|
||||
{
|
||||
password = null;
|
||||
using (KeyChainItemHandle handle = LookupKeyChainItem(credentialId))
|
||||
{
|
||||
if( handle == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
password = handle.Password;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private KeyChainItemHandle LookupKeyChainItem(string credentialId)
|
||||
{
|
||||
UInt32 passwordLength;
|
||||
IntPtr passwordPtr;
|
||||
IntPtr item;
|
||||
Interop.Security.OSStatus status = Interop.Security.SecKeychainFindGenericPassword(
|
||||
IntPtr.Zero,
|
||||
InteropUtils.GetLengthInBytes(credentialId),
|
||||
credentialId,
|
||||
0,
|
||||
null,
|
||||
out passwordLength,
|
||||
out passwordPtr,
|
||||
out item);
|
||||
|
||||
if(status == Interop.Security.OSStatus.ErrSecSuccess)
|
||||
{
|
||||
return new KeyChainItemHandle(item, passwordPtr, passwordLength);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool DeletePasswordImpl(string credentialId)
|
||||
{
|
||||
// Find password, then Delete, then cleanup
|
||||
using (KeyChainItemHandle handle = LookupKeyChainItem(credentialId))
|
||||
{
|
||||
if (handle == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Interop.Security.OSStatus status = Interop.Security.SecKeychainItemDelete(handle);
|
||||
return status == Interop.Security.OSStatus.ErrSecSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
private class KeyChainItemHandle : SafeCreateHandle
|
||||
{
|
||||
private IntPtr passwordPtr;
|
||||
private int passwordLength;
|
||||
|
||||
public KeyChainItemHandle() : base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public KeyChainItemHandle(IntPtr itemPtr) : this(itemPtr, IntPtr.Zero, 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public KeyChainItemHandle(IntPtr itemPtr, IntPtr passwordPtr, UInt32 passwordLength)
|
||||
: base(itemPtr)
|
||||
{
|
||||
this.passwordPtr = passwordPtr;
|
||||
this.passwordLength = (int) passwordLength;
|
||||
}
|
||||
|
||||
public string Password
|
||||
{
|
||||
get {
|
||||
if (IsInvalid)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return InteropUtils.CopyToString(passwordPtr, passwordLength);
|
||||
}
|
||||
}
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
if (passwordPtr != IntPtr.Zero)
|
||||
{
|
||||
Interop.Security.SecKeychainItemFreeContent(IntPtr.Zero, passwordPtr);
|
||||
}
|
||||
base.ReleaseHandle();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials
|
||||
|
||||
{
|
||||
/// <summary>
|
||||
/// This class is a wrapper around the Create pattern in OS X where
|
||||
/// if a Create* function is called, the caller must also CFRelease
|
||||
/// on the same pointer in order to correctly free the memory.
|
||||
/// </summary>
|
||||
[System.Security.SecurityCritical]
|
||||
internal partial class SafeCreateHandle : SafeHandle
|
||||
{
|
||||
internal SafeCreateHandle() : base(IntPtr.Zero, true) { }
|
||||
|
||||
internal SafeCreateHandle(IntPtr ptr) : base(IntPtr.Zero, true)
|
||||
{
|
||||
this.SetHandle(ptr);
|
||||
}
|
||||
|
||||
[System.Security.SecurityCritical]
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
Interop.CoreFoundation.CFRelease(handle);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool IsInvalid
|
||||
{
|
||||
[System.Security.SecurityCritical]
|
||||
get
|
||||
{
|
||||
return handle == IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//
|
||||
// Code originally from http://credentialmanagement.codeplex.com/,
|
||||
// Licensed under the Apache License 2.0
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
|
||||
{
|
||||
public class CredentialSet: List<Win32Credential>, IDisposable
|
||||
{
|
||||
bool _disposed;
|
||||
|
||||
public CredentialSet()
|
||||
{
|
||||
}
|
||||
|
||||
public CredentialSet(string target)
|
||||
: this()
|
||||
{
|
||||
if (string.IsNullOrEmpty(target))
|
||||
{
|
||||
throw new ArgumentNullException("target");
|
||||
}
|
||||
Target = target;
|
||||
}
|
||||
|
||||
public string Target { get; set; }
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
// Prevent GC Collection since we have already disposed of this object
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~CredentialSet()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
ForEach(cred => cred.Dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public CredentialSet Load()
|
||||
{
|
||||
LoadInternal();
|
||||
return this;
|
||||
}
|
||||
|
||||
private void LoadInternal()
|
||||
{
|
||||
uint count;
|
||||
|
||||
IntPtr pCredentials = IntPtr.Zero;
|
||||
bool result = NativeMethods.CredEnumerateW(Target, 0, out count, out pCredentials);
|
||||
if (!result)
|
||||
{
|
||||
Logger.Write(LogLevel.Error, string.Format("Win32Exception: {0}", new Win32Exception(Marshal.GetLastWin32Error()).ToString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Read in all of the pointers first
|
||||
IntPtr[] ptrCredList = new IntPtr[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
ptrCredList[i] = Marshal.ReadIntPtr(pCredentials, IntPtr.Size*i);
|
||||
}
|
||||
|
||||
// Now let's go through all of the pointers in the list
|
||||
// and create our Credential object(s)
|
||||
List<NativeMethods.CriticalCredentialHandle> credentialHandles =
|
||||
ptrCredList.Select(ptrCred => new NativeMethods.CriticalCredentialHandle(ptrCred)).ToList();
|
||||
|
||||
IEnumerable<Win32Credential> existingCredentials = credentialHandles
|
||||
.Select(handle => handle.GetCredential())
|
||||
.Select(nativeCredential =>
|
||||
{
|
||||
Win32Credential credential = new Win32Credential();
|
||||
credential.LoadInternal(nativeCredential);
|
||||
return credential;
|
||||
});
|
||||
AddRange(existingCredentials);
|
||||
|
||||
// The individual credentials should not be free'd
|
||||
credentialHandles.ForEach(handle => handle.SetHandleAsInvalid());
|
||||
|
||||
// Clean up memory to the Enumeration pointer
|
||||
NativeMethods.CredFree(pCredentials);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// Code originally from http://credentialmanagement.codeplex.com/,
|
||||
// Licensed under the Apache License 2.0
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
|
||||
{
|
||||
internal class NativeMethods
|
||||
{
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CREDENTIAL
|
||||
{
|
||||
public int Flags;
|
||||
public int Type;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string TargetName;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string Comment;
|
||||
public long LastWritten;
|
||||
public int CredentialBlobSize;
|
||||
public IntPtr CredentialBlob;
|
||||
public int Persist;
|
||||
public int AttributeCount;
|
||||
public IntPtr Attributes;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string TargetAlias;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string UserName;
|
||||
}
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern bool CredWrite([In] ref CREDENTIAL userCredential, [In] UInt32 flags);
|
||||
|
||||
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
|
||||
internal static extern bool CredFree([In] IntPtr cred);
|
||||
|
||||
[DllImport("advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode)]
|
||||
internal static extern bool CredDelete(StringBuilder target, CredentialType type, int flags);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
internal static extern bool CredEnumerateW(string filter, int flag, out uint count, out IntPtr pCredentials);
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
internal static extern void CoTaskMemFree(IntPtr ptr);
|
||||
|
||||
|
||||
internal abstract class CriticalHandleZeroOrMinusOneIsInvalid : CriticalHandle
|
||||
{
|
||||
protected CriticalHandleZeroOrMinusOneIsInvalid() : base(IntPtr.Zero)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsInvalid
|
||||
{
|
||||
get { return handle == new IntPtr(0) || handle == new IntPtr(-1); }
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CriticalCredentialHandle : CriticalHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
// Set the handle.
|
||||
internal CriticalCredentialHandle(IntPtr preexistingHandle)
|
||||
{
|
||||
SetHandle(preexistingHandle);
|
||||
}
|
||||
|
||||
internal CREDENTIAL GetCredential()
|
||||
{
|
||||
if (!IsInvalid)
|
||||
{
|
||||
// Get the Credential from the mem location
|
||||
return (CREDENTIAL)Marshal.PtrToStructure<CREDENTIAL>(handle);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Invalid CriticalHandle!");
|
||||
}
|
||||
}
|
||||
|
||||
// Perform any specific actions to release the handle in the ReleaseHandle method.
|
||||
// Often, you need to use Pinvoke to make a call into the Win32 API to release the
|
||||
// handle. In this case, however, we can use the Marshal class to release the unmanaged memory.
|
||||
|
||||
override protected bool ReleaseHandle()
|
||||
{
|
||||
// If the handle was set, free it. Return success.
|
||||
if (!IsInvalid)
|
||||
{
|
||||
// NOTE: We should also ZERO out the memory allocated to the handle, before free'ing it
|
||||
// so there are no traces of the sensitive data left in memory.
|
||||
CredFree(handle);
|
||||
// Mark the handle as invalid for future users.
|
||||
SetHandleAsInvalid();
|
||||
return true;
|
||||
}
|
||||
// Return false.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Credentials.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Credentials.Win32
|
||||
{
|
||||
/// <summary>
|
||||
/// Win32 implementation of the credential store
|
||||
/// </summary>
|
||||
internal class Win32CredentialStore : ICredentialStore
|
||||
{
|
||||
private const string AnyUsername = "*";
|
||||
|
||||
public bool DeletePassword(string credentialId)
|
||||
{
|
||||
using (Win32Credential cred = new Win32Credential() { Target = credentialId, Username = AnyUsername })
|
||||
{
|
||||
return cred.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPassword(string credentialId, out string password)
|
||||
{
|
||||
Validate.IsNotNullOrEmptyString("credentialId", credentialId);
|
||||
password = null;
|
||||
|
||||
using (CredentialSet set = new CredentialSet(credentialId).Load())
|
||||
{
|
||||
// Note: Credentials are disposed on disposal of the set
|
||||
Win32Credential foundCred = null;
|
||||
if (set.Count > 0)
|
||||
{
|
||||
foundCred = set[0];
|
||||
}
|
||||
|
||||
if (foundCred != null)
|
||||
{
|
||||
password = foundCred.Password;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Save(Credential credential)
|
||||
{
|
||||
Credential.ValidateForSave(credential);
|
||||
|
||||
using (Win32Credential cred =
|
||||
new Win32Credential(AnyUsername, credential.Password, credential.CredentialId, CredentialType.Generic)
|
||||
{ PersistanceType = PersistanceType.LocalComputer })
|
||||
{
|
||||
return cred.Save();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
//
|
||||
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a class that describes the capabilities of a language
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters to be used for reporting hosting-level errors, such as protocol violations
|
||||
/// </summary>
|
||||
public class HostingErrorParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The message of the error
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
}
|
||||
|
||||
public class HostingErrorEvent
|
||||
{
|
||||
public static readonly
|
||||
EventType<HostingErrorParams> Type =
|
||||
EventType<HostingErrorParams>.Create("hosting/error");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a message that is sent from the client to request
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a message that is sent from the client to request
|
||||
/// the version of the server.
|
||||
/// </summary>
|
||||
public class VersionRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<object, string> Type =
|
||||
RequestType<object, string>.Create("version");
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a base implementation for servers and their clients over a
|
||||
@@ -7,8 +7,9 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a client implementation for the standard I/O channel.
|
||||
@@ -6,8 +6,9 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a server implementation for the standard I/O channel.
|
||||
@@ -6,7 +6,7 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines an event type with a particular method name.
|
||||
@@ -6,7 +6,7 @@
|
||||
using System.Diagnostics;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines all possible message types.
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts
|
||||
{
|
||||
[DebuggerDisplay("RequestType MethodName = {MethodName}")]
|
||||
public class RequestType<TParams, TResult>
|
||||
@@ -4,8 +4,9 @@
|
||||
//
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides context for a received event so that handlers
|
||||
@@ -4,10 +4,11 @@
|
||||
//
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
internal interface IMessageSender
|
||||
public interface IMessageSender
|
||||
{
|
||||
Task SendEvent<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// A ProtocolEndpoint is used for inter-process communication. Services can register to
|
||||
/// respond to requests and events, send their own requests, and listen for notifications
|
||||
/// sent by the other side of the endpoint
|
||||
/// </summary>
|
||||
public interface IProtocolEndpoint : IMessageSender
|
||||
{
|
||||
void SetRequestHandler<TParams, TResult>(
|
||||
RequestType<TParams, TResult> requestType,
|
||||
Func<TParams, RequestContext<TResult>, Task> requestHandler);
|
||||
|
||||
void SetEventHandler<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
Func<TParams, EventContext, Task> eventHandler);
|
||||
|
||||
void SetEventHandler<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
Func<TParams, EventContext, Task> eventHandler,
|
||||
bool overrideExisting);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,17 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public class MessageDispatcher
|
||||
{
|
||||
@@ -197,10 +199,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
this.SynchronizationContext = SynchronizationContext.Current;
|
||||
|
||||
// Run the message loop
|
||||
bool isRunning = true;
|
||||
while (isRunning && !cancellationToken.IsCancellationRequested)
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Message newMessage = null;
|
||||
Message newMessage;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -209,12 +210,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
}
|
||||
catch (MessageParseException e)
|
||||
{
|
||||
// TODO: Write an error response
|
||||
|
||||
Logger.Write(
|
||||
LogLevel.Error,
|
||||
"Could not parse a message that was received:\r\n\r\n" +
|
||||
e.ToString());
|
||||
string message = string.Format("Exception occurred while parsing message: {0}", e.Message);
|
||||
Logger.Write(LogLevel.Error, message);
|
||||
await MessageWriter.WriteEvent(HostingErrorEvent.Type, new HostingErrorParams
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
|
||||
// Continue the loop
|
||||
continue;
|
||||
@@ -226,18 +227,29 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var b = e.Message;
|
||||
newMessage = null;
|
||||
// Log the error and send an error event to the client
|
||||
string message = string.Format("Exception occurred while receiving message: {0}", e.Message);
|
||||
Logger.Write(LogLevel.Error, message);
|
||||
await MessageWriter.WriteEvent(HostingErrorEvent.Type, new HostingErrorParams
|
||||
{
|
||||
Message = message
|
||||
});
|
||||
|
||||
// Continue the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
// The message could be null if there was an error parsing the
|
||||
// previous message. In this case, do not try to dispatch it.
|
||||
if (newMessage != null)
|
||||
{
|
||||
// Verbose logging
|
||||
string logMessage = string.Format("Received message of type[{0}] and method[{1}]",
|
||||
newMessage.MessageType, newMessage.Method);
|
||||
Logger.Write(LogLevel.Verbose, logMessage);
|
||||
|
||||
// Process the message
|
||||
await this.DispatchMessage(
|
||||
newMessage,
|
||||
this.MessageWriter);
|
||||
await this.DispatchMessage(newMessage, this.MessageWriter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public class MessageParseException : Exception
|
||||
{
|
||||
@@ -3,7 +3,7 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the possible message protocol types.
|
||||
@@ -3,16 +3,17 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public class MessageReader
|
||||
{
|
||||
@@ -23,22 +24,22 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
|
||||
private const int CR = 0x0D;
|
||||
private const int LF = 0x0A;
|
||||
private static string[] NewLineDelimiters = new string[] { Environment.NewLine };
|
||||
private static readonly string[] NewLineDelimiters = { Environment.NewLine };
|
||||
|
||||
private Stream inputStream;
|
||||
private IMessageSerializer messageSerializer;
|
||||
private Encoding messageEncoding;
|
||||
private readonly Stream inputStream;
|
||||
private readonly IMessageSerializer messageSerializer;
|
||||
private readonly Encoding messageEncoding;
|
||||
|
||||
private ReadState readState;
|
||||
private bool needsMoreData = true;
|
||||
private int readOffset;
|
||||
private int bufferEndOffset;
|
||||
private byte[] messageBuffer = new byte[DefaultBufferSize];
|
||||
private byte[] messageBuffer;
|
||||
|
||||
private int expectedContentLength;
|
||||
private Dictionary<string, string> messageHeaders;
|
||||
|
||||
enum ReadState
|
||||
private enum ReadState
|
||||
{
|
||||
Headers,
|
||||
Content
|
||||
@@ -83,7 +84,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
this.needsMoreData = false;
|
||||
|
||||
// Do we need to look for message headers?
|
||||
if (this.readState == ReadState.Headers &&
|
||||
if (this.readState == ReadState.Headers &&
|
||||
!this.TryReadMessageHeaders())
|
||||
{
|
||||
// If we don't have enough data to read headers yet, keep reading
|
||||
@@ -92,7 +93,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
}
|
||||
|
||||
// Do we need to look for message content?
|
||||
if (this.readState == ReadState.Content &&
|
||||
if (this.readState == ReadState.Content &&
|
||||
!this.TryReadMessageContent(out messageContent))
|
||||
{
|
||||
// If we don't have enough data yet to construct the content, keep reading
|
||||
@@ -104,16 +105,12 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that we have a message, reset the buffer's state
|
||||
ShiftBufferBytesAndShrink(readOffset);
|
||||
|
||||
// Get the JObject for the JSON content
|
||||
JObject messageObject = JObject.Parse(messageContent);
|
||||
|
||||
// Load the message
|
||||
Logger.Write(
|
||||
LogLevel.Verbose,
|
||||
string.Format(
|
||||
"READ MESSAGE:\r\n\r\n{0}",
|
||||
messageObject.ToString(Formatting.Indented)));
|
||||
|
||||
// Return the parsed message
|
||||
return this.messageSerializer.DeserializeMessage(messageObject);
|
||||
}
|
||||
@@ -160,8 +157,7 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
{
|
||||
int scanOffset = this.readOffset;
|
||||
|
||||
// Scan for the final double-newline that marks the
|
||||
// end of the header lines
|
||||
// Scan for the final double-newline that marks the end of the header lines
|
||||
while (scanOffset + 3 < this.bufferEndOffset &&
|
||||
(this.messageBuffer[scanOffset] != CR ||
|
||||
this.messageBuffer[scanOffset + 1] != LF ||
|
||||
@@ -171,45 +167,51 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
scanOffset++;
|
||||
}
|
||||
|
||||
// No header or body separator found (e.g CRLFCRLF)
|
||||
// Make sure we haven't reached the end of the buffer without finding a separator (e.g CRLFCRLF)
|
||||
if (scanOffset + 3 >= this.bufferEndOffset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.messageHeaders = new Dictionary<string, string>();
|
||||
// Convert the header block into a array of lines
|
||||
var headers = Encoding.ASCII.GetString(this.messageBuffer, this.readOffset, scanOffset)
|
||||
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var headers =
|
||||
Encoding.ASCII
|
||||
.GetString(this.messageBuffer, this.readOffset, scanOffset)
|
||||
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Read each header and store it in the dictionary
|
||||
foreach (var header in headers)
|
||||
try
|
||||
{
|
||||
int currentLength = header.IndexOf(':');
|
||||
if (currentLength == -1)
|
||||
// Read each header and store it in the dictionary
|
||||
this.messageHeaders = new Dictionary<string, string>();
|
||||
foreach (var header in headers)
|
||||
{
|
||||
throw new ArgumentException("Message header must separate key and value using :");
|
||||
int currentLength = header.IndexOf(':');
|
||||
if (currentLength == -1)
|
||||
{
|
||||
throw new ArgumentException("Message header must separate key and value using :");
|
||||
}
|
||||
|
||||
var key = header.Substring(0, currentLength);
|
||||
var value = header.Substring(currentLength + 1).Trim();
|
||||
this.messageHeaders[key] = value;
|
||||
}
|
||||
|
||||
var key = header.Substring(0, currentLength);
|
||||
var value = header.Substring(currentLength + 1).Trim();
|
||||
this.messageHeaders[key] = value;
|
||||
}
|
||||
// Parse out the content length as an int
|
||||
string contentLengthString;
|
||||
if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
|
||||
{
|
||||
throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
|
||||
}
|
||||
|
||||
// Make sure a Content-Length header was present, otherwise it
|
||||
// is a fatal error
|
||||
string contentLengthString = null;
|
||||
if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
|
||||
{
|
||||
throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
|
||||
// Parse the content length to an integer
|
||||
if (!int.TryParse(contentLengthString, out this.expectedContentLength))
|
||||
{
|
||||
throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the content length to an integer
|
||||
if (!int.TryParse(contentLengthString, out this.expectedContentLength))
|
||||
catch (Exception)
|
||||
{
|
||||
throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
|
||||
// The content length was invalid or missing. Trash the buffer we've read
|
||||
ShiftBufferBytesAndShrink(scanOffset + 4);
|
||||
throw;
|
||||
}
|
||||
|
||||
// Skip past the headers plus the newline characters
|
||||
@@ -232,31 +234,40 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
}
|
||||
|
||||
// Convert the message contents to a string using the specified encoding
|
||||
messageContent =
|
||||
this.messageEncoding.GetString(
|
||||
this.messageBuffer,
|
||||
this.readOffset,
|
||||
this.expectedContentLength);
|
||||
messageContent = this.messageEncoding.GetString(
|
||||
this.messageBuffer,
|
||||
this.readOffset,
|
||||
this.expectedContentLength);
|
||||
|
||||
// Move the remaining bytes to the front of the buffer for the next message
|
||||
var remainingByteCount = this.bufferEndOffset - (this.expectedContentLength + this.readOffset);
|
||||
Buffer.BlockCopy(
|
||||
this.messageBuffer,
|
||||
this.expectedContentLength + this.readOffset,
|
||||
this.messageBuffer,
|
||||
0,
|
||||
remainingByteCount);
|
||||
readOffset += expectedContentLength;
|
||||
|
||||
// Reset the offsets for the next read
|
||||
this.readOffset = 0;
|
||||
this.bufferEndOffset = remainingByteCount;
|
||||
|
||||
// Done reading content, now look for headers
|
||||
// Done reading content, now look for headers for the next message
|
||||
this.readState = ReadState.Headers;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ShiftBufferBytesAndShrink(int bytesToRemove)
|
||||
{
|
||||
// Create a new buffer that is shrunken by the number of bytes to remove
|
||||
// Note: by using Max, we can guarantee a buffer of at least default buffer size
|
||||
byte[] newBuffer = new byte[Math.Max(messageBuffer.Length - bytesToRemove, DefaultBufferSize)];
|
||||
|
||||
// If we need to do shifting, do the shifting
|
||||
if (bytesToRemove <= messageBuffer.Length)
|
||||
{
|
||||
// Copy the existing buffer starting at the offset to remove
|
||||
Buffer.BlockCopy(messageBuffer, bytesToRemove, newBuffer, 0, bufferEndOffset - bytesToRemove);
|
||||
}
|
||||
|
||||
// Make the new buffer the message buffer
|
||||
messageBuffer = newBuffer;
|
||||
|
||||
// Reset the read offset and the end offset
|
||||
readOffset = 0;
|
||||
bufferEndOffset -= bytesToRemove;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,16 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public class MessageWriter
|
||||
{
|
||||
@@ -3,19 +3,20 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Channel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides behavior for a client or server endpoint that
|
||||
/// communicates using the specified protocol.
|
||||
/// </summary>
|
||||
public class ProtocolEndpoint : IMessageSender
|
||||
public class ProtocolEndpoint : IMessageSender, IProtocolEndpoint
|
||||
{
|
||||
private bool isStarted;
|
||||
private int currentMessageId;
|
||||
@@ -3,10 +3,11 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol
|
||||
{
|
||||
public class RequestContext<TResult>
|
||||
{
|
||||
@@ -19,7 +20,9 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
this.messageWriter = messageWriter;
|
||||
}
|
||||
|
||||
public async Task SendResult(TResult resultDetails)
|
||||
public RequestContext() { }
|
||||
|
||||
public virtual async Task SendResult(TResult resultDetails)
|
||||
{
|
||||
await this.messageWriter.WriteResponse<TResult>(
|
||||
resultDetails,
|
||||
@@ -27,14 +30,14 @@ namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
requestMessage.Id);
|
||||
}
|
||||
|
||||
public async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
|
||||
public virtual async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
|
||||
{
|
||||
await this.messageWriter.WriteEvent(
|
||||
eventType,
|
||||
eventParams);
|
||||
}
|
||||
|
||||
public async Task SendError(object errorDetails)
|
||||
public virtual async Task SendError(object errorDetails)
|
||||
{
|
||||
await this.messageWriter.WriteMessage(
|
||||
Message.ResponseError(
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a common interface for message serializers.
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes messages in the JSON RPC format. Used primarily
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol.Serializers
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Serializers
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes messages in the V8 format. Used primarily for debug adapters.
|
||||
168
src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs
Normal file
168
src/Microsoft.SqlTools.ServiceLayer/Hosting/ServiceHost.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// SQL Tools VS Code Language Server request handler. Provides the entire JSON RPC
|
||||
/// implementation for sending/receiving JSON requests and dispatching the requests to
|
||||
/// handlers that are registered prior to startup.
|
||||
/// </summary>
|
||||
public sealed class ServiceHost : ServiceHostBase
|
||||
{
|
||||
#region Singleton Instance Code
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance of the service host for internal storage
|
||||
/// </summary>
|
||||
private static readonly Lazy<ServiceHost> instance = new Lazy<ServiceHost>(() => new ServiceHost());
|
||||
|
||||
/// <summary>
|
||||
/// Current instance of the ServiceHost
|
||||
/// </summary>
|
||||
public static ServiceHost Instance
|
||||
{
|
||||
get { return instance.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs new instance of ServiceHost using the host and profile details provided.
|
||||
/// Access is private to ensure only one instance exists at a time.
|
||||
/// </summary>
|
||||
private ServiceHost() : base(new StdioServerChannel())
|
||||
{
|
||||
// Initialize the shutdown activities
|
||||
shutdownCallbacks = new List<ShutdownCallback>();
|
||||
initializeCallbacks = new List<InitializeCallback>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provide initialization that must occur after the service host is started
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// Register the requests that this service host will handle
|
||||
this.SetRequestHandler(InitializeRequest.Type, this.HandleInitializeRequest);
|
||||
this.SetRequestHandler(ShutdownRequest.Type, this.HandleShutdownRequest);
|
||||
this.SetRequestHandler(VersionRequest.Type, HandleVersionRequest);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Member Variables
|
||||
|
||||
public delegate Task ShutdownCallback(object shutdownParams, RequestContext<object> shutdownRequestContext);
|
||||
|
||||
public delegate Task InitializeCallback(InitializeRequest startupParams, RequestContext<InitializeResult> requestContext);
|
||||
|
||||
private readonly List<ShutdownCallback> shutdownCallbacks;
|
||||
|
||||
private readonly List<InitializeCallback> initializeCallbacks;
|
||||
|
||||
private static readonly Version serviceVersion = Assembly.GetEntryAssembly().GetName().Version;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new callback to be called when the shutdown request is submitted
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback to perform when a shutdown request is submitted</param>
|
||||
public void RegisterShutdownTask(ShutdownCallback callback)
|
||||
{
|
||||
shutdownCallbacks.Add(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new method to be called when the initialize request is submitted
|
||||
/// </summary>
|
||||
/// <param name="callback">Callback to perform when an initialize request is submitted</param>
|
||||
public void RegisterInitializeTask(InitializeCallback callback)
|
||||
{
|
||||
initializeCallbacks.Add(callback);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Handlers
|
||||
|
||||
/// <summary>
|
||||
/// Handles the shutdown event for the Language Server
|
||||
/// </summary>
|
||||
private async Task HandleShutdownRequest(object shutdownParams, RequestContext<object> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Normal, "Service host is shutting down...");
|
||||
|
||||
// Call all the shutdown methods provided by the service components
|
||||
Task[] shutdownTasks = shutdownCallbacks.Select(t => t(shutdownParams, requestContext)).ToArray();
|
||||
await Task.WhenAll(shutdownTasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the initialization request
|
||||
/// </summary>
|
||||
/// <param name="initializeParams"></param>
|
||||
/// <param name="requestContext"></param>
|
||||
/// <returns></returns>
|
||||
private async Task HandleInitializeRequest(InitializeRequest initializeParams, RequestContext<InitializeResult> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleInitializationRequest");
|
||||
|
||||
// Call all tasks that registered on the initialize request
|
||||
var initializeTasks = initializeCallbacks.Select(t => t(initializeParams, requestContext));
|
||||
await Task.WhenAll(initializeTasks);
|
||||
|
||||
// TODO: Figure out where this needs to go to be agnostic of the language
|
||||
|
||||
// Send back what this server can do
|
||||
await requestContext.SendResult(
|
||||
new InitializeResult
|
||||
{
|
||||
Capabilities = new ServerCapabilities
|
||||
{
|
||||
TextDocumentSync = TextDocumentSyncKind.Incremental,
|
||||
DefinitionProvider = true,
|
||||
ReferencesProvider = true,
|
||||
DocumentHighlightProvider = true,
|
||||
DocumentSymbolProvider = true,
|
||||
WorkspaceSymbolProvider = true,
|
||||
CompletionProvider = new CompletionOptions
|
||||
{
|
||||
ResolveProvider = true,
|
||||
TriggerCharacters = new string[] { ".", "-", ":", "\\" }
|
||||
},
|
||||
SignatureHelpProvider = new SignatureHelpOptions
|
||||
{
|
||||
TriggerCharacters = new string[] { " " } // TODO: Other characters here?
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the version request. Sends back the server version as result.
|
||||
/// </summary>
|
||||
private static async Task HandleVersionRequest(
|
||||
object versionRequestParams,
|
||||
RequestContext<string> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleVersionRequest");
|
||||
await requestContext.SendResult(serviceVersion.ToString());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Channel;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.Hosting
|
||||
{
|
||||
public abstract class ServiceHostBase : ProtocolEndpoint
|
||||
{
|
||||
private bool isStarted;
|
||||
private TaskCompletionSource<bool> serverExitedTask;
|
||||
|
||||
protected ServiceHostBase(ChannelBase serverChannel) :
|
||||
base(serverChannel, MessageProtocolType.LanguageServer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task OnStart()
|
||||
{
|
||||
// Register handlers for server lifetime messages
|
||||
|
||||
this.SetEventHandler(ExitNotification.Type, this.HandleExitNotification);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
private async Task HandleExitNotification(
|
||||
object exitParams,
|
||||
EventContext eventContext)
|
||||
{
|
||||
// Stop the server channel
|
||||
await this.Stop();
|
||||
|
||||
// Notify any waiter that the server has exited
|
||||
if (this.serverExitedTask != null)
|
||||
{
|
||||
this.serverExitedTask.SetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SqlClient;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using Microsoft.SqlServer.Management.SmoMetadataProvider;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Binder;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Intellisense;
|
||||
using Microsoft.SqlServer.Management.SqlParser.MetadataProvider;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Main class for Autocomplete functionality
|
||||
/// </summary>
|
||||
public class AutoCompleteService
|
||||
{
|
||||
#region Singleton Instance Implementation
|
||||
|
||||
/// <summary>
|
||||
/// Singleton service instance
|
||||
/// </summary>
|
||||
private static Lazy<AutoCompleteService> instance
|
||||
= new Lazy<AutoCompleteService>(() => new AutoCompleteService());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the singleton service instance
|
||||
/// </summary>
|
||||
public static AutoCompleteService Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return instance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default, parameterless constructor.
|
||||
/// Internal constructor for use in test cases only
|
||||
/// </summary>
|
||||
internal AutoCompleteService()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private ConnectionService connectionService = null;
|
||||
|
||||
/// <summary>
|
||||
/// Internal for testing purposes only
|
||||
/// </summary>
|
||||
internal ConnectionService ConnectionServiceInstance
|
||||
{
|
||||
get
|
||||
{
|
||||
if(connectionService == null)
|
||||
{
|
||||
connectionService = ConnectionService.Instance;
|
||||
}
|
||||
return connectionService;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
connectionService = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void InitializeService(ServiceHost serviceHost)
|
||||
{
|
||||
// Register auto-complete request handler
|
||||
serviceHost.SetRequestHandler(CompletionRequest.Type, HandleCompletionRequest);
|
||||
|
||||
// Register a callback for when a connection is created
|
||||
ConnectionServiceInstance.RegisterOnConnectionTask(UpdateAutoCompleteCache);
|
||||
|
||||
// Register a callback for when a connection is closed
|
||||
ConnectionServiceInstance.RegisterOnDisconnectTask(RemoveAutoCompleteCacheUriReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-complete completion provider request callback
|
||||
/// </summary>
|
||||
/// <param name="textDocumentPosition"></param>
|
||||
/// <param name="requestContext"></param>
|
||||
/// <returns></returns>
|
||||
private static async Task HandleCompletionRequest(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
RequestContext<CompletionItem[]> requestContext)
|
||||
{
|
||||
// get the current list of completion items and return to client
|
||||
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(
|
||||
textDocumentPosition.TextDocument.Uri);
|
||||
|
||||
ConnectionInfo connInfo;
|
||||
ConnectionService.Instance.TryFindConnection(
|
||||
scriptFile.ClientFilePath,
|
||||
out connInfo);
|
||||
|
||||
var completionItems = Instance.GetCompletionItems(
|
||||
textDocumentPosition, scriptFile, connInfo);
|
||||
|
||||
await requestContext.SendResult(completionItems);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a reference to an autocomplete cache from a URI. If
|
||||
/// it is the last URI connected to a particular connection,
|
||||
/// then remove the cache.
|
||||
/// </summary>
|
||||
public async Task RemoveAutoCompleteCacheUriReference(ConnectionSummary summary)
|
||||
{
|
||||
// currently this method is disabled, but we need to reimplement now that the
|
||||
// implementation of the 'cache' has changed.
|
||||
await Task.FromResult(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cached autocomplete candidate list when the user connects to a database
|
||||
/// </summary>
|
||||
/// <param name="info"></param>
|
||||
public async Task UpdateAutoCompleteCache(ConnectionInfo info)
|
||||
{
|
||||
await Task.Run( () =>
|
||||
{
|
||||
if (!LanguageService.Instance.ScriptParseInfoMap.ContainsKey(info.OwnerUri))
|
||||
{
|
||||
var sqlConn = info.SqlConnection as SqlConnection;
|
||||
if (sqlConn != null)
|
||||
{
|
||||
var srvConn = new ServerConnection(sqlConn);
|
||||
var displayInfoProvider = new MetadataDisplayInfoProvider();
|
||||
var metadataProvider = SmoMetadataProvider.CreateConnectedProvider(srvConn);
|
||||
var binder = BinderProvider.CreateBinder(metadataProvider);
|
||||
|
||||
LanguageService.Instance.ScriptParseInfoMap.Add(info.OwnerUri,
|
||||
new ScriptParseInfo()
|
||||
{
|
||||
Binder = binder,
|
||||
MetadataProvider = metadataProvider,
|
||||
MetadataDisplayInfoProvider = displayInfoProvider
|
||||
});
|
||||
|
||||
var scriptFile = WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetFile(info.OwnerUri);
|
||||
|
||||
LanguageService.Instance.ParseAndBind(scriptFile, info);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the position of the previous delimeter for autocomplete token replacement.
|
||||
/// SQL Parser may have similar functionality in which case we'll delete this method.
|
||||
/// </summary>
|
||||
/// <param name="sql"></param>
|
||||
/// <param name="startRow"></param>
|
||||
/// <param name="startColumn"></param>
|
||||
/// <returns></returns>
|
||||
private int PositionOfPrevDelimeter(string sql, int startRow, int startColumn)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sql))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
int prevLineColumns = 0;
|
||||
for (int i = 0; i < startRow; ++i)
|
||||
{
|
||||
while (sql[prevLineColumns] != '\n' && prevLineColumns < sql.Length)
|
||||
{
|
||||
++prevLineColumns;
|
||||
}
|
||||
++prevLineColumns;
|
||||
}
|
||||
|
||||
startColumn += prevLineColumns;
|
||||
|
||||
if (startColumn - 1 < sql.Length)
|
||||
{
|
||||
while (--startColumn >= prevLineColumns)
|
||||
{
|
||||
if (sql[startColumn] == ' '
|
||||
|| sql[startColumn] == '\t'
|
||||
|| sql[startColumn] == '\n'
|
||||
|| sql[startColumn] == '.'
|
||||
|| sql[startColumn] == '+'
|
||||
|| sql[startColumn] == '-'
|
||||
|| sql[startColumn] == '*'
|
||||
|| sql[startColumn] == '>'
|
||||
|| sql[startColumn] == '<'
|
||||
|| sql[startColumn] == '='
|
||||
|| sql[startColumn] == '/'
|
||||
|| sql[startColumn] == '%')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return startColumn + 1 - prevLineColumns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a reparse and bind is required to provide autocomplete
|
||||
/// </summary>
|
||||
/// <param name="info"></param>
|
||||
/// <returns>TEMP: Currently hard-coded to false for perf</returns>
|
||||
private bool RequiresReparse(ScriptParseInfo info)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a list of Declaration objects to CompletionItem objects
|
||||
/// since VS Code expects CompletionItems but SQL Parser works with Declarations
|
||||
/// </summary>
|
||||
/// <param name="suggestions"></param>
|
||||
/// <param name="cursorRow"></param>
|
||||
/// <param name="cursorColumn"></param>
|
||||
/// <returns></returns>
|
||||
private CompletionItem[] ConvertDeclarationsToCompletionItems(
|
||||
IEnumerable<Declaration> suggestions,
|
||||
int row,
|
||||
int startColumn,
|
||||
int endColumn)
|
||||
{
|
||||
List<CompletionItem> completions = new List<CompletionItem>();
|
||||
foreach (var autoCompleteItem in suggestions)
|
||||
{
|
||||
// convert the completion item candidates into CompletionItems
|
||||
completions.Add(new CompletionItem()
|
||||
{
|
||||
Label = autoCompleteItem.Title,
|
||||
Kind = CompletionItemKind.Keyword,
|
||||
Detail = autoCompleteItem.Title,
|
||||
Documentation = autoCompleteItem.Description,
|
||||
TextEdit = new TextEdit
|
||||
{
|
||||
NewText = autoCompleteItem.Title,
|
||||
Range = new Range
|
||||
{
|
||||
Start = new Position
|
||||
{
|
||||
Line = row,
|
||||
Character = startColumn
|
||||
},
|
||||
End = new Position
|
||||
{
|
||||
Line = row,
|
||||
Character = endColumn
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return completions.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the completion item list for the current text position.
|
||||
/// This method does not await cache builds since it expects to return quickly
|
||||
/// </summary>
|
||||
/// <param name="textDocumentPosition"></param>
|
||||
public CompletionItem[] GetCompletionItems(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
ScriptFile scriptFile,
|
||||
ConnectionInfo connInfo)
|
||||
{
|
||||
string filePath = textDocumentPosition.TextDocument.Uri;
|
||||
|
||||
// Take a reference to the list at a point in time in case we update and replace the list
|
||||
if (connInfo == null
|
||||
|| !LanguageService.Instance.ScriptParseInfoMap.ContainsKey(textDocumentPosition.TextDocument.Uri))
|
||||
{
|
||||
return new CompletionItem[0];
|
||||
}
|
||||
|
||||
// reparse and bind the SQL statement if needed
|
||||
var scriptParseInfo = LanguageService.Instance.ScriptParseInfoMap[textDocumentPosition.TextDocument.Uri];
|
||||
if (RequiresReparse(scriptParseInfo))
|
||||
{
|
||||
LanguageService.Instance.ParseAndBind(scriptFile, connInfo);
|
||||
}
|
||||
|
||||
if (scriptParseInfo.ParseResult == null)
|
||||
{
|
||||
return new CompletionItem[0];
|
||||
}
|
||||
|
||||
// get the completion list from SQL Parser
|
||||
var suggestions = Resolver.FindCompletions(
|
||||
scriptParseInfo.ParseResult,
|
||||
textDocumentPosition.Position.Line + 1,
|
||||
textDocumentPosition.Position.Character + 1,
|
||||
scriptParseInfo.MetadataDisplayInfoProvider);
|
||||
|
||||
// convert the suggestion list to the VS Code format
|
||||
return ConvertDeclarationsToCompletionItems(
|
||||
suggestions,
|
||||
textDocumentPosition.Position.Line,
|
||||
PositionOfPrevDelimeter(
|
||||
scriptFile.Contents,
|
||||
textDocumentPosition.Position.Line,
|
||||
textDocumentPosition.Position.Character),
|
||||
textDocumentPosition.Position.Character);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@
|
||||
//
|
||||
|
||||
using System.Diagnostics;
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class CompletionRequest
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class DefinitionRequest
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class PublishDiagnosticsNotification
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public enum DocumentHighlightKind
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class ExpandAliasRequest
|
||||
{
|
||||
@@ -3,10 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class FindModuleRequest
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class MarkedString
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
class InstallModuleRequest
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class ReferencesRequest
|
||||
{
|
||||
@@ -3,9 +3,9 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class ShowOnlineHelpRequest
|
||||
{
|
||||
@@ -3,9 +3,10 @@
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using Microsoft.SqlTools.EditorServices.Protocol.MessageProtocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.EditorServices.Protocol.LanguageServer
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts
|
||||
{
|
||||
public class SignatureHelpRequest
|
||||
{
|
||||
@@ -0,0 +1,538 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.EditorServices.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting;
|
||||
using Microsoft.SqlTools.ServiceLayer.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.LanguageServices.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.SqlContext;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace;
|
||||
using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Parser;
|
||||
using Location = Microsoft.SqlTools.ServiceLayer.Workspace.Contracts.Location;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlServer.Management.SqlParser.Binder;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using Microsoft.SqlServer.Management.SqlParser;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
|
||||
{
|
||||
/// <summary>
|
||||
/// Main class for Language Service functionality including anything that reqires knowledge of
|
||||
/// the language to perfom, such as definitions, intellisense, etc.
|
||||
/// </summary>
|
||||
public sealed class LanguageService
|
||||
{
|
||||
|
||||
#region Singleton Instance Implementation
|
||||
|
||||
private static readonly Lazy<LanguageService> instance = new Lazy<LanguageService>(() => new LanguageService());
|
||||
|
||||
private Lazy<Dictionary<string, ScriptParseInfo>> scriptParseInfoMap
|
||||
= new Lazy<Dictionary<string, ScriptParseInfo>>(() => new Dictionary<string, ScriptParseInfo>());
|
||||
|
||||
internal Dictionary<string, ScriptParseInfo> ScriptParseInfoMap
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.scriptParseInfoMap.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public static LanguageService Instance
|
||||
{
|
||||
get { return instance.Value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default, parameterless constructor.
|
||||
/// </summary>
|
||||
internal LanguageService()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
private static CancellationTokenSource ExistingRequestCancellation { get; set; }
|
||||
|
||||
private SqlToolsSettings CurrentSettings
|
||||
{
|
||||
get { return WorkspaceService<SqlToolsSettings>.Instance.CurrentSettings; }
|
||||
}
|
||||
|
||||
private Workspace.Workspace CurrentWorkspace
|
||||
{
|
||||
get { return WorkspaceService<SqlToolsSettings>.Instance.Workspace; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current SQL Tools context
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private SqlToolsContext Context { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the Language Service instance
|
||||
/// </summary>
|
||||
/// <param name="serviceHost"></param>
|
||||
/// <param name="context"></param>
|
||||
public void InitializeService(ServiceHost serviceHost, SqlToolsContext context)
|
||||
{
|
||||
// Register the requests that this service will handle
|
||||
serviceHost.SetRequestHandler(DefinitionRequest.Type, HandleDefinitionRequest);
|
||||
serviceHost.SetRequestHandler(ReferencesRequest.Type, HandleReferencesRequest);
|
||||
serviceHost.SetRequestHandler(CompletionResolveRequest.Type, HandleCompletionResolveRequest);
|
||||
serviceHost.SetRequestHandler(SignatureHelpRequest.Type, HandleSignatureHelpRequest);
|
||||
serviceHost.SetRequestHandler(DocumentHighlightRequest.Type, HandleDocumentHighlightRequest);
|
||||
serviceHost.SetRequestHandler(HoverRequest.Type, HandleHoverRequest);
|
||||
serviceHost.SetRequestHandler(DocumentSymbolRequest.Type, HandleDocumentSymbolRequest);
|
||||
serviceHost.SetRequestHandler(WorkspaceSymbolRequest.Type, HandleWorkspaceSymbolRequest);
|
||||
|
||||
// Register a no-op shutdown task for validation of the shutdown logic
|
||||
serviceHost.RegisterShutdownTask(async (shutdownParams, shutdownRequestContext) =>
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "Shutting down language service");
|
||||
await Task.FromResult(0);
|
||||
});
|
||||
|
||||
// Register the configuration update handler
|
||||
WorkspaceService<SqlToolsSettings>.Instance.RegisterConfigChangeCallback(HandleDidChangeConfigurationNotification);
|
||||
|
||||
// Register the file change update handler
|
||||
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocChangeCallback(HandleDidChangeTextDocumentNotification);
|
||||
|
||||
// Register the file open update handler
|
||||
WorkspaceService<SqlToolsSettings>.Instance.RegisterTextDocOpenCallback(HandleDidOpenTextDocumentNotification);
|
||||
|
||||
// Store the SqlToolsContext for future use
|
||||
Context = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the SQL text and binds it to the SMO metadata provider if connected
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
/// <param name="sqlText"></param>
|
||||
/// <returns></returns>
|
||||
public ParseResult ParseAndBind(ScriptFile scriptFile, ConnectionInfo connInfo)
|
||||
{
|
||||
ScriptParseInfo parseInfo = null;
|
||||
if (this.ScriptParseInfoMap.ContainsKey(scriptFile.ClientFilePath))
|
||||
{
|
||||
parseInfo = this.ScriptParseInfoMap[scriptFile.ClientFilePath];
|
||||
}
|
||||
|
||||
// parse current SQL file contents to retrieve a list of errors
|
||||
ParseOptions parseOptions = new ParseOptions();
|
||||
ParseResult parseResult = Parser.IncrementalParse(
|
||||
scriptFile.Contents,
|
||||
parseInfo != null ? parseInfo.ParseResult : null,
|
||||
parseOptions);
|
||||
|
||||
// save previous result for next incremental parse
|
||||
if (parseInfo != null)
|
||||
{
|
||||
parseInfo.ParseResult = parseResult;
|
||||
}
|
||||
|
||||
if (connInfo != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<ParseResult> parseResults = new List<ParseResult>();
|
||||
parseResults.Add(parseResult);
|
||||
parseInfo.Binder.Bind(
|
||||
parseResults,
|
||||
connInfo.ConnectionDetails.DatabaseName,
|
||||
BindMode.Batch);
|
||||
}
|
||||
catch (ConnectionException)
|
||||
{
|
||||
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
|
||||
}
|
||||
catch (SqlParserInternalBinderError)
|
||||
{
|
||||
Logger.Write(LogLevel.Error, "Hit connection exception while binding - disposing binder object...");
|
||||
}
|
||||
}
|
||||
|
||||
return parseResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of semantic diagnostic marks for the provided script file
|
||||
/// </summary>
|
||||
/// <param name="scriptFile"></param>
|
||||
public ScriptFileMarker[] GetSemanticMarkers(ScriptFile scriptFile)
|
||||
{
|
||||
ConnectionInfo connInfo;
|
||||
ConnectionService.Instance.TryFindConnection(
|
||||
scriptFile.ClientFilePath,
|
||||
out connInfo);
|
||||
|
||||
var parseResult = ParseAndBind(scriptFile, connInfo);
|
||||
|
||||
// build a list of SQL script file markers from the errors
|
||||
List<ScriptFileMarker> markers = new List<ScriptFileMarker>();
|
||||
foreach (var error in parseResult.Errors)
|
||||
{
|
||||
markers.Add(new ScriptFileMarker()
|
||||
{
|
||||
Message = error.Message,
|
||||
Level = ScriptFileMarkerLevel.Error,
|
||||
ScriptRegion = new ScriptRegion()
|
||||
{
|
||||
File = scriptFile.FilePath,
|
||||
StartLineNumber = error.Start.LineNumber,
|
||||
StartColumnNumber = error.Start.ColumnNumber,
|
||||
StartOffset = 0,
|
||||
EndLineNumber = error.End.LineNumber,
|
||||
EndColumnNumber = error.End.ColumnNumber,
|
||||
EndOffset = 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return markers.ToArray();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Handlers
|
||||
|
||||
private static async Task HandleDefinitionRequest(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
RequestContext<Location[]> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDefinitionRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleReferencesRequest(
|
||||
ReferencesParams referencesParams,
|
||||
RequestContext<Location[]> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleReferencesRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleCompletionResolveRequest(
|
||||
CompletionItem completionItem,
|
||||
RequestContext<CompletionItem> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleCompletionResolveRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleSignatureHelpRequest(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
RequestContext<SignatureHelp> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleSignatureHelpRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleDocumentHighlightRequest(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
RequestContext<DocumentHighlight[]> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDocumentHighlightRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleHoverRequest(
|
||||
TextDocumentPosition textDocumentPosition,
|
||||
RequestContext<Hover> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleHoverRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleDocumentSymbolRequest(
|
||||
DocumentSymbolParams documentSymbolParams,
|
||||
RequestContext<SymbolInformation[]> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleDocumentSymbolRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
private static async Task HandleWorkspaceSymbolRequest(
|
||||
WorkspaceSymbolParams workspaceSymbolParams,
|
||||
RequestContext<SymbolInformation[]> requestContext)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "HandleWorkspaceSymbolRequest");
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Handlers for Events from Other Services
|
||||
|
||||
/// <summary>
|
||||
/// Handle the file open notification
|
||||
/// </summary>
|
||||
/// <param name="scriptFile"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
/// <returns></returns>
|
||||
public async Task HandleDidOpenTextDocumentNotification(
|
||||
ScriptFile scriptFile,
|
||||
EventContext eventContext)
|
||||
{
|
||||
await this.RunScriptDiagnostics(
|
||||
new ScriptFile[] { scriptFile },
|
||||
eventContext);
|
||||
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles text document change events
|
||||
/// </summary>
|
||||
/// <param name="textChangeParams"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
/// <returns></returns>
|
||||
public async Task HandleDidChangeTextDocumentNotification(ScriptFile[] changedFiles, EventContext eventContext)
|
||||
{
|
||||
await this.RunScriptDiagnostics(
|
||||
changedFiles.ToArray(),
|
||||
eventContext);
|
||||
|
||||
await Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle the file configuration change notification
|
||||
/// </summary>
|
||||
/// <param name="newSettings"></param>
|
||||
/// <param name="oldSettings"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
public async Task HandleDidChangeConfigurationNotification(
|
||||
SqlToolsSettings newSettings,
|
||||
SqlToolsSettings oldSettings,
|
||||
EventContext eventContext)
|
||||
{
|
||||
// If script analysis settings have changed we need to clear & possibly update the current diagnostic records.
|
||||
bool oldScriptAnalysisEnabled = oldSettings.ScriptAnalysis.Enable.HasValue;
|
||||
if ((oldScriptAnalysisEnabled != newSettings.ScriptAnalysis.Enable))
|
||||
{
|
||||
// If the user just turned off script analysis or changed the settings path, send a diagnostics
|
||||
// event to clear the analysis markers that they already have.
|
||||
if (!newSettings.ScriptAnalysis.Enable.Value)
|
||||
{
|
||||
ScriptFileMarker[] emptyAnalysisDiagnostics = new ScriptFileMarker[0];
|
||||
|
||||
foreach (var scriptFile in WorkspaceService<SqlToolsSettings>.Instance.Workspace.GetOpenedFiles())
|
||||
{
|
||||
await PublishScriptDiagnostics(scriptFile, emptyAnalysisDiagnostics, eventContext);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.RunScriptDiagnostics(CurrentWorkspace.GetOpenedFiles(), eventContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the settings in the current
|
||||
CurrentSettings.EnableProfileLoading = newSettings.EnableProfileLoading;
|
||||
CurrentSettings.ScriptAnalysis.Update(newSettings.ScriptAnalysis, CurrentWorkspace.WorkspacePath);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Runs script diagnostics on changed files
|
||||
/// </summary>
|
||||
/// <param name="filesToAnalyze"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
private Task RunScriptDiagnostics(ScriptFile[] filesToAnalyze, EventContext eventContext)
|
||||
{
|
||||
if (!CurrentSettings.ScriptAnalysis.Enable.Value)
|
||||
{
|
||||
// If the user has disabled script analysis, skip it entirely
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
// If there's an existing task, attempt to cancel it
|
||||
try
|
||||
{
|
||||
if (ExistingRequestCancellation != null)
|
||||
{
|
||||
// Try to cancel the request
|
||||
ExistingRequestCancellation.Cancel();
|
||||
|
||||
// If cancellation didn't throw an exception,
|
||||
// clean up the existing token
|
||||
ExistingRequestCancellation.Dispose();
|
||||
ExistingRequestCancellation = null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Write(
|
||||
LogLevel.Error,
|
||||
string.Format(
|
||||
"Exception while cancelling analysis task:\n\n{0}",
|
||||
e.ToString()));
|
||||
|
||||
TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>();
|
||||
cancelTask.SetCanceled();
|
||||
return cancelTask.Task;
|
||||
}
|
||||
|
||||
// Create a fresh cancellation token and then start the task.
|
||||
// We create this on a different TaskScheduler so that we
|
||||
// don't block the main message loop thread.
|
||||
ExistingRequestCancellation = new CancellationTokenSource();
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
DelayThenInvokeDiagnostics(
|
||||
750,
|
||||
filesToAnalyze,
|
||||
eventContext,
|
||||
ExistingRequestCancellation.Token),
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
TaskScheduler.Default);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Actually run the script diagnostics after waiting for some small delay
|
||||
/// </summary>
|
||||
/// <param name="delayMilliseconds"></param>
|
||||
/// <param name="filesToAnalyze"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
private async Task DelayThenInvokeDiagnostics(
|
||||
int delayMilliseconds,
|
||||
ScriptFile[] filesToAnalyze,
|
||||
EventContext eventContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// First of all, wait for the desired delay period before
|
||||
// analyzing the provided list of files
|
||||
try
|
||||
{
|
||||
await Task.Delay(delayMilliseconds, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// If the task is cancelled, exit directly
|
||||
return;
|
||||
}
|
||||
|
||||
// If we've made it past the delay period then we don't care
|
||||
// about the cancellation token anymore. This could happen
|
||||
// when the user stops typing for long enough that the delay
|
||||
// period ends but then starts typing while analysis is going
|
||||
// on. It makes sense to send back the results from the first
|
||||
// delay period while the second one is ticking away.
|
||||
|
||||
// Get the requested files
|
||||
foreach (ScriptFile scriptFile in filesToAnalyze)
|
||||
{
|
||||
Logger.Write(LogLevel.Verbose, "Analyzing script file: " + scriptFile.FilePath);
|
||||
ScriptFileMarker[] semanticMarkers = GetSemanticMarkers(scriptFile);
|
||||
Logger.Write(LogLevel.Verbose, "Analysis complete.");
|
||||
|
||||
await PublishScriptDiagnostics(scriptFile, semanticMarkers, eventContext);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send the diagnostic results back to the host application
|
||||
/// </summary>
|
||||
/// <param name="scriptFile"></param>
|
||||
/// <param name="semanticMarkers"></param>
|
||||
/// <param name="eventContext"></param>
|
||||
private static async Task PublishScriptDiagnostics(
|
||||
ScriptFile scriptFile,
|
||||
ScriptFileMarker[] semanticMarkers,
|
||||
EventContext eventContext)
|
||||
{
|
||||
var allMarkers = scriptFile.SyntaxMarkers != null
|
||||
? scriptFile.SyntaxMarkers.Concat(semanticMarkers)
|
||||
: semanticMarkers;
|
||||
|
||||
// Always send syntax and semantic errors. We want to
|
||||
// make sure no out-of-date markers are being displayed.
|
||||
await eventContext.SendEvent(
|
||||
PublishDiagnosticsNotification.Type,
|
||||
new PublishDiagnosticsNotification
|
||||
{
|
||||
Uri = scriptFile.ClientFilePath,
|
||||
Diagnostics =
|
||||
allMarkers
|
||||
.Select(GetDiagnosticFromMarker)
|
||||
.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a ScriptFileMarker to a Diagnostic that is Language Service compatible
|
||||
/// </summary>
|
||||
/// <param name="scriptFileMarker"></param>
|
||||
/// <returns></returns>
|
||||
private static Diagnostic GetDiagnosticFromMarker(ScriptFileMarker scriptFileMarker)
|
||||
{
|
||||
return new Diagnostic
|
||||
{
|
||||
Severity = MapDiagnosticSeverity(scriptFileMarker.Level),
|
||||
Message = scriptFileMarker.Message,
|
||||
Range = new Range
|
||||
{
|
||||
Start = new Position
|
||||
{
|
||||
Line = scriptFileMarker.ScriptRegion.StartLineNumber - 1,
|
||||
Character = scriptFileMarker.ScriptRegion.StartColumnNumber - 1
|
||||
},
|
||||
End = new Position
|
||||
{
|
||||
Line = scriptFileMarker.ScriptRegion.EndLineNumber - 1,
|
||||
Character = scriptFileMarker.ScriptRegion.EndColumnNumber - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map ScriptFileMarker severity to Diagnostic severity
|
||||
/// </summary>
|
||||
/// <param name="markerLevel"></param>
|
||||
private static DiagnosticSeverity MapDiagnosticSeverity(ScriptFileMarkerLevel markerLevel)
|
||||
{
|
||||
switch (markerLevel)
|
||||
{
|
||||
case ScriptFileMarkerLevel.Error:
|
||||
return DiagnosticSeverity.Error;
|
||||
|
||||
case ScriptFileMarkerLevel.Warning:
|
||||
return DiagnosticSeverity.Warning;
|
||||
|
||||
case ScriptFileMarkerLevel.Information:
|
||||
return DiagnosticSeverity.Information;
|
||||
|
||||
default:
|
||||
return DiagnosticSeverity.Error;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user