diff --git a/cspell.config.yml b/cspell.config.yml index fbba88d..cd2e9dd 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -13,6 +13,7 @@ words: - vsock - btrfs - chrony + - npipe dictionaries: - companies - softwareTerms diff --git a/src/index.ts b/src/index.ts index 5addad5..6f840f8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,20 @@ -import type { OVMDarwinOptions } from "./type"; +import type { OVMDarwinOptions, OVMWindowsOptions } from "./type"; import { DarwinOVM } from "./darwin"; +import { WindowsOVM } from "./windows"; export const createDarwinOVM = (options: OVMDarwinOptions): Promise => { return DarwinOVM.create(options); }; +export const createWindowsOVM = (options: OVMWindowsOptions): WindowsOVM => { + return WindowsOVM.create(options); +}; + export { OVMDarwinAppEventValue, OVMDarwinVzState, + OVMWindowsSysEventValue, + OVMWindowsAppEventValue, } from "./type"; export type { @@ -15,5 +22,7 @@ export type { OVMDarwinOptions, OVMDarwinInfo, OVMDarwinState, + OVMWindowsOptions, + OVMWindowsEventData, } from "./type"; -export type { DarwinOVM }; +export type { DarwinOVM, WindowsOVM }; diff --git a/src/request.ts b/src/request.ts index 7fb3f57..e2d3efb 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,6 +1,6 @@ import path from "node:path"; import http from "node:http"; -import type { OVMDarwinInfo, OVMDarwinState } from "./type"; +import type { OVMDarwinInfo, OVMDarwinState, OVMWindowsInfo } from "./type"; enum Method { GET = "GET", @@ -9,9 +9,10 @@ enum Method { } const DEFAULT_TIMEOUT = 200; +const NEVER_TIMEOUT = 0; abstract class Request { - public abstract info(): Promise; + public abstract info(): Promise; protected readonly socketPath: string; protected constructor(socketPath: string) { @@ -90,3 +91,25 @@ export class RequestDarwin extends Request { }); } } + +export class RequestWindows extends Request { + public constructor(name: string) { + super(`//./pipe/ovm-${name}`); + } + + public async info(): Promise { + return JSON.parse(await this.do("info", Method.GET)) as OVMWindowsInfo; + } + + public async enableFeature(): Promise { + await this.do("enable-feature", Method.POST, NEVER_TIMEOUT); + } + + public async reboot(): Promise { + await this.do("reboot", Method.POST, NEVER_TIMEOUT); + } + + public async updateWSL(): Promise { + await this.do("update-wsl", Method.PUT, NEVER_TIMEOUT); + } +} diff --git a/src/type.ts b/src/type.ts index 144cb49..4484f60 100644 --- a/src/type.ts +++ b/src/type.ts @@ -59,3 +59,57 @@ export interface OVMDarwinState { canRequestStop: boolean; CanStop: boolean; } + +// ----- windows + +export interface OVMWindowsInfo { + podmanHost: string; + podmanPort: number; +} + +export interface OVMWindowsOptions { + name: string; + ovmPath: string; + linuxPath: { + rootfs: string; + }; + imageDir: string; + logDir: string; + versions: OVMWindowsOptions["linuxPath"] & { data: string; }; +} + +export enum OVMWindowsSysEventValue { + SystemNotSupport = "SystemNotSupport", + + NeedEnableFeature = "NeedEnableFeature", + EnableFeaturing = "EnableFeaturing", + EnableFeatureFailed = "EnableFeatureFailed", + EnableFeatureSuccess = "EnableFeatureSuccess", + NeedReboot = "NeedReboot", + + NeedUpdateWSL = "NeedUpdateWSL", + UpdatingWSL = "UpdatingWSL", + UpdateWSLFailed = "UpdateWSLFailed", + UpdateWSLSuccess = "UpdateWSLSuccess", +} + +export enum OVMWindowsAppEventValue { + UpdatingRootFS = "UpdatingRootFS", + UpdateRootFSFailed = "UpdateRootFSFailed", + UpdateRootFSSuccess = "UpdateRootFSSuccess", + + UpdatingData = "UpdatingData", + UpdateDataFailed = "UpdateDataFailed", + UpdateDataSuccess = "UpdateDataSuccess", + + Starting = "Starting", + Ready = "Ready", + Exit = "Exit", +} + +export interface OVMWindowsEventData { + app: OVMWindowsAppEventValue, + sys: OVMWindowsSysEventValue, + error: string, +} + diff --git a/src/windows.ts b/src/windows.ts new file mode 100644 index 0000000..c5c1d01 --- /dev/null +++ b/src/windows.ts @@ -0,0 +1,79 @@ +import cp from "node:child_process"; +import type { EventReceiver } from "remitter"; +import { Remitter } from "remitter"; +import type { OVMWindowsEventData, OVMWindowsOptions } from "./type"; +import { Restful } from "./event_restful"; +import { RequestWindows } from "./request"; + +export class WindowsOVM extends RequestWindows { + public readonly events : EventReceiver; + readonly #events: Remitter; + private restful: Restful; + private readonly restfulNPipeName: string; + + private constructor(private options: OVMWindowsOptions) { + super(options.name); + this.restfulNPipeName = `ovm-${options.name}-restful`; + this.events = this.#events = new Remitter(); + } + + public static create(options: OVMWindowsOptions): WindowsOVM { + const ovm = new WindowsOVM(options); + ovm.initEventRestful(); + return ovm; + } + + private initEventRestful(): void { + this.restful = new Restful(); + + this.#events.remitAny((o) => { + return this.restful.events.onAny((data) => { + o.emit(data.event as keyof OVMWindowsEventData, data.data); + }); + }); + + this.restful.start(`//./pipe/${this.restfulNPipeName}`); + } + + public start(): void { + const versions = Object.keys(this.options.versions).map((key) => { + return `${key}=${this.options.versions[key]}`; + }).join(","); + + const launchTimeout = new Promise((resolve, reject) => { + const id = setTimeout(() => { + disposer(); + // eslint-disable-next-line prefer-promise-reject-errors + reject(); + }, 30 * 1000); + + const disposer = this.#events.onceAny(() => { + clearTimeout(id); + resolve(); + }); + }); + + const ovm = cp.spawn(this.options.ovmPath, [ + "-name", this.options.name, + "-log-path", this.options.logDir, + "-image-dir", this.options.imageDir, + "-rootfs-path", this.options.linuxPath.rootfs, + "-versions", versions, + "-event-npipe-name", this.restfulNPipeName, + "-bind-pid", String(process.pid), + ], { + timeout: 0, + windowsHide: true, + detached: true, + stdio: "ignore", + cwd: this.options.imageDir, + }); + + ovm.unref(); + + launchTimeout + .catch(() => { + this.#events.emit("error", "OVM start timeout"); + }); + } +}