utils.ts 14.4 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Deshui Yu's avatar
Deshui Yu committed
3
4
5

'use strict';

6
import * as assert from 'assert';
Deshui Yu's avatar
Deshui Yu committed
7
import { randomBytes } from 'crypto';
8
import * as cpp from 'child-process-promise';
9
10
import * as cp from 'child_process';
import { ChildProcess, spawn, StdioOptions } from 'child_process';
Deshui Yu's avatar
Deshui Yu committed
11
import * as fs from 'fs';
J-shang's avatar
J-shang committed
12
import * as net from 'net';
Deshui Yu's avatar
Deshui Yu committed
13
14
import * as os from 'os';
import * as path from 'path';
15
import * as lockfile from 'lockfile';
Deshui Yu's avatar
Deshui Yu committed
16
17
18
import { Deferred } from 'ts-deferred';
import { Container } from 'typescript-ioc';
import * as util from 'util';
19
import * as glob from 'glob';
Deshui Yu's avatar
Deshui Yu committed
20
21

import { Database, DataStore } from './datastore';
chicm-ms's avatar
chicm-ms committed
22
import { ExperimentStartupInfo, getExperimentStartupInfo, setExperimentStartupInfo } from './experimentStartupInfo';
23
import { ExperimentConfig, Manager } from './manager';
24
import { ExperimentManager } from './experimentManager';
QuanluZhang's avatar
QuanluZhang committed
25
import { HyperParameters, TrainingService, TrialJobStatus } from './trainingService';
Deshui Yu's avatar
Deshui Yu committed
26

27
function getExperimentRootDir(): string {
28
    return getExperimentStartupInfo()
29
        .getLogDir();
Deshui Yu's avatar
Deshui Yu committed
30
31
}

32
function getLogDir(): string {
Deshui Yu's avatar
Deshui Yu committed
33
34
35
    return path.join(getExperimentRootDir(), 'log');
}

36
function getLogLevel(): string {
37
    return getExperimentStartupInfo()
38
        .getLogLevel();
39
40
}

Deshui Yu's avatar
Deshui Yu committed
41
42
43
44
function getDefaultDatabaseDir(): string {
    return path.join(getExperimentRootDir(), 'db');
}

QuanluZhang's avatar
QuanluZhang committed
45
46
47
48
function getCheckpointDir(): string {
    return path.join(getExperimentRootDir(), 'checkpoint');
}

49
50
51
52
function getExperimentsInfoPath(): string {
    return path.join(os.homedir(), 'nni-experiments', '.experiment');
}

Deshui Yu's avatar
Deshui Yu committed
53
54
55
56
57
58
59
60
function mkDirP(dirPath: string): Promise<void> {
    const deferred: Deferred<void> = new Deferred<void>();
    fs.exists(dirPath, (exists: boolean) => {
        if (exists) {
            deferred.resolve();
        } else {
            const parent: string = path.dirname(dirPath);
            mkDirP(parent).then(() => {
61
                fs.mkdir(dirPath, (err: Error) => {
Deshui Yu's avatar
Deshui Yu committed
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
                    if (err) {
                        deferred.reject(err);
                    } else {
                        deferred.resolve();
                    }
                });
            }).catch((err: Error) => {
                deferred.reject(err);
            });
        }
    });

    return deferred.promise;
}

function mkDirPSync(dirPath: string): void {
    if (fs.existsSync(dirPath)) {
        return;
    }
    mkDirPSync(path.dirname(dirPath));
    fs.mkdirSync(dirPath);
}

const delay: (ms: number) => Promise<void> = util.promisify(setTimeout);

/**
 * Convert index to character
 * @param index index
 * @returns a mapping character
 */
function charMap(index: number): number {
    if (index < 26) {
        return index + 97;
    } else if (index < 52) {
        return index - 26 + 65;
    } else {
        return index - 52 + 48;
    }
}

/**
 * Generate a unique string by length
 * @param len length of string
 * @returns a unique string
 */
function uniqueString(len: number): string {
    if (len === 0) {
        return '';
    }
    const byteLength: number = Math.ceil((Math.log2(52) + Math.log2(62) * (len - 1)) / 8);
    let num: number = randomBytes(byteLength).reduce((a: number, b: number) => a * 256 + b, 0);
    const codes: number[] = [];
    codes.push(charMap(num % 52));
    num = Math.floor(num / 52);
    for (let i: number = 1; i < len; i++) {
        codes.push(charMap(num % 62));
        num = Math.floor(num / 62);
    }

    return String.fromCharCode(...codes);
}

