Skip to content

Commit

Permalink
fix: Runtime warning and error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
steilerDev committed Aug 27, 2023
1 parent 841c7e4 commit fab23e2
Show file tree
Hide file tree
Showing 36 changed files with 706 additions and 684 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"--runInBand",
"--config", "jest.config.json",
//"--detectOpenHandles",
"test/unit/app.test.ts"
"test/unit/sync-engine.helper.test.ts"
],
"env": {
"NODE_NO_WARNINGS": "1"
Expand Down
8 changes: 0 additions & 8 deletions app/src/app/error/codes/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,4 @@ export const SYNC: ErrorStruct = buildErrorStruct(

export const ARCHIVE: ErrorStruct = buildErrorStruct(
name, prefix, `ARCHIVE`, `Archive failed`,
);

export const LOGGER: ErrorStruct = buildErrorStruct(
name, prefix, `LOGGER`, `Unable to initialize logger`,
);

export const METRICS_EXPORTER: ErrorStruct = buildErrorStruct(
name, prefix, `METRICS_EXPORTER`, `Unable to initialize metrics exporter`,
);
4 changes: 0 additions & 4 deletions app/src/app/error/codes/icloud-photos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,4 @@ export const COUNT_DATA: ErrorStruct = buildErrorStruct(

export const FETCH_RECORDS: ErrorStruct = buildErrorStruct(
name, prefix, `FETCH_RECORDS`, `Unable to fetch records`,
);

export const COUNT_MISMATCH: ErrorStruct = buildErrorStruct(
name, prefix, `COUNT_MISMATCH`, `Received unexpected amount of records`,
);
16 changes: 0 additions & 16 deletions app/src/app/error/codes/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,10 @@ export const UNKNOWN_FILETYPE_EXTENSION: ErrorStruct = buildErrorStruct(
name, prefix, `UNKNOWN_FILETYPE_EXTENSION`, `Unknown filetype extension, please report in GH issue 143`,
);

export const INVALID_FILE: ErrorStruct = buildErrorStruct(
name, prefix, `INVALID_FILE`, `Found invalid file`,
);

export const DEAD_SYMLINK: ErrorStruct = buildErrorStruct(
name, prefix, `DEAD_SYMLINK`, `Found dead symlink (removing it)`,
);

export const UNKNOWN_SYMLINK_ERROR: ErrorStruct = buildErrorStruct(
name, prefix, `UNKNOWN_SYMLINK_ERROR`, `Unknown error while processing symlink`,
);

export const EXTRANEOUS_FILE: ErrorStruct = buildErrorStruct(
name, prefix, `EXTRANEOUS_FILE`, `Extraneous file found while processing a folder`,
);

export const NO_PARENT: ErrorStruct = buildErrorStruct(
name, prefix, `NO_PARENT`, `Unable to find parent of album`,
);
Expand All @@ -43,10 +31,6 @@ export const EXISTS: ErrorStruct = buildErrorStruct(
name, prefix, `EXISTS`, `Unable to create album: Already exists`,
);

export const LINK: ErrorStruct = buildErrorStruct(
name, prefix, `LINK`, `Unable to link assets`,
);

export const FIND_PATH: ErrorStruct = buildErrorStruct(
name, prefix, `FIND_PATH`, `Unable to find path`,
);
Expand Down
4 changes: 0 additions & 4 deletions app/src/app/error/codes/mfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ export const ADDR_IN_USE_ERR: ErrorStruct = buildErrorStruct(
name, prefix, `ADDR_IN_USE`, `HTTP Server could not start, because address/port is in use`,
);

export const RESEND_FAILED: ErrorStruct = buildErrorStruct(
name, prefix, `RESEND_FAILED`, `Unable to request new MFA code`,
);

export const SUBMIT_FAILED: ErrorStruct = buildErrorStruct(
name, prefix, `SUBMIT_FAILED`, `Unable to submit MFA code`,
);
Expand Down
4 changes: 4 additions & 0 deletions app/src/app/error/codes/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const UNABLE_TO_WRITE_FILE: ErrorStruct = buildErrorStruct(
name, prefix, `UNABLE_TO_WRITE_FILE`, `Unable to write resource file`,
);

export const UNABLE_TO_READ_FILE: ErrorStruct = buildErrorStruct(
name, prefix, `UNABLE_TO_READ_FILE`, `Unable to read resource file`,
);

export const NO_SESSION_SECRET: ErrorStruct = buildErrorStruct(
name, prefix, `NO_SESSION_SECRET`, `No session secret present`,
);
Expand Down
24 changes: 7 additions & 17 deletions app/src/app/error/error.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import {ErrorStruct, ERR_UNKNOWN} from "./error-codes.js";

/**
* Severity of the error
*/
type Severity = `WARN` | `FATAL`

/**
* Base class for this tool's error type
*/
Expand Down Expand Up @@ -35,9 +30,9 @@ export class iCPSError extends Error {
messages: string[] = [];

/**
* The severity of this error - fatal by default
* If this error was reported, it will receive a UUID for future reference
*/
sev: Severity = `FATAL`;
btUUID: string = undefined;

/**
* Creates an application specific error using the provided
Expand Down Expand Up @@ -93,21 +88,12 @@ export class iCPSError extends Error {
return this;
}

/**
* Sets the severity of this error to warning
* @returns This object for chaining convenience
*/
setWarning(): iCPSError {
this.sev = `WARN`;
return this;
}

/**
*
* @returns A description for this error, containing its cause chain's description
*/
getDescription(): string {
let desc = `${this.code} (${this.sev}): ${this.message}`;
let desc = `${this.code}: ${this.message}`;

if (this.messages.length > 0) {
desc += ` (${this.messages.join(`, `)})`;
Expand All @@ -122,6 +108,10 @@ export class iCPSError extends Error {
}
}

if (this.btUUID) {
desc += ` (error code: ${this.btUUID})`;
}

return desc;
}

Expand Down
113 changes: 62 additions & 51 deletions app/src/app/event/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import chalk from 'chalk';
import * as PACKAGE_INFO from '../../lib/package.js';
import {SingleBar} from 'cli-progress';
import {Resources} from '../../lib/resources/main.js';
import {iCPSEventApp, iCPSEventArchiveEngine, iCPSEventCloud, iCPSEventError, iCPSEventLog, iCPSEventMFA, iCPSEventPhotos, iCPSEventSyncEngine} from '../../lib/resources/events-types.js';
import {iCPSEventApp, iCPSEventArchiveEngine, iCPSEventCloud, iCPSEventMFA, iCPSEventPhotos, iCPSEventRuntimeError, iCPSEventRuntimeWarning, iCPSEventSyncEngine} from '../../lib/resources/events-types.js';
import {MFAMethod} from '../../lib/icloud/mfa/mfa-method.js';
import {iCPSError} from '../error/error.js';

/**
* This class handles the input/output to the command line
Expand All @@ -14,24 +15,14 @@ export class CLIInterface {
*/
progressBar: SingleBar;

/**
* Keeps track of write errors during sync to provide a summary at the end
*/
writeErrors: number = 0;

/**
* Keeps track of album link errors during sync to provide a summary at the end
*/
linkErrors: number = 0;

/**
* Creates a new CLI interface based on the provided components
* @param options - Parsed CLI Options
*/
constructor() {
this.progressBar = new SingleBar({
etaAsynchronousUpdate: true,
format: ` {bar} {percentage}% | Elapsed: {duration_formatted} | {value}/{total} assets downloaded`,
format: ` {bar} {percentage}% | Elapsed: {duration_formatted} | {value}/{total} assets downloaded\n`,
barCompleteChar: `\u25A0`,
barIncompleteChar: ` `,
});
Expand All @@ -47,21 +38,15 @@ export class CLIInterface {
this.print(chalk.white.bold(`Welcome to ${PACKAGE_INFO.NAME}, v.${PACKAGE_INFO.VERSION}!`));
this.print(chalk.green(`Made with <3 by steilerDev`));

if (Resources.manager().logToCli) {
Resources.events(this)
.on(iCPSEventLog.DEBUG, (instance: any, msg: string) => this.printLog(`debug`, instance, msg))
.on(iCPSEventLog.INFO, (instance: any, msg: string) => this.printLog(`info`, instance, msg))
.on(iCPSEventLog.WARN, (instance: any, msg: string) => this.printLog(`warn`, instance, msg))
.on(iCPSEventLog.ERROR, (instance: any, msg: string) => this.printLog(`error`, instance, msg));
}

if (!Resources.manager().suppressWarnings) {
Resources.events(this)
.on(iCPSEventError.HANDLER_WARN, (msg: string) => this.printWarning(msg));
.on(iCPSEventRuntimeWarning.MFA_ERROR, (err: Error) => this.printWarning(err.message))
.on(iCPSEventRuntimeWarning.FILETYPE_ERROR, (ext: string, descriptor: string) => this.printWarning(`Detected unknown filetype (${descriptor} with ${ext}), please report in GH issue 143`))
.on(iCPSEventRuntimeWarning.RESOURCE_FILE_ERROR, (err: iCPSError) => this.printWarning(err.getDescription()));
}

Resources.events(this)
.on(iCPSEventError.HANDLER_ERROR, (msg: string) => this.printFatalError(msg));
.on(iCPSEventRuntimeError.HANDLED_ERROR, (err: iCPSError) => this.printError(err.getDescription()));

Resources.events(this)
.on(iCPSEventCloud.AUTHENTICATION_STARTED, () => {
Expand Down Expand Up @@ -133,10 +118,35 @@ export class CLIInterface {
.on(iCPSEventSyncEngine.FETCH_N_LOAD, () => {
this.print(chalk.white(this.getHorizontalLine()));
this.print(chalk.white(`Loading local & fetching remote iCloud Library state...`));
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.EXTRANEOUS_FILE);
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.LIBRARY_LOAD_ERROR);

Resources.event().resetEventCounter(iCPSEventRuntimeWarning.COUNT_MISMATCH);
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.ICLOUD_LOAD_ERROR);
})
.on(iCPSEventSyncEngine.FETCH_N_LOAD_COMPLETED, (remoteAssetCount: number, remoteAlbumCount: number, localAssetCount: number, localAlbumCount: number) => {
this.print(chalk.green(`Loaded local state: ${localAssetCount} assets & ${localAlbumCount} albums`));

const extraneousFiles = Resources.event().getEventCount(iCPSEventRuntimeWarning.EXTRANEOUS_FILE);
if (extraneousFiles > 0) {
this.printWarning(`Detected ${extraneousFiles} extraneous files, please check the logs for more details.`);
}

const libraryLoadErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.LIBRARY_LOAD_ERROR);
if (libraryLoadErrors > 0) {
this.printWarning(`Unable to load ${libraryLoadErrors} local assets, please check the logs for more details.`);
}

this.print(chalk.green(`Fetched remote state: ${remoteAssetCount} assets & ${remoteAlbumCount} albums`));
const mismatchErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.COUNT_MISMATCH);
if (mismatchErrors > 0) {
this.printWarning(`Detected ${mismatchErrors} albums, where asset counts don't match, please check the logs for more details.`);
}

const iCloudLoadErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.ICLOUD_LOAD_ERROR);
if (iCloudLoadErrors > 0) {
this.printWarning(`Unable to load ${iCloudLoadErrors} remote assets, please check the logs for more details.`);
}
})
.on(iCPSEventSyncEngine.DIFF, () => {
this.print(chalk.white(`Diffing remote with local state...`));
Expand All @@ -150,40 +160,40 @@ export class CLIInterface {
.on(iCPSEventSyncEngine.WRITE_ASSETS, (toBeDeletedCount: number, toBeAddedCount: number, toBeKept: number) => {
this.print(chalk.cyan(`Syncing assets, by keeping ${toBeKept} and removing ${toBeDeletedCount} local assets, as well as adding ${toBeAddedCount} remote assets...`));
this.progressBar.start(toBeAddedCount, 0);
this.writeErrors = 0;

Resources.event().resetEventCounter(iCPSEventRuntimeWarning.WRITE_ASSET_ERROR);
})
.on(iCPSEventSyncEngine.WRITE_ASSET_COMPLETED, _recordName => {
.on(iCPSEventSyncEngine.WRITE_ASSET_COMPLETED, () => {
this.progressBar.increment();
})
.on(iCPSEventSyncEngine.WRITE_ASSET_ERROR, _recordName => {
this.writeErrors++;
.on(iCPSEventRuntimeWarning.WRITE_ASSET_ERROR, () => {
this.progressBar.increment();
})
.on(iCPSEventSyncEngine.WRITE_ASSETS_COMPLETED, () => {
this.progressBar.stop();

this.print(chalk.greenBright(`Asset sync completed!`));
if (this.writeErrors > 0) {
this.print(`Detected ${this.writeErrors} errors while adding assets, please check the logs for more details.`);
return;
const writeAssetErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.WRITE_ASSET_ERROR);
if (writeAssetErrors > 0) {
this.printWarning(`Detected ${writeAssetErrors} errors while adding assets, please check the logs for more details.`);
}

this.print(chalk.greenBright(`Successfully synced assets without errors!`));
})
.on(iCPSEventSyncEngine.WRITE_ALBUMS, (toBeDeletedCount: number, toBeAddedCount: number, toBeKept: number) => {
this.print(chalk.cyan(`Syncing albums, by keeping ${toBeKept} and removing ${toBeDeletedCount} local albums, as well as adding ${toBeAddedCount} remote albums...`));
this.linkErrors = 0;
})
.on(iCPSEventSyncEngine.LINK_ERROR, (_assetUUID: string, _albumName: string) => {
this.linkErrors++;
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.WRITE_ALBUM_ERROR);
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.LINK_ERROR);
})
.on(iCPSEventSyncEngine.WRITE_ALBUMS_COMPLETED, () => {
this.print(chalk.greenBright(`Album sync completed!`));
if (this.linkErrors > 0) {
this.print(`Detected ${this.linkErrors} errors while linking album assets, please check the logs for more details.`);
const linkErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.LINK_ERROR);
if (linkErrors > 0) {
this.printWarning(`Detected ${linkErrors} errors while linking assets to albums, please check the logs for more details.`);
}

this.print(chalk.greenBright(`Successfully synced albums without errors!`));
const writeAlbumErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.WRITE_ALBUM_ERROR);
if (writeAlbumErrors > 0) {
this.printWarning(`Detected ${writeAlbumErrors} errors while writing albums, please check the logs for more details.`);
}
})
.on(iCPSEventSyncEngine.WRITE_COMPLETED, () => {
this.print(chalk.green(`Successfully wrote diff to disk!`));
Expand All @@ -193,15 +203,17 @@ export class CLIInterface {
this.print(chalk.green.bold(`Successfully completed sync at ${this.getDateTime()}`));
this.print(chalk.white(this.getHorizontalLine()));
})
.on(iCPSEventSyncEngine.RETRY, retryCount => {
.on(iCPSEventSyncEngine.RETRY, (retryCount: number, err: iCPSError) => {
this.progressBar.stop();
this.print(chalk.magenta(`Detected error during sync, refreshing iCloud connection & retrying (attempt #${retryCount})...`));
this.print(chalk.magenta(`Detected error during sync: ${err.getDescription()}`));
this.print(chalk.magenta(`Refreshing iCloud connection & retrying (attempt #${retryCount})...`));
this.print(chalk.white(this.getHorizontalLine()));
});

Resources.events(this)
.on(iCPSEventArchiveEngine.ARCHIVE_START, (path: string) => {
this.print(chalk.white.bold(`Archiving local path ${path}`));
Resources.event().resetEventCounter(iCPSEventRuntimeWarning.ARCHIVE_ASSET_ERROR);
})
.on(iCPSEventArchiveEngine.PERSISTING_START, (numberOfAssets: number) => {
this.print(chalk.cyan(`Persisting ${numberOfAssets} assets`));
Expand All @@ -212,6 +224,11 @@ export class CLIInterface {
.on(iCPSEventArchiveEngine.ARCHIVE_DONE, () => {
this.print(chalk.white(this.getHorizontalLine()));
this.print(chalk.green.bold(`Successfully completed archiving`));
const archiveAssetErrors = Resources.event().getEventCount(iCPSEventRuntimeWarning.ARCHIVE_ASSET_ERROR);
if (archiveAssetErrors > 0) {
this.printWarning(`Detected ${archiveAssetErrors} errors while archiving assets, please check the logs for more details.`);
}

this.print(chalk.white(this.getHorizontalLine()));
});
}
Expand All @@ -226,26 +243,20 @@ export class CLIInterface {
}
}

printLog(level: string, instance: any, msg: string) {
console.log(`${chalk.gray(`[${new Date().toLocaleString()}] ${level.toUpperCase()}`)} ${chalk.white(`${String(instance.constructor.name)}: ${msg}`)}`);
}

/**
* Prints a warning
* @param msg - The message string
* @param msg - The warning string
*/
printWarning(msg: string) {
this.print(chalk.yellow(msg));
this.print(chalk.yellow(`Warning: ${msg}`));
}

/**
* Prints a fatal error
* Prints an error
* @param err - The error string
*/
printFatalError(err: string) {
this.print(chalk.red(this.getHorizontalLine()));
this.print(chalk.red(`Experienced fatal error at ${this.getDateTime()}: ${err}`));
this.print(chalk.red(this.getHorizontalLine()));
printError(err: string) {
this.print(chalk.red(`Error: ${err}`));
}

/**
Expand Down

0 comments on commit fab23e2

Please sign in to comment.