leffss / gowebssh

Webssh implemented by github.com/gorilla/websocket and golang.org/x/crypto/ssh
MIT License
80 stars 25 forks source link

在vue项目中引用了,但是报错Cannot read properties of undefined (reading 'Sentry') #7

Closed G-Pig closed 3 years ago

G-Pig commented 3 years ago

请问这个要怎解决呀 `import { Zmodem } from '@/utils/zmodem/zmodem.devel.js' const self = this this.zsentry = new Zmodem.Sentry({ to_terminal: function(octets) { console.log(octets) }, // i.e. send to the terminal

    on_detect(detection) {
      const zsession = detection.confirm()
      let promise
      if (zsession.type === 'receive') {
        self.handleType = 'download'
        promise = self.downloadFile(zsession)
      } else {
        self.handleType = 'upload'
        promise = self.uploadFile(zsession)
      }
      promise.catch(console.error.bind(console)).then(() => {
        //
      })
    },

    on_retract: function(octets) {
      console.log(octets)
    },

    sender: function(octets) {
      self.socket.send(new Uint8Array(octets))
    }
  })`
G-Pig commented 3 years ago

我直接引用了zmodem进去,但是在new Zmodem的时候报错Sentry的问题。我直接打断点到构建对象里面,直接没进去

leffss commented 3 years ago

image

index.vue

<template>
  <div class="app-container">
    <el-row :gutter="15">
      <el-col :span="4" class="card-box">
        <el-card shadow="always" style="border-radius: 0px;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)">
          <el-input v-model="filterText" clearable placeholder="输入关键字进行过滤" />
          <el-tree
            ref="tree"
            class="filter-tree"
            :data="treeData"
            :props="defaultProps"
            highlight-current
            :default-expand-all="true"
            :filter-node-method="filterNode"
            @node-contextmenu="nodeRightClick"
            @node-click="nodeClick"
          />
        </el-card>
      </el-col>
      <el-col :span="20" class="card-box">
        <el-card shadow="always" style="border-radius: 0px;box-shadow: 0 2px 4px rgba(0, 0, 0, .12), 0 0 6px rgba(0, 0, 0, .04)">
          <!--div style="margin-bottom: 10px;">
            <el-button
              type="primary"
              @click="addTab(editableTabsValue)"
            >
              添加webssh
            </el-button>
            <el-button
              type="danger"
              @click="removeAllTab"
            >
              移除所有webssh
            </el-button>
            <span style="margin-left: 5px">当前webssh数:{{ tabIndex }}</span>
            <span v-if="tabIndex > 0" style="margin-left: 5px">当前webssh:{{ currentTab }}</span>
          </div-->
          <el-tabs v-model="editableTabsValue" closable @edit="editTab">
            <el-tab-pane v-for="item in editableTabs" :key="item.name" :label="item.title" :name="item.name">
              <console :ref="item.name" :sid="item.name" :current-sid="editableTabsValue" />
            </el-tab-pane>
          </el-tabs>
        </el-card>
      </el-col>
    </el-row>
    <vue-context ref="menu" lazy @close="closeMenu">
      <template v-if="currentData.menus">
        <li v-for="menu in currentData.menus" :key="menu.label">
          <a @click.prevent="addTab(menu.label)" @contextmenu.prevent="addTabRightClick(menu.label)">
            {{ menu.label }}
          </a>
        </li>
        <li class="v-context__sub">
          <a>
            sftp
          </a>
          <ul class="v-context">
            <li>
              <a @click.prevent="addTab('root')" @contextmenu.prevent="addTabRightClick('root')">
                root
              </a>
            </li>
            <li>
              <a @click.prevent="addTab('test')" @contextmenu.prevent="addTabRightClick('test')">
                test
              </a>
            </li>
          </ul>
        </li>
      </template>
    </vue-context>
  </div>
</template>

<script>
import 'xterm/css/xterm.css'
import Console from './components/Console'
import VueContext from 'vue-context'
// import 'vue-context/src/sass/vue-context.scss'
// import 'vue-context/dist/css/vue-context.css'

