plugged memory leaks

This commit is contained in:
Eugene Pankov 2021-05-13 16:40:23 +02:00
parent c98fd2042d
commit 5c22e22caa
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
18 changed files with 211 additions and 144 deletions

View File

@ -21,7 +21,7 @@ if (process.platform === 'win32' && !('HOME' in process.env)) {
process.env.HOME = `${process.env.HOMEDRIVE}${process.env.HOMEPATH}`
}
if (process.env.TERMINUS_DEV) {
if (process.env.TERMINUS_DEV && !process.env.TERMINUS_FORCE_ANGULAR_PROD) {
console.warn('Running in debug mode')
} else {
enableProdMode()

View File

@ -1,3 +1,4 @@
export { BaseComponent, SubscriptionContainer } from '../components/base.component'
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
export { TabHeaderComponent } from '../components/tabHeader.component'
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'

View File

@ -0,0 +1,54 @@
import { Observable, Subscription } from 'rxjs'
interface CancellableEvent {
element: HTMLElement
event: string
handler: EventListenerOrEventListenerObject
options?: boolean|AddEventListenerOptions
}
export class SubscriptionContainer {
private subscriptions: Subscription[] = []
private events: CancellableEvent[] = []
addEventListener (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
element.addEventListener(event, handler, options)
this.events.push({
element,
event,
handler,
options,
})
}
subscribe <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.subscriptions.push(observable.subscribe(handler))
}
cancelAll (): void {
for (const s of this.subscriptions) {
s.unsubscribe()
}
for (const e of this.events) {
e.element.removeEventListener(e.event, e.handler, e.options)
}
this.subscriptions = []
this.events = []
}
}
export class BaseComponent {
private subscriptionContainer = new SubscriptionContainer()
addEventListenerUntilDestroyed (element: HTMLElement, event: string, handler: EventListenerOrEventListenerObject, options?: boolean|AddEventListenerOptions): void {
this.subscriptionContainer.addEventListener(element, event, handler, options)
}
subscribeUntilDestroyed <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.subscriptionContainer.subscribe(observable, handler)
}
ngOnDestroy (): void {
this.subscriptionContainer.cancelAll()
}
}

View File

