trzsz / trzsz.js

trzsz.js is the js version of trzsz, makes terminal built with electron and webshell to support trzsz ( trz / tsz ).
https://trzsz.github.io/js
MIT License
188 stars 12 forks source link

大佬问个问题 The browser doesn't support the File System Access API #9

Closed zundaren closed 1 year ago

zundaren commented 1 year ago

mac系统,我使用 xtermjs5.1.0,trzszjs, go wails,浏览器端可以正常打开文件窗口,但是打包成客户端后无法打开文件选择窗口, 我使用的antdv自带的上传组件是可以打开选择窗口的,这个要怎么弄下呢

lonnywong commented 1 year ago

我猜:不行的时候,url 不是 localhost 或 127.0.0.1 了吧?

非 localhost 或 127.0.0.1 时,不支持 http,只能用 https,这个是浏览器的限制。

zundaren commented 1 year ago

我猜:不行的时候,url 不是 localhost 或 127.0.0.1 了吧?

非 localhost 或 127.0.0.1 时,不支持 http,只能用 https,这个是浏览器的限制。

等下研究下这个协议问题,我看tabby里面trzsz插件好像也是你写的,里面是通过electron的api调文件窗口,这个不改源码可以实现吗,让go去打开窗口把结果给到trsz执行上传下载 (能否搞个选项1.浏览器自动操作,2.用户自定义打开窗口操作,3.其他)灵感来了

zundaren commented 1 year ago

我先找下有没有go的trzsz协议,不用js处理试试看

zundaren commented 1 year ago

我猜:不行的时候,url 不是 localhost 或 127.0.0.1 了吧?

非 localhost 或 127.0.0.1 时,不支持 http,只能用 https,这个是浏览器的限制。