export default {
  name: 'WebSsh',
  components: { Console, VueContext },
  data() {
    return {
      top: 0,
      left: 0,
      currentData: {},
      tabOpen: 5,
      editableTabsValue: '192.168.223.111',
      editableTabs: [{
        title: '192.168.223.111',
        name: '192.168.223.111'
      }],
      // tabIndex: 2
      filterText: '',
      treeData: [{
        label: '全部',
        children: [{
          label: '运维',
          children: [{
            id: 1,
            label: '主机1',
            menus: [{
              label: 'webssh'
            }, {
              label: 'clissh'
            }]
          }]
        },
        {
          label: '开发',
          children: [{
            id: 2,
            label: '主机2',
            menus: [{
              label: 'webssh'
            }, {
              label: 'vnc'
            }]
          }, {
            id: 3,
            label: '主机3',
            menus: [{
              label: 'webssh'
            }, {
              label: 'clissh'
            }, {
              label: 'vnc'
            }]
          }]
        },
        {
          label: '测试',
          children: [{
            id: 4,
            label: '主机4',
            menus: [{
              label: 'clissh'
            }]
          },
          {
            id: 5,
            label: '主机5',
            menus: [{
              label: 'webssh'
            }]
          }]
        },
        {
          id: 6,
          label: '主机6',
          menus: [{
            label: 'rdp'
          }]
        }, {
          id: 7,
          label: '主机7'
        },
        {
          id: 8,
          label: '主机8',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 8,
          label: '主机8',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 9,
          label: '主机9',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 10,
          label: '主机10',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 11,
          label: '主机11',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 12,
          label: '主机12',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 13,
          label: '主机13',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 14,
          label: '主机14',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 15,
          label: '主机15',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 16,
          label: '主机16',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 17,
          label: '主机17',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        },
        {
          id: 18,
          label: '主机18',
          menus: [{
            label: 'rdp'
          }, {
            label: 'ssh'
          }]
        }]
      }],
      defaultProps: {
        children: 'children',
        label: 'label'
      }
    }
  },
  computed: {
    tabIndex: function() {
      return this.editableTabs.length
    },
    currentTab: function() {
      if (this.tabIndex === 0) {
        return ''
      } else {
        return this.editableTabsValue
      }
    }
  },
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val)
    }
  },
  mounted() {
    window.onbeforeunload = function(e) {
      const dialogText = '禁止刷新'
      e.returnValue = dialogText
      return dialogText
    }
    // 自定义的 evil 函数(替代 eval)需要使用这个全局变量
    window.$refs = this.$refs
  },
  beforeDestroy() {
    window.onbeforeunload = function(e) {}
  },
  methods: {
    randomString(length, chars) {
      const xChars = chars || '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
      let result = ''
      for (let i = length; i > 0; --i) result += xChars[Math.floor(Math.random() * xChars.length)]
      return result
    },
    addTab(targetName) {
      if (this.tabIndex < this.tabOpen) {
        // this.tabIndex += 1
        const newTabName = this.randomString(8)
        this.editableTabs.push({
          title: newTabName,
          name: newTabName
        })
        this.editableTabsValue = newTabName
      } else {
        this.$message({
          type: 'error',
          message: `最多打开 ${this.tabOpen} 个标签`
        })
      }
    },
    addTabRightClick(targetName) {
      this.$refs.menu.close()
      this.addTab(targetName)
    },
    removeAllTab() {
      this.editableTabs = []
    },
    removeTab(targetName) {
      const tabs = this.editableTabs
      let activeName = this.editableTabsValue
      if (activeName === targetName) {
        tabs.forEach((tab, index) => {
          if (tab.name === targetName) {
            const nextTab = tabs[index + 1] || tabs[index - 1]
            if (nextTab) {
              activeName = nextTab.name
            }
          }
        })
      }
      this.editableTabsValue = activeName
      this.editableTabs = tabs.filter(tab => tab.name !== targetName)
      // this.tabIndex -= 1
    },
    editTab(targetName, action) {
      if (action === 'remove') {
        // 根据 $refs 获取子组件数据判断 webssh 是否关闭,如果已经关闭直接标签
        const closed = this.evil(`window.$refs['${targetName}'][0].closed`)
        if (closed) {
          this.removeTab(targetName)
        } else {
          this.$confirm(`确定关闭标签 ${targetName} 吗?`, '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          }).then(() => {
            this.removeTab(targetName)
          }).catch(() => {
            //
          })
        }
      }
    },
    filterNode(value, data) {
      if (!value) return true
      return data.label.indexOf(value) !== -1
    },
    nodeClick(object, node, element) {
      if (!object.children) {
        this.currentData = object
        const event = window.event || undefined
        if (event) {
          this.openMenu(event)
        } else {
          this.$message({
            type: 'error',
            message: '浏览器不支持 event 事件'
          })
        }
      }
    },
    nodeRightClick(event, object, node, element) {
      if (!object.children) {
        this.currentData = object
        this.openMenu(event)
      }
    },
    openMenu(e) {
      if (this.currentData.menus) {
        this.$refs.menu.open(event, name)
      } else {
        this.$message.error(`${this.currentData.label} 无可操作项`)
        this.$refs.menu.close()
      }
    },
    closeMenu() {
      this.currentData = {}
    },
    evil(fn) {
      const Fn = Function
      return new Fn('return ' + fn)()
    }
  }
}
</script>

