Skip to content

Commit

Permalink
darwin: cli install/uninstall
Browse files Browse the repository at this point in the history
  • Loading branch information
joaomoreno committed Feb 16, 2016
1 parent 771eedb commit d2a151f
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 2 deletions.
5 changes: 3 additions & 2 deletions extensions/shellscript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"languages": [{
"id": "shellscript",
"aliases": ["Shell Script (Bash)", "shellscript"],
"extensions": [".sh", ".bash", ".zsh", ".bashrc", ".bash_profile", ".bash_login", ".profile", ".bash_logout"],
"extensions": [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_login", ".profile", ".bash_logout", ".zsh", ".zshrc"],
"firstLine": "^#!.*\\b(bash|zsh|sh|tcsh)|^#\\s*-\\*-[^*]*mode:\\s*shell-script[^*]*-\\*-",
"configuration": "./shellscript.configuration.json"
"configuration": "./shellscript.configuration.json",
"mimetypes": ["text/x-shellscript"]
}],
"grammars": [{
"language": "shellscript",
Expand Down
210 changes: 210 additions & 0 deletions src/vs/workbench/electron-browser/darwin/cli.contribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import * as cp from 'child_process';
import * as pfs from 'vs/base/node/pfs';
import { nfcall } from 'vs/base/common/async';
import { TPromise } from 'vs/base/common/winjs.base';
import URI from 'vs/base/common/uri';
import { Action } from 'vs/base/common/actions';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actionRegistry';
import { IWorkbenchContributionsRegistry, IWorkbenchContribution, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/platform';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { IWorkspaceContextService } from 'vs/workbench/services/workspace/common/contextService';
import { IMessageService, Severity } from 'vs/platform/message/common/message';
import { IEditorService } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

function ignore<T>(code: string, value: T = null): (err: any) => TPromise<T> {
return err => err.code === code ? TPromise.as<T>(value) : TPromise.wrapError<T>(err);
}

const root = URI.parse(require.toUrl('')).fsPath;
const source = path.resolve(root, '..', 'bin', 'code');
const isAvailable = fs.existsSync(source);

class InstallAction extends Action {

static ID = 'workbench.action.installCommandLine';
static LABEL = nls.localize('install', 'Install in PATH');

constructor(
id: string,
label: string,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IMessageService private messageService: IMessageService,
@IEditorService private editorService: IEditorService
) {
super(id, label);
}

private get applicationName(): string {
return this.contextService.getConfiguration().env.applicationName;
}

private get target(): string {
return `/usr/local/bin/${ this.applicationName }`;
}

run(): TPromise<void> {
return this.checkLegacy()
.then(files => {
if (files.length > 0) {
const file = files[0];
const resource = URI.create('file', null, file);
const message = nls.localize('exists', "Please remove the 'code' alias in '{0}' and retry this action.", file);
const input = { resource, mime: 'text/x-shellscript' };
const actions = [
new Action('inlineEdit', nls.localize('editFile', "Edit '{0}'", file), '', true, () => {
return this.editorService.openEditor(input).then(() => {
const message = nls.localize('again', "Once you remove the 'code' alias, you can retry the PATH installation.");
const actions = [
new Action('cancel', nls.localize('cancel', "Cancel")),
new Action('yes', nls.localize('retry', "Retry"), '', true, () => this.run())
];

this.messageService.show(Severity.Info, { message, actions });
});
})
];

this.messageService.show(Severity.Warning, { message, actions });
return TPromise.as(null);
}

return this.isInstalled()
.then(isInstalled => {
if (!isAvailable || isInstalled) {
return TPromise.as(null);
} else {
const createSymlink = () => {
return pfs.unlink(this.target)
.then(null, ignore('ENOENT'))
.then(() => pfs.symlink(source, this.target));
};

return createSymlink().then(null, err => {
if (err.code === 'EACCES' || err.code === 'ENOENT') {
return this.createBinFolder()
.then(() => createSymlink());
}

return TPromise.wrapError(err);
});
}
})
.then(() => this.messageService.show(Severity.Info, nls.localize('success', 'Shortcut \'{0}\' successfully installed in PATH.', this.applicationName)));
});
}

private isInstalled(): TPromise<boolean> {
return pfs.lstat(this.target)
.then(stat => stat.isSymbolicLink())
.then(() => pfs.readlink(this.target))
.then(link => link === source)
.then(null, ignore('ENOENT', false));
}

private createBinFolder(): TPromise<void> {
const command = 'osascript -e "do shell script \\"mkdir -p /usr/local/bin && chown \\" & (do shell script (\\"whoami\\")) & \\" /usr/local/bin\\" with administrator privileges"';

return nfcall(cp.exec, command, {})
.then(null, _ => TPromise.wrapError(new Error(nls.localize('cantCreateBinFolder', "Unable to create '/usr/local/bin'."))));
}

public checkLegacy(): TPromise<string[]> {
const readOrEmpty = name => pfs.readFile(name, 'utf8')
.then(null, ignore('ENOENT', ''));

const files = [
path.join(os.homedir(), '.bash_profile'),
path.join(os.homedir(), '.bashrc'),
path.join(os.homedir(), '.zshrc')
];

return TPromise.join(files.map(f => readOrEmpty(f))).then(result => {
return result.reduce((result, contents, index) => {
const env = this.contextService.getConfiguration().env;

if (contents.indexOf(env.darwinBundleIdentifier) > -1) {
result.push(files[index]);
}

return result;
}, []);
});
}
}

class UninstallAction extends Action {

static ID = 'workbench.action.uninstallCommandLine';
static LABEL = nls.localize('uninstall', 'Uninstall from PATH');

constructor(
id: string,
label: string,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IMessageService private messageService: IMessageService
) {
super(id, label);
}

private get applicationName(): string {
return this.contextService.getConfiguration().env.applicationName;
}

private get target(): string {
return `/usr/local/bin/${ this.applicationName }`;
}

run(): TPromise<void> {
return pfs.unlink(this.target)
.then(null, ignore('ENOENT'))
.then(() => this.messageService.show(Severity.Info, nls.localize('success', 'Shortcut \'{0}\' successfully uninstalled from PATH.', this.applicationName)))
}
}

class DarwinCLIHelper implements IWorkbenchContribution {

constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IMessageService messageService: IMessageService
) {
const installAction = instantiationService.createInstance(InstallAction, InstallAction.ID, InstallAction.LABEL);

installAction.checkLegacy().done(files => {
if (files.length > 0) {
const message = nls.localize('update', "Code needs to update the command line launcher. Would you like to do this now?");
const actions = [
new Action('later', nls.localize('now', "Later")),
new Action('now', nls.localize('now', "Update Now"), '', true, () => installAction.run())
];

messageService.show(Severity.Info, { message, actions });
}
});
}

getId(): string {
return 'darwin.cli';
}
}

if (isAvailable && process.platform === 'darwin') {
const category = nls.localize('commandLine', "Command Line");

const workbenchActionsRegistry = <IWorkbenchActionRegistry>Registry.as(ActionExtensions.WorkbenchActions);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(InstallAction, InstallAction.ID, InstallAction.LABEL), category);
workbenchActionsRegistry.registerWorkbenchAction(new SyncActionDescriptor(UninstallAction, UninstallAction.ID, UninstallAction.LABEL), category);

const workbenchRegistry = <IWorkbenchContributionsRegistry>Registry.as(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(DarwinCLIHelper);
}
2 changes: 2 additions & 0 deletions src/vs/workbench/workbench.main.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ define([

'vs/workbench/parts/gettingStarted/electron-browser/electronGettingStarted.contribution',

'vs/workbench/electron-browser/darwin/cli.contribution',

'vs/workbench/electron-browser/main.contribution',
'vs/workbench/electron-browser/main'

Expand Down

0 comments on commit d2a151f

Please sign in to comment.