2021-11-18 01:26:32 +00:00
import * as electronRemote from '@electron/remote' ;
const electronConsole = electronRemote . getGlobal ( 'console' ) as Console ;
import Logger from '../../logger/logger' ;
const LOG = Logger . create ( __filename , electronConsole ) ;
2021-10-30 17:26:41 +00:00
import * as fs from 'fs/promises' ;
import * as path from 'path' ;
2021-11-18 01:26:32 +00:00
import * as socketio from 'socket.io-client' ;
2021-10-30 17:26:41 +00:00
2021-11-07 16:50:30 +00:00
import Q from './q-module' ;
import createErrorIndicator from './elements/error-indicator' ;
2021-11-18 01:26:32 +00:00
2021-10-30 17:26:41 +00:00
interface WithPotentialErrorParams {
taskFunc : ( ( ) = > Promise < void > ) ,
errorIndicatorAddFunc : ( ( element : HTMLElement ) = > Promise < void > ) ,
errorContainer : HTMLElement ,
errorClasses? : string [ ] ,
errorMessage : string ,
}
export default class Util {
static async exists ( path : string ) : Promise < boolean > {
// Check if a file exists
try {
await fs . stat ( path ) ;
return true ;
} catch ( e ) {
if ( e . code == 'ENOENT' ) {
return false ;
} else {
throw new Error ( 'Error checking if file exists: ' + e . code ) ;
}
}
}
static sanitize ( name : string ) : string {
// Windows Version (created for Windows, most likely works cross-platform too given my research)
// Allowed Characters: Extended Unicode Charset (1-255)
// Illegal file names: CON, PRN, AUX, NUL, COM1, COM2, ..., COM9, LPT1, LPT2, ..., LPT9
// Reserved Characters: <>:"/\|?*
// Solution: Replace reserved characters with empty string (''), bad characters with '_', and append '_' to bad names
// Illegal File Names (Windows)
if ( [ 'CON' , 'PRN' , 'AUX' , 'NUL' ,
'COM1' , 'COM2' , 'COM3' , 'COM4' , 'COM5' , 'COM6' , 'COM7' , 'COM8' , 'COM9' ,
'LPT1' , 'LPT2' , 'LPT3' , 'LPT4' , 'LPT5' , 'LPT6' , 'LPT7' , 'LPT8' , 'LPT9' ] . indexOf ( name ) != - 1 ) { // TODO: case insensitive?
name += '_' ;
}
// Reserved Characters
name = name . replace ( /[<>:\"\/\\|?*]/g , '' ) ;
// Allowed Characters
return name . split ( '' ) . map ( c = > c . charCodeAt ( 0 ) < 255 && c . charCodeAt ( 0 ) > 0 ? c : '_' ) . join ( '' ) ;
// Much stricter whitelist version
// replace bad characters with '_'
//return name.split('').map(c => /[A-Za-z0-9-]/.exec(c) ? c : '_').join('');
}
// Checks a directory for a file with matching name, adding a number to the name
// until such file does not exist in the directory. The number will start at 2 and
// increment by one. E.g. file.txt -> file2.txt -> file3.txt
// Does not join the name with the dir
static async getAvailableFileName ( dir : string , name : string ) : Promise < string > {
name = Util . sanitize ( name ) ;
let ext = path . extname ( name ) ;
let baseName = path . basename ( name , ext ) ;
let availableBaseName = baseName ;
let tries = 1 ;
while ( await Util . exists ( path . join ( dir , availableBaseName + ext ) ) ) {
availableBaseName = baseName + '-' + ( ++ tries ) ;
}
return availableBaseName + ext ;
}
// This MUST be called before an errorContainer is removed from the document to prevent memory leaks (if it's parent element is removed, that's the big problem)
// This can also be called to remove error indicators from an error container.
2021-11-07 16:50:30 +00:00
static removeErrorIndicators ( q : Q , errorContainer : HTMLElement , extraClasses? : string [ ] ) : void {
2021-10-30 17:26:41 +00:00
extraClasses = extraClasses ? ? [ ] ;
let querySelector = '.error-indicator' + extraClasses . map ( e = > '.' + e ) . join ( '' ) ;
2021-11-07 16:50:30 +00:00
for ( let element of q . $ $ $ $ ( errorContainer , querySelector ) ) {
2021-10-30 17:26:41 +00:00
element . parentElement ? . removeChild ( element ) ; // The MutationObserver will reject the outstanding Promise
}
}
// Will return once the fetchFunc was called successfully OR the request was canceled.
// If the error indicator element is removed from the error container, this will reject
// Note: Detected using MutationObservers
// If the error container removed from the document, this could result in memory leaks
// NOTE: This should NOT be called within an element lock
2021-11-07 16:50:30 +00:00
static async withPotentialError ( q : Q , params : WithPotentialErrorParams ) : Promise < unknown > {
2021-10-30 17:26:41 +00:00
const { taskFunc , errorIndicatorAddFunc , errorContainer , errorClasses , errorMessage } = params ;
return await new Promise ( async ( resolve , reject ) = > {
try {
await taskFunc ( ) ;
resolve ( null ) ;
} catch ( e ) {
2021-12-01 02:45:06 +00:00
LOG . debug ( 'params' , { params } ) ;
2021-10-30 17:26:41 +00:00
LOG . error ( 'caught potential error' , e ) ;
2021-11-07 16:50:30 +00:00
let errorIndicatorElement = createErrorIndicator ( q , {
2021-10-30 17:26:41 +00:00
container : errorContainer ,
classes : errorClasses ,
message : errorMessage ,
taskFunc : taskFunc ,
resolveFunc : resolve ,
rejectFunc : reject
} ) ;
await errorIndicatorAddFunc ( errorIndicatorElement ) ;
if ( errorIndicatorElement . parentElement != errorContainer ) {
if ( errorIndicatorElement . parentElement ) {
errorIndicatorElement . parentElement . removeChild ( errorIndicatorElement ) ;
}
LOG . error ( 'error indicator was not added to the error container' ) ;
reject ( new Error ( 'bad errorIndicatorAddFunc' ) ) ;
}
}
} ) ;
}
2021-11-07 16:50:30 +00:00
static async withPotentialErrorWarnOnCancel ( q : Q , params : WithPotentialErrorParams ) {
2021-10-30 17:26:41 +00:00
try {
2021-11-07 16:50:30 +00:00
await Util . withPotentialError ( q , params )
2021-10-30 17:26:41 +00:00
} catch ( e ) {
LOG . warn ( 'with potential error canceled:' , e ) ;
}
}
2021-11-18 01:26:32 +00:00
// This function is a promise for error stack tracking purposes.
// this function expects the last argument of args to be a callback(err, serverData)
static socketEmitTimeout (
socket : socketio.Socket ,
timeoutMS : number ,
endpoint : string ,
. . . args : any [ ]
) : Promise < void > {
return new Promise < void > ( ( resolve ) = > {
let socketArgs = args . slice ( 0 , args . length - 1 ) ;
let respond = args [ args . length - 1 ] ;
if ( typeof respond !== 'function' ) {
throw new Error ( 'no response function provided' ) ;
}
let cutoff = false ;
let timeout = setTimeout ( ( ) = > {
cutoff = true ;
respond ( 'emit timeout' ) ;
resolve ( ) ;
} , timeoutMS ) ;
socket . emit ( endpoint , . . . socketArgs , ( . . . respondArgs : any [ ] ) = > {
if ( cutoff ) {
LOG . warn ( ` @ ${ endpoint } /[ ${ LOG . inspect ( socketArgs ) } ]: result came after timeout ` ) ;
return ;
}
clearTimeout ( timeout ) ;
respond ( . . . respondArgs ) ;
resolve ( ) ;
} ) ;
} ) ;
}
2021-10-30 17:26:41 +00:00
}