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
    if (os.networkInterfaces().eth0) {
        for (const item of os.networkInterfaces().eth0) {
            if (item.family === 'IPv4') {
228
229
230
                cachedipv4Address = item.address;
                return cachedipv4Address;
            }
231
        }
232
233
    } else {
        throw Error('getIPV4Address() failed because os.networkInterfaces().eth0 is undefined.');
234
    }
235
236

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

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

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

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

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

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

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

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

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

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

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

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

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

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