Use an actual logger

This commit is contained in:
Morgan Dean
2025-06-12 20:27:28 -04:00
parent 3cdc08dabd
commit 75c7a8bbc1
5 changed files with 283 additions and 178 deletions

View File

@@ -39,6 +39,7 @@
"prepublishOnly": "pnpm run build"
},
"dependencies": {
"pino": "^9.7.0",
"sharp": "^0.33.0"
},
"devDependencies": {

View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
pino:
specifier: ^9.7.0
version: 9.7.0
sharp:
specifier: ^0.33.0
version: 0.33.5
@@ -636,6 +639,10 @@ packages:
resolution: {integrity: sha512-ROM2LlXbZBZVk97crfw8PGDOBzzsJvN2uJCmwswvPUNyfH14eg90mSN3xNqsri1JS1G9cz0VzeDUhxJkTrr4Ew==}
engines: {node: '>=20.18.0'}
atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
birpc@2.3.0:
resolution: {integrity: sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g==}
@@ -758,6 +765,10 @@ packages:
exsolve@1.0.5:
resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
fast-redact@3.5.0:
resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
engines: {node: '>=6'}
fdir@6.4.6:
resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
peerDependencies:
@@ -828,6 +839,10 @@ packages:
ohash@2.0.11:
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
on-exit-leak-free@2.1.2:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
@@ -848,6 +863,16 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pino-abstract-transport@2.0.0:
resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==}
pino-std-serializers@7.0.0:
resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==}
pino@9.7.0:
resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==}
hasBin: true
pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
@@ -855,9 +880,15 @@ packages:
resolution: {integrity: sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==}
engines: {node: ^10 || ^12 || >=14}
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
quansync@0.2.10:
resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
@@ -865,6 +896,10 @@ packages:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
@@ -898,6 +933,10 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
safe-stable-stringify@2.5.0:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@@ -913,10 +952,17 @@ packages:
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
sonic-boom@4.2.0:
resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
@@ -926,6 +972,9 @@ packages:
strip-literal@3.0.0:
resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==}
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1514,6 +1563,8 @@ snapshots:
'@babel/parser': 7.27.5
pathe: 2.0.3
atomic-sleep@1.0.0: {}
birpc@2.3.0: {}
bumpp@10.1.1:
@@ -1647,6 +1698,8 @@ snapshots:
exsolve@1.0.5: {}
fast-redact@3.5.0: {}
fdir@6.4.6(picomatch@4.0.2):
optionalDependencies:
picomatch: 4.0.2
@@ -1706,6 +1759,8 @@ snapshots:
ohash@2.0.11: {}
on-exit-leak-free@2.1.2: {}
package-manager-detector@1.3.0: {}
pathe@2.0.3: {}
@@ -1718,6 +1773,26 @@ snapshots:
picomatch@4.0.2: {}
pino-abstract-transport@2.0.0:
dependencies:
split2: 4.2.0
pino-std-serializers@7.0.0: {}
pino@9.7.0:
dependencies:
atomic-sleep: 1.0.0
fast-redact: 3.5.0
on-exit-leak-free: 2.1.2
pino-abstract-transport: 2.0.0
pino-std-serializers: 7.0.0
process-warning: 5.0.0
quick-format-unescaped: 4.0.4
real-require: 0.2.0
safe-stable-stringify: 2.5.0
sonic-boom: 4.2.0
thread-stream: 3.1.0
pkg-types@2.1.0:
dependencies:
confbox: 0.2.2
@@ -1730,8 +1805,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
process-warning@5.0.0: {}
quansync@0.2.10: {}
quick-format-unescaped@4.0.4: {}
rc9@2.1.2:
dependencies:
defu: 6.1.4
@@ -1739,6 +1818,8 @@ snapshots:
readdirp@4.1.2: {}
real-require@0.2.0: {}
resolve-pkg-maps@1.0.0: {}
rolldown-plugin-dts@0.13.11(rolldown@1.0.0-beta.9)(typescript@5.8.3):
@@ -1803,6 +1884,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.43.0
fsevents: 2.3.3
safe-stable-stringify@2.5.0: {}
semver@7.7.2: {}
sharp@0.33.5:
@@ -1837,8 +1920,14 @@ snapshots:
dependencies:
is-arrayish: 0.3.2
sonic-boom@4.2.0:
dependencies:
atomic-sleep: 1.0.0
source-map-js@1.2.1: {}
split2@4.2.0: {}
stackback@0.0.2: {}
std-env@3.9.0: {}
@@ -1847,6 +1936,10 @@ snapshots:
dependencies:
js-tokens: 9.0.1
thread-stream@3.1.0:
dependencies:
real-require: 0.2.0
tinybench@2.9.0: {}
tinyexec@0.3.2: {}

