-
Tony Salomone authoredTony Salomone authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
util.ts 11.99 KiB
/* eslint import/prefer-default-export: off */
import { URL } from 'url';
import path from 'path';
const fs = require('fs');
const os = require('os');
const { spawn, exec, ChildProcess } = require('child_process');
const util = require('node:util');
const awaitExec = util.promisify(require('node:child_process').exec);
const homeDir = os.homedir();
const transformerLabRootDir = path.join(homeDir, '.transformerlab');
const transformerLabDir = path.join(transformerLabRootDir, 'src');
var localServer: typeof ChildProcess = null;
// Standardize how we decide if app is running on windows
function isPlatformWindows() {
return process.platform == 'win32';
}
// WINDOWS SPECIFIC FUNCTION for figuring out how to access WSL file system
// API and workspace are installed in .transformerlab/ under the user's homedir
// On Windows, we use the home directory on WSL file system.
// This outputs how to access the WSL file system homedir from Windows.
async function getWSLHomeDir() {
const { stdout, stderr } = await awaitExec('wsl wslpath -w ~');
if (stderr) console.error(`stderr: ${stderr}`);
const homedir = stdout.trim();
return homedir;
}
// Need to wrap directories in functions to cover the windows-specific case
async function getTransformerLabRootDir() {
return isPlatformWindows()
? path.join(await getWSLHomeDir(), '.transformerlab')
: transformerLabRootDir;
}
async function getTransformerLabCodeDir() {
return isPlatformWindows()
? path.join(await getTransformerLabRootDir(), 'src')
: transformerLabDir;
}
export function resolveHtmlPath(htmlFileName: string) {
if (process.env.NODE_ENV === 'development') {
const port = process.env.PORT || 1212;
const url = new URL(`http://localhost:${port}`);
url.pathname = htmlFileName;
return url.href;
}
return `file://${path.resolve(__dirname, '../renderer/', htmlFileName)}`;
}
// returns a string with the first encountered missing system requirement
// otherwise returns false
export async function checkForMissingSystemRequirements() {
const platform = process.platform;
switch (platform) {
case 'win32':
try {
// Try running WSL to check for version
// This may throw an exception if the WSL command is not availabile
// Otherwise, return any errors
const { stdout, stderr } = await awaitExec('wsl -l -v');
if (stderr) return stderr;
} catch (error) {
return 'TransformerLab API requires WSL to run on Windows.';
}
try {
// We will need to be able to use the wslpath utility
// This may not be available if the user has not installed WSL
const { stdout, stderr } = await getWSLHomeDir();
if (stderr) return stderr;
} catch (error) {
return "WSL file system unavailable: Is WSL installed (try 'wsl --install')?";
}
// Everything checks out OK!
return false;
// Currently nothing to check for on other platforms
default:
return false;
}
}
export async function checkLocalServerVersion() {
const mainFile = path.join(
await getTransformerLabCodeDir(),
'LATEST_VERSION'
);
console.log('Checking if server is installed locally at', mainFile);
if (fs.existsSync(mainFile)) {
let version = fs.readFileSync(mainFile, 'utf8');
// remove whitespace:
version = version.replace(/\s/g, '');
console.log('Found version', version);
return version;
} else {
return false;
}
}
export async function startLocalServer() {
const server_dir = await getTransformerLabCodeDir();
const logFilePath = path.join(server_dir, 'local_server.log');
const out = fs.openSync(logFilePath, 'a');
const err = fs.openSync(logFilePath, 'a');
// Need to call bash script through WSL on Windows
// Windows will not let you set a UNC directory to cwd
// Consequently, we have to make a cd call first
const exec_cmd = isPlatformWindows() ? 'wsl' : 'bash';
const exec_args = isPlatformWindows()
? ['cd', '~/.transformerlab/src/', '&&', './run.sh']
: ['-l', path.join(server_dir, 'run.sh')];
const options = isPlatformWindows()
? {
stdio: ['ignore', out, err],
}
: {
cwd: server_dir,
stdio: ['ignore', out, err],
shell: '/bin/bash',
};
localServer = spawn(exec_cmd, exec_args, options);
console.log('Local server started with pid', localServer.pid);
return new Promise((resolve) => {
let err_msg;
// if there was an error spawning then stderr will be null
if (localServer.stderr) {
localServer.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
}
localServer.on('error', (error_msg) => {
console.log(`child process failed: ${error_msg}`);
err_msg = error_msg;
});
localServer.on('close', (code) => {
console.log(`child process exited with code ${code}`);
if (code === 0) {
resolve({ status: 'success', code: code });
} else {
resolve({
status: 'error',
code: code,
message: `${err_msg} (code ${code}). Check log for details: ${logFilePath}`,
});
}
});
});
}
export function killLocalServer() {
return new Promise((resolve) => {
console.log('Killing local server if not NULL');
if (localServer) {
console.log(
`Killing local server with pid ${localServer.pid} and all it children`
);
var kill = require('tree-kill');
kill(localServer.pid, 'SIGTERM', function (err) {
console.log('Finished killing local server');
console.log(err);
resolve(err);
});
// localServer.kill();
} else {
resolve(null);
}
});
}
export async function installLocalServer() {
console.log('Installing local server');
const root_dir = await getTransformerLabRootDir();
if (!fs.existsSync(root_dir)) {
fs.mkdirSync(root_dir);
}
// We can download the API in one line for linux/mac
// but it's a little more complicated for windows, so call a bat file
console.log('Platform:' + process.platform);
const download_cmd = `curl https://raw.githubusercontent.com/transformerlab/transformerlab-api/main/install.sh | bash -s -- download_transformer_lab`;
const installScriptCommand = isPlatformWindows()
? `wsl ` + download_cmd
: download_cmd;
const options = isPlatformWindows()
? {}
: { shell: '/bin/bash', cwd: root_dir };
try {
const child = exec(
installScriptCommand,
options,
(error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
}
);
} catch (err) {
console.log('Failed to install local server', err);
}
}
export async function checkIfCondaBinExists() {
// Look for the conda directory inside .transformerlab
const root_dir = await getTransformerLabRootDir();
const condaBin = path.join(root_dir, 'miniconda3', 'bin', 'conda');
if (fs.existsSync(condaBin)) {
return true;
} else {
console.log('Conda not found at ' + condaBin);
return false;
}
}
export async function checkDependencies() {
// This function returns an API like response with status, message and data field
let response = {
status: '',
message: '',
data: [],
};
// check if we've done an install/update of dependencies with this build
// if not, report back that we need to do an install/update!
const installedDependenciesFile = path.join(
await getTransformerLabCodeDir(),
'INSTALLED_DEPENDENCIES'
);
if (!fs.existsSync(installedDependenciesFile)) {
response.status = 'error';
response.message = 'Dependencies need to be installed for new API version.';
return response;
}
const { error, stdout, stderr } = await executeInstallStep(
'list_installed_packages'
);
// if there was an error abort processing
if (error) {
response.status = 'error';
response.message = 'Failed to detect packages';
response.data = { stdout: '', stderr: stderr.toString() };
console.log('Failed to detect packages');
console.log(JSON.stringify(response));
return response;
}
// parse returned JSON in to pipList
let pipList = [];
try {
pipList = JSON.parse(stdout);
} catch (e) {
console.log(e);
response.status = 'error';
response.message = 'Failed to parse package list';
response.data = { stdout, stderr };
return response;
}
const pipListNames = pipList.map((x) => x.name);
const keyDependencies = [
'fastapi',
'pydantic',
'uvicorn',
'sentencepiece',
'torch',
'transformers',
'peft',
'packaging',
'fschat',
];
//compare the list of dependencies to the keyDependencies
let missingDependencies = [];
for (let i = 0; i < keyDependencies.length; i++) {
if (!pipListNames.includes(keyDependencies[i])) {
missingDependencies.push(keyDependencies[i]);
}
}
response.data = missingDependencies;
console.log('missingDependencies', missingDependencies);
if (missingDependencies.legnth > 0) {
response.status = 'error';
const missingList = missingDependencies.data?.join(', ');
response.message = `Missing dependencies including: ${missingList}...`;
} else {
response.status = 'success';
}
return response;
}
export async function checkIfCondaEnvironmentExists() {
console.log('Checking if Conda environment "transformerlab" exists');
const { error, stdout, stderr } = await executeInstallStep(
'list_environments'
);
let response = {
status: '',
message: '',
data: [],
};
console.log(JSON.stringify({ error, stdout, stderr }));
if (error) {
response.status = 'error';
response.message = 'Conda environment check failed.';
response.data = { stdout: stdout?.toString(), stderr: stderr.toString() };
console.log('Conda environment check failed.');
return response;
}
// search for the string "transformerlab" in the output AND check that the directory exists
// On windows we don't have the full WSL homedir path so just check the end of the string
const root_dir = await getTransformerLabRootDir();
const env_path = isPlatformWindows()
? '.transformerlab/envs/transformerlab'
: path.join(root_dir, 'envs', 'transformerlab');
if (
typeof stdout === 'string' &&
stdout.includes(env_path) &&
fs.existsSync(path.join(root_dir, 'envs', 'transformerlab'))
) {
response.status = 'success';
return response;
} else {
response.status = 'error';
response.message = 'Conda environment "transformerlab" not found.';
return false;
}
}
/**
*
* @param argument parameter to pass to install.sh
* @returns the stdout of the process or false on failure.
*/
export async function executeInstallStep(argument: string) {
const server_dir = await getTransformerLabCodeDir();
if (!fs.existsSync(server_dir)) {
console.log(
'Install step failed. TransformerLab directory has not been setup.'
);
return false;
}
const installScriptFilename = 'install.sh';
const fullInstallScriptPath = path.join(server_dir, installScriptFilename);
// Set installer script filename and options based on platform
// For windows this is a bit hacky...we need to pass a unix-style path to WSL
const exec_cmd = isPlatformWindows()
? `wsl ~/.transformerlab/src/${installScriptFilename} ${argument}`
: `${fullInstallScriptPath} ${argument}`;
const options = isPlatformWindows() ? {} : { cwd: server_dir };
console.log(`Running: ${exec_cmd}`);
// Call installer script and return stdout if it succeeds
let error, stdout, stderr;
try {
({ error, stdout, stderr } = await awaitExec(exec_cmd, options));
} catch (err) {
console.log('Failed to execute install step', err);
console.log(JSON.stringify(err));
return {
error: err?.code,
stdout: err?.stdout?.toString(),
stderr: err?.stderr?.toString(),
};
}
if (stdout) console.log(`${installScriptFilename} stdout:`, stdout);
if (stderr) console.error(`${installScriptFilename} stderr:`, stderr);
return { error, stdout, stderr };
}