Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sonarlint.connectedMode.project": {
"connectionId": "sweetim-github",
"projectKey": "sweetim_linux-top-parser"
}
}
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <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
3 changes: 1 addition & 2 deletions example/read-from-file.ts
Original file line number Diff line number Diff line change
@@ -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")

Expand Down
9 changes: 6 additions & 3 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ program
.version(packageJson.version || "")
.description("parser for linux top command")
.addHelpText("after", HELPER_TEXT)
.option("-t, --timeOut_ms <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)
Expand All @@ -41,16 +42,18 @@ type CLIOptions = {
prettify: boolean
summary: boolean
filter: boolean
timeOut_ms: number
}

const { prettify, summary, filter } = program.opts<CLIOptions>()
const { prettify, summary, filter, timeOut_ms } = program.opts<CLIOptions>()

process.stdin
.pipe(topInfoTransform({
stringify: {
prettify
},
summary,
filter
filter,
timeOut_ms: Number(timeOut_ms)
}))
.pipe(process.stdout)
.on("data", console.log)
6 changes: 3 additions & 3 deletions src/parser/using-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 30 additions & 7 deletions src/streams/top-info-transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: false,
isPrettify: false,
isSummary: false,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions()).toStrictEqual(expected)
Expand All @@ -35,7 +36,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: false,
isPrettify: false,
isSummary: false,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected)
Expand All @@ -53,7 +55,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: true,
isPrettify: false,
isSummary: false,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected)
Expand All @@ -70,7 +73,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: true,
isPrettify: true,
isSummary: false,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected)
Expand All @@ -90,7 +94,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: true,
isPrettify: false,
isSummary: false,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected)
Expand All @@ -108,7 +113,8 @@ describe("parseTopInfoTransformOptions", () => {
isStringify: false,
isPrettify: false,
isSummary: summary,
isFilter: false
isFilter: false,
timeOut_ms: 100
}

expect(parseTopInfoTransformOptions(input)).toStrictEqual(expected)
Expand All @@ -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)
Expand Down
35 changes: 31 additions & 4 deletions src/streams/top-info-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface TopInfoTransformOptions {
stringify?: boolean | TopInfoTransformToStringOpions
summary?: boolean
filter?: boolean
timeOut_ms?: number
}

interface TopInfoTransformToStringOpions {
Expand All @@ -20,6 +21,7 @@ export interface TopInfoTransformConfig {
isPrettify: boolean
isSummary: boolean
isFilter: boolean
timeOut_ms: number
}

export function parseTopInfoTransformOptions(options?: TopInfoTransformOptions): TopInfoTransformConfig {
Expand All @@ -28,6 +30,7 @@ export function parseTopInfoTransformOptions(options?: TopInfoTransformOptions):
isPrettify: false,
isSummary: false,
isFilter: false,
timeOut_ms: 100
}

if (options === undefined) {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -109,22 +116,26 @@ export function topInfoTransform(options?: TopInfoTransformOptions): Transform {

return bufferTillNextHeader(
/(?=^top)/gm,
topInfoMapping(config)
topInfoMapping(config),
config.timeOut_ms
)
}

/**
* 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,
Expand All @@ -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) {
Expand All @@ -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()
},
})
Expand Down