View File

@@ -1,16 +1,25 @@
import type { Display, ComputerConfig } from '../models';
import type { BaseComputerInterface } from '../interface/base';
import { InterfaceFactory } from '../interface/factory';
import type { BaseVMProvider } from '../providers/base';
import { VMProviderType } from '../providers/base';
import { VMProviderFactory } from '../providers/factory';
import { Logger, LogLevel } from '../logger';
import { recordComputerInitialization, recordVMStart, recordVMStop } from '../telemetry';
import { setDefaultComputer } from '../helpers';
import { parseDisplayString, parseImageString, sleep, withTimeout } from '../utils';
import sharp from 'sharp';
import type { Display, ComputerConfig } from "../models";
import type { BaseComputerInterface } from "../interface/base";
import { InterfaceFactory } from "../interface/factory";
import type { BaseVMProvider } from "../providers/base";
import { VMProviderType } from "../providers/base";
import { VMProviderFactory } from "../providers/factory";
import pino from "pino";
import {
recordComputerInitialization,
recordVMStart,
recordVMStop,
} from "../telemetry";
import { setDefaultComputer } from "../helpers";
import {
parseDisplayString,
parseImageString,
sleep,
withTimeout,
} from "../utils";
import sharp from "sharp";
export type OSType = 'macos' | 'linux' | 'windows';
export type OSType = "macos" | "linux" | "windows";
export interface ComputerOptions {
display?: Display | { width: number; height: number } | string;
@@ -21,7 +30,7 @@ export interface ComputerOptions {
image?: string;
sharedDirectories?: string[];
useHostComputerServer?: boolean;
verbosity?: number | LogLevel;
verbosity?: pino.Level;
telemetryEnabled?: boolean;
providerType?: VMProviderType | string;
port?: number;
@@ -37,10 +46,8 @@ export interface ComputerOptions {
* Computer is the main class for interacting with the computer.
*/
export class Computer {
private logger: Logger;
private vmLogger: Logger;
private interfaceLogger: Logger;
private logger: pino.Logger;
private image: string;
private port?: number;
private noVNCPort?: number;
@@ -56,7 +63,7 @@ export class Computer {
private _telemetryEnabled: boolean;
private _initialized: boolean = false;
private _running: boolean = false;
private verbosity: number | LogLevel;
private useHostComputerServer: boolean;
private config?: ComputerConfig;
private _providerContext?: BaseVMProvider;
@@ -71,29 +78,28 @@ export class Computer {
*/
constructor(options: ComputerOptions = {}) {
const {
display = '1024x768',
memory = '8GB',
cpu = '4',
osType = 'macos',
name = '',
image = 'macos-sequoia-cua:latest',
display = "1024x768",
memory = "8GB",
cpu = "4",
osType = "macos",
name = "",
image = "macos-sequoia-cua:latest",
sharedDirectories = [],
useHostComputerServer = false,
verbosity = LogLevel.NORMAL,
verbosity = "info",
telemetryEnabled = true,
providerType = VMProviderType.LUME,
port = 7777,
noVNCPort = 8006,
host = process.env.PYLUME_HOST || 'localhost',
host = process.env.PYLUME_HOST || "localhost",
storage,
ephemeral = false,
apiKey,
experiments = []
experiments = [],
} = options;
this.verbosity = verbosity;
this.logger = new Logger('cua.computer', verbosity);
this.logger.info('Initializing Computer...');
this.logger = pino({ name: "cua.computer", level: verbosity });
this.logger.info("Initializing Computer...");
// Store original parameters
this.image = image;
@@ -106,46 +112,46 @@ export class Computer {
this.apiKey = apiKey;
this.experiments = experiments;
if (this.experiments.includes('app-use')) {
if (this.osType !== 'macos') {
throw new Error('App use experiment is only supported on macOS');
if (this.experiments.includes("app-use")) {
if (this.osType !== "macos") {
throw new Error("App use experiment is only supported on macOS");
}
}
// The default is currently to use non-ephemeral storage
if (storage && ephemeral && storage !== 'ephemeral') {
throw new Error('Storage path and ephemeral flag cannot be used together');
if (storage && ephemeral && storage !== "ephemeral") {
throw new Error(
"Storage path and ephemeral flag cannot be used together"
);
}
this.storage = ephemeral ? 'ephemeral' : storage;
this.storage = ephemeral ? "ephemeral" : storage;
// For Lumier provider, store the first shared directory path to use
// for VM file sharing
this.sharedPath = undefined;
if (sharedDirectories && sharedDirectories.length > 0) {
this.sharedPath = sharedDirectories[0];
this.logger.info(`Using first shared directory for VM file sharing: ${this.sharedPath}`);
this.logger.info(
`Using first shared directory for VM file sharing: ${this.sharedPath}`
);
}
// Store telemetry preference
this._telemetryEnabled = telemetryEnabled;
// Configure component loggers with proper hierarchy
this.vmLogger = new Logger('cua.vm', verbosity);
this.interfaceLogger = new Logger('cua.interface', verbosity);
this.useHostComputerServer = useHostComputerServer;
if (!useHostComputerServer) {
const imageInfo = parseImageString(image);
const vmName = name || image.replace(':', '_');
const vmName = name || image.replace(":", "_");
// Convert display parameter to Display object
let displayConfig: Display;
if (typeof display === 'string') {
if (typeof display === "string") {
const { width, height } = parseDisplayString(display);
displayConfig = { width, height };
} else if ('width' in display && 'height' in display) {
} else if ("width" in display && "height" in display) {
displayConfig = display as Display;
} else {
displayConfig = display as Display;
@@ -168,7 +174,9 @@ export class Computer {
if (telemetryEnabled) {
recordComputerInitialization();
} else {
this.logger.debug('Telemetry disabled - skipping initialization tracking');
this.logger.debug(
"Telemetry disabled - skipping initialization tracking"
);
}
}
@@ -180,11 +188,13 @@ export class Computer {
* @returns A proxy object with the Diorama interface, but using diorama_cmds.
*/
createDesktopFromApps(apps: string[]): any {
if (!this.experiments.includes('app-use')) {
throw new Error("App Usage is an experimental feature. Enable it by passing experiments=['app-use'] to Computer()");
if (!this.experiments.includes("app-use")) {
throw new Error(
"App Usage is an experimental feature. Enable it by passing experiments=['app-use'] to Computer()"
);
}
// DioramaComputer would be imported and used here
throw new Error('DioramaComputer not yet implemented');
throw new Error("DioramaComputer not yet implemented");
}
/**
@@ -208,11 +218,11 @@ export class Computer {
async run(): Promise<string | undefined> {
// If already initialized, just log and return
if (this._initialized) {
this.logger.info('Computer already initialized, skipping initialization');
this.logger.info("Computer already initialized, skipping initialization");
return;
}
this.logger.info('Starting computer...');
this.logger.info("Starting computer...");
const startTime = Date.now();
try {
@@ -220,29 +230,32 @@ export class Computer {
// If using host computer server
if (this.useHostComputerServer) {
this.logger.info('Using host computer server');
ipAddress = 'localhost';
this.logger.info("Using host computer server");
ipAddress = "localhost";
// Create the interface
this._interface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress
);
this.logger.info('Waiting for host computer server to be ready...');
this.logger.info("Waiting for host computer server to be ready...");
await this._interface.waitForReady();
this.logger.info('Host computer server ready');
this.logger.info("Host computer server ready");
} else {
// Start or connect to VM
this.logger.info(`Starting VM: ${this.image}`);
if (!this._providerContext) {
try {
const providerTypeName = typeof this.providerType === 'object'
? this.providerType
: this.providerType;
this.logger.verbose(`Initializing ${providerTypeName} provider context...`);
const providerTypeName =
typeof this.providerType === "object"
? this.providerType
: this.providerType;
this.logger.info(
`Initializing ${providerTypeName} provider context...`
);
// Create VM provider instance with explicit parameters
const providerOptions = {
@@ -251,14 +264,15 @@ export class Computer {
storage: this.storage,
sharedPath: this.sharedPath,
image: this.image,
verbose: this.verbosity >= LogLevel.DEBUG,
verbose:
this.logger.level === "debug" || this.logger.level === "trace",
ephemeral: this.ephemeral,
noVNCPort: this.noVNCPort,
apiKey: this.apiKey
apiKey: this.apiKey,
};
if (!this.config) {
throw new Error('Computer config not initialized');
throw new Error("Computer config not initialized");
}
this.config.vm_provider = await VMProviderFactory.createProvider(
@@ -267,33 +281,38 @@ export class Computer {
);
this._providerContext = await this.config.vm_provider.__aenter__();
this.logger.verbose('VM provider context initialized successfully');
this.logger.debug("VM provider context initialized successfully");
} catch (error) {
this.logger.error(`Failed to import provider dependencies: ${error}`);
this.logger.error(
`Failed to import provider dependencies: ${error}`
);
throw error;
}
}
// Run the VM
if (!this.config || !this.config.vm_provider) {
throw new Error('VM provider not initialized');
throw new Error("VM provider not initialized");
}
const runOpts = {
display: this.config.display,
memory: this.config.memory,
cpu: this.config.cpu,
shared_directories: this.sharedDirectories
shared_directories: this.sharedDirectories,
};
this.logger.info(`Running VM ${this.config.name} with options:`, runOpts);
this.logger.info(
`Running VM ${this.config.name} with options:`,
runOpts
);
if (this._telemetryEnabled) {
recordVMStart(this.config.name, String(this.providerType));
}
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
const storageParam = this.ephemeral ? "ephemeral" : this.storage;
try {
await this.config.vm_provider.runVM(
this.image,
@@ -302,7 +321,7 @@ export class Computer {
storageParam
);
} catch (error: any) {
if (error.message?.includes('already running')) {
if (error.message?.includes("already running")) {
this.logger.info(`VM ${this.config.name} is already running`);
} else {
throw error;
@@ -311,9 +330,9 @@ export class Computer {
// Wait for VM to be ready
try {
this.logger.info('Waiting for VM to be ready...');
this.logger.info("Waiting for VM to be ready...");
await this.waitVMReady();
// Get IP address
ipAddress = await this.getIP();
this.logger.info(`VM is ready with IP: ${ipAddress}`);
@@ -326,14 +345,22 @@ export class Computer {
// Initialize the interface
try {
// Verify we have a valid IP before initializing the interface
if (!ipAddress || ipAddress === 'unknown' || ipAddress === '0.0.0.0') {
throw new Error(`Cannot initialize interface - invalid IP address: ${ipAddress}`);
if (!ipAddress || ipAddress === "unknown" || ipAddress === "0.0.0.0") {
throw new Error(
`Cannot initialize interface - invalid IP address: ${ipAddress}`
);
}
this.logger.info(`Initializing interface for ${this.osType} at ${ipAddress}`);
this.logger.info(
`Initializing interface for ${this.osType} at ${ipAddress}`
);
// Pass authentication credentials if using cloud provider
if (this.providerType === VMProviderType.CLOUD && this.apiKey && this.config?.name) {
if (
this.providerType === VMProviderType.CLOUD &&
this.apiKey &&
this.config?.name
) {
this._interface = InterfaceFactory.createInterfaceForOS(
this.osType,
ipAddress,
@@ -348,7 +375,7 @@ export class Computer {
}
// Wait for the WebSocket interface to be ready
this.logger.info('Connecting to WebSocket interface...');
this.logger.info("Connecting to WebSocket interface...");
try {
await withTimeout(
@@ -356,9 +383,11 @@ export class Computer {
30000,
`Could not connect to WebSocket interface at ${ipAddress}:8000/ws`
);
this.logger.info('WebSocket interface connected successfully');
this.logger.info("WebSocket interface connected successfully");
} catch (error) {
this.logger.error(`Failed to connect to WebSocket interface at ${ipAddress}`);
this.logger.error(
`Failed to connect to WebSocket interface at ${ipAddress}`
);
throw error;
}
@@ -366,13 +395,13 @@ export class Computer {
if (!this.useHostComputerServer) {
// In TypeScript, we'll use a Promise instead of asyncio.Event
let resolveStop: () => void;
this._stopEvent = new Promise<void>(resolve => {
this._stopEvent = new Promise<void>((resolve) => {
resolveStop = resolve;
});
this._keepAliveTask = this._stopEvent;
}
this.logger.info('Computer is ready');
this.logger.info("Computer is ready");
// Set the initialization flag
this._initialized = true;
@@ -380,13 +409,15 @@ export class Computer {
// Set this instance as the default computer for remote decorators
setDefaultComputer(this);
this.logger.info('Computer successfully initialized');
this.logger.info("Computer successfully initialized");
} catch (error) {
throw error;
} finally {
// Log initialization time for performance monitoring
const durationMs = Date.now() - startTime;
this.logger.debug(`Computer initialization took ${durationMs.toFixed(2)}ms`);
this.logger.debug(
`Computer initialization took ${durationMs.toFixed(2)}ms`
);
}
} catch (error) {
this.logger.error(`Failed to initialize computer: ${error}`);
@@ -413,34 +444,37 @@ export class Computer {
const startTime = Date.now();
try {
this.logger.info('Stopping Computer...');
this.logger.info("Stopping Computer...");
// In VM mode, first explicitly stop the VM, then exit the provider context
if (!this.useHostComputerServer && this._providerContext && this.config?.vm_provider) {
if (
!this.useHostComputerServer &&
this._providerContext &&
this.config?.vm_provider
) {
try {
this.logger.info(`Stopping VM ${this.config.name}...`);
await this.config.vm_provider.stopVM(
this.config.name,
this.storage
);
await this.config.vm_provider.stopVM(this.config.name, this.storage);
} catch (error) {
this.logger.error(`Error stopping VM: ${error}`);
}
this.logger.verbose('Closing VM provider context...');
this.logger.info("Closing VM provider context...");
await this.config.vm_provider.__aexit__(null, null, null);
this._providerContext = undefined;
}
await this.disconnect();
this.logger.info('Computer stopped');
this.logger.info("Computer stopped");
} catch (error) {
this.logger.debug(`Error during cleanup: ${error}`);
} finally {
// Log stop time for performance monitoring
const durationMs = Date.now() - startTime;
this.logger.debug(`Computer stop process took ${durationMs.toFixed(2)}ms`);
this.logger.debug(
`Computer stop process took ${durationMs.toFixed(2)}ms`
);
if (this._telemetryEnabled && this.config?.name) {
recordVMStop(this.config.name, durationMs);
}
@@ -450,22 +484,27 @@ export class Computer {
/**
* Get the IP address of the VM or localhost if using host computer server.
*/
async getIP(maxRetries: number = 15, retryDelay: number = 2): Promise<string> {
async getIP(
maxRetries: number = 15,
retryDelay: number = 2
): Promise<string> {
// For host computer server, always return localhost immediately
if (this.useHostComputerServer) {
return '127.0.0.1';
return "127.0.0.1";
}
// Get IP from the provider
if (!this.config?.vm_provider) {
throw new Error('VM provider is not initialized');
throw new Error("VM provider is not initialized");
}
// Log that we're waiting for the IP
this.logger.info(`Waiting for VM ${this.config.name} to get an IP address...`);
this.logger.info(
`Waiting for VM ${this.config.name} to get an IP address...`
);
// Call the provider's get_ip method which will wait indefinitely
const storageParam = this.ephemeral ? 'ephemeral' : this.storage;
const storageParam = this.ephemeral ? "ephemeral" : this.storage;
// Log the image being used
this.logger.info(`Running VM using image: ${this.image}`);
@@ -496,16 +535,18 @@ export class Computer {
let lastStatus: string | undefined;
let attempts = 0;
this.logger.info(`Waiting for VM ${this.config?.name} to be ready (timeout: ${timeout}s)...`);
this.logger.info(
`Waiting for VM ${this.config?.name} to be ready (timeout: ${timeout}s)...`
);
while ((Date.now() / 1000) - startTime < timeout) {
while (Date.now() / 1000 - startTime < timeout) {
attempts++;
const elapsed = (Date.now() / 1000) - startTime;
const elapsed = Date.now() / 1000 - startTime;
try {
// Keep polling for VM info
if (!this.config?.vm_provider) {
this.logger.error('VM provider is not initialized');
this.logger.error("VM provider is not initialized");
return undefined;
}
@@ -522,14 +563,23 @@ export class Computer {
const currentStatus = vm?.status;
if (currentStatus !== lastStatus) {
this.logger.info(
`VM status changed to: ${currentStatus} (after ${elapsed.toFixed(1)}s)`
`VM status changed to: ${currentStatus} (after ${elapsed.toFixed(
1
)}s)`
);
lastStatus = currentStatus;
}
// Check if VM is ready
if (vm && vm.status === 'running' && vm.ip_address && vm.ip_address !== '0.0.0.0') {
this.logger.info(`VM ${this.config.name} is ready with IP: ${vm.ip_address}`);
if (
vm &&
vm.status === "running" &&
vm.ip_address &&
vm.ip_address !== "0.0.0.0"
) {
this.logger.info(
`VM ${this.config.name} is ready with IP: ${vm.ip_address}`
);
return vm;
}
@@ -541,7 +591,9 @@ export class Computer {
}
}
throw new Error(`VM ${this.config?.name} failed to become ready within ${timeout} seconds`);
throw new Error(
`VM ${this.config?.name} failed to become ready within ${timeout} seconds`
);
}
/**
@@ -549,12 +601,12 @@ export class Computer {
*/
async update(cpu?: number, memory?: string): Promise<void> {
if (this.useHostComputerServer) {
this.logger.warning('Cannot update settings for host computer server');
this.logger.warn("Cannot update settings for host computer server");
return;
}
if (!this.config?.vm_provider) {
throw new Error('VM provider is not initialized');
throw new Error("VM provider is not initialized");
}
await this.config.vm_provider.updateVM(
@@ -568,11 +620,13 @@ export class Computer {
/**
* Get the dimensions of a screenshot.
*/
async getScreenshotSize(screenshot: Buffer): Promise<{ width: number; height: number }> {
async getScreenshotSize(
screenshot: Buffer
): Promise<{ width: number; height: number }> {
const metadata = await sharp(screenshot).metadata();
return {
width: metadata.width || 0,
height: metadata.height || 0
height: metadata.height || 0,
};
}
@@ -581,7 +635,7 @@ export class Computer {
*/
get interface(): BaseComputerInterface {
if (!this._interface) {
throw new Error('Computer interface not initialized. Call run() first.');
throw new Error("Computer interface not initialized. Call run() first.");
}
return this._interface;
}
@@ -598,18 +652,18 @@ export class Computer {
*/
toScreenCoordinates(x: number, y: number): [number, number] {
if (!this.config?.display) {
throw new Error('Display configuration not available');
throw new Error("Display configuration not available");
}
return [
x * this.config.display.width,
y * this.config.display.height
];
return [x * this.config.display.width, y * this.config.display.height];
}
/**
* Convert screen coordinates to screenshot coordinates.
*/
async toScreenshotCoordinates(x: number, y: number): Promise<[number, number]> {
async toScreenshotCoordinates(
x: number,
y: number
): Promise<[number, number]> {
// In the Python version, this uses the interface to get screenshot dimensions
// For now, we'll assume 1:1 mapping
return [x, y];
@@ -618,10 +672,13 @@ export class Computer {
/**
* Install packages in a virtual environment.
*/
async venvInstall(venvName: string, requirements: string[]): Promise<[string, string]> {
async venvInstall(
venvName: string,
requirements: string[]
): Promise<[string, string]> {
// This would be implemented using the interface to run commands
// TODO: Implement venvInstall
throw new Error('venvInstall not yet implemented');
throw new Error("venvInstall not yet implemented");
}
/**
@@ -630,15 +687,19 @@ export class Computer {
async venvCmd(venvName: string, command: string): Promise<[string, string]> {
// This would be implemented using the interface to run commands
// TODO: Implement venvCmd
throw new Error('venvCmd not yet implemented');
throw new Error("venvCmd not yet implemented");
}
/**
* Execute function in a virtual environment using source code extraction.
*/
async venvExec(venvName: string, pythonFunc: Function, ...args: any[]): Promise<any> {
async venvExec(
venvName: string,
pythonFunc: Function,
...args: any[]
): Promise<any> {
// This would be implemented using the interface to run Python code
// TODO: Implement venvExec
throw new Error('venvExec not yet implemented');
throw new Error("venvExec not yet implemented");
}
}

View File

@@ -1,50 +0,0 @@
/**
* Logger implementation for the Computer library.
*/
export enum LogLevel {
DEBUG = 10,
VERBOSE = 15,
INFO = 20,
NORMAL = 20,
WARNING = 30,
ERROR = 40,
}
export class Logger {
private name: string;
private verbosity: number;
constructor(name: string, verbosity: number | LogLevel = LogLevel.NORMAL) {
this.name = name;
this.verbosity = typeof verbosity === 'number' ? verbosity : verbosity;
}
private log(level: LogLevel, message: string, ...args: any[]): void {
if (level >= this.verbosity) {
const timestamp = new Date().toISOString();
const levelName = LogLevel[level];
console.log(`[${timestamp}] [${this.name}] [${levelName}] ${message}`, ...args);
}
}
debug(message: string, ...args: any[]): void {
this.log(LogLevel.DEBUG, message, ...args);
}
info(message: string, ...args: any[]): void {
this.log(LogLevel.INFO, message, ...args);
}
verbose(message: string, ...args: any[]): void {
this.log(LogLevel.VERBOSE, message, ...args);
}
warning(message: string, ...args: any[]): void {
this.log(LogLevel.WARNING, message, ...args);
}
error(message: string, ...args: any[]): void {
this.log(LogLevel.ERROR, message, ...args);
}
}

View File

@@ -73,8 +73,8 @@ export class LumierProvider extends BaseVMProviderImpl {
const match = memoryStr.match(/^(\d+)([A-Za-z]*)$/);
if (match) {
const value = parseInt(match[1] || "0");
const unit = match[2]?.toUpperCase() || "";
const value = parseInt(match[1]);
const unit = match[2].toUpperCase();
if (unit === "GB" || unit === "G") {
return value * 1024;