Merge pull request #2345 from NguyenTranHoangSym/SDA-4415

SDA-4415: Add IV Zeus log to logs bundle
This commit is contained in:
NguyenTranHoangSym authored and GitHub committed 2025-04-08 10:52:59 +07:00
commit 5ea9928de2
7 files changed
+421 -7

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
import { FileHelper } from '../../src/app/helpers/file/files-helper';
import * as fs from 'fs';
import { IFile } from '../../src/app/interfaces/file-helper.interface';
import { LogCategory } from '../../src/app/interfaces/reports-handlers.interface';
import { isLinux, isMac } from '../../src/common/env';
describe('files-helper', () => {
let fileHelper: FileHelper;
let folderPath = 'C:\\abc';
let fsMock;
let logs = [
'C9Zeus.2529612.log',
'C9Zeus.1.log',
'C9Trader.25296.log',
'C9Trader.25.log',
'HEHE.txt',
'HAH.txt',
];
beforeEach(() => {
fileHelper = new FileHelper();
fileHelper.createFiles();
fsMock = jest.spyOn(fs, 'readdirSync').mockImplementation(() => logs);
});
afterEach(() => {
fsMock.mockRestore();
});
it('should clean file names', () => {
const validatedFileName = fileHelper.validateFilename(
'console.log("hello world").txt',
);
expect(validatedFileName).toBe('console.log__hello_world__.txt');
});
it('should clean file names', () => {
const validatedPath = fileHelper.validateFilePath(
'SELEC*FROM ABC/hello-world/haha',
);
expect(validatedPath).toBe(false);
});
it('should clean file path', () => {
const validatedPath = fileHelper.validateFilePath(
'SELEC*FROM ABC/hello-world/haha',
);
expect(validatedPath).toBe(false);
});
it('should return enough logs in folder - old modified', () => {
const stats = {
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atimeMs: 1318289051000.1,
mtimeMs: 1318289051000.1,
ctimeMs: 1318289051000.1,
birthtimeMs: 1318289051000.1,
atime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
mtime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
ctime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
birthtime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
isFile: () => true,
};
const now = new Date();
jest.spyOn(fs, 'statSync').mockImplementation((path) => {
switch (path) {
case `${folderPath}\\${logs[0]}`: {
const newStats = { ...stats };
newStats.mtime = new Date(now.getTime() - 360 * 60 * 1000);
return newStats;
}
case `${folderPath}\\${logs[2]}`: {
const newStats = { ...stats };
newStats.mtime = new Date(now.getTime() - 380 * 60 * 1000);
return stats;
}
default: {
return stats;
}
}
});
const files = fileHelper.getLatestModifiedFiles(folderPath);
const firstModifiedFile: IFile = {
fileName: logs[0],
platform: '',
modifiedTimestamp: new Date(now.getTime() - 360 * 60 * 1000).getTime(),
};
if (isLinux || isMac) {
expect(files.get(LogCategory.RECENT)).toEqual(undefined);
} else {
expect(files.get(LogCategory.RECENT)).toEqual(firstModifiedFile);
}
});
it('should return enough logs in folder - mixed modified but get latest ones', () => {
const stats = {
dev: 2114,
ino: 48064969,
mode: 33188,
nlink: 1,
uid: 85,
gid: 100,
rdev: 0,
size: 527,
blksize: 4096,
blocks: 8,
atimeMs: 1318289051000.1,
mtimeMs: 1318289051000.1,
ctimeMs: 1318289051000.1,
birthtimeMs: 1318289051000.1,
atime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
mtime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
ctime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
birthtime: new Date('Mon, 10 Oct 2011 23:24:11 GMT'),
isFile: () => true,
};
const now = new Date();
jest.spyOn(fs, 'statSync').mockImplementation((path) => {
switch (path) {
case `${folderPath}\\${logs[0]}`: {
const newStats = { ...stats };
newStats.mtime = new Date(now.getTime() - 15 * 60 * 1000);
return newStats;
}
case `${folderPath}\\${logs[2]}`: {
const newStats = { ...stats };
newStats.mtime = new Date(now.getTime() - 15 * 60 * 1000);
return newStats;
}
case `${folderPath}\\${logs[3]}`: {
const newStats = { ...stats };
newStats.mtime = new Date(now.getTime() - 380 * 60 * 1000);
return newStats;
}
default: {
return stats;
}
}
});
const files = fileHelper.getLatestModifiedFiles(folderPath);
const result = new Map<string, IFile>();
const firstModifiedFile: IFile = {
fileName: logs[0],
platform: '',
modifiedTimestamp: new Date(now.getTime() - 15 * 60 * 1000).getTime(),
};
const secondModifiedFile: IFile = {
fileName: logs[2],
platform: '',
modifiedTimestamp: new Date(now.getTime() - 15 * 60 * 1000).getTime(),
};
result.set(LogCategory.LATEST + '_0', firstModifiedFile);
result.set(LogCategory.LATEST + '_2', secondModifiedFile);
if (isLinux || isMac) {
expect(files.get(LogCategory.LATEST + '_0')).toEqual(undefined);
expect(files.get(LogCategory.LATEST + '_2')).toEqual(undefined);
} else {
expect(files.get(LogCategory.LATEST + '_0')).toEqual(firstModifiedFile);
expect(files.get(LogCategory.LATEST + '_2')).toEqual(secondModifiedFile);
}
});
});
+15
View File
@@ -0,0 +1,15 @@
import { IFile, IFileOptions } from '../../interfaces/file-helper.interface';
import { Files } from './files';
export type GetLatestModifiedFiles = (
folderPath: string,
options?: IFileOptions,
) => Map<string, IFile>;
export abstract class FileHelperBase {
public abstract getLatestModifiedFiles: GetLatestModifiedFiles;
public createFiles = () => {
return new Files();
};
}
+108
View File
@@ -0,0 +1,108 @@
import * as fs from 'fs';
import path = require('path');
import { isWindowsOS } from '../../../common/env';
import { logger } from '../../../common/logger';
import { IFile, IFiles } from '../../interfaces/file-helper.interface';
import {
LogCategory,
LogUtilities,
Platform,
} from '../../interfaces/reports-handlers.interface';
import { Files } from './files';
import { FileHelperBase, GetLatestModifiedFiles } from './files-base-helper';
export class FileHelper extends FileHelperBase {
private files: IFiles;
constructor() {
super();
this.files = this.createFiles();
}
public get = () => {
return this.files;
};
public set = (files: Files) => {
return (this.files = files);
};
public validateFilename = (filename: string): string => {
return filename?.replace(/[^a-zA-Z0-9/_\.-]/g, '_');
};
public validateFilePath = (path: string) => {
const unixPathRegex = /^\/([^\/]+\/)*([^\/]+)$/;
const windowsPathRegex = /^[a-zA-Z]:\\([^\\]+\\)*([^\\]+)$/;
if (isWindowsOS) {
return windowsPathRegex.test(path);
}
return unixPathRegex.test(path);
};
public unsetFiles = () => {
this.files = this.createFiles();
};
public getLatestModifiedFiles: GetLatestModifiedFiles = (
folderPath: string,
options = {
includeOnly: [''],
type: '',
},
) => {
const logFiles = fs.readdirSync(folderPath);
const now = new Date();
let latestModifiedLogTimestamp = 0;
logFiles?.forEach((file, index) => {
const sanitizedLogname = this.validateFilename(file);
const sanitizedFolderPath = path.normalize(folderPath);
if (!sanitizedLogname || !this.validateFilePath(sanitizedFolderPath)) {
logger.error(
'error',
'file-helper: Log file has a malicious format to be exported',
);
return;
}
const logFilePath = path.join(sanitizedFolderPath, sanitizedLogname);
const logFileStats = fs.statSync(logFilePath);
const currentLogModifiedTimestamp = logFileStats.mtime.getTime();
if (
logFileStats.isFile() &&
options.includeOnly?.some((option) =>
file.toLowerCase().includes(option),
)
) {
const logLastModifiedTimestamp = new Date(logFileStats.mtime);
const timestampDiffInMinutes =
(now.getTime() - logLastModifiedTimestamp.getTime()) /
LogUtilities.ONE_MINUTE;
const modifiedFile: IFile = {
fileName: file,
platform: options.type ?? Platform.IV,
modifiedTimestamp: currentLogModifiedTimestamp,
};
if (timestampDiffInMinutes <= LogUtilities.LOG_TIMESTAMP_THRESHOLD) {
this.files.get().set(`${LogCategory.LATEST}_${index}`, modifiedFile);
latestModifiedLogTimestamp = logFileStats.mtime.getTime();
} else if (currentLogModifiedTimestamp > latestModifiedLogTimestamp) {
this.files.get().set(LogCategory.RECENT, modifiedFile);
latestModifiedLogTimestamp = currentLogModifiedTimestamp;
}
}
});
if (this.files.get().has(`${LogCategory.LATEST}_1`)) {
this.files.get().delete(LogCategory.RECENT);
}
return this.files.get();
};
}
export const fileHelper = new FileHelper();
+17
View File
@@ -0,0 +1,17 @@
import { IFile, IFiles } from '../../interfaces/file-helper.interface';
export class Files implements IFiles {
private files = new Map<string, IFile>();
constructor(private _files?: Map<string, IFile>) {
this.set(this._files ?? this.files);
}
public get = () => {
return this.files;
};
public set = (files: Map<string, IFile>) => {
this.files = files;
};
}
@@ -0,0 +1,15 @@
export interface IFile {
fileName: string;
platform: string;
modifiedTimestamp: number;
}
export interface IFileOptions {
includeOnly?: string[];
type?: string;
}
export interface IFiles {
get: () => Map<string, IFile>;
set: (files: Map<string, IFile>) => void;
}
@@ -0,0 +1,19 @@
export enum Platform {
IV = 'IV',
}
export enum LogCategory {
LATEST = 'LATEST',
RECENT = 'RECENT',
C9_TRADER = 'c9trader',
C9_ZUES = 'c9zeus',
}
export enum LogUtilities {
ONE_MINUTE = 60000,
// Timestamp threshold, in minutes
LOG_TIMESTAMP_THRESHOLD = 30,
}
export type RetrieveIVLogs = (logsPath: string) => void;
export type RemoveIVLogs = (logsPath: string) => void;
+67 -7
View File
@@ -1,12 +1,19 @@
import * as zip from 'adm-zip';
import { app, BrowserWindow, dialog, shell, WebContents } from 'electron';
import * as fs from 'fs';
import { homedir as getHomedir } from 'os';
import * as path from 'path';
import { ILogs } from '../common/api-interface';
import { isLinux, isMac, isWindowsOS } from '../common/env';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
import { fileHelper } from './helpers/file/files-helper';
import {
LogCategory,
RemoveIVLogs,
RetrieveIVLogs,
} from './interfaces/reports-handlers.interface';
/**
* Archives files in the source directory
@@ -65,14 +72,10 @@ let logWebContents: WebContents;
const logTypes: string[] = [];
const receivedLogs: ILogs[] = [];
const validateFilename = (filename: string): string => {
return filename?.replace(/[^a-zA-Z0-9/_\.-]/g, '_');
};
const writeClientLogs = async (retrievedLogs: ILogs[]) => {
const writeLogs = async (retrievedLogs: ILogs[]) => {
for await (const logs of retrievedLogs) {
for (const logFile of logs.logFiles) {
const sanitizedFilename = validateFilename(logFile.filename);
const sanitizedFilename = fileHelper.validateFilename(logFile.filename);
if (!sanitizedFilename) {
continue;
}
@@ -123,7 +126,12 @@ export const packageLogs = async (retrievedLogs: ILogs[]): Promise<void> => {
const timestamp = new Date().getTime();
const destination = app.getPath('downloads') + destPath + timestamp + '.zip';
await writeClientLogs(retrievedLogs);
if (isWindowsOS) {
removeLastIVLogs(logsPath);
extractIVLogs(logsPath);
}
await writeLogs(retrievedLogs);
generateArchiveForDirectory(
logsPath,
destination,
@@ -278,3 +286,55 @@ const writeDataToFile = async (filePath: string, data: string) => {
writeStream.end();
};
const removeLastIVLogs: RemoveIVLogs = (logsPath: string) => {
const ivLogsList = fileHelper.getLatestModifiedFiles(logsPath, {
includeOnly: [LogCategory.C9_TRADER, LogCategory.C9_ZUES],
});
ivLogsList.forEach((log) => {
const logPath = `${logsPath}/${log.fileName}`;
try {
fs.unlinkSync(logPath);
logger.info(`reports-handler: removed ${log.fileName}`);
} catch (e) {
logger.error(`reports-handler: Error in removing file - ${e}`);
}
});
};
const extractIVLogs: RetrieveIVLogs = (logsPath: string) => {
const ivFolderPath = path.join(
getHomedir(),
'AppData',
'Local',
'Cloud9_Technologies',
'C9Trader',
'log',
);
fileHelper.set(fileHelper.createFiles());
const ivLogsList = fileHelper.getLatestModifiedFiles(ivFolderPath);
if (ivLogsList.size < 1) {
logger.info(`reports-handler: Cannot copy log, logs arent exist`);
} else {
try {
ivLogsList.forEach((log) => {
logger.info(`reports-handler: Start reading logs, ${log.fileName}`);
const ivLog = fs.readFileSync(`${ivFolderPath}/${log.fileName}`);
if (ivLog) {
logger.info(`reports-handler: Log found start copying content`);
fs.writeFileSync(`${logsPath}/${log.fileName}`, ivLog);
} else {
logger.error(`reports-handler: ${log} cannot be found.`);
}
});
fileHelper.unsetFiles();
} catch (e) {
logger.error(`reports-handler: ${e}`);
}
}
};