"use strict";
// Copyright (c) Jupyter Development Team.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.JupyterServer = void 0;
const child_process_1 = require("child_process");
const deepmerge_1 = __importDefault(require("deepmerge"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const coreutils_1 = require("@jupyterlab/coreutils");
const coreutils_2 = require("@lumino/coreutils");
const common_1 = require("./common");
/**
 * A Jupyter Server that runs as a child process.
 *
 * ### Notes
 * There can only be one running server at a time, since
 * PageConfig is global.  Any classes that use `ServerConnection.ISettings`
 * such as `ServiceManager` should be instantiated after the server
 * has fully started so they pick up the right `PageConfig`.
 *
 * #### Example
 * ```typescript
 * const server = new JupyterServer();
 *
 * beforeAll(async () => {
 *   await server.start();
 * });
 *
 * afterAll(async () => {
 *  await server.shutdown();
 * });
 * ```
 *
 */
class JupyterServer {
    /**
     * Start the server.
     *
     * @returns A promise that resolves with the url of the server
     *
     * @throws Error if another server is still running.
     */
    async start(options = {}) {
        if (Private.child !== null) {
            throw Error('Previous server was not disposed');
        }
        const startDelegate = new coreutils_2.PromiseDelegate();
        const env = {
            JUPYTER_CONFIG_DIR: Private.handleConfig(options),
            JUPYTER_DATA_DIR: Private.handleData(options),
            JUPYTER_RUNTIME_DIR: Private.mktempDir('jupyter_runtime'),
            IPYTHONDIR: Private.mktempDir('ipython'),
            PATH: process.env.PATH
        };
        // Create the child process for the server.
        const child = (Private.child = child_process_1.spawn('jupyter-lab', { env }));
        let started = false;
        // Handle server output.
        const handleOutput = (output) => {
            console.debug(output);
            if (started) {
                return;
            }
            const baseUrl = Private.handleStartup(output);
            if (baseUrl) {
                console.debug('Jupyter Server started');
                started = true;
                void Private.connect(baseUrl, startDelegate);
            }
        };
        child.stdout.on('data', data => {
            handleOutput(String(data));
        });
        child.stderr.on('data', data => {
            handleOutput(String(data));
        });
        const url = await startDelegate.promise;
        return url;
    }
    /**
     * Shut down the server, waiting for it to exit gracefully.
     */
    async shutdown() {
        if (!Private.child) {
            return Promise.resolve(void 0);
        }
        const stopDelegate = new coreutils_2.PromiseDelegate();
        const child = Private.child;
        child.on('exit', code => {
            Private.child = null;
            if (code !== null && code !== 0) {
                stopDelegate.reject('child process exited with code ' + String(code));
            }
            else {
                stopDelegate.resolve(void 0);
            }
        });
        child.kill();
        window.setTimeout(() => {
            if (Private.child) {
                Private.child.kill(9);
            }
        }, 3000);
        return stopDelegate.promise;
    }
}
exports.JupyterServer = JupyterServer;
/**
 * A namespace for module private data.
 */
var Private;
(function (Private) {
    Private.child = null;
    /**
     * Make a temporary directory.
     *
     * @param suffix the last portion of the dir naem.
     */
    function mktempDir(suffix) {
        const pathPrefix = '/tmp/jupyterServer';
        if (!fs.existsSync(pathPrefix)) {
            fs.mkdirSync(pathPrefix);
        }
        return fs.mkdtempSync(`${pathPrefix}/${suffix}`);
    }
    Private.mktempDir = mktempDir;
    /**
     * Install a spec in the data directory.
     */
    function installSpec(dataDir, name, spec) {
        const specDir = path.join(dataDir, 'kernels', name);
        fs.mkdirSync(specDir, { recursive: true });
        fs.writeFileSync(path.join(specDir, 'kernel.json'), JSON.stringify(spec));
        coreutils_1.PageConfig.setOption(`__kernelSpec_${name}`, JSON.stringify(spec));
    }
    Private.installSpec = installSpec;
    /**
     * Create and populate a notebook directory.
     */
    function createNotebookDir() {
        const nbDir = mktempDir('notebook');
        fs.mkdirSync(path.join(nbDir, 'src'));
        fs.writeFileSync(path.join(nbDir, 'src', 'temp.txt'), 'hello');
        const roFilepath = path.join(nbDir, 'src', 'readonly-temp.txt');
        fs.writeFileSync(roFilepath, 'hello from a ready only file', {
            mode: 0o444
        });
        return nbDir;
    }
    /**
     * Create a temporary directory for schemas.
     */
    function createAppDir() {
        const appDir = mktempDir('app');
        // Add a fake static/index.html for `ensure_app_check()`
        fs.mkdirSync(path.join(appDir, 'static'));
        fs.writeFileSync(path.join(appDir, 'static', 'index.html'), 'foo');
        // Add the apputils schema.
        const schemaDir = path.join(appDir, 'schemas');
        fs.mkdirSync(schemaDir, { recursive: true });
        const extensionDir = path.join(schemaDir, '@jupyterlab', 'apputils-extension');
        fs.mkdirSync(extensionDir, { recursive: true });
        // Get schema content.
        const schema = {
            title: 'Theme',
            description: 'Theme manager settings.',
            properties: {
                theme: {
                    type: 'string',
                    title: 'Selected Theme',
                    default: 'JupyterLab Light'
                }
            },
            type: 'object'
        };
        fs.writeFileSync(path.join(extensionDir, 'themes.json'), JSON.stringify(schema));
        return appDir;
    }
    /**
     * Handle configuration.
     */
    function handleConfig(options) {
        // Set up configuration.
        const token = coreutils_2.UUID.uuid4();
        coreutils_1.PageConfig.setOption('token', token);
        coreutils_1.PageConfig.setOption('terminalsAvailable', 'true');
        if (options.pageConfig) {
            Object.keys(options.pageConfig).forEach(key => {
                coreutils_1.PageConfig.setOption(key, options.pageConfig[key]);
            });
        }
        const configDir = mktempDir('config');
        const configPath = path.join(configDir, 'jupyter_server_config.json');
        const root_dir = createNotebookDir();
        const app_dir = createAppDir();
        const user_settings_dir = mktempDir('settings');
        const workspaces_dir = mktempDir('workspaces');
        const configData = deepmerge_1.default({
            LabApp: {
                user_settings_dir,
                workspaces_dir,
                app_dir,
                open_browser: false,
                log_level: 'DEBUG'
            },
            ServerApp: {
                token,
                root_dir,
                log_level: 'DEBUG'
            },
            MultiKernelManager: {
                default_kernel_name: 'echo'
            },
            KernelManager: {
                shutdown_wait_time: 1.0
            }
        }, options.configData || {});
        coreutils_1.PageConfig.setOption('__configData', JSON.stringify(configData));
        fs.writeFileSync(configPath, JSON.stringify(configData));
        return configDir;
    }
    Private.handleConfig = handleConfig;
    /**
     * Handle data.
     */
    function handleData(options) {
        const dataDir = mktempDir('data');
        // Install custom specs.
        installSpec(dataDir, 'echo', {
            argv: [
                'python',
                '-m',
                'jupyterlab.tests.echo_kernel',
                '-f',
                '{connection_file}'
            ],
            display_name: 'Echo Kernel',
            language: 'echo'
        });
        installSpec(dataDir, 'ipython', {
            argv: ['python', '-m', 'ipykernel_launcher', '-f', '{connection_file}'],
            display_name: 'Python 3',
            language: 'python'
        });
        if (options.additionalKernelSpecs) {
            Object.keys(options.additionalKernelSpecs).forEach(key => {
                installSpec(dataDir, key, options.additionalKernelSpecs[key]);
            });
        }
        return dataDir;
    }
    Private.handleData = handleData;
    /**
     * Handle process startup.
     *
     * @param output the process output
     *
     * @returns The baseUrl of the server or `null`.
     */
    function handleStartup(output) {
        let baseUrl = null;
        output.split('\n').forEach(line => {
            const baseUrlMatch = line.match(/(http:\/\/localhost:\d+\/[^?]*)/);
            if (baseUrlMatch) {
                baseUrl = baseUrlMatch[1].replace('/lab', '');
                coreutils_1.PageConfig.setOption('baseUrl', baseUrl);
            }
        });
        return baseUrl;
    }
    Private.handleStartup = handleStartup;
    /**
     * Connect to the Jupyter server.
     */
    async function connect(baseUrl, startDelegate) {
        // eslint-disable-next-line
        while (true) {
            try {
                await fetch(coreutils_1.URLExt.join(baseUrl, 'api'));
                startDelegate.resolve(baseUrl);
                return;
            }
            catch (e) {
                // spin until we can connect to the server.
                console.warn(e);
                await common_1.sleep(1000);
            }
        }
    }
    Private.connect = connect;
})(Private || (Private = {}));
//# sourceMappingURL=start_jupyter_server.js.map