diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..42a1b82 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "sonarlint.connectedMode.project": { + "connectionId": "sweetim-github", + "projectKey": "sweetim_linux-top-parser" + } +} diff --git a/README.md b/README.md index af9f760..d756c2b 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ createReadStream(TOP_FILE_PATH) ### Examples There are 2 code examples shown in the [example](https://github.com/sweetim/linux-top-parser/tree/master/example) folder on how to use this package -- [read from file](https://github.com/sweetim/linux-top-parser/blob/master/example/read-from-file.ts) -- [stream from the linux `top` command output](https://github.com/sweetim/linux-top-parser/blob/master/example/stream-from-top-command.ts) +- [read from file](https://github.com/sweetim/linux-top-parser/blob/master/example/read-from-file.ts) +- [stream from the linux `top` command output](https://github.com/sweetim/linux-top-parser/blob/master/example/stream-from-top-command.ts) ## CLI @@ -79,15 +79,16 @@ top -b | npx linux-top-parser | jq ".[0].summaryDisplay" ### Usage ``` -linux-top-parser [options] +Usage: linux-top-parser [options] Options: - -V, --version output the version number - -s, --summary output summary display only (default: false) - -p, --prettify output top info with indentation and color (default: false) - -f, --filter output process that has > 0% of CPU usage only (default: false) - -h, --help display help for command + -V, --version output the version number + -t, --timeOut_ms the maximum amount of time (in milliseconds) to wait for the next header before emitting data (default: "100") + -s, --summary output summary display only (default: false) + -p, --prettify output top info with indentation and color (default: false) + -f, --filter output process that has > 0% of CPU usage only (default: false) + -h, --help display help for command ``` ### Reference -- https://man7.org/linux/man-pages/man1/top.1.html +- https://man7.org/linux/man-pages/man1/top.1.html diff --git a/example/read-from-file.ts b/example/read-from-file.ts index 06165a9..61a805e 100644 --- a/example/read-from-file.ts +++ b/example/read-from-file.ts @@ -1,7 +1,6 @@ import { resolve } from "path" import { readFileSync, createReadStream } from "fs" -import { parseTopInfo } from "../src" -import { topInfoTransform } from "../src/streams" +import { parseTopInfo, topInfoTransform } from "../src" const TOP_FILE_PATH = resolve(__dirname, "..", "test", "data", "multi.txt") diff --git a/src/bin.ts b/src/bin.ts index 6415fe7..73f55c5 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -32,6 +32,7 @@ program .version(packageJson.version || "") .description("parser for linux top command") .addHelpText("after", HELPER_TEXT) + .option("-t, --timeOut_ms ", "the maximum amount of time (in milliseconds) to wait for the next header before emitting data", "100") .option("-s, --summary", "output summary display only", false) .option("-p, --prettify", "output top info with indentation and color", false) .option("-f, --filter", "output process that has > 0% of CPU usage only", false) @@ -41,9 +42,10 @@ type CLIOptions = { prettify: boolean summary: boolean filter: boolean + timeOut_ms: number } -const { prettify, summary, filter } = program.opts() +const { prettify, summary, filter, timeOut_ms } = program.opts() process.stdin .pipe(topInfoTransform({ @@ -51,6 +53,7 @@ process.stdin prettify }, summary, - filter + filter, + timeOut_ms: Number(timeOut_ms) })) - .pipe(process.stdout) + .on("data", console.log) diff --git a/src/parser/using-regex.ts b/src/parser/using-regex.ts index 2867a6e..1bab9c0 100644 --- a/src/parser/using-regex.ts +++ b/src/parser/using-regex.ts @@ -18,7 +18,7 @@ import { import { fromDays, fromHours, fromMinutes } from "./util" export function parseUpTime_s(input: string): number { - const matcher = /(?:(\d+)\s\bday[s]?,\s)?(?:(\d+:\d+)|(\d+)\smin)/gm + const matcher = /(?:(\d+)\s\bdays?,\s)?(?:(\d+:\d+)|(\d+)\smin)/gm const tokens = Array.from(input.matchAll(matcher)).flat() if (tokens.length === 0) { @@ -40,7 +40,7 @@ export function parseUpTime_s(input: string): number { } export function parseUpTimeAndLoadAverage(line: string): UpTimeAndLoadAverage { - const matcher = /top - ([\d:]+) up (.+(?=,\s+\d \buser)),\s+(\d) \buser[s]?,\s+load average:(\s[\d.]+),(\s[\d.]+),(\s[\d.]+,?)/gm + const matcher = /top - ([\d:]+) up (.+(?=,\s+\d \buser)),\s+(\d) \busers?,\s+load average:(\s[\d.]+),(\s[\d.]+),(\s[\d.]+,?)/gm const tokens = Array.from(line.matchAll(matcher)).flat() if (tokens.length === 0) { @@ -75,7 +75,7 @@ export function parseTaskStates(str: string): TaskStates { } export function parseCpuStates(line: string): CpuStates[] { - const matcher = /^%Cpu([\d]+|\b\(s\))\s*:\s*(\d+.\d+)\sus,\s*(\d+.\d+)\ssy,\s*(\d+.\d+)\sni,\s*(\d+.\d+)\sid,\s*(\d+.\d+)\swa,\s*(\d+.\d+)\shi,\s*(\d+.\d+)\ssi,\s*(\d+.\d+)\sst/gm + const matcher = /^%Cpu(\d+|\b\(s\))\s*:\s*(\d+.\d+)\sus,\s*(\d+.\d+)\ssy,\s*(\d+.\d+)\sni,\s*(\d+.\d+)\sid,\s*(\d+.\d+)\swa,\s*(\d+.\d+)\shi,\s*(\d+.\d+)\ssi,\s*(\d+.\d+)\sst/gm const tokens = Array.from(line.matchAll(matcher)) if (tokens.length === 0) { diff --git a/src/streams/top-info-transform.test.ts b/src/streams/top-info-transform.test.ts index a1b5c16..f4e6a15 100644 --- a/src/streams/top-info-transform.test.ts +++ b/src/streams/top-info-transform.test.ts @@ -19,7 +19,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: false, isPrettify: false, isSummary: false, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions()).toStrictEqual(expected) @@ -35,7 +36,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: false, isPrettify: false, isSummary: false, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) @@ -53,7 +55,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: true, isPrettify: false, isSummary: false, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) @@ -70,7 +73,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: true, isPrettify: true, isSummary: false, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) @@ -90,7 +94,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: true, isPrettify: false, isSummary: false, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) @@ -108,7 +113,8 @@ describe("parseTopInfoTransformOptions", () => { isStringify: false, isPrettify: false, isSummary: summary, - isFilter: false + isFilter: false, + timeOut_ms: 100 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) @@ -126,7 +132,24 @@ describe("parseTopInfoTransformOptions", () => { isStringify: false, isPrettify: false, isSummary: false, - isFilter: filter + isFilter: filter, + timeOut_ms: 100 + } + + expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) + }) + + it("should return timeOut_ms value", () => { + const input: TopInfoTransformOptions = { + timeOut_ms: 200 + } + + const expected: TopInfoTransformConfig = { + isStringify: false, + isPrettify: false, + isSummary: false, + isFilter: false, + timeOut_ms: 200 } expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected) diff --git a/src/streams/top-info-transform.ts b/src/streams/top-info-transform.ts index fe89069..0be5b18 100644 --- a/src/streams/top-info-transform.ts +++ b/src/streams/top-info-transform.ts @@ -9,6 +9,7 @@ export interface TopInfoTransformOptions { stringify?: boolean | TopInfoTransformToStringOpions summary?: boolean filter?: boolean + timeOut_ms?: number } interface TopInfoTransformToStringOpions { @@ -20,6 +21,7 @@ export interface TopInfoTransformConfig { isPrettify: boolean isSummary: boolean isFilter: boolean + timeOut_ms: number } export function parseTopInfoTransformOptions(options?: TopInfoTransformOptions): TopInfoTransformConfig { @@ -28,6 +30,7 @@ export function parseTopInfoTransformOptions(options?: TopInfoTransformOptions): isPrettify: false, isSummary: false, isFilter: false, + timeOut_ms: 100 } if (options === undefined) { @@ -59,6 +62,10 @@ export function parseTopInfoTransformOptions(options?: TopInfoTransformOptions): output.isFilter = options.filter } + if (options.timeOut_ms) { + output.timeOut_ms = options.timeOut_ms + } + return output } @@ -109,7 +116,8 @@ export function topInfoTransform(options?: TopInfoTransformOptions): Transform { return bufferTillNextHeader( /(?=^top)/gm, - topInfoMapping(config) + topInfoMapping(config), + config.timeOut_ms ) } @@ -117,14 +125,17 @@ export function topInfoTransform(options?: TopInfoTransformOptions): Transform { * Buffers data chunks until a header is found and then applies a mapping function to the buffer. * @param {RegExp} header - The regular expression that matches the header of a new chunk. * @param {(buffer: string) => any} [mappingFn] - The function that transforms the buffer into a desired format. By default, it returns the buffer as it is. + * @param {number} timeOut_ms - The maximum amount of time (in milliseconds) to wait for the next header before emitting data. Optional. * @returns {Transform} A transform stream that buffers and maps data chunks based on the header. */ export function bufferTillNextHeader( header: RegExp, /* eslint-disable @typescript-eslint/no-explicit-any */ - mappingFn: (buffer: string) => any = (buffer: string) => buffer): Transform + mappingFn: (buffer: string) => any = (buffer: string) => buffer, + timeOut_ms?: number): Transform { let buffer = "" + let timeOut_id: NodeJS.Timeout | null = null return new Transform({ objectMode: true, @@ -139,8 +150,10 @@ export function bufferTillNextHeader( const isHeader = header.test(msg) if (buffer && isHeader) { - this.push(mappingFn(buffer)) - buffer = "" + if (buffer.trim().length > 0) { + this.push(mappingFn(buffer)) + buffer = "" + } } if (isHeader) { @@ -152,6 +165,20 @@ export function bufferTillNextHeader( buffer += msg }) + if (timeOut_id) { + clearTimeout(timeOut_id) + timeOut_id = null + } + + if (timeOut_ms) { + timeOut_id = setTimeout(() => { + if (buffer.trim().length > 0) { + this.push(mappingFn(buffer)) + buffer = "" + } + }, timeOut_ms) + } + cb() }, })