diff --git a/server/app/db.js b/server/app/db.js index 71f1797..328eacb 100644 --- a/server/app/db.js +++ b/server/app/db.js @@ -151,8 +151,8 @@ function initScriptsDB() { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { let scriptList = await readScriptList() - let clientInstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh | bash' - let clientUninstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh | bash' + let clientInstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-install.sh && sh easynode-client-install.sh' + let clientUninstallScript = 'wget https://mirror.ghproxy.com/https://raw.githubusercontent.com/chaos-zhu/easynode/main/client/easynode-client-uninstall.sh && sh easynode-client-uninstall.sh' let clientVersion = process.env.CLIENT_VERSION consola.info('客户端版本:', clientVersion) let installId = `clientInstall${ clientVersion }` @@ -162,12 +162,18 @@ function initScriptsDB() { let isClientUninstall = scriptList?.find(script => script._id = uninstallId) let writeFlag = false if (!isClientInstall) { - scriptList.push({ _id: installId, name: `easynode-client-${ clientVersion }安装脚本`, remark: '系统内置|重启生成', content: clientInstallScript, index: 99 }) + console.info('初始化客户端安装脚本') + scriptList.push({ _id: installId, name: `easynode-客户端-${ clientVersion }安装脚本`, remark: '系统内置|重启生成', content: clientInstallScript, index: 1 }) writeFlag = true + } else { + console.info('客户端安装脚本已存在') } if (!isClientUninstall) { - scriptList.push({ _id: uninstallId, name: `easynode-client-${ clientVersion }卸载脚本`, remark: '系统内置|重启生成', content: clientUninstallScript, index: 98 }) + console.info('初始化客户端卸载脚本') + scriptList.push({ _id: uninstallId, name: `easynode-客户端-${ clientVersion }卸载脚本`, remark: '系统内置|重启生成', content: clientUninstallScript, index: 0 }) writeFlag = true + } else { + console.info('客户端卸载脚本已存在') } if (writeFlag) await writeScriptList(scriptList) resolve() diff --git a/server/app/socket/onekey.js b/server/app/socket/onekey.js index 54154bf..eee3c7e 100644 --- a/server/app/socket/onekey.js +++ b/server/app/socket/onekey.js @@ -1,6 +1,6 @@ const { Server } = require('socket.io') const { Client: SSHClient } = require('ssh2') -const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync, writeOneKeyRecord, throttle } = require('../utils') +const { readHostList, readSSHRecord, verifyAuthSync, AESDecryptSync, writeOneKeyRecord, shellThrottle } = require('../utils') const execStatusEnum = { connecting: '连接中', @@ -27,10 +27,17 @@ function disconnectAllExecClient() { } function execShell(socket, sshClient, curRes, resolve) { - const throttledDataHandler = throttle((data) => { - curRes.status = execStatusEnum.executing - curRes.result += data?.toString() || '' + const throttledDataHandler = shellThrottle(() => { socket.emit('output', execResult) + // const memoryUsage = process.memoryUsage() + // const formattedMemoryUsage = { + // rss: (memoryUsage.rss / 1024 / 1024).toFixed(2) + ' MB', // Resident Set Size: total memory allocated for the process execution + // heapTotal: (memoryUsage.heapTotal / 1024 / 1024).toFixed(2) + ' MB', // Total size of the allocated heap + // heapUsed: (memoryUsage.heapUsed / 1024 / 1024).toFixed(2) + ' MB', // Actual memory used during the execution + // external: (memoryUsage.external / 1024 / 1024).toFixed(2) + ' MB', // Memory used by "external" components like V8 external memory + // arrayBuffers: (memoryUsage.arrayBuffers / 1024 / 1024).toFixed(2) + ' MB' // Memory allocated for ArrayBuffer and SharedArrayBuffer, including all Node.js Buffers + // } + // console.log(formattedMemoryUsage) }, 500) // 防止内存爆破 sshClient.exec(curRes.command, function(err, stream) { if (err) { @@ -41,8 +48,9 @@ function execShell(socket, sshClient, curRes, resolve) { return } stream - .on('close', () => { - throttledDataHandler.flush() + .on('close', async () => { + // ssh连接关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失 + await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出 // console.log('onekey终端执行完成, 关闭连接: ', curRes.host) if (curRes.status === execStatusEnum.executing) { curRes.status = execStatusEnum.execSuccess @@ -53,16 +61,16 @@ function execShell(socket, sshClient, curRes, resolve) { }) .on('data', (data) => { // console.log(curRes.host, '执行中: \n' + data) - // curRes.status = execStatusEnum.executing - // curRes.result += data.toString() + curRes.status = execStatusEnum.executing + curRes.result += data.toString() // socket.emit('output', execResult) throttledDataHandler(data) }) .stderr .on('data', (data) => { // console.log(curRes.host, '命令执行过程中产生错误: ' + data) - // curRes.status = execStatusEnum.executing - // curRes.result += data.toString() + curRes.status = execStatusEnum.executing + curRes.result += data.toString() // socket.emit('output', execResult) throttledDataHandler(data) }) @@ -147,6 +155,7 @@ module.exports = (httpServer) => { consola.error('onekey终端连接失败:', err.level) curRes.status = execStatusEnum.connectFail curRes.result += err.message + socket.emit('output', execResult) resolve(curRes) }) .connect({ @@ -157,6 +166,7 @@ module.exports = (httpServer) => { consola.error('创建终端错误:', err.message) curRes.status = execStatusEnum.connectFail curRes.result += err.message + socket.emit('output', execResult) resolve(curRes) } }) diff --git a/server/app/utils/index.js b/server/app/utils/index.js index 815abfe..c634634 100644 --- a/server/app/utils/index.js +++ b/server/app/utils/index.js @@ -21,7 +21,7 @@ const { } = require('./storage') const { RSADecryptSync, AESEncryptSync, AESDecryptSync, SHA1Encrypt } = require('./encrypt') const { verifyAuthSync, isProd } = require('./verify-auth') -const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp, throttle } = require('./tools') +const { getNetIPInfo, throwError, isIP, randomStr, getUTCDate, formatTimestamp, shellThrottle } = require('./tools') const { emailTransporter, sendEmailToConfList } = require('./email') module.exports = { @@ -31,7 +31,7 @@ module.exports = { randomStr, getUTCDate, formatTimestamp, - throttle, + shellThrottle, verifyAuthSync, isProd, RSADecryptSync, diff --git a/server/app/utils/tools.js b/server/app/utils/tools.js index 45d95c4..ad60dae 100644 --- a/server/app/utils/tools.js +++ b/server/app/utils/tools.js @@ -204,40 +204,25 @@ function resolvePath(dir, path) { return path.resolve(dir, path) } -function throttle(func, limit) { - let lastFunc - let lastRan - let pendingArgs = null - - const runner = () => { - func.apply(this, pendingArgs) - lastRan = Date.now() - pendingArgs = null - } - - const throttled = function() { - const context = this - const args = arguments - pendingArgs = args - if (!lastRan || (Date.now() - lastRan >= limit)) { - if (lastFunc) { - clearTimeout(lastFunc) - } - runner.apply(context, args) - } else { - clearTimeout(lastFunc) - lastFunc = setTimeout(() => { - runner.apply(context, args) - }, limit - (Date.now() - lastRan)) +let shellThrottle = (fn, delay = 1000) => { + let timer = null + let args = null + function throttled() { + args = arguments + if (!timer) { + timer = setTimeout(() => { + fn(...args) + timer = null + }, delay) } } - - throttled.flush = () => { - if (pendingArgs) { - runner.apply(this, pendingArgs) - } + function delayMs() { + return new Promise(resolve => setTimeout(resolve, delay)) + } + throttled.last = async () => { + await delayMs() + fn(...args) } - return throttled } @@ -249,5 +234,5 @@ module.exports = { getUTCDate, formatTimestamp, resolvePath, - throttle + shellThrottle } \ No newline at end of file diff --git a/server/package.json b/server/package.json index eae4d89..9b4994e 100644 --- a/server/package.json +++ b/server/package.json @@ -4,8 +4,8 @@ "description": "easynode-server", "bin": "./bin/www", "scripts": { - "local": "cross-env EXEC_ENV=local nodemon ./app/index.js --max-old-space-size=4096", - "prod": "cross-env EXEC_ENV=production nodemon ./app/index.js", + "local": "cross-env EXEC_ENV=local nodemon index.js", + "prod": "cross-env EXEC_ENV=production nodemon index.js", "start": "node ./index.js", "lint": "eslint . --ext .js,.vue", "lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix" diff --git a/web/src/store/index.js b/web/src/store/index.js index 6574b33..5563ff8 100644 --- a/web/src/store/index.js +++ b/web/src/store/index.js @@ -61,35 +61,31 @@ const useStore = defineStore({ // console.log('scriptList:', scriptList) this.$patch({ scriptList }) }, - getHostPing() { - setTimeout(() => { - this.hostList.forEach((item) => { - const { host } = item - ping(`http://${ host }:${ this.$clientPort }`) - .then((res) => { - item.ping = res - }) - }) - // console.clear() - // console.warn('Please tick \'Preserve Log\'') - }, 1500) - }, + // getHostPing() { + // setInterval(() => { + // this.hostList.forEach((item) => { + // const { host } = item + // ping(`http://${ host }:${ this.$clientPort }`) + // .then((res) => { + // item.ping = res + // }) + // }) + // }, 2000) + // }, async wsHostStatus() { if (this.HostStatusSocket) this.HostStatusSocket.close() let socketInstance = io(this.serviceURI, { path: '/clients', forceNew: true, reconnectionDelay: 5000, - reconnectionAttempts: 2 + reconnectionAttempts: 3 }) this.HostStatusSocket = socketInstance socketInstance.on('connect', () => { - let flag = 5 console.log('clients websocket 已连接: ', socketInstance.id) let token = this.token socketInstance.emit('init_clients_data', { token }) socketInstance.on('clients_data', (data) => { - if ((flag++ % 5) === 0) this.getHostPing() this.hostList.forEach(item => { const { host } = item if (data[host] === null) return { ...item } diff --git a/web/src/views/credentials/index.vue b/web/src/views/credentials/index.vue index 101b93f..77feb14 100644 --- a/web/src/views/credentials/index.vue +++ b/web/src/views/credentials/index.vue @@ -36,15 +36,15 @@ > - 密钥 - 密码 + 密钥 + 密码 @@ -58,7 +58,7 @@ @change="handleSelectPrivateKeyFile" >
- - 批量下发指令 + + {{ isExecuting ? `执行中,剩余${timeRemaining}秒` : '批量下发指令' }} - + 删除全部记录
@@ -28,7 +38,7 @@ {{ row.host }} - + @@ -125,7 +135,7 @@ :rows="5" clearable autocomplete="off" - placeholder="shell script" + placeholder="shell script, ex: ping -c 10 google.com" />
@@ -166,17 +176,21 @@ let penddingRecord = ref([]) let checkAll = ref(false) let indeterminate = ref(false) const updateFormRef = ref(null) +let timeRemaining = ref(0) +const isClient = ref(false) let formData = reactive({ hosts: [], - command: 'ping -c 10 google.com', - timeout: 60 + command: '', + timeout: 120 }) const token = computed(() => $store.token) const hostList = computed(() => $store.hostList) let scriptList = computed(() => $store.scriptList) +let isExecuting = computed(() => timeRemaining.value > 0) const hasConfigHostList = computed(() => hostList.value.filter(item => item.isConfig)) + const tableData = computed(() => { return penddingRecord.value.concat(recordList.value).map(item => { item.loading = false @@ -210,12 +224,17 @@ watch(() => formData.hosts, (val) => { const createExecShell = (hosts = [], command = 'ls', timeout = 60) => { loading.value = true + timeRemaining.value = Number(formData.timeout) + let timer = null socket.value = io($serviceURI, { path: '/onekey', forceNew: false, reconnectionAttempts: 1 }) socket.value.on('connect', () => { + timer = setInterval(() => { + timeRemaining.value -= 1 + }, 1000) console.log('onekey socket已连接:', socket.value.id) socket.value.on('ready', () => { @@ -274,6 +293,10 @@ const createExecShell = (hosts = [], command = 'ls', timeout = 60) => { socket.value.on('disconnect', () => { loading.value = false + timeRemaining.value = 0 + if (isClient.value) $store.getHostList() // 如果是客户端安装/卸载脚本,更新下host + isClient.value = false + clearInterval(timer) console.warn('onekey websocket 连接断开') }) @@ -302,6 +325,7 @@ let selectAllHost = (val) => { } let handleImportScript = (scriptObj) => { + isClient.value = scriptObj.id.startsWith('client') formData.command = scriptObj.content } diff --git a/web/src/views/scripts/index.vue b/web/src/views/scripts/index.vue index f18c372..9703fcb 100644 --- a/web/src/views/scripts/index.vue +++ b/web/src/views/scripts/index.vue @@ -34,7 +34,7 @@ > - 密钥 - 密码 - 凭据 + 密钥 + 密码 + 凭据 {{ ipInfo.country || '--' }} {{ ipInfo.regionName }} {{ ipInfo.city }} -->
{{ ipInfo.country || '--' }} {{ ipInfo.regionName }}
- + - + -->