Initial commit (a fork of sorts from remote-code https://www.npmjs.com/package/remote-code)
Started refactoring the 4 primary classes Added ignore list generator from various default ignore files. wrote corresponding test -- passingmaster
parent
40f1e80bc4
commit
11ca82e084
|
@ -0,0 +1,37 @@
|
|||
module.exports = {
|
||||
"ecmaFeatures": {
|
||||
"modules": true,
|
||||
"spread" : true,
|
||||
"restParams" : true
|
||||
},
|
||||
// "plugins": [
|
||||
// "unicorn"
|
||||
// ],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
// "unicorn/no-array-instanceof": "error",
|
||||
"no-console": 0,
|
||||
"semi": ["error", "never"],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/node_modules/
|
||||
/coverage/
|
|
@ -0,0 +1,5 @@
|
|||
tests/
|
||||
test/
|
||||
*.test.js
|
||||
testing/
|
||||
example/
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "@uci/remote-code",
|
||||
"version": "0.0.1",
|
||||
"description": "module to copy, maintain, and launch hardware modules on other machines",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"testd": "./node_modules/.bin/mocha -r esm --watch --timeout 30000 || exit 0",
|
||||
"test": "./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0",
|
||||
"testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
|
||||
},
|
||||
"author": "David Kebler",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/uCOMmandIt/uci-remote-code.git"
|
||||
},
|
||||
"keywords": [
|
||||
"node.js",
|
||||
"I2C",
|
||||
"raspberryPi"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/uCOMmandIt/uci-remote-code/issues"
|
||||
},
|
||||
"homepage": "https://github.com/uCOMmandIt/uci-remote-code#readme",
|
||||
"dependencies": {
|
||||
"chokidar": "^2.0.4",
|
||||
"p-settle": "^2.1.0",
|
||||
"rsyncwrapper": "^3.0.1",
|
||||
"ssh2": "^0.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"codecov": "^3.1.0",
|
||||
"esm": "^3.1.4",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^5.x"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# uCOMmandIt Template Package Repository
|
||||
|
||||
<!-- find and replace the package name to match -->
|
||||
[![Build Status](https://img.shields.io/travis/uCOMmandIt/uci-pkg-template.svg?branch=master)](https://travis-ci.org/uCOMmandIt/uci-pkg-template)
|
||||
[![Inline docs](http://inch-ci.org/github/uCOMmandIt/uci-pkg-template.svg?branch=master)](http://inch-ci.org/github/uCOMmandIt/uci-pkg-template)
|
||||
[![Dependencies](https://img.shields.io/david/uCOMmandIt/uci-pkg-template.svg)](https://david-dm.org/uCOMmandIt/uci-pkg-template)
|
||||
[![devDependencies](https://img.shields.io/david/dev/uCOMmandIt/uci-pkg-template.svg)](https://david-dm.org/uCOMmandIt/uci-pkg-template?type=dev)
|
||||
[![codecov](https://img.shields.io/codecov/c/github/uCOMmandIt/uci-pkg-template/master.svg)](https://codecov.io/gh/uCOMmandIt/uci-pkg-template)
|
||||
|
||||
Clone this to get a quick start on a new uci package. It has all the testing ready to go with Travis-CI and code coverage. All the readme badges are included as well
|
||||
|
||||
Clone it for as a starting place for your own package!
|
||||
|
||||
You'll need codecov and travis-ci accounts
|
||||
|
||||
##Steps
|
||||
|
||||
1. Clone repo
|
||||
2. Edit package.json
|
||||
3. Edit badge urls above changing to repo path
|
||||
4. Start Coding
|
|
@ -0,0 +1,39 @@
|
|||
import settle from 'p-settle'
|
||||
import { promisify } from 'util'
|
||||
import { readFile } from 'fs'
|
||||
import path from 'path'
|
||||
const read = promisify(readFile)
|
||||
|
||||
// A helper function to return a list of paths to ignore from .npmignore, .gitignore, .rcignore
|
||||
function ignore (repoPath,files) {
|
||||
// console.log('additional files', files)
|
||||
let ignoreList = []
|
||||
let ignoreFiles = ['.npmignore','.gitignore','.rcignore']
|
||||
if (Array.isArray(files)) {
|
||||
ignoreFiles=[...ignoreFiles,...files]
|
||||
} else {
|
||||
if (files) ignoreFiles.push(files)
|
||||
}
|
||||
|
||||
// each set in an the array is new line delimited set of ignore patterns
|
||||
return settle(ignoreFiles.map(file => {
|
||||
// console.log('directory',path.dirname(file))
|
||||
if (path.dirname(file)==='.') file = repoPath+'/'+file
|
||||
// console.log('file', file)
|
||||
return read(file)
|
||||
}))
|
||||
.then((sets) => {
|
||||
sets.forEach( set => {
|
||||
if (set.isFulfilled) ignoreList.push(...set.value.toString().match(/.+/g))
|
||||
else console.log('READ ERROR=> ',set.reason.Error)
|
||||
})
|
||||
// console.log('built list=====================', ignoreList)
|
||||
return Promise.resolve(ignoreList)
|
||||
})
|
||||
.catch((err) => {
|
||||
// only returned when something horrible is wrong
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
export default ignore
|
|
@ -0,0 +1,8 @@
|
|||
import RemoteCode from './remote-code'
|
||||
import Ssh from './ssh'
|
||||
import Sync from './sync'
|
||||
import Watcher from './watcher'
|
||||
import ignore from './ignore'
|
||||
|
||||
export { RemoteCode, Ssh, Sync, Watcher, ignore }
|
||||
export default RemoteCode
|
|
@ -0,0 +1,123 @@
|
|||
import { EventEmitter as Emitter } from 'events'
|
||||
import stream from 'stream'
|
||||
import Watcher from './watcher'
|
||||
import Ssh from './ssh'
|
||||
import Sync from './lib/sync'
|
||||
|
||||
const devNull = new stream.Writable()
|
||||
devNull._write = () => null
|
||||
|
||||
class RemoteCode {
|
||||
constructor(opts = {}) {
|
||||
// Set defaults
|
||||
// a id emitted with every event emit that can identify which remote code emitted
|
||||
this.id = opts.id
|
||||
this.watch = opts.watch || false
|
||||
this.options.ssh.keepaliveInterval = opts.ssh.keepaliveInterval || 500
|
||||
this.options.ssh.readyTimeout = opts.ssh.readyTimeout || 2000
|
||||
this.options.ssh.port = opts.ssh.port || 22
|
||||
this.options.source = opts.source || '.'
|
||||
this.options.install = opts.install || 'yarn'
|
||||
this.options.install = opts.registry ? `${this.options.install} --registry ${opts.registry}` : this.options.install
|
||||
this.options.start = opts.start || 'nodemon .'
|
||||
if (!(opts.ssh.keyfilePath || opts.ssh.password)) {
|
||||
this.options.ssh.agent = process.env[opts.ssh.agent] || process.env.SSH_AUTH_SOCK || process.env.SSH_AGENT_SOCK
|
||||
if (!this.options.ssh.agent) {
|
||||
return new Error('no ssh authentification method provided')
|
||||
}
|
||||
}
|
||||
this.ssh = new Ssh()
|
||||
this.verbose = opts.verbose || false
|
||||
this.stdout = opts.stdout instanceof stream.Writable ? opts.stdout : new stream.Writable()
|
||||
this.stderr = opts.stderr instanceof stream.Writable ? opts.stderr : new stream.Writable()
|
||||
this.watcher = new Watcher(this.options)
|
||||
this.sync = new Sync(this.options)
|
||||
.addStdOutStream(this.stdout)
|
||||
.addStdErrStream(this.stderr)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_getStdOut() {
|
||||
if (this.verbose) {
|
||||
return this.stdout
|
||||
}
|
||||
return devNull
|
||||
}
|
||||
|
||||
_getStdErr() {
|
||||
if (this.verbose) {
|
||||
return this.stderr
|
||||
}
|
||||
return devNull
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.emit('sync',{})
|
||||
return this.sync.execute()
|
||||
}
|
||||
|
||||
watch() {
|
||||
this.watcher.start()
|
||||
const watchEmitter = this.watcher.getEventEmitter()
|
||||
watchEmitter.on('sync', () => {
|
||||
this.syncCode()
|
||||
})
|
||||
watchEmitter.on('install', () => {
|
||||
return this.install()
|
||||
.then(() => {
|
||||
this.ssh.liveReload.send('rs')
|
||||
})
|
||||
})
|
||||
return this.emitter
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.emitter.emit('start')
|
||||
const sshSettings = this.options.ssh
|
||||
return Promise.all([this.syncCode(), this.watch()])
|
||||
.then(() => this.install())
|
||||
.then(() => this.ssh.liveReload.connect(sshSettings))
|
||||
.then(() => {
|
||||
this.emitter.emit('nodemon', 'start')
|
||||
this.ssh.liveReload.send(`cd ${this.options.target} && ${this.options.start}`)
|
||||
})
|
||||
.catch(this._abort.bind(this))
|
||||
}
|
||||
|
||||
// execute a single command and then resolve
|
||||
execute(cmd, stdout, stderr) {
|
||||
console.log('remote codes exec', cmd)
|
||||
this.emitter.emit('exec', cmd)
|
||||
const ssh = new Ssh()
|
||||
const result = ssh.exec(this.options.ssh, cmd, stdout, stderr)
|
||||
return result
|
||||
}
|
||||
|
||||
async install() {
|
||||
this.emit('install', 'triggered')
|
||||
if (!this.installInProgress) {
|
||||
this.emitter.emit('install', 'started')
|
||||
console.log('calling execute from install')
|
||||
return this.execute(`cd ${this.options.target} && ${this.options.install}`, this._getStdOut(), this._getStdErr())
|
||||
.then(res => {
|
||||
this.emit('install', 'done', res)
|
||||
this.installInProgress = false
|
||||
return res
|
||||
})
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
close() {
|
||||
this.emitter.emit('close')
|
||||
return Promise.all([this.watcher.close(),
|
||||
this.ssh.liveReload.close()])
|
||||
}
|
||||
|
||||
_abort(err) {
|
||||
this.emitter.emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoteCode
|
|
@ -0,0 +1,97 @@
|
|||
const fs = require('fs')
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const ssh = require('ssh2')
|
||||
|
||||
const Client = ssh.Client
|
||||
|
||||
class Ssh extends Client {
|
||||
constructor() {
|
||||
super()
|
||||
this._emitter = new EventEmitter()
|
||||
this._ignoreStdOut = []
|
||||
return this.init()
|
||||
}
|
||||
|
||||
init() {
|
||||
const conn = this
|
||||
conn.on('ready', () => {
|
||||
conn.shell((err, stream) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
this.stream = stream
|
||||
this._emit('connect', 'Connection established')
|
||||
stream.on('close', () => {
|
||||
this._emit('close', 'Connection closed')
|
||||
conn.end()
|
||||
})
|
||||
stream.on('data', data => this._emit('data', data))
|
||||
stream.stderr.on('data', data => this._emit('error', data))
|
||||
})
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
connect(opts = {}) {
|
||||
opts.privateKey = opts.keyfilePath ? fs.readFileSync(opts.keyfilePath) : null
|
||||
super.connect(opts)
|
||||
return new Promise(resolve => {
|
||||
this.getEventEmitter().on('connect', () => resolve(this))
|
||||
})
|
||||
}
|
||||
|
||||
getEventEmitter() {
|
||||
return this._emitter
|
||||
}
|
||||
|
||||
_emit(...args) {
|
||||
this._emitter.emit(...args)
|
||||
}
|
||||
|
||||
send(line) {
|
||||
this._ignoreStdOut.push(line)
|
||||
if (this.stream.writable) {
|
||||
this.stream.write(`${line}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.end()
|
||||
this.stream = null
|
||||
return this
|
||||
}
|
||||
|
||||
// Supercedes ssh2.exec
|
||||
// execute a single command on the server and close & resolve
|
||||
// uses interactive shell to get stdout/stderr back
|
||||
// stdout, stderr are streams
|
||||
exec(opts, command = 'uptime', stdout, stderr) {
|
||||
let data = ''
|
||||
console.log('executing', opts, command)
|
||||
return new Promise((resolve, reject) => {
|
||||
this
|
||||
.on('ready', () => {
|
||||
this.shell((err, stream) => {
|
||||
stream
|
||||
.on('close', () => {
|
||||
this.end()
|
||||
resolve(data)
|
||||
})
|
||||
.on('data', s => {
|
||||
data += s
|
||||
stdout.write(s)
|
||||
})
|
||||
.stderr.on('data', s => {
|
||||
data += s
|
||||
stderr.write(s)
|
||||
})
|
||||
stream.end(command + '\nexit\n')
|
||||
})
|
||||
})
|
||||
.on('error', e => reject(e))
|
||||
.connect(opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Ssh
|
|
@ -0,0 +1,61 @@
|
|||
const path = require('path')
|
||||
const Rsync = require('rsync')
|
||||
|
||||
class Sync {
|
||||
constructor(opts) {
|
||||
this.options = opts
|
||||
this.rsync = new Rsync()
|
||||
.archive()
|
||||
.delete()
|
||||
.compress()
|
||||
.dirs()
|
||||
.exclude('node_modules/')
|
||||
.exclude('.git/')
|
||||
.source(path.join(opts.source, '/'))
|
||||
.destination(`${opts.ssh.username}@${opts.ssh.host}:${path.join(opts.target)}`)
|
||||
// can pass array of string of addition exlcudes - from a list likely
|
||||
if (opts.excludes) {
|
||||
this.rsync.exclude(opts.excludes)
|
||||
}
|
||||
if (opts.ssh.agent) {
|
||||
this.rsync.shell(`ssh -p ${opts.ssh.port}`)
|
||||
}
|
||||
if (opts.ssh.keyfilePath) {
|
||||
// This does NOT work with keys with pass phrase, use an ssh agent!
|
||||
this.rsync.shell(`ssh -i ${opts.ssh.keyfilePath} -p ${opts.ssh.port}`)
|
||||
}
|
||||
if (opts.ssh.password) {
|
||||
this.rsync.shell(`sshpass -p ${opts.ssh.password} ssh -o StrictHostKeyChecking=no -l ${opts.ssh.username}`)
|
||||
}
|
||||
console.log(this.rsync.command())
|
||||
this.syncInProgress = false
|
||||
return this
|
||||
}
|
||||
|
||||
addStdOutStream(stream) {
|
||||
this.stdOutStream = stream
|
||||
return this
|
||||
}
|
||||
|
||||
addStdErrStream(stream) {
|
||||
this.stdErrStream = stream
|
||||
return this
|
||||
}
|
||||
|
||||
execute() {
|
||||
this.syncInProgress = true
|
||||
return new Promise((resolve, reject) => {
|
||||
this.rsync.execute((err, resCode) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
this.syncInProgress = false
|
||||
resolve(resCode)
|
||||
},
|
||||
this.stdOutStream,
|
||||
this.stdErrStream)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sync
|
|
@ -0,0 +1,41 @@
|
|||
import { EventEmitter as Emitter } from 'events'
|
||||
import path from 'path'
|
||||
import chokidar from 'chokidar'
|
||||
|
||||
class Watcher extends Emitter {
|
||||
constructor(options) {
|
||||
super()
|
||||
// pass in ignores
|
||||
const opts = {
|
||||
ignored: '**/node_modules/**',
|
||||
ignoreInitial: true
|
||||
}
|
||||
const watcher = chokidar.watch(options.source, opts)
|
||||
this.watcher = watcher
|
||||
}
|
||||
|
||||
start() {
|
||||
const handler = (type, f) => {
|
||||
const fname = path.basename(f)
|
||||
if ( fname.toLowerCase() === 'package.json')
|
||||
if (type !=='change') {
|
||||
this.emit('error',new Error('package.json was added or removed, ignoring sync and reinstall'))
|
||||
return
|
||||
} else{
|
||||
this.emit('install', f)
|
||||
}
|
||||
this.emit('sync', f)
|
||||
} // end handler
|
||||
this.watcher
|
||||
.on('add', handler.bind(this, 'add'))
|
||||
.on('change', handler.bind(this, 'change'))
|
||||
.on('unlink', handler.bind(this, 'remove'))
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.watcher.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Watcher
|
|
@ -0,0 +1,49 @@
|
|||
import events from 'events';
|
||||
import test from 'ava';
|
||||
import td from 'testdouble';
|
||||
|
||||
const chokidarStub = {};
|
||||
|
||||
td.replace('chokidar', chokidarStub);
|
||||
const Fn = require('./watcher');
|
||||
|
||||
function setup(stubs = {}) {
|
||||
chokidarStub.watch = stubs.watch || (() => new events.EventEmitter());
|
||||
return new Fn({source: '.'});
|
||||
}
|
||||
|
||||
test('should emit "sync" if file was added/changed/deleted', t => {
|
||||
t.plan(3);
|
||||
const fn = setup();
|
||||
fn.start();
|
||||
fn.getEventEmitter().on('sync', () => t.pass('file change detected'));
|
||||
fn.watcher.emit('add', 'file.txt');
|
||||
fn.watcher.emit('change', 'file.txt');
|
||||
fn.watcher.emit('unlink', 'file.txt');
|
||||
});
|
||||
|
||||
test('should emit "install" if "package.json" or "yarn.lock" was added/changed/deleted', t => {
|
||||
t.plan(6);
|
||||
const fn = setup();
|
||||
fn.start();
|
||||
fn.getEventEmitter().on('install', () => t.pass('file change detected'));
|
||||
fn.watcher.emit('add', 'package.json');
|
||||
fn.watcher.emit('change', 'package.json');
|
||||
fn.watcher.emit('unlink', 'package.json');
|
||||
fn.watcher.emit('add', 'yarn.lock');
|
||||
fn.watcher.emit('change', 'yarn.lock');
|
||||
fn.watcher.emit('unlink', 'yarn.lock');
|
||||
});
|
||||
|
||||
test('should emit "sync" if "package.json" or "yarn.lock" was added/changed/deleted', t => {
|
||||
t.plan(6);
|
||||
const fn = setup();
|
||||
fn.start();
|
||||
fn.getEventEmitter().on('sync', () => t.pass('file change detected'));
|
||||
fn.watcher.emit('add', 'package.json');
|
||||
fn.watcher.emit('change', 'package.json');
|
||||
fn.watcher.emit('unlink', 'package.json');
|
||||
fn.watcher.emit('add', 'yarn.lock');
|
||||
fn.watcher.emit('change', 'yarn.lock');
|
||||
fn.watcher.emit('unlink', 'yarn.lock');
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
wtf/
|
|
@ -0,0 +1,64 @@
|
|||
'use strict'
|
||||
|
||||
import ignore from '../src/ignore'
|
||||
import { expect } from 'chai'
|
||||
import { it } from 'mocha'
|
||||
// pause = require('@uci/utils').pPause
|
||||
|
||||
describe (
|
||||
'Build Ignore Array',
|
||||
function () {
|
||||
// hooks()
|
||||
buildIgnoreList()
|
||||
// someothertests()
|
||||
})
|
||||
|
||||
//****************** TESTS **********************
|
||||
function buildIgnoreList() {
|
||||
it('==> can create array for multiple files of ignore lines', async function () {
|
||||
const shouldbe = [ 'tests/',
|
||||
'test/',
|
||||
'*.test.js',
|
||||
'testing/',
|
||||
'example/',
|
||||
'/node_modules/',
|
||||
'/coverage/',
|
||||
'foo/**',
|
||||
'bar/*.js' ]
|
||||
let result = await ignore(__dirname+'/repo')
|
||||
//console.log('returned to test',result, shouldbe)
|
||||
expect(result, 'list build failed').to.deep.equal(shouldbe)
|
||||
})
|
||||
it('==> can create array with additional passed in file and relative repo path', async function () {
|
||||
const shouldbe = [ 'tests/',
|
||||
'test/',
|
||||
'*.test.js',
|
||||
'testing/',
|
||||
'example/',
|
||||
'/node_modules/',
|
||||
'/coverage/',
|
||||
'foo/**',
|
||||
'bar/*.js',
|
||||
'bah/*/*.js'
|
||||
]
|
||||
let result = await ignore('./test/repo','.testignore')
|
||||
//console.log('returned to test',result, shouldbe)
|
||||
expect(result, 'list build failed').to.deep.equal(shouldbe)
|
||||
})
|
||||
it('==> can handle file in other than repo path', async function () {
|
||||
const shouldbe = [ 'tests/',
|
||||
'test/',
|
||||
'*.test.js',
|
||||
'testing/',
|
||||
'example/',
|
||||
'/node_modules/',
|
||||
'/coverage/',
|
||||
'foo/**',
|
||||
'bar/*.js',
|
||||
'wtf/'
|
||||
]
|
||||
let result = await ignore(__dirname+'/repo','./test/anotherignorefile')
|
||||
//console.log('returned to test',result, shouldbe)
|
||||
expect(result, 'list build failed').to.deep.equal(shouldbe)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/node_modules/
|
||||
/coverage/
|
|
@ -0,0 +1,5 @@
|
|||
tests/
|
||||
test/
|
||||
*.test.js
|
||||
testing/
|
||||
example/
|
|
@ -0,0 +1,2 @@
|
|||
foo/**
|
||||
bar/*.js
|
|
@ -0,0 +1 @@
|
|||
bah/*/*.js
|
Loading…
Reference in New Issue