124
125
126
127
function randomInt(max: number): number {
    return Math.floor(Math.random() * max);
}

128
129
130
131
132
function randomSelect<T>(a: T[]): T {
    assert(a !== undefined);

    return a[Math.floor(Math.random() * a.length)];
}
133

Deshui Yu's avatar
Deshui Yu committed
134
135
136
137
138
139
140
141
142
143
144
145
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 '';
}

146
function getCmdPy(): string {
147
    let cmd = 'python3';
148
    if (process.platform === 'win32') {
149
150
151
152
153
        cmd = 'python';
    }
    return cmd;
}

154
/**
155
 * Generate command line to start automl algorithm(s),
QuanluZhang's avatar
QuanluZhang committed
156
 * either start advisor or start a process which runs tuner and assessor
157
 *
chicm-ms's avatar
chicm-ms committed
158
 * @param expParams: experiment startup parameters
159
160
 *
 */
161
function getMsgDispatcherCommand(expParams: ExperimentConfig): string {
chicm-ms's avatar
chicm-ms committed
162
163
164
    const clonedParams = Object.assign({}, expParams);
    delete clonedParams.searchSpace;
    return `${getCmdPy()} -m nni --exp_params ${Buffer.from(JSON.stringify(clonedParams)).toString('base64')}`;
165
166
}

167
168
169
170
/**
 * Generate parameter file name based on HyperParameters object
 * @param hyperParameters HyperParameters instance
 */
chicm-ms's avatar
chicm-ms committed
171
function generateParamFileName(hyperParameters: HyperParameters): string {
172
173
174
    assert(hyperParameters !== undefined);
    assert(hyperParameters.index >= 0);

chicm-ms's avatar
chicm-ms committed
175
    let paramFileName: string;
176
    if (hyperParameters.index == 0) {
177
178
179
180
181
182
183
        paramFileName = 'parameter.cfg';
    } else {
        paramFileName = `parameter_${hyperParameters.index}.cfg`
    }
    return paramFileName;
}

Deshui Yu's avatar
Deshui Yu committed
184
185
186
187
188
189
190
191
192
193
/**
 * Initialize a pseudo experiment environment for unit test.
 * Must be paired with `cleanupUnitTest()`.
 */
function prepareUnitTest(): void {
    Container.snapshot(ExperimentStartupInfo);
    Container.snapshot(Database);
    Container.snapshot(DataStore);
    Container.snapshot(TrainingService);
    Container.snapshot(Manager);
194
    Container.snapshot(ExperimentManager);
Deshui Yu's avatar
Deshui Yu committed
195

196
197
198
    const logLevel: string = parseArg(['--log_level', '-ll']);

    setExperimentStartupInfo(true, 'unittest', 8080, 'unittest', undefined, logLevel);
Deshui Yu's avatar
Deshui Yu committed
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
    mkDirPSync(getLogDir());

    const sqliteFile: string = path.join(getDefaultDatabaseDir(), 'nni.sqlite');
    try {
        fs.unlinkSync(sqliteFile);
    } catch (err) {
        // file not exists, good
    }
}

/**
 * Clean up unit test pseudo experiment.
 * Must be paired with `prepareUnitTest()`.
 */
function cleanupUnitTest(): void {
    Container.restore(Manager);
    Container.restore(TrainingService);
    Container.restore(DataStore);
    Container.restore(Database);
    Container.restore(ExperimentStartupInfo);
219
    Container.restore(ExperimentManager);
Deshui Yu's avatar
Deshui Yu committed
220
221
}

chicm-ms's avatar
chicm-ms committed
222
let cachedipv4Address: string = '';
223
224
225
226
/**
 * Get IPv4 address of current machine
 */
function getIPV4Address(): string {
227
228
229
    if (cachedipv4Address && cachedipv4Address.length > 0) {
        return cachedipv4Address;
    }
230

231
232
233
    const networkInterfaces = os.networkInterfaces();
    if (networkInterfaces.eth0) {
        for (const item of networkInterfaces.eth0) {
234
            if (item.family === 'IPv4') {
235
236
237
                cachedipv4Address = item.address;
                return cachedipv4Address;
            }
238
        }
239
    } else {
240
        throw Error(`getIPV4Address() failed because os.networkInterfaces().eth0 is undefined. Please specify NNI manager IP in config.`);
241
    }
242
243

    throw Error('getIPV4Address() failed because no valid IPv4 address found.')
244
245
}

