diff --git a/homebox/Makefile b/homebox/Makefile index b2b47519..e10bd5a4 100644 --- a/homebox/Makefile +++ b/homebox/Makefile @@ -1,41 +1,44 @@ # SPDX-License-Identifier: GPL-3.0-only -# Copyright (c) 2020-2024 sirpdboy herboy2008@gmail.com +# Copyright (c) 2022-2026 sirpdboy herboy2008@gmail.com # include $(TOPDIR)/rules.mk PKG_NAME:=homebox -PKG_VERSION:=0.0.0.2024101306 -PKG_REAL_VER:=0.0.0-dev.2024101306 -PKG_RELEASE:=3 - -ifeq ($(ARCH),aarch64) - H_ARCH:=arm64 - -else ifeq ($(ARCH),arm) - H_ARCH:=arm +PKG_VERSION:=1.0.1 +PKG_RELEASE:=4 +ifneq ($(filter aarch64%,$(ARCH)),) + PKG_ARCH:=arm64 + PKG_HASH:=skip +else ifneq ($(filter arm%,$(ARCH)),) + PKG_ARCH:=armv7 + PKG_HASH:=skip else ifeq ($(ARCH),i386) - H_ARCH:=386 - -else ifeq ($(ARCH),mips) - H_ARCH:=mips - -else ifeq ($(ARCH),mipsel) - H_ARCH:=mips - + PKG_ARCH:=386 + PKG_HASH:=skip else ifeq ($(ARCH),x86_64) - H_ARCH:=amd64 + PKG_ARCH:=amd64 + PKG_HASH:=skip +else + $(error Unsupported architecture: $(ARCH)) endif +PKG_SOURCE:=homebox-linux-$(PKG_ARCH)-musl-v$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://github.com/sirpdboy/homebox/releases/download/v$(PKG_VERSION)/ + +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) + include $(INCLUDE_DIR)/package.mk define Package/$(PKG_NAME) - SECTION:=net - CATEGORY:=Network - TITLE:=A Toolbox for Home Local Networks Speed Test - URL:=https://github.com/XGHeaven/homebox - DEPENDS:=@(i386||x86_64||arm||aarch64||mipsel||mips) +libstdcpp + SECTION:=net + CATEGORY:=Network + SUBMENU:=Speed Test + TITLE:=Home Local Network Speed Test Tool + URL:=https://github.com/XGHeaven/homebox + DEPENDS:=@(i386||x86_64||arm||aarch64) + MAINTAINER:=sirpdboy endef define Package/$(PKG_NAME)/description @@ -43,9 +46,11 @@ define Package/$(PKG_NAME)/description endef define Build/Prepare - mkdir -p $(PKG_BUILD_DIR) - [ ! -f $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz ] && wget https://github.com/XGHeaven/homebox/releases/download/v$(PKG_REAL_VER)/server-linux-$(H_ARCH).tar.gz -O $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz - tar -xzvf $(PKG_BUILD_DIR)/server-linux-$(H_ARCH).tar.gz -C $(PKG_BUILD_DIR) + ( \ + pushd $(PKG_BUILD_DIR) ; \ + $(TAR) -zxf $(DL_DIR)/$(PKG_SOURCE) -C . ; \ + popd ; \ + ) endef define Build/Compile @@ -53,7 +58,7 @@ endef define Package/$(PKG_NAME)/install $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_BIN) $(PKG_BUILD_DIR)/server-linux-$(H_ARCH) $(1)/usr/bin/homebox + $(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin/homebox endef -$(eval $(call BuildPackage,$(PKG_NAME))) +$(eval $(call BuildPackage,$(PKG_NAME))) \ No newline at end of file diff --git a/luci-app-netspeedtest/Makefile b/luci-app-netspeedtest/Makefile index 936a8646..23ead72c 100644 --- a/luci-app-netspeedtest/Makefile +++ b/luci-app-netspeedtest/Makefile @@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-netspeedtest -PKG_VERSION:=5.1.3 -PKG_RELEASE:=2 +PKG_VERSION:=5.2.0 +PKG_RELEASE:=3 LUCI_TITLE:=LuCI Support for netspeedtest LUCI_DEPENDS:=+ookla-speedtest +homebox +python3-light +python3-pkg-resources +python3-xml +python3-email \ diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js index fca09a1a..54f53392 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/homebox.js @@ -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 + } }); \ No newline at end of file diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js index d00301c5..b6433020 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/iperf3.js @@ -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; - } - -}); + } +}); \ No newline at end of file diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/logs.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/logs.js index 55efcf30..67e60d80 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/logs.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/logs.js @@ -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 }); \ No newline at end of file diff --git a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js index a4ad65fc..1bfcd0f8 100644 --- a/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js +++ b/luci-app-netspeedtest/htdocs/luci-static/resources/view/netspeedtest/wanspeedtest.js @@ -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 = "" + @@ -212,7 +194,6 @@ return view.extend({ break; case 'unknown': - // 未知格式,直接显示 result_stat.innerHTML = "
" + "" + _('Test Results:') + "
" + "
" + 
@@ -221,9 +202,8 @@ return view.extend({
                     break;
                     
                 default:
-                    // 无结果
                     result_stat.innerHTML = "" +
-                        "" + _('No test results yet. Click "Start Speed Test" to begin.') + "" +
+                        "" + _('No test results yet.') + "" +
                         "";
             }
         }
@@ -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)
diff --git a/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po b/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po
index c0e9496e..2bd1d491 100644
--- a/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po
+++ b/luci-app-netspeedtest/po/zh_Hans/netspeedtest.po
@@ -70,41 +70,29 @@ msgstr "Iperf3服务端"
 msgid "Command failed"
 msgstr "命令无效"
 
-msgid "Failed to read log file"
-msgstr "读取日志失败"
-
-msgid "No log content available"
-msgstr "没有日志记录"
 
 msgid "Error: "
 msgstr "错误: "
 
-msgid "Iperf3 service control"
-msgstr "Iperf3 服务控制"
+msgid "Service Control"
+msgstr "服务控制"
 
-msgid "Listen Port"
-msgstr "监听端口"
 
-msgid "Invalid format. Use [::]:port or ip:port"
-msgstr "格式错误.如 [::]:端口 或 ip:端口"
-
-msgid "Enable Log View"
-msgstr "开启日志显示"
 
 msgid "Download iperf3 client"
 msgstr "下载iperf3客户端"
 
-msgid "Run Log"
-msgstr "运行日志"
+msgid "Refresh every %s seconds."
+msgstr "每 %s 秒刷新"
 
-msgid "Refresh Log"
-msgstr "刷新日志"
+msgid "Clear logs"
+msgstr "清除日志"
 
 msgid "Official Website"
 msgstr "官方网站"
 
-msgid "Please enable the Homebox service"
-msgstr "请将homebox服务启用"
+msgid "Please start the Homebox service"
+msgstr "请启动homebox服务"
 
 msgid "Homebox service control"
 msgstr "Homebox服务控制"
@@ -145,6 +133,18 @@ msgstr "由于浏览器安全策略,Homebox接口https不能直接嵌入。"
 msgid "Select speed measurement station"
 msgstr "选择测速站点"
 
+msgid "Iperf3 Port"
+msgstr "Iperf3监听端口"
+
+msgid "Port range must be 1024-65535"
+msgstr "端口范围1024-65535"
+
+msgid "Auto Start"
+msgstr "自动启动"
+
+msgid "Port Setting"
+msgstr "监听端口"
+
 msgid ""
 msgstr ""
 
