mirror of
https://github.com/Eugeny/tabby.git
synced 2024-11-21 01:17:16 +08:00
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:
parent
0f0f61f432
commit
9c89bab256
@ -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)))
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -22,7 +22,7 @@ export class SessionMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
export class SesssionMiddlewareStack extends SessionMiddleware {
|
||||
export class SessionMiddlewareStack extends SessionMiddleware {
|
||||
private stack: SessionMiddleware[] = []
|
||||
private subs = new SubscriptionContainer()
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
Loading…
Reference in New Issue
Block a user