QuanluZhang's avatar
QuanluZhang committed
246
247
248
249
250
251
252
/**
 * Get the status of canceled jobs according to the hint isEarlyStopped
 */
function getJobCancelStatus(isEarlyStopped: boolean): TrialJobStatus {
    return isEarlyStopped ? 'EARLY_STOPPED' : 'USER_CANCELED';
}

253
254
255
256
/**
 * Utility method to calculate file numbers under a directory, recursively
 * @param directory directory name
 */
chicm-ms's avatar
chicm-ms committed
257
function countFilesRecursively(directory: string): Promise<number> {
258
    if (!fs.existsSync(directory)) {
259
260
261
262
263
        throw Error(`Direcotory ${directory} doesn't exist`);
    }

    const deferred: Deferred<number> = new Deferred<number>();

chicm-ms's avatar
chicm-ms committed
264
265
    let timeoutId: NodeJS.Timer
    const delayTimeout: Promise<number> = new Promise((resolve: Function, reject: Function): void => {
266
267
268
269
270
271
272
        // Set timeout and reject the promise once reach timeout (5 seconds)
        timeoutId = setTimeout(() => {
            reject(new Error(`Timeout: path ${directory} has too many files`));
        }, 5000);
    });

    let fileCount: number = -1;
273
    let cmd: string;
274
    if (process.platform === "win32") {
275
276
        cmd = `powershell "Get-ChildItem -Path ${directory} -Recurse -File | Measure-Object | %{$_.Count}"`
    } else {
277
        cmd = `find ${directory} -type f | wc -l`;
278
279
    }
    cpp.exec(cmd).then((result) => {
280
        if (result.stdout && parseInt(result.stdout)) {
281
            fileCount = parseInt(result.stdout);
282
283
284
285
286
287
288
289
        }
        deferred.resolve(fileCount);
    });
    return Promise.race([deferred.promise, delayTimeout]).finally(() => {
        clearTimeout(timeoutId);
    });
}

290
export function validateFileName(fileName: string): boolean {
chicm-ms's avatar
chicm-ms committed
291
    const pattern: string = '^[a-z0-9A-Z._-]+$';
292
    const validateResult = fileName.match(pattern);
293
    if (validateResult) {
294
295
296
297
298
299
        return true;
    }
    return false;
}

async function validateFileNameRecursively(directory: string): Promise<boolean> {
300
    if (!fs.existsSync(directory)) {
301
302
303
304
305
        throw Error(`Direcotory ${directory} doesn't exist`);
    }

    const fileNameArray: string[] = fs.readdirSync(directory);
    let result = true;
306
    for (const name of fileNameArray) {
307
308
309
310
311
312
313
        const fullFilePath: string = path.join(directory, name);
        try {
            // validate file names and directory names
            result = validateFileName(name);
            if (fs.lstatSync(fullFilePath).isDirectory()) {
                result = result && await validateFileNameRecursively(fullFilePath);
            }
314
            if (!result) {
315
316
                return Promise.reject(new Error(`file name in ${fullFilePath} is not valid!`));
            }
317
        } catch (error) {
318
319
320
            return Promise.reject(error);
        }
    }
321
    return Promise.resolve(result);
322
323
}

324
325
326
327
/**
 * get the version of current package
 */
async function getVersion(): Promise<string> {
chicm-ms's avatar
chicm-ms committed
328
    const deferred: Deferred<string> = new Deferred<string>();
329
    import(path.join(__dirname, '..', 'package.json')).then((pkg) => {
330
        deferred.resolve(pkg.version);
331
332
    }).catch(() => {
        deferred.resolve('999.0.0-developing');
333
334
    });
    return deferred.promise;
335
}
336

337
338
339
/**
 * run command as ChildProcess
 */
J-shang's avatar
J-shang committed
340
function getTunerProc(command: string, stdio: StdioOptions, newCwd: string, newEnv: any, newShell: boolean = true, isDetached: boolean = false): ChildProcess {
341
342
    let cmd: string = command;
    let arg: string[] = [];
343
    if (process.platform === "win32") {
344
        cmd = command.split(" ", 1)[0];
345
        arg = command.substr(cmd.length + 1).split(" ");
346
        newShell = false;
347
        isDetached = true;
348
349
350
351
352
    }
    const tunerProc: ChildProcess = spawn(cmd, arg, {
        stdio,
        cwd: newCwd,
        env: newEnv,
353
354
        shell: newShell,
        detached: isDetached
355
356
357
358
359
360
361
    });
    return tunerProc;
}

/**
 * judge whether the process is alive
 */