diff --git a/luci-app-netspeedtest/root/etc/init.d/netspeedtest b/luci-app-netspeedtest/root/etc/init.d/netspeedtest
new file mode 100755
index 00000000..b403f4cb
--- /dev/null
+++ b/luci-app-netspeedtest/root/etc/init.d/netspeedtest
@@ -0,0 +1,64 @@
+#!/bin/sh /etc/rc.common
+# netspeedtest init script
+
+START=95
+STOP=15
+USE_PROCD=1
+
+PATH=/usr/bin:/bin:/usr/sbin:/sbin
+
+start_service() {
+    
+    START_IPERF3=$(uci get netspeedtest.config.iperf3_enabled 2>/dev/null || echo "0")
+    START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
+    
+    [ "$START_HOMEBOX" = "1" ] || return 0 
+    port=$(uci get netspeedtest.config.homebox_port 2>/dev/null || echo "3300")
+
+
+    procd_open_instance
+    procd_set_param command /usr/bin/homebox serve --port "$port"
+    procd_set_param stdout 1
+    procd_set_param stderr 1
+    procd_set_param respawn
+    procd_close_instance
+}
+
+stop_service() {
+    logger -t netspeedtest "Stopping homebox"
+    
+    ubus call service delete '{ "name": "netspeedtest" }' 2>/dev/null
+    
+    killall homebox 2>/dev/null || true
+
+    local retries=5
+    while pidof homebox >/dev/null 2>&1 && [ $retries -gt 0 ]; do
+        sleep 0.2
+        retries=$((retries-1))
+    done
+}
+
+service_triggers() {
+    procd_add_reload_trigger "netspeedtest"
+}
+
+reload_service() {
+    stop
+    start
+}
+
+status() {
+
+    START_HOMEBOX=$(uci get netspeedtest.config.homebox_enabled 2>/dev/null || echo "0")
+    
+    if [ "$START_HOMEBOX" != "1" ]; then
+        return 3
+    fi
+
+    _procd_status
+}
+
+boot() {
+    sleep 2
+    start
+}
diff --git a/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest b/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest
deleted file mode 100755
index 45970efa..00000000
--- a/luci-app-netspeedtest/root/usr/libexec/rpcd/luci.netspeedtest
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/bin/sh
-# Copyright (C) 2019-2026 sirpdboy
-# RPC plugin for netspeedtest
-
-. /usr/share/libubox/jshn.sh
-
-get_ookla_servers() {
-    local servers="[]"
-    
-    if [ -f '/usr/bin/ookla-speedtest' ] && [ -x '/usr/bin/ookla-speedtest' ]; then
-        local json_output=$(/usr/bin/ookla-speedtest --servers --format=json 2>/dev/null)
-        
-        if [ -n "$json_output" ]; then
-            servers=$(echo "$json_output" | sed -n 's/.*"servers":\(\[.*\]\).*/\1/p')
-            [ -z "$servers" ] && servers="[]"
-        fi
-    fi
-    
-    echo "$servers"
-}
-
-get_python_servers() {
-    local servers="[]"
-    local temp_file="/tmp/python_servers.tmp"
-    
-    if [ -f '/usr/bin/speedtest' ] && [ -x '/usr/bin/speedtest' ]; then
-        /usr/bin/speedtest --list > "$temp_file" 2>/dev/null
-        
-        if [ -s "$temp_file" ]; then
-            local json=""
-            local count=0
-            
-            while IFS= read -r line; do
-                if echo "$line" | grep -qE '^[0-9]+\)'; then
-                    local id=$(echo "$line" | sed -E 's/^([0-9]+)\).*/\1/')
-                    local sponsor=$(echo "$line" | sed -E 's/^[0-9]+\)[[:space:]]+([^(]+).*/\1/' | sed -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
-                    local city_country=$(echo "$line" | grep -o '([^)]*)' | head -1 | sed 's/[()]//g')
-                    local city=$(echo "$city_country" | cut -d',' -f1 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
-                    local country=$(echo "$city_country" | cut -d',' -f2 | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/"/\\"/g')
-                    local distance=$(echo "$line" | grep -o '\[[0-9.]* km\]' | sed 's/\[//g' | sed 's/ km\]//g')
-                    
-                    [ -z "$distance" ] && distance="0"
-                    
-                    if [ $count -gt 0 ]; then
-                        json="$json,"
-                    fi
-                    
-                    json="$json{\"id\":$id,\"sponsor\":\"$sponsor\",\"city\":\"$city\",\"country\":\"$country\",\"distance\":$distance}"
-                    count=$((count + 1))
-                    
-                    [ $count -ge 10 ] && break
-                fi
-            done < "$temp_file"
-            
-            [ -n "$json" ] && servers="[$json]"
-        fi
-        rm -f "$temp_file"
-    fi
-    
-    echo "$servers"
-}
-
-case "$1" in
-    list)
-        echo '{
-            "get_servers": {
-                "signature": [],
-                "help": "Get speedtest servers"
-            }
-        }'
-        ;;
-    call)
-        case "$2" in
-            get_servers)
-                json_init
-                json_add_string "ookla" "$(get_ookla_servers)"
-                json_add_string "python" "$(get_python_servers)"
-                json_dump
-                ;;
-            *)
-                echo '{"error": "Method not found"}'
-                ;;
-        esac
-        ;;
-    *)
-        echo '{"error": "Invalid command"}'
-        ;;
-esac
\ No newline at end of file
diff --git a/ookla-speedtest/Makefile b/ookla-speedtest/Makefile
index 1c6d4207..049847a2 100644
--- a/ookla-speedtest/Makefile
+++ b/ookla-speedtest/Makefile
@@ -1,51 +1,63 @@
+#
+# Copyright (C) 2024 sbwml 
+# Copyright (c) 2025-2026 sirpdboy 
+#
+# This is free software, licensed under the GPL-3.0 License.
+#
+
 include $(TOPDIR)/rules.mk
 
 PKG_NAME:=ookla-speedtest
 PKG_VERSION:=1.2.0
