diff --git a/package.json b/package.json index 5b73450aa4787ccafc119d1c416827fd877a4436..115548fd6f8d2fc7e13a08596b859c36f54da45e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "fast-plist": "0.1.2", "gc-signals": "^0.0.1", "getmac": "1.4.1", + "glob": "^5.0.13", "graceful-fs": "4.1.11", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.1", @@ -51,7 +52,8 @@ "vscode-textmate": "^4.0.1", "vscode-xterm": "3.7.0-beta4", "winreg": "^1.2.4", - "yauzl": "^2.9.1" + "yauzl": "^2.9.1", + "yazl": "^2.4.3" }, "devDependencies": { "7zip": "0.0.6", @@ -71,7 +73,6 @@ "eslint": "^3.4.0", "event-stream": "^3.1.7", "express": "^4.13.1", - "glob": "^5.0.13", "gulp": "^3.8.9", "gulp-atom-electron": "^1.16.1", "gulp-azure-storage": "^0.7.0", diff --git a/src/typings/glob.d.ts b/src/typings/glob.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2574b3f0ee262f5c1d1ffaf7002b2bd73b98c9c --- /dev/null +++ b/src/typings/glob.d.ts @@ -0,0 +1,302 @@ +// Generated by typings +// Source: https://raw.githubusercontent.com/typed-typings/npm-minimatch/74f47de8acb42d668491987fc6bc144e7d9aa891/minimatch.d.ts +declare module '~glob~minimatch/minimatch' { +function minimatch (target: string, pattern: string, options?: minimatch.Options): boolean; + +namespace minimatch { + export function match (list: string[], pattern: string, options?: Options): string[]; + export function filter (pattern: string, options?: Options): (element: string, indexed: number, array: string[]) => boolean; + export function makeRe (pattern: string, options?: Options): RegExp; + + /** + * All options are `false` by default. + */ + export interface Options { + /** + * Dump a ton of stuff to stderr. + */ + debug?: boolean; + /** + * Do not expand `{a,b}` and `{1..3}` brace sets. + */ + nobrace?: boolean; + /** + * Disable `**` matching against multiple folder names. + */ + noglobstar?: boolean; + /** + * Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot. + * + * Note that by default, `a\/**\/b` will not match `a/.d/b`, unless `dot` is set. + */ + dot?: boolean; + /** + * Disable "extglob" style patterns like `+(a|b)`. + */ + noext?: boolean; + /** + * Perform a case-insensitive match. + */ + nocase?: boolean; + /** + * When a match is not found by `minimatch.match`, return a list containing the pattern itself if this option is set. When not set, an empty list is returned if there are no matches. + */ + nonull?: boolean; + /** + * If set, then patterns without slashes will be matched against the basename of the path if it contains slashes. For example, `a?b` would match the path `/xyz/123/acb`, but not `/xyz/acb/123`. + */ + matchBase?: boolean; + /** + * Suppress the behavior of treating `#` at the start of a pattern as a comment. + */ + nocomment?: boolean; + /** + * Suppress the behavior of treating a leading `!` character as negation. + */ + nonegate?: boolean; + /** + * Returns from negate expressions the same as if they were not negated. (Ie, true on a hit, false on a miss.) + */ + flipNegate?: boolean; + } + + export class Minimatch { + constructor (pattern: string, options?: Options); + + /** + * The original pattern the minimatch object represents. + */ + pattern: string; + /** + * The options supplied to the constructor. + */ + options: Options; + + /** + * Created by the `makeRe` method. A single regular expression expressing the entire pattern. This is useful in cases where you wish to use the pattern somewhat like `fnmatch(3)` with `FNM_PATH` enabled. + */ + regexp: RegExp; + /** + * True if the pattern is negated. + */ + negate: boolean; + /** + * True if the pattern is a comment. + */ + comment: boolean; + /** + * True if the pattern is `""`. + */ + empty: boolean; + + /** + * Generate the regexp member if necessary, and return it. Will return false if the pattern is invalid. + */ + makeRe (): RegExp | boolean; + /** + * Return true if the filename matches the pattern, or false otherwise. + */ + match (fname: string): boolean; + /** + * Take a `/-`split filename, and match it against a single row in the `regExpSet`. This method is mainly for internal use, but is exposed so that it can be used by a glob-walker that needs to avoid excessive filesystem calls. + */ + matchOne (fileArray: string[], patternArray: string[], partial: boolean): boolean; + } +} + +export = minimatch; +} +declare module '~glob~minimatch' { +import main = require('~glob~minimatch/minimatch'); +export = main; +} + +// Generated by typings +// Source: https://raw.githubusercontent.com/types/npm-glob/59ca0f5d4696a8d4da27858035316c1014133fcb/glob.d.ts +declare module '~glob/glob' { +import events = require('events'); +import fs = require('fs'); +import minimatch = require('~glob~minimatch'); + +function glob (pattern: string, cb: (err: Error, matches: string[]) => void): void; +function glob (pattern: string, options: glob.Options, cb: (err: Error, matches: string[]) => void): void; + +namespace glob { + export function sync (pattern: string, options?: Options): string[]; + export function hasMagic (pattern: string, options?: Options): boolean; + + export interface Cache { + [path: string]: boolean | string | string[]; + } + + export interface StatCache { + [path: string]: fs.Stats; + } + + export interface Symlinks { + [path: string]: boolean; + } + + export interface Options extends minimatch.Options { + /** + * The current working directory in which to search. Defaults to `process.cwd()`. + */ + cwd?: string; + /** + * The place where patterns starting with `/` will be mounted onto. Defaults to `path.resolve(options.cwd, "/")` (`/` on Unix systems, and `C:\` or some such on Windows.) + */ + root?: string; + /** + * Include `.dot` files in normal matches and `globstar` matches. Note that an explicit dot in a portion of the pattern will always match dot files. + */ + dot?: boolean; + /** + * By default, a pattern starting with a forward-slash will be "mounted" onto the root setting, so that a valid filesystem path is returned. Set this flag to disable that behavior. + */ + nomount?: boolean; + /** + * Add a `/` character to directory matches. Note that this requires additional stat calls. + */ + mark?: boolean; + /** + * Don't sort the results. + */ + nosort?: boolean; + /** + * Set to true to stat all results. This reduces performance somewhat, and is completely unnecessary, unless `readdir` is presumed to be an untrustworthy indicator of file existence. + */ + stat?: boolean; + /** + * When an unusual error is encountered when attempting to read a directory, a warning will be printed to stderr. Set the `silent` option to true to suppress these warnings. + */ + silent?: boolean; + /** + * When an unusual error is encountered when attempting to read a directory, the process will just continue on in search of other matches. Set the `strict` option to raise an error in these cases. + */ + strict?: boolean; + /** + * See `cache` property above. Pass in a previously generated cache object to save some fs calls. + */ + cache?: Cache; + /** + * A cache of results of filesystem information, to prevent unnecessary stat calls. While it should not normally be necessary to set this, you may pass the statCache from one glob() call to the options object of another, if you know that the filesystem will not change between calls. (See https://github.com/isaacs/node-glob#race-conditions) + */ + statCache?: StatCache; + /** + * A cache of known symbolic links. You may pass in a previously generated `symlinks` object to save lstat calls when resolving `**` matches. + */ + symlinks?: Symlinks; + /** + * DEPRECATED: use `glob.sync(pattern, opts)` instead. + */ + sync?: boolean; + /** + * In some cases, brace-expanded patterns can result in the same file showing up multiple times in the result set. By default, this implementation prevents duplicates in the result set. Set this flag to disable that behavior. + */ + nounique?: boolean; + /** + * Set to never return an empty set, instead returning a set containing the pattern itself. This is the default in glob(3). + */ + nonull?: boolean; + /** + * Set to enable debug logging in minimatch and glob. + */ + debug?: boolean; + /** + * Do not expand `{a,b}` and `{1..3}` brace sets. + */ + nobrace?: boolean; + /** + * Do not match `**` against multiple filenames. (Ie, treat it as a normal `*` instead.) + */ + noglobstar?: boolean; + /** + * Do not match `+(a|b)` "extglob" patterns. + */ + noext?: boolean; + /** + * Perform a case-insensitive match. Note: on case-insensitive filesystems, non-magic patterns will match by default, since `stat` and `readdir` will not raise errors. + */ + nocase?: boolean; + /** + * Perform a basename-only match if the pattern does not contain any slash characters. That is, `*.js` would be treated as equivalent to `**\/*.js`, matching all js files in all directories. + */ + matchBase?: any; + /** + * Do not match directories, only files. (Note: to match only directories, simply put a `/` at the end of the pattern.) + */ + nodir?: boolean; + /** + * Add a pattern or an array of glob patterns to exclude matches. Note: `ignore` patterns are always in `dot:true` mode, regardless of any other settings. + */ + ignore?: string | string[]; + /** + * Follow symlinked directories when expanding `**` patterns. Note that this can result in a lot of duplicate references in the presence of cyclic links. + */ + follow?: boolean; + /** + * Set to true to call `fs.realpath` on all of the results. In the case of a symlink that cannot be resolved, the full absolute path to the matched entry is returned (though it will usually be a broken symlink) + */ + realpath?: boolean; + } + + export class Glob extends events.EventEmitter { + constructor (pattern: string, cb?: (err: Error, matches: string[]) => void); + constructor (pattern: string, options: Options, cb?: (err: Error, matches: string[]) => void); + + /** + * The minimatch object that the glob uses. + */ + minimatch: minimatch.Minimatch; + /** + * The options object passed in. + */ + options: Options; + /** + * Boolean which is set to true when calling `abort()`. There is no way at this time to continue a glob search after aborting, but you can re-use the statCache to avoid having to duplicate syscalls. + * @type {boolean} + */ + aborted: boolean; + /** + * Convenience object. + */ + cache: Cache; + /** + * Cache of `fs.stat` results, to prevent statting the same path multiple times. + */ + statCache: StatCache; + /** + * A record of which paths are symbolic links, which is relevant in resolving `**` patterns. + */ + symlinks: Symlinks; + /** + * An optional object which is passed to `fs.realpath` to minimize unnecessary syscalls. It is stored on the instantiated Glob object, and may be re-used. + */ + realpathCache: { [path: string]: string }; + found: string[]; + + /** + * Temporarily stop the search. + */ + pause(): void; + /** + * Resume the search. + */ + resume(): void; + /** + * Stop the search forever. + */ + abort(): void; + } +} + +export = glob; +} +declare module 'glob/glob' { +import main = require('~glob/glob'); +export = main; +} +declare module 'glob' { +import main = require('~glob/glob'); +export = main; +} diff --git a/src/typings/yazl.d.ts b/src/typings/yazl.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..0aa227a6c8243105139d62f73c10f82aec904a79 --- /dev/null +++ b/src/typings/yazl.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'yazl' { + import * as stream from 'stream'; + + class ZipFile { + outputStream: stream.Stream; + addBuffer(buffer: Buffer, path: string); + addFile(localPath: string, path: string); + end(); + } +} \ No newline at end of file diff --git a/src/vs/base/node/zip.ts b/src/vs/base/node/zip.ts index 652fb21c73bc138f81a5df22570bbfe8f60967cd..4ef2b28376905fc81d605edbb3b460fea5adaa10 100644 --- a/src/vs/base/node/zip.ts +++ b/src/vs/base/node/zip.ts @@ -11,6 +11,7 @@ import { nfcall, ninvoke, SimpleThrottler } from 'vs/base/common/async'; import { mkdirp, rimraf } from 'vs/base/node/pfs'; import { TPromise } from 'vs/base/common/winjs.base'; import { open as _openZip, Entry, ZipFile } from 'yauzl'; +import * as yazl from 'yazl'; import { ILogService } from 'vs/platform/log/common/log'; export interface IExtractOptions { @@ -151,6 +152,27 @@ function openZip(zipFile: string, lazy: boolean = false): TPromise { .then(null, err => TPromise.wrapError(toExtractError(err))); } +export interface IFile { + path: string; + contents?: Buffer | string; + localPath?: string; +} + +export function zip(zipPath: string, files: IFile[]): TPromise { + return new TPromise((c, e) => { + const zip = new yazl.ZipFile(); + files.forEach(f => f.contents ? zip.addBuffer(typeof f.contents === 'string' ? new Buffer(f.contents, 'utf8') : f.contents, f.path) : zip.addFile(f.localPath, f.path)); + zip.end(); + + const zipStream = createWriteStream(zipPath); + zip.outputStream.pipe(zipStream); + + zip.outputStream.once('error', e); + zipStream.once('error', e); + zipStream.once('finish', () => c(zipPath)); + }); +} + export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, logService: ILogService): TPromise { const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 547d1bd99d6c12bde8243ed7766b2a76f887438c..c8c5fb57de0e6dc6ccf127163efd9e7384d4fa0f 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -43,6 +43,8 @@ import { LocalizationsChannel } from 'vs/platform/localizations/common/localizat import { DialogChannelClient } from 'vs/platform/dialogs/common/dialogIpc'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadServiceChannelClient } from 'vs/platform/download/node/downloadIpc'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -84,9 +86,13 @@ function main(server: Server, initData: ISharedProcessInitData, configuration: I const activeWindowManager = new ActiveWindowManager(windowsService); const route = () => activeWindowManager.getActiveClientId(); + const dialogChannel = server.getChannel('dialog', { routeCall: route, routeEvent: route }); services.set(IDialogService, new DialogChannelClient(dialogChannel)); + const downloadChannel = server.getChannel('download', { routeCall: route, routeEvent: route }); + services.set(IDownloadService, new DownloadServiceChannelClient(downloadChannel)); + const instantiationService = new InstantiationService(services); instantiationService.invokeFunction(accessor => { diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 08bd0945583ccfb7f6dc300304be61f8296ce03e..bc78b2e146483c9982d4f55caa99554ce10d1aeb 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -41,6 +41,9 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { CommandLineDialogService } from 'vs/platform/dialogs/node/dialogService'; import { areSameExtensions, getGalleryExtensionIdFromLocal } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import Severity from 'vs/base/common/severity'; +import URI from 'vs/base/common/uri'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { DownloadService } from 'vs/platform/download/node/download'; const notFound = (id: string) => localize('notFound', "Extension '{0}' not found.", id); const notInstalled = (id: string) => localize('notInstalled', "Extension '{0}' is not installed.", id); @@ -101,7 +104,7 @@ class Main { .map(id => () => { const extension = path.isAbsolute(id) ? id : path.join(process.cwd(), id); - return this.extensionManagementService.install(extension).then(() => { + return this.extensionManagementService.install(URI.file(extension)).then(() => { console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed!", getBaseLabel(extension))); }, error => { if (isPromiseCanceledError(error)) { @@ -240,6 +243,7 @@ export function main(argv: ParsedArgs): TPromise { services.set(IExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); services.set(IExtensionGalleryService, new SyncDescriptor(ExtensionGalleryService)); services.set(IDialogService, new SyncDescriptor(CommandLineDialogService)); + services.set(IDownloadService, new SyncDescriptor(DownloadService)); const appenders: AppInsightsAppender[] = []; if (isBuilt && !extensionDevelopmentPath && !envService.args['disable-telemetry'] && product.enableTelemetry) { diff --git a/src/vs/platform/download/common/download.ts b/src/vs/platform/download/common/download.ts new file mode 100644 index 0000000000000000000000000000000000000000..80a9de7cbc089e89728933a79145e8ced738ed5c --- /dev/null +++ b/src/vs/platform/download/common/download.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import URI from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TPromise } from 'vs/base/common/winjs.base'; + +export const IDownloadService = createDecorator('downloadService'); + +export interface IDownloadService { + + _serviceBrand: any; + + download(location: URI, file: string): TPromise; + +} \ No newline at end of file diff --git a/src/vs/platform/download/node/download.ts b/src/vs/platform/download/node/download.ts new file mode 100644 index 0000000000000000000000000000000000000000..061d25c5df208fa612991498fbd36970985dbcd6 --- /dev/null +++ b/src/vs/platform/download/node/download.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import URI from 'vs/base/common/uri'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IDownloadService } from 'vs/platform/download/common/download'; + +export class DownloadService implements IDownloadService { + + _serviceBrand: any; + + download(from: URI, to: string): TPromise { + throw new Error('Not supported'); + } + +} \ No newline at end of file diff --git a/src/vs/platform/download/node/downloadIpc.ts b/src/vs/platform/download/node/downloadIpc.ts new file mode 100644 index 0000000000000000000000000000000000000000..89b77323fc8d91d72631fa98423002cf38b15bf7 --- /dev/null +++ b/src/vs/platform/download/node/downloadIpc.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import URI from 'vs/base/common/uri'; +import * as path from 'path'; +import * as fs from 'fs'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { Event, Emitter, buffer } from 'vs/base/common/event'; +import { IDownloadService } from 'vs/platform/download/common/download'; +import { mkdirp } from 'vs/base/node/pfs'; +import { onUnexpectedError } from 'vs/base/common/errors'; + +export type UploadResponse = Buffer | Error | undefined; + +export function upload(uri: URI): Event { + const stream = new Emitter(); + fs.open(uri.fsPath, 'r', (err, fd) => { + if (err) { + if (err.code === 'ENOENT') { + stream.fire(new Error('File Not Found')); + } + return; + } + const finish = (err?: any) => { + if (err) { + stream.fire(err); + } else { + stream.fire(); + if (fd) { + fs.close(fd, err => { + if (err) { + onUnexpectedError(err); + } + }); + } + } + }; + let currentPosition: number = 0; + const readChunk = () => { + const chunkBuffer = Buffer.allocUnsafe(64 * 1024); // 64K Chunk + fs.read(fd, chunkBuffer, 0, chunkBuffer.length, currentPosition, (err, bytesRead) => { + currentPosition += bytesRead; + if (err) { + finish(err); + } else { + if (bytesRead === 0) { + // no more data -> finish + finish(); + } else { + stream.fire(chunkBuffer.slice(0, bytesRead)); + readChunk(); + } + } + }); + }; + // start reading + readChunk(); + }); + return stream.event; +} + +export interface IDownloadServiceChannel extends IChannel { + listen(event: 'upload', uri: URI): Event; + listen(event: string, arg?: any): Event; +} + +export class DownloadServiceChannel implements IDownloadServiceChannel { + + constructor() { } + + listen(event: string, arg?: any): Event { + switch (event) { + case 'upload': return buffer(upload(arg)); + } + return undefined; + } + + call(command: string, arg?: any): TPromise { + throw new Error('No calls'); + } +} + +export class DownloadServiceChannelClient implements IDownloadService { + + _serviceBrand: any; + + constructor(private channel: IDownloadServiceChannel) { } + + download(from: URI, to: string): TPromise { + const dirName = path.dirname(to); + let out: fs.WriteStream; + return new TPromise((c, e) => { + return mkdirp(dirName) + .then(() => { + out = fs.createWriteStream(to); + out.once('close', () => c(null)); + out.once('error', e); + const uploadStream = this.channel.listen('upload', from); + const disposable = uploadStream((result: Buffer | Error | undefined) => { + if (result === void 0) { + out.end(); + out.close(); + disposable.dispose(); + c(null); + } else if (result instanceof Buffer) { + out.write(result); + } else if (result instanceof Error) { + out.close(); + disposable.dispose(); + e(result); + } + }); + }); + }); + } +} \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f7a4416631b5fdc70826b6b07752e5d15f519c26..bc65fb0b02415cae625e26d602783719094826e5 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -306,7 +306,9 @@ export interface IExtensionManagementService { onUninstallExtension: Event; onDidUninstallExtension: Event; - install(zipPath: string): TPromise; + zip(extension: ILocalExtension): TPromise; + unzip(zipLocation: URI): TPromise; + install(vsix: URI): TPromise; installFromGallery(extension: IGalleryExtension): TPromise; uninstall(extension: ILocalExtension, force?: boolean): TPromise; reinstallFromGallery(extension: ILocalExtension): TPromise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 4180af7c8a32f8775cf853609b970a93be7c71ba..40048fe5efa92a19a2035c0edd28a1cd5fe4a999 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -17,7 +17,10 @@ export interface IExtensionManagementChannel extends IChannel { listen(event: 'onDidInstallExtension'): Event; listen(event: 'onUninstallExtension'): Event; listen(event: 'onDidUninstallExtension'): Event; - call(command: 'install', args: [string]): TPromise; + + call(command: 'zip', args: [ILocalExtension]): TPromise; + call(command: 'unzip', args: [URI]): TPromise; + call(command: 'install', args: [URI]): TPromise; call(command: 'installFromGallery', args: [IGalleryExtension]): TPromise; call(command: 'uninstall', args: [ILocalExtension, boolean]): TPromise; call(command: 'reinstallFromGallery', args: [ILocalExtension]): TPromise; @@ -53,7 +56,9 @@ export class ExtensionManagementChannel implements IExtensionManagementChannel { call(command: string, args?: any): TPromise { switch (command) { - case 'install': return this.service.install(args[0]); + case 'zip': return this.service.zip(this._transform(args[0])); + case 'unzip': return this.service.unzip(URI.revive(args[0])); + case 'install': return this.service.install(URI.revive(args[0])); case 'installFromGallery': return this.service.installFromGallery(args[0]); case 'uninstall': return this.service.uninstall(this._transform(args[0]), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(this._transform(args[0])); @@ -81,8 +86,16 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer get onUninstallExtension(): Event { return this.channel.listen('onUninstallExtension'); } get onDidUninstallExtension(): Event { return this.channel.listen('onDidUninstallExtension'); } - install(zipPath: string): TPromise { - return this.channel.call('install', [zipPath]); + zip(extension: ILocalExtension): TPromise { + return this.channel.call('zip', [extension]).then(result => URI.revive(this.uriTransformer.transformIncoming(result))); + } + + unzip(zipLocation: URI): TPromise { + return this.channel.call('unzip', [zipLocation]); + } + + install(vsix: URI): TPromise { + return this.channel.call('install', [vsix]); } installFromGallery(extension: IGalleryExtension): TPromise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index 505673fa7fc83daa2d2994991f11bf17c4e691b9..f07dafd169fe2dc7e77f447a4e0a33bb982c7340 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -5,7 +5,7 @@ 'use strict'; -import { ILocalExtension, IGalleryExtension, EXTENSION_IDENTIFIER_REGEX, IExtensionIdentifier, IReportedExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, EXTENSION_IDENTIFIER_REGEX, IExtensionIdentifier, IReportedExtension, IExtensionManifest } from 'vs/platform/extensionManagement/common/extensionManagement'; export function areSameExtensions(a: IExtensionIdentifier, b: IExtensionIdentifier): boolean { if (a.uuid && b.uuid) { @@ -117,4 +117,28 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set, server) => { emitter.add(server.extensionManagementService.onInstallExtension); return emitter; }, new EventMultiplexer()).event; this.onDidInstallExtension = this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer()).event; this.onUninstallExtension = this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onUninstallExtension); return emitter; }, new EventMultiplexer()).event; this.onDidUninstallExtension = this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUninstallExtension); return emitter; }, new EventMultiplexer()).event; + this.syncExtensions(); } getInstalled(type?: LocalExtensionType): TPromise { @@ -49,8 +58,16 @@ export class MulitExtensionManagementService implements IExtensionManagementServ return this.getServer(extension).extensionManagementService.updateMetadata(extension, metadata); } - install(zipPath: string): TPromise { - return this.extensionManagementServerService.getLocalExtensionManagementServer().extensionManagementService.install(zipPath); + zip(extension: ILocalExtension): TPromise { + throw new Error('Not Supported'); + } + + unzip(zipLocation: URI): TPromise { + return TPromise.join(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(() => null); + } + + install(vsix: URI): TPromise { + return TPromise.join(this.servers.map(({ extensionManagementService }) => extensionManagementService.install(vsix))).then(() => null); } installFromGallery(extension: IGalleryExtension): TPromise { @@ -64,4 +81,65 @@ export class MulitExtensionManagementService implements IExtensionManagementServ private getServer(extension: ILocalExtension): IExtensionManagementServer { return this.extensionManagementServerService.getExtensionManagementServer(extension.location); } + + private async syncExtensions(): Promise { + const localServer = this.extensionManagementServerService.getLocalExtensionManagementServer(); + localServer.extensionManagementService.getInstalled() + .then(async localExtensions => { + const workspaceExtensions = localExtensions.filter(e => isWorkspaceExtension(e.manifest)); + const otherServers = this.servers.filter(s => s !== localServer); + + const extensionsToSync: Map = await this.getExtensionsToSync(workspaceExtensions, otherServers); + + if (extensionsToSync.size > 0) { + const handler = this.notificationService.notify({ severity: Severity.Info, message: localize('synchronising', "Synchronizing workspace extensions...") }); + handler.progress.infinite(); + const promises: TPromise[] = []; + const vsixById: Map> = new Map>(); + extensionsToSync.forEach((extensions, server) => { + for (const extension of extensions) { + let vsix = vsixById.get(extension.galleryIdentifier.id); + if (!vsix) { + vsix = localServer.extensionManagementService.zip(extension); + vsixById.set(extension.galleryIdentifier.id, vsix); + promises.push(vsix); + } + promises.push(vsix.then(location => server.extensionManagementService.unzip(location))); + } + }); + TPromise.join(promises).then(() => { + handler.progress.done(); + handler.updateMessage(localize('Synchronize.finished', "Finished synchronizing workspace extensions. Please reload now.")); + handler.updateActions({ + primary: [ + new Action('Synchronize.reloadNow', localize('Synchronize.reloadNow', "Reload Now"), null, true, () => this.windowService.reloadWindow()) + ] + }); + }); + } + }, err => { + console.log(err); + }); + } + + private async getExtensionsToSync(workspaceExtensions: ILocalExtension[], servers: IExtensionManagementServer[]): Promise> { + const extensionsToSync: Map = new Map(); + for (const server of servers) { + const extensions = await server.extensionManagementService.getInstalled(); + const groupedById = this.groupById(extensions); + const toSync = workspaceExtensions.filter(e => !groupedById.has(e.galleryIdentifier.id)); + if (toSync.length) { + extensionsToSync.set(server, toSync); + } + } + return extensionsToSync; + } + + private groupById(extensions: ILocalExtension[]): Map { + const result: Map = new Map(); + for (const extension of extensions) { + result.set(extension.galleryIdentifier.id, extension); + } + return result; + } } \ No newline at end of file diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index fca1b8fcc1eedca233bd95ef59ad6d0d3904e895..ba5c82e925f4c45c09f4454b681b5f8ee94254fa 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -7,12 +7,13 @@ import * as nls from 'vs/nls'; import * as path from 'path'; +import * as glob from 'glob'; import * as pfs from 'vs/base/node/pfs'; import * as errors from 'vs/base/common/errors'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { flatten } from 'vs/base/common/arrays'; -import { extract, buffer, ExtractError } from 'vs/base/node/zip'; +import { extract, buffer, ExtractError, zip, IFile } from 'vs/base/node/zip'; import { TPromise, ValueCallback, ErrorCallback } from 'vs/base/common/winjs.base'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, @@ -41,6 +42,9 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isEngineValid } from 'vs/platform/extensions/node/extensionValidator'; import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { tmpdir } from 'os'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IDownloadService } from 'vs/platform/download/common/download'; const SystemExtensionsRoot = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', 'extensions')); const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; @@ -136,6 +140,7 @@ export class ExtensionManagementService extends Disposable implements IExtension @IDialogService private dialogService: IDialogService, @IExtensionGalleryService private galleryService: IExtensionGalleryService, @ILogService private logService: ILogService, + @IDownloadService private downloadService: IDownloadService, @ITelemetryService private telemetryService: ITelemetryService, ) { super(); @@ -153,8 +158,32 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - install(zipPath: string): TPromise { - zipPath = path.resolve(zipPath); + zip(extension: ILocalExtension): TPromise { + return this.collectFiles(extension) + .then(files => zip(path.join(tmpdir(), generateUuid()), files)) + .then(path => URI.file(path)); + } + + unzip(zipLocation: URI): TPromise { + const downloadedLocation = path.join(tmpdir(), generateUuid()); + return this.downloadService.download(zipLocation, downloadedLocation).then(() => this.install(URI.file(downloadedLocation))); + } + + private collectFiles(extension: ILocalExtension): TPromise { + return new TPromise((c, e) => { + glob('**', { cwd: extension.location.fsPath, nodir: true, dot: true }, (err: Error, files: string[]) => { + if (err) { + e(err); + } else { + c(files.map(f => f.replace(/\\/g, '/')) + .map(f => ({ path: `extension/${f}`, localPath: path.join(extension.location.fsPath, f) }))); + } + }); + }); + } + + install(vsix: URI): TPromise { + const zipPath = path.resolve(vsix.fsPath); return validateLocalExtension(zipPath) .then(manifest => { diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index 33bea4a0ca07789e5913902a82c7d187af26b943..4df8355c5581d5af84de666000ecae3f3cedf814 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -96,6 +96,7 @@ import { OpenerService } from 'vs/editor/browser/services/openerService'; import { SearchHistoryService } from 'vs/workbench/services/search/node/searchHistoryService'; import { MulitExtensionManagementService } from 'vs/platform/extensionManagement/common/multiExtensionManagement'; import { ExtensionManagementServerService } from 'vs/workbench/services/extensions/node/extensionManagementServerService'; +import { DownloadServiceChannel } from 'vs/platform/download/node/downloadIpc'; /** * Services that we require for the Shell @@ -335,7 +336,10 @@ export class WorkbenchShell extends Disposable { .then(() => connectNet(this.environmentService.sharedIPCHandle, `window:${this.configuration.windowId}`)); sharedProcess - .done(client => client.registerChannel('dialog', instantiationService.createInstance(DialogChannel))); + .done(client => { + client.registerChannel('download', instantiationService.createInstance(DownloadServiceChannel)); + client.registerChannel('dialog', instantiationService.createInstance(DialogChannel)); + }); // Warm up font cache information before building up too many dom elements restoreFontInfo(this.storageService); diff --git a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts index dde370485c80817a804989c810abe2f1db9c0966..cf54929edc348aacd356a3136bcfd2a462aca135 100644 --- a/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts +++ b/src/vs/workbench/parts/extensions/node/extensionsWorkbenchService.ts @@ -672,7 +672,7 @@ export class ExtensionsWorkbenchService implements IExtensionsWorkbenchService, location: ProgressLocation.Extensions, title: nls.localize('installingVSIXExtension', 'Installing extension from VSIX...'), source: `${extension}` - }, () => this.extensionService.install(extension).then(() => null)); + }, () => this.extensionService.install(URI.file(extension)).then(() => null)); } if (!(extension instanceof Extension)) { diff --git a/yarn.lock b/yarn.lock index e67580f77c1d997d214d6d438d67aea6f4511c3b..9123288ecb7849e2d5fed6332ff07dd5b64ecb6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8241,7 +8241,7 @@ yauzl@^2.2.1, yauzl@^2.3.1, yauzl@^2.9.1: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" -yazl@^2.2.1, yazl@^2.2.2: +yazl@^2.2.1, yazl@^2.2.2, yazl@^2.4.3: version "2.4.3" resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.4.3.tgz#ec26e5cc87d5601b9df8432dbdd3cd2e5173a071" dependencies: