Merge pull request #767 from hydralauncher/hyd-226-investigate-if-its-possible-to-use-psutil-to-list-processes

remove UAC; replace ps-list with psutil
This commit is contained in:
Zamitto 2024-07-03 16:30:01 -03:00 committed by GitHub
commit 56c8349899
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 142 additions and 154 deletions

1
.gitignore vendored
View File

@ -1,7 +1,6 @@
.vscode .vscode
node_modules node_modules
hydra-download-manager/ hydra-download-manager/
fastlist.exe
__pycache__ __pycache__
dist dist
out out

View File

@ -5,10 +5,7 @@ directories:
extraResources: extraResources:
- hydra-download-manager - hydra-download-manager
- seeds - seeds
- from: node_modules/ps-list/vendor/fastlist-0.3.0-x64.exe
to: fastlist.exe
- from: node_modules/create-desktop-shortcuts/src/windows.vbs - from: node_modules/create-desktop-shortcuts/src/windows.vbs
- from: resources/hydralauncher.vbs
files: files:
- "!**/.vscode/*" - "!**/.vscode/*"
- "!src/*" - "!src/*"
@ -20,7 +17,6 @@ asarUnpack:
- resources/** - resources/**
win: win:
executableName: Hydra executableName: Hydra
requestedExecutionLevel: requireAdministrator
target: target:
- nsis - nsis
- portable - portable
@ -33,7 +29,6 @@ nsis:
allowToChangeInstallationDirectory: true allowToChangeInstallationDirectory: true
portable: portable:
artifactName: ${name}-${version}-portable.${ext} artifactName: ${name}-${version}-portable.${ext}
requestExecutionLevel: admin
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
extendInfo: extendInfo:

View File

@ -65,11 +65,11 @@
"lottie-react": "^2.4.0", "lottie-react": "^2.4.0",
"parse-torrent": "^11.0.16", "parse-torrent": "^11.0.16",
"piscina": "^4.5.1", "piscina": "^4.5.1",
"ps-list": "^8.1.1",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-loading-skeleton": "^3.4.0", "react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1", "react-redux": "^9.1.1",
"react-router-dom": "^6.22.3", "react-router-dom": "^6.22.3",
"sudo-prompt": "^9.2.1",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
"user-agents": "^1.1.193", "user-agents": "^1.1.193",
"yaml": "^2.4.1", "yaml": "^2.4.1",

View File

@ -3,3 +3,4 @@ cx_Freeze
cx_Logging; sys_platform == 'win32' cx_Logging; sys_platform == 'win32'
lief; sys_platform == 'win32' lief; sys_platform == 'win32'
pywin32; sys_platform == 'win32' pywin32; sys_platform == 'win32'
psutil

View File

@ -1,3 +0,0 @@
Set WshShell = CreateObject("WScript.Shell" )
WshShell.Run """%localappdata%\Programs\Hydra\Hydra.exe""", 0 'Must quote command if it has spaces; must escape quotes
Set WshShell = Nothing

View File

@ -14,12 +14,3 @@ export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
export const seedsPath = app.isPackaged export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds") ? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds"); : path.join(__dirname, "..", "..", "seeds");
export const windowsStartupPath = path.join(
app.getPath("appData"),
"Microsoft",
"Windows",
"Start Menu",
"Programs",
"Startup"
);

View File

@ -1,6 +1,6 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import * as Sentry from "@sentry/electron/main"; import * as Sentry from "@sentry/electron/main";
import { HydraApi, TorrentDownloader, gamesPlaytime } from "@main/services"; import { HydraApi, PythonInstance, gamesPlaytime } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import { DownloadQueue, Game, UserAuth } from "@main/entity"; import { DownloadQueue, Game, UserAuth } from "@main/entity";
@ -24,7 +24,7 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
Sentry.setUser(null); Sentry.setUser(null);
/* Disconnects libtorrent */ /* Disconnects libtorrent */
TorrentDownloader.kill(); PythonInstance.killTorrent();
await Promise.all([ await Promise.all([
databaseOperations, databaseOperations,

View File

@ -1,39 +1,45 @@
import path from "node:path";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { getProcesses } from "@main/helpers";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { PythonInstance, logger } from "@main/services";
import sudo from "sudo-prompt";
import { app } from "electron";
const getKillCommand = (pid: number) => {
if (process.platform == "win32") {
return `taskkill /PID ${pid}`;
}
return `kill -9 ${pid}`;
};
const closeGame = async ( const closeGame = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
gameId: number gameId: number
) => { ) => {
const processes = await getProcesses(); const processes = await PythonInstance.getProcessList();
const game = await gameRepository.findOne({ const game = await gameRepository.findOne({
where: { id: gameId, isDeleted: false }, where: { id: gameId, isDeleted: false },
}); });
if (!game) return false; if (!game) return;
const executablePath = game.executablePath!;
const basename = path.win32.basename(executablePath);
const basenameWithoutExtension = path.win32.basename(
executablePath,
path.extname(executablePath)
);
const gameProcess = processes.find((runningProcess) => { const gameProcess = processes.find((runningProcess) => {
if (process.platform === "win32") { return runningProcess.exe === game.executablePath;
return runningProcess.name === basename;
}
return [basename, basenameWithoutExtension].includes(runningProcess.name);
}); });
if (gameProcess) return process.kill(gameProcess.pid); if (gameProcess) {
return false; try {
process.kill(gameProcess.pid);
} catch (err) {
sudo.exec(
getKillCommand(gameProcess.pid),
{ name: app.getName() },
(error, _stdout, _stderr) => {
logger.error(error);
}
);
}
}
}; };
registerEvent("closeGame", closeGame); registerEvent("closeGame", closeGame);

View File

@ -4,6 +4,7 @@ import { IsNull, Not } from "typeorm";
import createDesktopShortcut from "create-desktop-shortcuts"; import createDesktopShortcut from "create-desktop-shortcuts";
import path from "node:path"; import path from "node:path";
import { app } from "electron"; import { app } from "electron";
import { removeSymbolsFromName } from "@shared";
const createGameShortcut = async ( const createGameShortcut = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -22,7 +23,7 @@ const createGameShortcut = async (
const options = { const options = {
filePath, filePath,
name: game.title, name: removeSymbolsFromName(game.title),
}; };
return createDesktopShortcut({ return createDesktopShortcut({

View File

@ -1,9 +1,6 @@
import { windowsStartupPath } from "@main/constants";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import AutoLaunch from "auto-launch"; import AutoLaunch from "auto-launch";
import { app } from "electron"; import { app } from "electron";
import fs from "node:fs";
import path from "node:path";
const autoLaunch = async ( const autoLaunch = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
@ -15,23 +12,10 @@ const autoLaunch = async (
name: app.getName(), name: app.getName(),
}); });
if (process.platform == "win32") { if (enabled) {
const destination = path.join(windowsStartupPath, "Hydra.vbs"); appLauncher.enable().catch();
if (enabled) {
const scriptPath = path.join(process.resourcesPath, "hydralauncher.vbs");
fs.copyFileSync(scriptPath, destination);
} else {
appLauncher.disable().catch();
fs.rmSync(destination);
}
} else { } else {
if (enabled) { appLauncher.disable().catch();
appLauncher.enable().catch();
} else {
appLauncher.disable().catch();
}
} }
}; };

View File

@ -57,5 +57,4 @@ export const requestWebPage = async (url: string) => {
.then((response) => response.data); .then((response) => response.data);
}; };
export * from "./ps";
export * from "./download-source"; export * from "./download-source";

View File

@ -1,41 +0,0 @@
import psList from "ps-list";
import path from "node:path";
import childProcess from "node:child_process";
import { promisify } from "node:util";
import { app } from "electron";
const TEN_MEGABYTES = 1000 * 1000 * 10;
const execFile = promisify(childProcess.execFile);
export const getProcesses = async () => {
if (process.platform == "win32") {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "fastlist.exe")
: path.join(
__dirname,
"..",
"..",
"node_modules",
"ps-list",
"vendor",
"fastlist-0.3.0-x64.exe"
);
const { stdout } = await execFile(binaryPath, {
maxBuffer: TEN_MEGABYTES,
windowsHide: true,
});
return stdout
.trim()
.split("\r\n")
.map((line) => line.split("\t"))
.map(([pid, ppid, name]) => ({
pid: Number.parseInt(pid, 10),
ppid: Number.parseInt(ppid, 10),
name,
}));
} else {
return psList();
}
};

View File

@ -5,7 +5,7 @@ import i18n from "i18next";
import path from "node:path"; import path from "node:path";
import url from "node:url"; import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils"; import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, TorrentDownloader, WindowManager } from "@main/services"; import { logger, PythonInstance, WindowManager } from "@main/services";
import { dataSource } from "@main/data-source"; import { dataSource } from "@main/data-source";
import * as resources from "@locales"; import * as resources from "@locales";
import { userPreferencesRepository } from "@main/repository"; import { userPreferencesRepository } from "@main/repository";
@ -120,7 +120,7 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => { app.on("before-quit", () => {
/* Disconnects libtorrent */ /* Disconnects libtorrent */
TorrentDownloader.kill(); PythonInstance.kill();
}); });
app.on("activate", () => { app.on("activate", () => {

View File

@ -1,4 +1,9 @@
import { DownloadManager, RepacksManager, startMainLoop } from "./services"; import {
DownloadManager,
RepacksManager,
PythonInstance,
startMainLoop,
} from "./services";
import { import {
downloadQueueRepository, downloadQueueRepository,
repackRepository, repackRepository,
@ -12,8 +17,6 @@ import { MoreThan } from "typeorm";
import { HydraApi } from "./services/hydra-api"; import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync"; import { uploadGamesBatch } from "./services/library-sync";
startMainLoop();
const loadState = async (userPreferences: UserPreferences | null) => { const loadState = async (userPreferences: UserPreferences | null) => {
await RepacksManager.updateRepacks(); await RepacksManager.updateRepacks();
@ -35,8 +38,13 @@ const loadState = async (userPreferences: UserPreferences | null) => {
}, },
}); });
if (nextQueueItem?.game.status === "active") if (nextQueueItem?.game.status === "active") {
DownloadManager.startDownload(nextQueueItem.game); DownloadManager.startDownload(nextQueueItem.game);
} else {
PythonInstance.spawn();
}
startMainLoop();
const now = new Date(); const now = new Date();

View File

@ -1,6 +1,6 @@
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { Downloader } from "@shared"; import { Downloader } from "@shared";
import { TorrentDownloader } from "./torrent-downloader"; import { PythonInstance } from "./python-instance";
import { WindowManager } from "../window-manager"; import { WindowManager } from "../window-manager";
import { downloadQueueRepository, gameRepository } from "@main/repository"; import { downloadQueueRepository, gameRepository } from "@main/repository";
import { publishDownloadCompleteNotification } from "../notifications"; import { publishDownloadCompleteNotification } from "../notifications";
@ -16,7 +16,7 @@ export class DownloadManager {
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.RealDebrid) {
status = await RealDebridDownloader.getStatus(); status = await RealDebridDownloader.getStatus();
} else { } else {
status = await TorrentDownloader.getStatus(); status = await PythonInstance.getStatus();
} }
if (status) { if (status) {
@ -65,7 +65,7 @@ export class DownloadManager {
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.pauseDownload(); RealDebridDownloader.pauseDownload();
} else { } else {
await TorrentDownloader.pauseDownload(); await PythonInstance.pauseDownload();
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -77,7 +77,7 @@ export class DownloadManager {
RealDebridDownloader.startDownload(game); RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid; this.currentDownloader = Downloader.RealDebrid;
} else { } else {
TorrentDownloader.startDownload(game); PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent; this.currentDownloader = Downloader.Torrent;
} }
} }
@ -86,7 +86,7 @@ export class DownloadManager {
if (this.currentDownloader === Downloader.RealDebrid) { if (this.currentDownloader === Downloader.RealDebrid) {
RealDebridDownloader.cancelDownload(); RealDebridDownloader.cancelDownload();
} else { } else {
TorrentDownloader.cancelDownload(gameId); PythonInstance.cancelDownload(gameId);
} }
WindowManager.mainWindow?.setProgressBar(-1); WindowManager.mainWindow?.setProgressBar(-1);
@ -98,7 +98,7 @@ export class DownloadManager {
RealDebridDownloader.startDownload(game); RealDebridDownloader.startDownload(game);
this.currentDownloader = Downloader.RealDebrid; this.currentDownloader = Downloader.RealDebrid;
} else { } else {
TorrentDownloader.startDownload(game); PythonInstance.startDownload(game);
this.currentDownloader = Downloader.Torrent; this.currentDownloader = Downloader.Torrent;
} }
} }

View File

@ -1,2 +1,2 @@
export * from "./download-manager"; export * from "./download-manager";
export * from "./torrent-downloader"; export * from "./python-instance";

View File

@ -1,7 +1,11 @@
import cp from "node:child_process"; import cp from "node:child_process";
import { Game } from "@main/entity"; import { Game } from "@main/entity";
import { RPC_PASSWORD, RPC_PORT, startTorrentClient } from "./torrent-client"; import {
RPC_PASSWORD,
RPC_PORT,
startTorrentClient as startRPCClient,
} from "./torrent-client";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { DownloadProgress } from "@types"; import { DownloadProgress } from "@types";
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity";
@ -13,10 +17,11 @@ import {
PauseDownloadPayload, PauseDownloadPayload,
LibtorrentStatus, LibtorrentStatus,
LibtorrentPayload, LibtorrentPayload,
ProcessPayload,
} from "./types"; } from "./types";
export class TorrentDownloader { export class PythonInstance {
private static torrentClient: cp.ChildProcess | null = null; private static pythonProcess: cp.ChildProcess | null = null;
private static downloadingGameId = -1; private static downloadingGameId = -1;
private static rpc = axios.create({ private static rpc = axios.create({
@ -26,18 +31,31 @@ export class TorrentDownloader {
}, },
}); });
private static spawn(args: StartDownloadPayload) { public static spawn(args?: StartDownloadPayload) {
this.torrentClient = startTorrentClient(args); this.pythonProcess = startRPCClient(args);
} }
public static kill() { public static kill() {
if (this.torrentClient) { if (this.pythonProcess) {
this.torrentClient.kill(); this.pythonProcess.kill();
this.torrentClient = null; this.pythonProcess = null;
this.downloadingGameId = -1; this.downloadingGameId = -1;
} }
} }
public static killTorrent() {
if (this.pythonProcess) {
this.rpc.post("/action", { action: "kill-torrent" });
this.downloadingGameId = -1;
}
}
public static async getProcessList() {
return (
(await this.rpc.get<ProcessPayload[] | null>("/process-list")).data || []
);
}
public static async getStatus() { public static async getStatus() {
if (this.downloadingGameId === -1) return null; if (this.downloadingGameId === -1) return null;
@ -113,7 +131,7 @@ export class TorrentDownloader {
} }
static async startDownload(game: Game) { static async startDownload(game: Game) {
if (!this.torrentClient) { if (!this.pythonProcess) {
this.spawn({ this.spawn({
game_id: game.id, game_id: game.id,
magnet: game.uri!, magnet: game.uri!,

View File

@ -15,12 +15,12 @@ export const BITTORRENT_PORT = "5881";
export const RPC_PORT = "8084"; export const RPC_PORT = "8084";
export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex"); export const RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
export const startTorrentClient = (args: StartDownloadPayload) => { export const startTorrentClient = (args?: StartDownloadPayload) => {
const commonArgs = [ const commonArgs = [
BITTORRENT_PORT, BITTORRENT_PORT,
RPC_PORT, RPC_PORT,
RPC_PASSWORD, RPC_PASSWORD,
encodeURIComponent(JSON.stringify(args)), args ? encodeURIComponent(JSON.stringify(args)) : "",
]; ];
if (app.isPackaged) { if (app.isPackaged) {

View File

@ -31,3 +31,8 @@ export interface LibtorrentPayload {
status: LibtorrentStatus; status: LibtorrentStatus;
gameId: number; gameId: number;
} }
export interface ProcessPayload {
exe: string;
pid: number;
}

View File

@ -1,11 +1,9 @@
import path from "node:path";
import { IsNull, Not } from "typeorm"; import { IsNull, Not } from "typeorm";
import { gameRepository } from "@main/repository"; import { gameRepository } from "@main/repository";
import { getProcesses } from "@main/helpers";
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync"; import { createGame, updateGamePlaytime } from "./library-sync";
import { GameRunning } from "@types"; import { GameRunning } from "@types";
import { PythonInstance } from "./download";
export const gamesPlaytime = new Map< export const gamesPlaytime = new Map<
number, number,
@ -21,23 +19,13 @@ export const watchProcesses = async () => {
}); });
if (games.length === 0) return; if (games.length === 0) return;
const processes = await PythonInstance.getProcessList();
const processes = await getProcesses();
for (const game of games) { for (const game of games) {
const executablePath = game.executablePath!; const executablePath = game.executablePath!;
const basename = path.win32.basename(executablePath);
const basenameWithoutExtension = path.win32.basename(
executablePath,
path.extname(executablePath)
);
const gameProcess = processes.find((runningProcess) => { const gameProcess = processes.find((runningProcess) => {
if (process.platform === "win32") { return executablePath == runningProcess.exe;
return runningProcess.name === basename;
}
return [basename, basenameWithoutExtension].includes(runningProcess.name);
}); });
if (gameProcess) { if (gameProcess) {

View File

@ -122,7 +122,7 @@ export function HeroPanelActions() {
<Button <Button
onClick={() => setShowGameOptionsModal(true)} onClick={() => setShowGameOptionsModal(true)}
theme="outline" theme="outline"
disabled={deleting || isGameRunning} disabled={deleting}
className={styles.heroPanelAction} className={styles.heroPanelAction}
> >
<GearIcon /> <GearIcon />

View File

@ -30,6 +30,16 @@ class Downloader:
self.torrent_handles[game_id] = None self.torrent_handles[game_id] = None
self.downloading_game_id = -1 self.downloading_game_id = -1
def abort_session(self):
for game_id in self.torrent_handles:
torrent_handle = self.torrent_handles[game_id]
torrent_handle.pause()
self.session.remove_torrent(torrent_handle)
self.session.abort()
self.torrent_handles = {}
self.downloading_game_id = -1
def get_download_status(self): def get_download_status(self):
if self.downloading_game_id == -1: if self.downloading_game_id == -1:
return None return None

View File

@ -2,16 +2,20 @@ import sys
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
import json import json
import urllib.parse import urllib.parse
import psutil
from downloader import Downloader from downloader import Downloader
torrent_port = sys.argv[1] torrent_port = sys.argv[1]
http_port = sys.argv[2] http_port = sys.argv[2]
rpc_password = sys.argv[3] rpc_password = sys.argv[3]
initial_download = json.loads(urllib.parse.unquote(sys.argv[4])) start_download_payload = sys.argv[4]
downloader = Downloader(torrent_port) downloader = None
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path']) if start_download_payload:
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloader = Downloader(torrent_port)
downloader.start_download(initial_download['game_id'], initial_download['magnet'], initial_download['save_path'])
class Handler(BaseHTTPRequestHandler): class Handler(BaseHTTPRequestHandler):
rpc_password_header = 'x-hydra-rpc-password' rpc_password_header = 'x-hydra-rpc-password'
@ -30,11 +34,28 @@ class Handler(BaseHTTPRequestHandler):
status = downloader.get_download_status() status = downloader.get_download_status()
self.wfile.write(json.dumps(status).encode('utf-8')) self.wfile.write(json.dumps(status).encode('utf-8'))
if self.path == "/healthcheck":
elif self.path == "/healthcheck":
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()
elif self.path == "/process-list":
if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401)
self.end_headers()
return
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'username'])]
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(process_list).encode('utf-8'))
def do_POST(self): def do_POST(self):
global downloader
if self.path == "/action": if self.path == "/action":
if self.headers.get(self.rpc_password_header) != rpc_password: if self.headers.get(self.rpc_password_header) != rpc_password:
self.send_response(401) self.send_response(401)
@ -45,12 +66,18 @@ class Handler(BaseHTTPRequestHandler):
post_data = self.rfile.read(content_length) post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8')) data = json.loads(post_data.decode('utf-8'))
if downloader is None:
downloader = Downloader(torrent_port)
if data['action'] == 'start': if data['action'] == 'start':
downloader.start_download(data['game_id'], data['magnet'], data['save_path']) downloader.start_download(data['game_id'], data['magnet'], data['save_path'])
elif data['action'] == 'pause': elif data['action'] == 'pause':
downloader.pause_download(data['game_id']) downloader.pause_download(data['game_id'])
elif data['action'] == 'cancel': elif data['action'] == 'cancel':
downloader.cancel_download(data['game_id']) downloader.cancel_download(data['game_id'])
elif data['action'] == 'kill-torrent':
downloader.abort_session()
downloader = None
self.send_response(200) self.send_response(200)
self.end_headers() self.end_headers()

View File

@ -6311,11 +6311,6 @@ proxy-from-env@^1.1.0:
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
ps-list@^8.1.1:
version "8.1.1"
resolved "https://registry.npmjs.org/ps-list/-/ps-list-8.1.1.tgz"
integrity sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==
psl@^1.1.33: psl@^1.1.33:
version "1.9.0" version "1.9.0"
resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"
@ -6999,6 +6994,11 @@ strtok3@^7.0.0:
"@tokenizer/token" "^0.3.0" "@tokenizer/token" "^0.3.0"
peek-readable "^5.0.0" peek-readable "^5.0.0"
sudo-prompt@^9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/sudo-prompt/-/sudo-prompt-9.2.1.tgz#77efb84309c9ca489527a4e749f287e6bdd52afd"
integrity sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==
sumchecker@^3.0.1: sumchecker@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz" resolved "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz"