utils.ts 13.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
12
13
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
14
import * as lockfile from 'lockfile';
Deshui Yu's avatar
Deshui Yu committed
15
16
17
import { Deferred } from 'ts-deferred';
import { Container } from 'typescript-ioc';
import * as util from 'util';
18
import * as glob from 'glob';
Deshui Yu's avatar
Deshui Yu committed
19
20

import { Database, DataStore } from './datastore';
chicm-ms's avatar
chicm-ms committed
21
import { ExperimentStartupInfo, getExperimentStartupInfo, setExperimentStartupInfo } from './experimentStartupInfo';
chicm-ms's avatar
chicm-ms committed
22
import { ExperimentParams, Manager } from './manager';
23
import { ExperimentManager } from './experimentManager';
QuanluZhang's avatar
QuanluZhang committed
24
import { HyperParameters, TrainingService, TrialJobStatus } from './trainingService';
25
import { logLevelNameMap } from './log';
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
 *
 */
chicm-ms's avatar
chicm-ms committed
161
162
163
164
function getMsgDispatcherCommand(expParams: ExperimentParams): string {
    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
199
200
201
    const logLevel: string = parseArg(['--log_level', '-ll']);
    if (logLevel.length > 0 && !logLevelNameMap.has(logLevel)) {
        console.log(`FATAL: invalid log_level: ${logLevel}`);
    }

    setExperimentStartupInfo(true, 'unittest', 8080, 'unittest', undefined, logLevel);
Deshui Yu's avatar
Deshui Yu committed
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
    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);
222
    Container.restore(ExperimentManager);
Deshui Yu's avatar
Deshui Yu committed
223
224
}

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

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

    throw Error('getIPV4Address() failed because no valid IPv4 address found.')
247
248
}

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

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

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

chicm-ms's avatar
chicm-ms committed
267
268
    let timeoutId: NodeJS.Timer
    const delayTimeout: Promise<number> = new Promise((resolve: Function, reject: Function): void => {
269
270
271
272
273
274
275
        // 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;
276
    let cmd: string;
277
    if (process.platform === "win32") {
278
279
        cmd = `powershell "Get-ChildItem -Path ${directory} -Recurse -File | Measure-Object | %{$_.Count}"`
    } else {
280
        cmd = `find ${directory} -type f | wc -l`;
281
282
    }
    cpp.exec(cmd).then((result) => {
283
        if (result.stdout && parseInt(result.stdout)) {
284
            fileCount = parseInt(result.stdout);
285
286
287
288
289
290
291
292
        }
        deferred.resolve(fileCount);
    });
    return Promise.race([deferred.promise, delayTimeout]).finally(() => {
        clearTimeout(timeoutId);
    });
}

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

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

    const fileNameArray: string[] = fs.readdirSync(directory);
    let result = true;
309
    for (const name of fileNameArray) {
310
311
312
313
314
315
316
        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);
            }
317
            if (!result) {
318
319
                return Promise.reject(new Error(`file name in ${fullFilePath} is not valid!`));
            }
320
        } catch (error) {
321
322
323
            return Promise.reject(error);
        }
    }
324
    return Promise.resolve(result);
325
326
}

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

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

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

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

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

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

428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
/**
 * 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) => {
            return fs.existsSync(fileName) && Date.now() - fs.statSync(fileName).mtimeMs > lockOpts.stale;
        }).filter(isExpired=>isExpired === false).length === 0;
        if (!canLock) {
            throw new Error('File has been locked.');
        }
    }
    lockfile.lockSync(lockName, lockOpts);
    const result = func(...args);
    lockfile.unlockSync(lockName);
    return result;
}

449
export {
450
451
    countFilesRecursively, validateFileNameRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir, getExperimentsInfoPath,
    getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin, withLockSync,
452
453
    mkDirP, mkDirPSync, delay, prepareUnitTest, parseArg, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getCmdPy, getTunerProc, isAlive, killPid, getNewLine
};