Skip to content

Commit

Permalink
feat(vm): support auto pause and resume vm (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackHole1 authored Oct 27, 2023
1 parent 7919ba8 commit 7a3aec6
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 20 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 96 additions & 9 deletions src/darwin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<OVMEventData>();
Expand All @@ -34,6 +40,8 @@ export class DarwinOVM {
private gvproxyProcess?: ChildProcessWithoutNullStreams;
private vfkitProcess?: ChildProcessWithoutNullStreams;

private monitor = createMacPowerMonitor();

private logGVProxy: Logger;
private logVFKit: Logger;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
});
Expand All @@ -254,10 +261,12 @@ export class DarwinOVM {
await ssh.execCommand(commands.join(" && "));
ssh.dispose();

this.addPowerMonitor();
this.remitter.emit("ready");
}

private async internalStop(): Promise<void> {
this.removePowerMonitor();
await request.post("http://vf/vm/state",
JSON.stringify({
state: "Stop",
Expand Down Expand Up @@ -296,7 +305,85 @@ export class DarwinOVM {
}

public async resetPath(): Promise<void> {
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<OVMVfkitFullState> {
const currentState = await request.get("http://vf/vm/state",
this.socket.vfkitRestful,
100,
);

return JSON.parse(currentState);
}

public async vmPause(): Promise<void> {
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<void> {
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,
});


}
}
26 changes: 26 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,36 @@ export interface OVMDarwinOptions {
export interface OVMEventData {
ready: void,
close: void,
vmPause: void,
vmResume: void,
error: Error,
}

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;
}
33 changes: 23 additions & 10 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,27 @@ export const pRetry = <T>(fn: () => Promise<T>, options: { retries: number, inte
};

class Request {
public get(url: string, timeout: number) {
return new Promise<void>((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<string>((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);
Expand All @@ -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);
});

Expand Down

0 comments on commit 7a3aec6

Please sign in to comment.