Yuge Zhang's avatar
Yuge Zhang committed
362
async function isAlive(pid: any): Promise<boolean> {
chicm-ms's avatar
chicm-ms committed
363
    const deferred: Deferred<boolean> = new Deferred<boolean>();
364
    let alive: boolean = false;
Yuge Zhang's avatar
Yuge Zhang committed
365
    if (process.platform === 'win32') {
366
367
368
369
370
371
372
        try {
            const str = cp.execSync(`powershell.exe Get-Process -Id ${pid} -ErrorAction SilentlyContinue`).toString();
            if (str) {
                alive = true;
            }
        }
        catch (error) {
chicm-ms's avatar
chicm-ms committed
373
            //ignore
374
375
        }
    }
Yuge Zhang's avatar
Yuge Zhang committed
376
    else {
377
378
379
380
381
382
383
384
385
386
387
388
        try {
            await cpp.exec(`kill -0 ${pid}`);
            alive = true;
        } catch (error) {
            //ignore
        }
    }
    deferred.resolve(alive);
    return deferred.promise;
}

/**
389
 * kill process
390
 */
Yuge Zhang's avatar
Yuge Zhang committed
391
async function killPid(pid: any): Promise<void> {
chicm-ms's avatar
chicm-ms committed
392
    const deferred: Deferred<void> = new Deferred<void>();
393
394
    try {
        if (process.platform === "win32") {
Yuge Zhang's avatar
Yuge Zhang committed
395
            await cpp.exec(`cmd.exe /c taskkill /PID ${pid} /F`);
396
        }
397
        else {
398
399
400
401
402
403
404
405
406
            await cpp.exec(`kill -9 ${pid}`);
        }
    } catch (error) {
        // pid does not exist, do nothing here
    }
    deferred.resolve();
    return deferred.promise;
}

407
function getNewLine(): string {
408
409
410
    if (process.platform === "win32") {
        return "\r\n";
    }
411
    else {
412
413
414
415
        return "\n";
    }
}

416
417
/**
 * Use '/' to join path instead of '\' for all kinds of platform
418
 * @param path
419
420
421
422
423
424
425
 */
function unixPathJoin(...paths: any[]): string {
    const dir: string = paths.filter((path: any) => path !== '').join('/');
    if (dir === '') return '.';
    return dir;
}

426
427
428
429
430
431
432
433
434
/**
 * lock a file sync
 */
function withLockSync(func: Function, filePath: string, lockOpts: {[key: string]: any}, ...args: any): any {
    const lockName = path.join(path.dirname(filePath), path.basename(filePath) + `.lock.${process.pid}`);
    if (typeof lockOpts.stale === 'number'){
        const lockPath = path.join(path.dirname(filePath), path.basename(filePath) + '.lock.*');
        const lockFileNames: string[] = glob.sync(lockPath);
        const canLock: boolean = lockFileNames.map((fileName) => {
435
436
            return fs.existsSync(fileName) && Date.now() - fs.statSync(fileName).mtimeMs < lockOpts.stale;
        }).filter(unexpired=>unexpired === true).length === 0;
437
438
439
440
441
442
443
444
445
446
        if (!canLock) {
            throw new Error('File has been locked.');
        }
    }
    lockfile.lockSync(lockName, lockOpts);
    const result = func(...args);
    lockfile.unlockSync(lockName);
    return result;
}

J-shang's avatar
J-shang committed
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
async function isPortOpen(host: string, port: number): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => {
        try{
            const stream = net.createConnection(port, host);
            const id = setTimeout(() => {
                stream.destroy();
                resolve(false);
            }, 1000);

            stream.on('connect', () => {
                clearTimeout(id);
                stream.destroy();
                resolve(true);
            });

            stream.on('error', () => {
                clearTimeout(id);
                stream.destroy();
                resolve(false);
            });
        } catch (error) {
            reject(error);
        }
    });
}

async function getFreePort(host: string, start: number, end: number): Promise<number> {
    if (start > end) {
        throw new Error(`no more free port`);
    }
    if (await isPortOpen(host, start)) {
        return await getFreePort(host, start + 1, end);
    } else {
        return start;
    }
}

484
export {
485
    countFilesRecursively, validateFileNameRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir, getExperimentsInfoPath,
J-shang's avatar
J-shang committed
486
    getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin, withLockSync, getFreePort, isPortOpen,
487
488
    mkDirP, mkDirPSync, delay, prepareUnitTest, parseArg, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getCmdPy, getTunerProc, isAlive, killPid, getNewLine
};