<style lang="scss" scoped>
// vue-content 内容参考 'vue-context/src/sass/vue-context.scss'
$menu-bg: #fff;
$menu-border: rgba(0, 0, 0, 0.15);
$item-color: #212529;
$item-hover-bg: #ece6e6;
$item-hover-color: #212529;
.v-context {
  &, & ul {
    background-color: $menu-bg;
    background-clip: padding-box;
    // border-radius: .25rem;
    border-radius: 0;
    // border: 1px solid $menu-border;
    // box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    display: block;
    margin: 0;
    padding: 3px 0;
    min-width: 8rem;
    z-index: 1500;
    position: fixed;
    list-style: none;
    box-sizing: border-box;
    max-height: calc(100% - 50px);
    overflow-y: auto;
    > li {
      margin: 0;
      position: relative;
      > a {
        display: block;
        padding: .5rem 1.5rem;
        font-size: 14px;
        font-weight: 400;
        color: $item-color;
        text-decoration: none;
        white-space: nowrap;
        background-color: transparent;
        border: 0;
        &:hover,
        &:focus {
          text-decoration: none;
          color: $item-hover-color;
          background-color: $item-hover-bg;
        }
        &:focus {
          outline: 0;
        }
      }
    }
    &:focus {
      outline: 0;
    }
  }
  &__sub {
    > a:after {
      content: "\203A";
      float: right;
      padding-left: 1rem;
    }
    > ul {
      display: none;
    }
  }
}
</style>

xterm.js

import { Terminal } from 'xterm'
import { AttachAddon } from 'xterm-addon-attach'
import { FitAddon } from 'xterm-addon-fit'
import { SearchAddon } from 'xterm-addon-search'
import { Unicode11Addon } from 'xterm-addon-unicode11'
import { WebLinksAddon } from 'xterm-addon-web-links'
import { WebglAddon } from 'xterm-addon-webgl'

export { Terminal, AttachAddon, FitAddon, SearchAddon, Unicode11Addon, WebLinksAddon, WebglAddon }

Console.vue

<template>
  <div>
    <div>
      <el-input v-model="search" placeholder="搜索终端内容" clearable style="width: 250px;" @keyup.enter.native="searchContent" @clear="searchContent" />
      <el-button style="margin-left: 10px;" type="primary" icon="el-icon-search" @click="searchContent">
        搜索
      </el-button>
    </div>
    <div :id="sid" style="margin-top: 10px;height: 100%" />
    <input :id="'upload-' + sid" type="text" hidden>
    <el-dialog v-el-drag-dialog :visible.sync="dialogUploadVisible" width="480px" style="text-align: center;" :before-close="dialogBeforeClose">
      <el-upload
        drag
        action="/upload"
        :multiple="true"
        :limit="1"
        :show-file-list="false"
        :before-upload="beforeUpload"
        :on-exceed="handleExceed"
      >
        <i class="el-icon-upload" />
        <div class="el-upload__text">将文件拖到此处,或 <em>点击上传</em></div>
        <div slot="tip" class="el-upload__tip">
          文件个数上限: {{ fileLimit }} ,单文件大小上限: {{ maxFileSize | bytesHuman }}
        </div>
      </el-upload>
    </el-dialog>
  </div>
</template>

<script>
import elDragDialog from '@/directive/el-drag-dialog' // base on element-ui
import { Terminal, SearchAddon, Unicode11Addon } from './xterm'
import store from '@/store'
import { getToken } from '@/utils/auth'
import { node } from '@/api/node'
import { bytesHuman, getBrowser } from '@/utils'
// import Zmodem from 'zmodem.js'
import * as Zmodem from 'zmodem.js/src/zmodem_browser'
import streamSaver from 'streamsaver'
import * as ponyfill from 'web-streams-polyfill/ponyfill'
streamSaver.WritableStream = streamSaver.WritableStream || ponyfill.WritableStream
streamSaver.mitm = '/streamsaver/mitm.html' // 默认使用 https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0

// if (!streamSaver.WritableStream) {
//   // 有些浏览器不支持 WritableStream (比如 firefox),无法使用 streamsaver.js
//   // 使用 web-streams-polyfill/ponyfill 库弥补
//   const ponyfill = require('web-streams-polyfill/ponyfill')
//   streamSaver.WritableStream = ponyfill.WritableStream
//   streamSaver.ReadableStream = ponyfill.ReadableStream
//   window.ReadableStream = ponyfill.ReadableStream
// }

