Skip to content

Commit

Permalink
support zmodem lrzsz ( rz / sz )
Browse files Browse the repository at this point in the history
  • Loading branch information
lonnywong committed Dec 3, 2023
1 parent b45bed0 commit fc290aa
Show file tree
Hide file tree
Showing 14 changed files with 637 additions and 155 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ DefaultDownloadPath = /Users/username/Downloads/
- If the `DefaultDownloadPath` is not empty, downloading files will be saved to the path automatically instead of asking each time.
## Zmodem support
- Use `-z` or `--zmodem` to enable the `rz / sz` feature. e.g., `trzsz -z ssh remote_server`.
- `lrzsz` needs to be installed on the client ( local computer ). e.g., `brew install lrzsz`, `apt install lrzsz`, etc.
- `trzsz --zmodem ssh xxx` is not supported on Windows. You can use [trzsz-ssh ( tssh )](https://trzsz.github.io/ssh) instead, `tssh --zmodem xxx`.
- About the progress, the transferred and speed are not precise. They appear larger than reality. It just indicating that the transfer is in progress.
## Trouble shooting
- If using [MSYS2](https://www.msys2.org/) or [Git Bash](https://www.atlassian.com/git/tutorials/git-bash) on windows, and getting an error `The handle is invalid`.
Expand Down
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ module github.com/trzsz/trzsz-go
go 1.20

require (
github.com/UserExistsError/conpty v0.1.1
github.com/creack/pty v1.1.18
github.com/klauspost/compress v1.17.1
github.com/UserExistsError/conpty v0.1.2
github.com/creack/pty v1.1.21
github.com/klauspost/compress v1.17.4
github.com/ncruces/zenity v0.10.10
github.com/stretchr/testify v1.8.4
github.com/trzsz/go-arg v1.5.2
github.com/trzsz/promptui v0.10.3
golang.org/x/sys v0.13.0
golang.org/x/term v0.13.0
golang.org/x/text v0.13.0
github.com/trzsz/promptui v0.10.5
golang.org/x/sys v0.15.0
golang.org/x/term v0.15.0
golang.org/x/text v0.14.0
)

require (
Expand All @@ -24,6 +24,6 @@ require (
github.com/josephspurrier/goversioninfo v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/image v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
32 changes: 16 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/UserExistsError/conpty v0.1.1 h1:cHDsU/XeoeDAQmVvCTV53SrXLG39YJ4++Pp3iAi1gXE=
github.com/UserExistsError/conpty v0.1.1/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
github.com/UserExistsError/conpty v0.1.2 h1:ikx+zk1ekB8Agiajun6Cpg4Ju/cEaU/mnRZQYT21naI=
github.com/UserExistsError/conpty v0.1.2/go.mod h1:PDglKIkX3O/2xVk0MV9a6bCWxRmPVfxqZoTG/5sSd9I=
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
Expand All @@ -10,17 +10,17 @@ github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pOG7rYTmWsTCvyEWFsMjg+HcOaA=
github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw=
github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8=
github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY=
github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g=
github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/ncruces/zenity v0.10.10 h1:V/rtAhr5QLdDThahOkm7EYlnw4RuEsf7oN+Xb6lz1j0=
github.com/ncruces/zenity v0.10.10/go.mod h1:k3k4hJ4Wt1MUbeV48y+Gbl7Fp9skfGszN/xtKmuvhZk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -34,20 +34,20 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/trzsz/go-arg v1.5.2 h1:zGxCuTKvtC3jBf7HbvNk0HooUjv8uKAy2mY+bHVhRas=
github.com/trzsz/go-arg v1.5.2/go.mod h1:IC6Z/FiVH7uYvcbp1/gJhDYCFPS/GkL0APYakVvgY4I=
github.com/trzsz/promptui v0.10.3 h1:uhcLQsLZqMxEtGiYoeM2lR/Hd4pSxoYsd2eFctH8MCs=
github.com/trzsz/promptui v0.10.3/go.mod h1:GMZtu6ZTzU73CBFkzGtmB4wnTROIAbv4GFA74fV8V8g=
github.com/trzsz/promptui v0.10.5 h1:tlzJkx+JOeE0sqKWmqgaoToZiYqj5G1Mz+QDV97VFu8=
github.com/trzsz/promptui v0.10.5/go.mod h1:GMZtu6ZTzU73CBFkzGtmB4wnTROIAbv4GFA74fV8V8g=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
6 changes: 6 additions & 0 deletions trzsz/comm.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ import (

var onExitFuncs []func()

func cleanupOnExit() {
for i := len(onExitFuncs) - 1; i >= 0; i-- {
onExitFuncs[i]()
}
}

var timeNowFunc = time.Now

var linuxRuntime bool = (runtime.GOOS == "linux")
Expand Down
55 changes: 47 additions & 8 deletions trzsz/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type TrzszOptions struct {
// DetectTraceLog is for debugging.
// If DetectTraceLog is true, will detect the server output to determine whether to enable trace logging.
DetectTraceLog bool
// EnableZmodem enable zmodem lrzsz ( rz / sz ) feature.
EnableZmodem bool
}

// TrzszFilter is a filter that supports trzsz ( trz / tsz ).
Expand All @@ -63,6 +65,7 @@ type TrzszFilter struct {
serverOut io.Reader
options TrzszOptions
transfer atomic.Pointer[trzszTransfer]
zmodem atomic.Pointer[zmodemTransfer]
progress atomic.Pointer[textProgressBar]
promptPipe atomic.Pointer[io.PipeWriter]
trigger *trzszTrigger
Expand Down Expand Up @@ -227,6 +230,8 @@ func (filter *TrzszFilter) getDefaultDownloadPath() string {
return resolveHomeDir(path)
}

var errUserCanceled = fmt.Errorf("Cancelled")

var parentWindowID = getParentWindowID()

func zenityErrorWithTips(err error) error {
Expand Down Expand Up @@ -265,12 +270,12 @@ func (filter *TrzszFilter) chooseDownloadPath() (string, error) {
options = append(options, zenity.Attach(parentWindowID))
}
path, err := zenity.SelectFile(options...)
if err == zenity.ErrCanceled || len(path) == 0 {
return "", errUserCanceled
}
if err != nil {
return "", zenityErrorWithTips(err)
}
if len(path) == 0 {
return "", zenity.ErrCanceled
}
return path, nil
}

Expand All @@ -294,18 +299,18 @@ func (filter *TrzszFilter) chooseUploadPaths(directory bool) ([]string, error) {
options = append(options, zenity.Attach(parentWindowID))
}
files, err := zenity.SelectFileMultiple(options...)
if err == zenity.ErrCanceled || len(files) == 0 {
return nil, errUserCanceled
}
if err != nil {
return nil, zenityErrorWithTips(err)
}
if len(files) == 0 {
return nil, zenity.ErrCanceled
}
return files, nil
}

func (filter *TrzszFilter) downloadFiles(transfer *trzszTransfer) error {
path, err := filter.chooseDownloadPath()
if err == zenity.ErrCanceled {
if err == errUserCanceled {
return transfer.sendAction(false, filter.trigger.version, filter.trigger.winServer)
}
if err != nil {
Expand Down Expand Up @@ -344,7 +349,7 @@ func (filter *TrzszFilter) downloadFiles(transfer *trzszTransfer) error {

func (filter *TrzszFilter) uploadFiles(transfer *trzszTransfer, directory bool) error {
paths, err := filter.chooseUploadPaths(directory)
if err == zenity.ErrCanceled {
if err == errUserCanceled {
return transfer.sendAction(false, filter.trigger.version, filter.trigger.winServer)
}
if err != nil {
Expand Down Expand Up @@ -602,6 +607,16 @@ func (filter *TrzszFilter) sendInput(buf []byte, detectDragFile *atomic.Bool) {
}
return
}
if filter.options.EnableZmodem {
if zmodem := filter.zmodem.Load(); zmodem != nil {
if len(buf) == 1 && buf[0] == '\x03' {
zmodem.stopTransferringFiles() // `ctrl + c` to stop transferring files
}
if zmodem.isTransferringFiles() {
return
}
}
}
if detectDragFile.Load() {
dragFiles, hasDir, ignore := detectDragFiles(buf)
if dragFiles != nil {
Expand Down Expand Up @@ -657,6 +672,16 @@ func (filter *TrzszFilter) wrapOutput() {
if filter.logger != nil {
buf = filter.logger.writeTraceLog(buf, "svrout")
}
if filter.options.EnableZmodem {
if zmodem := filter.zmodem.Load(); zmodem != nil {
if zmodem.handleServerOutput(buf) {
continue
} else {
filter.zmodem.Store(nil)
}
}
}

var trigger *trzszTrigger
buf, trigger = detector.detectTrzsz(buf, filter.tunnelConnector.Load() != nil)
if trigger != nil {
Expand All @@ -676,6 +701,20 @@ func (filter *TrzszFilter) wrapOutput() {
continue
}
}

if filter.options.EnableZmodem {
if zmodem := detectZmodem(buf); zmodem != nil {
_ = writeAll(filter.clientOut, buf)
filter.zmodem.Store(zmodem)
go zmodem.handleZmodemEvent(filter.logger, filter.serverIn, filter.clientOut,
func() ([]string, error) {
return filter.chooseUploadPaths(false)
},
filter.chooseDownloadPath)
continue
}
}

_ = writeAll(filter.clientOut, buf)
}
if err == io.EOF {
Expand Down
72 changes: 40 additions & 32 deletions trzsz/progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,43 @@ func convertTimeToString(seconds float64) string {

const kSpeedArraySize = 30

type recentSpeed struct {
speedCnt int
speedIdx int
timeArray [kSpeedArraySize]*time.Time
stepArray [kSpeedArraySize]int64
}

func (s *recentSpeed) initFirstStep(now *time.Time) {
s.timeArray[0] = now
s.stepArray[0] = 0
s.speedCnt = 1
s.speedIdx = 1
}

func (s *recentSpeed) getSpeed(step int64, now *time.Time) float64 {
var speed float64
if s.speedCnt <= kSpeedArraySize {
s.speedCnt++
speed = float64(step-s.stepArray[0]) / (float64(now.Sub(*s.timeArray[0])) / float64(time.Second))
} else {
speed = float64(step-s.stepArray[s.speedIdx]) / (float64(now.Sub(*s.timeArray[s.speedIdx])) / float64(time.Second))
}

s.timeArray[s.speedIdx] = now
s.stepArray[s.speedIdx] = step

s.speedIdx++
if s.speedIdx >= kSpeedArraySize {
s.speedIdx = 0
}

if math.IsNaN(speed) {
return -1
}
return speed
}

type textProgressBar struct {
writer io.Writer
columns atomic.Int32
Expand All @@ -149,10 +186,7 @@ type textProgressBar struct {
startTime *time.Time
lastUpdateTime *time.Time
firstWrite bool
speedCnt int
speedIdx int
timeArray [kSpeedArraySize]*time.Time
stepArray [kSpeedArraySize]int64
recentSpeed recentSpeed
pausing atomic.Bool
tmuxPrefix string
}
Expand Down Expand Up @@ -193,10 +227,7 @@ func (p *textProgressBar) onName(name string) {
p.fileIdx++
now := timeNowFunc()
p.startTime = &now
p.timeArray[0] = p.startTime
p.stepArray[0] = 0
p.speedCnt = 1
p.speedIdx = 1
p.recentSpeed.initFirstStep(&now)
p.preSize = 0
p.fileStep = -1
}
Expand Down Expand Up @@ -268,7 +299,7 @@ func (p *textProgressBar) showProgress() {
percentage = fmt.Sprintf("%.0f%%", math.Round(float64(p.fileStep)*100.0/float64(p.fileSize)))
}
total := convertSizeToString(float64(p.fileStep))
speed := p.getSpeed(&now)
speed := p.recentSpeed.getSpeed(p.fileStep, &now)
speedStr := "--- B/s"
etaStr := "--- ETA"
if speed > 0 {
Expand All @@ -290,29 +321,6 @@ func (p *textProgressBar) showProgress() {
}
}

func (p *textProgressBar) getSpeed(now *time.Time) float64 {
var speed float64
if p.speedCnt <= kSpeedArraySize {
p.speedCnt++
speed = float64(p.fileStep-p.stepArray[0]) / (float64(now.Sub(*p.timeArray[0])) / float64(time.Second))
} else {
speed = float64(p.fileStep-p.stepArray[p.speedIdx]) / (float64(now.Sub(*p.timeArray[p.speedIdx])) / float64(time.Second))
}

p.timeArray[p.speedIdx] = now
p.stepArray[p.speedIdx] = p.fileStep

p.speedIdx++
if p.speedIdx >= kSpeedArraySize {
p.speedIdx %= kSpeedArraySize
}

if math.IsNaN(speed) {
return -1
}
return speed
}

func (p *textProgressBar) getProgressText(percentage, total, speed, eta string) string {
const barMinLength = 24

Expand Down
12 changes: 4 additions & 8 deletions trzsz/pty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type trzszPty struct {
closed atomic.Bool
}

func setupVirtualTerminal() error {
return nil
}

func spawn(name string, arg ...string) (*trzszPty, error) {
// spawn a pty
cmd := exec.Command(name, arg...)
Expand Down Expand Up @@ -125,13 +129,5 @@ func syscallAccessRok(path string) error {
return syscall.Access(path, unix.R_OK)
}

func enableVirtualTerminal() (uint32, uint32, error) {
return 0, 0, nil
}

func resetVirtualTerminal(inMode, outMode uint32) error {
return nil
}

func setupConsoleOutput() {
}
Loading

0 comments on commit fc290aa

Please sign in to comment.