zone-ified sftp promises

This commit is contained in:
Eugene Pankov 2021-06-13 21:03:33 +02:00
parent a2ed674b10
commit 6ce76af9be
No known key found for this signature in database
GPG Key ID: 5896FCBBDD1CF4F4
5 changed files with 108 additions and 77 deletions

View File

@ -5,6 +5,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { AsyncSubject, Subject, Observable } from 'rxjs' import { AsyncSubject, Subject, Observable } from 'rxjs'
import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component' import { UnlockVaultModalComponent } from '../components/unlockVaultModal.component'
import { NotificationsService } from '../services/notifications.service' import { NotificationsService } from '../services/notifications.service'
import { wrapPromise } from 'utils'
const PBKDF_ITERATIONS = 100000 const PBKDF_ITERATIONS = 100000
const PBKDF_DIGEST = 'sha512' const PBKDF_DIGEST = 'sha512'
@ -120,7 +121,7 @@ export class VaultService {
passphrase = await this.getPassphrase() passphrase = await this.getPassphrase()
} }
try { try {
return await this.wrapPromise(decryptVault(storage, passphrase)) return await wrapPromise(this.zone, decryptVault(storage, passphrase))
} catch (e) { } catch (e) {
_rememberedPassphrase = null _rememberedPassphrase = null
if (e.toString().includes('BAD_DECRYPT')) { if (e.toString().includes('BAD_DECRYPT')) {
@ -144,7 +145,7 @@ export class VaultService {
if (_rememberedPassphrase) { if (_rememberedPassphrase) {
_rememberedPassphrase = passphrase _rememberedPassphrase = passphrase
} }
return this.wrapPromise(encryptVault(vault, passphrase)) return wrapPromise(this.zone, encryptVault(vault, passphrase))
} }
async save (vault: Vault, passphrase?: string): Promise<void> { async save (vault: Vault, passphrase?: string): Promise<void> {
@ -210,14 +211,4 @@ export class VaultService {
isEnabled (): boolean { isEnabled (): boolean {
return !!this.store return !!this.store
} }
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
promise.then(result => {
this.zone.run(() => resolve(result))
}).catch(error => {
this.zone.run(() => reject(error))
})
})
}
} }

View File

@ -1,4 +1,5 @@
import * as os from 'os' import * as os from 'os'
import { NgZone } from '@angular/core'
export const WIN_BUILD_CONPTY_SUPPORTED = 17692 export const WIN_BUILD_CONPTY_SUPPORTED = 17692
export const WIN_BUILD_CONPTY_STABLE = 18309 export const WIN_BUILD_CONPTY_STABLE = 18309
@ -20,3 +21,13 @@ export function getCSSFontFamily (config: any): string {
fonts = fonts.map(x => `"${x}"`) fonts = fonts.map(x => `"${x}"`)
return fonts.join(', ') return fonts.join(', ')
} }
export function wrapPromise <T> (zone: NgZone, promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
promise.then(result => {
zone.run(() => resolve(result))
}).catch(error => {
zone.run(() => reject(error))
})
})
}

View File

