Skip to content

Commit 0ebdbec

Browse files
committed
SP-695 Adds threads on local cryptography scanning
1 parent 51cea14 commit 0ebdbec

File tree

10 files changed

+233
-55
lines changed

10 files changed

+233
-55
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
### [0.13.0](https://github.com/scanoss/scanoss.js/compare/v0.12.2...v0.13.0) (2024-05-13)
6+
7+
### [0.12.2](https://github.com/scanoss/scanoss.js/compare/v0.12.0...v0.12.2) (2024-05-10)
8+
59
### [0.12.0](https://github.com/scanoss/scanoss.js/compare/v0.11.5...v0.12.0) (2024-05-06)
610

711
### [0.11.5](https://github.com/scanoss/scanoss.js/compare/v0.11.4...v0.11.5) (2024-04-19)

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scanoss",
3-
"version": "0.12.2",
3+
"version": "0.13.0",
44
"description": "The SCANOSS JS package provides a simple, easy to consume module for interacting with SCANOSS APIs/Engine.",
55
"main": "build/main/index.js",
66
"typings": "build/main/index.d.ts",

Diff for: src/cli/bin/cli-bin.ts

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ async function main() {
9090
// Options
9191
cryptography.addOption(new Option("-r, --rules <rules>", "Crypto rules"));
9292
cryptography.addOption(new Option("-o, --output <filename>", "Output result file name (optional - default stdout)"));
93+
cryptography.addOption(new Option("-T, --threads <threads>", "Number of threads to use while scanning (optional - default 5)"));
9394

9495
cryptography.action((source, options) => {
9596
cryptoHandler(source, options).catch((e) => {

Diff for: src/cli/commands/crypto.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,18 @@ import { DependencyFilter } from '../../sdk/tree/Filters/DependencyFilter';
1111
import { CryptoCfg } from '../../sdk/Cryptography/CryptoCfg';
1212
import fs from 'fs';
1313

14-
export async function cryptoHandler(rootPath: string, options: any){
14+
export async function cryptoHandler(rootPath: string, options: any): Promise<void> {
1515
rootPath = rootPath.replace(/\/$/, ''); // Remove trailing slash if exists
1616
rootPath = rootPath.replace(/^\./, process.env.PWD); // Convert relative path to absolute path.
1717
const pathIsFolder = await isFolder(rootPath);
1818

1919
let cryptoRules = null;
2020
if(options.rules) cryptoRules = options.rules;
2121

22+
let threads = null;
23+
if(options.threads) threads = options.threads;
2224

23-
const cryptoScanner = new CryptographyScanner(new CryptoCfg(cryptoRules));
24-
25+
const cryptoScanner = new CryptographyScanner(new CryptoCfg({threads, rulesPath: cryptoRules}));
2526

2627
let fileList: Array<string> = [];
2728
fileList.push(rootPath);
@@ -35,7 +36,7 @@ export async function cryptoHandler(rootPath: string, options: any){
3536
console.log("Searching for local cryptography...")
3637
const results = await cryptoScanner.scan(fileList);
3738

38-
if (options.output) {
39+
if(options.output) {
3940
await fs.promises.writeFile(options.output, JSON.stringify(results, null, 2));
4041
console.log(`Results found in ${options.output}`);
4142
} else {

Diff for: src/sdk/Cryptography/CryptoCfg.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,21 @@
33
*/
44
export class CryptoCfg {
55

6+
private readonly DEFAULT_THREADS = 5;
7+
68
private readonly rulesPath: string;
79

10+
private readonly threads: number;
11+
812
/**
913
* Creates an instance of CryptoCfg.
10-
* @param rulesPath Optional. Path to the cryptography rules file.
14+
* @param {Object} cfg - Configuration object.
15+
* @param {number} [cfg.threads=5] - The number of threads to use. Defaults to 5 if not provided.
16+
* @param {string} [cfg.rulesPath] - Optional. Path to the cryptography rules file.
1117
*/
12-
constructor(rulesPath?: string) {
13-
this.rulesPath = rulesPath;
18+
constructor( cfg: { threads: number, rulesPath: string }) {
19+
this.rulesPath = cfg.rulesPath;
20+
this.threads = cfg.threads ? Number(cfg.threads) : this.DEFAULT_THREADS;
1421
}
1522

1623
/**
@@ -21,4 +28,8 @@ export class CryptoCfg {
2128
return this.rulesPath;
2229
}
2330

31+
public getNumberOfThreads(){
32+
return this.threads;
33+
}
34+
2435
}

Diff for: src/sdk/Cryptography/CryptoProvider/LocalCrypto.ts

+13-45
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,29 @@ import {
44
} from '../CryptoDef/CryptoDef';
55
import fs from 'fs';
66
import { CryptoAlgorithm, CryptoAlgorithmRules } from '../CryptographyTypes';
7+
import { ThreadPool } from '../Worker/ThreadPool';
8+
79

810
/**
911
* Represents a CryptoCalculator used for searching cryptographic algorithms in files.
1012
*/
1113
export class LocalCrypto {
1214

13-
private cryptoMapper : Map<string, CryptoAlgorithm>;
15+
private cryptoMapper: Map<string, CryptoAlgorithm>;
1416

1517
private cryptoRules: Map<string, RegExp>;
1618

17-
private readonly MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
19+
private threads: number;
1820

1921
/**
2022
* Constructs a new LocalCrypto.
2123
* @param cryptoRules An array of CryptoAlgorithmRules used to create the search rules.
24+
* @param threads Number of threads to be use to scan local cryptography (default = 5).
2225
*/
23-
constructor(cryptoRules: Array<CryptoAlgorithmRules>) {
26+
constructor(cryptoRules: Array<CryptoAlgorithmRules>, threads: number) {
2427
this.cryptoRules = createCryptoKeywordMapper(cryptoRules);
25-
this.cryptoMapper = getCryptoMapper(cryptoRules)
28+
this.cryptoMapper = getCryptoMapper(cryptoRules);
29+
this.threads = threads;
2630
}
2731

2832
/**
@@ -31,47 +35,11 @@ export class LocalCrypto {
3135
*/
3236
public async search(files: Array<string>): Promise<Array<CryptoItem>> {
3337
if (files.length <= 0) return [];
34-
const cryptoItems = files.map((f)=> { return new CryptoItem(f) });
35-
36-
for(let c of cryptoItems) {
37-
await this.searchCrypto(c);
38-
}
39-
40-
return cryptoItems;
41-
}
42-
43-
/**
44-
* Asynchronously searches for cryptographic algorithms in the content of a file.
45-
* @param cryptoItem The CryptoItem to search for cryptographic algorithms.
46-
* @returns A promise that resolves when the search is complete.
47-
*/
48-
private async searchCrypto(cryptoItem: CryptoItem){
49-
const cryptoFound = new Array<string>();
50-
const stats = await fs.promises.stat(cryptoItem.getPath());
51-
if (stats.size > this.MAX_FILE_SIZE) {
52-
cryptoItem.setAlgorithms([]);
53-
return;
54-
}
55-
let content = await fs.promises.readFile(cryptoItem.getPath(), 'utf-8');
56-
this.cryptoRules.forEach((value, key) => {
57-
try {
58-
const matches = content.match(value);
59-
if (matches) {
60-
cryptoFound.push(key);
61-
}
62-
} catch (e){
63-
console.error(e);
64-
}
65-
});
66-
// Release memory
67-
content = null;
68-
const results: Array<CryptoAlgorithm> = [];
69-
cryptoFound.forEach((cf)=>{
70-
results.push(this.cryptoMapper.get(cf));
38+
const threadPool = new ThreadPool(this.threads, this.cryptoRules, this.cryptoMapper);
39+
files.forEach((f) => {
40+
threadPool.enqueueTask(new CryptoItem(f))
7141
});
72-
cryptoItem.setAlgorithms(results);
42+
await threadPool.init();
43+
return await threadPool.processQueue();
7344
}
74-
7545
}
76-
77-

Diff for: src/sdk/Cryptography/CryptographyScanner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class CryptographyScanner {
3232
*/
3333
public async scan(files: Array<string>): Promise<ILocalCryptographyResponse> {
3434
const cryptographyRules = await this.loadRules(this.cryptoConfig.getRulesPath());
35-
const localCrypto = new LocalCrypto(cryptographyRules);
35+
const localCrypto = new LocalCrypto(cryptographyRules, this.cryptoConfig.getNumberOfThreads());
3636
const cryptoItems = await localCrypto.search(files);
3737
return mapToILocalCryptographyResponse(cryptoItems);
3838
}

Diff for: src/sdk/Cryptography/Worker/ThreadPool.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { CryptoAlgorithm } from '../CryptographyTypes';
2+
import { CryptoItem } from '../Scanneable/CryptoItem';
3+
import { IWorkerResponse, Worker } from './Worker';
4+
5+
export class ThreadPool {
6+
7+
private readonly maxWorkers: number;
8+
9+
private readonly workers: Array<Worker> = [];
10+
11+
private readonly jobsQueue: any;
12+
13+
private readonly cryptoRules: Map<string, RegExp>;
14+
15+
private readonly cryptoMapper: Map<string, CryptoAlgorithm>;
16+
17+
private results = [];
18+
19+
private activeWorkers = 0;
20+
21+
private resolve;
22+
23+
private reject;
24+
constructor(maxWorkers = 3, rules:Map<string, RegExp>, cryptoMapper: Map<string, CryptoAlgorithm> ) {
25+
this.maxWorkers = maxWorkers;
26+
this.workers = [];
27+
this.jobsQueue = [];
28+
this.cryptoRules = rules;
29+
this.cryptoMapper = cryptoMapper;
30+
}
31+
32+
enqueueTask(item: CryptoItem) {
33+
return new Promise((resolve, reject) => {
34+
const job = { item, resolve, reject };
35+
this.jobsQueue.push(job);
36+
});
37+
}
38+
39+
async init(): Promise<void> {
40+
for (let i = 0; i < this.maxWorkers; i++) {
41+
const worker = new Worker();
42+
worker.on('message', async(item: IWorkerResponse) => {
43+
this.results.push(item.result);
44+
this.releaseWorker(item.id);
45+
await this.next();
46+
});
47+
48+
// TODO: See what can be done in case an error on the thread
49+
worker.on('error', async (error) => {
50+
console.log(error);
51+
this.releaseWorker(worker.getId());
52+
await this.next();
53+
});
54+
55+
this.workers.push(worker);
56+
}
57+
}
58+
59+
private releaseWorker(id: number): number {
60+
const wId = this.workers.findIndex(w => w.getId() === id);
61+
const w = this.workers[wId];
62+
w.release();
63+
return w.getId();
64+
}
65+
66+
private async next(){
67+
this.activeWorkers -= 1;
68+
if (this.activeWorkers === 0 && this.jobsQueue.length === 0) {
69+
await this.destroyAllWorkers();
70+
this.resolve(this.results);
71+
} else {
72+
this.processItem();
73+
}
74+
}
75+
76+
private async destroyAllWorkers() {
77+
for (const worker of this.workers) {
78+
await worker.terminate(); // Terminate each worker
79+
}
80+
}
81+
82+
async processQueue(): Promise<Array<CryptoItem>> {
83+
return new Promise(async(resolve, reject) => {
84+
this.resolve = resolve;
85+
this.reject = reject;
86+
this.processItem();
87+
});
88+
}
89+
90+
private processItem() {
91+
if (this.workers.length > 0 && this.jobsQueue.length > 0) {
92+
const freeWorkerIndices = this.workers.reduce((indices, worker, index) => {
93+
if (worker.isFree()) {
94+
indices.push(index);
95+
}
96+
return indices;
97+
}, []);
98+
99+
freeWorkerIndices.forEach(workerIndex => {
100+
if (this.jobsQueue.length <= 0) return;
101+
const { item, reject } = this.jobsQueue.shift();
102+
const worker = this.workers[workerIndex];
103+
worker.run({ item, rules: this.cryptoRules, cryptoMapper: this.cryptoMapper });
104+
this.activeWorkers += 1;
105+
});
106+
}
107+
}
108+
}
109+
110+

Diff for: src/sdk/Cryptography/Worker/Worker.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { TransferListItem, Worker as W, WorkerOptions } from 'worker_threads';
2+
import { URL } from 'node:url';
3+
import { CryptoItem } from '../Scanneable/CryptoItem';
4+
5+
const stringWorker = `
6+
const { parentPort } = require('worker_threads');
7+
const fs = require('fs');
8+
9+
parentPort.on('message', async (data) => {
10+
11+
const MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024;
12+
13+
const { item, rules , cryptoMapper, id } = data;
14+
15+
const cryptoFound = new Array();
16+
const stats = await fs.promises.stat(item.file);
17+
if (stats.size > MAX_FILE_SIZE) {
18+
item.algorithms = [];
19+
parentPort.postMessage({ result: item, id });
20+
return;
21+
}
22+
23+
let content = fs.readFileSync(item.file, 'utf-8');
24+
rules.forEach((value, key) => {
25+
try {
26+
const matches = content.match(value);
27+
if (matches) {
28+
cryptoFound.push(key);
29+
}
30+
} catch (e){
31+
console.error(e);
32+
}
33+
});
34+
const results = [];
35+
cryptoFound.forEach((cf)=>{
36+
results.push(cryptoMapper.get(cf));
37+
});
38+
item.algorithms = results;
39+
parentPort.postMessage({ result: item, id });
40+
});
41+
`;
42+
43+
export interface IWorkerResponse {
44+
result: CryptoItem;
45+
id: number;
46+
}
47+
48+
export class Worker extends W {
49+
50+
private free: boolean ;
51+
52+
53+
constructor() {
54+
super(stringWorker, { eval: true });
55+
this.free = true;
56+
}
57+
58+
public getId(): number {
59+
return this.threadId;
60+
}
61+
62+
public release(){
63+
this.free = true;
64+
}
65+
66+
public isFree(): boolean {
67+
return this.free;
68+
}
69+
70+
on(event, listener) {
71+
if (event === 'error') {
72+
this.free = true;
73+
}
74+
// Call super.on with the provided arguments
75+
return super.on(event, listener);
76+
77+
}
78+
public run (value: any, transferList?: ReadonlyArray<TransferListItem>){
79+
this.free = false;
80+
this.postMessage({...value, id: this.threadId });
81+
}
82+
83+
}

Diff for: src/sdk/Cryptography/utils/adapters/cryptoAdapters.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ICryptoItem, ILocalCryptographyResponse } from '../../CryptographyTypes
77
* @returns An ILocalCryptographyResponse object containing mapped cryptographic items.
88
*/
99
export function mapToILocalCryptographyResponse(ci: Array<CryptoItem>): ILocalCryptographyResponse {
10-
const fileList: Array<ICryptoItem> = ci.map((c)=> ({ file: c.getPath(), algorithms: c.getAlgorithms() }));
10+
const fileList: Array<ICryptoItem> = ci.map((c)=> ({ file: c.file, algorithms: c.algorithms }));
1111
return {
1212
fileList
1313
}

0 commit comments

Comments
 (0)