ssh split off removed other files, ssh cli operational, fixed one off shell command issues
parent
9caf1c1da4
commit
606978c37e
|
@ -1,2 +1,3 @@
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/coverage/
|
/coverage/
|
||||||
|
*.log
|
||||||
|
|
|
@ -3,3 +3,4 @@ test/
|
||||||
*.test.js
|
*.test.js
|
||||||
testing/
|
testing/
|
||||||
example/
|
example/
|
||||||
|
nodemon.json
|
||||||
|
|
14
bin/ssh.js
14
bin/ssh.js
|
@ -1,27 +1,29 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
// third party imports
|
||||||
import Ssh from '../src/ssh'
|
import Ssh from '../src/ssh'
|
||||||
import to from 'await-to-js'
|
import to from 'await-to-js'
|
||||||
import logger from '@uci/logger'
|
import logger from '@uci-utils/logger'
|
||||||
import yargs from 'yargs'
|
import yargs from 'yargs'
|
||||||
|
|
||||||
|
const log = logger({appName:'sshu', package:'ssh', bin:'./bin/ssh.js', class:'Ssh', id: 'ssh-binary' })
|
||||||
|
|
||||||
console.log(process.argv)
|
log.debug({arguments:process.argv, msg:'arguments passed to ssh cli'})
|
||||||
let arg = yargs
|
let arg = yargs
|
||||||
.alias('c','config')
|
.alias('c','config')
|
||||||
.argv
|
.argv
|
||||||
|
|
||||||
;
|
;
|
||||||
(async () => {
|
(async () => {
|
||||||
const log = logger({ repo: 'remote-code', bin:'./bin/ssh.js', class:'Ssh', file:'src/ssh.js', id: 'binary' })
|
|
||||||
const ssh = new Ssh()
|
const ssh = new Ssh()
|
||||||
console.log('config file', arg)
|
arg.c ? log.debug(`loading ssh config file ${arg.c}`) : log.debug('loading ssh default config file')
|
||||||
await ssh.configure(arg.c ? arg.c : null) // default config
|
await ssh.configure(arg.c) // null = default config
|
||||||
log.info(`making connection to ${ssh.opts.host}`)
|
log.info(`making connection to ${ssh.opts.host}`)
|
||||||
let [err] = await to(ssh.connect())
|
let [err] = await to(ssh.connect())
|
||||||
if (err) {
|
if (err) {
|
||||||
log.info({err:err, msg:'unable to connect'})
|
log.info({err:err, msg:'unable to connect'})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// let cmd = arg._.join(' ')
|
// let cmd = arg._.join(' ')
|
||||||
// console.log(cmd)
|
// console.log(cmd)
|
||||||
// let [err2,res] = await to(ssh.exec(cmd))
|
// let [err2,res] = await to(ssh.exec(cmd))
|
||||||
|
@ -33,7 +35,7 @@ let arg = yargs
|
||||||
// return res
|
// return res
|
||||||
|
|
||||||
let res = await ssh.shell({cli:true})
|
let res = await ssh.shell({cli:true})
|
||||||
console.log('ALL DONE', res.command, res.cmds, )
|
log.info({history:res.cmds, msg:'returned from remote shell'})
|
||||||
|
|
||||||
|
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
DEV_BIN=/mnt/238/nas/david/hacking/active-dev-repos/uci/lib/uci-remote-code/bin
|
#!/bin/bash
|
||||||
UCI_DEV=true /usr/bin/node $DEV_BIN/ssh-cli $@
|
LINK=`readlink -f $0`
|
||||||
|
DEV_BIN=`dirname $LINK`
|
||||||
|
UCI_DEV=true /usr/bin/node $DEV_BIN/ssh-cli "$@"
|
||||||
|
|
26
package.json
26
package.json
|
@ -1,22 +1,22 @@
|
||||||
{
|
{
|
||||||
"name": "@uci/remote-code",
|
"name": "@uci-utils/ssh",
|
||||||
"version": "0.0.2",
|
"version": "0.0.4",
|
||||||
"description": "module to copy, maintain, and launch hardware modules on other machines",
|
"description": "ssh client cli and api, a wrapper on ssh2",
|
||||||
"main": "src/index.js",
|
"main": "src/ssh.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"ssh": "./bin/ssh.js"
|
"ssh": "./bin/ssh.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ssh": "node -r esm ./bin/ssh",
|
"ssh": "node -r esm ./bin/ssh",
|
||||||
"testd": "UCI_DEV=true ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000'",
|
"testd": "UCI_ENV=dev ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha -r esm --timeout 30000'",
|
||||||
"test": "UCI_PRO=./test/test.log ./node_modules/.bin/mocha -r esm --timeout 30000 || exit 0",
|
"test": "UCI_ENV=pro UCI_LOG_PATH=./test/test.log ./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"
|
"testibc": "istanbul cover ./node_modules/.bin/_mocha test/ --report lcovonly -- -R spec --recursive && codecov || true"
|
||||||
},
|
},
|
||||||
"author": "David Kebler",
|
"author": "David Kebler",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/uCOMmandIt/uci-remote-code.git"
|
"url": "git+https://github.com/uCOMmandIt/uci-ssh.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node.js",
|
"node.js",
|
||||||
|
@ -24,24 +24,22 @@
|
||||||
"raspberryPi"
|
"raspberryPi"
|
||||||
],
|
],
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/uCOMmandIt/uci-remote-code/issues"
|
"url": "https://github.com/uCOMmandIt/uci-ssh/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/uCOMmandIt/uci-remote-code#readme",
|
"homepage": "https://github.com/uCOMmandIt/uci-ssh#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uci/logger": "0.0.9",
|
"@uci-utils/logger": "0.0.13",
|
||||||
"await-to-js": "^2.1.1",
|
"await-to-js": "^2.1.1",
|
||||||
"chokidar": "^2.0.4",
|
|
||||||
"conf": "^2.2.0",
|
"conf": "^2.2.0",
|
||||||
|
"esm": "^3.2.4",
|
||||||
"fs-read-data": "^1.0.4",
|
"fs-read-data": "^1.0.4",
|
||||||
"p-settle": "^2.1.0",
|
|
||||||
"ssh2": "^0.8.2",
|
"ssh2": "^0.8.2",
|
||||||
"yargs": "^12.0.5"
|
"yargs": "^13.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"chai-arrays": "^2.0.0",
|
"chai-arrays": "^2.0.0",
|
||||||
"codecov": "^3.1.0",
|
"codecov": "^3.1.0",
|
||||||
"esm": "^3.1.4",
|
|
||||||
"istanbul": "^0.4.5",
|
"istanbul": "^0.4.5",
|
||||||
"mocha": "^5.x",
|
"mocha": "^5.x",
|
||||||
"nodemon": "^1.18.9"
|
"nodemon": "^1.18.9"
|
||||||
|
|
17
readme.md
17
readme.md
|
@ -1,4 +1,6 @@
|
||||||
# uCOMmandIt Template Package Repository
|
# uCOMmandIt SSH Client
|
||||||
|
|
||||||
|
## Secure Shell (ssh) API and CLI client based on ssh2
|
||||||
|
|
||||||
<!-- find and replace the package name to match -->
|
<!-- 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)
|
[![Build Status](https://img.shields.io/travis/uCOMmandIt/uci-pkg-template.svg?branch=master)](https://travis-ci.org/uCOMmandIt/uci-pkg-template)
|
||||||
|
@ -6,16 +8,3 @@
|
||||||
[![Dependencies](https://img.shields.io/david/uCOMmandIt/uci-pkg-template.svg)](https://david-dm.org/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)
|
[![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)
|
[![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
|
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
import RemoteCode from './remote-code'
|
|
||||||
import Ssh from './ssh'
|
import Ssh from './ssh'
|
||||||
import Sync from './sync'
|
export { Ssh }
|
||||||
import Watcher from './watcher'
|
export default Ssh
|
||||||
import ignore from './ignore'
|
|
||||||
|
|
||||||
export { RemoteCode, Ssh, Sync, Watcher, ignore }
|
|
||||||
export default RemoteCode
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
|
|
||||||
import { promisify } from 'util'
|
|
||||||
import { readFile, writeFile } from 'fs'
|
|
||||||
const read = promisify(readFile)
|
|
||||||
const write = promisify(writeFile)
|
|
||||||
// alternative with new promise experimental fs
|
|
||||||
// import fs from 'fs'
|
|
||||||
// const read = fs.promises.readFile
|
|
||||||
import settle from 'p-settle'
|
|
||||||
import path from 'path'
|
|
||||||
import logger from '@uci/logger'
|
|
||||||
let log = logger({ name: 'remote-code', file:'src/read-lines.js'})
|
|
||||||
|
|
||||||
// A promise helper function to return a list of paths to ignore from .npmignore, .gitignore, .rcignore
|
|
||||||
function readLines (files=[],dir) {
|
|
||||||
// console.log('additional files', files)
|
|
||||||
let list = []
|
|
||||||
if (!Array.isArray(files)) files=[files]
|
|
||||||
|
|
||||||
// each set in an the array is new line delimited set of ignore patterns
|
|
||||||
// settle returns array of error,value pairs
|
|
||||||
return settle(files.map(file => {
|
|
||||||
// console.log('directory',path.dirname(file))
|
|
||||||
if (path.dirname(file)==='.') file = dir+'/'+file
|
|
||||||
log.info({function:'readLines',file:file,msg:'reading a file of lines into array'})
|
|
||||||
return read(file)
|
|
||||||
}))
|
|
||||||
.then((sets) => {
|
|
||||||
sets.forEach( set => {
|
|
||||||
if (set.isFulfilled) list.push(...set.value.toString().match(/.+/g))
|
|
||||||
else log.warn({function:'readLines', error:set.reason, msg:' was unable to read file'})
|
|
||||||
})
|
|
||||||
return Promise.resolve(list)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// only returned when something horrible is wrong
|
|
||||||
return Promise.reject(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// an ignore list should not be huge so can serailize at once
|
|
||||||
function writeLines (filePath,list) {
|
|
||||||
|
|
||||||
return write(filePath,list.join('\n'))
|
|
||||||
.then(() => {
|
|
||||||
log.info({function:'writeLines', file:filePath, msg:'wrote array to file of lines'})
|
|
||||||
})
|
|
||||||
.catch( err => {
|
|
||||||
log.fatal({function:'writeLines', error:err, msg:'unable to write array to file of lines'})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default readLines
|
|
||||||
export { readLines, writeLines }
|
|
|
@ -1,123 +0,0 @@
|
||||||
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
|
|
43
src/ssh.js
43
src/ssh.js
|
@ -1,12 +1,14 @@
|
||||||
|
// native imports
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import readline from 'readline'
|
import readline from 'readline'
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
import to from 'await-to-js'
|
|
||||||
import { readFile } from 'fs'
|
import { readFile } from 'fs'
|
||||||
const read = promisify(readFile)
|
const read = promisify(readFile)
|
||||||
|
// third party imports
|
||||||
|
import to from 'await-to-js'
|
||||||
import { readFile as readConfig } from 'fs-read-data'
|
import { readFile as readConfig } from 'fs-read-data'
|
||||||
import { Client } from 'ssh2'
|
import { Client } from 'ssh2'
|
||||||
import logger from '@uci/logger'
|
import logger from '@uci-utils/logger'
|
||||||
import Conf from 'conf'
|
import Conf from 'conf'
|
||||||
let log = {} // declare module wide log to be set during construction
|
let log = {} // declare module wide log to be set during construction
|
||||||
|
|
||||||
|
@ -15,11 +17,11 @@ class Ssh extends Client {
|
||||||
constructor(opts={}) {
|
constructor(opts={}) {
|
||||||
super()
|
super()
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
log = logger({ name: 'remote-code', file:'src/ssh.js', class:'Ssh', id: opts.id })
|
log = logger({id: opts.id })
|
||||||
this.ready = false
|
this.ready = false
|
||||||
this.config = new Conf({projectName:'ssh-uci'})
|
this.config = new Conf({projectName:'ssh-uci'})
|
||||||
this.configsDir = process.env.SSH_CONFIG_DIR || this.config.get('configsDir') || dirname(this.config.path)
|
this.configsDir = process.env.SSH_CONFIG_DIR || this.config.get('configsDir') || dirname(this.config.path)
|
||||||
console.log(this.configsDir)
|
log.debug({configsDir:this.configsDir, msg:'configuration files directory'})
|
||||||
this.rpath = opts.rpath || '/opt' // can be used to make all commands run in a particular remote path
|
this.rpath = opts.rpath || '/opt' // can be used to make all commands run in a particular remote path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,16 +37,15 @@ class Ssh extends Client {
|
||||||
opts = await readConfig(configPath)
|
opts = await readConfig(configPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('configPath',configPath)
|
log.debug({configPath:this.configPath, msg:'path to actual configuration file used'})
|
||||||
console.log('opts',opts)
|
log.debug({opts:opts, msg:'connection options as read in from file'})
|
||||||
this.opts = overwrite ? Object.assign(this.opts,opts) : Object.assign(opts, this.opts)
|
this.opts = overwrite ? Object.assign(this.opts,opts) : Object.assign(opts, this.opts)
|
||||||
|
log.debug({opts:opts, msg:'connection options as ammended from configure() argument'})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(copts = {}) {
|
async connect(copts = {}) {
|
||||||
const opts = Object.assign({},this.opts,copts) // merge any changes pasted via connect
|
const opts = Object.assign({},this.opts,copts) // merge any changes pasted via connect
|
||||||
// log.info('connection options ', opts)
|
log.debug({opts:opts, msg:'final connection options as ammend via connect() argument and as used'})
|
||||||
if (opts.agentenv) opts.agent = process.env[opts.agentenv]
|
if (opts.agentenv) opts.agent = process.env[opts.agentenv]
|
||||||
opts.readyTimeout = opts.readyTimeout | 5000 // default was 20 seconds...too long
|
opts.readyTimeout = opts.readyTimeout | 5000 // default was 20 seconds...too long
|
||||||
opts.privateKey = opts.privateKey || opts.privateKeyPath ? await read(opts.privateKeyPath) : null
|
opts.privateKey = opts.privateKey || opts.privateKeyPath ? await read(opts.privateKeyPath) : null
|
||||||
|
@ -57,7 +58,7 @@ class Ssh extends Client {
|
||||||
resolve(`connected to ${this.opts.host}`)
|
resolve(`connected to ${this.opts.host}`)
|
||||||
})
|
})
|
||||||
this.on('error', (err) => {
|
this.on('error', (err) => {
|
||||||
log.info({err:err, opts:this.opts, msg:'connection error'})
|
log.warn({err:err, opts:this.opts, msg:'connection error'})
|
||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -90,21 +91,20 @@ class Ssh extends Client {
|
||||||
let [err] = await to(this.connect())
|
let [err] = await to(this.connect())
|
||||||
if (err) return err
|
if (err) return err
|
||||||
}
|
}
|
||||||
// log.info('executing command ', command)
|
const remote = this // TODO bind this on promise instead
|
||||||
const remote = this
|
|
||||||
const superexec = opts.shell? super.shell.bind(this) : super.exec.bind(this)
|
const superexec = opts.shell? super.shell.bind(this) : super.exec.bind(this)
|
||||||
let command = opts.shell ? opts : cmd
|
let command = opts.shell ? opts : cmd
|
||||||
|
// log.info(`executing command ${command} on shell? ${opts.shell}`)
|
||||||
return new Promise(function(resolve, reject) {
|
return new Promise(function(resolve, reject) {
|
||||||
let reply
|
let reply =''
|
||||||
let error
|
let error = ''
|
||||||
superexec(command, function(err, stream) {
|
superexec(command, function(err, stream) {
|
||||||
console.log('command,cmd,opts',command,cmd,opts)
|
log.info({command:command, cmd:cmd, opts:opts, msg:'before ssh exec/shell'})
|
||||||
const done = () => {
|
const done = () => {
|
||||||
if (error) remote.emit('remote:error',error)
|
if (error) remote.emit('remote:error',error)
|
||||||
resolve({command:command,cmds:cmd,reply:reply,error:error})
|
resolve({command:command,cmds:cmd,reply:reply,error:error})
|
||||||
}
|
}
|
||||||
if (err) reject(err)
|
if (err) reject(err)
|
||||||
// listerners
|
|
||||||
stream.once('close', () => {
|
stream.once('close', () => {
|
||||||
if (remote.opts.close || opts.close || opts.cli) {
|
if (remote.opts.close || opts.close || opts.cli) {
|
||||||
remote.on('end', done)
|
remote.on('end', done)
|
||||||
|
@ -125,21 +125,22 @@ class Ssh extends Client {
|
||||||
const cli= readline.createInterface({input:process.stdin, output:process.stdout})
|
const cli= readline.createInterface({input:process.stdin, output:process.stdout})
|
||||||
cli.on('line', line => {
|
cli.on('line', line => {
|
||||||
cmd.push(line)
|
cmd.push(line)
|
||||||
console.log('storedcmds=>', cmd)
|
|
||||||
if(line.trim() === 'exit') {
|
if(line.trim() === 'exit') {
|
||||||
console.log('ending stream')
|
|
||||||
cli.removeAllListeners()
|
cli.removeAllListeners()
|
||||||
cli.close()
|
cli.close()
|
||||||
stream.end(`${line}\n`)
|
stream.end(`${line}\n`)
|
||||||
} else {
|
} else {
|
||||||
console.log('not exit')
|
|
||||||
stream.write(`${line}\n`)
|
stream.write(`${line}\n`)
|
||||||
}
|
}
|
||||||
}) // end terminal listener
|
}) // end terminal listener
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
else {
|
||||||
stream.end(cmd) // one off shell command
|
// give a bit of time for MOTD to display so it's not part of reply
|
||||||
|
setTimeout(() => {
|
||||||
|
reply=''
|
||||||
|
stream.end(`${cmd}\nexit\n`)
|
||||||
|
}, opts.delay || 300)
|
||||||
|
}
|
||||||
} // end shell processing
|
} // end shell processing
|
||||||
}) // end super
|
}) // end super
|
||||||
}) // end Promise
|
}) // end Promise
|
||||||
|
|
113
src/sync.js
113
src/sync.js
|
@ -1,113 +0,0 @@
|
||||||
import { EventEmitter as Emitter } from 'events'
|
|
||||||
import path from 'path'
|
|
||||||
// import promisify from 'util'
|
|
||||||
import { readFile as readConfig } from 'fs-read-data'
|
|
||||||
import { exec as child } from 'child_process'
|
|
||||||
// import ignoreFiles from './ignore'
|
|
||||||
import logger from '@uci/logger'
|
|
||||||
let log = {} // declare module wide log to be set during construction
|
|
||||||
|
|
||||||
class Sync extends Emitter {
|
|
||||||
constructor(opts = {}) {
|
|
||||||
super()
|
|
||||||
log = logger({ name: 'remote-code', file:'src/sync.js', class:'Sync'})
|
|
||||||
this.opts = opts
|
|
||||||
// this.opts.more = this.opts.more || []
|
|
||||||
// this.opts.ssh = this.opts.ssh || []
|
|
||||||
this.jobOpts = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
async configure(opts,overwrite) {
|
|
||||||
if (typeof opts === 'string') opts = await readConfig(opts) // opts here is path to configuration file
|
|
||||||
this.opts = overwrite ? Object.assign(this.opts,opts) : Object.assign(opts, this.opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
addOpt(option,value) {
|
|
||||||
this.opts.more = this.opts.more || []
|
|
||||||
this.opts.more.push(`${(option.length > 1)?'--':'-'}${option} ${value}`)
|
|
||||||
log.info (this.opts.more)
|
|
||||||
}
|
|
||||||
|
|
||||||
addSshOpt(option,value) {
|
|
||||||
this.opts.ssh = this.opts.ssh || []
|
|
||||||
this.opts.ssh.push(`${(option.length > 1)?'--':'-'}${option} ${value}`)
|
|
||||||
log.info (this.opts.ssh)
|
|
||||||
}
|
|
||||||
|
|
||||||
command(src, dest) {
|
|
||||||
this.opts.src = src || this.opts.src || process.cwd()
|
|
||||||
this.opts.dest = dest || this.opts.dest || `/opt/${process.cwd().match(/([^/]*)\/*$/)[1]}`
|
|
||||||
this.cmd=`rsync ${this._opts} ${this._ssh()} ${this.opts.src}/ ${this.opts.dest}`
|
|
||||||
// this.cmd = JSON.stringify(this.opts)
|
|
||||||
return this.cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
_ssh() {
|
|
||||||
if (!(this.opts.username && this.opts.host)) return ''
|
|
||||||
this.opts.dest = `${this.opts.username}@${this.opts.host}:${this.opts.dest}`
|
|
||||||
if (this.opts.keyFilePath) this.addSshOpt('i',this.opts.keyFilePath)
|
|
||||||
if (this.opts.port) this.addSshOpt('p',this.opts.port)
|
|
||||||
if (!this.opts.ssh) return ''
|
|
||||||
return `-e "ssh ${this.opts.ssh.join(' ')}"`
|
|
||||||
}
|
|
||||||
|
|
||||||
_opts() {
|
|
||||||
if (typeof opts.mirror==='boolean' && !opts.mirror)
|
|
||||||
|
|
||||||
|
|
||||||
// if ssh agent is not available in child process can make it so
|
|
||||||
_agent(path) {
|
|
||||||
// this.jobOpts.env
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns a promise when job is done
|
|
||||||
execute(src,dest) {
|
|
||||||
const sync = this
|
|
||||||
let reply=[]
|
|
||||||
let error=[]
|
|
||||||
sync.emit('busy')
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// may need to be sure that child has agent socket envirnoment variable
|
|
||||||
if (this.opts.agent) this._agent(this.opts.agent)
|
|
||||||
const job = child(this.command(src,dest), this.jobOpts)
|
|
||||||
|
|
||||||
job.on('error', (err) => {
|
|
||||||
log.fatal({err:err, opts:sync.opts, msg:'failed to spawn rsync job'})
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
job.on('close', (code) => {
|
|
||||||
log.info(`sync job exited with code ${code}`)
|
|
||||||
if (error.length!==0) sync.emit('remote:error',error)
|
|
||||||
sync.emit('done',{command:sync.cmd, reply:reply})
|
|
||||||
resolve({command:sync.cmd,reply:reply,error:error})
|
|
||||||
})
|
|
||||||
|
|
||||||
job.stdout.on('data', (data) => {
|
|
||||||
log.info({remote:sync.opts.host, console:data, msg:'console data from remote'})
|
|
||||||
reply.push(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
job.stderr.on('data', (data) => {
|
|
||||||
log.warn({remote:sync.opts.host, error:data, msg:'error from remote'})
|
|
||||||
error.push(data)
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}// end Class Sync
|
|
||||||
|
|
||||||
export default Sync
|
|
||||||
|
|
||||||
function isPlainObject (obj) {
|
|
||||||
return Object.prototype.toString.call(obj) === '[object Object]'
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeSpaces (str) {
|
|
||||||
if (typeof str === 'string') {
|
|
||||||
return str.replace(/\b\s/g, '\\ ')
|
|
||||||
} else {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
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
|
|
|
@ -1,6 +1,6 @@
|
||||||
host: switches.kebler.net
|
host: switches.kebler.net
|
||||||
username: sysadmin
|
username: sysadmin
|
||||||
# agent: /run/user/1000/ssh-agent.socket
|
#agent: /run/user/1000/ssh-agent.socket
|
||||||
# agentenv: SSH_AUTH_SOCK
|
#agentenv: SSH_AUTH_SOCK
|
||||||
#privateKeyPath: /home/david/.ssh/privatekeys/sysadmin.kebler.net
|
privateKeyPath: /home/david/.ssh/privatekeys/sysadmin.kebler.net
|
||||||
#passphrase: '51535560'
|
passphrase: '51535560'
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
host: giskard.kebler.net
|
|
||||||
#port: 33
|
|
||||||
username: sysadmin
|
|
||||||
keyFilePath: '~/.ssh/keyfile'
|
|
||||||
src: ./test/repo
|
|
||||||
dest: /opt/repo
|
|
|
@ -1,54 +0,0 @@
|
||||||
// let ignoreFiles = ['.npmignore','.gitignore']
|
|
||||||
|
|
||||||
import { readLines, writeLines } from '../src/read-lines'
|
|
||||||
import chai from 'chai'
|
|
||||||
import assertArrays from 'chai-arrays'
|
|
||||||
import { it } from 'mocha'
|
|
||||||
|
|
||||||
chai.use(assertArrays)
|
|
||||||
const expect = chai.expect
|
|
||||||
|
|
||||||
describe (
|
|
||||||
'Read a File of Lines to Array and vice versa',
|
|
||||||
function () {
|
|
||||||
readList()
|
|
||||||
writeList()
|
|
||||||
})
|
|
||||||
|
|
||||||
//****************** TESTS **********************
|
|
||||||
function readList() {
|
|
||||||
it('==> can read one or more files (no order) each line as element in an array with common directory', async function () {
|
|
||||||
const shouldbe = [ 'tests/',
|
|
||||||
'test/',
|
|
||||||
'*.test.js',
|
|
||||||
'testing/',
|
|
||||||
'example/',
|
|
||||||
'/node_modules/',
|
|
||||||
'/coverage/' ]
|
|
||||||
let result = await readLines(['.gitignore','.npmignore'],__dirname+'/repo')
|
|
||||||
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('==> can read two files one relative the other absolute', async function () {
|
|
||||||
const shouldbe = [ 'tests/',
|
|
||||||
'test/',
|
|
||||||
'*.test.js',
|
|
||||||
'testing/',
|
|
||||||
'example/',
|
|
||||||
'/node_modules/',
|
|
||||||
'/coverage/' ]
|
|
||||||
let result = await readLines(['./test/repo/.gitignore',__dirname+'/repo/.npmignore'])
|
|
||||||
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function writeList() {
|
|
||||||
|
|
||||||
it('==> can write an array items as lines in a file', async function () {
|
|
||||||
const shouldbe = await readLines(['.gitignore','.npmignore'],__dirname+'/repo')
|
|
||||||
await writeLines(__dirname+'/repo/combined.list',shouldbe)
|
|
||||||
const result = await readLines(['combined.list'],__dirname+'/repo')
|
|
||||||
expect(result, 'list build failed').to.be.containingAllOf(shouldbe)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
/node_modules/
|
|
||||||
/coverage/
|
|
|
@ -1,5 +0,0 @@
|
||||||
tests/
|
|
||||||
test/
|
|
||||||
*.test.js
|
|
||||||
testing/
|
|
||||||
example/
|
|
|
@ -1,2 +0,0 @@
|
||||||
foo/**
|
|
||||||
bar/*.js
|
|
|
@ -1 +0,0 @@
|
||||||
bah/*/*.js
|
|
|
@ -1,7 +0,0 @@
|
||||||
/node_modules/
|
|
||||||
/coverage/
|
|
||||||
tests/
|
|
||||||
test/
|
|
||||||
*.test.js
|
|
||||||
testing/
|
|
||||||
example/
|
|
|
@ -16,7 +16,7 @@ describe('SSH Class Testing ',async ()=> {
|
||||||
let [err] = await to(remote.connect())
|
let [err] = await to(remote.connect())
|
||||||
if (err) {
|
if (err) {
|
||||||
log.info('unable to connect aborting test', err)
|
log.info('unable to connect aborting test', err)
|
||||||
return err
|
throw new Error (err)
|
||||||
}
|
}
|
||||||
log.info('ready for testing')
|
log.info('ready for testing')
|
||||||
})
|
})
|
||||||
|
@ -25,7 +25,7 @@ describe('SSH Class Testing ',async ()=> {
|
||||||
remote.close()
|
remote.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('simple connect and reply to "cd /opt && pwd"' , async function () {
|
it('simple connect and execute "cd /opt && pwd" on remote' , async function () {
|
||||||
let [err,res] = await to(remote.exec('cd /opt && pwd'))
|
let [err,res] = await to(remote.exec('cd /opt && pwd'))
|
||||||
if (err) {
|
if (err) {
|
||||||
log.info('error running command aborting test', err)
|
log.info('error running command aborting test', err)
|
||||||
|
@ -35,6 +35,18 @@ describe('SSH Class Testing ',async ()=> {
|
||||||
expect(res.reply.toString().trim(), 'test failed').to.equal('/opt')
|
expect(res.reply.toString().trim(), 'test failed').to.equal('/opt')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('connect via an "interactive shell" and cat the test.txt file in home directory"' , async function () {
|
||||||
|
let [err,res] = await to(remote.shell('cat test.txt')) // cd /opt && pwd
|
||||||
|
if (err) {
|
||||||
|
log.info('error running shell command aborting test', err)
|
||||||
|
throw new Error(err)
|
||||||
|
}
|
||||||
|
let passed = false
|
||||||
|
if (res.reply.indexOf('47fciogjei439djfjla') > 0) passed=true
|
||||||
|
log.info({remoteHost:remote.opts.host, reply:res.reply.toString().trim(), err:err, cmd:res.command, msg:`remote command completed ${res.command}`})
|
||||||
|
expect(passed, 'test failed').to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// function hooks(remote) {
|
// function hooks(remote) {
|
|
@ -1,48 +0,0 @@
|
||||||
import Sync from '../src/sync'
|
|
||||||
import to from 'await-to-js'
|
|
||||||
import { expect } from 'chai'
|
|
||||||
import { it } from 'mocha'
|
|
||||||
import logger from '@uci/logger'
|
|
||||||
// pause = require('@uci/utils').pPause
|
|
||||||
|
|
||||||
describe('Sync Class Testing ',async ()=> {
|
|
||||||
|
|
||||||
let sync
|
|
||||||
let log
|
|
||||||
before(async () => {
|
|
||||||
log = logger({ name: 'remote-code', test:'/test/sync.test.js', class:'sync',file:'src/sync.js', id: 'testing' })
|
|
||||||
sync = new Sync()
|
|
||||||
await sync.configure('./test/repo/config')
|
|
||||||
log.info({cmd:sync.command(), msg:'Rsync Command that will Run'})
|
|
||||||
// log.info(`making connection to ${remote.opts.host}`)
|
|
||||||
// log.info('ready for testing')
|
|
||||||
})
|
|
||||||
|
|
||||||
// after(async () => {
|
|
||||||
// remote.close()
|
|
||||||
// })
|
|
||||||
|
|
||||||
it('can sync a directory' , async function () {
|
|
||||||
// let [err,res] = await to(remote.exec('cd /opt && pwd'))
|
|
||||||
// if (err) {
|
|
||||||
// log.info('error running command aborting test', err)
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// log.info(`result of remote command ${res.command} => ${res.reply.toString().trim()}`)
|
|
||||||
expect('test', 'test failed').to.equal('/opt')
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
// function hooks(remote) {
|
|
||||||
//
|
|
||||||
|
|
||||||
// // beforeEach(async() => {
|
|
||||||
// // await someasyncfunctiontodobeforeeachtest()
|
|
||||||
// // })
|
|
||||||
//
|
|
||||||
// // after(async() => {
|
|
||||||
// // await someasyncfunctiontodoaftereeachtest()
|
|
||||||
// // })
|
|
||||||
//
|
|
||||||
// }
|
|
|
@ -1,49 +0,0 @@
|
||||||
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');
|
|
||||||
});
|
|
Loading…
Reference in New Issue