utils.ts 12.1 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
14
15
16
17
18
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { Deferred } from 'ts-deferred';
import { Container } from 'typescript-ioc';
import * as util from 'util';

import { Database, DataStore } from './datastore';
chicm-ms's avatar
chicm-ms committed
19
import { ExperimentStartupInfo, getExperimentStartupInfo, setExperimentStartupInfo } from './experimentStartupInfo';
chicm-ms's avatar
chicm-ms committed
20
import { ExperimentParams, Manager } from './manager';
QuanluZhang's avatar
QuanluZhang committed
21
import { HyperParameters, TrainingService, TrialJobStatus } from './trainingService';
22
import { logLevelNameMap } from './log';
Deshui Yu's avatar
Deshui Yu committed
23

24
function getExperimentRootDir(): string {
25
    return getExperimentStartupInfo()
26
        .getLogDir();
Deshui Yu's avatar
Deshui Yu committed
27
28
}

29
function getLogDir(): string {
Deshui Yu's avatar
Deshui Yu committed
30
31
32
    return path.join(getExperimentRootDir(), 'log');
}

33
function getLogLevel(): string {
34
    return getExperimentStartupInfo()
35
        .getLogLevel();
36
37
}

Deshui Yu's avatar
Deshui Yu committed
38
39
40
41
function getDefaultDatabaseDir(): string {
    return path.join(getExperimentRootDir(), 'db');
}

QuanluZhang's avatar
QuanluZhang committed
42
43
44
45
function getCheckpointDir(): string {
    return path.join(getExperimentRootDir(), 'checkpoint');
}

Deshui Yu's avatar
Deshui Yu committed
46
47
48
49
50
51
52
53
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(() => {
54
                fs.mkdir(dirPath, (err: Error) => {
Deshui Yu's avatar
Deshui Yu committed
55
56
57
58
59
60
61
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
                    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);
}

117
118
119
120
function randomInt(max: number): number {
    return Math.floor(Math.random() * max);
}

121
122
123
124
125
function randomSelect<T>(a: T[]): T {
    assert(a !== undefined);

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

Deshui Yu's avatar
Deshui Yu committed
127
128
129
130
131
132
133
134
135
136
137
138
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 '';
}

139
function getCmdPy(): string {
140
    let cmd = 'python3';
141
    if (process.platform === 'win32') {
142
143
144
145
146
        cmd = 'python';
    }
    return cmd;
}

147
/**
148
 * Generate command line to start automl algorithm(s),
QuanluZhang's avatar
QuanluZhang committed
149
 * either start advisor or start a process which runs tuner and assessor
150
 *
chicm-ms's avatar
chicm-ms committed
151
 * @param expParams: experiment startup parameters
152
153
 *
 */
chicm-ms's avatar
chicm-ms committed
154
155
156
157
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')}`;
158
159
}

160
161
162
163
/**
 * Generate parameter file name based on HyperParameters object
 * @param hyperParameters HyperParameters instance
 */
chicm-ms's avatar
chicm-ms committed
164
function generateParamFileName(hyperParameters: HyperParameters): string {
165
166
167
    assert(hyperParameters !== undefined);
    assert(hyperParameters.index >= 0);

chicm-ms's avatar
chicm-ms committed
168
    let paramFileName: string;
169
    if (hyperParameters.index == 0) {
170
171
172
173
174
175
176
        paramFileName = 'parameter.cfg';
    } else {
        paramFileName = `parameter_${hyperParameters.index}.cfg`
    }
    return paramFileName;
}

Deshui Yu's avatar
Deshui Yu committed
177
178
179
180
181
182
183
184
185
186
187
/**
 * 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);

188
189
190
191
192
193
    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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
    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);
}

chicm-ms's avatar
chicm-ms committed
216
let cachedipv4Address: string = '';
217
218
219
220
/**
 * Get IPv4 address of current machine
 */
function getIPV4Address(): string {
221
222
223
    if (cachedipv4Address && cachedipv4Address.length > 0) {
        return cachedipv4Address;
    }
224

225
226
227
    const networkInterfaces = os.networkInterfaces();
    if (networkInterfaces.eth0) {
        for (const item of networkInterfaces.eth0) {
228
            if (item.family === 'IPv4') {
229
230
231
                cachedipv4Address = item.address;
                return cachedipv4Address;
            }
232
        }
233
    } else {
234
        throw Error(`getIPV4Address() failed because os.networkInterfaces().eth0 is undefined. Please specify NNI manager IP in config.`);
235
    }
236
237

    throw Error('getIPV4Address() failed because no valid IPv4 address found.')
238
239
}

QuanluZhang's avatar
QuanluZhang committed
240
241
242
243
244
245
246
/**
 * Get the status of canceled jobs according to the hint isEarlyStopped
 */
function getJobCancelStatus(isEarlyStopped: boolean): TrialJobStatus {
    return isEarlyStopped ? 'EARLY_STOPPED' : 'USER_CANCELED';
}

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

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

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

284
export function validateFileName(fileName: string): boolean {
chicm-ms's avatar
chicm-ms committed
285
    const pattern: string = '^[a-z0-9A-Z._-]+$';
286
    const validateResult = fileName.match(pattern);
287
    if (validateResult) {
288
289
290
291
292
293
        return true;
    }
    return false;
}

async function validateFileNameRecursively(directory: string): Promise<boolean> {
294
    if (!fs.existsSync(directory)) {
295
296
297
298
299
        throw Error(`Direcotory ${directory} doesn't exist`);
    }

    const fileNameArray: string[] = fs.readdirSync(directory);
    let result = true;
300
    for (const name of fileNameArray) {
301
302
303
304
305
306
307
        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);
            }
308
            if (!result) {
309
310
                return Promise.reject(new Error(`file name in ${fullFilePath} is not valid!`));
            }
311
        } catch (error) {
312
313
314
            return Promise.reject(error);
        }
    }
