From 55585087846842ff8628e16e5062f6af7b1fbeb7 Mon Sep 17 00:00:00 2001 From: XZB-1248 Date: Sat, 9 Jul 2022 11:55:23 +0800 Subject: [PATCH] fix: deadlock when download more than one item --- CHANGELOG.md | 24 +++++--- client/service/file/file.go | 101 ++++++++++++++------------------- server/main.go | 1 + web/src/components/explorer.js | 86 +++++++++++++--------------- web/src/index.js | 4 +- web/src/locale/en.json | 1 + web/src/locale/zh-CN.json | 1 + web/src/pages/overview.js | 24 +++----- web/src/utils/utils.js | 17 +++++- 9 files changed, 127 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc5ede..f82bb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,18 @@ +## v0.1.3 + +* Optimize: basic operations for macOS. +* Fix: deadlock when download more than one item. + +* 优化:macOS下,基础操作改为API调用的方式实现。 +* 修复:下载目录或多文件时发生死锁,导致压缩文件不完整。 + + + ## v0.1.2 * Optimize: compress frontend assets. -* 优化: 压缩前端资源,加快加载速度。 +* 优化:压缩前端资源,加快加载速度。 @@ -15,10 +25,10 @@ * BREAKING-CHANGE: API `/device/file/get` parameter `file` changed to `files`. * BREAKING-CHANGE: API `/device/file/remove` parameter `file` changed to `files`. -* 新增: 文本文件编辑器。 -* 新增: 文件管理器多选。 -* 新增: 文件管理器过滤。 -* 修复: 一些潜在的bug。 +* 新增:文本文件编辑器。 +* 新增:文件管理器多选。 +* 新增:文件管理器过滤。 +* 修复:一些潜在的bug。 * 破坏性变动:API `/device/file/get` 参数 `file` 变为 `files`。 * 破坏性变动:API `/device/file/remove` 参数 `file` 变为 `files`。 @@ -49,8 +59,8 @@ * Add: file upload. * Optimize: project structure. -* 新增: 文件上传功能。 -* 优化: 项目结构。 +* 新增:文件上传功能。 +* 优化:项目结构。 diff --git a/client/service/file/file.go b/client/service/file/file.go index 47d5763..a6cb749 100644 --- a/client/service/file/file.go +++ b/client/service/file/file.go @@ -100,6 +100,19 @@ func FetchFile(dir, file, bridge string) error { return err } +func getTempFileName(dir, file string) string { + exists := true + tempFile := `` + for i := 0; exists; i++ { + tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i)) + _, err := os.Stat(tempFile) + if os.IsNotExist(err) { + exists = false + } + } + return tempFile +} + func RemoveFiles(files []string) error { for i := 0; i < len(files); i++ { if files[i] == `\` || files[i] == `/` || len(files[i]) == 0 { @@ -273,6 +286,10 @@ func uploadMulti(files []string, writer *io.PipeWriter, req *req.Request) error go func() { for _, subJob := range spare { lock.Lock() + if escape { + lock.Unlock() + break + } queue <- subJob lock.Unlock() } @@ -296,6 +313,10 @@ func uploadMulti(files []string, writer *io.PipeWriter, req *req.Request) error return err } lock.Lock() + if escape { + lock.Unlock() + break + } queue <- Job{stat, items[i], []string{stat.Name()}} lock.Unlock() } @@ -312,31 +333,36 @@ func uploadMulti(files []string, writer *io.PipeWriter, req *req.Request) error select { case job := <-queue: if escape { + // Try to get next job, to make locked producer unlock. + // Job is useless so there's no need to keep it. + lock.Lock() + if len(queue) > 0 { + _, _ = <-queue + } + lock.Unlock() break } handleJob(job) - if escape { - // Try to get next job, to make locking producer unlock. - // Escaping now, job is useless so there's no need to keep it. - _, _ = <-queue - } default: escape = true - _, _ = <-queue - break - } - if escape { lock.Lock() - close(queue) - lock.Unlock() - if len(fails) > 0 { - zipWriter.SetComment(`Those files could not be archived:` + "\n" + strings.Join(fails, "\n")) + if len(queue) > 0 { + _, _ = <-queue } - zipWriter.Close() - writer.Close() + lock.Unlock() break } } + if escape { + lock.Lock() + close(queue) + lock.Unlock() + if len(fails) > 0 { + zipWriter.SetComment(`Those files could not be archived:` + "\n" + strings.Join(fails, "\n")) + } + zipWriter.Close() + writer.Close() + } }() return nil } @@ -363,59 +389,20 @@ func UploadTextFile(path, bridge string) error { }) uploadReq.RawRequest.ContentLength = size - // Check file if is a text file. - // UTF-8 and GBK are only supported yet. + // Check file if is a text file with UTF-8 encoding. buf := make([]byte, size) _, err = file.Read(buf) if err != nil { return err } - if utf8.Valid(buf) { - uploadReq.SetHeader(`FileEncoding`, `UTF-8`) - } else if gbkValidate(buf) { - uploadReq.SetHeader(`FileEncoding`, `GBK`) - } else { + if !utf8.Valid(buf) { return errors.New(`${i18n|fileEncodingUnsupported}`) } - file.Seek(0, 0) url := config.GetBaseURL(false) + `/api/bridge/push` _, err = uploadReq. - SetBody(file). + SetBody(buf). SetQueryParam(`bridge`, bridge). Send(`PUT`, url) return err } - -func gbkValidate(b []byte) bool { - length := len(b) - var i int = 0 - for i < length { - if b[i] <= 0x7f { - i++ - continue - } else { - if i+1 < length { - if b[i] >= 0x81 && b[i] <= 0xfe && b[i+1] >= 0x40 && b[i+1] <= 0xfe && b[i+1] != 0xf7 { - i += 2 - continue - } - } - return false - } - } - return true -} - -func getTempFileName(dir, file string) string { - exists := true - tempFile := `` - for i := 0; exists; i++ { - tempFile = path.Join(dir, file+`.tmp.`+strconv.Itoa(i)) - _, err := os.Stat(tempFile) - if os.IsNotExist(err) { - exists = false - } - } - return tempFile -} diff --git a/server/main.go b/server/main.go index 0b923c6..0b6e2c5 100644 --- a/server/main.go +++ b/server/main.go @@ -65,6 +65,7 @@ func main() { gin.SetMode(gin.ReleaseMode) } app := gin.New() + app.Use(gin.Recovery()) if config.Config.Debug.Pprof { pprof.Register(app) } diff --git a/web/src/components/explorer.js b/web/src/components/explorer.js index 2d7ca8d..e4ce60d 100644 --- a/web/src/components/explorer.js +++ b/web/src/components/explorer.js @@ -13,8 +13,8 @@ import { Space, Spin } from "antd"; -import ProTable from "@ant-design/pro-table"; -import {formatSize, orderCompare, post, preventClose, request, translate, waitTime} from "../utils/utils"; +import ProTable, {TableDropdown} from "@ant-design/pro-table"; +import {catchBlobReq, formatSize, orderCompare, post, preventClose, request, translate, waitTime} from "../utils/utils"; import dayjs from "dayjs"; import i18n from "../locale/locale"; import {VList} from "virtuallist-antd"; @@ -134,34 +134,40 @@ function FileBrowser(props) { }, [props.device, props.visible]); function renderOperation(file) { - const remove = ( - removeFiles(file.name)} + let menus = [ + {key: 'delete', name: i18n.t('delete')}, + {key: 'editAsText', name: i18n.t('editAsText')}, + ]; + if (file.type === 1) { + menus.pop(); + } else if (file.type === 2) { + return []; + } + if (file.name === '..') { + return []; + } + return [ + downloadFiles(file.name)} > - {i18n.t('delete')} - - ); - switch (file.type) { - case 0: - return [ - downloadFiles(file.name)} - >{i18n.t('download')}, - remove, - ]; - case 1: - return [remove]; - case 2: - return []; + {i18n.t('download')} + , + onDropdownSelect(key, file)} + menus={menus} + />, + ]; + } + function onDropdownSelect(key, file) { + switch (key) { + case 'delete': + removeFiles(file.name); + break; + case 'editAsText': + textEdit(file); } - return []; } function onRowClick(file) { const separator = props.isWindows ? '\\' : '/'; @@ -202,21 +208,13 @@ function FileBrowser(props) { responseType: 'blob', timeout: 10000 }).then(res => { - if (res.status !== 200) { - res.data.text().then((str) => { - let data = {}; - try { - data = JSON.parse(str); - } catch (e) { } - message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed')); - }); - } else { + if (res.status === 200) { if (preview.length > 0) { URL.revokeObjectURL(preview); } setPreview(URL.createObjectURL(res.data)); } - }).finally(() => { + }).catch(catchBlobReq).finally(() => { setLoading(false); }); } @@ -229,22 +227,14 @@ function FileBrowser(props) { responseType: 'blob', timeout: 7000 }).then(res => { - if (res.status !== 200) { - res.data.text().then(str => { - let data = {}; - try { - data = JSON.parse(str); - } catch (e) { } - message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed')); - }); - } else { + if (res.status === 200) { res.data.text().then(str => { setEditingContent(str); setDraggable(false); setEditingFile(file.name); }); } - }).finally(() => { + }).catch(catchBlobReq).finally(() => { setLoading(false); }); } diff --git a/web/src/index.js b/web/src/index.js index cf533a5..fd5c13f 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -13,7 +13,7 @@ import Overview from "./pages/overview"; import {translate} from "./utils/utils"; axios.defaults.baseURL = '.'; -axios.interceptors.response.use(async (res) => { +axios.interceptors.response.use(async res => { let data = res.data; if (data.hasOwnProperty('code')) { if (data.code !== 0){ @@ -26,7 +26,7 @@ axios.interceptors.response.use(async (res) => { } } return Promise.resolve(res); -}, (err) => { +}, err => { // console.error(err); if (err.code === 'ECONNABORTED') { message.error(i18n.t('requestTimeout')); diff --git a/web/src/locale/en.json b/web/src/locale/en.json index 224b687..761b40c 100644 --- a/web/src/locale/en.json +++ b/web/src/locale/en.json @@ -48,6 +48,7 @@ "upload": "Upload", "delete": "Delete", "download": "Download", + "editAsText": "Edit as text", "uploading": "Uploading...", "uploadFailed": "Upload Failed", "uploadAborted": "Upload Aborted", diff --git a/web/src/locale/zh-CN.json b/web/src/locale/zh-CN.json index 718c49e..fa7435e 100644 --- a/web/src/locale/zh-CN.json +++ b/web/src/locale/zh-CN.json @@ -49,6 +49,7 @@ "upload": "上传", "delete": "删除", "download": "下载", + "editAsText": "编辑文本", "uploading": "上传中...", "uploadFailed": "上传失败", "uploadAborted": "取消上传", diff --git a/web/src/pages/overview.js b/web/src/pages/overview.js index 5a6604b..43fbb0d 100644 --- a/web/src/pages/overview.js +++ b/web/src/pages/overview.js @@ -1,7 +1,7 @@ import React, {useEffect, useRef, useState} from 'react'; import ProTable, {TableDropdown} from '@ant-design/pro-table'; import {Button, Image, message, Modal, Progress, Tooltip} from 'antd'; -import {formatSize, request, translate, tsToTime, waitTime} from "../utils/utils"; +import {catchBlobReq, formatSize, request, translate, tsToTime, waitTime} from "../utils/utils"; import Terminal from "../components/terminal"; import ProcMgr from "../components/procmgr"; import Generate from "../components/generate"; @@ -284,10 +284,12 @@ function overview(props) { { setExplorer(device.id); setIsWindows(device.os === 'windows'); - }}>{i18n.t('fileMgr')}, + }}> + {i18n.t('fileMgr')} + , callDevice(key, device)} + onSelect={key => callDevice(key, device)} menus={menus} />, ] @@ -297,7 +299,7 @@ function overview(props) { if (act === 'screenshot') { request('/api/device/screenshot/get', {device: device.id}, {}, { responseType: 'blob' - }).then((res) => { + }).then(res => { console.log(res.data.type); if ((res.data.type ?? '').substring(0, 5) === 'image') { if (screenBlob.length > 0) { @@ -305,19 +307,7 @@ function overview(props) { } setScreenBlob(URL.createObjectURL(res.data)); } - }).catch((e) => { - let res = e.response; - if ((res?.data?.type ?? '').substring(0, 16) === 'application/json') { - let data = res?.data ?? {}; - data.text().then((str) => { - let data = {}; - try { - data = JSON.parse(str); - } catch (e) { } - message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed')); - }); - } - }); + }).catch(catchBlobReq); return; } Modal.confirm({ diff --git a/web/src/utils/utils.js b/web/src/utils/utils.js index 01f6d5d..88fefde 100644 --- a/web/src/utils/utils.js +++ b/web/src/utils/utils.js @@ -1,6 +1,7 @@ import axios from "axios"; import Qs from "qs"; import i18n, {getLang} from "../locale/locale"; +import {message} from "antd"; let orderCompare; try { @@ -99,4 +100,18 @@ function preventClose(e) { return ''; } -export {post, request, waitTime, formatSize, tsToTime, getBaseURL, translate, preventClose, orderCompare}; \ No newline at end of file +function catchBlobReq(err) { + let res = err.response; + if ((res?.data?.type ?? '').startsWith('application/json')) { + let data = res?.data ?? {}; + data.text().then((str) => { + let data = {}; + try { + data = JSON.parse(str); + } catch (e) { } + message.warn(data.msg ? translate(data.msg) : i18n.t('requestFailed')); + }); + } +} + +export {post, request, waitTime, formatSize, tsToTime, getBaseURL, translate, preventClose, catchBlobReq, orderCompare}; \ No newline at end of file