stop using wmic, as it's deprecated #776 #675

Massimo Melina committed Nov 2, 2024 at 23:21 UTC fc46293b1f2477e5b40fad13479c46257baf40e8
2 files changed +19 -44
package.json
+1
@@ -70,6 +70,7 @@
70 ]
71 },
72 "dependencies": {
73 + "@gregoranders/csv": "^0.0.13",
74 "@koa/router": "^13.0.1",
75 "@node-rs/crc32": "^1.10.3",
76 "@rejetto/kvstorage": "^0.12.2",
src/util-os.ts
+18 -44
@@ -1,23 +1,19 @@
1 -import { dirname, resolve } from 'path'
2 -import { existsSync } from 'fs'
3 -import { exec, ExecOptions, execSync, spawnSync } from 'child_process'
4 -import { exists, onlyTruthy, prefix, splitAt } from './misc'
5 -import _ from 'lodash'
1 +import { dirname } from 'path'
2 +import { existsSync, statfsSync } from 'fs'
3 +import { exec, ExecOptions } from 'child_process'
4 +import { isWindowsDrive, onlyTruthy, promiseBestEffort } from './misc'
5 +import Parser from '@gregoranders/csv';
6 import { pid } from 'node:process'
7 import { promisify } from 'util'
8 import { IS_WINDOWS } from './const'
9
10 const DF_TIMEOUT = 2000
11 -const LOGICALDISK_TIMEOUT = DF_TIMEOUT
11
13 -// not using statfsSync because it's not available in node 18.5.0 (latest version with pkg)
12 export function getDiskSpaceSync(path: string) {
15 - if (IS_WINDOWS)
16 - return parseLogicaldisk(execSync(makeLogicaldisk(path), { timeout: LOGICALDISK_TIMEOUT }).toString())[0]
13 while (path && !existsSync(path))
14 path = dirname(path)
19 - try { return parseDfResult(spawnSync('df', ['-k', path], { timeout: DF_TIMEOUT }).stdout.toString())[0] }
20 - catch(e: any) { throw parseDfResult(e) }
15 + const res = statfsSync(path)
16 + return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path }
17 }
18
19 export function bashEscape(par: string) {
@@ -29,17 +25,16 @@ export function cmdEscape(par: string) {
25 }
26
27 export async function getDiskSpace(path: string) {
32 - if (IS_WINDOWS)
33 - return parseLogicaldisk(await runCmd(makeLogicaldisk(path), [], { timeout: LOGICALDISK_TIMEOUT }))[0]
34 - while (path && !await exists(path))
28 + while (path && !isWindowsDrive(path) && !existsSync(path))
29 path = dirname(path)
36 - return parseDfResult(await promisify(exec)(`df -k`, { timeout: DF_TIMEOUT }).then(x => x.stdout, e => e))[0]
30 + const res = statfsSync(path)
31 + return { free: res.bavail * res.bsize, total: res.blocks * res.bsize, name: path }
32 }
33
34 export async function getDiskSpaces(): Promise<{ name: string, free: number, total: number, description?: string }[]> {
35 if (IS_WINDOWS) {
41 - const drives = await getDrives() // since network-drives can hang 'wmic' for many seconds checking disk space (issue#648), and a single timeout would make whole operation fail, so we fork the job on each drive
42 - return onlyTruthy(await Promise.all(drives.map(getDiskSpace)))
36 + const drives = await getDrives()
37 + return onlyTruthy(await promiseBestEffort(drives.map(getDiskSpace)))
38 }
39 return parseDfResult(await promisify(exec)(`df -k`, { timeout: DF_TIMEOUT }).then(x => x.stdout, e => e))
40 }
@@ -64,8 +59,8 @@ function parseDfResult(result: string | Error) {
59 }
60
61 export async function getDrives() {
67 - const stdout = await runCmd('wmic logicaldisk get name')
68 - return stdout.split('\n').slice(1).map(x => x.trim()).filter(Boolean)
62 + const res = await runCmd('fsutil fsinfo drives') // example output: `Drives: C:\ D:\ Z:\`
63 + return res.trim().replaceAll('\\', '').split(' ').slice(1)
64 }
65
66 // execute win32 shell commands
@@ -76,8 +71,10 @@ export async function runCmd(cmd: string, args: string[] = [], options: ExecOpti
71 }
72
73 async function getWindowsServicePids() {
79 - const res = await runCmd(`wmic service get ProcessId`)
80 - return _.uniq(res.split('\n').slice(1).map(x => Number(x.trim())))
74 + const res = await runCmd('tasklist /svc /fo csv')
75 + const parsed = new Parser().parse(res)
76 + const no = parsed?.[1]?.[2]
77 + return parsed.slice(2).filter(x => x[2] !== no).map(x => Number(x[1]))
78 }
79
80 export const RUNNING_AS_SERVICE = IS_WINDOWS && getWindowsServicePids().then(x => {
@@ -88,26 +85,3 @@ export const RUNNING_AS_SERVICE = IS_WINDOWS && getWindowsServicePids().then(x =
85 console.log("couldn't determine if we are running as a service")
86 console.debug(e)
87 })
91 -
92 -function parseKeyValueObjects<T extends string>(all: string, keySep='=', lineSep='\n', objectSep=/\n\n+/) {
93 - return all.split(objectSep).map(obj =>
94 - Object.fromEntries(obj.split(lineSep).map(kv => splitAt(keySep, kv))) ) as { [k in T]: string }[]
95 -}
96 -
97 -const wmicFields = ['Size','FreeSpace','Name','Description'] as const
98 -
99 -function makeLogicaldisk(path='') {
100 - const drive = resolve(path).slice(0, 2).toUpperCase()
101 - if (!drive.match(/^(|\w:)$/)) throw 'invalid-path'
102 - return `wmic logicaldisk ${prefix(`where "DeviceID = '`, drive, `'"`)} get ${wmicFields.join()} /format:list`
103 -}
104 -
105 -function parseLogicaldisk(out: string) {
106 - const objs = parseKeyValueObjects<typeof wmicFields[number]>(out.replace(/\r/g, ''))
107 - return onlyTruthy(objs.map(x => x.Size && {
108 - total: Number(x.Size),
109 - free: Number(x.FreeSpace),
110 - name: x.Name,
111 - description: x.Description
112 - }))
113 -}