gowails框架打包用的自定义的协议。。,但是为啥ui框自带的文件上传组件能打开窗口,都是在webkit环境中运行[https://github.com/vueComponent/ant-design-vue/blob/main/components/vc-upload/traverseFileTree.ts

lonnywong commented 1 year ago

go 可能可以用 https://github.com/trzsz/trzsz-go ,不过我在开发的时候,没把它设计成一个组件。 现在客户端是编译出一个 trzsz 可执行程序,如果封装好一点,应该也可以让其他程序引用。

客户端相关的代码主要在:https://github.com/trzsz/trzsz-go/blob/main/trzsz/trzsz.go

zundaren commented 1 year ago

go 可能可以用 https://github.com/trzsz/trzsz-go ,不过我在开发的时候,没把它设计成一个组件。 现在客户端是编译出一个 trzsz 可执行程序,如果封装好一点,应该也可以让其他程序引用。

客户端相关的代码主要在:https://github.com/trzsz/trzsz-go/blob/main/trzsz/trzsz.go

好滴,我研究下。 我查了下 window.showOpenFilePicker这些方法在不同浏览器兼容性不一样,如果创建input标签对话框能实现和前面一样的功能,那可以在掉不通系统api的情况下启用这个备选方案,这个可行不

lonnywong commented 1 year ago

input 标签只能实现上传,实现不了下载。

lonnywong commented 1 year ago

@zundaren 你的 js 是运行在 nodejs 环境里,还是运行在纯浏览器的环境里?如果是 nodejs 环境,可以自己实现弹出对话框的逻辑,和 electron 的实现类似,可参考 https://github.com/trzsz/trzsz.js/tree/main/examples/electron

如果 go 能控制远程服务器的输入和输出,可能可以使用 https://github.com/trzsz/trzsz-go ,trzsz-go 应该要稍微改一下,需要研究研究怎么改更易对接。

zundaren commented 1 year ago

@zundaren 你的 js 是运行在 nodejs 环境里,还是运行在纯浏览器的环境里?如果是 nodejs 环境,可以自己实现弹出对话框的逻辑,和 electron 的实现类似,可参考 https://github.com/trzsz/trzsz.js/tree/main/examples/electron

如果 go 能控制远程服务器的输入和输出,可能可以使用 https://github.com/trzsz/trzsz-go ,trzsz-go 应该要稍微改一下,需要研究研究怎么改更易对接。

是在webkit内核运行,嵌入到go里面的,刚发现了两个设置项应该可以解决,感谢大佬帮忙 chooseSendFiles?: (directory?: boolean) => Promise<string[] | undefined>; chooseSaveDirectory?: () => Promise<string | undefined>;

lonnywong commented 1 year ago

这两个接口要求 js 能直接操作文件系统的,webkit 可能没权限,你可以试试看。

lonnywong commented 1 year ago

如果是 node-webkit 应该就可以。

zundaren commented 1 year ago

如果是 node-webkit 应该就可以。

框架换不了,写太多代码了,chooseSendFiles这个方法我直接拦截trzsz的命令选文件发送是不是就行了,还需要解析协议之类的吗

lonnywong commented 1 year ago

只要实现那两个接口就行了,会自动回调它们的。如果不回调,可能是被判定为没有 fs 包,不能直接操作文件系统了。

lonnywong commented 1 year ago

node webkit 按理说是兼容 webkit 的,有可能你一行代码都不用改,就可以换成它。

zundaren commented 1 year ago

node webkit 按理说是兼容 webkit 的,有可能你一行代码都不用改,就可以换成它。

..刚刚那两个接口没暴露出来调用不了. gowails windows下是用的webview2,可以正常打开上传下载,mac上面不清楚怎么玩,这个node webkit应该是用不了,程序打包就一个文件,双击直接就运行了,都调用不了其他的东西。。。而且这个一百兆体积太大了,我程序才20m环境就要这么大~

lonnywong commented 1 year ago

node webkit 是一个 sdk,应该是你的代码依赖它,可能原来的 webkit 就可以去掉了,然后编译出来,就和原来编译出来的一样了。我也没实际用过,猜的。

lonnywong commented 1 year ago

我去看了一下 https://wails.io/docs/howdoesitwork 你用这个的话,是不好换成 node webkit。而 webkit 是不能操作文件系统的。不过,我从 wails 的架构图看出,js 可以调 go ,而 go 是可以操作文件系统的。

zundaren commented 1 year ago

我去看了一下 https://wails.io/docs/howdoesitwork 你用这个的话,是不好换成 node webkit。而 webkit 是不能操作文件系统的。不过,我从 wails 的架构图看出,js 可以调 go ,而 go 是可以操作文件系统的。

go打开窗口操作我都能做,就是js打不开窗口go也不能插手很难瘦,中午还研究了下用trzszgo做中转,没搞成功。。

lonnywong commented 1 year ago

trzsz-go 应该也是可以搞的,我要知道你是怎么与远程服务器交互的,才能知道怎么搞。

js 这个也是可以搞的,因为 js 可以调 go,你需要把读写文件的函数用 go 提供出来,不单单只是打开窗口,可能有一点复杂。

zundaren commented 1 year ago

trzsz-go 应该也是可以搞的,我要知道你是怎么与远程服务器交互的,才能知道怎么搞。

js 这个也是可以搞的,因为 js 可以调 go,你需要把读写文件的函数用 go 提供出来,不单单只是打开窗口,可能有一点复杂。

就是xtermjs+websocket 和go ssh做了绑定,现在trz->websoket->trzszfilter->terminal, 我对js也不是很懂,如果可以继承trzsz修改读写文件窗口等方法就好办了,想拖拽也是读写文件,想着js读写应该没有问题啊。

lonnywong commented 1 year ago

问题是,在 webkit 里 js 没权限直接读写文件。

websocket 的读写是 go 负责吗?

zundaren commented 1 year ago

问题是,在 webkit 里 js 没权限直接读写文件。

websocket 的读写是 go 负责吗?

对,js部分只负责渲染,大部分文件,ssh操作都是调用的go

lonnywong commented 1 year ago

那 go 应该也是可以搞的,不过 go 版我没有像 js 这样实现一个 Filter,所以对接可能会麻烦一些。不过 go 版现在领先于 js 版,它的传输速度会快一些。你的软件是开源的吗?可能简单看一下代码才知道具体怎么搞。

用 js 版的话,我刚看了 wails 的文档,大概知道怎么搞了,对接会比现在的 go 版简单一点。js 版要追上 go 版的传输速度的话,可能要较长的时间,等我有空了才会搞。

zundaren commented 1 year ago

那 go 应该也是可以搞的,不过 go 版我没有像 js 这样实现一个 Filter,所以对接可能会麻烦一些。不过 go 版现在领先于 js 版,它的传输速度会快一些。你的软件是开源的吗?可能简单看一下代码才知道具体怎么搞。

用 js 版的话,我刚看了 wails 的文档,大概知道怎么搞了,对接会比现在的 go 版简单一点。js 版要追上 go 版的传输速度的话,可能要较长的时间,等我有空了才会搞。

我的主页有个go zmodem协议的demo,也是类似过滤器一样的,大佬先休息吧

lonnywong commented 1 year ago

是不是都用 golang.org/x/crypto/ssh 连接远程服务器的?

zundaren commented 1 year ago

是不是都用 golang.org/x/crypto/ssh 连接远程服务器的?

是的

lonnywong commented 1 year ago

发现 go ssh 不像一个正常的终端,ctrl + c 之类都不可用,https://stackoverflow.com/questions/28921409/how-can-i-send-terminal-escape-sequences-through-ssh-with-go

最大的两个问题: 1、服务器输出 \n,go ssh 总是转换成 \r\n。 2、输入的内容,总是会 echo 回显。 这两个问题不管怎么设置都没用。

不知你能不能替换成其他的,如 https://github.com/creack/pty

zundaren commented 1 year ago

发现 go ssh 不像一个正常的终端,ctrl + c 之类都不可用,https://stackoverflow.com/questions/28921409/how-can-i-send-terminal-escape-sequences-through-ssh-with-go

最大的两个问题: 1、服务器输出 \n,go ssh 总是转换成 \r\n。 2、输入的内容,总是会 echo 回显。 这两个问题不管怎么设置都没用。

不知你能不能替换成其他的,如 https://github.com/creack/pty

回显我设置的1,需要和前端做映射,\n和\r\n主要区分是换行,回车(光标位置),ctrl+c目前我通过xtermjs传给服务器也是没问题的,感觉你说的问题是有特殊字符要解码。我也研究下pty看能不能和xtermjs结合

pty好像是个本地客户端,gossh那个产生的是个服务器伪终端

lonnywong commented 1 year ago

我发现 go ssh 上下箭头键也是不能用的,设置了 tty 的属性也没用,很奇怪。

zundaren commented 1 year ago

我发现 go ssh 上下箭头键也是不能用的,设置了 tty 的属性也没用,很奇怪。

xtermjs操作,接收data,websocket转发,copy websocket的[]byte到ssh的输入管道,目前上下左右,ctrl+c等操作就和xshell一样的, 是不是你的终端尺寸resize没发给服务器同步

lonnywong commented 1 year ago

可能是我直接编译成一个控制台程序有关系。方便的话,发个 xterm.js 的 demo 源码来看看?

zundaren commented 1 year ago

可能是我直接编译成一个控制台程序有关系。方便的话,发个 xterm.js 的 demo 源码来看看?

代码主要的通信就是下面的 github.com/gorilla/websocket

<template>
    <div ref="termRef" @dragover.prevent @drop.prevent="drag" @contextmenu.prevent="pasteFromClip($event)" @mouseup.middle="copy2clip(term.getSelection())"</div>
</template>

<script setup lang="ts">

let ws: WebSocket
let trzsz: TrzszFilter
let term: Terminal
const termRef = ref<HTMLElement>()

const fitAddon = new FitAddon()
const searchAddon = new SearchAddon()
let canvasAddon = new CanvasAddon();
const webglAddon = new WebglAddon();
webglAddon.onContextLoss(e => {
  webglAddon.dispose();
  if (term) {
    term.loadAddon(canvasAddon)
  }
});

function createWs() {
  if (prop.cfg.port == "") {
    throw "port is null"
  }

  ws = new WebSocket("ws://localhost:" + prop.cfg.port + "/ssh?clientId=" + prop.clientId);
  ws.onopen = ev => {
    createTerm()
    resize()
    init_trzsz()
  }
  ws.onerror = e => {
    console.error("ws onError:", e)
    closeAll()
  }
  ws.onclose = e => {
    closeAll()
  }
  ws.onmessage = e => {
    if (typeof e.data === "string") {
      trzsz.processServerOutput(e.data)
    } else if (e.data instanceof Blob) {
      // let reader = new FileReader();
      // reader.readAsText(data, "utf-8")
      // reader.onloadend = ev => {
      //   // let msg = JSON.parse(reader.result as string)
      // }
    }
  }

}

function init_trzsz() {
  trzsz = new TrzszFilter({
    sendToServer: (data) => ws.send(data),
    writeToTerminal: (data) => {
      if (typeof data === "string") {
        term.write(data)
      }
    },
  });
}

function createTerm() {
  let {theme, fontSize, fontFamily} = mergeTheme();

  term = new Terminal({
    cols: tcols.value,
    disableStdin: false,
    letterSpacing: 0, // 字符间距
    lineHeight: 1.2,
    fontSize: fontSize,
    fontFamily: fontFamily,
    fontWeight: 500,
    cursorBlink: true,
    cursorStyle: 'block',
    convertEol: true, //启用时,光标将设置为下一行的开头
    scrollback: 10000,   //终端中的回滚量
    windowsMode: false,
    allowProposedApi: true,
    theme: theme,
  });

  term.onData((data) => {
    if (ws) {
      if (prop.sendAll) {
        SSHApi.SendAll(data as string)
      } else {
        trzsz.processTerminalInput(data)
      }
    }
  })

  term.onBinary((data) => trzsz.processBinaryInput(data))

  term.loadAddon(fitAddon)
  term.loadAddon(searchAddon)
  term.loadAddon(webglAddon)

  term.open(termRef.value as HTMLElement)
  term.focus()

  window.addEventListener('resize', resize)

  return term
}

function drag(e: DragEvent) {
  if (!e.dataTransfer) {
    return
  }
  trzsz.uploadFiles(e.dataTransfer.items).then(() => success()).catch((err) => console.log(err));
}

function sendBinaryData(obj: BinaryData) {
  try {
    ws.send(new Blob([JSON.stringify(obj)], {type : 'application/json'}))
  } catch (e: unknown) {
    console.error(e)
    LogError("js ws sendBinaryData exception: " + String(e))
  }
}

function setTermVh100() {
  let ele = term.element as HTMLElement;
  let attribute = ele.getAttribute("class") as string
  let ss = attribute.split(" ");
  if (!ss.includes("vh100")) {
    ss.push("vh100")
    ele.setAttribute("class", ss.join(" "))
  }
}

const resize = useDebounceFn(() => {
  const termResize = () => {
    try {
      fitAddon.fit()
      term.resize(tcols.value, trows.value)
      trzsz.setTerminalColumns(tcols.value)
      sendBinaryData({type: "windowSize", high: term.rows, width: term.cols} as WindowSize)
      setTermVh100()
      calcSuspPos()
    }catch (e) {
      console.error(e)
    }
  }

  let count = 0
  let result: number[] = []
  let timer = setInterval(function () {
    if (count > 180) {
      clearInterval(timer)
      closeAll()
      return
    }
    if (result.length != 0 && result.pop() == 1) {
      clearInterval(timer)
      termResize()
      count = 0
      return
    }

    count++

    let cw = Number(term.textarea?.style.width?.replace("px", ""))
    if (cw != 0) {
      let ch = term.textarea?.offsetHeight as number
      tcols.value = Math.floor(prop.w as number / cw)
      trows.value = Math.floor(prop.h as number / ch)
      result.push(1)
    } else {
      result.push(0)
    }
  }, 10)

}, 50)

function pasteFromClip(e: any) {
  ClipboardGetText().then(v => {
    term.paste(v)
  })
}

onMounted(() => {
  createWs()
})

watch([prop], (value: any, oldValue: any, onCleanup: any) => {
  if (prop.dragging) {
    setTermVh100()
    return
  }
  resize()
})

</script>

// ======================================================================================
// ======================================================================================go

func WsServer() {
    gin.SetMode(gin.ReleaseMode)
    router := gin.New()
    router.RedirectTrailingSlash = true
    router.Use(cors.Default())

    router.GET("/ssh", func(c *gin.Context) {
        cli, b := GetConnector(c.Query("clientId"))
        if !b {
            return
        }

        conn, err := upgrade.Upgrade(c.Writer, c.Request, nil)
        if err != nil {
                return
        }
        conn.EnableWriteCompression(true)
        err = conn.SetCompressionLevel(5)
        if err != nil {
            return
        }

        go cli.BridgeWS(conn)
    })

    server := &http.Server{
        Addr:              fmt.Sprintf("127.0.0.1:%s", "8888"),
        Handler:           router,
        ReadHeaderTimeout: 0,
    }
    go server.ListenAndServe()
}

func (c *SshConnector) BridgeWS(conn *websocket.Conn) {
    c.Ws.Conn = conn

    _ = c.Ws.Conn.SetReadDeadline(time.Now().Add(messageWait))
    msgType, msg, err := c.Ws.Conn.ReadMessage()
    if err != nil {
        return
    }
    if msgType != websocket.BinaryMessage {
        return
    }

    wdSize := new(windowSize)
    if err = json.Unmarshal(msg, wdSize); err != nil {
        return
    }

    c.Ws.Session, err = c.NativeSsh.NewSession()
    if err != nil {
        return
    }
    c.Ws.Session.Stderr = os.Stderr

    inPipe, err := c.Ws.Session.StdinPipe()
    if err != nil {
        return
    }
    outPipe, err := c.Ws.Session.StdoutPipe()
    if err != nil {
        return
    }
    c.Ws.InPipe = inPipe
    c.Ws.OutPipe = outPipe

    if err := c.Ws.Session.RequestPty("xterm-256color", wdSize.High, wdSize.Width, terminalModes); err != nil {
        g.Log.Error(fmt.Sprintf("ssh session RequestPty error: %+v", err))
        return
    }
    if err := c.Ws.Session.Shell(); err != nil {
        g.Log.Error(fmt.Sprintf("ssh session Shell error: %+v", err))
        return
    }

    go func() {
        wsRead(c)
    }()

    go func() {
        wsWrite(c)
    }()
}

func wsWrite(c *SshConnector) {
    for {
        bytes := bufPool.Get().([]byte)
        n, err := c.Ws.OutPipe.Read(bytes)
        if err != nil {
            return
        }

        if n > 0 {
            _ = c.Ws.Conn.SetWriteDeadline(time.Now().Add(messageWait))
            if err := c.Ws.Conn.WriteMessage(websocket.TextMessage, c.Decode(bytes[:n])); err != nil {
                return
            }
        }

        bufPool.Put(bytes)
        time.Sleep(200 * time.Microsecond)
    }
}

func wsRead(c *SshConnector) {
    var infiniteWait time.Time
    _ = c.Ws.Conn.SetReadDeadline(infiniteWait)

    go func() {
        time.Sleep(100 * time.Millisecond)
        _, _ = c.Ws.InPipe.Write([]byte("export PS1='[\\u@\\h \\W]\\$ ' \r"))
    }()

    for {
        msgType, data, err := c.Ws.Conn.ReadMessage()
        if err != nil {
            return
        }
        if msgType != websocket.BinaryMessage {
            _, err = c.Ws.InPipe.Write(data)
            if err != nil {
                return
            }
            continue
        }

        p := make(map[string]any, 0)
        if err = json.Unmarshal(data, &p); err != nil {
            g.Log.Error(fmt.Sprintf("ws read binarymessage decode error %v", err))
            return
        }

        //fmt.Printf("%v\n", p)
        v, ok := p["type"]
        if !ok || v == "" {
            return
        }

        switch v {
        case HeartType:
            _ = c.Ws.Conn.SetWriteDeadline(time.Now().Add(messageWait))
            msg := ujson.ToBytes(PongMsg{Type: HeartType})
            if err := c.Ws.Conn.WriteMessage(websocket.BinaryMessage, msg); err != nil {
                return
            }
        case WindowSizeType:
            wdSize := &windowSize{}
            _ = maputil.MapTo(p, wdSize)
            if err := c.Ws.Session.WindowChange(wdSize.High, wdSize.Width); err != nil {
                break
            }
        }
    }

}
lonnywong commented 1 year ago

打包一份能编译运行的(最好精简一下,把不影响基础运行的去掉),发到我的邮箱?

lonnywong@qq.com

zundaren commented 1 year ago

打包一份能编译运行的(最好精简一下,把不影响基础运行的去掉),发到我的邮箱?

lonnywong@qq.com

go的trzszfilter我写了一个,上传下载都没问题,就是里面进度条,传输协议都是copy出来改改,你的代码升级了使用者不好改,感觉这部分还是需要拆出来一个公共的依赖,协议对接输入输出管道,进度条单独设置管道

lonnywong commented 1 year ago

我周末重构后,会提供一个 go 版的 filter 接口。

lonnywong commented 1 year ago

@zundaren go 版 filter 代码已提交 https://github.com/trzsz/trzsz-go/commit/2af6c381d37a986ced4a57773894fc7a6ffa0c50

zundaren commented 1 year ago

@zundaren go 版 filter 代码已提交 trzsz/trzsz-go@2af6c38

上传下载拖拽都测过,暂时没什么大问题,

服务器直接yum安装的,测了两个服务器,ctrl+c的显示一个有报错,一个stoped,功能没什么问题 image

这个客户端的版本和服务器端的版本是否需要一致,如果后期服务器软件升级,连接协议这块有没有什么要注意的

lonnywong commented 1 year ago

不需要一致,会往前兼容的。

lonnywong commented 1 year ago

那个问题好复现不?如果可以复现,试试启用 log,下面这样:

trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serverIn, serverOut, trzsz.TrzszOptions{
  TerminalColumns: width,
  DetectTraceLog: true,
})

