"git@developer.sourcefind.cn:gaoqiong/migraphx.git" did not exist on "4b96da8d47cc2f0b4e45241aa877e91c17f36086"
Unverified Commit 5a7c6eca authored by liuzhe-lz's avatar liuzhe-lz Committed by GitHub
Browse files

update main.ts (#4662)

parent 3ac19db9
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT license. // Licensed under the MIT license.
import assert from 'assert'; import assert from 'assert/strict';
import os from 'os';
import path from 'path'; import path from 'path';
const API_ROOT_URL: string = '/api/v1/nni'; import type { NniManagerArgs } from 'common/globals/arguments';
let singleton: ExperimentStartupInfo | null = null; let singleton: ExperimentStartupInfo | null = null;
export class ExperimentStartupInfo { export class ExperimentStartupInfo {
public experimentId: string = ''; public experimentId: string;
public newExperiment: boolean = true; public newExperiment: boolean;
public basePort: number = -1; public basePort: number;
public initialized: boolean = false;
public logDir: string = ''; public logDir: string = '';
public logLevel: string = ''; public logLevel: string;
public readonly: boolean = false; public readonly: boolean;
public dispatcherPipe: string | null = null; public dispatcherPipe: string | null;
public platform: string = ''; public platform: string;
public urlprefix: string = ''; public urlprefix: string;
constructor( constructor(args: NniManagerArgs) {
newExperiment: boolean, this.experimentId = args.experimentId;
experimentId: string, this.newExperiment = (args.action === 'create');
basePort: number, this.basePort = args.port;
platform: string, this.logDir = path.join(args.experimentsDirectory, args.experimentId); // TODO: handle in globals
logDir?: string, this.logLevel = args.logLevel;
logLevel?: string, this.readonly = (args.action === 'view');
readonly?: boolean, this.dispatcherPipe = args.dispatcherPipe ?? null;
dispatcherPipe?: string, this.platform = args.mode as string;
urlprefix?: string) { this.urlprefix = args.urlPrefix;
this.newExperiment = newExperiment;
this.experimentId = experimentId;
this.basePort = basePort;
this.platform = platform;
if (logDir !== undefined && logDir.length > 0) {
this.logDir = path.join(path.normalize(logDir), experimentId);
} else {
this.logDir = path.join(os.homedir(), 'nni-experiments', experimentId);
}
if (logLevel !== undefined && logLevel.length > 1) {
this.logLevel = logLevel;
}
if (readonly !== undefined) {
this.readonly = readonly;
}
if (dispatcherPipe != undefined && dispatcherPipe.length > 0) {
this.dispatcherPipe = dispatcherPipe;
}
if(urlprefix != undefined && urlprefix.length > 0){
this.urlprefix = urlprefix;
}
}
public get apiRootUrl(): string {
return this.urlprefix === '' ? API_ROOT_URL : `/${this.urlprefix}${API_ROOT_URL}`;
} }
public static getInstance(): ExperimentStartupInfo { public static getInstance(): ExperimentStartupInfo {
assert(singleton !== null); assert.notEqual(singleton, null);
return singleton!; return singleton!;
} }
} }
...@@ -74,27 +42,8 @@ export function getExperimentStartupInfo(): ExperimentStartupInfo { ...@@ -74,27 +42,8 @@ export function getExperimentStartupInfo(): ExperimentStartupInfo {
return ExperimentStartupInfo.getInstance(); return ExperimentStartupInfo.getInstance();
} }
export function setExperimentStartupInfo( export function setExperimentStartupInfo(args: NniManagerArgs): void {
newExperiment: boolean, singleton = new ExperimentStartupInfo(args);
experimentId: string,
basePort: number,
platform: string,
logDir?: string,
logLevel?: string,
readonly?: boolean,
dispatcherPipe?: string,
urlprefix?: string): void {
singleton = new ExperimentStartupInfo(
newExperiment,
experimentId,
basePort,
platform,
logDir,
logLevel,
readonly,
dispatcherPipe,
urlprefix
);
} }
export function getExperimentId(): string { export function getExperimentId(): string {
...@@ -120,12 +69,3 @@ export function isReadonly(): boolean { ...@@ -120,12 +69,3 @@ export function isReadonly(): boolean {
export function getDispatcherPipe(): string | null { export function getDispatcherPipe(): string | null {
return getExperimentStartupInfo().dispatcherPipe; return getExperimentStartupInfo().dispatcherPipe;
} }
export function getAPIRootUrl(): string {
return getExperimentStartupInfo().apiRootUrl;
}
export function getPrefixUrl(): string {
const prefix = getExperimentStartupInfo().urlprefix === '' ? '' : `/${getExperimentStartupInfo().urlprefix}`;
return prefix;
}
...@@ -104,18 +104,6 @@ function randomSelect<T>(a: T[]): T { ...@@ -104,18 +104,6 @@ function randomSelect<T>(a: T[]): T {
return a[Math.floor(Math.random() * a.length)]; return a[Math.floor(Math.random() * a.length)];
} }
function parseArg(names: string[]): string {
if (process.argv.length >= 4) {
for (let i: number = 2; i < process.argv.length - 1; i++) {
if (names.includes(process.argv[i])) {
return process.argv[i + 1];
}
}
}
return '';
}
function getCmdPy(): string { function getCmdPy(): string {
let cmd = 'python3'; let cmd = 'python3';
if (process.platform === 'win32') { if (process.platform === 'win32') {
...@@ -165,9 +153,17 @@ function prepareUnitTest(): void { ...@@ -165,9 +153,17 @@ function prepareUnitTest(): void {
Container.snapshot(Manager); Container.snapshot(Manager);
Container.snapshot(ExperimentManager); Container.snapshot(ExperimentManager);
const logLevel: string = parseArg(['--log_level', '-ll']); setExperimentStartupInfo({
port: 8080,
setExperimentStartupInfo(true, 'unittest', 8080, 'unittest', undefined, logLevel); experimentId: 'unittest',
action: 'create',
experimentsDirectory: path.join(os.homedir(), 'nni-experiments'),
logLevel: 'info',
foreground: false,
urlPrefix: '',
mode: 'unittest',
dispatcherPipe: undefined,
});
mkDirPSync(getLogDir()); mkDirPSync(getLogDir());
const sqliteFile: string = path.join(getDefaultDatabaseDir(), 'nni.sqlite'); const sqliteFile: string = path.join(getDefaultDatabaseDir(), 'nni.sqlite');
...@@ -188,8 +184,6 @@ function cleanupUnitTest(): void { ...@@ -188,8 +184,6 @@ function cleanupUnitTest(): void {
Container.restore(DataStore); Container.restore(DataStore);
Container.restore(Database); Container.restore(Database);
Container.restore(ExperimentManager); Container.restore(ExperimentManager);
const logLevel: string = parseArg(['--log_level', '-ll']);
setExperimentStartupInfo(true, 'unittest', 8080, 'unittest', undefined, logLevel);
} }
let cachedIpv4Address: string | null = null; let cachedIpv4Address: string | null = null;
...@@ -434,5 +428,5 @@ export function importModule(modulePath: string): any { ...@@ -434,5 +428,5 @@ export function importModule(modulePath: string): any {
export { export {
countFilesRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir, getExperimentsInfoPath, countFilesRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir, getExperimentsInfoPath,
getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin, withLockSync, getFreePort, isPortOpen, getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin, withLockSync, getFreePort, isPortOpen,
mkDirP, mkDirPSync, delay, prepareUnitTest, parseArg, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getCmdPy, getTunerProc, isAlive, killPid, getNewLine mkDirP, mkDirPSync, delay, prepareUnitTest, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getCmdPy, getTunerProc, isAlive, killPid, getNewLine
}; };
// Copyright (c) Microsoft Corporation. // Copyright (c) Microsoft Corporation.
// Licensed under the MIT license. // Licensed under the MIT license.
import 'app-module-path/register'; /**
* Entry point of NNI manager.
*
* NNI manager is normally started by "nni/experiment/launcher.py".
* It requires command line arguments defined as NniManagerArgs in "common/globals/arguments.ts".
*
* Example usage:
*
* node main.js \
* --port 8080 \
* --experiment-id ID \
* --action create \
* --experiments-directory /home/USER/nni-experiments \
* --log-level info \
* --foreground false \ (optional)
* --mode local (required for now, will be removed later)
**/
import 'app-module-path/register'; // so we can use absolute path to import
import fs from 'fs';
import { Container, Scope } from 'typescript-ioc'; import { Container, Scope } from 'typescript-ioc';
import * as fs from 'fs'; import * as component from 'common/component';
import * as path from 'path'; import { Database, DataStore } from 'common/datastore';
import * as component from './common/component'; import { ExperimentManager } from 'common/experimentManager';
import { Database, DataStore } from './common/datastore'; import { NniManagerArgs, parseArgs } from 'common/globals/arguments';
import { setExperimentStartupInfo } from './common/experimentStartupInfo'; import { getLogger, setLogLevel, startLogging } from 'common/log';
import { getLogger, setLogLevel, startLogging } from './common/log'; import { Manager } from 'common/manager';
import { Manager, ExperimentStartUpMode } from './common/manager'; import { TensorboardManager } from 'common/tensorboardManager';
import { ExperimentManager } from './common/experimentManager'; import { NNIDataStore } from 'core/nniDataStore';
import { TensorboardManager } from './common/tensorboardManager'; import { NNIExperimentsManager } from 'core/nniExperimentsManager';
import { getLogDir, mkDirP, parseArg } from './common/utils'; import { NNITensorboardManager } from 'core/nniTensorboardManager';
import { NNIDataStore } from './core/nniDataStore'; import { NNIManager } from 'core/nnimanager';
import { NNIManager } from './core/nnimanager'; import { SqlDB } from 'core/sqlDatabase';
import { SqlDB } from './core/sqlDatabase'; import { RestServer } from 'rest_server';
import { NNIExperimentsManager } from './core/nniExperimentsManager';
import { NNITensorboardManager } from './core/nniTensorboardManager';
import { RestServer } from './rest_server';
import { parseArgs } from 'common/globals/arguments';
const args = parseArgs(process.argv.slice(2));
async function initContainer(): Promise<void> {
Container.bind(Manager)
.to(NNIManager)
.scope(Scope.Singleton);
Container.bind(Database)
.to(SqlDB)
.scope(Scope.Singleton);
Container.bind(DataStore)
.to(NNIDataStore)
.scope(Scope.Singleton);
Container.bind(ExperimentManager)
.to(NNIExperimentsManager)
.scope(Scope.Singleton);
Container.bind(TensorboardManager)
.to(NNITensorboardManager)
.scope(Scope.Singleton);
const DEFAULT_LOGFILE: string = path.join(getLogDir(), 'nnimanager.log');
if (!args.foreground) {
startLogging(DEFAULT_LOGFILE);
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setLogLevel(args.logLevel);
const ds: DataStore = component.get(DataStore);
import path from 'path';
import { setExperimentStartupInfo } from 'common/experimentStartupInfo';
// TODO: this line should be inside initGlobals()
const args: NniManagerArgs = parseArgs(process.argv.slice(2));
async function start(): Promise<void> {
getLogger('main').info('Start NNI manager');
Container.bind(Manager).to(NNIManager).scope(Scope.Singleton);
Container.bind(Database).to(SqlDB).scope(Scope.Singleton);
Container.bind(DataStore).to(NNIDataStore).scope(Scope.Singleton);
Container.bind(ExperimentManager).to(NNIExperimentsManager).scope(Scope.Singleton);
Container.bind(TensorboardManager).to(NNITensorboardManager).scope(Scope.Singleton);
const ds: DataStore = component.get(DataStore);
await ds.init(); await ds.init();
}
setExperimentStartupInfo( const restServer = new RestServer(args.port, args.urlPrefix);
args.action === 'create',
args.experimentId,
args.port,
args.mode,
args.experimentsDirectory,
args.logLevel,
args.action === 'view',
args.dispatcherPipe ?? '',
args.urlPrefix
);
mkDirP(getLogDir())
.then(async () => {
try {
await initContainer();
const restServer: RestServer = component.get(RestServer);
await restServer.start(); await restServer.start();
} catch (err) { }
getLogger('main').error(`${err.stack}`);
throw err;
}
})
.catch((err: Error) => {
console.error(`Failed to create log dir: ${err.stack}`);
});
function cleanUp(): void { function shutdown(): void {
(component.get(Manager) as Manager).stopExperiment(); (component.get(Manager) as Manager).stopExperiment();
} }
process.on('SIGTERM', cleanUp); // Register callbacks to free training service resources on unexpected shutdown.
process.on('SIGBREAK', cleanUp); // A graceful stop should use REST API,
process.on('SIGINT', cleanUp); // because interrupts can cause strange behaviors in children processes.
process.on('SIGTERM', shutdown);
process.on('SIGBREAK', shutdown);
process.on('SIGINT', shutdown);
/* main */
// TODO: these should be handled inside globals module
setExperimentStartupInfo(args);
const logDirectory = path.join(args.experimentsDirectory, args.experimentId, 'log');
fs.mkdirSync(logDirectory, { recursive: true });
startLogging(path.join(logDirectory, 'nnimanager.log'));
setLogLevel(args.logLevel);
start().then(() => {
getLogger('main').debug('start() returned.');
}).catch((error) => {
try {
getLogger('main').error('Failed to start:', error);
} catch (loggerError) {
console.log('Failed to start:', error);
console.log('Seems logger is faulty:', loggerError);
}
process.exit(1);
});
// Node.js exits when there is no active handler,
// and we have registered a lot of handlers which are never cleaned up.
// So it runs forever until NNIManager calls `process.exit()`.
...@@ -18,11 +18,10 @@ ...@@ -18,11 +18,10 @@
* 2. Refactor ClusterJobRestServer to an express-ws application so it doesn't require extra port. * 2. Refactor ClusterJobRestServer to an express-ws application so it doesn't require extra port.
* 3. Provide public API to register express app, so this can be decoupled with other modules' implementation. * 3. Provide public API to register express app, so this can be decoupled with other modules' implementation.
* 4. Refactor NNIRestHandler. It's a mess. * 4. Refactor NNIRestHandler. It's a mess.
* 5. Get rid of IOC. * 5. Deal with log path mismatch between REST API and file system.
* 6. Deal with log path mismatch between REST API and file system.
* 7. Strip slashes of URL prefix inside ExperimentStartupInfo.
**/ **/
import assert from 'assert/strict';
import type { Server } from 'http'; import type { Server } from 'http';
import type { AddressInfo } from 'net'; import type { AddressInfo } from 'net';
import path from 'path'; import path from 'path';
...@@ -32,7 +31,6 @@ import httpProxy from 'http-proxy'; ...@@ -32,7 +31,6 @@ import httpProxy from 'http-proxy';
import { Deferred } from 'ts-deferred'; import { Deferred } from 'ts-deferred';
import { Singleton } from 'common/component'; import { Singleton } from 'common/component';
import { getBasePort, getPrefixUrl } from 'common/experimentStartupInfo';
import { Logger, getLogger } from 'common/log'; import { Logger, getLogger } from 'common/log';
import { getLogDir } from 'common/utils'; import { getLogDir } from 'common/utils';
import { createRestHandler } from './restHandler'; import { createRestHandler } from './restHandler';
...@@ -50,25 +48,23 @@ export class RestServer { ...@@ -50,25 +48,23 @@ export class RestServer {
private server: Server | null = null; private server: Server | null = null;
private logger: Logger = getLogger('RestServer'); private logger: Logger = getLogger('RestServer');
// I would prefer to get port and urlPrefix by constructor parameters, constructor(port: number, urlPrefix: string) {
// but this is impossible due to limitation of IOC. assert(!urlPrefix.startsWith('/') && !urlPrefix.endsWith('/'));
constructor() { this.port = port;
this.port = getBasePort(); this.urlPrefix = urlPrefix;
// Stripping slashes should be done inside ExperimentInfo, but I don't want to touch it for now.
this.urlPrefix = '/' + stripSlashes(getPrefixUrl());
} }
// The promise is resolved when it's ready to serve requests. // The promise is resolved when it's ready to serve requests.
// This worth nothing for now, // This worth nothing for now,
// but for example if we connect to tuner using WebSocket then it must be launched after promise resolved. // but for example if we connect to tuner using WebSocket then it must be launched after promise resolved.
public start(): Promise<void> { public start(): Promise<void> {
this.logger.info(`Starting REST server at port ${this.port}, URL prefix: "${this.urlPrefix}"`); this.logger.info(`Starting REST server at port ${this.port}, URL prefix: "/${this.urlPrefix}"`);
const app = express(); const app = express();
// FIXME: We should have a global handler for critical errors. // FIXME: We should have a global handler for critical errors.
// `shutdown()` is not a callback and should not be passed to NNIRestHandler. // `shutdown()` is not a callback and should not be passed to NNIRestHandler.
app.use(this.urlPrefix, rootRouter(this.shutdown.bind(this))); app.use('/' + this.urlPrefix, rootRouter(this.shutdown.bind(this)));
app.all('*', (_req: Request, res: Response) => { res.status(404).send(`Outside prefix "${this.urlPrefix}"`); }); app.all('*', (_req: Request, res: Response) => { res.status(404).send(`Outside prefix "/${this.urlPrefix}"`); });
this.server = app.listen(this.port); this.server = app.listen(this.port);
const deferred = new Deferred<void>(); const deferred = new Deferred<void>();
...@@ -126,7 +122,7 @@ function rootRouter(stopCallback: () => Promise<void>): Router { ...@@ -126,7 +122,7 @@ function rootRouter(stopCallback: () => Promise<void>): Router {
// The REST API path "/logs" does not match file system path "/log". // The REST API path "/logs" does not match file system path "/log".
// Here we use an additional router to workaround this problem. // Here we use an additional router to workaround this problem.
const logRouter = Router(); const logRouter = Router();
logRouter.get('*', express.static(getLogDir())); logRouter.get('*', express.static(logDirectory ?? getLogDir()));
router.use('/logs', logRouter); router.use('/logs', logRouter);
/* NAS model visualization */ /* NAS model visualization */
...@@ -153,12 +149,9 @@ function netronProxy(): Router { ...@@ -153,12 +149,9 @@ function netronProxy(): Router {
return router; return router;
} }
function stripSlashes(str: string): string {
return str.replace(/^\/+/, '').replace(/\/+$/, '');
}
let webuiPath: string = path.resolve('static'); let webuiPath: string = path.resolve('static');
let netronUrl: string = 'https://netron.app'; let netronUrl: string = 'https://netron.app';
let logDirectory: string | undefined = undefined;
export namespace UnitTestHelpers { export namespace UnitTestHelpers {
export function getPort(server: RestServer): number { export function getPort(server: RestServer): number {
...@@ -172,4 +165,8 @@ export namespace UnitTestHelpers { ...@@ -172,4 +165,8 @@ export namespace UnitTestHelpers {
export function setNetronUrl(mockUrl: string): void { export function setNetronUrl(mockUrl: string): void {
netronUrl = mockUrl; netronUrl = mockUrl;
} }
export function setLogDirectory(path: string): void {
logDirectory = path;
}
} }
...@@ -21,6 +21,8 @@ import { testExperimentManagerProvider } from '../mock/experimentManager'; ...@@ -21,6 +21,8 @@ import { testExperimentManagerProvider } from '../mock/experimentManager';
import { TensorboardManager } from '../../common/tensorboardManager'; import { TensorboardManager } from '../../common/tensorboardManager';
import { NNITensorboardManager } from '../../core/nniTensorboardManager'; import { NNITensorboardManager } from '../../core/nniTensorboardManager';
let restServer: RestServer;
describe('Unit test for rest server', () => { describe('Unit test for rest server', () => {
let ROOT_URL: string; let ROOT_URL: string;
...@@ -32,7 +34,7 @@ describe('Unit test for rest server', () => { ...@@ -32,7 +34,7 @@ describe('Unit test for rest server', () => {
Container.bind(TrainingService).to(MockedTrainingService); Container.bind(TrainingService).to(MockedTrainingService);
Container.bind(ExperimentManager).provider(testExperimentManagerProvider); Container.bind(ExperimentManager).provider(testExperimentManagerProvider);
Container.bind(TensorboardManager).to(NNITensorboardManager); Container.bind(TensorboardManager).to(NNITensorboardManager);
const restServer: RestServer = component.get(RestServer); restServer = new RestServer(8080, '');
restServer.start().then(() => { restServer.start().then(() => {
ROOT_URL = `http://localhost:8080/api/v1/nni`; ROOT_URL = `http://localhost:8080/api/v1/nni`;
done(); done();
...@@ -42,7 +44,7 @@ describe('Unit test for rest server', () => { ...@@ -42,7 +44,7 @@ describe('Unit test for rest server', () => {
}); });
after(() => { after(() => {
component.get<RestServer>(RestServer).shutdown(); restServer.shutdown();
cleanupUnitTest(); cleanupUnitTest();
}); });
......
...@@ -128,24 +128,10 @@ async function configRestServer(urlPrefix?: string) { ...@@ -128,24 +128,10 @@ async function configRestServer(urlPrefix?: string) {
await restServer.shutdown(); await restServer.shutdown();
} }
// Set port, URL prefix, and log path. UnitTestHelpers.setLogDirectory(path.join(__dirname, 'log'));
// There should be a better way to do this.
// Maybe rewire? I can't get it work with TypeScript.
setExperimentStartupInfo(
true,
path.basename(__dirname), // hacking getLogDir()
0, // ask for a random idle port
'local',
path.dirname(__dirname),
undefined,
undefined,
undefined,
urlPrefix
);
UnitTestHelpers.setWebuiPath(path.join(__dirname, 'static')); UnitTestHelpers.setWebuiPath(path.join(__dirname, 'static'));
restServer = new RestServer(); restServer = new RestServer(0, urlPrefix ?? '');
await restServer.start(); await restServer.start();
const port = UnitTestHelpers.getPort(restServer); const port = UnitTestHelpers.getPort(restServer);
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment