🎁 Sync 2026-03-15 08:51:25

This commit is contained in:
github-actions[bot] committed 2026-03-15 08:51:25 +08:00
1 parent 80bd1ecded
commit 5a6ba160b1
10 files changed
+597 -354

No files matched your search

@@ -1,4 +1,4 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com */
'use strict';
'require view';
'require fs';
@@ -6,36 +6,142 @@
'require uci';
'require form';
'require poll';
var state = {
running: false,
port: 3300,
enabled: false,
operationInProgress: false,
operationType: null // 'start' 或 'stop'
};
const logPath = '/tmp/netspeedtest.log';
function checkProcess(quick = false) {
if (quick) {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
return res.code === 0 && res.stdout.trim() !== '';
})
.catch(function() {
return false;
});
} else {
return fs.exec('/usr/bin/pgrep', ['homebox'])
.then(function(res) {
if (res.code === 0 && res.stdout.trim()) {
return {
running: true,
pid: res.stdout.trim()
};
}
return fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid='])
.then(function(psRes) {
var pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
});
})
.catch(function(err) {
return { running: false, pid: null };
});
}
}
function controlService(action, port) {
if (action === 'start') {
return fs.exec('/usr/bin/killall', ['homebox'])
.catch(function() { return Promise.resolve(); })
.then(function() {
var command = 'nohup /usr/bin/homebox serve --port ' + port + ' > ' + logPath + ' 2>&1 &';
return fs.exec('/bin/sh', ['-c', command]);
});
} else {
return fs.exec('/etc/init.d/netspeedtest', ['stop']);
}
}
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption homebox_port '${newPort}'
\toption homebox_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
})
.then(() => {
return uci.load('netspeedtest');
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]).then(() => {
var port = uci.get('netspeedtest', 'config', 'homebox_port');
if (port) {
state.port = parseInt(port);
}
var enabled = uci.get('netspeedtest', 'config', 'homebox_enabled');
state.enabled = enabled === '1';
});
},
render: function() {
var state = {
running: false,
port: 3300
};
var container = E('div');
var statusSection = E('div', { 'class': 'cbi-section' });
var statusIcon = E('span', { 'style': 'margin-right: 5px;' });
var statusText = E('span');
var toggleBtn = E('button', { 'class': 'btn cbi-button' });
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('img', {
src: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCI+PHBhdGggZmlsbD0iI2RmMDAwMCIgZD0iTTk0Mi40MjEgMjM0LjYyNGw4MC44MTEtODAuODExLTE1My4wNDUtMTUzLjA0NS04MC44MTEgODAuODExYy03OS45NTctNTEuNjI3LTE3NS4xNDctODEuNTc5LTI3Ny4zNzYtODEuNTc5LTI4Mi43NTIgMC01MTIgMjI5LjI0OC01MTIgNTEyIDAgMTAyLjIyOSAyOS45NTIgMTk3LjQxOSA4MS41NzkgMjc3LjM3NmwtODAuODExIDgwLjgxMSAxNTMuMDQ1IDE1My4wNDUgODAuODExLTgwLjgxMWM3OS45NTcgNTEuNjI3IDE3NS4xNDcgODEuNTc5IDI3Ny4zNzYgODEuNTc5IDI4Mi43NTIgMCA1MTItMjI5LjI0OCA1MTItNTEyIDAtMTAyLjIyOS0yOS45NTItMTk3LjQxOS04MS41NzktMjc3LjM3NnpNMTk0Ljk0NCA1MTJjMC0xNzUuMTA0IDE0MS45NTItMzE3LjA1NiAzMTcuMDU2LTMxNy4wNTYgNDggMCA5My40ODMgMTAuNjY3IDEzNC4yMjkgMjkuNzgxbC00MjEuNTQ3IDQyMS41NDdjLTE5LjA3Mi00MC43ODktMjkuNzM5LTg2LjI3Mi0yOS43MzktMTM0LjI3MnpNNTEyIDgyOS4wNTZjLTQ4IDAtOTMuNDgzLTEwLjY2Ny0xMzQuMjI5LTI5Ljc4MWw0MjEuNTQ3LTQyMS41NDdjMTkuMDcyIDQwLjc4OSAyOS43ODEgODYuMjcyIDI5Ljc4MSAxMzQuMjI5LTAuMDQzIDE3NS4xNDctMTQxLjk5NSAzMTcuMDk5LTMxNy4wOTkgMzE3LjA5OXoiLz48L3N2Zz4=',
style: 'width: 100px; height: 100px; margin-bottom: 1em;'
}),
E('h2', {}, _('Homebox Service Not Running')),
E('p', {}, _('Please enable the Homebox service'))
]);
var saveBtn = E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;'
}, _('Save'));
var enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'homebox_enable',
'class': 'cbi-input-checkbox'
});
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('div', { style: 'font-size: 5em; color: #f39c12; margin-bottom: 0.2em;' }, '⚠️'),
E('h2', {}, _('Homebox Service Not Running')),
E('p', { style: 'color: #666; margin-top: 1em;' }, _('Please start the Homebox service'))
]);
var isHttps = window.location.protocol === 'https:';
var iframe;
if (!isHttps) {
iframe = E('iframe', {
src: window.location.origin + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
}
var portInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'style': 'width: 100px;',
'value': state.port,
'min': 1024,
'max': 65535,
'placeholder': '3300'
});
var portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
function createHttpsButton() {
return E('div', {
@@ -49,57 +155,57 @@ return view.extend({
class: 'cbi-button cbi-button-apply',
style: 'display: inline-block; margin-top: 1em; padding: 10px 20px; font-size: 16px; text-decoration: none; color: white;'
}, _('Open Web Interface'))
]);
}
async function checkProcess() {
try {
// 尝试使用pgrep
const res = await fs.exec('/usr/bin/pgrep', ['homebox']);
return {
running: res.code === 0,
pid: res.stdout.trim() || null
};
} catch (err) {
// 回退到ps方法
try {
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
} catch (err) {
return { running: false, pid: null };
}
}
}
function controlService(action) {
var command = action === 'start'
? 'nohup /usr/bin/homebox >> /tmp/netspeedtest.log 2>&1 &'
: '/usr/bin/killall homebox';
return fs.exec('/bin/sh', ['-c', command]);
}
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Homebox Server') + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.textContent = _('Homebox Server') + ' ' + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red';
statusText.style.fontWeight = 'bold';
statusText.style.fontSize = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
if (state.operationInProgress) {
if (state.operationType === 'start') {
toggleBtn.textContent = _('Starting...');
} else if (state.operationType === 'stop') {
toggleBtn.textContent = _('Stopping...');
}
toggleBtn.disabled = true;
if (state.operationType === 'stop' && !state.running) {
console.log('Stop confirmed - operation complete');
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
} else if (state.operationType === 'start' && state.running) {
console.log('Start confirmed - operation complete');
state.operationInProgress = false;
state.operationType = null;
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
} else {
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = 'btn cbi-button cbi-button-' + (state.running ? 'reset' : 'apply');
toggleBtn.disabled = false;
}
enableCheckbox.checked = state.enabled;
portInput.value = state.port;
// Update container content based on state and protocol
container.textContent = '';
container.innerHTML = '';
if (state.running) {
if (isHttps) {
container.appendChild(createHttpsButton());
} else {
iframe = E('iframe', {
src: 'http://' + window.location.hostname + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
container.appendChild(iframe);
}
} else {
@@ -107,59 +213,185 @@ return view.extend({
}
}
toggleBtn.addEventListener('click', ui.createHandlerFn(this, function() {
toggleBtn.addEventListener('click', function(ev) {
ev.preventDefault();
if (toggleBtn.disabled || state.operationInProgress) return;
var action = state.running ? 'stop' : 'start';
return controlService(action)
.then(checkProcess)
.then(res => {
state.running = res.running;
var startTime = Date.now();
state.operationInProgress = true;
state.operationType = action;
updateStatus();
controlService(action, state.port)
.then(function() {
return new Promise(function(resolve, reject) {
var checkCount = 0;
var maxChecks = 30;
function doCheck() {
checkProcess(true).then(function(isRunning) {
if (action === 'stop' && !isRunning) {
console.log('Stop success after', Date.now() - startTime, 'ms');
resolve({ running: false });
} else if (action === 'start' && isRunning) {
console.log('Start success after', Date.now() - startTime, 'ms');
checkProcess().then(resolve).catch(resolve);
} else if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 200);
} else {
console.log('Check timeout, using full check');
checkProcess().then(resolve).catch(resolve);
}
}).catch(function() {
if (checkCount < maxChecks) {
checkCount++;
setTimeout(doCheck, 100);
} else {
checkProcess().then(resolve).catch(resolve);
}
});
}
doCheck();
});
})
.then(function(res) {
if (res) {
if (typeof res === 'boolean') {
state.running = res;
} else {
state.running = res.running || false;
if (res.port) {
state.port = res.port;
}
}
}
state.operationInProgress = false;
state.operationType = null;
updateStatus();
var message = action === 'start' ?
_('Homebox server started on port %s').replace('%s', state.port) :
_('Homebox server stopped');
})
.catch(function(err) {
console.error('Service control error:', err);
// 发生错误时重新检查
checkProcess().then(function(res) {
state.running = res.running || false;
if (res.port) state.port = res.port;
state.operationInProgress = false;
state.operationType = null;
updateStatus();
}).catch(function() {
state.operationInProgress = false;
state.operationType = null;
updateStatus();
});
});
}));
});
saveBtn.addEventListener('click', function(ev) {
ev.preventDefault();
var newPort = parseInt(portInput.value, 10);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveBtn.disabled = true;
saveBtn.textContent = _('Saving...');
saveConfiguration(newPort, enableCheckbox.checked)
.then(function() {
state.port = newPort;
state.enabled = enableCheckbox.checked;
updateStatus();
})
.catch(function(err) {
})
.finally(function() {
saveBtn.disabled = false;
saveBtn.textContent = _('Save');
});
});
enableCheckbox.addEventListener('change', function(ev) {
saveConfiguration(state.port, enableCheckbox.checked)
.then(function() {
state.enabled = enableCheckbox.checked;
updateStatus();
var message = enableCheckbox.checked ?
_('Auto start enabled') :
_('Auto start disabled');
})
.catch(function(err) {
enableCheckbox.checked = state.enabled;
});
});
statusSection.appendChild(E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Homebox')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Homebox service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Auto Start')),
E('div', {'class': 'cbi-value-field'}, [
enableCheckbox,
E('label', {'for': 'homebox_enable', 'style': 'margin-left: 5px;'})
])
]),
E('div', {'class': 'cbi-value' }, [
E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-field', 'style': 'align-items: center;'}, [
portInput,
saveBtn,
portError
])
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn)
])
]));
// Initial status check
checkProcess().then(res => {
checkProcess().then(function(res) {
state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus();
toggleBtn.disabled = false;
// Start polling
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
poll.add(function() {
return checkProcess().then(function(res) {
if (res.running !== state.running || (res.port && res.port !== state.port)) {
state.running = res.running;
if (res.port) {
state.port = res.port;
}
updateStatus();
toggleBtn.disabled = false;
}
});
}, 5);
poll.start();
});
return E('div', {}, [
statusSection,
container
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
}
});
@@ -9,7 +9,7 @@
var state = {
running: false,
port: null
port: 5201
};
const logPath = '/tmp/netspeedtest.log';
@@ -34,10 +34,9 @@ async function checkProcess() {
}
}
function controlService(action) {
function controlService(action, port) {
const commands = {
start: `/usr/bin/iperf3 -s -D -p 5201 --logfile ${logPath} 2>&1`,
start: `/usr/bin/iperf3 -s -D -p ${port} --logfile ${logPath} 2>&1`,
stop: '/usr/bin/killall -q iperf3'
};
@@ -50,45 +49,105 @@ function controlService(action) {
throw err;
});
}
function saveConfiguration(newPort, enabled) {
const uciContent = `config netspeedtest 'config'
\toption iperf3port '${newPort}'
\toption iperf3_enabled '${enabled ? '1' : '0'}'
`;
return fs.write('/etc/config/netspeedtest', uciContent)
.then(() => {
// 更新开机自启
if (enabled) {
return fs.exec('/etc/init.d/netspeedtest', ['enable']);
} else {
return fs.exec('/etc/init.d/netspeedtest', ['disable']);
}
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]);
return Promise.all([
uci.load('netspeedtest')
]).then(() => {
const port = uci.get('netspeedtest', 'config', 'iperf3port');
if (port) {
state.port = parseInt(port);
}
});
},
render: function() {
// 创建状态元素
const statusIcon = E('span', { 'style': 'margin-right: 5px;' });
const btnGroup = E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; gap: 10px;' });
const statusText = E('span');
const portInput = E('input', {
'type': 'number',
'class': 'cbi-input-text',
'style': 'width: 100px;',
'value': state.port,
'min': 1024,
'max': 65535,
'placeholder': '5201'
});
const enableCheckbox = E('input', {
'type': 'checkbox',
'id': 'iperf3_enable',
'class': 'cbi-input-checkbox',
'checked': state.enabled
});
const portError = E('span', {
'class': 'error',
'style': 'color: red; margin-left: 10px; display: none;'
}, _('Port range must be 1024-65535'));
const statusContainer = E('div', { 'class': 'cbi-map-descr' }, [
statusIcon,
statusText
]);
const toggleBtn = E('button', {
'class': 'btn cbi-button',
'click': ui.createHandlerFn(this, function() {
const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true; // 禁用按钮
return controlService(action)
.then(() => checkProcess())
.then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false; // 恢复按钮
})
.catch(err => {
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
toggleBtn.disabled = false; // 出错时也要恢复按钮
});
const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true;
controlService(action, state.port)
.then(() => {
return new Promise(resolve => setTimeout(resolve, 500));
})
.then(() => checkProcess())
.then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
})
.catch(err => {
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
toggleBtn.disabled = false;
});
})
});
const savePortBtn = E('button', {
'class': 'btn cbi-button cbi-button-apply',
'style': 'margin-left: 10px;',
'click': ui.createHandlerFn(this, function() {
const newPort = parseInt(portInput.value);
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
portError.style.display = 'inline';
return;
}
portError.style.display = 'none';
saveConfiguration(newPort,state.enabled)
})
}, _('Save'));
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
@@ -98,51 +157,54 @@ return view.extend({
statusText.style['font-size'] = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
portInput.value = state.port;
}
// 初始化状态
statusIcon.textContent = '...';
statusText.textContent = _('Checking status...');
toggleBtn.textContent = _('Loading...');
toggleBtn.disabled = true;
// 构建UI
const statusSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Iperf3')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Iperf3 service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')),
E('div', {'class': 'cbi-value-field'}, [
E('div', {
'class': 'cbi-value-field',
'style': 'display: flex;'
}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/luci-app-netspeedtest/releases', '_blank'))
}, _('GitHub'))
const statusSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Iperf3')),
statusContainer,
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Port Setting')),
E('div', {'class': 'cbi-value-field'}, [
portInput,
savePortBtn,
portError
])
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Service Control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
]),
E('div', {'class': 'cbi-value'}, [
E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')),
E('div', {'class': 'cbi-value-field'}, [
E('div', {
'class': 'cbi-value-field',
'style': 'display: flex;'
}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/netspeedtest/releases', '_blank'))
}, _('GitHub'))
])
])
])
])
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy/luci-app-netspeedtest',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
]);
// 初始化状态检查
checkProcess().then(res => {
@@ -161,8 +223,6 @@ const statusSection = E('div', { 'class': 'cbi-section' }, [
}, 5);
});
return statusSection;
}
});
}
});
@@ -78,21 +78,11 @@ return view.extend({
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
]),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 10px;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
}
},
// handleSaveApply: null,
// handleSave: null,
// handleReset: null
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -1,5 +1,5 @@
// Copyright (C) 2023-2025 muink
// Copyright (C) 2019-2026 sirpdboy
// Fixed version - better Ookla result parsing
'use strict';
'require view';
@@ -25,7 +25,6 @@ return view.extend({
]);
},
// 检测可用版本
detectVersions: function(res) {
var hasOokla = !!(res[0] && res[0].path);
var hasPython = !!(res[1] && res[1].path);
@@ -37,17 +36,14 @@ return view.extend({
};
},
// 检查是否真正在测试中(文件新鲜且内容为Testing)
isTesting: function(resultContent, resultMtime) {
if (!resultContent || resultContent.length === 0) return false;
if (resultContent[0].trim() !== 'Testing') return false;
// 检查文件是否在超时时间内
var fileAge = Date.now() - resultMtime;
return fileAge < Timeout;
},
// 解析结果内容
parseResult: function(content) {
if (!content || content.length === 0) {
return { type: 'none', data: null };
@@ -55,17 +51,13 @@ return view.extend({
var firstLine = content[0].trim();
// 检查是否是Testing状态
if (firstLine === 'Testing') {
return { type: 'testing', data: null };
}
// 检查是否是失败状态
if (firstLine === 'Test failed') {
return { type: 'failed', data: null };
}
// 检查是否包含Result URL (Ookla格式)
var resultUrl = null;
for (var i = 0; i < content.length; i++) {
var line = content[i];
@@ -82,18 +74,13 @@ return view.extend({
return { type: 'url', data: resultUrl };
}
// 检查是否是直接URL (Python格式)
if (firstLine.match(/^https?:\/\//) || firstLine.match(/\.png$/)) {
return { type: 'url', data: firstLine };
}
// 检查是否是速度数据
if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) {
return { type: 'speed', data: content.join('\n') };
}
// 其他情况,可能是完整的速度测试结果
// 检查是否包含Download和Upload信息
var hasDownload = false;
var hasUpload = false;
for (var j = 0; j < content.length; j++) {
@@ -114,18 +101,13 @@ return view.extend({
var result_stat = nodes.querySelector('#speedtest_result');
var start_btn = nodes.querySelector('.cbi-button-apply');
// 解析结果
var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime);
// 更新测试按钮状态
if (start_btn) {
start_btn.disabled = is_testing;
}
// 更新结果状态
if (result_stat) {
if (is_testing) {
result_stat.innerHTML = "<span style='color:green;font-weight:bold;margin-left:20px'>" +
@@ -212,7 +194,6 @@ return view.extend({
break;
case 'unknown':
// 未知格式,直接显示
result_stat.innerHTML = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>" +
"<strong>" + _('Test Results:') + "</strong><br>" +
"<pre style='margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'>" +
@@ -221,9 +202,8 @@ return view.extend({
break;
default:
// 无结果
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
"<em>" + _('No test results yet. Click "Start Speed Test" to begin.') + "</em>" +
"<em>" + _('No test results yet.') + "</em>" +
"</span>";
}
}
@@ -241,9 +221,8 @@ return view.extend({
var is_testing = this.isTesting(result_content, result_mtime);
var m, s, o;
m = new form.Map('netspeedtest', _('WAN SpeedTest'));
m = new form.Map('netspeedtest', _('Wan SpeedTest'));
// 结果显示区域
s = m.section(form.TypedSection, '_result');
s.anonymous = true;
s.render = function(section_id) {
@@ -353,7 +332,6 @@ return view.extend({
]);
case 'unknown':
// 未知格式,直接显示
return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, [
E('strong', {}, _('Test Results:')),
@@ -365,20 +343,17 @@ return view.extend({
]);
default:
// 无结果
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:gray;margin-left:20px' }, [
E('em', {}, _('No test results yet. Click "Start Speed Test" to begin.'))
E('em', {}, _('No test results yet.'))
])
]);
}
};
// 配置部分
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// 版本选择
o = s.option(form.ListValue, 'test_version', _('Select Test Version'));
if (has_ookla) {
@@ -404,7 +379,6 @@ return view.extend({
o.inputtitle = _('Click to start speed test');
o.inputstyle = 'apply';
// 只有真正在测试中才禁用按钮
if (is_testing) {
o.readonly = true;
}
@@ -413,17 +387,14 @@ return view.extend({
var btn = this;
btn.disabled = true;
// 获取选中的版本
var versionSelect = document.getElementById('widget.cbid.netspeedtest.config.test_version');
if (!versionSelect) {
versionSelect = document.querySelector('select[name="test_version"]');
}
var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python');
// 先写入Testing状态
return fs.write(ResultFile, 'Testing\n')
.then(function() {
// 在后台执行测试脚本
var cmd = 'nohup ' + SpeedtestScript;
if (version) {
cmd += ' --version ' + version;
@@ -445,9 +416,7 @@ return view.extend({
return m.render()
.then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime; // 保存结果文件修改时间
// 添加轮询 - 每2秒检查一次结果
nodes.result_mtime = result_mtime;
poll.add(L.bind(function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
@@ -469,7 +438,6 @@ return view.extend({
handleReset: null
});
// 辅助函数
function escapeHTML(str) {
if (!str) return '';
return String(str)