Skip to content

Commit e2f3c1a

Browse files
Merge pull request #56 from timleavitt/registry-import
Registry import command
2 parents 4e19655 + 6644ca8 commit e2f3c1a

File tree

4 files changed

+318
-12
lines changed

4 files changed

+318
-12
lines changed

package-lock.json

Lines changed: 27 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"lint-fix": "tslint --project tsconfig.json -t verbose --fix"
4141
},
4242
"dependencies": {
43-
"@types/vscode": "^1.44.0"
43+
"@types/vscode": "^1.44.0",
44+
"node-cmd": "^4.0.0"
4445
},
4546
"devDependencies": {
4647
"@types/glob": "^7.1.1",
@@ -59,7 +60,8 @@
5960
"main": "./out/extension",
6061
"activationEvents": [
6162
"onCommand:intersystems-community.servermanager.storePassword",
62-
"onCommand:intersystems-community.servermanager.clearPassword"
63+
"onCommand:intersystems-community.servermanager.clearPassword",
64+
"onCommand:intersystems-community.servermanager.importServers"
6365
],
6466
"contributes": {
6567
"configuration": {
@@ -190,7 +192,20 @@
190192
"command": "intersystems-community.servermanager.clearPassword",
191193
"category": "InterSystems Server Manager",
192194
"title": "Clear Password from Keychain"
195+
},
196+
{
197+
"command": "intersystems-community.servermanager.importServers",
198+
"category": "InterSystems Server Manager",
199+
"title": "Import Servers from Registry"
193200
}
194-
]
201+
],
202+
"menus": {
203+
"commandPalette": [
204+
{
205+
"command": "intersystems-community.servermanager.importServers",
206+
"when": "isWindows"
207+
}
208+
]
209+
}
195210
}
196211
}

