├── .gitignore ├── .vscodeignore ├── CHANGELOG.md ├── src ├── utils │ ├── Logger.ts │ └── randomString.ts ├── lib │ ├── Command.ts │ ├── StatusBarItem.ts │ ├── RemoteFile.ts │ ├── Server.ts │ └── Session.ts └── extension.ts ├── tsconfig.json ├── .travis.yml ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .eslintrc.json ├── test ├── index.ts ├── Command.test.ts └── RemoteFile.test.ts ├── LICENSE.txt ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | typings/** 4 | out/test/** 5 | test/** 6 | src/** 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.0 (July 11, 2017) 4 | - Add `remote.dontShowPortAlreadyInUseError` setting that if set to true, error for remote.port already in use won't be shown anymore; 5 | - Add a status bar icon that shows the current server status. -------------------------------------------------------------------------------- /src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as log4js from "log4js"; 2 | log4js.configure({ 3 | appenders: { 4 | out: { type: "console" } 5 | }, 6 | categories: { 7 | default: { appenders: [ "out" ], level: "trace" } 8 | } 9 | }); 10 | 11 | export default log4js; 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "." 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test" 15 | ] 16 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | os: 4 | - osx 5 | - linux 6 | 7 | before_install: 8 | - if [ $TRAVIS_OS_NAME == "linux" ]; then 9 | export CXX="g++-4.9" CC="gcc-4.9" DISPLAY=:99.0; 10 | sh -e /etc/init.d/xvfb start; 11 | sleep 3; 12 | fi 13 | 14 | install: 15 | - npm install 16 | - npm run vscode:prepublish 17 | 18 | script: 19 | - npm test --silent 20 | 21 | language: node_js 22 | 23 | node_js: "node" -------------------------------------------------------------------------------- /src/utils/randomString.ts: -------------------------------------------------------------------------------- 1 | var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghiklmnopqrstuvwxyz'; 2 | 3 | export default function randomString(length : number) { 4 | length = length ? length : 32; 5 | 6 | var string = ''; 7 | 8 | for (var i = 0; i < length; i++) { 9 | var randomNumber = Math.floor(Math.random() * chars.length); 10 | string += chars.substring(randomNumber, randomNumber + 1); 11 | } 12 | 13 | return string; 14 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib", 10 | "vsicons.presets.angular": false // we want to use the TS server from our node_modules folder to control its version 11 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /src/lib/Command.ts: -------------------------------------------------------------------------------- 1 | import Logger from '../utils/Logger'; 2 | import RemoteFile from './RemoteFile'; 3 | 4 | const L = Logger.getLogger('Command'); 5 | 6 | class Command { 7 | name : string; 8 | variables : Map; 9 | 10 | constructor(name : string) { 11 | L.trace('constructor', name); 12 | this.variables = new Map(); 13 | this.setName(name); 14 | } 15 | 16 | setName(name : string) { 17 | L.trace('setName', name); 18 | this.name = name; 19 | } 20 | 21 | getName() : string { 22 | L.trace('getName'); 23 | return this.name; 24 | } 25 | 26 | addVariable(key : string, value : any) { 27 | L.trace('addVariable', key, value); 28 | this.variables.set(key, value); 29 | } 30 | 31 | getVariable(key : string) : any { 32 | L.trace('getVariable', key); 33 | return this.variables.get(key); 34 | } 35 | } 36 | 37 | export default Command; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Rafael Maiolla 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 6 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 7 | is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 12 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 13 | BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 14 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/Command.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import Command from '../src/lib/Command'; 4 | 5 | suite("Command Tests", () => { 6 | 7 | test("constructor", () => { 8 | var name = "test"; 9 | var command = new Command(name); 10 | 11 | assert.equal(name, command.getName()); 12 | }); 13 | 14 | test("setName", () => { 15 | var name = "test"; 16 | var command = new Command(name); 17 | 18 | var name = "another test"; 19 | command.setName(name); 20 | assert.equal(name, command.getName()); 21 | }); 22 | 23 | test("getName", () => { 24 | var name = "test"; 25 | var command = new Command(name); 26 | 27 | var name = "another test"; 28 | command.setName(name); 29 | assert.equal(name, command.getName()); 30 | }); 31 | 32 | test("addVariable", () => { 33 | var name = "test"; 34 | var key = "variableKey"; 35 | var value = "variableValue"; 36 | var command = new Command(name); 37 | 38 | command.addVariable(key, value); 39 | assert.equal(value, command.getVariable(key)); 40 | }); 41 | 42 | test("getVariable", () => { 43 | var name = "test"; 44 | var key = "variableKey"; 45 | var value = "variableValue"; 46 | var command = new Command(name); 47 | 48 | command.addVariable(key, value); 49 | assert.equal(value, command.getVariable(key)); 50 | }); 51 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remote VSCode 2 | 3 | [![Build Status](https://travis-ci.org/rafaelmaiolla/remote-vscode.svg?branch=master)](https://travis-ci.org/rafaelmaiolla/remote-vscode) 4 | [![Known Vulnerabilities](https://snyk.io/test/github/rafaelmaiolla/remote-vscode/badge.svg)](https://snyk.io/test/github/rafaelmaiolla/remote-vscode) 5 | [![Dependency Status](https://david-dm.org/rafaelmaiolla/remote-vscode.svg)](https://david-dm.org/rafaelmaiolla/remote-vscode) 6 | [![devDependency Status](https://david-dm.org/rafaelmaiolla/remote-vscode/dev-status.svg)](https://david-dm.org/rafaelmaiolla/remote-vscode#info=devDependencies) 7 | 8 | A package that implements the Textmate's 'rmate' feature for VSCode. 9 | 10 | ## Installation 11 | 12 | * Install the package from the editor's extension manager. 13 | * Install a rmate version 14 | - Ruby version: https://github.com/textmate/rmate 15 | - Bash version: https://github.com/aurora/rmate 16 | - Perl version: https://github.com/davidolrik/rmate-perl 17 | - Python version: https://github.com/sclukey/rmate-python 18 | - Nim version: https://github.com/aurora/rmate-nim 19 | - C version: https://github.com/hanklords/rmate.c 20 | - Node.js version: https://github.com/jrnewell/jmate 21 | - Golang version: https://github.com/mattn/gomate 22 | 23 | ## Usage 24 | 25 | * Configure the following in VS Code User Settings: 26 | ```javascript 27 | //-------- Remote VSCode configuration -------- 28 | 29 | // Port number to use for connection. 30 | "remote.port": 52698, 31 | 32 | // Launch the server on start up. 33 | "remote.onstartup": true, 34 | 35 | // Address to listen on. 36 | "remote.host": "127.0.0.1", 37 | 38 | // If set to true, error for remote.port already in use won't be shown anymore. 39 | "remote.dontShowPortAlreadyInUseError": false, 40 | ``` 41 | 42 | * Start the server in the command palette - Press F1 and type `Remote: Start server`, and press `ENTER` to start the server. 43 | You may see a `Starting server` at the status bar in the bottom. 44 | 45 | * Create an ssh tunnel 46 | ```bash 47 | ssh -R 52698:127.0.0.1:52698 user@example.org 48 | ``` 49 | 50 | * Go to the remote system and run 51 | ```bash 52 | rmate -p 52698 file 53 | ``` 54 | 55 | ## License 56 | [MIT](LICENSE.txt) 57 | -------------------------------------------------------------------------------- /src/lib/StatusBarItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import Server from './Server'; 3 | import Logger from '../utils/Logger'; 4 | 5 | const L = Logger.getLogger('StatusBarItem'); 6 | 7 | class StatusBarItem { 8 | server: Server = null; 9 | item: vscode.StatusBarItem; 10 | 11 | constructor() { 12 | L.trace('constructor'); 13 | 14 | this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); 15 | this.item.color = new vscode.ThemeColor('statusBar.foreground'); 16 | this.item.text = '$(rss)'; 17 | } 18 | 19 | setServer(server: Server) { 20 | L.trace('setServer'); 21 | 22 | if (this.server) { 23 | L.debug('setServer', 'remove all listeners'); 24 | this.server.removeAllListeners(); 25 | } 26 | 27 | this.server = server; 28 | 29 | this.handleEvents(server); 30 | } 31 | 32 | handleEvents(server: Server) { 33 | L.trace('handleEvents'); 34 | 35 | server.on('restarting', this.onRestarting.bind(this)); 36 | server.on('starting', this.onStarting.bind(this)); 37 | server.on('ready', this.onReady.bind(this)); 38 | server.on('error', this.onError.bind(this)); 39 | server.on('stopped', this.onStopped.bind(this)) 40 | } 41 | 42 | onRestarting() { 43 | L.trace('onRestarting'); 44 | 45 | this.item.tooltip = 'Remote: Restarting server...'; 46 | this.item.color = new vscode.ThemeColor('statusBar.foreground'); 47 | this.item.show(); 48 | } 49 | 50 | onStarting() { 51 | L.trace('onStarting'); 52 | 53 | this.item.tooltip = 'Remote: Starting server...'; 54 | this.item.color = new vscode.ThemeColor('statusBar.foreground'); 55 | this.item.show(); 56 | } 57 | 58 | onReady() { 59 | L.trace('onReady'); 60 | 61 | this.item.tooltip = 'Remote: Server ready.'; 62 | this.item.color = new vscode.ThemeColor('statusBar.foreground'); 63 | this.item.show(); 64 | } 65 | 66 | onError(e) { 67 | L.trace('onError'); 68 | 69 | if (e.code == 'EADDRINUSE') { 70 | L.debug('onError', 'EADDRINUSE'); 71 | this.item.tooltip = 'Remote error: Port already in use.'; 72 | 73 | } else { 74 | this.item.tooltip = 'Remote error: Failed to start server.'; 75 | } 76 | 77 | this.item.color = new vscode.ThemeColor('errorForeground'); 78 | this.item.show(); 79 | } 80 | 81 | onStopped() { 82 | L.trace('onStopped'); 83 | 84 | this.item.tooltip = 'Remote: Server stopped.'; 85 | this.item.color = new vscode.ThemeColor('statusBar.foreground'); 86 | this.item.hide(); 87 | } 88 | } 89 | 90 | export default StatusBarItem; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote-vscode", 3 | "displayName": "Remote VSCode", 4 | "description": "A package that implements the Textmate's 'rmate' feature for VSCode.", 5 | "version": "1.2.0", 6 | "publisher": "rafaelmaiolla", 7 | "license": "MIT", 8 | "author": "Rafael Maiolla ", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/rafaelmaiolla/remote-vscode.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/rafaelmaiolla/remote-vscode/issues" 15 | }, 16 | "engines": { 17 | "vscode": "^1.13.0" 18 | }, 19 | "keywords": [ 20 | "rmate", 21 | "Remote", 22 | "Visual Studio Code", 23 | "VSCode" 24 | ], 25 | "categories": [ 26 | "Other" 27 | ], 28 | "activationEvents": [ 29 | "*", 30 | "onCommand:extension.startServer", 31 | "onCommand:extension.stopServer" 32 | ], 33 | "main": "./out/src/extension", 34 | "contributes": { 35 | "configuration": { 36 | "type": "object", 37 | "title": "Remote VSCode configuration", 38 | "properties": { 39 | "remote.port": { 40 | "type": "number", 41 | "default": 52698, 42 | "description": "Port number to use for connection." 43 | }, 44 | "remote.onstartup": { 45 | "type": "boolean", 46 | "default": false, 47 | "description": "Launch the server on start up." 48 | }, 49 | "remote.host": { 50 | "type": "string", 51 | "default": "127.0.0.1", 52 | "description": "Address to listen on." 53 | }, 54 | "remote.dontShowPortAlreadyInUseError": { 55 | "type": "boolean", 56 | "default": false, 57 | "description": "If set to true, error for remote.port already in use won't be shown anymore." 58 | } 59 | } 60 | }, 61 | "commands": [ 62 | { 63 | "command": "extension.startServer", 64 | "title": "Remote: Start Server" 65 | }, 66 | { 67 | "command": "extension.stopServer", 68 | "title": "Remote: Stop Server" 69 | } 70 | ] 71 | }, 72 | "scripts": { 73 | "test": "node ./node_modules/vscode/bin/test", 74 | "vscode:prepublish": "tsc -p ./", 75 | "compile": "tsc -watch -p ./", 76 | "postinstall": "node ./node_modules/vscode/bin/install", 77 | "lint": "eslint src" 78 | }, 79 | "devDependencies": { 80 | "@types/fs-extra": "5.0.4", 81 | "@types/log4js": "2.3.5", 82 | "@types/mocha": "2.2.39", 83 | "@types/node": "10.12.12", 84 | "vscode": "1.1.26", 85 | "typescript": "3.2.1" 86 | }, 87 | "dependencies": { 88 | "fs-extra": "7.0.1", 89 | "log4js": "3.0.6" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import Server from './lib/Server'; 4 | import Logger from './utils/Logger'; 5 | import StatusBarItem from './lib/StatusBarItem'; 6 | 7 | const L = Logger.getLogger('extension'); 8 | 9 | var server : Server; 10 | var changeConfigurationDisposable : vscode.Disposable; 11 | var port : number; 12 | var host : string; 13 | var onStartup : boolean; 14 | var dontShowPortAlreadyInUseError : boolean; 15 | var statusBarItem : StatusBarItem; 16 | 17 | const startServer = () => { 18 | L.trace('startServer'); 19 | 20 | if (!server) { 21 | server = new Server(); 22 | } 23 | 24 | if (!statusBarItem) { 25 | statusBarItem = new StatusBarItem(); 26 | } 27 | 28 | server.setPort(port); 29 | server.setHost(host); 30 | server.setDontShowPortAlreadyInUseError(dontShowPortAlreadyInUseError); 31 | server.start(false); 32 | 33 | statusBarItem.setServer(server); 34 | }; 35 | 36 | const stopServer = () => { 37 | L.trace('stopServer'); 38 | 39 | if (server) { 40 | server.stop(); 41 | } 42 | }; 43 | 44 | const initialize = () => { 45 | L.trace('initialize'); 46 | 47 | var configuration = getConfiguration(); 48 | onStartup = configuration.onStartup; 49 | port = configuration.port; 50 | host = configuration.host; 51 | dontShowPortAlreadyInUseError = configuration.dontShowPortAlreadyInUseError; 52 | 53 | if (onStartup) { 54 | startServer(); 55 | } 56 | }; 57 | 58 | const getConfiguration = () => { 59 | L.trace('getConfiguration'); 60 | var remoteConfig = vscode.workspace.getConfiguration('remote'); 61 | 62 | var configuration = { 63 | onStartup: remoteConfig.get('onstartup'), 64 | dontShowPortAlreadyInUseError: remoteConfig.get('dontShowPortAlreadyInUseError'), 65 | port: remoteConfig.get('port'), 66 | host: remoteConfig.get('host') 67 | }; 68 | 69 | L.debug("getConfiguration", configuration); 70 | 71 | return configuration; 72 | }; 73 | 74 | const hasConfigurationChanged = (configuration) => { 75 | L.trace('hasConfigurationChanged'); 76 | var hasChanged = ((configuration.port !== port) || 77 | (configuration.onStartup !== onStartup) || 78 | (configuration.host !== host) || 79 | (configuration.dontShowPortAlreadyInUseError !== dontShowPortAlreadyInUseError)); 80 | 81 | L.debug("hasConfigurationChanged?", hasChanged); 82 | return hasChanged; 83 | } 84 | 85 | const onConfigurationChange = () => { 86 | L.trace('onConfigurationChange'); 87 | 88 | var configuration = getConfiguration(); 89 | 90 | if (hasConfigurationChanged(configuration)) { 91 | initialize(); 92 | } 93 | }; 94 | 95 | export function activate(context: vscode.ExtensionContext) { 96 | initialize(); 97 | 98 | context.subscriptions.push(vscode.commands.registerCommand('extension.startServer', startServer)); 99 | context.subscriptions.push(vscode.commands.registerCommand('extension.stopServer', stopServer)); 100 | 101 | changeConfigurationDisposable = vscode.workspace.onDidChangeConfiguration(onConfigurationChange); 102 | } 103 | 104 | export function deactivate() { 105 | stopServer(); 106 | changeConfigurationDisposable.dispose(); 107 | } 108 | -------------------------------------------------------------------------------- /src/lib/RemoteFile.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as fse from 'fs-extra'; 3 | import * as os from 'os'; 4 | import * as path from 'path'; 5 | import randomString from '../utils/randomString'; 6 | import Logger from '../utils/Logger'; 7 | 8 | const L = Logger.getLogger('RemoteFile'); 9 | 10 | class RemoteFile { 11 | dataSize : number; 12 | writtenDataSize : number = 0; 13 | 14 | token : string; 15 | localFilePath : string; 16 | 17 | remoteHost : string; 18 | remoteBaseName : string; 19 | 20 | fd : number; 21 | 22 | constructor() { 23 | L.trace('constructor'); 24 | } 25 | 26 | setToken(token : string) { 27 | this.token = token; 28 | } 29 | 30 | getToken() { 31 | L.trace('getRemoteBaseName'); 32 | return this.token; 33 | } 34 | 35 | setDisplayName(displayName : string) { 36 | var displayNameSplit = displayName.split(':'); 37 | 38 | if (displayNameSplit.length === 1) { 39 | this.remoteHost = ""; 40 | 41 | } else { 42 | this.remoteHost = displayNameSplit.shift(); 43 | } 44 | 45 | this.remoteBaseName = displayNameSplit.join(":"); 46 | } 47 | 48 | getHost() { 49 | L.trace('getHost', this.remoteHost); 50 | return this.remoteHost; 51 | } 52 | 53 | getRemoteBaseName() { 54 | L.trace('getRemoteBaseName'); 55 | return this.remoteBaseName; 56 | } 57 | 58 | createLocalFilePath() { 59 | L.trace('createLocalFilePath'); 60 | this.localFilePath = path.join(os.tmpdir(), randomString(10), this.getRemoteBaseName()); 61 | } 62 | 63 | getLocalDirectoryName() { 64 | L.trace('getLocalDirectoryName', path.dirname(this.localFilePath || "")); 65 | if (!this.localFilePath) { 66 | return; 67 | } 68 | return path.dirname(this.localFilePath); 69 | } 70 | 71 | createLocalDir() { 72 | L.trace('createLocalDir'); 73 | fse.mkdirsSync(this.getLocalDirectoryName()); 74 | } 75 | 76 | getLocalFilePath() { 77 | L.trace('getLocalFilePath', this.localFilePath); 78 | return this.localFilePath; 79 | } 80 | 81 | openSync() { 82 | L.trace('openSync'); 83 | this.fd = fs.openSync(this.getLocalFilePath(), 'w'); 84 | } 85 | 86 | closeSync() { 87 | L.trace('closeSync'); 88 | fs.closeSync(this.fd); 89 | this.fd = null; 90 | } 91 | 92 | initialize() { 93 | L.trace('initialize'); 94 | this.createLocalFilePath(); 95 | this.createLocalDir(); 96 | this.openSync(); 97 | } 98 | 99 | writeSync(buffer : any, offset : number, length : number) { 100 | L.trace('writeSync'); 101 | if (this.fd) { 102 | L.debug('writing data'); 103 | fs.writeSync(this.fd, buffer, offset, length, undefined); 104 | } 105 | } 106 | 107 | readFileSync() : Buffer { 108 | L.trace('readFileSync'); 109 | return fs.readFileSync(this.localFilePath); 110 | } 111 | 112 | appendData(buffer : Buffer) { 113 | L.trace('appendData', buffer.length); 114 | 115 | var length = buffer.length; 116 | if (this.writtenDataSize + length > this.dataSize) { 117 | length = this.dataSize - this.writtenDataSize; 118 | } 119 | 120 | this.writtenDataSize += length; 121 | L.debug("writtenDataSize", this.writtenDataSize); 122 | 123 | this.writeSync(buffer, 0, length); 124 | } 125 | 126 | setDataSize(dataSize : number) { 127 | L.trace('setDataSize', dataSize); 128 | this.dataSize = dataSize; 129 | } 130 | 131 | getDataSize() : number { 132 | L.trace('getDataSize'); 133 | L.debug('getDataSize', this.dataSize); 134 | return this.dataSize; 135 | } 136 | 137 | isEmpty() : boolean { 138 | L.trace('isEmpty'); 139 | L.debug('isEmpty?', this.dataSize == null); 140 | return this.dataSize == null; 141 | } 142 | 143 | isReady() : boolean { 144 | L.trace('isReady'); 145 | L.debug('isReady?', this.writtenDataSize == this.dataSize); 146 | return this.writtenDataSize == this.dataSize; 147 | } 148 | } 149 | 150 | export default RemoteFile; -------------------------------------------------------------------------------- /src/lib/Server.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net'; 2 | import Session from "./Session"; 3 | import * as vscode from 'vscode'; 4 | import Logger from '../utils/Logger'; 5 | import {EventEmitter} from 'events'; 6 | 7 | const L = Logger.getLogger('Server'); 8 | 9 | const DEFAULT_PORT = 52698; 10 | const DEFAULT_HOST = '127.0.0.1'; 11 | 12 | class Server extends EventEmitter { 13 | online : boolean = false; 14 | server : net.Server; 15 | port : number; 16 | host : string; 17 | dontShowPortAlreadyInUseError : boolean = false; 18 | defaultSession : Session; 19 | 20 | constructor() { 21 | super(); 22 | L.trace('constructor'); 23 | } 24 | 25 | start(quiet : boolean) { 26 | L.trace('start', quiet); 27 | 28 | if (this.isOnline()) { 29 | this.stop(); 30 | L.info("Restarting server"); 31 | vscode.window.setStatusBarMessage("Restarting server", 2000); 32 | this.emit('restarting'); 33 | 34 | } else { 35 | if (!quiet) { 36 | L.info("Starting server"); 37 | vscode.window.setStatusBarMessage("Starting server", 2000); 38 | } 39 | 40 | this.emit('starting'); 41 | } 42 | 43 | this.server = net.createServer(this.onServerConnection.bind(this)); 44 | 45 | this.server.on('listening', this.onServerListening.bind(this)); 46 | this.server.on('error', this.onServerError.bind(this)); 47 | this.server.on("close", this.onServerClose.bind(this)); 48 | 49 | this.server.listen(this.getPort(), this.getHost()); 50 | } 51 | 52 | setPort(port : number) { 53 | L.trace('setPort', port); 54 | this.port = port; 55 | } 56 | 57 | getPort() : number { 58 | L.trace('getPort', +(this.port || DEFAULT_PORT)); 59 | return +(this.port || DEFAULT_PORT); 60 | } 61 | 62 | setHost(host : string) { 63 | L.trace('setHost', host); 64 | this.host = host; 65 | } 66 | 67 | getHost() : string { 68 | L.trace('getHost', +(this.host || DEFAULT_HOST)); 69 | return (this.host || DEFAULT_HOST); 70 | } 71 | 72 | setDontShowPortAlreadyInUseError(dontShowPortAlreadyInUseError : boolean) { 73 | L.trace('setDontShowPortAlreadyInUseError', dontShowPortAlreadyInUseError); 74 | this.dontShowPortAlreadyInUseError = dontShowPortAlreadyInUseError; 75 | } 76 | 77 | onServerConnection(socket) { 78 | L.trace('onServerConnection'); 79 | 80 | var session = new Session(socket); 81 | session.send("VSCode " + 1); 82 | 83 | session.on('connect', () => { 84 | console.log("connect"); 85 | this.defaultSession = session; 86 | }); 87 | } 88 | 89 | onServerListening(e) { 90 | L.trace('onServerListening'); 91 | this.setOnline(true); 92 | this.emit('ready'); 93 | } 94 | 95 | onServerError(e) { 96 | L.trace('onServerError', e); 97 | 98 | this.emit('error', e); 99 | 100 | if (e.code == 'EADDRINUSE') { 101 | if (this.dontShowPortAlreadyInUseError) { 102 | return; 103 | } else { 104 | return vscode.window.showErrorMessage(`Failed to start server, port ${e.port} already in use`); 105 | } 106 | } 107 | 108 | vscode.window.showErrorMessage(`Failed to start server, will try again in 10 seconds}`); 109 | 110 | setTimeout(() => { 111 | this.start(true); 112 | }, 10000); 113 | } 114 | 115 | onServerClose() { 116 | L.trace('onServerClose'); 117 | } 118 | 119 | stop() { 120 | L.trace('stop'); 121 | 122 | this.emit('stopped'); 123 | 124 | if (this.isOnline()) { 125 | vscode.window.setStatusBarMessage("Stopping server", 2000); 126 | this.server.close(); 127 | this.setOnline(false); 128 | } 129 | } 130 | 131 | setOnline(online : boolean) { 132 | L.trace('setOnline', online); 133 | this.online = online; 134 | } 135 | 136 | isOnline() : boolean { 137 | L.trace('isOnline'); 138 | 139 | L.debug('isOnline?', this.online); 140 | return this.online; 141 | } 142 | } 143 | 144 | export default Server; 145 | -------------------------------------------------------------------------------- /test/RemoteFile.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as assert from 'assert'; 3 | import * as os from 'os'; 4 | import * as path from 'path'; 5 | import * as vscode from 'vscode'; 6 | import RemoteFile from '../src/lib/RemoteFile'; 7 | 8 | suite("Command Tests", () => { 9 | test("constructor", () => { 10 | var remoteFile = new RemoteFile(); 11 | }); 12 | 13 | test("setToken", () => { 14 | var token = "test"; 15 | var remoteFile = new RemoteFile(); 16 | 17 | remoteFile.setToken(token); 18 | 19 | assert.equal(token, remoteFile.getToken()); 20 | }); 21 | 22 | test("getToken", () => { 23 | var token = "test"; 24 | var remoteFile = new RemoteFile(); 25 | 26 | remoteFile.setToken(token); 27 | 28 | assert.equal(token, remoteFile.getToken()); 29 | }); 30 | 31 | test("setDisplayName - stdin", () => { 32 | var remoteFile = new RemoteFile(); 33 | 34 | var hostname = "hostname"; 35 | var remoteBasename = "untitled (stdin)"; 36 | 37 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 38 | 39 | assert.equal(hostname, remoteFile.getHost()); 40 | assert.equal(remoteBasename, remoteFile.getRemoteBaseName()); 41 | }); 42 | 43 | test("setDisplayName - file path", () => { 44 | var remoteFile = new RemoteFile(); 45 | 46 | var hostname = "hostname"; 47 | var remoteBasename = "someFile"; 48 | 49 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 50 | 51 | assert.equal(hostname, remoteFile.getHost()); 52 | assert.equal(remoteBasename, remoteFile.getRemoteBaseName()); 53 | }); 54 | 55 | test("setDisplayName - file name", () => { 56 | var remoteFile = new RemoteFile(); 57 | 58 | var fileName = "this is a file name"; 59 | 60 | remoteFile.setDisplayName(`${fileName}`); 61 | 62 | assert.equal(fileName, remoteFile.getRemoteBaseName()); 63 | }); 64 | 65 | test("getHost", () => { 66 | var remoteFile = new RemoteFile(); 67 | 68 | var hostname = "hostname"; 69 | var remoteBasename = "untitled (stdin)"; 70 | 71 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 72 | assert.equal(hostname, remoteFile.getHost()); 73 | }); 74 | 75 | test("getRemoteBaseName", () => { 76 | var remoteFile = new RemoteFile(); 77 | 78 | var hostname = "hostname"; 79 | var remoteBasename = "someFile"; 80 | 81 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 82 | 83 | assert.equal(remoteBasename, remoteFile.getRemoteBaseName()); 84 | }); 85 | 86 | test("createLocalFilePath", () => { 87 | var remoteFile = new RemoteFile(); 88 | 89 | var hostname = "hostname"; 90 | var remoteBasename = "someFile"; 91 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 92 | remoteFile.createLocalFilePath(); 93 | 94 | var localFilePath = remoteFile.getLocalFilePath(); 95 | 96 | assert.equal(true, localFilePath.startsWith(os.tmpdir())); 97 | assert.equal(true, localFilePath.endsWith(remoteBasename)); 98 | }); 99 | 100 | test("getLocalDirectoryName", () => { 101 | var remoteFile = new RemoteFile(); 102 | 103 | var hostname = "hostname"; 104 | var remoteBasename = "someFile"; 105 | 106 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 107 | 108 | assert.equal(undefined, remoteFile.getLocalDirectoryName()); 109 | 110 | remoteFile.createLocalFilePath(); 111 | 112 | var directoryPath = path.dirname(remoteFile.getLocalFilePath()); 113 | assert.equal(directoryPath, remoteFile.getLocalDirectoryName()); 114 | }); 115 | 116 | test("createLocalDir", () => { 117 | var remoteFile = new RemoteFile(); 118 | 119 | var hostname = "hostname"; 120 | var remoteBasename = "someFile"; 121 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 122 | remoteFile.createLocalFilePath(); 123 | var directoryPath = remoteFile.getLocalDirectoryName(); 124 | remoteFile.createLocalDir(); 125 | 126 | assert.equal(true, fs.statSync(directoryPath).isDirectory()); 127 | }); 128 | 129 | test("getLocalFilePath", () => { 130 | var remoteFile = new RemoteFile(); 131 | 132 | var hostname = "hostname"; 133 | var remoteBasename = "someFile"; 134 | remoteFile.setDisplayName(`${hostname}:${remoteBasename}`); 135 | 136 | assert.equal(undefined, remoteFile.getLocalFilePath()); 137 | 138 | remoteFile.createLocalFilePath(); 139 | 140 | var localFilePath = remoteFile.getLocalFilePath(); 141 | 142 | assert.equal(true, localFilePath.startsWith(os.tmpdir())); 143 | assert.equal(true, localFilePath.endsWith(remoteBasename)); 144 | 145 | }); 146 | 147 | test("openSync", () => { 148 | var remoteFile = new RemoteFile(); 149 | }); 150 | 151 | test("closeSync", () => { 152 | var remoteFile = new RemoteFile(); 153 | }); 154 | 155 | test("writeSync", () => { 156 | var remoteFile = new RemoteFile(); 157 | }); 158 | 159 | test("readFileSync", () => { 160 | var remoteFile = new RemoteFile(); 161 | }); 162 | 163 | test("appendData", () => { 164 | var remoteFile = new RemoteFile(); 165 | }); 166 | 167 | test("setDataSize", () => { 168 | var remoteFile = new RemoteFile(); 169 | }); 170 | 171 | test("getDataSize", () => { 172 | var remoteFile = new RemoteFile(); 173 | }); 174 | 175 | test("isEmpty", () => { 176 | var remoteFile = new RemoteFile(); 177 | }); 178 | 179 | test("isReady", () => { 180 | var remoteFile = new RemoteFile(); 181 | }); 182 | }); -------------------------------------------------------------------------------- /src/lib/Session.ts: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import * as vscode from 'vscode'; 3 | import * as net from 'net'; 4 | import Logger from '../utils/Logger'; 5 | import Command from './Command'; 6 | import RemoteFile from './RemoteFile'; 7 | 8 | const L = Logger.getLogger('Session'); 9 | 10 | class Session extends EventEmitter { 11 | command : Command; 12 | socket : net.Socket; 13 | online : boolean; 14 | subscriptions : Array = []; 15 | remoteFile : RemoteFile; 16 | attempts : number = 0; 17 | closeTimeout : NodeJS.Timer; 18 | 19 | constructor(socket : net.Socket) { 20 | super(); 21 | L.trace('constructor'); 22 | 23 | this.socket = socket; 24 | this.online = true; 25 | 26 | this.socket.on('data', this.onSocketData.bind(this)); 27 | this.socket.on('close', this.onSocketClose.bind(this)); 28 | } 29 | 30 | onSocketData(chunk : Buffer) { 31 | L.trace('onSocketData', chunk); 32 | 33 | if (chunk) { 34 | this.parseChunk(chunk); 35 | } 36 | } 37 | 38 | onSocketClose() { 39 | L.trace('onSocketClose'); 40 | this.online = false; 41 | } 42 | 43 | parseChunk(buffer : any) { 44 | L.trace('parseChunk', buffer); 45 | 46 | if (this.command && this.remoteFile.isReady()) { 47 | return; 48 | } 49 | 50 | var chunk = buffer.toString("utf8"); 51 | var lines = chunk.split("\n"); 52 | 53 | if (!this.command) { 54 | this.command = new Command(lines.shift()); 55 | this.remoteFile = new RemoteFile(); 56 | } 57 | 58 | if (this.remoteFile.isEmpty()) { 59 | while (lines.length) { 60 | var line = lines.shift().trim(); 61 | 62 | if (!line) { 63 | break; 64 | } 65 | 66 | var s = line.split(':'); 67 | var name = s.shift().trim(); 68 | var value = s.join(":").trim(); 69 | 70 | if (name == 'data') { 71 | this.remoteFile.setDataSize(parseInt(value, 10)); 72 | this.remoteFile.setToken(this.command.getVariable('token')); 73 | this.remoteFile.setDisplayName(this.command.getVariable('display-name')); 74 | this.remoteFile.initialize(); 75 | 76 | this.remoteFile.appendData(buffer.slice(buffer.indexOf(line) + Buffer.byteLength(`${line}\n`))); 77 | break; 78 | 79 | } else { 80 | this.command.addVariable(name, value); 81 | } 82 | } 83 | 84 | } else { 85 | this.remoteFile.appendData(buffer); 86 | } 87 | 88 | if (this.remoteFile.isReady()) { 89 | this.remoteFile.closeSync(); 90 | this.handleCommand(this.command); 91 | } 92 | } 93 | 94 | handleCommand(command : Command) { 95 | L.trace('handleCommand', command.getName()); 96 | 97 | switch (command.getName()) { 98 | case 'open': 99 | this.handleOpen(command); 100 | break; 101 | 102 | case 'list': 103 | this.handleList(command); 104 | this.emit('list'); 105 | break; 106 | 107 | case 'connect': 108 | this.handleConnect(command); 109 | this.emit('connect'); 110 | break; 111 | } 112 | } 113 | 114 | openInEditor() { 115 | L.trace('openInEditor'); 116 | 117 | vscode.workspace.openTextDocument(this.remoteFile.getLocalFilePath()).then((textDocument : vscode.TextDocument) => { 118 | if (!textDocument && this.attempts < 3) { 119 | L.warn("Failed to open the text document, will try again"); 120 | 121 | setTimeout(() => { 122 | this.attempts++; 123 | this.openInEditor(); 124 | }, 100); 125 | return; 126 | 127 | } else if (!textDocument) { 128 | L.error("Could NOT open the file", this.remoteFile.getLocalFilePath()); 129 | vscode.window.showErrorMessage(`Failed to open file ${this.remoteFile.getRemoteBaseName()}`); 130 | return; 131 | } 132 | 133 | vscode.window.showTextDocument(textDocument).then((textEditor : vscode.TextEditor) => { 134 | this.handleChanges(textDocument); 135 | L.info(`Opening ${this.remoteFile.getRemoteBaseName()} from ${this.remoteFile.getHost()}`); 136 | vscode.window.setStatusBarMessage(`Opening ${this.remoteFile.getRemoteBaseName()} from ${this.remoteFile.getHost()}`, 2000); 137 | 138 | this.showSelectedLine(textEditor); 139 | }); 140 | }); 141 | } 142 | 143 | handleChanges(textDocument : vscode.TextDocument) { 144 | L.trace('handleChanges', textDocument.fileName); 145 | 146 | this.subscriptions.push(vscode.workspace.onDidSaveTextDocument((savedTextDocument : vscode.TextDocument) => { 147 | if (savedTextDocument == textDocument) { 148 | this.save(); 149 | } 150 | })); 151 | 152 | this.subscriptions.push(vscode.workspace.onDidCloseTextDocument((closedTextDocument : vscode.TextDocument) => { 153 | if (closedTextDocument == textDocument) { 154 | this.closeTimeout && clearTimeout(this.closeTimeout); 155 | // If you change the textDocument language, it will close and re-open the same textDocument, so we add 156 | // a timeout to make sure it is really being closed before close the socket. 157 | this.closeTimeout = setTimeout(() => { 158 | this.close(); 159 | }, 2); 160 | } 161 | })); 162 | 163 | this.subscriptions.push(vscode.workspace.onDidOpenTextDocument((openedTextDocument : vscode.TextDocument) => { 164 | if (openedTextDocument == textDocument) { 165 | this.closeTimeout && clearTimeout(this.closeTimeout); 166 | } 167 | })); 168 | } 169 | 170 | showSelectedLine(textEditor : vscode.TextEditor) { 171 | var selection = +(this.command.getVariable('selection')); 172 | if (selection) { 173 | textEditor.revealRange(new vscode.Range(selection, 0, selection + 1, 1)); 174 | } 175 | } 176 | 177 | handleOpen(command : Command) { 178 | L.trace('handleOpen', command.getName()); 179 | this.openInEditor(); 180 | } 181 | 182 | handleConnect(command : Command) { 183 | L.trace('handleConnect', command.getName()); 184 | } 185 | 186 | handleList(command : Command) { 187 | L.trace('handleList', command.getName()); 188 | } 189 | 190 | send(cmd : string) { 191 | L.trace('send', cmd); 192 | 193 | if (this.isOnline()) { 194 | this.socket.write(cmd + "\n"); 195 | } 196 | } 197 | 198 | open(filePath : string) { 199 | L.trace('filePath', filePath); 200 | 201 | this.send("open"); 202 | this.send(`path: ${filePath}`); 203 | this.send(""); 204 | } 205 | 206 | list(dirPath : string) { 207 | L.trace('list', dirPath); 208 | 209 | this.send("list"); 210 | this.send(`path: ${dirPath}`); 211 | this.send(""); 212 | } 213 | 214 | save() { 215 | L.trace('save'); 216 | 217 | if (!this.isOnline()) { 218 | L.error("NOT online"); 219 | vscode.window.showErrorMessage(`Error saving ${this.remoteFile.getRemoteBaseName()} to ${this.remoteFile.getHost()}`); 220 | return; 221 | } 222 | 223 | vscode.window.setStatusBarMessage(`Saving ${this.remoteFile.getRemoteBaseName()} to ${this.remoteFile.getHost()}`, 2000); 224 | 225 | var buffer = this.remoteFile.readFileSync(); 226 | 227 | this.send("save"); 228 | this.send(`token: ${this.remoteFile.getToken()}`); 229 | this.send("data: " + buffer.length); 230 | this.socket.write(buffer); 231 | this.send(""); 232 | } 233 | 234 | close() { 235 | L.trace('close'); 236 | 237 | if (this.isOnline()) { 238 | this.online = false; 239 | this.send("close"); 240 | this.send(""); 241 | this.socket.end(); 242 | } 243 | 244 | this.subscriptions.forEach((disposable : vscode.Disposable) => disposable.dispose()); 245 | } 246 | 247 | isOnline() { 248 | L.trace('isOnline'); 249 | 250 | L.debug('isOnline?', this.online); 251 | return this.online; 252 | } 253 | } 254 | 255 | export default Session; 256 | --------------------------------------------------------------------------------