@ -1,6 +1,7 @@
import { Observable, Subject } from 'rxjs'
import { ViewRef } from '@angular/core'
import { RecoveryToken } from '../api/tabRecovery'
import { BaseComponent } from './base.component'
/**
* Represents an active "process" inside a tab,
@ -13,7 +14,7 @@ export interface BaseTabProcess {
/**
* Abstract base class for custom tab components
*/
export abstract class BaseTabComponent {
export abstract class BaseTabComponent extends BaseComponent {
/**
* Parent tab (usually a SplitTabComponent)
*/
@ -69,6 +70,7 @@ export abstract class BaseTabComponent {
get recoveryStateChangedHint$ (): Observable<void> { return this.recoveryStateChangedHint }
protected constructor () {
super()
this.focused$.subscribe(() => {
this.hasFocus = true
})
@ -158,10 +160,17 @@ export abstract class BaseTabComponent {
this.blurred.complete()
this.titleChange.complete()
this.progress.complete()
this.activity.complete()
this.recoveryStateChangedHint.complete()
if (!skipDestroyedEvent) {
this.destroyed.next()
}
this.destroyed.complete()
}
/** @hidden */
ngOnDestroy (): void {
this.destroy()
super.ngOnDestroy()
}
}

View File

@ -1,4 +1,4 @@
import { Observable, Subject, Subscription } from 'rxjs'
import { Observable, Subject } from 'rxjs'
import { Component, Injectable, ViewChild, ViewContainerRef, EmbeddedViewRef, AfterViewInit, OnDestroy } from '@angular/core'
import { BaseTabComponent, BaseTabProcess } from './baseTab.component'
import { TabRecoveryProvider, RecoveredTab, RecoveryToken } from '../api/tabRecovery'
@ -163,7 +163,6 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
private focusedTab: BaseTabComponent|null = null
private maximizedTab: BaseTabComponent|null = null
private hotkeysSubscription: Subscription
private viewRefs: Map<BaseTabComponent, EmbeddedViewRef<any>> = new Map()
private tabAdded = new Subject<BaseTabComponent>()
@ -210,7 +209,7 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
})
this.blurred$.subscribe(() => this.getAllTabs().forEach(x => x.emitBlurred()))
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus || !this.focusedTab) {
return
}
@ -272,7 +271,9 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
/** @hidden */
ngOnDestroy (): void {
this.hotkeysSubscription.unsubscribe()
this.tabAdded.complete()
this.tabRemoved.complete()
super.ngOnDestroy()
}
/** @returns Flat list of all sub-tabs */
@ -497,18 +498,18 @@ export class SplitTabComponent extends BaseTabComponent implements AfterViewInit
const ref = this.viewContainer.insert(tab.hostView) as EmbeddedViewRef<any> // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
this.viewRefs.set(tab, ref)
ref.rootNodes[0].addEventListener('click', () => this.focus(tab))
tab.addEventListenerUntilDestroyed(ref.rootNodes[0], 'click', () => this.focus(tab))
tab.titleChange$.subscribe(t => this.setTitle(t))
tab.activity$.subscribe(a => a ? this.displayActivity() : this.clearActivity())
tab.progress$.subscribe(p => this.setProgress(p))
tab.subscribeUntilDestroyed(tab.titleChange$, t => this.setTitle(t))
tab.subscribeUntilDestroyed(tab.activity$, a => a ? this.displayActivity() : this.clearActivity())
tab.subscribeUntilDestroyed(tab.progress$, p => this.setProgress(p))
if (tab.title) {
this.setTitle(tab.title)
}
tab.recoveryStateChangedHint$.subscribe(() => {
tab.subscribeUntilDestroyed(tab.recoveryStateChangedHint$, () => {
this.recoveryStateChangedHint.next()
})
tab.destroyed$.subscribe(() => {
tab.subscribeUntilDestroyed(tab.destroyed$, () => {
this.removeTab(tab)
})
}

View File

@ -11,6 +11,7 @@ import { ElectronService } from '../services/electron.service'
import { AppService } from '../services/app.service'
import { HostAppService, Platform } from '../services/hostApp.service'
import { ConfigService } from '../services/config.service'
import { BaseComponent } from './base.component'
/** @hidden */
export interface SortableComponentProxy {
@ -23,7 +24,7 @@ export interface SortableComponentProxy {
template: require('./tabHeader.component.pug'),
styles: [require('./tabHeader.component.scss')],
})
export class TabHeaderComponent {
export class TabHeaderComponent extends BaseComponent {
@Input() index: number
@Input() @HostBinding('class.active') active: boolean
@Input() tab: BaseTabComponent
@ -41,7 +42,8 @@ export class TabHeaderComponent {
@Inject(SortableComponent) private parentDraggable: SortableComponentProxy,
@Optional() @Inject(TabContextMenuItemProvider) protected contextMenuProviders: TabContextMenuItemProvider[],
) {
this.hotkeys.matchedHotkey.subscribe((hotkey) => {
super()
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, (hotkey) => {
if (this.app.activeTab === this.tab) {
if (hotkey === 'rename-tab') {
this.showRenameTabModal()
@ -52,7 +54,7 @@ export class TabHeaderComponent {
}
ngOnInit () {
this.tab.progress$.subscribe(progress => {
this.subscribeUntilDestroyed(this.tab.progress$, progress => {
this.zone.run(() => {
this.progress = progress
})

View File

@ -1,7 +1,7 @@
import { Injectable, Inject, NgZone, EventEmitter } from '@angular/core'
import { Observable, Subject } from 'rxjs'
import { HotkeyDescription, HotkeyProvider } from '../api/hotkeyProvider'
import { stringifyKeySequence } from './hotkeys.util'
import { stringifyKeySequence, EventData } from './hotkeys.util'
import { ConfigService } from './config.service'
import { ElectronService } from './electron.service'
import { HostAppService } from './hostApp.service'
@ -14,10 +14,6 @@ export interface PartialHotkeyMatch {
const KEY_TIMEOUT = 2000
interface EventBufferEntry {
event: KeyboardEvent
time: number
}
@Injectable({ providedIn: 'root' })
export class HotkeysService {
@ -32,7 +28,7 @@ export class HotkeysService {
get hotkey$ (): Observable<string> { return this._hotkey }
private _hotkey = new Subject<string>()
private currentKeystrokes: EventBufferEntry[] = []
private currentKeystrokes: EventData[] = []
private disabledLevel = 0
private hotkeyDescriptions: HotkeyDescription[] = []
@ -73,7 +69,16 @@ export class HotkeysService {
*/
pushKeystroke (name: string, nativeEvent: KeyboardEvent): void {
(nativeEvent as any).event = name
this.currentKeystrokes.push({ event: nativeEvent, time: performance.now() })
this.currentKeystrokes.push({
ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey,
altKey: nativeEvent.altKey,
shiftKey: nativeEvent.shiftKey,
code: nativeEvent.code,
key: nativeEvent.key,
eventName: name,
time: performance.now(),
})
}
/**
@ -104,7 +109,7 @@ export class HotkeysService {
getCurrentKeystrokes (): string[] {
this.currentKeystrokes = this.currentKeystrokes.filter(x => performance.now() - x.time < KEY_TIMEOUT)
return stringifyKeySequence(this.currentKeystrokes.map(x => x.event))
return stringifyKeySequence(this.currentKeystrokes)
}
getCurrentFullyMatchedHotkey (): string|null {

View File

@ -10,15 +10,26 @@ export const altKeyName = {
linux: 'Alt',
}[process.platform]
export interface EventData {
ctrlKey: boolean
metaKey: boolean
altKey: boolean
shiftKey: boolean
key: string
code: string
eventName: string
time: number
}
const REGEX_LATIN_KEYNAME = /^[A-Za-z]$/
export function stringifyKeySequence (events: KeyboardEvent[]): string[] {
export function stringifyKeySequence (events: EventData[]): string[] {
const items: string[] = []
events = events.slice()
while (events.length > 0) {
const event = events.shift()!
if ((event as any).event === 'keydown') {
if (event.eventName === 'keydown') {
const itemKeys: string[] = []
if (event.ctrlKey) {
itemKeys.push('Ctrl')

View File

@ -6,7 +6,6 @@ import { first } from 'rxjs/operators'
import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SerialService } from '../services/serial.service'
import { SerialConnection, SerialSession, BAUD_RATES } from '../api'
import { Subscription } from 'rxjs'
/** @hidden */
@Component({
@ -20,7 +19,6 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
session: SerialSession|null = null
serialPort: any
private serialService: SerialService
private homeEndSubscription: Subscription
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (
@ -33,7 +31,7 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
ngOnInit () {
this.logger = this.log.create('terminalTab')
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) {
return
}
@ -130,9 +128,4 @@ export class SerialTabComponent extends BaseTerminalTabComponent {
this.serialPort.update({ baudRate: rate })
this.connection!.baudrate = rate
}
ngOnDestroy () {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
}
}

View File

@ -1,8 +1,7 @@
import { Component, Input } from '@angular/core'
import { trigger, transition, style, animate } from '@angular/animations'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { Subscription } from 'rxjs'
import { HotkeysService } from 'terminus-core'
import { HotkeysService, BaseComponent } from 'terminus-core'
const INPUT_TIMEOUT = 1000
@ -36,11 +35,10 @@ const INPUT_TIMEOUT = 1000
]),
],
})
export class HotkeyInputModalComponent {
export class HotkeyInputModalComponent extends BaseComponent {
@Input() value: string[] = []
@Input() timeoutProgress = 0
private keySubscription: Subscription
private lastKeyEvent: number|null = null
private keyTimeoutInterval: number|null = null
@ -48,8 +46,9 @@ export class HotkeyInputModalComponent {
private modalInstance: NgbActiveModal,
public hotkeys: HotkeysService,
) {
super()
this.hotkeys.clearCurrentKeystrokes()
this.keySubscription = hotkeys.key.subscribe((event) => {
this.subscribeUntilDestroyed(hotkeys.key, (event) => {
this.lastKeyEvent = performance.now()
this.value = this.hotkeys.getCurrentKeystrokes()
event.preventDefault()
@ -75,10 +74,10 @@ export class HotkeyInputModalComponent {
}
ngOnDestroy (): void {
this.keySubscription.unsubscribe()
this.hotkeys.clearCurrentKeystrokes()
this.hotkeys.enable()
clearInterval(this.keyTimeoutInterval!)
super.ngOnDestroy()
}
close (): void {

View File

@ -58,7 +58,7 @@ export class SettingsTabComponent extends BaseTabComponent {
&& config.store.appearance.tabsLocation !== 'top'
}
this.configSubscription = config.changed$.subscribe(onConfigChange)
this.configSubscription = this.subscribeUntilDestroyed(config.changed$, onConfigChange)
onConfigChange()
}

View File

@ -9,6 +9,7 @@ import {
Platform,
isWindowsBuild,
WIN_BUILD_FLUENT_BG_SUPPORTED,
BaseComponent,
} from 'terminus-core'
@ -17,7 +18,7 @@ import {
selector: 'window-settings-tab',
template: require('./windowSettingsTab.component.pug'),
})
export class WindowSettingsTabComponent {
export class WindowSettingsTabComponent extends BaseComponent {
screens: any[]
Platform = Platform
isFluentVibrancySupported = false
@ -29,10 +30,11 @@ export class WindowSettingsTabComponent {
public zone: NgZone,
@Inject(Theme) public themes: Theme[],
) {
super()
this.screens = this.docking.getScreens()
this.themes = config.enabledServices(this.themes)
hostApp.displaysChanged$.subscribe(() => {
this.subscribeUntilDestroyed(hostApp.displaysChanged$, () => {
this.zone.run(() => this.screens = this.docking.getScreens())
})

View File

@ -8,7 +8,6 @@ import { BaseTerminalTabComponent } from 'terminus-terminal'
import { SSHService } from '../services/ssh.service'
import { SSHConnection, SSHSession } from '../api'
import { SSHPortForwardingModalComponent } from './sshPortForwardingModal.component'
import { Subscription } from 'rxjs'
/** @hidden */
@ -22,7 +21,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
connection?: SSHConnection
session: SSHSession|null = null
private sessionStack: SSHSession[] = []
private homeEndSubscription: Subscription
private recentInputs = ''
private reconnectOffered = false
private spinner = new Spinner({
@ -50,7 +48,7 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
this.enableDynamicTitle = !this.connection.disableDynamicTitle
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) {
return
}
@ -225,11 +223,6 @@ export class SSHTabComponent extends BaseTerminalTabComponent {
)).response === 1
}
ngOnDestroy (): void {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
}
private startSpinner () {
this.spinner.setSpinnerString(6)
this.spinner.start()

View File

@ -4,7 +4,7 @@ import { first } from 'rxjs/operators'
import colors from 'ansi-colors'
import { NgZone, OnInit, OnDestroy, Injector, ViewChild, HostBinding, Input, ElementRef, InjectFlags } from '@angular/core'
import { trigger, transition, style, animate, AnimationTriggerMetadata } from '@angular/animations'
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent } from 'terminus-core'
import { AppService, ConfigService, BaseTabComponent, ElectronService, HostAppService, HotkeysService, NotificationsService, Platform, LogService, Logger, TabContextMenuItemProvider, SplitTabComponent, SubscriptionContainer } from 'terminus-core'
import { BaseSession, SessionsService } from '../services/sessions.service'
import { TerminalFrontendService } from '../services/terminalFrontend.service'
@ -95,12 +95,10 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
protected logger: Logger
protected output = new Subject<string>()
protected sessionChanged = new Subject<BaseSession|null>()
private sessionCloseSubscription: Subscription
private hotkeysSubscription: Subscription
private bellPlayer: HTMLAudioElement
private termContainerSubscriptions: Subscription[] = []
private termContainerSubscriptions = new SubscriptionContainer()
private allFocusModeSubscription: Subscription|null = null
private sessionHandlers: Subscription[] = []
private sessionHandlers = new SubscriptionContainer()
get input$ (): Observable<Buffer> {
if (!this.frontend) {
@ -149,7 +147,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.logger = this.log.create('baseTerminalTab')
this.setTitle('Terminal')
this.hotkeysSubscription = this.hotkeys.matchedHotkey.subscribe(async hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, async hotkey => {
if (!this.hasFocus) {
return
}
@ -475,7 +473,13 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
/** @hidden */
ngOnDestroy (): void {
super.ngOnDestroy()
}
async destroy (): Promise<void> {
this.frontend?.detach(this.content.nativeElement)
this.frontend = undefined
this.content.nativeElement.remove()
this.detachTermContainerHandlers()
this.config.enabledServices(this.decorators).forEach(decorator => {
try {
@ -484,14 +488,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.logger.warn('Decorator attach() throws', e)
}
})
this.hotkeysSubscription.unsubscribe()
if (this.sessionCloseSubscription) {
this.sessionCloseSubscription.unsubscribe()
}
this.output.complete()
}
async destroy (): Promise<void> {
super.destroy()
if (this.session?.open) {
await this.session.destroy()
@ -499,10 +497,7 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
protected detachTermContainerHandlers (): void {
for (const subscription of this.termContainerSubscriptions) {
subscription.unsubscribe()
}
this.termContainerSubscriptions = []
this.termContainerSubscriptions.cancelAll()
}
protected attachTermContainerHandlers (): void {
@ -518,71 +513,69 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
}
this.termContainerSubscriptions = [
this.frontend.title$.subscribe(title => this.zone.run(() => {
if (this.enableDynamicTitle) {
this.setTitle(title)
this.termContainerSubscriptions.subscribe(this.frontend.title$, title => this.zone.run(() => {
if (this.enableDynamicTitle) {
this.setTitle(title)
}
}))
this.termContainerSubscriptions.subscribe(this.focused$, () => this.frontend && (this.frontend.enableResizing = true))
this.termContainerSubscriptions.subscribe(this.blurred$, () => this.frontend && (this.frontend.enableResizing = false))
this.termContainerSubscriptions.subscribe(this.frontend.mouseEvent$, async event => {
if (event.type === 'mousedown') {
if (event.which === 2) {
if (this.config.store.terminal.pasteOnMiddleClick) {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
}
})),
this.focused$.subscribe(() => this.frontend && (this.frontend.enableResizing = true)),
this.blurred$.subscribe(() => this.frontend && (this.frontend.enableResizing = false)),
this.frontend.mouseEvent$.subscribe(async event => {
if (event.type === 'mousedown') {
if (event.which === 2) {
if (this.config.store.terminal.pasteOnMiddleClick) {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
}
if (event.which === 3 || event.which === 1 && event.ctrlKey) {
if (this.config.store.terminal.rightClick === 'menu') {
this.hostApp.popupContextMenu(await this.buildContextMenu())
} else if (this.config.store.terminal.rightClick === 'paste') {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
if (event.which === 3 || event.which === 1 && event.ctrlKey) {
if (this.config.store.terminal.rightClick === 'menu') {
this.hostApp.popupContextMenu(await this.buildContextMenu())
} else if (this.config.store.terminal.rightClick === 'paste') {
this.paste()
}
event.preventDefault()
event.stopPropagation()
return
}
if (event.type === 'mousewheel') {
let wheelDeltaY = 0
}
if (event.type === 'mousewheel') {
let wheelDeltaY = 0
if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
}
if (event.altKey) {
event.preventDefault()
const delta = Math.round(wheelDeltaY / 50)
this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
}
if ('wheelDeltaY' in event) {
wheelDeltaY = (event as MouseWheelEvent)['wheelDeltaY']
} else {
wheelDeltaY = (event as MouseWheelEvent)['deltaY']
}
}),
this.frontend.input$.subscribe(data => {
this.sendInput(data)
}),
if (event.altKey) {
event.preventDefault()
const delta = Math.round(wheelDeltaY / 50)
this.sendInput((delta > 0 ? '\u001bOA' : '\u001bOB').repeat(Math.abs(delta)))
}
}
})
this.frontend.resize$.subscribe(({ columns, rows }) => {
this.logger.debug(`Resizing to ${columns}x${rows}`)
this.size = { columns, rows }
this.zone.run(() => {
if (this.session?.open) {
this.session.resize(columns, rows)
}
})
}),
this.termContainerSubscriptions.subscribe(this.frontend.input$, data => {
this.sendInput(data)
})
this.hostApp.displayMetricsChanged$.subscribe(maybeConfigure),
this.hostApp.windowMoved$.subscribe(maybeConfigure),
]
this.termContainerSubscriptions.subscribe(this.frontend.resize$, ({ columns, rows }) => {
this.logger.debug(`Resizing to ${columns}x${rows}`)
this.size = { columns, rows }
this.zone.run(() => {
if (this.session?.open) {
this.session.resize(columns, rows)
}
})
})
this.termContainerSubscriptions.subscribe(this.hostApp.displayMetricsChanged$, maybeConfigure)
this.termContainerSubscriptions.subscribe(this.hostApp.windowMoved$, maybeConfigure)
}
setSession (session: BaseSession|null, destroyOnSessionClose = false): void {
@ -600,8 +593,8 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
this.sessionChanged.next(session)
}
protected attachSessionHandler (subscription: Subscription): void {
this.sessionHandlers.push(subscription)
protected attachSessionHandler <T> (observable: Observable<T>, handler: (v: T) => void): void {
this.sessionHandlers.subscribe(observable, handler)
}
protected attachSessionHandlers (destroyOnSessionClose = false): void {
@ -610,29 +603,26 @@ export class BaseTerminalTabComponent extends BaseTabComponent implements OnInit
}
// this.session.output$.bufferTime(10).subscribe((datas) => {
this.attachSessionHandler(this.session.output$.subscribe(data => {
this.attachSessionHandler(this.session.output$, data => {
if (this.enablePassthrough) {
this.output.next(data)
this.write(data)
}
}))
})
if (destroyOnSessionClose) {
this.attachSessionHandler(this.sessionCloseSubscription = this.session.closed$.subscribe(() => {
this.attachSessionHandler(this.session.closed$, () => {
this.frontend?.destroy()
this.destroy()
}))
})
}
this.attachSessionHandler(this.session.destroyed$.subscribe(() => {
this.attachSessionHandler(this.session.destroyed$, () => {
this.setSession(null)
}))
})
}
protected detachSessionHandlers (): void {
for (const s of this.sessionHandlers) {
s.unsubscribe()
}
this.sessionHandlers = []
this.sessionHandlers.cancelAll()
}
}

View File

@ -1,5 +1,4 @@
import { Component, Input, Injector } from '@angular/core'
import { Subscription } from 'rxjs'
import { BaseTabProcess, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBuild } from 'terminus-core'
import { BaseTerminalTabComponent } from '../api/baseTerminalTab.component'
import { SessionOptions } from '../api/interfaces'
@ -14,7 +13,6 @@ import { Session } from '../services/sessions.service'
})
export class TerminalTabComponent extends BaseTerminalTabComponent {
@Input() sessionOptions: SessionOptions
private homeEndSubscription: Subscription
session: Session|null = null
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
@ -30,7 +28,7 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
const isConPTY = isWindowsBuild(WIN_BUILD_CONPTY_SUPPORTED) && this.config.store.terminal.useConPTY
this.homeEndSubscription = this.hotkeys.matchedHotkey.subscribe(hotkey => {
this.subscribeUntilDestroyed(this.hotkeys.matchedHotkey, hotkey => {
if (!this.hasFocus) {
return
}
@ -106,7 +104,6 @@ export class TerminalTabComponent extends BaseTerminalTabComponent {
}
ngOnDestroy (): void {
this.homeEndSubscription.unsubscribe()
super.ngOnDestroy()
this.session?.destroy()
}

View File

@ -25,7 +25,7 @@ export class DebugDecorator extends TerminalDecorator {
}
}))
terminal.content.nativeElement.addEventListener('keyup', e => {
terminal.addEventListenerUntilDestroyed(terminal.content.nativeElement, 'keyup', (e: KeyboardEvent) => {
// Ctrl-Shift-Alt-1
if (e.which === 49 && e.ctrlKey && e.shiftKey && e.altKey) {
this.doSaveState(terminal)

View File

@ -34,7 +34,9 @@ export class XTermFrontend extends Frontend {
private fitAddon = new FitAddon()
private serializeAddon = new SerializeAddon()
private ligaturesAddon?: LigaturesAddon
private webGLAddon?: WebglAddon
private opened = false
private resizeObserver?: any
constructor () {
super()
@ -141,7 +143,8 @@ export class XTermFrontend extends Frontend {
await new Promise(resolve => setTimeout(resolve, process.env.XWEB ? 1000 : 0))
if (this.enableWebGL) {
this.xterm.loadAddon(new WebglAddon())
this.webGLAddon = new WebglAddon()
this.xterm.loadAddon(this.webGLAddon)
}
this.ready.next()
@ -160,12 +163,19 @@ export class XTermFrontend extends Frontend {
host.addEventListener('mouseup', event => this.mouseEvent.next(event))
host.addEventListener('mousewheel', event => this.mouseEvent.next(event as MouseEvent))
const ro = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler()))
ro.observe(host)
this.resizeObserver = new window['ResizeObserver'](() => setTimeout(() => this.resizeHandler()))
this.resizeObserver.observe(host)
}
detach (_host: HTMLElement): void {
window.removeEventListener('resize', this.resizeHandler)
this.resizeObserver?.disconnect()
}
destroy (): void {
super.destroy()
this.webGLAddon?.dispose()
this.xterm.dispose()
}
getSelection (): string {

View File

@ -7,7 +7,7 @@ const bundleAnalyzer = new BundleAnalyzerPlugin({
module.exports = options => {
const isDev = !!process.env.TERMINUS_DEV
const devtool = isDev && process.platform === 'win32' ? 'eval-cheap-module-source-map' : 'cheap-module-source-map'
const devtool = process.env.WEBPACK_DEVTOOL ?? (isDev && process.platform === 'win32' ? 'eval-cheap-module-source-map' : 'cheap-module-source-map')
const config = {
target: 'node',
entry: 'src/index.ts',