315
    return Promise.resolve(result);
316
317
}

318
319
320
321
/**
 * get the version of current package
 */
async function getVersion(): Promise<string> {
chicm-ms's avatar
chicm-ms committed
322
    const deferred: Deferred<string> = new Deferred<string>();
323
    import(path.join(__dirname, '..', 'package.json')).then((pkg) => {
324
        deferred.resolve(pkg.version);
325
    }).catch((error) => {
326
327
328
        deferred.reject(error);
    });
    return deferred.promise;
329
}
330

331
332
333
/**
 * run command as ChildProcess
 */
334
function getTunerProc(command: string, stdio: StdioOptions, newCwd: string, newEnv: any): ChildProcess {
335
336
337
    let cmd: string = command;
    let arg: string[] = [];
    let newShell: boolean = true;
338
    if (process.platform === "win32") {
339
        cmd = command.split(" ", 1)[0];
340
        arg = command.substr(cmd.length + 1).split(" ");
341
342
343
344
345
346
347
348
349
350
351
352
353
354
        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
355
async function isAlive(pid: any): Promise<boolean> {
chicm-ms's avatar
chicm-ms committed
356
    const deferred: Deferred<boolean> = new Deferred<boolean>();
357
    let alive: boolean = false;
Yuge Zhang's avatar
Yuge Zhang committed
358
    if (process.platform === 'win32') {
359
360
361
362
363
364
365
        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
366
            //ignore
367
368
        }
    }
Yuge Zhang's avatar
Yuge Zhang committed
369
    else {
370
371
372
373
374
375
376
377
378
379
380
381
        try {
            await cpp.exec(`kill -0 ${pid}`);
            alive = true;
        } catch (error) {
            //ignore
        }
    }
    deferred.resolve(alive);
    return deferred.promise;
}

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

400
function getNewLine(): string {
401
402
403
    if (process.platform === "win32") {
        return "\r\n";
    }
404
    else {
405
406
407
408
        return "\n";
    }
}

409
410
/**
 * Use '/' to join path instead of '\' for all kinds of platform
411
 * @param path
412
413
414
415
416
417
418
 */
function unixPathJoin(...paths: any[]): string {
    const dir: string = paths.filter((path: any) => path !== '').join('/');
    if (dir === '') return '.';
    return dir;
}

419
420
export {
    countFilesRecursively, validateFileNameRecursively, generateParamFileName, getMsgDispatcherCommand, getCheckpointDir,
421
    getLogDir, getExperimentRootDir, getJobCancelStatus, getDefaultDatabaseDir, getIPV4Address, unixPathJoin,
422
423
    mkDirP, mkDirPSync, delay, prepareUnitTest, parseArg, cleanupUnitTest, uniqueString, randomInt, randomSelect, getLogLevel, getVersion, getCmdPy, getTunerProc, isAlive, killPid, getNewLine
};