插件优化,增加FTP上传进度监控.

This commit is contained in:
andrewlu 2021-02-02 22:20:01 +08:00
parent 46b65e4f9c
commit 99ec923ebc
8 changed files with 292 additions and 103 deletions

View File

@ -2,6 +2,7 @@
const fs = require('fs');
const FileUtil = require('./FileUtils');
const Client = require("ftp");
const logger = require('./logger');
class FileUploader {
ftps = {};
@ -13,7 +14,7 @@ class FileUploader {
lastIdleState = true;
constructor() {
this.loopId = setInterval(this.loop.bind(this), 200);
this.loopId = setInterval(this.loop.bind(this), 1000);
}
setOption(options, maxThread) {
@ -25,6 +26,10 @@ class FileUploader {
this.idleCallback = callback;
}
restart(tName) {
this.ftps[tName] && this.ftps[tName].restart();
}
prepare() {
const currLen = Object.keys(this.ftps).length;
logger.info('准备创建上传线程:', this.maxClients, currLen);
@ -52,6 +57,7 @@ class FileUploader {
const curState = this.checkState();
if (curState != this.lastIdleState) {
this.lastIdleState = curState;
logger.info('当前状态:', curState);
this.idleCallback && this.idleCallback(curState);
}
}
@ -65,6 +71,29 @@ class FileUploader {
return idleState;
}
// 检测所有线程运行状态
checkThreads() {
let str = [];
for (let i in this.ftps) {
let p = 0;
if (this.ftps[i].totalTasks && !this.ftps[i].idleState) {
p = (this.ftps[i].totalTasks - this.ftps[i].queue.length) / this.ftps[i].totalTasks;
}
p = Math.floor(p * 10000) / 100;
str.push({
name: i,
idle: this.ftps[i].idleState,
remain: this.ftps[i].queue.length,
hasError: this.ftps[i].hasError,
totalTasks: this.ftps[i].totalTasks,
curTaskPath: this.ftps[i].curTaskPath,
taskStartTime: this.ftps[i].taskStartTime,
progress: p
});
}
return str;
}
/**
* 上传文件目录到ftp服务器
* @param source
@ -105,6 +134,12 @@ class UploadThread {
stopped = false;
options = null;
idleState = true;
hasError = false;
// 总任务数量, 所有任务上传完成时,total清零.
totalTasks = 0;
curTaskPath = "";// 显示当前任务.
taskStartTime = 0; // 显示任务用时.
constructor(options) {
this.options = options;
@ -117,6 +152,7 @@ class UploadThread {
if (this.stopped) {
this.ftp = this.newConnection();
}
this.totalTasks += 1;
}
newConnection() {
@ -124,6 +160,10 @@ class UploadThread {
const ftp = new Client();
ftp.on('ready', () => {
this.ready = true;
if (this.hasError) {
this.hasError = false;
logger.success('连接恢复,当前剩余任务:', this.queue.length);
}
});
ftp.on('close', () => {
this.ready = false;
@ -138,47 +178,78 @@ class UploadThread {
return ftp;
}
destroy() {
this.stopped = true;
getTaskLength() {
return this.queue.length;
}
async sleep(ms) {
destroy() {
this.stopped = true;
this.ftp.destroy();
this.totalTasks = 0;
}
sleep(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
setTimeout(resolve, ms, true);
});
}
// 定时循环.直到destroy()执行.
async run() {
while (!this.stopped) {
if (!this.ready || this.queue.length <= 0) {
await this.sleep(500);
if (this.hasError || !this.ready) {
await this.sleep(1000);
let ts = new Date().getTime();
if (this.hasError && ts - this.taskStartTime > 60000) {
this.taskStartTime = ts;
this.ftp.destroy();
this.ftp = this.newConnection();
logger.log('线程异常超时,尝试重启线程');
}
continue;
}
if (this.queue.length <= 0) {
this.idleState = true;
this.totalTasks = 0;
await this.sleep(1000);
continue;
}
this.idleState = false;
const once = this.queue[0];
this.curTaskPath = once;
this.taskStartTime = new Date().getTime();
const err = await this.uploadOnce(this.ftp, once);
if (!err) {
this.queue.shift();
logger.info('上传完成:', once.src);
this.curTaskPath = "";
continue;
} else {
logger.warn('上传失败,准备重试:', once.src);
this.ftp.end();
this.ftp.logout();
this.ftp.destroy();
// 上传失败有可能是线程强制结束导致的上传失败.
if (!this.stopped) {
this.hasError = true;
this.ftp = this.newConnection();
await this.sleep(5000);
}
}
}
this.ftp.end();
this.ftp.destroy();
}
uploadOnce(ftp, once) {
return new Promise(resolve => {
return Promise.race([new Promise(function (resolve) {
ftp.put(once.src, once.dst, false, resolve)
});
}), this.sleep(20000)]);
}
restart() {
this.hasError = true;
this.ready = false;
this.ftp.destroy();
this.ftp = null;
this.ftp = this.newConnection();
}
}

View File

@ -1,14 +1,6 @@
const fs = require('fs')
const path = require('path')
// console adapter.
global.logger = global.logger || {};
logger.log = (Editor && Editor.log) || console.log;
logger.info = (Editor && Editor.info) || console.info;
logger.warn = (Editor && Editor.warn) || console.warn;
logger.error = (Editor && Editor.error) || console.error;
logger.success = (Editor && Editor.success) || (Editor && Editor.info) || console.log;
class FileUtil {
/*
* 获取window上的文件目录以及文件列表信息

View File

@ -0,0 +1,12 @@
const logger = (global && global.Editor) || (global && global.console) || (window && window.console);
/**
* 适配不同Logger 对象.
* @type {{warn: (message?: any, ...optionalParams: any[]) => void, log: (message?: any, ...optionalParams: any[]) => void, success: any | ((message?: any, ...optionalParams: any[]) => void), error: (message?: any, ...optionalParams: any[]) => void, info: (message?: any, ...optionalParams: any[]) => void}}
*/
module.exports = {
log: logger.log,
info: logger.info,
warn: logger.warn,
error: logger.error,
success: logger.success
}

View File

@ -3,6 +3,7 @@ const fs = require('fs');
const {join} = require('path');
const FileUtil = require('./js/FileUtils');
const FileUpload = require('./js/FileUploader');
const logger = require('./js/logger');
// 生成更新文件.
const doMakeUpdatePackage = function (opt, cb) {
@ -70,10 +71,12 @@ module.exports = {
},
onUploadStateChange(state) {
logger.log('当前任务执行器状态:', state);
if (!state) {
Editor.info('开始上传更新包');
this.startUploadTime = new Date().getTime();
logger.info('开始上传更新包', this.startUploadTime);
} else {
Editor.success('所有文件上传完成!');
logger.success(`所有文件上传完成,耗时:${Math.floor((new Date().getTime() - this.startUploadTime) / 1000)}`);
}
},
@ -95,6 +98,15 @@ module.exports = {
'getProjectPath'(event) {
event.reply(Editor.Project.path);
},
"checkThreads"(event) {
event.reply(this.ftp.checkThreads());
},
'queryThreadStates'(event) {
event.reply(this.ftp.checkState());
},
'restartThread'(event, name) {
this.ftp.restart(name);
},
'uploadStop'() {
if (this.ftp) {
this.ftp.destroy();

View File

@ -13,8 +13,9 @@
"main": "panel/index.html",
"type": "simple",
"title": "热更新配置",
"width": 500,
"height": 600
"width": 600,
"height": 800,
"resizable": false
},
"dependencies": {
"ftp": "^0.3.10"

View File

@ -4,24 +4,14 @@
<meta charset="UTF-8">
<!-- import CSS -->
<link rel="stylesheet" href="./element-ui/index.css">
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #3646ea;
background-color: #fafcfc;
padding: 35px;
height: 100%;
}
</style>
<link rel="stylesheet" href="./main.css">
<title>热更新配置</title>
</head>
<body style="height: 1000px;">
<div id="app">
<el-tabs v-model="tabName">
<el-tab-pane label="版本配置" name="versionConfig" style="height: 400px;">
<el-form ref="version" :model="versionCfg" label-width="100px">
<el-tab-pane label="版本配置" name="versionConfig" class="tab-content">
<el-form ref="version" :model="versionCfg" label-width="80px">
<el-tooltip effect="dark" :content="tip.versionCode" open-delay=500 placement="top">
<el-form-item label="新版本号:">
<el-input type="number" v-model="versionCfg.versionCode" placeholder="1"></el-input>
@ -38,6 +28,21 @@
<el-input v-model="versionCfg.versionType" placeholder="dev"></el-input>
</el-form-item>
</el-tooltip>
<!--<el-form-item label="其他设置:">-->
<!--<el-tooltip content="开启时将强制更新用户端版本" placement="top">-->
<!--<el-switch v-model="versionCfg.forceUpdate"-->
<!--:active-text="versionCfg.forceUpdate?'强制静默更新':'用户决定更新'"-->
<!--active-color="#13ce66" inactive-color="#ff4949"-->
<!--style="margin-right: 20px;"></el-switch>-->
<!--</el-tooltip>-->
<!--<el-tooltip placement="top">-->
<!--<div slot="content">开启时将在每次启动时直接请求更新信息<br/>否则将在进入游戏后才请求更新信息</div>-->
<!--<el-switch v-model="versionCfg.updateMoment"-->
<!--:active-text="versionCfg.updateMoment?'每次启动时更新':'二次启动时更新'"-->
<!--active-color="#13ce66" inactive-color="#ff4949"></el-switch>-->
<!--</el-tooltip>-->
<!--</el-form-item>-->
<el-tooltip effect="dark" :content="tip.baseUrl" open-delay=500 placement="top">
<el-form-item label="热更地址:">
<el-input v-model="versionCfg.baseUrl"
@ -52,8 +57,8 @@
</el-tooltip>
</el-form>
</el-tab-pane>
<el-tab-pane label="上传配置" name="ftpConfig" style="height: 400px;">
<el-form ref="ftpCfg" :model="ftpCfg" label-width="100px">
<el-tab-pane label="上传配置" name="ftpConfig" class="tab-content">
<el-form ref="ftpCfg" :model="ftpCfg" label-width="80px">
<el-tooltip effect="dark" :content="tip.ip" placement="top" open-delay=500>
<el-form-item label="IP:">
<el-col :span="15">
@ -103,27 +108,72 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="使用帮助" name="help" style="height: 400px;">
<div style="height: 300px;">
<el-alert :closable="false"
title="如不需要发布新版本,可无需做任何操作,以正常步骤打包发布即可"
type="info">
</el-alert>
<el-divider content-position="left"></el-divider>
<el-steps direction="vertical" :active="1" space="100px">
<el-step title="版本配置"
description="使用前需要先配置版本信息,并点击保存配置."></el-step>
<el-step title="构建版本"
description="开始构建版本,构建完成后会在构建目录中生成remote文件夹以及update-dev.json更新描述文件
<el-tab-pane label="使用帮助" name="help" class="tab-content">
<el-alert :closable="false"
title="如不需要发布新版本,可无需做任何操作,以正常步骤打包发布即可"
type="info">
</el-alert>
<el-divider content-position="left"></el-divider>
<el-steps direction="vertical" :active="1" space="100px">
<el-step title="版本配置"
description="使用前需要先配置版本信息,并点击保存配置."></el-step>
<el-step title="构建版本"
description="开始构建版本,构建完成后会在构建目录中生成remote文件夹以及update-dev.json更新描述文件
注:不需要设置Bundle为远程包,插件将自动调整远程包设置"></el-step>
<el-step title="上传更新包"
description="构建完成时,如需发布新版本,则需要点击[上传更新包],更新包会向游戏代码中注入热更逻辑."></el-step>
</el-steps>
</div>
<el-step title="上传更新包"
description="构建完成时,如需发布新版本,则需要点击[上传更新包],更新包会向游戏代码中注入热更逻辑."></el-step>
</el-steps>
</el-tab-pane>
<el-tab-pane label="任务状态" name="states" class="tab-content">
<template v-if="states.length>0">
<el-row :gutter="10">
<el-col v-for="(value,index) in states" :key="index" :span="24" style="margin-bottom:10px">
<el-card :body-style="{ padding: '0px' }">
<div slot="header" class="clearfix">
<el-col :span="16">
<h3 style="line-height: 5px;">{{index+1}}-线程:{{value.name}}</h3>
</el-col>
<el-col :span="8">
<el-button :loading="!value.idle" size="small"
:type="value.idle?'success':'danger'">
{{value.idle?"空闲中":"正忙"}}
</el-button>
<el-tooltip effect="dark" content="如果任务长时间卡住,点击重试即可" placement="top" open-delay=500>
<el-button icon="el-icon-refresh-right" size="small" circle type="warning"
@click="onRestart(value.name)"></el-button>
</el-tooltip>
</el-col>
</div>
<el-form label-position="right" label-width="80px">
<el-form-item label="剩余任务:" style="margin-bottom: 5px;">
<span>{{value.totalTasks-value.remain}}/{{value.totalTasks}}</span>
</el-form-item>
<el-form-item label="是否异常:" style="margin-bottom: 5px;">
<span>{{value.hasError?"正在重试中...":"线程正常"}}</span>
</el-form-item>
<el-form-item label="当前上传:" style="margin-bottom: 5px;">
<span>{{value.curTaskPath?value.curTaskPath.dst.substring(value.curTaskPath.dst.length>>1):"无目标"}}</span>
</el-form-item>
<el-form-item label="上传用时:" style="margin-bottom: 5px;">
<span>{{(!value.idle && value.taskStartTime)? Math.floor(new Date().getTime()-value.taskStartTime):0}} 毫秒</span>
</el-form-item>
<el-form-item label="任务进度:" style="margin-bottom: 5px;">
<el-progress :text-inside="true" :stroke-width="22"
style="width: 90%; line-height: unset;"
:percentage="value.progress" :color="customColors"></el-progress>
</el-form-item>
</el-form>
</el-card>
</el-col>
</el-row>
</template>
<template v-else>
<h1 style="text-align: center; vertical-align: center;line-height: 15;">无任务</h1>
</template>
</el-tab-pane>
</el-tabs>
<el-row>
<el-row style="margin-top: 20px;">
<el-col :span="8">
<el-button type="success" @click="onSave">保存配置信息</el-button>
</el-col>
@ -138,6 +188,7 @@
</body>
<!-- import Vue before Element -->
<script src="../js/vue.2.5.16.js"></script>
<script src="../js/logger.js"></script>
<!-- import JavaScript -->
<script src="./element-ui/index.js"></script>
<script>
@ -155,7 +206,9 @@
versionName: "1.0.0",
versionType: "dev",
versionLog: "",
baseUrl: ""
baseUrl: "",
// forceUpdate: true, // 强制静默更新.
// updateMoment: true // 启动时直接请求更新.
},
ftpCfg: {
host: "127.0.0.1",
@ -177,7 +230,10 @@
port: "FTP端口号,默认21",
user: "FTP登录验证帐号及密码",
rootPath: "FTP文件上传根目录"
}
},
states: [],
refreshTimer: 0,
uploadState: false
};
},
created: function () {
@ -192,8 +248,25 @@
self.projectPath = arg;
});
},
mounted() {
this.refreshTimer = setInterval(this.checkThreads.bind(this), 200);
},
destroyed() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
},
methods: {
customColors(p) {
return `#${this.getR(0x90, 0x67, p)}${this.getR(0x93, 0xc2, p)}${this.getR(0x99, 0x3a, p)}`;
},
getR(from, to, percent) {
return Math.floor(from + (to - from) * percent / 100).toString(16)
},
onRestart(id) {
Editor.Ipc.sendToMain('update-manager:restartThread', id);
},
onSave() {
if (!this.versionCfg.baseUrl.endsWith('/') && this.versionCfg.baseUrl.length > 0) {
this.versionCfg.baseUrl += "/";
@ -207,7 +280,14 @@
this.$message({message: "配置保存成功!", type: "success"});
},
onUpload() {
Editor.info("准备上传更新包");
if (this.uploadState) {
this.$message('上传正在进行中,请勿重复点击哦~');
logger.warn('上传正在进行中,请勿重复点击哦~');
return;
}
this.uploadState = true;
logger.info("准备上传更新包");
this.$message("准备上传更新包");
if (!this.versionCfg.baseUrl.endsWith('/') && this.versionCfg.baseUrl.length > 0) {
this.versionCfg.baseUrl += "/";
@ -226,7 +306,7 @@
}
if (this.ftpCfg.onlyJson) {
Editor.warn('当前仅上传JSON文件,请手动上传remote目录');
logger.warn('当前仅上传JSON文件,请手动上传remote目录');
return;
}
// 上传更新描述文件.
@ -241,6 +321,16 @@
onStopUpload() {
Editor.Ipc.sendToMain('update-manager:uploadStop');
},
checkThreads() {
const self = this;
Editor.Ipc.sendToMain('update-manager:checkThreads', function (args) {
self.states = args;
});
// 检测是否空闲中.
Editor.Ipc.sendToMain('update-manager:queryThreadStates', function (args) {
self.uploadState = !args;
});
},
notifyMainJs(updateUrl, serverUrl) {
this.$message.info("notifyMainJs");
const filePath = path.join(this.projectPath, 'build/jsb-link/main.js');

View File

@ -0,0 +1,33 @@
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: left;
vertical-align: center;
color: #3646ea;
background-color: #fafcfc;
padding: 35px;
height: 100%;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.badge-item {
margin-top: 10px;
margin-right: 40px;
}
.tab-content {
height: 600px;
overflow-y: auto;
overflow-x: hidden;
overflow-scrolling: auto;
}

View File

@ -11,62 +11,41 @@ window.beforeBoot = function () {
window.boot();
return;
}
url += `?_t=${Math.random()}`;
cc.log("请求更新地址:", url);
get(url, function (err, asset) {
cc.log("请求更新信息:", url, err && err.toLocaleString(), JSON.stringify(asset));
if (err || !asset) {
window.boot();
return;
}
// 请求缓存信息,判断是否需要更新.
let assetStr = window.localStorage.getItem('version_data');
if (!assetStr) {
window.boot();
} else {
let asset = JSON.parse(assetStr);
window.mergeVersion(asset);
window.boot();
cc.log("游戏已启动.");
}
// 游戏启动后再请求更新,避免影响启动速度.
url += `?_t=${Math.random()}`;
cc.log("请求更新地址:", url);
get(url, function (err, asset) {
if (err || !asset) {
return;
}
window.localStorage.setItem('version_data', asset);
});
};
window.mergeVersion = function (updateInfo) {
const currentVer = cc.sys.localStorage.getItem("currentVer");
let isFirstRun = false;
let newVerFlag = false;
if (!currentVer) {
isFirstRun = true;
cc.log("当前为首次运行");
} else {
const oldVerInfo = JSON.parse(currentVer);
if (oldVerInfo && oldVerInfo.versionCode != updateInfo.versionCode) {
newVerFlag = true;
cc.log("发现新版本信息:", updateInfo.versionCode, oldVerInfo.versionCode);
}
}
const settings = window._CCSettings;
if (updateInfo.server) {
settings.server = updateInfo.server;
cc.log("更新远程资源地址:", updateInfo.server);
}
const bundleVers = updateInfo.bundles
const bundleVers = updateInfo.bundles;
if (bundleVers) {
let changed = false;
for (let b in bundleVers) {
if (bundleVers[b] != settings.bundleVers[b]) {
// 配置中的bundleVer版本不一致,则添加到remote列表中去,以供远程加载.
if (settings.remoteBundles.indexOf(b) < 0) {
settings.remoteBundles.push(b);
}
changed = true;
cc.log("发现更新Bundle:", b);
}
}
settings.bundleVers = bundleVers;
// 如果首次运行,但检测版本有差异,则标记有更新.
if (isFirstRun && changed) {
newVerFlag = true;
cc.log("标记为新版本.");
}
}
cc.sys.localStorage.setItem('newVerFlag', newVerFlag ? 1 : 0);
cc.sys.localStorage.setItem('firstRunFlag', isFirstRun ? 1 : 0);
cc.sys.localStorage.setItem('currentVer', JSON.stringify(updateInfo));
return;
}
// ajax 请求.
@ -77,8 +56,8 @@ function get(url, cb) {
ajax.onreadystatechange = function () {
if (ajax.readyState == 4) {
if (ajax.status == 200) {
var response = JSON.parse(ajax.responseText)
cb && cb(null, response);
// var response = JSON.parse(ajax.responseText)
cb && cb(null, ajax.responseText);
return;
} else {
cb && cb("request error!");
@ -87,4 +66,3 @@ function get(url, cb) {
}
ajax.send(null);
}