utils.ts•7.24 kB
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import type { Runtime } from '../manager/types';
import { isConnectionRefusedError } from './errors';
import { logger } from './logger';
export interface DevToolsInfo {
/** URL of the dev tools server. */
url: string;
/** Timestamp of when the dev tools server was started. */
timestamp: string;
}
/**
* Finds the project root by looking for a `package.json` file.
*/
export async function findProjectRoot(): Promise<string> {
let currentDir = process.cwd();
while (currentDir !== path.parse(currentDir).root) {
const packageJsonPath = path.join(currentDir, 'package.json');
const goModPath = path.join(currentDir, 'go.mod');
const pyprojectPath = path.join(currentDir, 'pyproject.toml');
const pyproject2Path = path.join(currentDir, 'requirements.txt');
try {
const [
packageJsonExists,
goModExists,
pyprojectExists,
pyproject2Exists,
] = await Promise.all([
fs
.access(packageJsonPath)
.then(() => true)
.catch(() => false),
fs
.access(goModPath)
.then(() => true)
.catch(() => false),
fs
.access(pyprojectPath)
.then(() => true)
.catch(() => false),
fs
.access(pyproject2Path)
.then(() => true)
.catch(() => false),
]);
if (
packageJsonExists ||
goModExists ||
pyprojectExists ||
pyproject2Exists
) {
return currentDir;
}
} catch {
// Continue searching if any errors occur
}
currentDir = path.dirname(currentDir);
}
return process.cwd();
}
/**
* Finds the Genkit hidden directory containing runtime state files.
*/
export async function findRuntimesDir(projectRoot: string): Promise<string> {
return path.join(projectRoot, '.genkit', 'runtimes');
}
/**
* Finds the Genkit hidden directory containing server (UI server, telemetry server, etc) state files.
*/
export async function findServersDir(projectRoot: string): Promise<string> {
return path.join(projectRoot, '.genkit', 'servers');
}
/**
* Extract project name (i.e. basename of the project root folder) from the
* path to a runtime file.
*
* e.g.) /path/to/<basename>/.genkit/runtimes/123.json returns <basename>.
*
* @param filePath path to a runtime file
* @returns project name
*/
export function projectNameFromGenkitFilePath(filePath: string): string {
const parts = filePath.split('/');
const basePath = parts
.slice(
0,
Math.max(
parts.findIndex((value) => value === '.genkit'),
0
)
)
.join('/');
return basePath === '' ? 'unknown' : path.basename(basePath);
}
/**
* Detects what runtime is used in the current directory.
* @returns Runtime of the project directory.
*/
export async function detectRuntime(directory: string): Promise<Runtime> {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isFile() && (path.extname(file) === '.go' || file === 'go.mod')) {
return 'go';
}
}
try {
await fs.access(path.join(directory, 'package.json'));
return 'nodejs';
} catch {
return undefined;
}
}
/**
* Checks the health of a server with a /api/__health endpoint.
*/
export async function checkServerHealth(url: string): Promise<boolean> {
try {
const response = await fetch(`${url}/api/__health`);
return response.status === 200;
} catch (error) {
if (isConnectionRefusedError(error)) {
return false;
}
}
return true;
}
/**
* Waits until the server is healthy or the timeout is reached.
*/
export async function waitUntilHealthy(
url: string,
maxTimeout = 10000
): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < maxTimeout) {
try {
const response = await fetch(`${url}/api/__health`);
if (response.status === 200) {
return true;
}
} catch (error) {
// Ignore errors and continue retrying
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
/**
* Waits until the server becomes unresponsive or the timeout is reached.
*/
export async function waitUntilUnresponsive(
url: string,
maxTimeout = 10000
): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < maxTimeout) {
try {
const health = await fetch(`${url}/api/__health`);
} catch (error) {
if (isConnectionRefusedError(error)) {
return true;
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
return false;
}
export async function retriable<T>(
fn: () => Promise<T>,
opts: { maxRetries?: number; delayMs?: number }
): Promise<T> {
const maxRetries = opts.maxRetries ?? 3;
const delayMs = opts.delayMs ?? 0;
let attempt = 0;
while (true) {
try {
return await fn();
} catch (e) {
if (attempt >= maxRetries - 1) {
throw e;
}
if (delayMs > 0) {
await new Promise((r) => setTimeout(r, delayMs));
}
}
attempt++;
}
}
/**
* Checks if the provided data is a valid dev tools server state file.
*/
export function isValidDevToolsInfo(data: any): data is DevToolsInfo {
return (
typeof data === 'object' &&
typeof data.url === 'string' &&
typeof data.timestamp === 'string'
);
}
/**
* Writes the toolsInfo file to the project root
*/
export async function writeToolsInfoFile(url: string, projectRoot: string) {
const serversDir = await findServersDir(projectRoot);
const toolsJsonPath = path.join(serversDir, `tools-${process.pid}.json`);
try {
const serverInfo = {
url,
timestamp: new Date().toISOString(),
} as DevToolsInfo;
await fs.mkdir(serversDir, { recursive: true });
await fs.writeFile(toolsJsonPath, JSON.stringify(serverInfo, null, 2));
logger.debug(`Tools Info file written: ${toolsJsonPath}`);
} catch (error) {
logger.error('Error writing tools config', error);
}
}
/**
* Removes the toolsInfo file.
*/
export async function removeToolsInfoFile(
fileName: string,
projectRoot: string
) {
try {
const serversDir = await findServersDir(projectRoot);
const filePath = path.join(serversDir, fileName);
await fs.unlink(filePath);
logger.debug(`Removed unhealthy toolsInfo file ${fileName} from manager.`);
} catch (error) {
logger.debug(`Failed to delete toolsInfo file: ${error}`);
}
}