@electerm/electerm-react 3.11.12 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/common/bookmark-schemas.js +2 -1
- package/client/common/constants.js +11 -2
- package/client/common/is-absolute-path.js +1 -1
- package/client/common/normalize-remote-path.js +20 -0
- package/client/common/resolve.js +16 -0
- package/client/components/ai/agent-tools.js +204 -0
- package/client/components/ai/agent.js +12 -10
- package/client/components/ai/ai-chat-history-item.jsx +15 -25
- package/client/components/ai/ai-chat.jsx +24 -9
- package/client/components/bookmark-form/bookmark-schema.js +2 -1
- package/client/components/bookmark-form/config/serial.js +3 -2
- package/client/components/footer/cmd-history.jsx +20 -11
- package/client/components/main/main.jsx +1 -1
- package/client/components/sftp/address-bar.jsx +22 -6
- package/client/components/sftp/file-item.jsx +31 -1
- package/client/components/sftp/file-read.js +11 -2
- package/client/components/sftp/sftp-entry.jsx +38 -3
- package/client/components/shortcuts/shortcut-handler.js +7 -0
- package/client/components/sidebar/history.jsx +16 -8
- package/client/components/sys-menu/icons-map.jsx +10 -2
- package/client/components/terminal/attach-addon-custom.js +1 -1
- package/client/components/terminal/drop-file-modal.jsx +53 -22
- package/client/components/terminal/terminal-apis.js +9 -0
- package/client/components/terminal/terminal.jsx +179 -3
- package/client/components/terminal/xmodem-client.js +244 -0
- package/client/components/terminal-info/base.jsx +41 -38
- package/client/components/terminal-info/data-cols-parser.jsx +2 -1
- package/client/components/terminal-info/disk.jsx +4 -2
- package/client/components/terminal-info/log-path-edit.jsx +3 -2
- package/client/components/terminal-info/network.jsx +3 -1
- package/client/components/terminal-info/resource.jsx +3 -3
- package/client/components/tree-list/tree-list.styl +7 -1
- package/client/store/mcp-handler.js +41 -9
- package/client/store/quick-command.js +3 -2
- package/client/store/watch.js +1 -1
- package/package.json +1 -1
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
Dropdown
|
|
8
8
|
} from 'antd'
|
|
9
9
|
import message from '../common/message'
|
|
10
|
+
import { notification } from '../common/notification'
|
|
11
|
+
import ShowItem from '../common/show-item.jsx'
|
|
10
12
|
import Modal from '../common/modal'
|
|
11
13
|
import classnames from 'classnames'
|
|
12
14
|
import './terminal.styl'
|
|
@@ -26,13 +28,15 @@ import AttachAddon from './attach-addon-custom.js'
|
|
|
26
28
|
import getProxy from '../../common/get-proxy.js'
|
|
27
29
|
import { ZmodemClient } from './zmodem-client.js'
|
|
28
30
|
import { TrzszClient } from './trzsz-client.js'
|
|
31
|
+
import { XmodemClient } from './xmodem-client.js'
|
|
29
32
|
import DropFileModal from './drop-file-modal.jsx'
|
|
30
33
|
import keyControlPressed from '../../common/key-control-pressed.js'
|
|
31
34
|
import NormalBuffer from './normal-buffer.jsx'
|
|
32
|
-
import { createTerm, resizeTerm } from './terminal-apis.js'
|
|
35
|
+
import { createTerm, resizeTerm, startTerminalLogFile, toggleTerminalLog } from './terminal-apis.js'
|
|
33
36
|
import { shortcutExtend, shortcutDescExtend } from '../shortcuts/shortcut-handler.js'
|
|
34
37
|
import { KeywordHighlighterAddon } from './highlight-addon.js'
|
|
35
38
|
import { getFilePath, isUnsafeFilename } from '../../common/file-drop-utils.js'
|
|
39
|
+
import { getFolderFromFilePath } from '../sftp/file-read.js'
|
|
36
40
|
import { CommandTrackerAddon } from './command-tracker-addon.js'
|
|
37
41
|
import AIIcon from '../icons/ai-icon.jsx'
|
|
38
42
|
import {
|
|
@@ -71,6 +75,9 @@ class Term extends Component {
|
|
|
71
75
|
saveTerminalLogToFile: !!this.props.config.saveTerminalLogToFile,
|
|
72
76
|
addTimeStampToTermLog: !!this.props.config.addTimeStampToTermLog,
|
|
73
77
|
logPath: this.props.config.sessionLogPath || createDefaultLogPath(),
|
|
78
|
+
logFileName: '',
|
|
79
|
+
recording: false,
|
|
80
|
+
recordingFilePath: '',
|
|
74
81
|
passType: 'password',
|
|
75
82
|
lines: [],
|
|
76
83
|
searchResults: [],
|
|
@@ -165,6 +172,7 @@ class Term extends Component {
|
|
|
165
172
|
this.fitAddon = null
|
|
166
173
|
this.zmodemClient = null
|
|
167
174
|
this.trzszClient = null
|
|
175
|
+
this.xmodemClient = null
|
|
168
176
|
this.searchAddon = null
|
|
169
177
|
this.fitAddon = null
|
|
170
178
|
this.cmdAddon = null
|
|
@@ -391,6 +399,7 @@ class Term extends Component {
|
|
|
391
399
|
const fromFile = dt.getData('fromFile')
|
|
392
400
|
const notSafeMsg = 'File name contains unsafe characters'
|
|
393
401
|
const isSshTerminal = this.props.tab.type === connectionMap.ssh
|
|
402
|
+
const isSerialTerminal = this.props.tab.type === connectionMap.serial
|
|
394
403
|
|
|
395
404
|
if (fromFile) {
|
|
396
405
|
try {
|
|
@@ -412,6 +421,13 @@ class Term extends Component {
|
|
|
412
421
|
}
|
|
413
422
|
return
|
|
414
423
|
}
|
|
424
|
+
if (isSerialTerminal) {
|
|
425
|
+
this.setState({
|
|
426
|
+
dropFileModalVisible: true,
|
|
427
|
+
droppedFiles: [{ path: filePath, isRemote: false }]
|
|
428
|
+
})
|
|
429
|
+
return
|
|
430
|
+
}
|
|
415
431
|
this.attachAddon._sendData(`"${filePath}" `)
|
|
416
432
|
return
|
|
417
433
|
} catch (e) {
|
|
@@ -443,6 +459,14 @@ class Term extends Component {
|
|
|
443
459
|
return
|
|
444
460
|
}
|
|
445
461
|
|
|
462
|
+
if (isSerialTerminal) {
|
|
463
|
+
this.setState({
|
|
464
|
+
dropFileModalVisible: true,
|
|
465
|
+
droppedFiles: filePaths.map(path => ({ path, isRemote: false }))
|
|
466
|
+
})
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
446
470
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
447
471
|
this.attachAddon._sendData(filesAll)
|
|
448
472
|
}
|
|
@@ -485,6 +509,19 @@ class Term extends Component {
|
|
|
485
509
|
this.attachAddon._sendData('rz\r')
|
|
486
510
|
break
|
|
487
511
|
}
|
|
512
|
+
case 'xmodem': {
|
|
513
|
+
if (this.xmodemClient && this.xmodemClient.isActive) {
|
|
514
|
+
message.warning('A transfer is already in progress')
|
|
515
|
+
this.handleDropFileModalCancel()
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
// Use XMODEM send with the dropped files
|
|
519
|
+
window._apiControlSelectFile = filePaths
|
|
520
|
+
if (this.xmodemClient) {
|
|
521
|
+
this.xmodemClient.initiateSend()
|
|
522
|
+
}
|
|
523
|
+
break
|
|
524
|
+
}
|
|
488
525
|
case 'inputOnly':
|
|
489
526
|
default: {
|
|
490
527
|
const filesAll = filePaths.map(path => `"${path}"`).join(' ')
|
|
@@ -645,15 +682,109 @@ class Term extends Component {
|
|
|
645
682
|
)
|
|
646
683
|
}
|
|
647
684
|
|
|
685
|
+
getTerminalBufferText = () => {
|
|
686
|
+
const { addTimeStampToTermLog } = this.state
|
|
687
|
+
const buffer = this.term.buffer.active
|
|
688
|
+
const len = buffer.length
|
|
689
|
+
const rawLines = []
|
|
690
|
+
for (let i = 0; i < len; i++) {
|
|
691
|
+
const line = buffer.getLine(i)
|
|
692
|
+
rawLines.push(line ? line.translateToString(false) : '')
|
|
693
|
+
}
|
|
694
|
+
// trim trailing blank lines before applying timestamps
|
|
695
|
+
while (rawLines.length && !rawLines[rawLines.length - 1].trim()) {
|
|
696
|
+
rawLines.pop()
|
|
697
|
+
}
|
|
698
|
+
if (!addTimeStampToTermLog) {
|
|
699
|
+
return rawLines.join('\n')
|
|
700
|
+
}
|
|
701
|
+
return rawLines.map(text => {
|
|
702
|
+
const now = new Date()
|
|
703
|
+
const ts = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}.${String(now.getMilliseconds()).padStart(3, '0')}] `
|
|
704
|
+
return ts + text
|
|
705
|
+
}).join('\n')
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
syncTermInfo = (stateUpdate) => {
|
|
709
|
+
this.setState(stateUpdate)
|
|
710
|
+
const infoUpdate = pick(stateUpdate, ['saveTerminalLogToFile', 'addTimeStampToTermLog', 'logPath', 'logFileName'])
|
|
711
|
+
if (Object.keys(infoUpdate).length) {
|
|
712
|
+
refs.get('term-info-' + this.props.tab.id)?.setState(infoUpdate)
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
openLogSaveDialog = async (titleKey) => {
|
|
717
|
+
const { logName } = this.props
|
|
718
|
+
const result = await window.api.saveDialog({
|
|
719
|
+
title: e(titleKey),
|
|
720
|
+
defaultPath: logName + '.log',
|
|
721
|
+
filters: [
|
|
722
|
+
{ name: 'Log files', extensions: ['log'] }
|
|
723
|
+
],
|
|
724
|
+
properties: ['createDirectory', 'showOverwriteConfirmation']
|
|
725
|
+
})
|
|
726
|
+
if (result.canceled || !result.filePath) {
|
|
727
|
+
return null
|
|
728
|
+
}
|
|
729
|
+
return result.filePath
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
onSaveTerminalLog = async () => {
|
|
733
|
+
const filePath = await this.openLogSaveDialog('saveTerminalLogToFile')
|
|
734
|
+
if (!filePath) {
|
|
735
|
+
return
|
|
736
|
+
}
|
|
737
|
+
const content = this.getTerminalBufferText()
|
|
738
|
+
await window.fs.writeFile(filePath, content).catch(window.store.onError)
|
|
739
|
+
const { addTimeStampToTermLog } = this.state
|
|
740
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
741
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
742
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
743
|
+
notification.success({
|
|
744
|
+
message: e('saveTerminalLogToFile'),
|
|
745
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
746
|
+
duration: 5
|
|
747
|
+
})
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
onRecord = async () => {
|
|
751
|
+
const filePath = await this.openLogSaveDialog('record')
|
|
752
|
+
if (!filePath) {
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
const { addTimeStampToTermLog } = this.state
|
|
756
|
+
startTerminalLogFile(this.pid, filePath, addTimeStampToTermLog).catch(window.store.onError)
|
|
757
|
+
const { path: logPath, name: logFileName } = getFolderFromFilePath(filePath, false)
|
|
758
|
+
this.syncTermInfo({ saveTerminalLogToFile: true, logPath, logFileName })
|
|
759
|
+
this.setState({ recording: true, recordingFilePath: filePath })
|
|
760
|
+
notification.success({
|
|
761
|
+
message: e('record'),
|
|
762
|
+
description: <ShowItem to={filePath}>{filePath}</ShowItem>,
|
|
763
|
+
duration: 5
|
|
764
|
+
})
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
onStopRecord = () => {
|
|
768
|
+
const { recordingFilePath } = this.state
|
|
769
|
+
toggleTerminalLog(this.pid).catch(window.store.onError)
|
|
770
|
+
this.syncTermInfo({ saveTerminalLogToFile: false })
|
|
771
|
+
this.setState({ recording: false, recordingFilePath: '' })
|
|
772
|
+
notification.success({
|
|
773
|
+
message: e('stopRecord'),
|
|
774
|
+
description: <ShowItem to={recordingFilePath}>{recordingFilePath}</ShowItem>
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
648
778
|
renderContextMenu = () => {
|
|
649
|
-
const { hasSelection } = this.state
|
|
779
|
+
const { hasSelection, recording } = this.state
|
|
650
780
|
const copyed = true
|
|
651
781
|
const copyShortcut = this.getShortcut('terminal_copy')
|
|
652
782
|
const pasteShortcut = this.getShortcut('terminal_paste')
|
|
653
783
|
const clearShortcut = this.getShortcut('terminal_clear')
|
|
654
784
|
const searchShortcut = this.getShortcut('terminal_search')
|
|
655
785
|
const selectAllShortcut = isMacJs ? 'meta+a' : 'ctrl+shift+a'
|
|
656
|
-
|
|
786
|
+
const isSerial = this.props.tab?.type === connectionMap.serial
|
|
787
|
+
const items = [
|
|
657
788
|
{
|
|
658
789
|
key: 'onCopy',
|
|
659
790
|
icon: <iconsMap.CopyOutlined />,
|
|
@@ -698,14 +829,56 @@ class Term extends Component {
|
|
|
698
829
|
icon: <iconsMap.SearchOutlined />,
|
|
699
830
|
label: e('search'),
|
|
700
831
|
extra: searchShortcut
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
key: 'onSaveTerminalLog',
|
|
835
|
+
icon: <iconsMap.SaveOutlined />,
|
|
836
|
+
label: e('saveTerminalLogToFile')
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
key: recording ? 'onStopRecord' : 'onRecord',
|
|
840
|
+
icon: recording ? <iconsMap.StopOutlined /> : <iconsMap.PlayCircleFilled />,
|
|
841
|
+
label: e(recording ? 'stopRecord' : 'record')
|
|
701
842
|
}
|
|
702
843
|
]
|
|
844
|
+
if (isSerial) {
|
|
845
|
+
items.push(
|
|
846
|
+
{
|
|
847
|
+
type: 'divider'
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
key: 'onXmodemSend',
|
|
851
|
+
icon: <iconsMap.CloudUploadOutlined />,
|
|
852
|
+
label: 'XMODEM Send'
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
key: 'onXmodemReceive',
|
|
856
|
+
icon: <iconsMap.CloudDownloadOutlined />,
|
|
857
|
+
label: 'XMODEM Receive'
|
|
858
|
+
}
|
|
859
|
+
)
|
|
860
|
+
}
|
|
861
|
+
return items
|
|
703
862
|
}
|
|
704
863
|
|
|
705
864
|
onContextMenu = ({ key }) => {
|
|
706
865
|
this[key]()
|
|
707
866
|
}
|
|
708
867
|
|
|
868
|
+
onXmodemSend = () => {
|
|
869
|
+
if (this.xmodemClient) {
|
|
870
|
+
this.xmodemClient.initiateSend()
|
|
871
|
+
}
|
|
872
|
+
this.term.focus()
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
onXmodemReceive = () => {
|
|
876
|
+
if (this.xmodemClient) {
|
|
877
|
+
this.xmodemClient.initiateReceive()
|
|
878
|
+
}
|
|
879
|
+
this.term.focus()
|
|
880
|
+
}
|
|
881
|
+
|
|
709
882
|
notifyOnData = debounce(() => {
|
|
710
883
|
window.store.notifyTabOnData(this.props.tab.id)
|
|
711
884
|
}, 1000)
|
|
@@ -1320,6 +1493,8 @@ class Term extends Component {
|
|
|
1320
1493
|
this.zmodemClient.init(socket)
|
|
1321
1494
|
this.trzszClient = new TrzszClient(this)
|
|
1322
1495
|
this.trzszClient.init(socket)
|
|
1496
|
+
this.xmodemClient = new XmodemClient(this)
|
|
1497
|
+
this.xmodemClient.init(socket)
|
|
1323
1498
|
this.fitAddon.fit()
|
|
1324
1499
|
term.displayRaw = displayRaw
|
|
1325
1500
|
term.loadAddon(
|
|
@@ -1581,6 +1756,7 @@ class Term extends Component {
|
|
|
1581
1756
|
<DropFileModal
|
|
1582
1757
|
visible={this.state.dropFileModalVisible}
|
|
1583
1758
|
files={this.state.droppedFiles}
|
|
1759
|
+
isSerial={this.props.tab?.type === connectionMap.serial}
|
|
1584
1760
|
onSelect={this.handleDropFileAction}
|
|
1585
1761
|
onCancel={this.handleDropFileModalCancel}
|
|
1586
1762
|
/>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMODEM client handler for web terminal
|
|
3
|
+
* Handles UI interactions and communicates with server-side xmodem
|
|
4
|
+
* Unlike zmodem/trzsz, XMODEM requires explicit user initiation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { throttle } from 'lodash-es'
|
|
8
|
+
import { filesize } from 'filesize'
|
|
9
|
+
import { TransferClientBase } from './transfer-client-base.js'
|
|
10
|
+
import { transferTypeMap } from '../../common/constants.js'
|
|
11
|
+
|
|
12
|
+
const XMODEM_SAVE_PATH_KEY = 'xmodem-save-path'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* XmodemClient class handles XMODEM UI and client-side logic
|
|
16
|
+
*/
|
|
17
|
+
export class XmodemClient extends TransferClientBase {
|
|
18
|
+
constructor (terminal) {
|
|
19
|
+
super(terminal, XMODEM_SAVE_PATH_KEY)
|
|
20
|
+
this.transferStartTime = 0
|
|
21
|
+
this.pendingMode = null // 'send' or 'receive' - waiting for user action
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the action name for this protocol
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
getActionName () {
|
|
29
|
+
return 'xmodem-event'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get protocol display name
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
getProtocolDisplayName () {
|
|
37
|
+
return 'XMODEM'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Handle server xmodem events
|
|
42
|
+
* @param {Object} msg - Message from server
|
|
43
|
+
*/
|
|
44
|
+
handleServerEvent (msg) {
|
|
45
|
+
const { event } = msg
|
|
46
|
+
|
|
47
|
+
switch (event) {
|
|
48
|
+
case 'receive-start':
|
|
49
|
+
this.onReceiveStart()
|
|
50
|
+
break
|
|
51
|
+
case 'send-start':
|
|
52
|
+
this.onSendStart()
|
|
53
|
+
break
|
|
54
|
+
case 'file-start':
|
|
55
|
+
this.onFileStart(msg.name, msg.size)
|
|
56
|
+
break
|
|
57
|
+
case 'progress':
|
|
58
|
+
this.onProgress(msg)
|
|
59
|
+
break
|
|
60
|
+
case 'file-complete':
|
|
61
|
+
this.onFileComplete(msg.name, msg.path)
|
|
62
|
+
break
|
|
63
|
+
case 'session-end':
|
|
64
|
+
this.onSessionEnd()
|
|
65
|
+
break
|
|
66
|
+
case 'session-error':
|
|
67
|
+
this.onError(msg.error)
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initiate XMODEM receive from UI
|
|
74
|
+
* User must have already started `sx` (or similar) on the remote device
|
|
75
|
+
*/
|
|
76
|
+
async initiateReceive () {
|
|
77
|
+
if (this.isActive) {
|
|
78
|
+
this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.isActive = true
|
|
83
|
+
this.writeBanner('RECEIVE', null)
|
|
84
|
+
|
|
85
|
+
// Ask user for save directory
|
|
86
|
+
const savePath = await this.openSaveFolderSelect()
|
|
87
|
+
if (savePath) {
|
|
88
|
+
this.savePath = savePath
|
|
89
|
+
this.sendToServer({
|
|
90
|
+
event: 'start-receive'
|
|
91
|
+
})
|
|
92
|
+
// Also send save path
|
|
93
|
+
this.sendToServer({
|
|
94
|
+
event: 'set-save-path',
|
|
95
|
+
path: savePath
|
|
96
|
+
})
|
|
97
|
+
} else {
|
|
98
|
+
// User cancelled
|
|
99
|
+
this.isActive = false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Initiate XMODEM send from UI
|
|
105
|
+
* User must have already started `rx` (or similar) on the remote device
|
|
106
|
+
*/
|
|
107
|
+
async initiateSend () {
|
|
108
|
+
if (this.isActive) {
|
|
109
|
+
this.writeToTerminal('\r\n\x1b[33m\x1b[1mXMODEM: Transfer already in progress\x1b[0m\r\n')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.isActive = true
|
|
114
|
+
this.writeBanner('SEND', null)
|
|
115
|
+
|
|
116
|
+
// Ask user to select files
|
|
117
|
+
const files = await this.openFileSelect({
|
|
118
|
+
title: 'Choose file(s) to send via XMODEM',
|
|
119
|
+
message: 'Choose file(s) to send via XMODEM'
|
|
120
|
+
})
|
|
121
|
+
if (files && files.length > 0) {
|
|
122
|
+
this.sendToServer({
|
|
123
|
+
event: 'start-send'
|
|
124
|
+
})
|
|
125
|
+
// Also send files
|
|
126
|
+
this.sendToServer({
|
|
127
|
+
event: 'send-files',
|
|
128
|
+
files
|
|
129
|
+
})
|
|
130
|
+
} else {
|
|
131
|
+
// User cancelled
|
|
132
|
+
this.isActive = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Handle receive session start (from server)
|
|
138
|
+
*/
|
|
139
|
+
onReceiveStart () {
|
|
140
|
+
// Server confirmed it's ready to receive
|
|
141
|
+
this.writeToTerminal('\r\n\x1b[36mWaiting for remote to start sending file...\x1b[0m\r\n')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle send session start (from server)
|
|
146
|
+
*/
|
|
147
|
+
onSendStart () {
|
|
148
|
+
// Server confirmed it's ready to send
|
|
149
|
+
this.writeToTerminal('\r\n\x1b[36mWaiting for remote to request file (NAK/C)...\x1b[0m\r\n')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handle file start event
|
|
154
|
+
* @param {string} name - File name
|
|
155
|
+
* @param {number} size - File size
|
|
156
|
+
*/
|
|
157
|
+
onFileStart (name, size) {
|
|
158
|
+
this.currentTransfer = {
|
|
159
|
+
name,
|
|
160
|
+
size,
|
|
161
|
+
transferred: 0,
|
|
162
|
+
type: this.savePath ? transferTypeMap.download : transferTypeMap.upload,
|
|
163
|
+
path: null
|
|
164
|
+
}
|
|
165
|
+
this.transferStartTime = Date.now()
|
|
166
|
+
this.writeProgress()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle progress update
|
|
171
|
+
* @param {Object} msg - Progress message
|
|
172
|
+
*/
|
|
173
|
+
onProgress (msg) {
|
|
174
|
+
if (!this.currentTransfer) {
|
|
175
|
+
this.currentTransfer = {
|
|
176
|
+
name: msg.name,
|
|
177
|
+
size: msg.size,
|
|
178
|
+
transferred: 0,
|
|
179
|
+
type: msg.type,
|
|
180
|
+
path: msg.path || null
|
|
181
|
+
}
|
|
182
|
+
this.transferStartTime = Date.now()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.currentTransfer.transferred = msg.transferred
|
|
186
|
+
this.currentTransfer.serverSpeed = msg.speed
|
|
187
|
+
this.currentTransfer.path = msg.path || this.currentTransfer.path
|
|
188
|
+
this.writeProgress()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handle file complete
|
|
193
|
+
* @param {string} name - File name
|
|
194
|
+
* @param {string} path - File path
|
|
195
|
+
*/
|
|
196
|
+
onFileComplete (name, path) {
|
|
197
|
+
if (this.currentTransfer) {
|
|
198
|
+
this.currentTransfer.transferred = this.currentTransfer.size || this.currentTransfer.transferred
|
|
199
|
+
this.currentTransfer.path = path
|
|
200
|
+
this._doWriteProgress(true)
|
|
201
|
+
this.writeToTerminal('\r\n')
|
|
202
|
+
this._prevProgressRows = 0
|
|
203
|
+
}
|
|
204
|
+
this.currentTransfer = null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Handle error from server
|
|
209
|
+
* @param {string} error - Error message
|
|
210
|
+
*/
|
|
211
|
+
onError (error) {
|
|
212
|
+
this.writeToTerminal(`\r\n\x1b[31m\x1b[1mXMODEM Error: ${error}\x1b[0m\r\n`)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Write progress to terminal (throttled)
|
|
217
|
+
*/
|
|
218
|
+
writeProgress = throttle((isComplete = false) => {
|
|
219
|
+
this._doWriteProgress(isComplete)
|
|
220
|
+
}, 500)
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Internal function to actually write progress
|
|
224
|
+
*/
|
|
225
|
+
_doWriteProgress (isComplete = false) {
|
|
226
|
+
if (!this.currentTransfer || !this.terminal?.term) return
|
|
227
|
+
|
|
228
|
+
const { name, size, transferred, path, serverSpeed } = this.currentTransfer
|
|
229
|
+
const speed = serverSpeed || 0
|
|
230
|
+
const displayName = path || name
|
|
231
|
+
const formatSize = (bytes) => filesize(bytes)
|
|
232
|
+
|
|
233
|
+
this.writeProgressBar({
|
|
234
|
+
name: displayName,
|
|
235
|
+
size,
|
|
236
|
+
transferred,
|
|
237
|
+
speed,
|
|
238
|
+
isComplete,
|
|
239
|
+
formatSize
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export default XmodemClient
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
Button
|
|
9
9
|
} from 'antd'
|
|
10
10
|
import defaults from '../../common/default-setting'
|
|
11
|
-
import { toggleTerminalLog, toggleTerminalLogTimestamp
|
|
11
|
+
import { toggleTerminalLog, toggleTerminalLogTimestamp } from '../terminal/terminal-apis'
|
|
12
12
|
import {
|
|
13
13
|
ClockCircleOutlined,
|
|
14
14
|
BorderlessTableOutlined,
|
|
@@ -18,7 +18,9 @@ import {
|
|
|
18
18
|
PartitionOutlined
|
|
19
19
|
} from '@ant-design/icons'
|
|
20
20
|
import { refs } from '../common/ref'
|
|
21
|
-
import
|
|
21
|
+
import ShowItem from '../common/show-item'
|
|
22
|
+
import { osResolve } from '../../common/resolve'
|
|
23
|
+
import createDefaultLogPath from '../../common/default-log-path'
|
|
22
24
|
|
|
23
25
|
const e = window.translate
|
|
24
26
|
|
|
@@ -35,15 +37,20 @@ export default class TerminalInfoBase extends Component {
|
|
|
35
37
|
state = {
|
|
36
38
|
saveTerminalLogToFile: false,
|
|
37
39
|
addTimeStampToTermLog: false,
|
|
38
|
-
logPath: ''
|
|
40
|
+
logPath: '',
|
|
41
|
+
logFileName: ''
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
componentDidMount () {
|
|
45
|
+
const { pid } = this.props
|
|
46
|
+
refs.add('term-info-' + pid, this)
|
|
42
47
|
this.getState()
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
componentWillUnmount () {
|
|
46
51
|
clearTimeout(this.timer)
|
|
52
|
+
const { pid } = this.props
|
|
53
|
+
refs.remove('term-info-' + pid)
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
handleToggleTimestamp = () => {
|
|
@@ -74,17 +81,6 @@ export default class TerminalInfoBase extends Component {
|
|
|
74
81
|
})
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
onLogPathChange = (v) => {
|
|
78
|
-
const { pid } = this.props
|
|
79
|
-
setTerminalLogPath(pid, v)
|
|
80
|
-
refs.get('term-' + pid)?.setState({
|
|
81
|
-
logPath: v
|
|
82
|
-
})
|
|
83
|
-
this.setState({
|
|
84
|
-
logPath: v
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
handleToggle = () => {
|
|
89
85
|
const { saveTerminalLogToFile, addTimeStampToTermLog } = this.state
|
|
90
86
|
const {
|
|
@@ -112,7 +108,8 @@ export default class TerminalInfoBase extends Component {
|
|
|
112
108
|
this.setState({
|
|
113
109
|
saveTerminalLogToFile: term.state.saveTerminalLogToFile,
|
|
114
110
|
addTimeStampToTermLog: term.state.addTimeStampToTermLog,
|
|
115
|
-
logPath: term.state.logPath
|
|
111
|
+
logPath: term.state.logPath,
|
|
112
|
+
logFileName: term.state.logFileName || ''
|
|
116
113
|
})
|
|
117
114
|
} else {
|
|
118
115
|
this.timer = setTimeout(this.getState, 100)
|
|
@@ -166,39 +163,45 @@ export default class TerminalInfoBase extends Component {
|
|
|
166
163
|
render () {
|
|
167
164
|
const {
|
|
168
165
|
id,
|
|
169
|
-
logName
|
|
170
|
-
pid
|
|
166
|
+
logName
|
|
171
167
|
} = this.props
|
|
172
|
-
const { saveTerminalLogToFile, logPath } = this.state
|
|
168
|
+
const { saveTerminalLogToFile, logPath, logFileName } = this.state
|
|
173
169
|
const name = e('saveTerminalLogToFile')
|
|
170
|
+
const base = logPath || createDefaultLogPath()
|
|
171
|
+
const fileName = logFileName || (logName + '.log')
|
|
172
|
+
const fullPath = osResolve(base, fileName)
|
|
174
173
|
return (
|
|
175
174
|
<div className='terminal-info-section terminal-info-base'>
|
|
176
|
-
<div className='
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
175
|
+
<div className='pd1b'>
|
|
176
|
+
<b>ID:</b> {id}
|
|
177
|
+
</div>
|
|
178
|
+
<div className='pd1b'>
|
|
179
|
+
<Switch
|
|
180
|
+
checkedChildren={name}
|
|
181
|
+
unCheckedChildren={name}
|
|
182
|
+
checked={saveTerminalLogToFile}
|
|
183
|
+
onChange={this.handleToggle}
|
|
184
|
+
className='mg1r mg1b'
|
|
185
|
+
/>
|
|
186
|
+
{
|
|
187
|
+
this.renderTimestamp()
|
|
188
|
+
}
|
|
190
189
|
</div>
|
|
190
|
+
{
|
|
191
|
+
saveTerminalLogToFile
|
|
192
|
+
? (
|
|
193
|
+
<div className='pd1b font-xs color-grey'>
|
|
194
|
+
{e('terminalLogPath')}: {fullPath} <ShowItem to={fullPath} />
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
: null
|
|
198
|
+
}
|
|
191
199
|
<div className='pd2y'>
|
|
192
200
|
{
|
|
193
201
|
this.renderInfoSelection()
|
|
194
202
|
}
|
|
195
203
|
</div>
|
|
196
|
-
|
|
197
|
-
pid={pid}
|
|
198
|
-
logPath={logPath}
|
|
199
|
-
logName={logName}
|
|
200
|
-
setLogPath={this.onLogPathChange}
|
|
201
|
-
/>
|
|
204
|
+
|
|
202
205
|
</div>
|
|
203
206
|
)
|
|
204
207
|
}
|
|
@@ -24,7 +24,8 @@ export default (data) => {
|
|
|
24
24
|
return Object.keys(data).map(k => {
|
|
25
25
|
const rd = (txt) => {
|
|
26
26
|
// txt is in KB, convert to bytes for filesize function
|
|
27
|
-
const
|
|
27
|
+
const parsed = parseInt(txt, 10)
|
|
28
|
+
const r = k === 'mem' ? filesize(Number.isFinite(parsed) ? parsed * 1024 : 0) : txt
|
|
28
29
|
const itemProps = {
|
|
29
30
|
className: 'activity-item pointer',
|
|
30
31
|
'data-content': r,
|
|
@@ -14,10 +14,12 @@ export default function TerminalInfoDisk (props) {
|
|
|
14
14
|
}
|
|
15
15
|
const col = colsParser(disks[0])
|
|
16
16
|
disks.sort((a, b) => {
|
|
17
|
-
|
|
17
|
+
const af = a.filesystem || ''
|
|
18
|
+
const bf = b.filesystem || ''
|
|
19
|
+
if (af.startsWith('/') && !bf.startsWith('/')) {
|
|
18
20
|
return -1
|
|
19
21
|
}
|
|
20
|
-
if (!
|
|
22
|
+
if (!af.startsWith('/') && bf.startsWith('/')) {
|
|
21
23
|
return 1
|
|
22
24
|
}
|
|
23
25
|
return 0
|
|
@@ -12,10 +12,11 @@ import { osResolve } from '../../common/resolve'
|
|
|
12
12
|
|
|
13
13
|
const e = window.translate
|
|
14
14
|
|
|
15
|
-
export default function LogPathEdit ({ pid, logPath, logName, setLogPath }) {
|
|
15
|
+
export default function LogPathEdit ({ pid, logPath, logName, logFileName, setLogPath }) {
|
|
16
16
|
const defaultPath = createDefaultLogPath()
|
|
17
17
|
const base = logPath || defaultPath
|
|
18
|
-
const
|
|
18
|
+
const fileName = logFileName || (logName + '.log')
|
|
19
|
+
const fullPath = osResolve(base, fileName)
|
|
19
20
|
|
|
20
21
|
const testAndSet = async (v) => {
|
|
21
22
|
if (v) {
|