登录服务器之后,先执行 echo -e '<ENABLE_TRZSZ_TRACE_LOG\x3E',然后你会看到一个日志文件路径,在本地电脑上的。 然后传文件,如果能复现,把日志文件发给我看看,可以发到我邮箱。

zundaren commented 1 year ago

那个问题好复现不?如果可以复现,试试启用 log,下面这样:

trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serverIn, serverOut, trzsz.TrzszOptions{
  TerminalColumns: width,
  DetectTraceLog: true,
})

登录服务器之后,先执行 echo -e '<ENABLE_TRZSZ_TRACE_LOG\x3E',然后你会看到一个日志文件路径,在本地电脑上的。 然后传文件,如果能复现,把日志文件发给我看看,可以发到我邮箱。

那个问题好复现不?如果可以复现,试试启用 log,下面这样:

trzszFilter = trzsz.NewTrzszFilter(clientIn, clientOut, serverIn, serverOut, trzsz.TrzszOptions{
  TerminalColumns: width,
  DetectTraceLog: true,
})

登录服务器之后,先执行 echo -e '<ENABLE_TRZSZ_TRACE_LOG\x3E',然后你会看到一个日志文件路径,在本地电脑上的。 然后传文件,如果能复现,把日志文件发给我看看,可以发到我邮箱。

我的问题,把异常随便写了errors.new, 应该要用newSimpleTrzszError image