electron实现软件(热)更新(附带示例源码)

热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用,所以我们需要在本地将app.asar文件反编译,编译出一个app文件夹,里面有项目所需的所有源码文件,这时通过vue或react打包的渲染进程代码是经过压缩的,但是主进程代码会直接暴露,所以刚好,我们可以将主进程代码做压缩混淆,然后生成一个app.zip压缩包,上传至服务器,然后下载这个压缩包,解压,将其编译成app.asar文件替换到resources目录中,从而实现electron软件的热更新。

  1. 主进程(nodejs)侧代码:
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const fs = require("fs");
const http = require("http");
const asar = require('asar');
const AdmZip = require('adm-zip');
const fsExtra = require('fs-extra');const mainData = require("./mainData");
const txtConsole = require("./txtConsole");//当前环境
const production = mainData?.production;//Electron 安装根目录
const rootPath = production === 'dev' ? path.resolve('./public') : path.dirname(app.getPath('exe'));
mainData.rootPath = rootPath;// 软件更新配置信息(目前需要手动修改~~~)
const winUpdateConfig = {currentVersion: null, //当前版本updateVersionFilePath: 'http://103.117.121.53:8002/latest', //远程版本信息路径updateFilePath: 'http://103.117.121.53:8002/app.zip', //远程包路径localUpdateVersionFilePath: production === 'dev' ? `${rootPath}/latest` : `${rootPath}/resources/latest`, //本地版本信息路径localUpdateFilePath: production === 'dev' ? rootPath : `${rootPath}/resources`, //本地包路径updateSteps: [{id: 1, desc: '开始下载并解压更新文件,请勿重启!', active: 'active'},{id: 2, desc: '下载并解压完成, 开始覆盖安装!', active: 'wait'},{id: 3, desc: '更新完毕, 即将重启,请稍候!(第3步完成后也可以手动重启)', active: 'wait'},], //更新步骤  active:正在进行  wait:等待   success:执行成功  error: 执行失败
};let versionInfo = ''; //获取最新版本信息
let locallatest = ''; //本地版本号function appInit() {txtConsole.log('初始化');try {locallatest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');locallatest = JSON.parse(locallatest);//设置当前版本信息winUpdateConfig.currentVersion = locallatest?.version;txtConsole.log('已设置当前版本信息', locallatest?.version);//删除日志txtConsole.clearLog();} catch (err) {txtConsole.log(err);}
}//创建主窗口
function createWindow() {const mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {// preload: path.join(__dirname, 'preload.js'),nodeIntegration: true,contextIsolation: false,webSecurity: false, //禁用同源策略}});mainWindow.loadFile('index.html').then();// 打开开发者工具 控制台if (mainData.winControl === 'dev') {mainWindow.webContents.openDevTools();}// 检查更新ipcMain.on('window-version', function (event) {try {txtConsole.log('检查更新', versionInfo);if (!versionInfo) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');return;}const v = {...(versionInfo || {})};//最新版本号let firNewVersion = versionInfo?.version?.split('.')?.[0]; //第一位let secNewVersion = versionInfo?.version?.split('.')?.[1]; //第二位let thiNewVersion = versionInfo?.version?.split('.')?.[2]; //第三位//当前版本号let firOldVersion = versionInfo?.currentVersion?.split('.')?.[0]; //第一位let secOldVersion = versionInfo?.currentVersion?.split('.')?.[1]; //第二位let thiOldVersion = versionInfo?.currentVersion?.split('.')?.[2]; //第三位//按位比较是否需要更新if (Number(firNewVersion || 10000) > Number(firOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(secNewVersion || 10000) > Number(secOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(thiNewVersion || 10000) > Number(thiOldVersion || 10000)) {v['versionVisible'] = true;}else {v['versionVisible'] = false;v['currentVersion'] = versionInfo?.version;}if (!v['versionVisible']) {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo?.version || latest?.version;latest.currentVersion = versionInfo?.version || latest?.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('hot: ', latest.version);}txtConsole.log('versionVisible=> ', v['versionVisible']);!event?.sender?.isDestroyed() && event?.sender?.send('window-version-msg', v);} catch (err) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');txtConsole.log('检查更新err:', err);}});// 下载更新文件ipcMain.on('window-download-newfile', function (event) {txtConsole.log('开始下载并解压更新文件  热更新');event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);const file = fs.createWriteStream(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'),);let downloadedBytes = 0;let totalBytes = 0;http.get(winUpdateConfig.updateFilePath, (response) => {totalBytes = parseInt(response?.headers['content-length'], 10);let prevTimestamp = Date.now();response?.on('data', (chunk) => {downloadedBytes += chunk.length;const timestamp = Date.now();const timeDiff = timestamp - prevTimestamp;// 每1.5秒钟更新一次进度if (timeDiff >= 1500) {const progress = ((downloadedBytes / totalBytes) * 100).toFixed(2);txtConsole.log(`下载进度:${progress}% `, totalBytes);prevTimestamp = timestamp;event?.sender?.send('window-download-progress-msg', Math.min(Number(progress), 80));}});response?.pipe(file);}).on('error', (err) => {txtConsole.log(`下载错误: ${err.message}`);event?.sender?.send('window-download-newfile-err-msg', '更新文件下载失败');});file?.on('finish', function () {event.sender.send('window-download-progress-msg', 90);winUpdateConfig.updateSteps[0]['active'] = 'success';winUpdateConfig.updateSteps[1]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);// 文件已经完全写入磁盘,开始解压try {const zip = new AdmZip(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'), void 0);zip.extractAllTo(winUpdateConfig.localUpdateFilePath, true, void 0, void 0);} catch (err) {txtConsole.log('解压异常 error: ', err);!event?.sender?.isDestroyed() &&event?.sender?.send('window-download-newfile-err-msg', '解压异常');return;}winUpdateConfig.updateSteps[1]['active'] = 'success';winUpdateConfig.updateSteps[2]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);event?.sender?.send('window-download-progress-msg', 95);const sourceDir = path.join(winUpdateConfig.localUpdateFilePath, 'apps');const destPath = path.join(winUpdateConfig.localUpdateFilePath, 'app.asar');asar.createPackage(sourceDir, destPath).then(() => {if (fs.existsSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'))) {fs.unlinkSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'));}txtConsole.log('更新完毕');event.sender.send('window-download-progress-msg', 100);winUpdateConfig.updateSteps[2]['active'] = 'success';event?.sender?.send('window-download-newfile-msg',winUpdateConfig.updateSteps,'success',);//设置当前版本信息try {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('更新后已设置当前版本信息', latest?.version);//删除apps文件夹  防止执行文件夹内的代码deleteFolderRecursive(sourceDir);} catch (err) {txtConsole.log(err);}}).catch((err) => {txtConsole.log('创建asar文件失败: ', err);event.sender.send('window-download-newfile-err-msg', 'asar文件创建失败');});});file?.on('error', function (err) {txtConsole.log('更新asar=>Error: ', err);event.sender.send('window-download-newfile-err-msg', err);});});
}//检查更新
function checkUpdate(callback) {txtConsole.log('检查更新');http.get(winUpdateConfig.updateVersionFilePath, (res) => {res.on('data', (chunk) => {versionInfo += chunk;});res.on('end', () => {try {if (versionInfo && versionInfo?.indexOf('404 Not Found') < 0) {versionInfo = JSON.parse(versionInfo);winUpdateConfig.updateFilePath = versionInfo.updateFilePath;//热更最新信息let asarVersionInfo = {newVersionDesc: versionInfo.newVersionDesc,currentVersion: winUpdateConfig.currentVersion,};versionInfo.currentVersion = winUpdateConfig.currentVersion;let writeNewVersonInfo;//不存在则创建latest文件if (!fs.existsSync(winUpdateConfig.localUpdateVersionFilePath)) {writeNewVersonInfo = versionInfo;txtConsole.log('latest文件重新创建成功');}else {let currentVersion = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath,'utf8',);currentVersion = JSON.parse(currentVersion);currentVersion['updateFilePath'] = '';//只覆盖热更版本信息writeNewVersonInfo = {...currentVersion, ...asarVersionInfo};}//将整理好的配置文件信息写入fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath,JSON.stringify(writeNewVersonInfo),);// txtConsole.log('已将新的更新配置文件信息写入:', JSON.stringify(writeNewVersonInfo));txtConsole.log(`更新检查完毕:最新版本:${versionInfo.version}, 当前版本:${asarVersionInfo.currentVersion}`,);txtConsole.log('-------------------------------------------------------');callback?.(null, versionInfo);}else {txtConsole.log('更新配置文件读取失败');callback?.('更新配置文件读取失败');}} catch (err) {txtConsole.log('更新配置文件覆写失败');callback?.('更新配置文件覆写失败');}});}).on('error', (error) => {txtConsole.log(`更新配置文件下载失败: ${error.message}`);callback?.('更新配置文件下载失败');});
}//删除更新文件
function deleteFolderRecursive(folderPath) {if (fs.existsSync(folderPath)) {fs.readdirSync(folderPath).forEach((file) => {const curPath = path.join(folderPath, file);if (fs.lstatSync(curPath).isDirectory()) {// 递归删除子文件夹deleteFolderRecursive(curPath);}else {// 删除文件fs.unlinkSync(curPath);}});// 删除子文件夹后删除文件夹本身fs.rmdirSync(folderPath);}
}app.whenReady().then(() => {//初始化appInit();//检查更新checkUpdate(async (check, versionInfo = {}) => {if (check) {txtConsole.log('检查更新执行失败');}else {txtConsole.log('检查更新执行成功');}createWindow();app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})});
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})
  1. 渲染进程侧代码(以原生为例):
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/><title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button onclick="onCheckUpdate()">检查更新</button>
<button onclick="onUpdateVersion()">测试更新</button><div class="updateInfo"></div>
<div class="descInfo"></div><script>const {ipcRenderer} = require("electron");const onVersion = {updateDsec: [],process: 0,};//检查更新function onCheckUpdate() {ipcRenderer?.send('window-version');ipcRenderer?.once('window-version-msg', (_, bool) => {document.querySelector('.updateInfo').innerHTML = JSON.stringify(bool);});ipcRenderer?.once('window-version-err-msg', (_, err) => {document.querySelector('.updateInfo').innerHTML = err;});}//测试更新function onUpdateVersion() {onVersion.updateDsec = [];ipcRenderer?.send('window-download-newfile');//监听下载版本信息ipcRenderer?.on('window-download-newfile-msg', (event, updateDsec, status) => {if (!event.handel) {event.handel = true;onVersion.isStartUpdate = true;onVersion.updateDsec = updateDsec;document.querySelector('.descInfo').innerHTML = updateDsec.map(item => `<span style="color:${item.active === 'success' ? 'green' : 'orangered'}">${item.desc}</span>`).join('</br>');if (status === 'success') {ipcRenderer?.send('window-restart-app');}onVersion.visible = true;}});//监听更新包下载进度ipcRenderer?.on('window-download-progress-msg', (event, process) => {if (!event.handel) {event.handel = true;document.querySelector('.updateInfo').innerHTML = String("完成进度:" + process + '%');}});//监听下载版本错误信息ipcRenderer?.once('window-download-newfile-err-msg', (event, res) => {console.log(res)});}
</script>
</body>
</html>
  1. 轮子(实现压缩混淆反编译):
//生成 反编译app.asar  并生成压缩包
const asar = require('asar');
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const zlib = require('zlib');
const archiver = require('archiver');
const uglify = require('uglify-js');
const moment = require('moment');
const {exec} = require('child_process');
const JavaScriptObfuscator = require('javascript-obfuscator');const mainData = require('./mainData');const startTime = moment().unix(); //秒级时间戳const rootPath = path.resolve(__dirname); // 获取项目根路径
const asarPath = './build/win-ia32-unpacked/resources/app.asar'; // 获取 app.asar 文件路径
const sourceDir = './apps'; // 要压缩的文件夹路径
const asarAppPath = './apps/apps'; // asar反编译文件的存放路径
const buildPath = './build'; //electron 打包后的build文件夹
const destFile = './app.zip'; // 压缩后的文件路径
const publicLogPath = './public/log.txt';
const publicAsarPath = './public/app.asar';// 配置环境路径为项目根路径
const env = Object.assign({}, process.env, {PATH: rootPath + ';' + process.env.PATH,npm_config_prefix: 'C:\\Program Files\\nodejs\\npm', // 这里是你的 npm 安装路径
});const Console = {log(p1 = '', p2 = '', p3 = '', p4 = '', p5 = '') {console.log(`${moment().format('HH:mm:ss')}  |  ${p1}${p2}${p3}${p4}${p5}`);},
};//压缩主进程 main.js 相关代码
function zipMainJS() {try {const dir = path.resolve(asarAppPath, 'main.js');const dirJs = fs.readFileSync(dir, 'utf8');//压缩代码 mangle: true,const result = uglify.minify(dirJs, {mangle: {toplevel: true,},});// 混淆代码const obfuscationResult = JavaScriptObfuscator.obfuscate(result.code, {compact: true,controlFlowFlattening: true,controlFlowFlatteningThreshold: 0.75,numbersToExpressions: true,simplify: true,shuffleStringArray: true,splitStrings: true,stringArrayThreshold: 0.75,});fs.writeFileSync(dir, obfuscationResult.getObfuscatedCode());return true;} catch (err) {Console.log(err);return false;}
}//添加开始执行 app.asar反编译逻辑
async function init() {//执行app.asar 反编译、压缩、混淆Console.log('正在执行app.asar 反编译、压缩、混淆');// 将 app.asar 解压缩到指定文件夹中asar.extractAll(asarPath, asarAppPath);Console.log('正在压缩app文件夹到项目根目录');//压缩main.js相关代码let zipRes = zipMainJS();if (!zipRes) {Console.log('!!!压缩main.js主进程代码失败!');return;}Console.log('主进程相关代码压缩完毕');//再次生成 app.asarasar.createPackage(asarAppPath, asarPath).then(() => {Console.log('已再次生成 app.asar 文件(代码压缩后的asar文件)');onAppZip();});
}function onAppZip() {// 创建一个可写流,将压缩后的文件写入到目标文件中const destStream = fs.createWriteStream(destFile);// 创建一个 archiver 实例const archive = archiver('zip', {zlib: {level: zlib.constants.Z_BEST_COMPRESSION},});// 将可写流传递给 archiver 实例archive.pipe(destStream);// 将要压缩的文件夹添加到 archiver 实例中archive.directory(sourceDir, false, null);// 完成压缩并关闭可写流archive.finalize();// 监听可写流的 'close' 事件,表示压缩完成destStream.on('close', () => {Console.log(`压缩完毕,压缩包路径:【${path.resolve(__dirname, destFile)}`);Console.log('共用时:' + (moment().unix() - startTime) + '秒');});
}try {if (mainData?.production === 'dev') {throw "请将环境切换为生产环境 mainData.js => 【const production = 'pro';】";}if (mainData?.winControl === 'dev') {throw '请关闭主窗口调试控制台!' + 'winControl';}fsExtra.removeSync(buildPath);Console.log('已删除build文件夹内容');fsExtra.removeSync(publicLogPath);Console.log('已删除public/log.txt');fsExtra.removeSync(publicAsarPath);Console.log('已删除public/app.asar');fsExtra.removeSync(asarAppPath);Console.log('已删除apps');//执行 打包命令Console.log('正在执行【npm run packager32】命令');exec('npm run packager32', env, (error, stdout, stderr) => {if (error) {Console.log(`执行出错: ${error}`);return;}stderr && Console.log('【npm run packager32】 stderr=>', stderr);//生成app.init();});} catch (err) {Console.log(err);
}

示例Demo: https://github.com/qglovehy/electron-updater.git

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/670847.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

uniapp父子组件通信

在Uni-app中&#xff0c;可以使用props和$emit方法实现父子组件之间的通信。 父组件向子组件传递数据&#xff08;props&#xff09;&#xff1a; 1. 在父组件中&#xff0c;在子组件的标签上使用属性绑定方式传递数据 <template> <child-component :message"p…

飞天使-k8s知识点16-kubernetes实操1-pod

文章目录 深入Pod 创建Pod&#xff1a;配置文件详解写个pod的yaml 文件深入Pod 探针&#xff1a;探针技术详解 深入Pod 创建Pod&#xff1a;配置文件详解 资源清单参考链接&#xff1a;https://juejin.cn/post/6844904078909128712写个pod的yaml 文件 apiVersion: v1 kind: P…

Android Compose 一个音视频APP——Magic Music Player

Magic Music APP Magic Music APP Magic Music APP概述效果预览-视频资源功能预览Library歌曲播放效果预览歌曲播放依赖注入设置播放源播放进度上一首&下一首UI响应 歌词歌词解析解析成行逐行解析 视频播放AndroidView引入Exoplayer自定义Exoplayer样式横竖屏切换 歌曲多任…

Unix五种I/O模型(阻塞、非阻塞、多路复用、信号驱动、异步)

文章目录 概要一、I/O基础二、阻塞式I/O三、非阻塞式I/O三、I/O多路复用四、信号驱动I/O五、异步I/O六、小结 概要 在工作中&#xff0c;经常使用Nginx、Redis等开源组件&#xff0c;常提到其高性能的原因是网络I/O的实现是基于epoll&#xff08;多路复用&#xff09;。这次呢…

Android设置默认8时区和默认24小时制

1、写在前面 不同的产品开发有不同的需求对于默认的时间 2设置默认8时区和默认24小时制的核心类 build/make/tools/buildinfo_common.sh framework/base/packages/apps/SettingsProvider/res/values/defaults.xml framework/base/packages/apps/SettingsProvider/src/com/an…

护眼灯的色温标准是什么?护眼灯参数标准介绍

选择合适的护眼台灯不仅能提升家居的品质&#xff0c;还能为我们的生活增添一份温馨与舒适。不过有些色温调节不当不仅不能达到很好的学习效率&#xff0c;还容易打瞌睡&#xff0c;甚至伤眼睛的情况也有可能出现&#xff0c;那么什么色温有什么标准呢&#xff1f; 一、合适的…

pnpm + vite 从外网迁移到内网环境开发

离线安装pnpm 在有外网的机器上执行以下命令&#xff0c;下载pnpm的tgz压缩包至桌面&#xff0c;注意下载版本和当前使用版本保持一致 npm pack -g pnpm7.4.0 --pack-destination ~/Desktop将tgz压缩包拷贝至离线机器在离线机器中执行命令 npm -g i /home/user/offline/pnpm…

Linux命令-arpwatch命令(监听网络上ARP的记录)

说明 arpwatch命令 用来监听网络上arp的记录。 语法 arpwatch(选项)选项 -d&#xff1a;启动排错模式&#xff1b; -f<记录文件>&#xff1a;设置存储ARP记录的文件&#xff0c;预设为/var/arpwatch/arp.dat&#xff1b; -i<接口>&#xff1a;指定监听ARP的接口…

[Lucene]核心类和概念介绍

先上一个使用Lucene读写文件的DEMO import java.io.IOException; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; impo…

互联网加竞赛 基于深度学习的动物识别 - 卷积神经网络 机器视觉 图像识别

文章目录 0 前言1 背景2 算法原理2.1 动物识别方法概况2.2 常用的网络模型2.2.1 B-CNN2.2.2 SSD 3 SSD动物目标检测流程4 实现效果5 部分相关代码5.1 数据预处理5.2 构建卷积神经网络5.3 tensorflow计算图可视化5.4 网络模型训练5.5 对猫狗图像进行2分类 6 最后 0 前言 &#…

大型语言模型(LLM)的优势、劣势和风险

最近关于大型语言模型的奇迹&#xff08;&#xff09;已经说了很多LLMs。这些荣誉大多是当之无愧的。让 ChatGPT 描述广义相对论&#xff0c;你会得到一个非常好&#xff08;且准确&#xff09;的答案。然而&#xff0c;归根结底&#xff0c;ChatGPT 仍然是一个盲目执行其指令集…

进程控制(Linux)

进程控制 一、进程创建1. 再识fork2. 写时拷贝 二、进程终止前言——查看进程退出码1. 退出情况正常运行&#xff0c;结果不正确异常退出 2. 退出码strerror和errno系统中设置的错误码信息perror异常信息 3. 退出方法exit和_exit 三、进程等待1. 解决等待的三个问题2. 系统调用…

银行数据仓库体系实践(17)--数据应用之营销分析

营销是每个银行业务部门重要的工作任务&#xff0c;银行产品市场竞争激烈&#xff0c;没有好的营销体系是不可能有立足之地&#xff0c;特别是随着互联网金融发展,金融脱媒”已越来越普遍&#xff0c;数字化营销方兴未艾&#xff0c;银行的营销体系近些年也不断发展&#xff0c…

Go语言教学(一)起源

目录 一.Go语言来源 二.Go语言应用 一.Go语言来源 Go语言&#xff0c;又称Golang&#xff0c;是Google公司于2009年11月正式对外公开的一门编程语言。它是一门静态强类型、编译型的语言&#xff0c;其语法与C相近&#xff0c;但在功能上有所改进和增加&#xff0c;如内存安全…

【精选】java继承进阶,子类继承父类(内存图、内存分析工具)

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

ftrace工具学习笔记

ftrace是一个功能强大的Linux内核跟踪工具&#xff0c;可用于分析内核的行为和性能问题。它可以用来收集各种内核跟踪数据&#xff0c;如函数调用、内存分配、中断处理等。以下是ftrace的一些主要特点和用法&#xff1a; ftrace是内核自带的跟踪工具&#xff0c;因此无需安装。…

FRP内网穿透如何避免SSH暴力破解(二)——指定地区允许访问

背景 上篇文章说到&#xff0c;出现了试图反复通过FRP的隧道&#xff0c;建立外网端口到内网服务器TCP链路的机器人&#xff0c;同时试图暴力破解ssh。这些连接造成了流量的浪费和不必要的通信开销。考虑到服务器使用者主要分布在A、B、C地区和国家&#xff0c;我打算对上一篇…

ELAdmin 前端启动

开发工具 官方指导的是使用WebStorm&#xff0c;但是本人后端开发一枚&#xff0c;最终还是继续使用了 idea&#xff0c;主打一个能用就行。 idea正式版激活方式&#xff1a; 访问这个查找可用链接&#xff1a;https://3.jetbra.in/进入任意一个能用的里面&#xff0c;顶部提…

消息中间件之RocketMQ源码分析(六)

Consumer消费方式 RocketMQ的消费方式包含Pull和Push两种 Pull方式。 用户主动Pull消息&#xff0c;自主管理位点&#xff0c;可以灵活地掌控消费进度和消费速度&#xff0c;适合流计算、消费特别耗时等特殊的消费场景。 缺点也显而易见&#xff0c;需要从代码层面精准地控制…

vue3+ts+vite搭建步骤

vue3 ts vite搭建步骤如下&#xff1a; 1&#xff0c;首先确保你的机器上已经安装了 Node.js 和 npm。你可以通过在终端运行 node -v 和 npm -v 来检查是否已经安装。 2&#xff0c;安装 Vite。在你的终端运行以下命令 npm install -g create-vite3&#xff0c;使用 Vite 创…