export default {
  directives: { elDragDialog },
  props: {
    sid: {
      type: String,
      default: 'terminal'
    },
    currentSid: {
      type: String,
      default: 'terminal'
    }
  },
  data() {
    return {
      // 当使用 zmodemjs 自带的 Zmodem.Browser.send_files 函数上传文件的时候,
      // 由于没有分段读取(不同浏览器默认读取方式可能不一样,比如 chrome 会分段读取
      // 但是,分段不固定,可能会很大,firefox 直接一次性读取,两种在读大文件时都很占
      // 内存),会很大几率导致服务端抛出数据包超时错误,好在 html5 FileReader 提供了
      // 分段读取 api,所以这里重写了 Zmodem.Browser.send_files 函数,使用分段读取的
      // 方式避免超时错误(也可以 rz -O 解决)。理论上来说,分段读取后就不会占用大内存,
      // 但是经过实际测试发现 chrome 84 在上传较大文件时还是会占用较大内存,最终出现 out
      // of memory 错误,而 firefox 78 这方面就做得比较好了,不会占用大内存,原因未知,可
      // 能是内部机制不一样。然后下载使用 streamsaver.js 写入流的方式以减小内存占用,但是
      // 由于 firefox 不支持 WritableStream 需要使用第三方库 web-streams-polyfill/ponyfill 弥补。
      // 并且经过测试发现 chrome 84 下载时还是会占用较大内存(但是比直接完全保存到内存再保存
      // 文件要小很多),而 firefox 会占用很大内存,原因猜测是 zmodem.js 这个库内存泄露吧。
      // 所以最优的使用方法是使用 chrome 下载,firefox 上传。
      // rz firefox 可以 4096, chrome 2048
      // sz firefox 可以 1024, chrome 2048
      // 并且根据最新的测试,如果直接分段读取文件,不管 chrome 或者 firefox 都非常快,并且
      // 不占用内存,如果分段读取后直接发送 websocket,这时候会占用约等同于文件大小的内存,估计
      // 是 websocket 网络发送速率跟不上读取速度,文件内容缓存在内存中的原因;然后如果直接从
      // websocket 读取二进制数据不经过 zmodem.js,而是直接使用 streamsaver 保存到文件,速度
      // 相对来说比较快,并且都不占用内存(chrome、firefox 都是)。故上面使用 zmodem.js 上传
      // 和下载文件所遇到的速度以及内存占用问题,估计是 websocket(缓存内容占用内存) 或者
      // zmodem.js 库的问题(内存泄露或者其他?)
      maxFileSize: 2048 * 1024 * 1024, // rz 最大上传文件大小
      maxFileSizeFireFox: 4096 * 1024 * 1024, // rz firefox 最大上传文件大小;firefox 分段读取支持好,可以上传更大文件
      maxDownloadFileSize: 2048 * 1024 * 1024, // sz 最大下载文件大小
      fileLimit: 50, // rz 一次性最大上传文件个数
      block: 1024 * 1024, // rz 上传分段读取大小
      webssh: '',
      terminal: '',
      // attachAddon: '',
      // fitAddon: '',
      // webglAddon: '',
      unicode11Addon: '',
      // webLinksAddon: '',
      searchAddon: '',
      terminalWidth: 0,
      // cols: 80,
      rows: 40,
      search: '',
      closed: false,
      wsServer: '',
      dialogUploadVisible: false,
      zsentry: '',
      zsession: '',
      selectFile: '',
      fileElement: '',
      host: '192.168.223.101',
      port: '22',
      user: 'root',
      pass: '123456',
      zmodemMode: false,
      keyDispose: '',
      lastOffset: 0,
      perOffset: 1024 * 1024
    }
  },
  computed: {
    cols: function() {
      if (this.terminalWidth === 0) {
        return Math.floor(document.getElementById(this.sid).offsetWidth / 9)
      }
      return Math.floor(this.terminalWidth / 9)
    }
  },
  watch: {
    currentSid(val) {
      // 监听父组件中 tab 标签页活动页,如果当前子组件是活动页,则注册当前组件的 id 到 windows.resize 事件
      // 以实现 webssh 动态改变窗口大小,不过经过测试发现改变窗口大小的操作有时会有点问题,比如会导致 top 命令显示错乱
      if (val === this.sid) {
        this.terminal.focus()
        this.setResize()
        this.terminalWidth = document.getElementById(this.sid).offsetWidth
      }
    },
    terminalWidth(val) {
      if (!this.zmodemMode) {
        this.webssh.send(JSON.stringify({ type: 'resize', cols: this.cols, rows: this.rows }))
        this.terminal.resize(this.cols, this.rows)
      }
    },
    selectFile(val) {
      if (val !== '') {
        this.fileElement._value = this.randomString(24)
      }
    }
  },
  mounted() {
    const browser = getBrowser()
    if (browser === 'Firefox') {
      this.maxFileSize = this.maxFileSizeFireFox
    }
    this.fileElement = document.getElementById('upload-' + this.sid)
    this.terminal = new Terminal({
      rendererType: 'canvas', // 渲染类型 dom,canvas,渲染速度:dom < canvas < webgl,兼容性反之,使用 webgl 的方式是设置 canvas,然后加载 WebglAddon 插件
      scrollback: 12800, // 终端回滚量
      cols: this.cols,
      rows: this.rows,
      useStyle: true,
      cursorStyle: 'block', // 光标样式 'block' | 'underline' | 'bar'
      cursorBlink: true,
      theme: {
        foreground: '#8eb1b2',
        background: '#002833'
        // cursor: 'help', // 设置光标
      }
    })
    const terminalContainer = document.getElementById(this.sid)
    this.terminal.open(terminalContainer)
    // 使用 fit 插件可能会导致 top 命令显示问题
    // this.fitAddon = new FitAddon()
    // this.terminal.loadAddon(this.fitAddon)
    // this.fitAddon.fit()
    // 部分浏览器目前默认没有开启 WebGL 支持,比如 chrome 81(firefox 75 可用)
    // 使用 webgl 渲染时可以解决部分汉字显示被截头的问题
    // if (this.checkWebgl2Support()) {
    //   this.webglAddon = new WebglAddon()
    //   this.terminal.loadAddon(this.webglAddon)
    // }
    this.unicode11Addon = new Unicode11Addon()
    this.terminal.loadAddon(this.unicode11Addon)
    // this.webLinksAddon = new WebLinksAddon()
    // this.terminal.loadAddon(this.webLinksAddon)
    this.searchAddon = new SearchAddon()
    this.terminal.loadAddon(this.searchAddon)
    this.terminal.focus()
    node().then(response => {
      this.wsServer = response.data.lists
      const wsUrl = `ws://${this.wsServer.ip}:${this.wsServer.http}/ws/webssh`
      // const wsUrl = `ws://${this.wsServer.ip}:${this.wsServer.http}/ws/webssh2`
      let token = ''
      if (store.getters.token) {
        token = getToken()
      }
      if (token === '') {
        this.webssh = new WebSocket(wsUrl)
      } else {
        this.webssh = new WebSocket(wsUrl, [token, 'webssh'])
      }
      // this.webssh = new WebSocket('ws://127.0.0.1:8000/api/ssh', ['webssh'])
      this.webssh.binaryType = 'arraybuffer'
      this.webssh.onopen = this.wsOnOpen
      this.webssh.onerror = this.wsOnError
      this.webssh.onclose = this.wsOnClose
      this.setResize()
      const that = this
      this.zsentry = new Zmodem.Sentry({
        to_terminal: (octets) => {
          //
        },
        on_detect: (detection) => {
          const zsession = detection.confirm()
          const zsessionType = zsession.type
          that.keyDispose = that.terminal.onKey(e => {
            if (zsessionType === 'receive') {
              const event = e.domEvent
              if (event.ctrlKey && event.key === 'c') {
                // 这种方式 ctrl + c 终止好像只能 sz 下载时工作正常
                console.log('ctrl + c 终止')
                detection.deny()
              }
            } else {
              const event = e.domEvent
              if (event.ctrlKey && event.key === 'c') {
                // console.log('ctrl + c 终止')
                // that.webssh.send(new Uint8Array([42, 42, 24, 66, 48, 56, 48, 48, 48, 48, 48, 48, 48, 48, 48, 50, 50, 100, 13, 10]))
                // that.webssh.send(new Uint8Array([79, 79]))
              }
            }
          })
          let promise
          if (zsessionType === 'receive') {
            // promise = that.downloadFile(zsession)
            promise = that.downloadFileStreamSaver(zsession)
          } else {
            promise = that.uploadFile(zsession)
          }
          try {
            promise.catch(console.error.bind(console)).then(() => {
              //
            })
          } catch (err) {
            console.log(err)
          }
        },
        on_retract: () => {
          //
        },
        sender: (octets) => {
          that.webssh.send(new Uint8Array(octets))
        }
      })
    }).catch(() => {
      //
    })
  },
  beforeDestroy() {
    this.webssh.close()
    this.terminal.dispose()
  },
  methods: {
    closeZsession() {
      try {
        // zsession 每 5s 发送一个 ZACK 包,5s 后会出现提示最后一个包是 ”ZACK“ 无法正常关闭
        // 这里直接设置 _last_header_name 为 ZRINIT,就可以强制关闭了
        this.zsession._last_header_name = 'ZRINIT'
        this.zsession.close()
      } catch (err) {
        console.log(err)
      }
    },
    dialogBeforeClose(done) {
      done()
      this.closeZsession()
    },
    handleExceed(files, fileList) {
      // 多文件上传
      if (files.length > this.fileLimit) {
        this.$message.error(`文件个数不能超过 ${this.fileLimit} 个`)
        return ''
      } else {
        const tmp = []
        for (let i = 0; i <= files.length - 1; i++) {
          const checkSize = files[i].size <= this.maxFileSize
          if (!checkSize) {
            this.$message.error('文件大小不能超过 ' + bytesHuman(this.maxFileSize, 1))
            return ''
          }
          tmp[i] = files[i]
        }
        this.selectFile = tmp
        this.dialogUploadVisible = false
      }
    },
    beforeUpload(file) {
      // 单文件上传
      const checkSize = file.size <= this.maxFileSize
      if (!checkSize) {
        this.$message.error('文件大小不能超过 ' + bytesHuman(this.maxFileSize, 1))
      } else {
        this.selectFile = file
        // 直接设置 dialogUploadVisible 隐藏对话框不会触发对话框的 before-close 回调
        this.dialogUploadVisible = false
      }
      return false
    },
    checkWebgl2Support() {
      const isWebGL2Supported = () => !!document.createElement('canvas').getContext('webgl2')
      return isWebGL2Supported()
    },
    setResize() {
      const that = this
      window.onresize = () => {
        return (() => {
          that.terminalWidth = document.getElementById(that.sid).offsetWidth
        })()
      }
    },
    utf8_to_b64(rawString) {
      return btoa(unescape(encodeURIComponent(rawString)))
    },
    b64_to_utf8(encodeString) {
      return decodeURIComponent(escape(atob(encodeString)))
    },
    randomString(len) {
      len = len || 32
      const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
      const maxPos = $chars.length
      let pwd = ''
      for (let i = 0; i < len; i++) {
        pwd += $chars.charAt(Math.floor(Math.random() * maxPos))
      }
      return pwd
    },
    searchContent() {
      if (this.search !== '') {
        this.searchAddon.findNext(this.search)
      }
    },
    uploadFile(zsession) {
      // const that = this
      this.zsession = zsession
      this.selectFile = ''
      this.dialogUploadVisible = true
      const that = this
      return new Promise((res) => {
        const upload = function(e) {
          that.terminal.setOption('disableStdin', true)
          that.zmodemMode = true
          let files_obj
          if (typeof that.selectFile.length === 'undefined') {
            files_obj = [that.selectFile]
          } else {
            files_obj = that.selectFile
          }
          that.sendBlockFile(zsession, files_obj, {
          // that.sendFile(zsession, files_obj, {
          // Zmodem.Browser.send_files(zsession, files_obj, {
            on_offer_response(obj, xfer) {
              if (xfer) {
                // that.terminal.write('\r\n')
              } else {
                that.terminal.write(obj.name + ' was skipped upload\r\n')
              }
            },
            on_progress(obj, xfer) {
              that.updateProgress(xfer, 'upload')
            },
            on_file_complete(obj) {
              that.terminal.write('\r\n')
            }
          }).then(zsession.close.bind(zsession), console.error.bind(console)
          ).then(() => {
            res()
            that.terminal.setOption('disableStdin', false)
            that.keyDispose.dispose()
            that.keyDispose = ''
            that.zmodemMode = false
          })
        }

        that.fileElement.addEventListener('onchange', upload, false)
        Object.defineProperty(that.fileElement, '_value', {
          configurable: true,
          set: function(value) {
            this.value = value
            upload()
          },
          get: function() {
            return this.value
          }
        })
      })
    },
    downloadFileStreamSaver(zsession) {
      const that = this
      zsession.on('offer', (xfer) => {
        if (xfer.get_details().size > that.maxDownloadFileSize) {
          xfer.skip()
          that.$message({
            type: 'error',
            message: xfer.get_details().name + ' 文件大小超过: ' + bytesHuman(that.maxDownloadFileSize, 1) + ', 跳过'
          })
          return
        }
        that.terminal.setOption('disableStdin', true)
        that.zmodemMode = true
        const fileStream = streamSaver.createWriteStream(xfer.get_details().name, {
          size: xfer.get_details().size
        })
        const writer = fileStream.getWriter()
        xfer.on('input', (payload) => {
          that.updateProgress(xfer, 'download')
          writer.write(new Uint8Array(payload))
        })
        xfer.accept().then(
          () => {
            that.terminal.write('\r\n')
            writer.close()
          },
          console.error.bind(console)
        )
        // function makeReadableZsessionStream() {
        //   return new ReadableStream({
        //     start(ctrl) {
        //       xfer.on('input', (payload) => {
        //         that.updateProgress(xfer, 'download')
        //         ctrl.enqueue(new Uint8Array(payload))
        //       })
        //       xfer.accept().then(
        //         () => {
        //           ctrl.close()
        //           that.terminal.write('\r\n')
        //         },
        //         console.error.bind(console)
        //       )
        //     },
        //     cancel() {
        //       //
        //     }
        //   })
        // }
        // const readable = makeReadableZsessionStream()
        // readable.pipeTo(fileStream).then(() => {
        //   console.log('success write file')
        // }, (err) => {
        //   console.log(err, 'failed write file')
        // })
      })
      const promise = new Promise((res) => {
        zsession.on('session_end', () => {
          res()
          that.terminal.setOption('disableStdin', false)
          that.keyDispose.dispose()
          that.keyDispose = ''
          that.zmodemMode = false
        })
      })
      zsession.start()
      return promise
    },
    downloadFile(zsession) {
      const that = this
      zsession.on('offer', (xfer) => {
        if (xfer.get_details().size > that.maxDownloadFileSize) {
          xfer.skip()
          that.$message({
            type: 'error',
            message: xfer.get_details().name + ' 文件大小超过: ' + bytesHuman(that.maxDownloadFileSize, 1) + ', 跳过'
          })
          return
        }
        that.terminal.setOption('disableStdin', true)
        that.zmodemMode = true
        const fileBuffer = []
        xfer.on('input', (payload) => {
          that.updateProgress(xfer, 'download')
          fileBuffer.push(new Uint8Array(payload))
        })
        xfer.accept().then(
          () => {
            that.saveFile(xfer, fileBuffer)
            that.terminal.write('\r\n')
          },
          console.error.bind(console)
        )
      })
      const promise = new Promise((res) => {
        zsession.on('session_end', () => {
          res()
          that.terminal.setOption('disableStdin', false)
          that.keyDispose.dispose()
          that.keyDispose = ''
          that.zmodemMode = false
        })
      })
      zsession.start()
      return promise
    },
    async updateProgressX(xfer, action = 'download') {
      // if (typeof action === 'undefined') action = 'download'
      const detail = xfer.get_details()
      const name = detail.name
      const total = detail.size
      const offset = xfer.get_offset()
      let percent
      if (total === 0 || offset === total) {
        percent = 100
      } else {
        percent = Math.round(offset / total * 100)
      }
      this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' ' + percent + '% ')
    },
    async updateProgress(xfer, action) {
      if (typeof action === 'undefined') action = 'download'
      const detail = xfer.get_details()
      const name = detail.name
      const total = detail.size
      const offset = xfer.get_offset()
      if ((offset - this.lastOffset) >= this.perOffset) {
        this.lastOffset = offset
        let percent
        if (total === 0 || offset === total) {
          percent = 100
          this.lastOffset = 0
        } else {
          percent = Math.round(offset / total * 100)
        }
        this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' ' + percent + '% ')
      } else {
        if (total === 0 || offset === total) {
          this.terminal.write('\r' + action + ' ' + name + ': ' + bytesHuman(offset, 1) + '/' + bytesHuman(total, 1) + ' 100% ')
          this.lastOffset = 0
        }
      }
    },
    saveFile(xfer, buffer) {
      return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name)
    },
    sendBlockFile(session, files, options) {
      // 分段上传,可以读取大文件,网页不会因内存占用奔溃
      const that = this
      if (!options) options = {}
      const batch = []
      let total_size = 0
      for (let f = files.length - 1; f >= 0; f--) {
        const fobj = files[f]
        total_size += fobj.size
        batch[f] = {
          obj: fobj,
          name: fobj.name,
          size: fobj.size,
          mtime: new Date(fobj.lastModified),
          files_remaining: files.length - f,
          bytes_remaining: total_size
        }
      }
      let file_idx = 0
      function promise_callback() {
        const cur_b = batch[file_idx]
        if (!cur_b) {
          return Promise.resolve()
        }
        file_idx++
        return session.send_offer(cur_b).then(function after_send_offer(xfer) {
          if (options.on_offer_response) {
            options.on_offer_response(cur_b.obj, xfer)
          }
          if (xfer === undefined) {
            return promise_callback()
          }
          return new Promise(function(res) {
            const fileSize = cur_b.size // 文件总大小
            let fileLoaded = 0 // 当前已读取大小
            const reader = new FileReader()
            reader.onerror = function reader_onerror(e) {
              console.error('file read error', e)
              throw ('File read error: ' + e)
            }
            function readBlob() {
              // 每次读取一个 block
              let blob
              if (cur_b.obj.slice) {
                blob = cur_b.obj.slice(fileLoaded, fileLoaded + that.block + 1)
              } else if (cur_b.obj.mozSlice) {
                blob = cur_b.obj.mozSlice(fileLoaded, fileLoaded + that.block + 1)
              } else if (cur_b.obj.webkitSlice) {
                blob = cur_b.obj.webkitSlice(fileLoaded, fileLoaded + that.block + 1)
              } else {
                blob = cur_b.obj
              }
              reader.readAsArrayBuffer(blob)
            }
            reader.onload = function reader_onload(e) {
              fileLoaded += e.total
              if (fileLoaded < fileSize) {
                if (e.target.result) {
                  const piece = new Uint8Array(e.target.result)
                  that.checkAborted(session)
                  xfer.send(piece)
                  if (options.on_progress) {
                    options.on_progress(cur_b.obj, xfer, piece)
                  }
                }
                readBlob()
              } else {
                if (e.target.result) {
                  const piece = new Uint8Array(e.target.result)
                  that.checkAborted(session)
                  xfer.end(piece).then(function() {
                    if (options.on_progress && piece.length) {
                      options.on_progress(cur_b.obj, xfer, piece)
                    }
                    if (options.on_file_complete) {
                      options.on_file_complete(cur_b.obj, xfer)
                    }
                    res(promise_callback())
                  })
                }
              }
            }
            readBlob()
          })
        })
      }
      return promise_callback()
    },
    sendFile(session, files, options) {
      // FileReader 默认分段读取,分段大小不定,会占用较大内存
      // 还会造成大文件上传时超时错误
      const that = this
      if (!options) options = {}
      const batch = []
      let total_size = 0
      for (let f = files.length - 1; f >= 0; f--) {
        const fobj = files[f]
        total_size += fobj.size
        batch[f] = {
          obj: fobj,
          name: fobj.name,
          size: fobj.size,
          mtime: new Date(fobj.lastModified),
          files_remaining: files.length - f,
          bytes_remaining: total_size
        }
      }
      let file_idx = 0
      function promise_callback() {
        const cur_b = batch[file_idx]
        if (!cur_b) {
          return Promise.resolve()
        }
        file_idx++
        return session.send_offer(cur_b).then(function after_send_offer(xfer) {
          if (options.on_offer_response) {
            options.on_offer_response(cur_b.obj, xfer)
          }
          if (xfer === undefined) {
            return promise_callback()
          }
          return new Promise(function(res) {
            const reader = new FileReader()
            reader.onerror = function reader_onerror(e) {
              console.error('file read error', e)
              throw ('File read error: ' + e)
            }
            let piece
            reader.onprogress = function reader_onprogress(e) {
              if (e.target.result) {
                piece = new Uint8Array(e.target.result, xfer.get_offset())
                that.checkAborted(session)
                xfer.send(piece)
                if (options.on_progress) {
                  options.on_progress(cur_b.obj, xfer, piece)
                }
              }
            }
            reader.onload = function reader_onload(e) {
              piece = new Uint8Array(e.target.result, xfer, piece)
              that.checkAborted(session)
              xfer.end(piece).then(function() {
                if (options.on_progress && piece.length) {
                  options.on_progress(cur_b.obj, xfer, piece)
                }
                if (options.on_file_complete) {
                  options.on_file_complete(cur_b.obj, xfer)
                }
                res(promise_callback())
              })
            }
            reader.readAsArrayBuffer(cur_b.obj)
          })
        })
      }
      return promise_callback()
    },
    checkAborted(session) {
      if (session.aborted()) {
        throw new Zmodem.Error('aborted')
      }
    },
    sendData(data) {
      this.webssh.send(JSON.stringify({ type: 'stdin', data: this.utf8_to_b64(data) }))
    },
    wsOnOpen() {
      this.webssh.send(JSON.stringify({ type: 'addr', data: this.utf8_to_b64(this.host + ':' + this.port) }))
      this.webssh.send(JSON.stringify({ type: 'login', data: this.utf8_to_b64(this.user) }))
      this.webssh.send(JSON.stringify({ type: 'password', data: this.utf8_to_b64(this.pass) }))
      this.webssh.send(JSON.stringify({ type: 'resize', cols: this.cols, rows: this.rows }))
      this.terminal.resize(this.cols, this.rows)
      // this.attachAddon = new AttachAddon(this.webssh)
      // this.terminal.loadAddon(this.attachAddon)
      this.webssh.onmessage = this.wsOnMessage
      this.terminal.onData(this.sendData)
    },
    wsOnMessage(recv) {
      if (typeof recv.data === 'object') {
        this.zsentry.consume(recv.data)
      } else {
        try {
          const msg = JSON.parse(recv.data)
          switch (msg.type) {
            case 'stdout':
            case 'stderr':
              this.terminal.write(this.b64_to_utf8(msg.data))
              // this.terminal.writeUtf8(this.b64_to_utf8(msg.data))
              break
            case 'console':
              console.log(this.b64_to_utf8(msg.data))
              break
            case 'alert':
              this.$message.warning(this.b64_to_utf8(msg.data))
              break
            case 'disableStdin':
              this.terminal.setOption('disableStdin', true)
              break
            case 'enableStdin':
              this.terminal.setOption('disableStdin', false)
              break
            default:
              console.log('unsupport type msg', msg)
          }
        } catch (e) {
          console.log(e, 'unsupport data', recv.data)
        }
      }
    },
    wsOnError(err) {
      this.closed = true
      this.$message.error(`${this.sid} connect error`)
      this.terminal.write('connect error\r\n')
      console.log(err)
    },
    wsOnClose() {
      this.closed = true
      this.$message.error(`${this.sid} disconnect`)
      this.terminal.write('disconnect\r\n')
      // this.attachAddon.dispose()
    }
  }
}
</script>

这是我写的,是可以正常工作的,只能帮你到这了

G-Pig commented 3 years ago

我看你引入的时候 import * as Zmodem from 'zmodem.js/src/zmodem_browser' 这个文件从哪来的?我在你的项目中没有看到,你之后也是直接new Zmodem.Sentry(),我也是跟你写的一样的但会报错,所以我怀疑就是在引入zmodem的时候没引入对导致的报错

G-Pig commented 3 years ago

噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。

leffss commented 3 years ago

噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。

image

G-Pig commented 3 years ago

噢噢噢噢 我知道了 原来安装的依赖名字叫做zmodem.js.... 我一直安装别的名字安装不上去.. - -。

image

好的,感谢大佬。自己一直试着安装依赖,试了zmodemjs、zmodem-js、zmodem-browser之类的...唯独没有试zmodem.js。。。感谢大佬的醍醐灌顶~