diff --git a/package.json b/package.json index d9a4660..9cfa5d1 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "typescript": "^5.2.2" }, "dependencies": { + "@oomol-lab/mac-power-monitor": "^1.0.0", "adm-zip": "^0.5.10", "node-ssh": "^13.1.0", "remitter": "^0.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72f8372..12c5dc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,10 +1,13 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true excludeLinksFromLockfile: false dependencies: + '@oomol-lab/mac-power-monitor': + specifier: ^1.0.0 + version: 1.0.0 adm-zip: specifier: ^0.5.10 version: 0.5.10 @@ -411,6 +414,10 @@ packages: - supports-color dev: true + /@oomol-lab/mac-power-monitor@1.0.0: + resolution: {integrity: sha512-KseIQ3F68QXXWbD79kD9o4d3lsvH+SCvbBKZ/LzS/LyLUcpbxe57tTp5/4vCsTHaXORNX1pqxmOFqWWlHnzCJQ==} + dev: false + /@oomol-lab/tsconfig@0.0.1: resolution: {integrity: sha512-T45eJq+qKK6++ccNORZ3XyFRJ5MOf6dqazFcgx8SP4yJTJ5EsQhg0sECLI1hRsakjW6N1NxOCuQ4Z+XU37FPSQ==} dev: true diff --git a/src/darwin.ts b/src/darwin.ts index 0220033..b30b2d3 100644 --- a/src/darwin.ts +++ b/src/darwin.ts @@ -11,15 +11,21 @@ import { sleep, unzip, } from "./utils"; -import type { OVMDarwinOptions, OVMEventData } from "./type"; +import { Logger } from "./logger"; import os from "node:os"; import cp from "node:child_process"; -import type { ChildProcessWithoutNullStreams } from "node:child_process"; -import { Logger } from "./logger"; +import path from "node:path"; import { NodeSSH } from "node-ssh"; import { Remitter } from "remitter"; -import type { OVMInfo } from "./type"; -import path from "node:path"; +import { createMacPowerMonitor } from "@oomol-lab/mac-power-monitor"; +import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import { + type OVMInfo, + type OVMDarwinOptions, + type OVMEventData, + type OVMVfkitFullState, + OVMVfkitState, +} from "./type"; export class DarwinOVM { private readonly remitter = new Remitter(); @@ -34,6 +40,8 @@ export class DarwinOVM { private gvproxyProcess?: ChildProcessWithoutNullStreams; private vfkitProcess?: ChildProcessWithoutNullStreams; + private monitor = createMacPowerMonitor(); + private logGVProxy: Logger; private logVFKit: Logger; @@ -113,8 +121,7 @@ export class DarwinOVM { this.sshPort = await findUsablePort(2223); } - public on(event: "ready", listener: () => void): void; - public on(event: "close", listener: () => void): void; + public on(event: "ready" | "close" | "vmPause" | "vmResume", listener: () => void): void; public on(event: "error", listener: (error: Error) => void): void; public on(event: keyof OVMEventData, listener: (...args: any[]) => void): void { this.remitter.on(event, listener); @@ -229,7 +236,7 @@ export class DarwinOVM { ); await sleep(1000); - await pRetry(() => request.get(`http://localhost:${this.podmanPort}/libpod/_ping`, 100), { + await pRetry(() => request.get(`http://localhost:${this.podmanPort}/libpod/_ping`, null, 100), { interval: 10, retries: 20, }); @@ -254,10 +261,12 @@ export class DarwinOVM { await ssh.execCommand(commands.join(" && ")); ssh.dispose(); + this.addPowerMonitor(); this.remitter.emit("ready"); } private async internalStop(): Promise { + this.removePowerMonitor(); await request.post("http://vf/vm/state", JSON.stringify({ state: "Stop", @@ -296,7 +305,85 @@ export class DarwinOVM { } public async resetPath(): Promise { - await this.internalStop(); await this.overridePath(true); } + + private addPowerMonitor() { + this.removePowerMonitor(); + this.monitor.listenOnWillSleep(async () => { + await this.vmPause(); + }); + this.monitor.listenOnWillWake(async () => { + await this.vmResume(); + }); + } + + private removePowerMonitor() { + this.monitor.unregisterAll(); + } + + public async vmState(): Promise { + const currentState = await request.get("http://vf/vm/state", + this.socket.vfkitRestful, + 100, + ); + + return JSON.parse(currentState); + } + + public async vmPause(): Promise { + await pRetry(async () => { + const { state: currentState, canPause } = await this.vmState(); + if (currentState === OVMVfkitState.VirtualMachineStatePaused || currentState === OVMVfkitState.VirtualMachineStatePausing) { + this.remitter.emit("vmPause"); + return; + } + + if (!canPause) { + throw new Error("vm can not pause"); + } + + await request.post("http://vf/vm/state", + JSON.stringify({ + state: "Pause", + }), + this.socket.vfkitRestful, + 100, + ); + + this.remitter.emit("vmPause"); + }, { + retries: 3, + interval: 500, + }); + } + + public async vmResume(): Promise { + await pRetry(async () => { + const { state: currentState, canResume } = await this.vmState(); + if (currentState === OVMVfkitState.VirtualMachineStateRunning || currentState === OVMVfkitState.VirtualMachineStateResuming) { + this.remitter.emit("vmResume"); + return; + } + + if (!canResume) { + throw new Error("vm can not resume"); + } + + await request.post("http://vf/vm/state", + JSON.stringify({ + state: "Resume", + }), + this.socket.vfkitRestful, + 100, + ); + + this.remitter.emit("vmResume"); + }, { + retries: 3, + interval: 500, + }); + + + } } diff --git a/src/type.ts b/src/type.ts index 9a06fc3..165d576 100644 --- a/src/type.ts +++ b/src/type.ts @@ -14,6 +14,8 @@ export interface OVMDarwinOptions { export interface OVMEventData { ready: void, close: void, + vmPause: void, + vmResume: void, error: Error, } @@ -21,3 +23,27 @@ export interface OVMInfo { podmanPort: number; sshPort: number; } + +/** + * @see https://github.com/Code-Hex/vz/blob/bd29a7ea3d39465c4224bfb01e990e8c220a8449/virtualization.go#L23 + */ +export enum OVMVfkitState { + VirtualMachineStateStopped = "VirtualMachineStateStopped", + VirtualMachineStateRunning = "VirtualMachineStateRunning", + VirtualMachineStatePaused = "VirtualMachineStatePaused", + VirtualMachineStateError = "VirtualMachineStateError", + VirtualMachineStateStarting = "VirtualMachineStateStarting", + VirtualMachineStatePausing = "VirtualMachineStatePausing", + VirtualMachineStateResuming = "VirtualMachineStateResuming", + VirtualMachineStateStopping = "VirtualMachineStateStopping", + VirtualMachineStateSaving = "VirtualMachineStateSaving", + VirtualMachineStateRestoring = "VirtualMachineStateRestoring", +} + +export interface OVMVfkitFullState { + state: OVMVfkitState; + canPause: boolean; + canResume: boolean; + canStop: boolean; + canHardStop: boolean; +} diff --git a/src/utils.ts b/src/utils.ts index f7ad479..186d38c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -87,15 +87,27 @@ export const pRetry = (fn: () => Promise, options: { retries: number, inte }; class Request { - public get(url: string, timeout: number) { - return new Promise((resolve, reject) => { - http.get(url, { timeout }, (response) => { - const { statusCode } = response; - if (!statusCode || statusCode >= 400) { - reject(new Error(`Request Failed. Status Code: ${statusCode}`)); - } else { - resolve(); - } + public get(url: string, socketPath: string | null, timeout: number) { + return new Promise((resolve, reject) => { + http.get(url, { + timeout, + socketPath: socketPath ?? undefined, + }, (response) => { + response.setEncoding("utf8"); + + let body = ""; + response.on("data", (chunk) => { + body += chunk; + }); + + response.on("end", () => { + const { statusCode } = response; + if (!statusCode || statusCode >= 400) { + return reject(new Error(`Request Failed. Status Code: ${statusCode}, Response: ${body}`)); + } else { + resolve(body); + } + }); }) .once("error", (error) => { reject(error); @@ -113,13 +125,14 @@ class Request { }, (response) => { const { statusCode } = response; if (!statusCode || statusCode >= 400) { + response.resume(); reject(new Error(`Request Failed. Status Code: ${statusCode}`)); } else { resolve(); } }); - req.on("error", (error) => { + req.once("error", (error) => { reject(error); });