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 { Application } from './app'
import { UTF8Splitter } from './utfSplitter' import { UTF8Splitter } from './utfSplitter'
import { Subject, debounceTime } from 'rxjs' import { Subject, debounceTime } from 'rxjs'
import { StringDecoder } from './stringDecoder'
class PTYDataQueue { class PTYDataQueue {
private buffers: Buffer[] = [] private buffers: Buffer[] = []
@ -91,7 +90,6 @@ class PTYDataQueue {
export class PTY { export class PTY {
private pty: nodePTY.IPty private pty: nodePTY.IPty
private outputQueue: PTYDataQueue private outputQueue: PTYDataQueue
private decoder = new StringDecoder()
exited = false exited = false
constructor (private id: string, private app: Application, ...args: any[]) { constructor (private id: string, private app: Application, ...args: any[]) {
@ -101,7 +99,7 @@ export class PTY {
} }
this.outputQueue = new PTYDataQueue(this.pty, data => { 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))) 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 logger: Logger
protected output = new Subject<string>() protected output = new Subject<string>()
protected binaryOutput = new Subject<Buffer>()
protected sessionChanged = new Subject<BaseSession|null>() protected sessionChanged = new Subject<BaseSession|null>()
private bellPlayer: HTMLAudioElement private bellPlayer: HTMLAudioElement
private termContainerSubscriptions = new SubscriptionContainer() private termContainerSubscriptions = new SubscriptionContainer()
@ -153,6 +154,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
get output$ (): Observable<string> { return this.output } get output$ (): Observable<string> { return this.output }
get binaryOutput$ (): Observable<Buffer> { return this.binaryOutput }
get resize$ (): Observable<ResizeEvent> { get resize$ (): Observable<ResizeEvent> {
if (!this.frontend) { if (!this.frontend) {
@ -369,7 +371,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.configure() this.configure()
setTimeout(() => { setTimeout(() => {
this.output.subscribe(() => { this.binaryOutput$.subscribe(() => {
this.displayActivity() this.displayActivity()
}) })
}, 1000) }, 1000)
@ -564,6 +566,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
} }
}) })
this.output.complete() this.output.complete()
this.binaryOutput.complete()
this.frontendReady.complete() this.frontendReady.complete()
super.destroy() 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) { if (destroyOnSessionClose) {
this.attachSessionHandler(this.session.closed$, () => { this.attachSessionHandler(this.session.closed$, () => {
this.destroy() 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 stack: SessionMiddleware[] = []
private subs = new SubscriptionContainer() private subs = new SubscriptionContainer()

View File

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