add: 添加 Speedtest.net 功能

This commit is contained in:
samlm0
2024-01-13 16:04:27 +00:00
parent b9af97d0ab
commit 821f8bbb5a
18 changed files with 451 additions and 59 deletions

View File

@@ -3,6 +3,7 @@ package als
import (
"log"
"github.com/samlm0/als/v2/als/client"
"github.com/samlm0/als/v2/als/timer"
"github.com/samlm0/als/v2/config"
alsHttp "github.com/samlm0/als/v2/http"
@@ -20,5 +21,6 @@ func Init() {
go timer.SetupInterfaceBroadcast()
}
go timer.UpdateSystemResource()
go client.HandleQueue()
aHttp.Start()
}

View File

@@ -7,38 +7,57 @@ import (
var queueLine = make(map[context.Context]context.CancelFunc, 0)
var queueLock = sync.Mutex{}
var hasQueueWorker = false
var queueStop = make(chan struct{})
func WaitQueue(ctx context.Context) {
var queueNotify = make(map[context.Context]func(), 0)
func WaitQueue(ctx context.Context, cb func()) {
queueCtx, cancel := context.WithCancel(ctx)
queueLine[ctx] = cancel
LISTEN:
queueLock.Lock()
if !hasQueueWorker {
hasQueueWorker = true
go handleQueue()
if cb != nil {
queueNotify[ctx] = cb
}
queueLock.Unlock()
select {
case <-queueCtx.Done():
case <-ctx.Done():
return
case <-queueStop:
go handleQueue()
goto LISTEN
}
}
func handleQueue() {
for ctx, notify := range queueLine {
notify()
<-ctx.Done()
delete(queueLine, ctx)
func GetQueuePostitionByCtx(ctx context.Context) (int, int) {
total := len(queueLine)
found := false
count := 0
queueLock.Lock()
for v, _ := range queueLine {
count++
if v == ctx {
found = true
break
}
}
queueLock.Unlock()
if !found {
return 0, 0
}
return count, total
}
func HandleQueue() {
for {
for ctx, notify := range queueLine {
notify()
<-ctx.Done()
delete(queueLine, ctx)
delete(queueNotify, ctx)
for _, callNotify := range queueNotify {
callNotify()
}
}
}
hasQueueWorker = false
<-queueStop
}

View File

@@ -37,7 +37,6 @@ func Handle(c *gin.Context) {
writer := func(pipe io.ReadCloser, err error) {
if err != nil {
fmt.Println("Pipe closed", err)
return
}
for {
@@ -68,11 +67,6 @@ func Handle(c *gin.Context) {
}
cmd.Wait()
// err = cmd.Wait()
// if err != nil {
// 处理错误
// fmt.Println("Error waiting for command:", err)
// }
c.JSON(200, &gin.H{
"success": true,

View File

@@ -59,7 +59,7 @@ func HandleFakeFile(c *gin.Context) {
return
}
client.WaitQueue(c.Request.Context())
client.WaitQueue(c.Request.Context(), nil)
filename = filename[0 : len(filename)-5]
if !contains(config.Config.SpeedtestFileList, filename) {

View File

@@ -0,0 +1,102 @@
package speedtest
import (
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/samlm0/als/v2/als/client"
)
var count = 1
var lock = sync.Mutex{}
func fakeQueue() {
go func() {
lock.Lock()
count++
lock.Unlock()
ctx, cancel := context.WithCancel(context.TODO())
client.WaitQueue(ctx, nil)
fmt.Println(count)
time.Sleep(time.Duration(count) * time.Second)
cancel()
}()
}
func HandleSpeedtestDotNet(c *gin.Context) {
nodeId, ok := c.GetQuery("node_id")
v, _ := c.Get("clientSession")
clientSession := v.(*client.ClientSession)
if !ok {
nodeId = ""
}
closed := false
timeout := time.Second * 60
count = 1
ctx, cancel := context.WithTimeout(clientSession.GetContext(c.Request.Context()), timeout)
defer func() {
cancel()
closed = true
}()
go func() {
<-ctx.Done()
closed = true
}()
client.WaitQueue(ctx, func() {
pos, totalPos := client.GetQueuePostitionByCtx(ctx)
msg, _ := json.Marshal(gin.H{"type": "queue", "pos": pos, "totalPos": totalPos})
if !closed {
clientSession.Channel <- &client.Message{
Name: "SpeedtestStream",
Content: string(msg),
}
}
})
args := []string{"--accept-license", "-f", "jsonl"}
if nodeId != "" {
args = append(args, "-s", nodeId)
}
cmd := exec.Command("speedtest", args...)
go func() {
<-ctx.Done()
if cmd.Process != nil {
cmd.Process.Kill()
}
}()
writer := func(pipe io.ReadCloser, err error) {
if err != nil {
fmt.Println("Pipe closed", err)
return
}
for {
buf := make([]byte, 1024)
n, err := pipe.Read(buf)
if err != nil {
return
}
if !closed {
clientSession.Channel <- &client.Message{
Name: "SpeedtestStream",
Content: string(buf[:n]),
}
}
}
}
go writer(cmd.StdoutPipe())
go writer(cmd.StderrPipe())
cmd.Run()
fmt.Println("speedtest-cli quit")
c.JSON(200, &gin.H{
"success": true,
})
}

View File

@@ -28,6 +28,10 @@ func SetupHttpRoute(e *gin.Engine) {
v1.GET("/ping", ping.Handle)
}
if config.Config.FeatureSpeedtestDotNet {
v1.GET("/speedtest_dot_net", speedtest.HandleSpeedtestDotNet)
}
if config.Config.FeatureIfaceTraffic {
v1.GET("/cache/interfaces", cache.UpdateInterfaceCache)
}

View File

@@ -10,11 +10,11 @@ import (
func UpdateSystemResource() {
var m runtime.MemStats
ticker := time.NewTicker(1 * time.Second)
ticker := time.NewTicker(5 * time.Second)
for {
<-ticker.C
runtime.ReadMemStats(&m)
client.BroadCastMessage("MemoryUsage", strconv.Itoa(int(m.Alloc)))
client.BroadCastMessage("MemoryUsage", strconv.Itoa(int(m.Sys)))
}
}

View File

@@ -27,14 +27,15 @@ type ALSConfig struct {
SponsorMessage string `json:"sponsor_message"`
FeaturePing bool `json:"feature_ping"`
FeatureShell bool `json:"feature_shell"`
FeatureLibrespeed bool `json:"feature_librespeed"`
FeatureFileSpeedtest bool `json:"feature_filespeedtest"`
FeatureIperf3 bool `json:"feature_iperf3"`
FeatureMTR bool `json:"feature_mtr"`
FeatureTraceroute bool `json:"feature_traceroute"`
FeatureIfaceTraffic bool `json:"feature_iface_traffic"`
FeaturePing bool `json:"feature_ping"`
FeatureShell bool `json:"feature_shell"`
FeatureLibrespeed bool `json:"feature_librespeed"`
FeatureFileSpeedtest bool `json:"feature_filespeedtest"`
FeatureSpeedtestDotNet bool `json:"feature_speedtest_dot_net"`
FeatureIperf3 bool `json:"feature_iperf3"`
FeatureMTR bool `json:"feature_mtr"`
FeatureTraceroute bool `json:"feature_traceroute"`
FeatureIfaceTraffic bool `json:"feature_iface_traffic"`
}
func GetDefaultConfig() *ALSConfig {
@@ -49,14 +50,15 @@ func GetDefaultConfig() *ALSConfig {
PublicIPv4: "",
PublicIPv6: "",
FeaturePing: true,
FeatureShell: true,
FeatureLibrespeed: true,
FeatureFileSpeedtest: true,
FeatureIperf3: true,
FeatureMTR: true,
FeatureTraceroute: true,
FeatureIfaceTraffic: true,
FeaturePing: true,
FeatureShell: true,
FeatureLibrespeed: true,
FeatureFileSpeedtest: true,
FeatureSpeedtestDotNet: true,
FeatureIperf3: true,
FeatureMTR: true,
FeatureTraceroute: true,
FeatureIfaceTraffic: true,
}
return defaultConfig
@@ -70,6 +72,7 @@ func Load() {
func LoadWebConfig() {
Load()
LoadSponsorMessage()
log.Default().Println("Loading config for web services...")
_, err := exec.LookPath("iperf3")
@@ -102,10 +105,11 @@ func LoadSponsorMessage() {
if err == nil {
content, err := io.ReadAll(resp.Body)
if err == nil {
log.Default().Println("Loaded sponser message from url.")
Config.SponsorMessage = string(content)
return
}
}
log.Default().Panicln("Failed to load sponsor message.")
log.Default().Println("ERROR: Failed to load sponsor message.")
}

View File

@@ -23,12 +23,13 @@ func LoadFromEnv() {
}
envVarsBool := map[string]*bool{
"DISPLAY_TRAFFIC": &Config.FeatureIfaceTraffic,
"ENABLE_SPEEDTEST": &Config.FeatureLibrespeed,
"UTILITIES_PING": &Config.FeaturePing,
"UTILITIES_FAKESHELL": &Config.FeatureShell,
"UTILITIES_IPERF3": &Config.FeatureIperf3,
"UTILITIES_MTR": &Config.FeatureMTR,
"DISPLAY_TRAFFIC": &Config.FeatureIfaceTraffic,
"ENABLE_SPEEDTEST": &Config.FeatureLibrespeed,
"UTILITIES_SPEEDTESTDOTNET": &Config.FeatureSpeedtestDotNet,
"UTILITIES_PING": &Config.FeaturePing,
"UTILITIES_FAKESHELL": &Config.FeatureShell,
"UTILITIES_IPERF3": &Config.FeatureIperf3,
"UTILITIES_MTR": &Config.FeatureMTR,
}
for envVar, configField := range envVarsString {

View File

@@ -23,6 +23,7 @@ func defineMenuCommands(a *console.Console) console.Commands {
"ping": config.Config.FeaturePing,
"traceroute": config.Config.FeatureTraceroute,
"nexttrace": config.Config.FeatureTraceroute,
"speedtest": config.Config.FeatureSpeedtestDotNet,
"mtr": config.Config.FeatureMTR,
}

View File

@@ -29,4 +29,6 @@ install_from_github(){
}
install_from_github "nxtrace" "Ntrace-V1" "/usr/local/bin/nexttrace" `fix_arch`
chmod +x "/usr/local/bin/nexttrace"
chmod +x "/usr/local/bin/nexttrace"
sh install-speedtest.sh

View File

@@ -0,0 +1,5 @@
#!/bin/sh
wget -O /tmp/speedtest.tgz https://install.speedtest.net/app/cli/ookla-speedtest-1.2.0-linux-`uname -m`.tgz
tar zxf /tmp/speedtest.tgz -C /tmp
mv /tmp/speedtest /usr/local/bin/speedtest
rm -rf /tmp/*

View File

@@ -12,7 +12,16 @@ const props = defineProps({
const isClicked = ref(false)
const message = useMessage()
const copy = async (value) => {
await navigator.clipboard.writeText(value)
try {
await navigator.clipboard.writeText(value)
} catch (error) {
const textarea = document.createElement('textarea')
document.body.appendChild(textarea)
textarea.textContent = value
textarea.select()
document?.execCommand('copy')
textarea.remove()
}
isClicked.value = true
if (!props['hideMessage']) {
message.info('已复制到剪贴板')

View File

@@ -35,3 +35,10 @@ const configKeyMap = {
</div>
</n-card>
</template>
<style>
.sponsor a {
color: #70c0e8;
text-decoration: none;
}
</style>

View File

@@ -24,6 +24,12 @@ const tools = ref([
enable: false,
componentNode: _v(() => import('./Utilities/IPerf3.vue'))
},
{
label: 'Speedtest.net',
show: false,
enable: false,
componentNode: _v(() => import('./Utilities/SpeedtestNet.vue'))
},
{
label: 'Shell',
show: false,
@@ -34,7 +40,7 @@ const tools = ref([
onMounted(() => {
for (var tool of tools.value) {
const configKey = 'feature_' + tool.label.toLowerCase()
const configKey = 'feature_' + tool.label.toLowerCase().replace('.', '_dot_')
console.log(configKey, config.value[configKey])
tool.enable = config.value[configKey] ?? false
}

View File

@@ -0,0 +1,234 @@
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { useAppStore } from '@/stores/app'
import { formatBytes } from '@/helper/unit'
let abortController = markRaw(new AbortController())
const appStore = useAppStore()
const working = ref(false)
const serverId = ref()
const isCrash = ref(false)
const isQueue = ref(false)
const isSpeedtest = ref(false)
const action = ref('')
const queueStat = ref({
pos: 0,
total: 0
})
const progress = ref({
sub: 0,
full: 0
})
const speedtestData = ref({
ping: '0',
download: '',
upload: '',
result: '',
serverInfo: {
id: '',
name: '',
pos: ''
}
})
const steps = ref({
start: false,
ping: false,
download: false,
upload: false,
end: false
})
const handleMessage = (e) => {
const data = JSON.parse(e.data)
console.log(data)
switch (data.type) {
case 'queue':
isQueue.value = true
queueStat.value.pos = data.pos
queueStat.value.total = data.totalPos
console.log(queueStat)
break
case 'testStart':
isQueue.value = false
isSpeedtest.value = true
steps.value.start = true
speedtestData.value.serverInfo.id = data.server.id
speedtestData.value.serverInfo.name = data.server.name
speedtestData.value.serverInfo.pos = data.server.country + ' - ' + data.server.location
break
case 'ping':
action.value = '测试延迟'
speedtestData.value.ping = data.ping.latency
break
case 'download':
action.value = '下载'
speedtestData.value.download = formatBytes(data.download.bandwidth, 2, true)
progress.value.sub = Math.round(data.download.progress * 100)
progress.value.full = Math.round(progress.value.sub / 2)
break
case 'upload':
action.value = '上传'
speedtestData.value.upload = formatBytes(data.upload.bandwidth, 2, true)
progress.value.sub = Math.round(data.upload.progress * 100)
progress.value.full = 50 + Math.round(progress.value.sub / 2)
break
case 'result':
speedtestData.value.result = data.result.url
speedtestData.value.download = formatBytes(data.download.bandwidth, 2, true)
speedtestData.value.upload = formatBytes(data.upload.bandwidth, 2, true)
break
}
}
const stopTest = () => {
abortController.abort('')
appStore.source.removeEventListener('SpeedtestStream', handleMessage)
isSpeedtest.value = false
}
const speedtest = async () => {
if (working.value) return false
abortController = new AbortController()
working.value = true
isSpeedtest.value = true
action.value = ''
isCrash.value = false
progress.value = {
sub: 0,
full: 0
}
speedtestData.value = {
ping: '0',
download: '',
upload: '',
result: '',
serverInfo: {
id: '',
name: '',
pos: ''
}
}
appStore.source.addEventListener('SpeedtestStream', handleMessage)
try {
await appStore.requestMethod(
'speedtest_dot_net',
{ node_id: serverId.value },
abortController.signal
)
} catch (e) {}
appStore.source.removeEventListener('SpeedtestStream', handleMessage)
working.value = false
}
onUnmounted(() => {
stopTest()
})
</script>
<template>
<n-space vertical>
<n-input-group>
<n-input
:disabled="working"
v-model:value="serverId"
:style="{ width: '90%' }"
placeholder="speedtest.net 服务器 ID (可空)"
@keyup.enter="speedtest"
/>
<n-button :loading="working" type="primary" ghost @click="speedtest()"> Run </n-button>
</n-input-group>
<n-collapse-transition :show="isQueue">
<n-spin>
<n-alert :show-icon="false" :bordered="false">
<br />
<br />
</n-alert>
<template #description>
测速请求正在排队中, 目前您在第 {{ queueStat.pos }} ( {{ queueStat.total }} )
</template>
</n-spin>
</n-collapse-transition>
<n-collapse-transition :show="!isQueue && isSpeedtest && action == '' && !isCrash">
<n-alert :show-icon="false" :bordered="false"> 测试很快开始... </n-alert>
</n-collapse-transition>
<n-collapse-transition :show="speedtestData.result != ''">
<n-alert :show-icon="false" :bordered="false">
<a :href="speedtestData.result" target="_blank">
<img
:src="speedtestData.result + '.png'"
style="max-width: 300px; height: 100%; display: flex; margin: auto"
/>
</a>
</n-alert>
</n-collapse-transition>
<n-collapse-transition :show="isSpeedtest && action != ''">
<n-collapse-transition :show="working">
<p>
{{ action }} - 进度
<span style="float: right">{{ progress.sub }}%</span>
</p>
<n-progress
type="line"
:percentage="progress.sub"
:show-indicator="false"
:processing="working"
/>
<p>
总进度 <span style="float: right">{{ progress.full }}%</span>
</p>
<n-progress
type="line"
:percentage="progress.full"
:show-indicator="false"
:processing="working"
/>
</n-collapse-transition>
<n-collapse-transition :show="isSpeedtest && speedtestData.serverInfo.id != ''">
<n-divider v-if="working" />
<n-table :bordered="true" :single-line="false">
<tbody>
<tr>
<td>服务器 ID</td>
<td>{{ speedtestData.serverInfo.id }}</td>
</tr>
<tr>
<td>服务器位置</td>
<td>{{ speedtestData.serverInfo.pos }}</td>
</tr>
<tr>
<td>服务器名称</td>
<td>{{ speedtestData.serverInfo.name }}</td>
</tr>
</tbody>
</n-table>
</n-collapse-transition>
<n-collapse-transition :show="isSpeedtest && speedtestData.ping != '0'">
<n-divider />
<n-table :bordered="true" :single-line="false">
<tbody>
<tr>
<td>延迟</td>
<td v-if="speedtestData.ping == '0'">等待开始</td>
<td v-else>{{ speedtestData.ping }} ms</td>
</tr>
<tr>
<td>下载速度</td>
<td v-if="speedtestData.download == ''">等待开始</td>
<td v-else>{{ speedtestData.download }}</td>
</tr>
<tr>
<td>上传速度</td>
<td v-if="speedtestData.upload == ''">等待开始</td>
<td v-else>{{ speedtestData.upload }}</td>
</tr>
</tbody>
</n-table>
</n-collapse-transition>
</n-collapse-transition>
</n-space>
</template>

View File

@@ -1,7 +1,7 @@
export const formatBytes = (bytes, decimals = 2, bandwidth = false) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
let k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const bandwidthSizes = ['Bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbs', 'Ebps', 'Zbps', 'Ybps']
@@ -9,7 +9,8 @@ export const formatBytes = (bytes, decimals = 2, bandwidth = false) => {
const i = Math.floor(Math.log(bytes) / Math.log(k))
if (bandwidth) {
bytes = bytes * 10
let k = 1000
bytes = bytes * 8
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + bandwidthSizes[i]
}
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]

View File

@@ -13,8 +13,8 @@ export const useAppStore = defineStore('app', () => {
const handleResize = () => {
let width = window.innerWidth
if (width > 650) {
drawerWidth.value = 650
if (width > 800) {
drawerWidth.value = 800
} else {
drawerWidth.value = width
}
@@ -34,6 +34,7 @@ export const useAppStore = defineStore('app', () => {
const eventSource = new EventSource('./session')
eventSource.addEventListener('SessionId', (e) => {
sessionId.value = e.data
console.log('session', e.data)
})
eventSource.addEventListener('Config', (e) => {
@@ -58,7 +59,7 @@ export const useAppStore = defineStore('app', () => {
const requestMethod = (method, data = {}, signal = null) => {
let axiosConfig = {
timeout: 1000 * 30, // 请求超时时间
timeout: 1000 * 120, // 请求超时时间
headers: {
session: sessionId.value
}