@ -5,7 +5,7 @@ import * as os from 'os'
import promiseIpc from 'electron-promise-ipc' import promiseIpc from 'electron-promise-ipc'
import { execFile } from 'mz/child_process' import { execFile } from 'mz/child_process'
import { Injectable, NgZone } from '@angular/core' 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 const fontManager = require('fontmanager-redux') // eslint-disable-line
/* eslint-disable block-scoped-var */ /* eslint-disable block-scoped-var */
@ -181,7 +181,7 @@ export class ElectronPlatformService extends PlatformService {
return Promise.all(result.filePaths.map(async p => { return Promise.all(result.filePaths.map(async p => {
const transfer = new ElectronFileUpload(p) const transfer = new ElectronFileUpload(p)
await this.wrapPromise(transfer.open()) await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer) this.fileTransferStarted.next(transfer)
return transfer return transfer
})) }))
@ -198,20 +198,10 @@ export class ElectronPlatformService extends PlatformService {
return null return null
} }
const transfer = new ElectronFileDownload(result.filePath, size) const transfer = new ElectronFileDownload(result.filePath, size)
await this.wrapPromise(transfer.open()) await wrapPromise(this.zone, transfer.open())
this.fileTransferStarted.next(transfer) this.fileTransferStarted.next(transfer)
return transfer return transfer
} }
private wrapPromise <T> (promise: Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
promise.then(result => {
this.zone.run(() => resolve(result))
}).catch(error => {
this.zone.run(() => reject(error))
})
})
}
} }
class ElectronFileUpload extends FileUpload { class ElectronFileUpload extends FileUpload {

View File

@ -5,12 +5,13 @@ import * as sshpk from 'sshpk'
import colors from 'ansi-colors' import colors from 'ansi-colors'
import stripAnsi from 'strip-ansi' import stripAnsi from 'strip-ansi'
import socksv5 from 'socksv5' import socksv5 from 'socksv5'
import { Injector } from '@angular/core' import { Injector, NgZone } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 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 { BaseSession } from 'terminus-terminal'
import { Server, Socket, createServer, createConnection } from 'net' import { Server, Socket, createServer, createConnection } from 'net'
import { Client, ClientChannel, SFTPWrapper } from 'ssh2' import { Client, ClientChannel, SFTPWrapper } from 'ssh2'
import type { FileEntry, Stats } from 'ssh2-streams'
import { Subject, Observable } from 'rxjs' import { Subject, Observable } from 'rxjs'
import { ProxyCommandStream } from './services/ssh.service' import { ProxyCommandStream } from './services/ssh.service'
import { PasswordStorageService } from './services/passwordStorage.service' import { PasswordStorageService } from './services/passwordStorage.service'
@ -137,6 +138,77 @@ interface AuthMethod {
path?: string path?: string
} }
export class SFTPFileHandle {
position = 0
constructor (
private sftp: SFTPWrapper,
private handle: Buffer,
private zone: NgZone,
) { }
read (): Promise<Buffer> {
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<void> {
return wrapPromise(this.zone, new Promise<void>((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<void> {
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<FileEntry[]> {
return wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
}
readlink (p: string): Promise<string> {
return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
}
stat (p: string): Promise<Stats> {
return wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
}
async open (p: string, mode: string): Promise<SFTPFileHandle> {
const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
return new SFTPFileHandle(this.sftp, handle, this.zone)
}
}
export class SSHSession extends BaseSession { export class SSHSession extends BaseSession {
scripts?: LoginScript[] scripts?: LoginScript[]
shell?: ClientChannel shell?: ClientChannel
@ -161,6 +233,7 @@ export class SSHSession extends BaseSession {
private hostApp: HostAppService private hostApp: HostAppService
private platform: PlatformService private platform: PlatformService
private notifications: NotificationsService private notifications: NotificationsService
private zone: NgZone
constructor ( constructor (
injector: Injector, injector: Injector,
@ -172,6 +245,7 @@ export class SSHSession extends BaseSession {
this.hostApp = injector.get(HostAppService) this.hostApp = injector.get(HostAppService)
this.platform = injector.get(PlatformService) this.platform = injector.get(PlatformService)
this.notifications = injector.get(NotificationsService) this.notifications = injector.get(NotificationsService)
this.zone = injector.get(NgZone)
this.scripts = connection.scripts ?? [] this.scripts = connection.scripts ?? []
this.destroyed$.subscribe(() => { this.destroyed$.subscribe(() => {
@ -223,11 +297,11 @@ export class SSHSession extends BaseSession {
this.remainingAuthMethods.push({ type: 'hostbased' }) this.remainingAuthMethods.push({ type: 'hostbased' })
} }
async openSFTP (): Promise<SFTPWrapper> { async openSFTP (): Promise<SFTPSession> {
if (!this.sftp) { if (!this.sftp) {
this.sftp = await promisify<SFTPWrapper>(f => this.ssh.sftp(f))() this.sftp = await wrapPromise(this.zone, promisify<SFTPWrapper>(f => this.ssh.sftp(f))())
} }
return this.sftp return new SFTPSession(this.sftp, this.zone)
} }
async start (): Promise<void> { async start (): Promise<void> {

View File

@ -1,8 +1,6 @@
import { Component, Input, Output, EventEmitter } from '@angular/core' import { Component, Input, Output, EventEmitter } from '@angular/core'
import { SFTPWrapper } from 'ssh2' import type { FileEntry } from 'ssh2-streams'
import type { FileEntry, Stats } from 'ssh2-streams' import { SSHSession, SFTPSession } from '../api'
import { promisify } from 'util'
import { SSHSession } from '../api'
import * as path from 'path' import * as path from 'path'
import * as C from 'constants' import * as C from 'constants'
import { FileUpload, PlatformService } from 'terminus-core' import { FileUpload, PlatformService } from 'terminus-core'
@ -21,7 +19,7 @@ interface PathSegment {
export class SFTPPanelComponent { export class SFTPPanelComponent {
@Input() session: SSHSession @Input() session: SSHSession
@Output() closed = new EventEmitter<void>() @Output() closed = new EventEmitter<void>()
sftp: SFTPWrapper sftp: SFTPSession
fileList: FileEntry[]|null = null fileList: FileEntry[]|null = null
path = '/' path = '/'
pathSegments: PathSegment[] = [] pathSegments: PathSegment[] = []
@ -49,8 +47,7 @@ export class SFTPPanelComponent {
} }
this.fileList = null this.fileList = null
this.fileList = await promisify<FileEntry[]>(f => this.sftp.readdir(this.path, f))() this.fileList = await this.sftp.readdir(this.path)
console.log(this.fileList)
const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0 const dirKey = a => (a.attrs.mode & C.S_IFDIR) === C.S_IFDIR ? 1 : 0
this.fileList.sort((a, b) => this.fileList.sort((a, b) =>
@ -77,8 +74,8 @@ export class SFTPPanelComponent {
if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) { if ((item.attrs.mode & C.S_IFDIR) === C.S_IFDIR) {
this.navigate(path.join(this.path, item.filename)) this.navigate(path.join(this.path, item.filename))
} else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) { } else if ((item.attrs.mode & C.S_IFLNK) === C.S_IFLNK) {
const target = await promisify<string>(f => this.sftp.readlink(itemPath, f))() const target = await this.sftp.readlink(itemPath)
const stat = await promisify<Stats>(f => this.sftp.stat(target, f))() const stat = await this.sftp.stat(target)
if (stat.isDirectory()) { if (stat.isDirectory()) {
this.navigate(itemPath) this.navigate(itemPath)
} else { } else {
@ -104,30 +101,15 @@ export class SFTPPanelComponent {
async uploadOne (transfer: FileUpload): Promise<void> { async uploadOne (transfer: FileUpload): Promise<void> {
const itemPath = path.join(this.path, transfer.getName()) const itemPath = path.join(this.path, transfer.getName())
try { try {
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'w', f))() const handle = await this.sftp.open(itemPath, 'w')
let position = 0
while (true) { while (true) {
const chunk = await transfer.read() const chunk = await transfer.read()
if (!chunk.length) { if (!chunk.length) {
break break
} }
const p = position await handle.write(chunk)
await new Promise<void>((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
} }
this.sftp.close(handle, () => null) handle.close()
transfer.close() transfer.close()
} catch (e) { } catch (e) {
transfer.cancel() transfer.cancel()
@ -141,33 +123,16 @@ export class SFTPPanelComponent {
return return
} }
try { try {
const handle = await promisify<Buffer>(f => this.sftp.open(itemPath, 'r', f))() const handle = await this.sftp.open(itemPath, 'r')
const buffer = Buffer.alloc(256 * 1024)
let position = 0
while (true) { while (true) {
const p = position const chunk = await handle.read()
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
}
}
})
if (!chunk.length) { if (!chunk.length) {
break break
} }
await transfer.write(chunk) await transfer.write(chunk)
position += chunk.length
} }
transfer.close() transfer.close()
this.sftp.close(handle, () => null) handle.close()
} catch (e) { } catch (e) {
transfer.cancel() transfer.cancel()
throw e throw e