fixed zmodem errors - fixes #6677, fixes #5845, fixes #5243, fixes #5132, fixes #5021, fixes #7511, fixes #7053, fixes #6917, fixes #6639, fixes #6259, fixes #6182, fixes #6122, fixes #5845, fixes #5737, fixes #5701, fixes #5609, fixes #5311, fixes #5243, fixes #5231, fixes #5132

This commit is contained in:
Eugene Pankov 2022-11-20 19:25:48 +01:00
parent 0f0f61f432
commit 9c89bab256
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
6 changed files with 99 additions and 185 deletions

View File

@ -4,7 +4,6 @@ import { ipcMain } from 'electron'
import { Application } from './app'
import { UTF8Splitter } from './utfSplitter'
import { Subject, debounceTime } from 'rxjs'
import { StringDecoder } from './stringDecoder'
class PTYDataQueue {
private buffers: Buffer[] = []
@ -91,7 +90,6 @@ class PTYDataQueue {
export class PTY {
private pty: nodePTY.IPty
private outputQueue: PTYDataQueue
private decoder = new StringDecoder()
exited = false
constructor (private id: string, private app: Application, ...args: any[]) {
@ -101,7 +99,7 @@ export class PTY {
}
this.outputQueue = new PTYDataQueue(this.pty, data => {
setImmediate(() => this.emit('data', this.decoder.write(data)))
setImmediate(() => this.emit('data', data))
})
this.pty.onData(data => this.outputQueue.push(Buffer.from(data)))

View File

@ -1,105 +0,0 @@
// based on Joyent's StringDecoder
// https://github.com/nodejs/string_decoder/blob/master/lib/string_decoder.js
export class StringDecoder {
lastNeed: number
lastTotal: number
lastChar: Buffer
constructor () {
this.lastNeed = 0
this.lastTotal = 0
this.lastChar = Buffer.allocUnsafe(4)
}
write (buf: Buffer): Buffer {
if (buf.length === 0) {
return buf
}
let r: Buffer|undefined = undefined
let i = 0
if (this.lastNeed) {
r = this.fillLast(buf)
if (r === undefined) {
return Buffer.from('')
}
i = this.lastNeed
this.lastNeed = 0
}
if (i < buf.length) {
return r ? Buffer.concat([r, this.text(buf, i)]) : this.text(buf, i)
}
return r
}
// For UTF-8, a replacement character is added when ending on a partial
// character.
end (buf?: Buffer): Buffer {
const r = buf?.length ? this.write(buf) : Buffer.from('')
if (this.lastNeed) {
console.log('end', r)
return Buffer.concat([r, Buffer.from('\ufffd')])
}
return r
}
// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a
// partial character, the character's bytes are buffered until the required
// number of bytes are available.
private text (buf: Buffer, i: number) {
const total = this.utf8CheckIncomplete(buf, i)
if (!this.lastNeed) {
return buf.slice(i)
}
this.lastTotal = total
const end = buf.length - (total - this.lastNeed)
buf.copy(this.lastChar, 0, end)
return buf.slice(i, end)
}
// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer
private fillLast (buf: Buffer): Buffer|undefined {
if (this.lastNeed <= buf.length) {
buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed)
return this.lastChar.slice(0, this.lastTotal)
}
buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length)
this.lastNeed -= buf.length
return undefined
}
// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a
// continuation byte. If an invalid byte is detected, -2 is returned.
private utf8CheckByte (byte) {
if (byte <= 0x7F) {return 0} else if (byte >> 5 === 0x06) {return 2} else if (byte >> 4 === 0x0E) {return 3} else if (byte >> 3 === 0x1E) {return 4}
return byte >> 6 === 0x02 ? -1 : -2
}
// Checks at most 3 bytes at the end of a Buffer in order to detect an
// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4)
// needed to complete the UTF-8 character (if applicable) are returned.
private utf8CheckIncomplete (buf, i) {
let j = buf.length - 1
if (j < i) {return 0}
let nb = this.utf8CheckByte(buf[j])
if (nb >= 0) {
if (nb > 0) {this.lastNeed = nb - 1}
return nb
}
if (--j < i || nb === -2) {return 0}
nb = this.utf8CheckByte(buf[j])
if (nb >= 0) {
if (nb > 0) {this.lastNeed = nb - 2}
return nb
}
if (--j < i || nb === -2) {return 0}
nb = this.utf8CheckByte(buf[j])
if (nb >= 0) {
if (nb > 0) {
if (nb === 2) {nb = 0} else {this.lastNeed = nb - 3}
}
return nb
}
return 0
}
}

View File

@ -123,6 +123,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected logger: Logger
protected output = new Subject<string>()
protected binaryOutput = new Subject<Buffer>()
protected sessionChanged = new Subject<BaseSession|null>()
private bellPlayer: HTMLAudioElement
private termContainerSubscriptions = new SubscriptionContainer()
@ -153,6 +154,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
get output$ (): Observable<string> { return this.output }
get binaryOutput$ (): Observable<Buffer> { return this.binaryOutput }
get resize$ (): Observable<ResizeEvent> {
if (!this.frontend) {
@ -369,7 +371,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.configure()
setTimeout(() => {
this.output.subscribe(() => {
this.binaryOutput$.subscribe(() => {
this.displayActivity()
})
}, 1000)
@ -564,6 +566,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
})
this.output.complete()
this.binaryOutput.complete()
this.frontendReady.complete()
super.destroy()
@ -741,6 +744,12 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
})
this.attachSessionHandler(this.session.binaryOutput$, data => {
if (this.enablePassthrough) {
this.binaryOutput.next(data)
}
})
if (destroyOnSessionClose) {
this.attachSessionHandler(this.session.closed$, () => {
this.destroy()

View File

@ -22,7 +22,7 @@ export class SessionMiddleware {
}
}
export class SesssionMiddlewareStack extends SessionMiddleware {
export class SessionMiddlewareStack extends SessionMiddleware {
private stack: SessionMiddleware[] = []
private subs = new SubscriptionContainer()

View File

@ -4,13 +4,14 @@ import { Observable, filter, first } from 'rxjs'
import { Injectable } from '@angular/core'
import { TerminalDecorator } from '../api/decorator'
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
import { SessionMiddleware } from '../api/middleware'
import { LogService, Logger, HotkeysService, PlatformService, FileUpload } from 'tabby-core'
const SPACER = ' '
/** @hidden */
@Injectable()
export class ZModemDecorator extends TerminalDecorator {
class ZModemMiddleware extends SessionMiddleware {
private sentry: ZModem.Sentry
private isActive = false
private logger: Logger
private activeSession: any = null
private cancelEvent: Observable<any>
@ -21,65 +22,52 @@ export class ZModemDecorator extends TerminalDecorator {
private platform: PlatformService,
) {
super()
this.logger = log.create('zmodem')
this.cancelEvent = hotkeys.hotkey$.pipe(filter(x => x === 'ctrl-c'))
}
this.cancelEvent = this.outputToSession$.pipe(filter(x => x.length === 1 && x[0] === 3))
attach (terminal: BaseTerminalTabComponent): void {
let isActive = false
const sentry = new ZModem.Sentry({
this.logger = log.create('zmodem')
this.sentry = new ZModem.Sentry({
to_terminal: data => {
if (isActive) {
terminal.write(data)
if (this.isActive) {
this.outputToTerminal.next(Buffer.from(data))
}
},
sender: data => terminal.session!.feedFromTerminal(Buffer.from(data)),
sender: data => this.outputToSession.next(Buffer.from(data)),
on_detect: async detection => {
try {
terminal.enablePassthrough = false
isActive = true
await this.process(terminal, detection)
this.isActive = true
await this.process(detection)
} finally {
terminal.enablePassthrough = true
isActive = false
this.isActive = false
}
},
on_retract: () => {
this.showMessage(terminal, 'transfer cancelled')
this.showMessage('transfer cancelled')
},
})
setTimeout(() => {
this.attachToSession(sentry, terminal)
this.subscribeUntilDetached(terminal, terminal.sessionChanged$.subscribe(() => {
this.attachToSession(sentry, terminal)
}))
})
}
private attachToSession (sentry, terminal) {
if (!terminal.session) {
return
}
this.subscribeUntilDetached(terminal, terminal.session.binaryOutput$.subscribe(data => {
const chunkSize = 1024
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
try {
sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
} catch (e) {
this.showMessage(terminal, colors.bgRed.black(' Error ') + ' ' + e)
this.logger.error('protocol error', e)
this.activeSession.abort()
this.activeSession = null
terminal.enablePassthrough = true
return
}
feedFromSession (data: Buffer): void {
const chunkSize = 1024
for (let i = 0; i <= Math.floor(data.length / chunkSize); i++) {
try {
this.sentry.consume(Buffer.from(data.slice(i * chunkSize, (i + 1) * chunkSize)))
} catch (e) {
this.showMessage(colors.bgRed.black(' Error ') + ' ' + e)
this.logger.error('protocol error', e)
this.activeSession.abort()
this.activeSession = null
this.isActive = false
return
}
}))
}
if (!this.isActive) {
this.outputToTerminal.next(data)
}
}
private async process (terminal, detection): Promise<void> {
this.showMessage(terminal, colors.bgBlue.black(' ZMODEM ') + ' Session started')
this.showMessage(terminal, '------------------------')
private async process (detection): Promise<void> {
this.showMessage(colors.bgBlue.black(' ZMODEM ') + ' Session started')
this.showMessage('------------------------')
const zsession = detection.confirm()
this.activeSession = zsession
@ -90,7 +78,7 @@ export class ZModemDecorator extends TerminalDecorator {
let filesRemaining = transfers.length
let sizeRemaining = transfers.reduce((a, b) => a + b.getSize(), 0)
for (const transfer of transfers) {
await this.sendFile(terminal, zsession, transfer, filesRemaining, sizeRemaining)
await this.sendFile(zsession, transfer, filesRemaining, sizeRemaining)
filesRemaining--
sizeRemaining -= transfer.getSize()
}
@ -98,7 +86,7 @@ export class ZModemDecorator extends TerminalDecorator {
await zsession.close()
} else {
zsession.on('offer', xfer => {
this.receiveFile(terminal, xfer, zsession)
this.receiveFile(xfer, zsession)
})
zsession.start()
@ -108,29 +96,27 @@ export class ZModemDecorator extends TerminalDecorator {
}
}
private async receiveFile (terminal, xfer, zsession) {
private async receiveFile (xfer, zsession) {
const details: {
name: string,
size: number,
} = xfer.get_details()
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + details.name, true)
this.showMessage(colors.bgYellow.black(' Offered ') + ' ' + details.name, true)
this.logger.info('offered', xfer)
const transfer = await this.platform.startDownload(details.name, 0o644, details.size)
if (!transfer) {
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + details.name)
this.showMessage(colors.bgRed.black(' Rejected ') + ' ' + details.name)
xfer.skip()
return
}
let canceled = false
const cancelSubscription = this.cancelEvent.subscribe(() => {
if (terminal.hasFocus) {
try {
zsession._skip()
} catch {}
canceled = true
}
try {
zsession._skip()
} catch {}
canceled = true
})
try {
@ -141,7 +127,7 @@ export class ZModemDecorator extends TerminalDecorator {
return
}
transfer.write(Buffer.from(chunk))
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true)
this.showMessage(colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / details.size).toString().padStart(3, ' ') + '% ') + ' ' + details.name, true)
},
}),
this.cancelEvent.pipe(first()).toPromise(),
@ -150,19 +136,19 @@ export class ZModemDecorator extends TerminalDecorator {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (canceled) {
transfer.cancel()
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + details.name)
this.showMessage(colors.bgRed.black(' Canceled ') + ' ' + details.name)
} else {
transfer.close()
this.showMessage(terminal, colors.bgGreen.black(' Received ') + ' ' + details.name)
this.showMessage(colors.bgGreen.black(' Received ') + ' ' + details.name)
}
} catch {
this.showMessage(terminal, colors.bgRed.black(' Error ') + ' ' + details.name)
this.showMessage(colors.bgRed.black(' Error ') + ' ' + details.name)
}
cancelSubscription.unsubscribe()
}
private async sendFile (terminal, zsession, transfer: FileUpload, filesRemaining, sizeRemaining) {
private async sendFile (zsession, transfer: FileUpload, filesRemaining, sizeRemaining) {
const offer = {
name: transfer.getName(),
size: transfer.getSize(),
@ -171,15 +157,13 @@ export class ZModemDecorator extends TerminalDecorator {
bytes_remaining: sizeRemaining,
}
this.logger.info('offering', offer)
this.showMessage(terminal, colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
this.showMessage(colors.bgYellow.black(' Offered ') + ' ' + offer.name, true)
const xfer = await zsession.send_offer(offer)
if (xfer) {
let canceled = false
const cancelSubscription = this.cancelEvent.subscribe(() => {
if (terminal.hasFocus) {
canceled = true
}
canceled = true
})
while (true) {
@ -190,7 +174,7 @@ export class ZModemDecorator extends TerminalDecorator {
}
await xfer.send(chunk)
this.showMessage(terminal, colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
this.showMessage(colors.bgYellow.black(' ' + Math.round(100 * transfer.getCompletedBytes() / offer.size).toString().padStart(3, ' ') + '% ') + offer.name, true)
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -204,23 +188,51 @@ export class ZModemDecorator extends TerminalDecorator {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (canceled) {
this.showMessage(terminal, colors.bgRed.black(' Canceled ') + ' ' + offer.name)
this.showMessage(colors.bgRed.black(' Canceled ') + ' ' + offer.name)
} else {
this.showMessage(terminal, colors.bgGreen.black(' Sent ') + ' ' + offer.name)
this.showMessage(colors.bgGreen.black(' Sent ') + ' ' + offer.name)
}
cancelSubscription.unsubscribe()
} else {
transfer.cancel()
this.showMessage(terminal, colors.bgRed.black(' Rejected ') + ' ' + offer.name)
this.showMessage(colors.bgRed.black(' Rejected ') + ' ' + offer.name)
this.logger.warn('rejected by the other side')
}
}
private showMessage (terminal, msg: string, overwrite = false) {
terminal.write(Buffer.from(`\r${msg}${SPACER}`))
private showMessage (msg: string, overwrite = false) {
this.outputToTerminal.next(Buffer.from(`\r${msg}${SPACER}`))
if (!overwrite) {
terminal.write(Buffer.from('\r\n'))
this.outputToTerminal.next(Buffer.from('\r\n'))
}
}
}
/** @hidden */
@Injectable()
export class ZModemDecorator extends TerminalDecorator {
constructor (
private log: LogService,
private hotkeys: HotkeysService,
private platform: PlatformService,
) {
super()
}
attach (terminal: BaseTerminalTabComponent): void {
setTimeout(() => {
this.attachToSession(terminal)
this.subscribeUntilDetached(terminal, terminal.sessionChanged$.subscribe(() => {
this.attachToSession(terminal)
}))
})
}
private attachToSession (terminal: BaseTerminalTabComponent) {
if (!terminal.session) {
return
}
terminal.session.middleware.unshift(new ZModemMiddleware(this.log, this.hotkeys, this.platform))
}
}

View File

@ -2,7 +2,7 @@ import { Observable, Subject } from 'rxjs'
import { Logger } from 'tabby-core'
import { LoginScriptProcessor, LoginScriptsOptions } from './middleware/loginScriptProcessing'
import { OSCProcessor } from './middleware/oscProcessing'
import { SesssionMiddlewareStack } from './api/middleware'
import { SessionMiddlewareStack } from './api/middleware'
/**
* A session object for a [[BaseTerminalTabComponent]]
@ -11,8 +11,8 @@ import { SesssionMiddlewareStack } from './api/middleware'
export abstract class BaseSession {
open: boolean
truePID?: number
oscProcessor = new OSCProcessor()
protected readonly middleware = new SesssionMiddlewareStack()
readonly oscProcessor = new OSCProcessor()
readonly middleware = new SessionMiddlewareStack()
protected output = new Subject<string>()
protected binaryOutput = new Subject<Buffer>()
protected closed = new Subject<void>()