-PKG_RELEASE:=2
+PKG_RELEASE:=3
 
 ifeq ($(ARCH),aarch64)
+  PKG_ARCH_SUFFIX:=aarch64
   PKG_HASH:=skip
 else ifeq ($(ARCH),arm)
-  ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
+    ARM_CPU_FEATURES:=$(word 2,$(subst +,$(space),$(call qstrip,$(CONFIG_CPU_TYPE))))
   ifeq ($(ARM_CPU_FEATURES),)
-    ARCH:=armel
+    PKG_ARCH_SUFFIX:=armel
     PKG_HASH:=skip
   else
-    ARCH:=armhf
+    PKG_ARCH_SUFFIX:=armhf
     PKG_HASH:=skip
   endif
 else ifeq ($(ARCH),i386)
+  PKG_ARCH_SUFFIX:=i386
   PKG_HASH:=skip
 else ifeq ($(ARCH),x86_64)
+  PKG_ARCH_SUFFIX:=x86_64
   PKG_HASH:=skip
 endif
 
-PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz
+PKG_SOURCE:=ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz
 PKG_SOURCE_URL:=https://install.speedtest.net/app/cli
-
-PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
+PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)-$(PKG_ARCH_SUFFIX)
 
 PKG_MAINTAINER:=sbwml 
-
 include $(INCLUDE_DIR)/package.mk
 
 define Package/$(PKG_NAME)
 	SECTION:=net
 	CATEGORY:=Network
+	SUBMENU:=Speed Test
 	TITLE:=Speedtest CLI by Ookla
+	URL:=https://www.speedtest.net/
 	DEPENDS:=@(aarch64||arm||i386||x86_64) +ca-certificates
 	URL:=https://www.speedtest.net/
 endef
 
 define Package/$(PKG_NAME)/description
-  The Global Broadband Speed Test
+  Speedtest CLI by Ookla is the official command line client
+  for testing internet bandwidth using speedtest.net servers.
 endef
 
+
 define Build/Prepare
 	( \
 		pushd $(PKG_BUILD_DIR) ; \
-			$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(ARCH).tgz -C . ; \
+			$(TAR) -zxf $(DL_DIR)/ookla-speedtest-$(PKG_VERSION)-linux-$(PKG_ARCH_SUFFIX).tgz -C . ; \
 		popd ; \
 	)
 endef
@@ -58,4 +70,4 @@ define Package/$(PKG_NAME)/install
 	$(INSTALL_BIN) $(PKG_BUILD_DIR)/speedtest $(1)/usr/bin/ookla-speedtest
 endef
 
-$(eval $(call BuildPackage,$(PKG_NAME)))
+$(eval $(call BuildPackage,$(PKG_NAME)))
\ No newline at end of file