src/commands/importFromRegistry.ts

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import * as vscode from "vscode";
2+
import { Keychain } from "../keychain";
3+
4+
// To avoid overhead querying the registry, cache responses (each time the command is run)
5+
const regQueryCache = new Map<string, any>();
6+
7+
export async function importFromRegistry(scope?: vscode.ConfigurationScope) {
8+
const config = vscode.workspace.getConfiguration("intersystems", scope);
9+
const serverDefinitions: any = config.get("servers");
10+
11+
const newServerNames = new Array<string>();
12+
const serversMissingUsernames = new Array<string>();
13+
14+
regQueryCache.clear();
15+
16+
return vscode.window.withProgress({
17+
"location": vscode.ProgressLocation.Notification,
18+
"cancellable": true
19+
}, async (progress, cancellationToken) => {
20+
progress.report({message: "Loading server definitions from Windows registry..."});
21+
cancellationToken.onCancellationRequested(() => {
22+
vscode.window.showInformationMessage("Cancelled server import.");
23+
});
24+
25+
// This forces the progress bar to actually show before the possibly long-running load of registry data
26+
await new Promise(resolve => setTimeout(resolve,0));
27+
28+
await loadRegistryData(config, serverDefinitions, serversMissingUsernames, newServerNames);
29+
30+
if (cancellationToken.isCancellationRequested) {
31+
return false;
32+
}
33+
return true;
34+
}).then(async (keepGoing) => {
35+
if (!keepGoing) {
36+
return;
37+
}
38+
if (!await promptForUsernames(serverDefinitions, serversMissingUsernames)) {
39+
vscode.window.showInformationMessage("Cancelled server import.");
40+
return;
41+
}
42+
await promptForPasswords(serverDefinitions, newServerNames);
43+
await config.update(`servers`, serverDefinitions, vscode.ConfigurationTarget.Global)
44+
.then(() => {
45+
return vscode.window.showInformationMessage("Server import completed.");
46+
}, (reason) => {
47+
let message = "Something went wrong importing servers.";
48+
if (reason instanceof Error) {
49+
message = reason.message;
50+
}
51+
vscode.window.showErrorMessage(message);
52+
});
53+
});
54+
}
55+
56+
async function loadRegistryData(config, serverDefinitions, serversMissingUsernames, newServerNames): Promise<void> {
57+
const cmd = require("node-cmd");
58+
const hkeyLocalMachine = "HKEY_LOCAL_MACHINE";
59+
preloadRegistryCache(cmd, "HKEY_CURRENT_USER\\Software\\InterSystems\\Cache\\Servers");
60+
for (const folder of ['','\\WOW6432Node']) {
61+
const subFolder = "\\Intersystems\\Cache\\Servers";
62+
const path = hkeyLocalMachine + "\\SOFTWARE" + folder + subFolder;
63+
preloadRegistryCache(cmd, path);
64+
const regData = cmd.runSync("reg query " + path);
65+
if (regData.data === null) {
66+
// e.g., because the key in question isn't there
67+
continue;
68+
}
69+
regData.data.split("\r\n").forEach((serverName) => {
70+
// We only want folders, not keys (e.g., DefaultServer)
71+
if (serverName.indexOf(hkeyLocalMachine) !== 0) {
72+
return;
73+
}
74+
75+
// For WOW6432Node, skip the line for the subfolder itself
76+
if (serverName.split(subFolder).pop().length === 0) {
77+
return;
78+
}
79+
80+
// remove HKEY_LOCAL_MACHINE\ and whitespace from the server name
81+
const path = serverName.substring(hkeyLocalMachine.length + 1).trim();
82+
83+
const originalName: string = serverName.split("\\").pop().trim();
84+
// Enforce the rules from package.json on the server name
85+
const name = originalName.toLowerCase().replace(/[^a-z0-9-_~]/g, "~");
86+
const getProperty = (property: string) => getStringRegKey(cmd, hkeyLocalMachine, path, property);
87+
88+
if (name !== "" && !config.has("servers." + name)) {
89+
if (!newServerNames.includes(name)) {
90+
newServerNames.push(name);
91+
}
92+
const username = getStringRegKey(cmd,
93+
"HKEY_CURRENT_USER",
94+
// NOTE: this doesn't ever use WOW6432Node!
95+
"Software\\InterSystems\\Cache\\Servers\\" + originalName,
96+
"Server User Name",
97+
);
98+
99+
if ((username === undefined) && !serversMissingUsernames.includes(name)) {
100+
serversMissingUsernames.push(name);
101+
}
102+
103+
const usesHttps = getProperty("HTTPS") === "1";
104+
const instanceName = getProperty("WebServerInstanceName");
105+
serverDefinitions[name] = {
106+
description: getProperty("Comment"),
107+
username,
108+
webServer: {
109+
host: getProperty("WebServerAddress") || getProperty("Address"),
110+
pathPrefix: instanceName ? '/' + instanceName : undefined,
111+
port: parseInt(getProperty("WebServerPort") || "", 10),
112+
scheme: usesHttps ? "https" : "http",
113+
},
114+
}
115+
}
116+
});
117+
}
118+
}
119+
120+
async function promptForUsernames(serverDefinitions: any, serversMissingUsernames: string[]): Promise<boolean> {
121+
if (serversMissingUsernames.length) {
122+
let serverName = serversMissingUsernames.splice(0,1)[0];
123+
let username = await vscode.window.showInputBox({
124+
ignoreFocusOut: true,
125+
placeHolder: "Enter a username. Leave empty to be prompted at connect time.",
126+
prompt: `Username for server '${serverName}'`,
127+
});
128+
if (username === undefined) {
129+
// Was cancelled
130+
return false;
131+
}
132+
if (username === '') {
133+
// If unspecified, actually set to undefined to leave it empty in serverDefinitions
134+
username = undefined;
135+
}
136+
serverDefinitions[serverName].username = username;
137+
if (serversMissingUsernames.length > 0) {
138+
const reuseMessage = (username === undefined) ? `Prompt for username at connect time for all of them` : `Use '${username}' as the username for all of them`;
139+
const items = [
140+
`Enter a username individually for each of them`,
141+
reuseMessage,
142+
`Cancel import`].map((label) => {
143+
return { label };
144+
});
145+
const result = await vscode.window.showQuickPick(items, {
146+
canPickMany: false,
147+
ignoreFocusOut: true,
148+
placeHolder: `${serversMissingUsernames.length} more servers lack a username. What do you want to do?`
149+
});
150+
if (result === undefined || result.label === items[2].label) {
151+
return false;
152+
} else if (result.label === items[1].label) {
153+
for (serverName of serversMissingUsernames) {
154+
serverDefinitions[serverName].username = username;
155+
}
156+
} else {
157+
for (serverName of serversMissingUsernames) {
158+
username = await vscode.window.showInputBox({
159+
ignoreFocusOut: true,
160+
prompt: `Username for server '${serverName}'`,
161+
validateInput: ((value) => {
162+
return value.length > 0 ? "" : "Mandatory field";
163+
}),
164+
value: username,
165+
});
166+
if (username === undefined) {
167+
return false;
168+
}
169+
serverDefinitions[serverName].username = username;
170+
}
171+
}
172+
}
173+
}
174+
return true;
175+
}
176+
177+
async function promptForPasswords(serverDefinitions: any, newServerNames: string[]): Promise<void> {
178+
let reusePassword;
179+
let password : string | undefined = '';
180+
const promptServerNames = new Array();
181+
// Only prompt for servers with a username specified, of course.
182+
newServerNames.forEach(name => {
183+
if (serverDefinitions[name].username !== undefined) {
184+
promptServerNames.push(name);
185+
}
186+
});
187+
for (const serverName of promptServerNames) {
188+
if (!reusePassword) {
189+
password = await vscode.window.showInputBox({
190+
ignoreFocusOut: true,
191+
password: true,
192+
placeHolder: "Enter password to store in keychain. Leave empty to be prompted at connect time.",
193+
prompt: `Password for connection to InterSystems server '${serverName}'
194+
as ${serverDefinitions[serverName].username}`
195+
});
196+
197+
if (password === undefined) {
198+
return;
199+
}
200+
201+
if (password === '') {
202+
password = undefined;
203+
}
204+
}
205+
206+
if ((reusePassword === undefined) && (promptServerNames.length > 1)) {
207+
const placeHolder = (password === undefined) ? `Enter password later for remaining ${promptServerNames.length - 1} server(s)?` : `Store the same password for remaining ${promptServerNames.length - 1} server(s)?`
208+
const items = [
209+
`No`,
210+
`Yes`,
211+
`Cancel (enter passwords later)`].map((label) => {
212+
return { label };
213+
});
214+
const result = await vscode.window.showQuickPick(items, {
215+
canPickMany: false,
216+
ignoreFocusOut: true,
217+
placeHolder
218+
});
219+
if (result === undefined || result.label === items[2].label) {
220+
return;
221+
}
222+
reusePassword = (result.label === items[1].label);
223+
}
224+
225+
if ((password !== "") && (password !== undefined)) {
226+
await new Keychain(serverName).setPassword(password).then(() => {
227+
vscode.window.showInformationMessage(`Password for '${serverName}' stored in keychain.`);
228+
});
229+
}
230+
}
231+
}
232+
233+
function preloadRegistryCache(cmd, fullPath) {
234+
const regData = cmd.runSync("reg query " + fullPath + " /s");
235+
if (!regData.data) {
236+
return;
237+
}
238+
regData.data.split("\r\n\r\n").forEach(pathResult => {
239+
// Equivalent of running "reg query " + queryPath
240+
const lines = pathResult.split("\r\n");
241+
const queryPath = lines.splice(0,1)[0];
242+
const queryResult = lines.join("\r\n");
243+
regQueryCache.set(queryPath, { data: queryResult });
244+
});
245+
}
246+
247+
function getStringRegKey(cmd, hive, path, key): string | undefined {
248+
const queryPath = hive + "\\" + path;
249+
const regData = regQueryCache.get(queryPath) || cmd.runSync("reg query " + queryPath);
250+
if (!regData.data) {
251+
return undefined;
252+
}
253+
regQueryCache.set(queryPath, regData);
254+
const results = regData.data.split("\r\n")
255+
// Result lines from reg query are 4-space-delimited
256+
.map(line => line.split(' '))
257+
// Registry has format [<empty>, key, type, value]
258+
.filter(line => line.length === 4)
259+
// We're only interested in the specified key
260+
.filter(line => line[1] === key)
261+
// We want the value...
262+
.map(line => line[3])
263+
// ... and we'll treat empty strings as undefined
264+
.filter(result => result != '');
265+
return results[0];
266+
}

0 commit comments

Comments
 (0)