diff --git a/backend/remill/lib/Arch/AArch64/Arch.cpp b/backend/remill/lib/Arch/AArch64/Arch.cpp index 2e9556fc..19420c7f 100644 --- a/backend/remill/lib/Arch/AArch64/Arch.cpp +++ b/backend/remill/lib/Arch/AArch64/Arch.cpp @@ -5895,6 +5895,27 @@ bool TryDecodeFCVTZU_ASISDMISC_R(const InstData &data, Instruction &inst) { return true; } +// FCVTZS , (scalar, converts FP to signed integer) +bool TryDecodeFCVTZS_ASISDMISC_R(const InstData &data, Instruction &inst) { + inst.sema_func_arg_type = SemaFuncArgType::State; + RegClass dst_class, src_class; + if (1 == data.sz) { + inst.function += "_64"; + dst_class = kReg2D; // Output: integer register + src_class = kReg2DF; // Input: floating-point register + } else { + inst.function += "_32"; + dst_class = kReg4S; // Output: integer register + src_class = kReg4SF; // Input: floating-point register + } + if (inst.lift_config.float_exception_enabled) { + AddArrangementSpecifierFPSRStatus(inst); + } + AddRegOperand(inst, kActionWrite, dst_class, kUseAsValue, data.Rd); + AddRegOperand(inst, kActionRead, src_class, kUseAsValue, data.Rn); + return true; +} + // SCVTF ., . (only 32bit or 64bit) bool TryDecodeSCVTF_ASIMDMISC_R(const InstData &data, Instruction &inst) { inst.sema_func_arg_type = SemaFuncArgType::State; diff --git a/backend/remill/lib/Arch/AArch64/Decode.cpp b/backend/remill/lib/Arch/AArch64/Decode.cpp index bdaef9d3..cbf1bbfb 100644 --- a/backend/remill/lib/Arch/AArch64/Decode.cpp +++ b/backend/remill/lib/Arch/AArch64/Decode.cpp @@ -25475,9 +25475,7 @@ bool TryDecodeFCVTZS_ASISDMISCFP16_R(const InstData &, Instruction &) { // 30 1 // 31 0 // FCVTZS , -bool TryDecodeFCVTZS_ASISDMISC_R(const InstData &, Instruction &) { - return false; -} +// Implemented in Arch.cpp // FCVTZS FCVTZS_asimdmiscfp16_R: // 0 x Rd 0 diff --git a/backend/remill/lib/Arch/AArch64/Semantics/CONVERT.cpp b/backend/remill/lib/Arch/AArch64/Semantics/CONVERT.cpp index 0232c17c..925c016f 100644 --- a/backend/remill/lib/Arch/AArch64/Semantics/CONVERT.cpp +++ b/backend/remill/lib/Arch/AArch64/Semantics/CONVERT.cpp @@ -152,6 +152,29 @@ DEF_SEM_T_STATE(FCVTZU_Float64ToUInt64_FROMV_FPSRStatus, VIf64v2 src) { return res; } +// FCVTZS , (scalar, converts FP to signed integer in vector register) +DEF_SEM_T_STATE(FCVTZS_Float32ToSInt32_FROMV, VIf32v4 src) { + _ecv_u32v4_t res = {}; + res[0] = CheckedCast(state, FExtractVI32(FReadVI32(src), 0)); + return res; +} +DEF_SEM_T_STATE(FCVTZS_Float64ToSInt64_FROMV, VIf64v2 src) { + _ecv_u64v2_t res = {}; + res[0] = CheckedCast(state, FExtractVI64(FReadVI64(src), 0)); + return res; +} +// FPSR +DEF_SEM_T_STATE(FCVTZS_Float32ToSInt32_FROMV_FPSRStatus, VIf32v4 src) { + _ecv_u32v4_t res = {}; + res[0] = CheckedCastFPSRStatus(state, FExtractVI32(FReadVI32(src), 0)); + return res; +} +DEF_SEM_T_STATE(FCVTZS_Float64ToSInt64_FROMV_FPSRStatus, VIf64v2 src) { + _ecv_u64v2_t res = {}; + res[0] = CheckedCastFPSRStatus(state, FExtractVI64(FReadVI64(src), 0)); + return res; +} + // FCVTZS , DEF_SEM_U32_STATE(FCVTZS_Float32ToSInt32, RF32 src) { return CheckedCast(state, Read(src)); @@ -305,6 +328,13 @@ DEF_ISEL(FCVTZS_32D_FLOAT2INT_FPSRSTATUS) = DEF_ISEL(FCVTZS_64D_FLOAT2INT_FPSRSTATUS) = FCVTZS_Float64ToSInt64_FPSRStatus; // FCVTZS , +// FCVTZS (scalar, FP to signed integer in vector register) - ASISDMISC +DEF_ISEL(FCVTZS_ASISDMISC_R_32) = FCVTZS_Float32ToSInt32_FROMV; // FCVTZS , +DEF_ISEL(FCVTZS_ASISDMISC_R_64) = FCVTZS_Float64ToSInt64_FROMV; // FCVTZS , +// FPSR +DEF_ISEL(FCVTZS_ASISDMISC_R_32_FPSRSTATUS) = FCVTZS_Float32ToSInt32_FROMV_FPSRStatus; // FCVTZS , +DEF_ISEL(FCVTZS_ASISDMISC_R_64_FPSRSTATUS) = FCVTZS_Float64ToSInt64_FROMV_FPSRStatus; // FCVTZS , + DEF_ISEL(FCVTAS_64D_FLOAT2INT) = FCVTAS_Float64ToSInt64; // FCVTAS , // FPSR DEF_ISEL(FCVTAS_64D_FLOAT2INT_FPSRSTATUS) = diff --git a/backend/remill/tests/AArch64/SIMD/FCVTZS_ASISDMISC_R.S b/backend/remill/tests/AArch64/SIMD/FCVTZS_ASISDMISC_R.S new file mode 100644 index 00000000..b4c45d7f --- /dev/null +++ b/backend/remill/tests/AArch64/SIMD/FCVTZS_ASISDMISC_R.S @@ -0,0 +1,41 @@ +TEST_BEGIN(FCVTZS_ASISDMISC_R_32, fcvtzs_s0_s1_sisd, 1) +TEST_INPUTS(0) + fmov s1, #2.5 + fcvtzs s0, s1 +TEST_END + +TEST_BEGIN(FCVTZS_ASISDMISC_R_32_v2, fcvtzs_s0_s1_sisd_v2, 1) +TEST_INPUTS(0) + fmov s1, #-2.5 + fcvtzs s0, s1 +TEST_END + +TEST_BEGIN(FCVTZS_ASISDMISC_R_64, fcvtzs_d0_d1_sisd, 1) +TEST_INPUTS(0) + fmov d1, #2.5 + fcvtzs d0, d1 +TEST_END + +TEST_BEGIN(FCVTZS_ASISDMISC_R_64_v2, fcvtzs_d0_d1_sisd_v2, 1) +TEST_INPUTS(0) + fmov d1, #-4.0 + fcvtzs d0, d1 +TEST_END + +TEST_BEGIN(FCVTZS_ASISDMISC_R_32_INPUTS, fcvtzs_s_inputs, 1) +TEST_INPUTS( + 0x3F8000003F800000, + 0x40000000C0000000, + 0x41200000BF800000) + fmov d1, ARG1_64 + fcvtzs s0, s1 +TEST_END + +TEST_BEGIN(FCVTZS_ASISDMISC_R_64_INPUTS, fcvtzs_d_inputs, 1) +TEST_INPUTS( + 0x3FF0000000000000, + 0xC000000000000000, + 0x4024000000000000) + fmov d1, ARG1_64 + fcvtzs d0, d1 +TEST_END diff --git a/backend/remill/tests/AArch64/Tests.S b/backend/remill/tests/AArch64/Tests.S index 3f7a33b0..20756b9a 100644 --- a/backend/remill/tests/AArch64/Tests.S +++ b/backend/remill/tests/AArch64/Tests.S @@ -485,6 +485,7 @@ SYMBOL(__aarch64_test_table_begin): // #include "tests/AArch64/SIMD/SCVTF_ASIMDMISC_R.S" // #include "tests/AArch64/SIMD/UCVTF_ASIMDMISC_R.S" // #include "tests/AArch64/SIMD/FCVTZU_ASISDMISC_R.S" +#include "tests/AArch64/SIMD/FCVTZS_ASISDMISC_R.S" #include "tests/AArch64/SYSTEM/Mn_n_SYSTEM_FPSR.S" #include "tests/AArch64/SYSTEM/Mn_n_SYSTEM_FPCR.S" diff --git a/browser/js-kernel.js b/browser/js-kernel.js index 80c34336..60e1fc70 100644 --- a/browser/js-kernel.js +++ b/browser/js-kernel.js @@ -367,12 +367,22 @@ var Module = (() => { // call the target kernel function. let sysRval = tgtKernelFunction(...sysArgs); - // store the return value of syscall function executing. - m32View[sysRvalPtr] = sysRval; - // notify to process worker - Atomics.store(m32View, waitPtr, 1); - Atomics.notify(m32View, waitPtr, 1); + // support async syscall handlers (e.g. poll with timeout) + if (sysRval instanceof Promise) { + sysRval.then(val => { + m32View[sysRvalPtr] = val; + Atomics.store(m32View, waitPtr, 1); + Atomics.notify(m32View, waitPtr, 1); + }); + } else { + // store the return value of syscall function executing. + m32View[sysRvalPtr] = sysRval; + + // notify to process worker + Atomics.store(m32View, waitPtr, 1); + Atomics.notify(m32View, waitPtr, 1); + } } function exitHandling(workerId) { @@ -3000,8 +3010,10 @@ var Module = (() => { ["seq", "busybox"], ["sleep", "busybox"], ["tail", "busybox"], + ["top", "busybox"], ["tree", "busybox"], ["uname", "busybox"], + ["uptime", "busybox"], ["vi", "busybox"], ["cat", "busybox"], ["touch", "busybox"], @@ -3340,9 +3352,16 @@ var Module = (() => { }; var PROCFS = { ops_table: null, + _bootTime: null, init(initPid) { PROCFS.mount(); + PROCFS._bootTime = Date.now(); this.createMyProc(initPid); + // system-wide /proc files + FS.mknod("/proc/stat", S_IFREG | S_IRUSR | S_IRGRP | S_IROTH, 740); + FS.mknod("/proc/meminfo", S_IFREG | S_IRUSR | S_IRGRP | S_IROTH, 741); + FS.mknod("/proc/uptime", S_IFREG | S_IRUSR | S_IRGRP | S_IROTH, 742); + FS.mknod("/proc/loadavg", S_IFREG | S_IRUSR | S_IRGRP | S_IROTH, 743); // /proc/self let selfNode = FS.symlink(`/proc/${initPid}`, `/proc/self`); // /proc/self/exe for python (FIXME) @@ -3531,6 +3550,52 @@ var Module = (() => { readProcCmdline(task) { return new TextEncoder().encode(task.comm).buffer; }, + readProcSystemStat() { + // Minimal /proc/stat for busybox top + let now = Date.now(); + let uptimeMs = now - PROCFS._bootTime; + let jiffies = Math.floor(uptimeMs / 10); // USER_HZ=100 + let user = Math.floor(jiffies * 0.05); + let system = Math.floor(jiffies * 0.02); + let idle = jiffies - user - system; + let lines = [ + `cpu ${user} 0 ${system} ${idle} 0 0 0 0 0 0`, + `cpu0 ${user} 0 ${system} ${idle} 0 0 0 0 0 0`, + `intr 0`, + `ctxt 0`, + `btime ${Math.floor(PROCFS._bootTime / 1000)}`, + `processes 1`, + `procs_running 1`, + `procs_blocked 0`, + ]; + return new TextEncoder().encode(lines.join("\n") + "\n").buffer; + }, + readProcMeminfo() { + let totalKB = 512 * 1024; + let freeKB = 256 * 1024; + let availKB = 384 * 1024; + let buffersKB = 16 * 1024; + let cachedKB = 64 * 1024; + let lines = [ + `MemTotal: ${totalKB} kB`, + `MemFree: ${freeKB} kB`, + `MemAvailable: ${availKB} kB`, + `Buffers: ${buffersKB} kB`, + `Cached: ${cachedKB} kB`, + `SwapCached: 0 kB`, + `SwapTotal: 0 kB`, + `SwapFree: 0 kB`, + ]; + return new TextEncoder().encode(lines.join("\n") + "\n").buffer; + }, + readProcUptime() { + let uptimeSec = ((Date.now() - PROCFS._bootTime) / 1000).toFixed(2); + let idleSec = (uptimeSec * 0.95).toFixed(2); + return new TextEncoder().encode(`${uptimeSec} ${idleSec}\n`).buffer; + }, + readProcLoadavg() { + return new TextEncoder().encode("0.00 0.00 0.00 1/1 1\n").buffer; + }, readProcMaps() { // Minimal /proc/self/maps template for your 256MiB arena. // @@ -3623,17 +3688,28 @@ var Module = (() => { let parent = FS.lookupPath(stream.path, { parent: true }).node; - // get the target content. - let task = processes.get(+(parent.name)).task; let content; - if (stream.node.name === `stat`) { - content = PROCFS.readProcStat(task); - } else if (stream.node.name === `cmdline`) { - content = PROCFS.readProcCmdline(task); - } else if (stream.node.name == `maps`) { - content = PROCFS.readProcMaps(); + // system-wide /proc files (parent is "proc") + if (parent.name === "proc") { + switch (stream.node.name) { + case "stat": content = PROCFS.readProcSystemStat(); break; + case "meminfo": content = PROCFS.readProcMeminfo(); break; + case "uptime": content = PROCFS.readProcUptime(); break; + case "loadavg": content = PROCFS.readProcLoadavg(); break; + default: throw new FS.ErrnoError(2); + } } else { - throw new FS.ErrnoError(2); + // per-process /proc// files + let task = processes.get(+(parent.name)).task; + if (stream.node.name === `stat`) { + content = PROCFS.readProcStat(task); + } else if (stream.node.name === `cmdline`) { + content = PROCFS.readProcCmdline(task); + } else if (stream.node.name == `maps`) { + content = PROCFS.readProcMaps(); + } else { + throw new FS.ErrnoError(2); + } } // copy the buffer if (position >= content.byteLength) { @@ -4400,38 +4476,52 @@ var Module = (() => { } function ___syscall_poll_scan(fds, nfds, tmSec, tmNsec) { - try { - growMemViews(gWasmMemory); + growMemViews(gWasmMemory); - let timeout = 0; - if (tmSec == -1) { - timeout = -1; - } else { - timeout = tmSec + tmNsec * 1e-9; - } + let timeoutSec = 0; + if (tmSec == -1) { + timeoutSec = -1; + } else { + timeoutSec = tmSec + tmNsec * 1e-9; + } - let nonzero = 0; - for (var i = 0; i < nfds; i++) { - var pollfd = fds + 8 * i; - var fd = HEAP32[pollfd >> 2]; - var events = HEAP16[pollfd + 4 >> 1]; - var mask = 32; - var stream = FS.getStream(fd); - if (stream) { - if (stream.stream_ops.poll) { - mask = stream.stream_ops.poll(stream, events, timeout) + // Scan all FDs for readiness, tolerating PTY "wait again" exceptions. + let nonzero = 0; + let needsWait = false; + for (var i = 0; i < nfds; i++) { + var pollfd = fds + 8 * i; + var fd = HEAP32[pollfd >> 2]; + var events = HEAP16[pollfd + 4 >> 1]; + var mask = 32; + var stream = FS.getStream(fd); + if (stream) { + if (stream.stream_ops.poll) { + try { + mask = stream.stream_ops.poll(stream, events, timeoutSec); + } catch (e) { + // PTY throws ErrnoError(1006) when not readable and timeout is set. + if (e.name === "ErrnoError") { + mask = 0; + needsWait = true; + } else { + throw e; + } } } - mask &= events | POLLERR | POLLHUP; - if (mask) nonzero++; - HEAP16[pollfd + 6 >> 1] = mask } - return nonzero - } catch (e) { - if (typeof FS == "undefined" || !(e.name === "ErrnoError")) throw e; - return -e.errno + mask &= events | POLLERR | POLLHUP; + if (mask) nonzero++; + HEAP16[pollfd + 6 >> 1] = mask; } + + // If no FDs are ready and there is a non-zero timeout, sleep before returning. + if (nonzero === 0 && (timeoutSec > 0 || (timeoutSec === -1 && needsWait))) { + var delayMs = timeoutSec > 0 ? Math.min(timeoutSec * 1000, 30000) : 1000; + return new Promise(resolve => setTimeout(() => resolve(0), delayMs)); + } + + return nonzero; } function ___syscall_pselect6_scan(nfds, readfdsP, writefdsP, exceptfdsP, tmSec, tmNsec, sigmaskP) { diff --git a/runtime/syscalls/SyscallBrowser.cpp b/runtime/syscalls/SyscallBrowser.cpp index d0d3367d..8db87974 100644 --- a/runtime/syscalls/SyscallBrowser.cpp +++ b/runtime/syscalls/SyscallBrowser.cpp @@ -172,6 +172,36 @@ struct _linux_statx { uint64_t __spare3[12]; }; +/* + for sysinfo +*/ +struct _elfarm64_sysinfo { + int64_t uptime; + uint64_t loads[3]; + uint64_t totalram; + uint64_t freeram; + uint64_t sharedram; + uint64_t bufferram; + uint64_t totalswap; + uint64_t freeswap; + uint16_t procs; + uint16_t pad; + uint32_t pad2; + uint64_t totalhigh; + uint64_t freehigh; + uint32_t mem_unit; +}; + +/* + for times +*/ +struct _elfarm64_tms { + int64_t tms_utime; + int64_t tms_stime; + int64_t tms_cutime; + int64_t tms_cstime; +}; + /* for rusage */ @@ -424,9 +454,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { // Scheduling ECV_SCHED_SETPARAM, - ECV_SCHED_SETSCHEDULER, - ECV_SCHED_GETSCHEDULER, - ECV_SCHED_GETPARAM, ECV_SCHED_SETAFFINITY, ECV_SCHED_YIELD, ECV_SCHED_GET_PRIORITY_MAX, @@ -460,8 +487,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_GETCPU, ECV_SETTIMEOFDAY, ECV_ADJTIMEX, - ECV_SYSINFO, - // User/group ID operations ECV_SETREGID, ECV_SETGID, @@ -473,7 +498,6 @@ constexpr uint32_t UNIMPLEMENTED_SYSCALLS[] = { ECV_GETRESGID, ECV_SETFSUID, ECV_SETFSGID, - ECV_TIMES, ECV_GETSID, ECV_SETSID, ECV_GETGROUPS, @@ -1272,6 +1296,42 @@ void RuntimeManager::SVCBrowserCall(uint8_t *arena_ptr) { } break; } + case ECV_SYSINFO: /* sysinfo (struct sysinfo *info) */ + { + auto info = (_elfarm64_sysinfo *) TranslateVMA(this, arena_ptr, X0_Q); + memset(info, 0, sizeof(_elfarm64_sysinfo)); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + info->uptime = ts.tv_sec; + info->loads[0] = 0; + info->loads[1] = 0; + info->loads[2] = 0; + info->totalram = 512ULL * 1024 * 1024; + info->freeram = 256ULL * 1024 * 1024; + info->sharedram = 0; + info->bufferram = 0; + info->totalswap = 0; + info->freeswap = 0; + info->procs = 1; + info->totalhigh = 0; + info->freehigh = 0; + info->mem_unit = 1; + X0_Q = 0; + break; + } + case ECV_TIMES: /* clock_t times(struct tms *buf) */ + { + auto buf = (_elfarm64_tms *) TranslateVMA(this, arena_ptr, X0_Q); + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + int64_t ticks = ts.tv_sec * 100 + ts.tv_nsec / 10000000; + buf->tms_utime = ticks; + buf->tms_stime = 0; + buf->tms_cutime = 0; + buf->tms_cstime = 0; + X0_Q = (uint64_t) ticks; + break; + } case ECV_GETPID: /* getpid () */ X0_D = main_ecv_pr->ecv_pid; break; case ECV_GETPPID: /* getppid () */ X0_D = main_ecv_pr->par_ecv_pid; break; case ECV_GETUID: /* getuid () */ X0_D = main_ecv_pr->ecv_uid; break; @@ -1524,7 +1584,8 @@ void RuntimeManager::SVCBrowserCall(uint8_t *arena_ptr) { } case ECV_FCHOWNAT: /* int fchownat(int dfd, const char *filename, uid_t user, gid_t group, int flag) */ { - int res = fchownat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, X3_D, X4_D); + int res = + fchownat(X0_D, (const char *) TranslateVMA(this, arena_ptr, X1_Q), X2_D, X3_D, X4_D); X0_Q = SetSyscallRes(res); break; } @@ -1556,9 +1617,29 @@ void RuntimeManager::SVCBrowserCall(uint8_t *arena_ptr) { X0_Q = (uint64_t) res; break; } + case ECV_SCHED_GETSCHEDULER: /* int sched_getscheduler(pid_t pid) */ + { + X0_Q = 0; /* SCHED_OTHER */ + break; + } + case ECV_SCHED_SETSCHEDULER: /* int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param) */ + { + X0_Q = 0; + break; + } + case ECV_SCHED_GETPARAM: /* int sched_getparam(pid_t pid, struct sched_param *param) */ + { + if (X1_Q != 0) { + auto p = (int32_t *) TranslateVMA(this, arena_ptr, X1_Q); + *p = 0; /* sched_priority = 0 */ + } + X0_Q = 0; + break; + } case ECV_RT_SIGPROCMASK: /* int rt_sigprocmask(int how, const sigset_t *set, sigset_t *oldset, size_t sigsetsize) */ { - const sigset_t *set_p = X1_Q == 0 ? nullptr : (const sigset_t *) TranslateVMA(this, arena_ptr, X1_Q); + const sigset_t *set_p = + X1_Q == 0 ? nullptr : (const sigset_t *) TranslateVMA(this, arena_ptr, X1_Q); sigset_t *oldset_p = X2_Q == 0 ? nullptr : (sigset_t *) TranslateVMA(this, arena_ptr, X2_Q); int res = sigprocmask(X0_D, set_p, oldset_p); X0_Q = SetSyscallRes(res); diff --git a/tests/browser/syscall-probe.spec.js b/tests/browser/syscall-probe.spec.js index 1f5112c0..4c234824 100644 --- a/tests/browser/syscall-probe.spec.js +++ b/tests/browser/syscall-probe.spec.js @@ -106,4 +106,67 @@ test.describe('Syscall probe - new syscalls', () => { const out = await waitForTerminalContent(page, '5'); expect(await out.jsonValue()).toContain('5'); }); + + test('/proc files readable (cat /proc/uptime and /proc/meminfo)', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + // Test /proc/uptime + await typeCommand(page, 'cat /proc/uptime'); + await new Promise(r => setTimeout(r, 1000)); + + // Test /proc/meminfo + await typeCommand(page, 'head -3 /proc/meminfo'); + await new Promise(r => setTimeout(r, 1000)); + + // Dump buffer to verify + const dump = await page.evaluate(() => { + const xterm = window.__test_xterm; + if (!xterm) return ''; + const buf = xterm.buffer.active; + let lines = []; + for (let i = 0; i < buf.length; i++) { + const line = buf.getLine(i); + if (line) { + let text = line.translateToString(true); + if (text.trim()) lines.push(text); + } + } + return lines.join('\n'); + }); + + // /proc/uptime should output digits.digits + expect(dump).toMatch(/\d+\.\d+/); + // /proc/meminfo should have MemTotal + expect(dump).toContain('MemTotal'); + }); + + test('top shows system info then quits', async ({ page }) => { + await page.goto('/'); + await waitForTerminalContent(page, 'bash-static.wasm'); + + // Run top + await typeCommand(page, 'top -b -n 1'); + // Wait for top output to appear (Mem or CPU line) + await new Promise(r => setTimeout(r, 5000)); + + // Dump buffer + const dump = await page.evaluate(() => { + const xterm = window.__test_xterm; + if (!xterm) return ''; + const buf = xterm.buffer.active; + let lines = []; + for (let i = 0; i < buf.length; i++) { + const line = buf.getLine(i); + if (line) { + let text = line.translateToString(true); + if (text.trim()) lines.push(text); + } + } + return lines.join('\n'); + }); + + // top should show memory info and CPU info + expect(dump).toMatch(/Mem:/i); + }); });