@electerm/electerm-react 3.15.198 → 5.0.6

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.
@@ -10,7 +10,6 @@ import { isEqual, last, isNumber, some, isArray, pick, uniq, debounce } from 'lo
10
10
  import FileSection from './file-item'
11
11
  import resolve from '../../common/resolve'
12
12
  import wait from '../../common/wait'
13
- import isAbsPath from '../../common/is-absolute-path'
14
13
  import classnames from 'classnames'
15
14
  import sorterIndex from '../../common/index-sorter'
16
15
  import { handleErr } from '../../common/fetch'
@@ -107,8 +106,6 @@ export default class Sftp extends Component {
107
106
  this.sftp = null
108
107
  clearTimeout(this.timer4)
109
108
  this.timer4 = null
110
- clearTimeout(this.timer5)
111
- this.timer5 = null
112
109
  // Clear sort cache to prevent memory leaks
113
110
  this._sortCache?.clear()
114
111
  this._lastSortArgs = null
@@ -455,9 +452,7 @@ export default class Sftp extends Component {
455
452
  }
456
453
  const type = files[0]?.type || _type
457
454
  const func = this[type + 'Del']
458
- for (const f of files) {
459
- await func(f)
460
- }
455
+ await Promise.all(files.map(f => func(f)))
461
456
  if (type === typeMap.remote) {
462
457
  await wait(500)
463
458
  }
@@ -560,8 +555,10 @@ export default class Sftp extends Component {
560
555
  }
561
556
 
562
557
  initRemoteAll = async () => {
563
- await this.remoteList()
564
- this.remoteListOwner()
558
+ await Promise.all([
559
+ this.remoteList(),
560
+ this.remoteListOwner().catch(e => console.debug('remoteListOwner error:', e))
561
+ ])
565
562
  }
566
563
 
567
564
  modifier = (...args) => {
@@ -647,12 +644,10 @@ export default class Sftp extends Component {
647
644
  }
648
645
 
649
646
  remoteListOwner = async () => {
650
- const remoteUidTree = await owner.remoteListUsers(
651
- this.props.pid
652
- )
653
- const remoteGidTree = await owner.remoteListGroups(
654
- this.props.pid
655
- )
647
+ const [remoteUidTree, remoteGidTree] = await Promise.all([
648
+ owner.remoteListUsers(this.props.pid),
649
+ owner.remoteListGroups(this.props.pid)
650
+ ])
656
651
  this.setState({
657
652
  remoteGidTree,
658
653
  remoteUidTree
@@ -660,8 +655,10 @@ export default class Sftp extends Component {
660
655
  }
661
656
 
662
657
  localListOwner = async () => {
663
- const localUidTree = await owner.localListUsers()
664
- const localGidTree = await owner.localListGroups()
658
+ const [localUidTree, localGidTree] = await Promise.all([
659
+ owner.localListUsers(),
660
+ owner.localListGroups()
661
+ ])
665
662
  this.setState({
666
663
  localGidTree,
667
664
  localUidTree
@@ -800,14 +797,6 @@ export default class Sftp extends Component {
800
797
  sftpCreated: true
801
798
  })
802
799
  })
803
- this.timer5 = setTimeout(() => {
804
- if (this.type !== 'ftp') {
805
- this.updateRemoteList(remote, remotePath, sftp)
806
- }
807
- this.props.editTab(tab.id, {
808
- sftpCreated: true
809
- })
810
- }, 1000)
811
800
  } catch (e) {
812
801
  const update = {
813
802
  remoteLoading: false,
@@ -828,41 +817,34 @@ export default class Sftp extends Component {
828
817
  remotePath,
829
818
  sftp
830
819
  ) => {
831
- const remote = []
832
- for (const r of remotes) {
833
- const { name } = r
834
- if (r.isSymbol) {
835
- const linkPath = resolve(remotePath, name)
836
- let realpath = await sftp.readlink(linkPath)
837
- .catch(e => {
838
- console.debug(e)
839
- return null
840
- })
841
- if (!realpath) {
842
- continue
843
- }
844
- if (!isAbsPath(realpath)) {
845
- realpath = resolve(remotePath, realpath)
846
- realpath = await sftp.realpath(realpath)
820
+ // Resolve all symlinks in parallel for faster updates.
821
+ // Use sftp.stat (which follows symlinks) directly on the link path
822
+ // instead of readlink + realpath + stat, reducing 2-3 SFTP round
823
+ // trips per symlink to just 1.
824
+ const results = await Promise.all(
825
+ remotes.map(async r => {
826
+ if (!r.isSymbol) {
827
+ r.isSymbolicLink = false
828
+ return r
847
829
  }
830
+ const linkPath = resolve(remotePath, r.name)
848
831
  const realFileInfo = await getRemoteFileInfo(
849
832
  sftp,
850
- realpath
833
+ linkPath
851
834
  ).catch(e => {
852
835
  console.debug('seems a bad symbolic link')
853
836
  console.debug(e)
854
837
  return null
855
838
  })
856
839
  if (!realFileInfo) {
857
- continue
840
+ return null
858
841
  }
859
842
  r.isSymbolicLink = true
860
843
  r.isDirectory = realFileInfo.isDirectory
861
- } else {
862
- r.isSymbolicLink = false
863
- }
864
- remote.push(r)
865
- }
844
+ return r
845
+ })
846
+ )
847
+ const remote = results.filter(Boolean)
866
848
  const update = {
867
849
  remote,
868
850
  remoteFileTree: this.buildTree(remote, typeMap.remote)
@@ -892,14 +874,13 @@ export default class Sftp extends Component {
892
874
  this.getCwdLocal() ||
893
875
  this.getLocalHome()
894
876
  const locals = await window.fs.readdirAsync(localPath)
895
- const local = []
896
- for (const name of locals) {
897
- const p = resolve(localPath, name)
898
- const fileObj = await getLocalFileInfo(p).catch(console.log)
899
- if (fileObj) {
900
- local.push(fileObj)
901
- }
902
- }
877
+ const results = await Promise.all(
878
+ locals.map(name => {
879
+ const p = resolve(localPath, name)
880
+ return getLocalFileInfo(p).catch(console.log)
881
+ })
882
+ )
883
+ const local = results.filter(Boolean)
903
884
  const update = {
904
885
  local,
905
886
  inited: true,
@@ -15,7 +15,7 @@ import {
15
15
  } from '@ant-design/icons'
16
16
  import message from '../common/message'
17
17
  import Modal from '../common/modal'
18
- import { copy } from '../../common/clipboard'
18
+ import { copy, readClipboard } from '../../common/clipboard'
19
19
  import VncForm from './vnc-form'
20
20
  import RemoteFloatControl from '../common/remote-float-control'
21
21
  import './vnc.styl'
@@ -49,11 +49,16 @@ export default class VncSession extends PureComponent {
49
49
 
50
50
  domRef = createRef()
51
51
 
52
+ // clipboard sync state (local -> remote direction)
53
+ lastClip = null
54
+ vncViewOnly = false
55
+
52
56
  componentDidMount () {
53
57
  this.remoteInit()
54
58
  }
55
59
 
56
60
  componentWillUnmount () {
61
+ this._detachClipboardSync()
57
62
  this.rfb && this.rfb.disconnect()
58
63
  delete this.rfb
59
64
  clearTimeout(this.timer)
@@ -379,6 +384,7 @@ export default class VncSession extends PureComponent {
379
384
  rfb.compressionLevel = compressionLevel
380
385
  rfb.viewOnly = viewOnly
381
386
  rfb.showDotCursor = showDotCursor
387
+ this.vncViewOnly = viewOnly
382
388
  this.rfb = rfb
383
389
  }
384
390
 
@@ -388,12 +394,14 @@ export default class VncSession extends PureComponent {
388
394
  this.setState({
389
395
  loading: false
390
396
  })
397
+ this._attachClipboardSync()
391
398
  }
392
399
 
393
400
  onDesktopsize = (event) => {
394
401
  }
395
402
 
396
403
  onDisconnect = () => {
404
+ this._detachClipboardSync()
397
405
  this.setStatus(statusMap.error)
398
406
  }
399
407
 
@@ -433,7 +441,61 @@ export default class VncSession extends PureComponent {
433
441
  }
434
442
 
435
443
  onClipboard = (event) => {
436
- copy(event.detail.text)
444
+ const text = event?.detail?.text
445
+ if (!text || text === this.lastClip) {
446
+ return
447
+ }
448
+ // Track so our click-driven sync won't echo this same text back to the
449
+ // server (some servers re-broadcast what they receive).
450
+ this.lastClip = text
451
+ copy(text)
452
+ }
453
+
454
+ // Push the current local clipboard to the remote when it has changed.
455
+ // This implements the local -> remote direction that noVNC cannot do itself.
456
+ // Triggered on view click instead of polling, so it only runs right when the
457
+ // user is about to interact with the remote desktop (e.g. before pasting).
458
+ handleViewMouseDown = () => {
459
+ const rfb = this.rfb
460
+ if (!rfb || this.vncViewOnly) {
461
+ return
462
+ }
463
+ let text
464
+ try {
465
+ text = readClipboard()
466
+ } catch (e) {
467
+ return
468
+ }
469
+ if (!text || text === this.lastClip) {
470
+ return
471
+ }
472
+ this.lastClip = text
473
+ try {
474
+ rfb.clipboardPasteFrom(text)
475
+ } catch (e) {
476
+ // clipboardPasteFrom can throw if the socket is in a bad state; ignore
477
+ }
478
+ }
479
+
480
+ // noVNC calls stopPropagation() on canvas mouse events in the bubble phase,
481
+ // so React's delegated onClick (or any bubble-phase listener) never fires.
482
+ // We attach a CAPTURE-phase mousedown listener on the wrapper so it runs
483
+ // before noVNC can stop propagation. mousedown also fires earlier than click,
484
+ // giving the remote clipboard time to update before the user pastes.
485
+ _attachClipboardSync = () => {
486
+ this._detachClipboardSync()
487
+ const el = this.domRef.current
488
+ if (!el) {
489
+ return
490
+ }
491
+ el.addEventListener('mousedown', this.handleViewMouseDown, true)
492
+ }
493
+
494
+ _detachClipboardSync = () => {
495
+ const el = this.domRef.current
496
+ if (el) {
497
+ el.removeEventListener('mousedown', this.handleViewMouseDown, true)
498
+ }
437
499
  }
438
500
 
439
501
  onBell = (event) => {
@@ -542,6 +604,7 @@ export default class VncSession extends PureComponent {
542
604
  }
543
605
 
544
606
  handleReInit = () => {
607
+ this._detachClipboardSync()
545
608
  this.rfb?.disconnect()
546
609
  delete this.rfb
547
610
  this.remoteInit()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/electerm-react",
3
- "version": "3.15.198",
3
+ "version": "5.0.6",
4
4
  "description": "react components src for electerm",
5
5
  "main": "./client/components/main/main.jsx",
6
6
  "license": "MIT",