diff --git a/terminus-core/src/services/vault.service.ts b/terminus-core/src/services/vault.service.ts index 47ba5a0b..3f7826f4 100644 --- a/terminus-core/src/services/vault.service.ts +++ b/terminus-core/src/services/vault.service.ts @@ -5,6 +5,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { AsyncSubject, Subject, Observable } from 'rxjs' import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component' import { NotificationsService } from '../services/notifications.service' +import { wrapPromise } from 'utils' const PBKDF_ITERATIONS = 100000 const PBKDF_DIGEST = 'sha512' @@ -120,7 +121,7 @@ export class VaultService { passphrase = await this.getPassphrase() } try { - return await this.wrapPromise(decryptVault(storage, passphrase)) + return await wrapPromise(this.zone, decryptVault(storage, passphrase)) } catch (e) { _rememberedPassphrase = null if (e.toString().includes('BAD_DECRYPT')) { @@ -144,7 +145,7 @@ export class VaultService { if (_rememberedPassphrase) { _rememberedPassphrase = passphrase } - return this.wrapPromise(encryptVault(vault, passphrase)) + return wrapPromise(this.zone, encryptVault(vault, passphrase)) } async save (vault: Vault, passphrase?: string): Promise { @@ -210,14 +211,4 @@ export class VaultService { isEnabled (): boolean { return !!this.store } - - private wrapPromise (promise: Promise): Promise { - return new Promise((resolve, reject) => { - promise.then(result => { - this.zone.run(() => resolve(result)) - }).catch(error => { - this.zone.run(() => reject(error)) - }) - }) - } } diff --git a/terminus-core/src/utils.ts b/terminus-core/src/utils.ts index 6f74d512..3c32ecfc 100644 --- a/terminus-core/src/utils.ts +++ b/terminus-core/src/utils.ts @@ -1,4 +1,5 @@ import * as os from 'os' +import { NgZone } from '@angular/core' export const WIN_BUILD_CONPTY_SUPPORTED = 17692 export const WIN_BUILD_CONPTY_STABLE = 18309 @@ -20,3 +21,13 @@ export function getCSSFontFamily (config: any): string { fonts = fonts.map(x => `"${x}"`) return fonts.join(', ') } + +export function wrapPromise (zone: NgZone, promise: Promise): Promise { + return new Promise((resolve, reject) => { + promise.then(result => { + zone.run(() => resolve(result)) + }).catch(error => { + zone.run(() => reject(error)) + }) + }) +} diff --git a/terminus-electron/src/services/platform.service.ts b/terminus-electron/src/services/platform.service.ts index 486621d4..5049fca4 100644 --- a/terminus-electron/src/services/platform.service.ts +++ b/terminus-electron/src/services/platform.service.ts @@ -5,7 +5,7 @@ import * as os from 'os' import promiseIpc from 'electron-promise-ipc' import { execFile } from 'mz/child_process' import { Injectable, NgZone } from '@angular/core' -import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions } from 'terminus-core' +import { PlatformService, ClipboardContent, HostAppService, Platform, ElectronService, MenuItemOptions, MessageBoxOptions, MessageBoxResult, FileUpload, FileDownload, FileUploadOptions, wrapPromise } from 'terminus-core' const fontManager = require('fontmanager-redux') // eslint-disable-line /* eslint-disable block-scoped-var */ @@ -181,7 +181,7 @@ export class ElectronPlatformService extends PlatformService { return Promise.all(result.filePaths.map(async p => { const transfer = new ElectronFileUpload(p) - await this.wrapPromise(transfer.open()) + await wrapPromise(this.zone, transfer.open()) this.fileTransferStarted.next(transfer) return transfer })) @@ -198,20 +198,10 @@ export class ElectronPlatformService extends PlatformService { return null } const transfer = new ElectronFileDownload(result.filePath, size) - await this.wrapPromise(transfer.open()) + await wrapPromise(this.zone, transfer.open()) this.fileTransferStarted.next(transfer) return transfer } - - private wrapPromise (promise: Promise): Promise { - return new Promise((resolve, reject) => { - promise.then(result => { - this.zone.run(() => resolve(result)) - }).catch(error => { - this.zone.run(() => reject(error)) - }) - }) - } } class ElectronFileUpload extends FileUpload { diff --git a/terminus-ssh/src/api.ts b/terminus-ssh/src/api.ts index ce74c838..bffffeaa 100644 --- a/terminus-ssh/src/api.ts +++ b/terminus-ssh/src/api.ts @@ -5,12 +5,13 @@ import * as sshpk from 'sshpk' import colors from 'ansi-colors' import stripAnsi from 'strip-ansi' import socksv5 from 'socksv5' -import { Injector } from '@angular/core' +import { Injector, NgZone } from '@angular/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' -import { HostAppService, Logger, NotificationsService, Platform, PlatformService } from 'terminus-core' +import { HostAppService, Logger, NotificationsService, Platform, PlatformService, wrapPromise } from 'terminus-core' import { BaseSession } from 'terminus-terminal' import { Server, Socket, createServer, createConnection } from 'net' import { Client, ClientChannel, SFTPWrapper } from 'ssh2' +import type { FileEntry, Stats } from 'ssh2-streams' import { Subject, Observable } from 'rxjs' import { ProxyCommandStream } from './services/ssh.service' import { PasswordStorageService } from './services/passwordStorage.service' @@ -137,6 +138,77 @@ interface AuthMethod { path?: string } +export class SFTPFileHandle { + position = 0 + + constructor ( + private sftp: SFTPWrapper, + private handle: Buffer, + private zone: NgZone, + ) { } + + read (): Promise { + const buffer = Buffer.alloc(256 * 1024) + return wrapPromise(this.zone, new Promise((resolve, reject) => { + while (true) { + const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => { + if (err) { + reject(err) + return + } + this.position += read + resolve(buffer.slice(0, read)) + }) + if (!wait) { + break + } + } + })) + } + + write (chunk: Buffer): Promise { + return wrapPromise(this.zone, new Promise((resolve, reject) => { + while (true) { + const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => { + if (err) { + return reject(err) + } + this.position += chunk.length + resolve() + }) + if (!wait) { + break + } + } + })) + } + + close (): Promise { + return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle)) + } +} + +export class SFTPSession { + constructor (private sftp: SFTPWrapper, private zone: NgZone) { } + + readdir (p: string): Promise { + return wrapPromise(this.zone, promisify(f => this.sftp.readdir(p, f))()) + } + + readlink (p: string): Promise { + return wrapPromise(this.zone, promisify(f => this.sftp.readlink(p, f))()) + } + + stat (p: string): Promise { + return wrapPromise(this.zone, promisify(f => this.sftp.stat(p, f))()) + } + + async open (p: string, mode: string): Promise { + const handle = await wrapPromise(this.zone, promisify(f => this.sftp.open(p, mode, f))()) + return new SFTPFileHandle(this.sftp, handle, this.zone) + } +} + export class SSHSession extends BaseSession { scripts?: LoginScript[] shell?: ClientChannel @@ -161,6 +233,7 @@ export class SSHSession extends BaseSession { private hostApp: HostAppService private platform: PlatformService private notifications: NotificationsService + private zone: NgZone constructor ( injector: Injector, @@ -172,6 +245,7 @@ export class SSHSession extends BaseSession { this.hostApp = injector.get(HostAppService) this.platform = injector.get(PlatformService) this.notifications = injector.get(NotificationsService) + this.zone = injector.get(NgZone) this.scripts = connection.scripts ?? [] this.destroyed$.subscribe(() => { @@ -223,11 +297,11 @@ export class SSHSession extends BaseSession { this.remainingAuthMethods.push({ type: 'hostbased' }) } - async openSFTP (): Promise { + async openSFTP (): Promise { if (!this.sftp) { - this.sftp = await promisify(f => this.ssh.sftp(f))() + this.sftp = await wrapPromise(this.zone, promisify(f => this.ssh.sftp(f))()) } - return this.sftp + return new SFTPSession(this.sftp, this.zone) } async start (): Promise { diff --git a/terminus-ssh/src/components/sftpPanel.component.ts b/terminus-ssh/src/components/sftpPanel.component.ts index 22fd5560..49129b58 100644 --- a/terminus-ssh/src/components/sftpPanel.component.ts +++ b/terminus-ssh/src/components/sftpPanel.component.ts @@ -1,8 +1,6 @@ import { Component, Input, Output, EventEmitter } from '@angular/core' -import { SFTPWrapper } from 'ssh2' -import type { FileEntry, Stats } from 'ssh2-streams' -import { promisify } from 'util' -import { SSHSession } from '../api' +import type { FileEntry } from 'ssh2-streams' +import { SSHSession, SFTPSession } from '../api' import * as path from 'path' import * as C from 'constants' import { FileUpload, PlatformService } from 'terminus-core' @@ -21,7 +19,7 @@ interface PathSegment { export class SFTPPanelComponent { @Input() session: SSHSession @Output() closed = new EventEmitter() - sftp: SFTPWrapper + sftp: SFTPSession fileList: FileEntry[]|null = null path = '/' pathSegments: PathSegment[] = [] @@ -49,8 +47,7 @@ export class SFTPPanelComponent { } this.fileList = null - this.fileList = await promisify(f => this.sftp.readdir(this.path, f))() - console.log(this.fileList) + this.fileList = await this.sftp.readdir(this.path) const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0 this.fileList.sort((a, b) => @@ -77,8 +74,8 @@ export class SFTPPanelComponent { if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) { this.navigate(path.join(this.path, item.filename)) } else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) { - const target = await promisify(f => this.sftp.readlink(itemPath, f))() - const stat = await promisify(f => this.sftp.stat(target, f))() + const target = await this.sftp.readlink(itemPath) + const stat = await this.sftp.stat(target) if (stat.isDirectory()) { this.navigate(itemPath) } else { @@ -104,30 +101,15 @@ export class SFTPPanelComponent { async uploadOne (transfer: FileUpload): Promise { const itemPath = path.join(this.path, transfer.getName()) try { - const handle = await promisify(f => this.sftp.open(itemPath, 'w', f))() - let position = 0 + const handle = await this.sftp.open(itemPath, 'w') while (true) { const chunk = await transfer.read() if (!chunk.length) { break } - const p = position - await new Promise((resolve, reject) => { - while (true) { - const wait = this.sftp.write(handle, chunk, 0, chunk.length, p, err => { - if (err) { - return reject(err) - } - resolve() - }) - if (!wait) { - break - } - } - }) - position += chunk.length + await handle.write(chunk) } - this.sftp.close(handle, () => null) + handle.close() transfer.close() } catch (e) { transfer.cancel() @@ -141,33 +123,16 @@ export class SFTPPanelComponent { return } try { - const handle = await promisify(f => this.sftp.open(itemPath, 'r', f))() - const buffer = Buffer.alloc(256 * 1024) - let position = 0 + const handle = await this.sftp.open(itemPath, 'r') while (true) { - const p = position - const chunk: Buffer = await new Promise((resolve, reject) => { - while (true) { - const wait = this.sftp.read(handle, buffer, 0, buffer.length, p, (err, read) => { - if (err) { - reject(err) - return - } - resolve(buffer.slice(0, read)) - }) - if (!wait) { - break - } - } - }) + const chunk = await handle.read() if (!chunk.length) { break } await transfer.write(chunk) - position += chunk.length } transfer.close() - this.sftp.close(handle, () => null) + handle.close() } catch (e) { transfer.cancel() throw e