
/*
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) NGINX, Inc.
 */


#include <njs.h>
#include <njs_unix.h>
#include <njs_arr.h>
#include <njs_queue.h>
#include <njs_rbtree.h>

#if (NJS_HAVE_QUICKJS)
#include <qjs.h>
#include <pthread.h>
#include <dlfcn.h>
#endif

#if (!defined NJS_FUZZER_TARGET && defined NJS_HAVE_READLINE)

#include <locale.h>
#include <signal.h>
#include <sys/select.h>
#if (NJS_HAVE_EDITLINE)
#include <editline/readline.h>
#elif (NJS_HAVE_EDIT_READLINE)
#include <edit/readline/readline.h>
#else
#include <readline/readline.h>
#endif

#endif


typedef struct {
    uint8_t                 disassemble;
    uint8_t                 denormals;
    uint8_t                 interactive;
    uint8_t                 module;
    uint8_t                 quiet;
    uint8_t                 sandbox;
    uint8_t                 safe;
    uint8_t                 version;
    uint8_t                 ast;
    uint8_t                 unhandled_rejection;
    uint8_t                 suppress_stdout;
    uint8_t                 opcode_debug;
    uint8_t                 generator_debug;
    uint8_t                 can_block;
    int                     exit_code;
    int                     stack_size;

    char                    *file;
    njs_str_t               command;
    size_t                  n_paths;
    njs_str_t               *paths;
    char                    **argv;
    njs_uint_t              argc;

    enum {
        NJS_ENGINE_NJS = 0,
        NJS_ENGINE_QUICKJS = 1,
    }                       engine;
} njs_opts_t;


typedef enum {
    NJS_LOG_ERROR = 4,
    NJS_LOG_WARN = 5,
    NJS_LOG_INFO = 7,
} njs_log_level_t;


typedef struct {
    size_t                  index;
    njs_arr_t               *suffix_completions;
} njs_completion_t;


typedef struct {
    NJS_RBTREE_NODE         (node);
    union {
        struct {
            njs_function_t      *function;
            njs_value_t         *args;
        }                       njs;
#if (NJS_HAVE_QUICKJS)
        struct {
            JSValue             function;
            JSValue             *args;
        }                       qjs;
#endif
    }                           u;

    njs_uint_t              nargs;
    uint32_t                id;

    njs_queue_link_t        link;
} njs_ev_t;


typedef struct {
    njs_str_t               name;
    uint64_t                time;
    njs_queue_link_t        link;
} njs_timelabel_t;


typedef struct {
    union {
        struct {
            njs_opaque_value_t  promise;
            njs_opaque_value_t  message;
        }                       njs;
#if (NJS_HAVE_QUICKJS)
        struct {
            JSValue             promise;
            JSValue             message;
        }                       qjs;
#endif
    }                           u;
} njs_rejected_promise_t;


typedef struct {
    int                 fd;
    njs_str_t           name;
    njs_str_t           file;
    char                path[NJS_MAX_PATH + 1];
} njs_module_info_t;


typedef struct  njs_engine_s     njs_engine_t;


struct njs_engine_s {
    union {
        struct {
            njs_vm_t            *vm;

            njs_opaque_value_t  value;
        }                       njs;
#if (NJS_HAVE_QUICKJS)
        struct {
            JSRuntime           *rt;
            JSContext           *ctx;
            JSValue             value;
        }                       qjs;
#endif
    }                           u;

    njs_int_t                 (*eval)(njs_engine_t *engine, njs_str_t *script);
    njs_int_t                 (*execute_pending_job)(njs_engine_t *engine);
    njs_int_t                 (*unhandled_rejection)(njs_engine_t *engine);
    njs_int_t                 (*process_events)(njs_engine_t *engine);
    njs_int_t                 (*destroy)(njs_engine_t *engine);
    njs_int_t                 (*output)(njs_engine_t *engine, njs_int_t ret);
    njs_arr_t                *(*complete)(njs_engine_t *engine, njs_str_t *ex);

    unsigned                    type;
    njs_mp_t                    *pool;
    njs_completion_t            completion;
};

typedef struct {
    njs_engine_t            *engine;

    uint32_t                event_id;
    njs_rbtree_t            events;  /* njs_ev_t * */
    njs_queue_t             posted_events;

    njs_queue_t             labels;

    njs_str_t               cwd;

    njs_arr_t               *rejected_promises;

    njs_bool_t              suppress_stdout;
    njs_bool_t              interactive;
    njs_bool_t              module;
    char                    **argv;
    njs_uint_t              argc;

#if (NJS_HAVE_QUICKJS)
    JSValue                 process;

    njs_queue_t             agents;
    njs_queue_t             reports;
    pthread_mutex_t         agent_mutex;
    pthread_cond_t          agent_cond;
    pthread_mutex_t         report_mutex;
#endif
} njs_console_t;


#if (NJS_HAVE_QUICKJS)
typedef struct {
    njs_queue_link_t        link;
    pthread_t               tid;
    njs_console_t           *console;
    char                    *script;
    JSValue                 broadcast_func;
    njs_bool_t              broadcast_pending;
    JSValue                 broadcast_sab;
    uint8_t                 *broadcast_sab_buf;
    size_t                  broadcast_sab_size;
    int32_t                 broadcast_val;
} njs_262agent_t;


typedef struct {
    njs_queue_link_t        link;
    char                    *str;
} njs_agent_report_t;
#endif


static njs_int_t njs_main(njs_opts_t *opts);
static njs_int_t njs_console_init(njs_opts_t *opts, njs_console_t *console);
static njs_int_t njs_externals_init(njs_vm_t *vm);
static njs_engine_t *njs_create_engine(njs_opts_t *opts);
static njs_int_t njs_process_file(njs_opts_t *opts);
static njs_int_t njs_process_script(njs_engine_t *engine,
    njs_console_t *console, njs_str_t *script);

#ifndef NJS_FUZZER_TARGET

static njs_int_t njs_options_parse(njs_opts_t *opts, int argc, char **argv);
static njs_int_t njs_options_parse_engine(njs_opts_t *opts, const char *engine);
static njs_int_t njs_options_add_path(njs_opts_t *opts, char *path, size_t len);
static void njs_options_free(njs_opts_t *opts);

#ifdef NJS_HAVE_READLINE
static njs_int_t njs_interactive_shell(njs_opts_t *opts);
static njs_int_t njs_editline_init(void);
static char *njs_completion_generator(const char *text, int state);
#endif

#endif

static njs_int_t njs_set_timeout(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t njs_set_immediate(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t njs_clear_timeout(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t njs_ext_console_log(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t magic, njs_value_t *retval);
static njs_int_t njs_ext_console_time(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
static njs_int_t njs_ext_console_time_end(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);

static void njs_console_log(njs_log_level_t level, const char *fmt, ...);
static void njs_console_logger(njs_log_level_t level, const u_char *start,
    size_t length);

static njs_int_t njs_console_time(njs_console_t *console, njs_str_t *name);
static void njs_console_time_end(njs_console_t *console, njs_str_t *name,
    uint64_t time);
static intptr_t njs_event_rbtree_compare(njs_rbtree_node_t *node1,
    njs_rbtree_node_t *node2);
static uint64_t njs_time(void);

njs_int_t njs_array_buffer_detach(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);


static njs_external_t  njs_ext_console[] = {

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("dump"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_log,
#define NJS_LOG_DUMP  16
#define NJS_LOG_MASK  15
            .magic8 = NJS_LOG_INFO | NJS_LOG_DUMP,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("error"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_log,
            .magic8 = NJS_LOG_ERROR,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("info"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_log,
            .magic8 = NJS_LOG_INFO,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("log"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_log,
            .magic8 = NJS_LOG_INFO,
        }
    },

    {
        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
        .u.property = {
            .value = "Console",
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("time"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_time,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("timeEnd"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_time_end,
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("warn"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_ext_console_log,
            .magic8 = NJS_LOG_WARN,
        }
    },

};


static njs_external_t  njs_ext_262[] = {

    {
        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
        .u.property = {
            .value = "$262",
        }
    },

    {
        .flags = NJS_EXTERN_METHOD,
        .name.string = njs_str("detachArrayBuffer"),
        .writable = 1,
        .configurable = 1,
        .enumerable = 1,
        .u.method = {
            .native = njs_array_buffer_detach,
        }
    },

};


njs_module_t  njs_console_module = {
    .name = njs_str("console"),
    .preinit = NULL,
    .init = njs_externals_init,
};


static njs_module_t *njs_console_addon_modules[] = {
    &njs_console_module,
    NULL,
};


static njs_int_t      njs_console_proto_id;


static njs_console_t  njs_console;


static njs_int_t
njs_main(njs_opts_t *opts)
{
    njs_int_t     ret;
    njs_engine_t  *engine;

    if (opts->file == NULL) {
        if (opts->command.length != 0) {
            opts->file = (char *) "string";
        }

#ifdef NJS_HAVE_READLINE
        else if (opts->interactive) {
            opts->file = (char *) "shell";
        }
#endif

        if (opts->file == NULL) {
            njs_stderror("file name is required in non-interactive mode\n");
            return NJS_ERROR;
        }
    }

    ret = njs_console_init(opts, &njs_console);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_stderror("njs_console_init() failed\n");
        return NJS_ERROR;
    }

#if (!defined NJS_FUZZER_TARGET && defined NJS_HAVE_READLINE)

    if (opts->interactive) {
        ret = njs_interactive_shell(opts);

    } else

#endif

    if (opts->command.length != 0) {
        engine = njs_create_engine(opts);
        if (engine == NULL) {
            return NJS_ERROR;
        }

        ret = njs_process_script(engine, &njs_console, &opts->command);
        engine->destroy(engine);

    } else {
        ret = njs_process_file(opts);
    }

    return ret;
}


#ifndef NJS_FUZZER_TARGET

int
main(int argc, char **argv)
{
    njs_int_t   ret;
    njs_opts_t  opts;

    njs_memzero(&opts, sizeof(njs_opts_t));
    opts.interactive = 1;

    ret = njs_options_parse(&opts, argc, argv);
    if (ret != NJS_OK) {
        ret = (ret == NJS_DONE) ? NJS_OK : NJS_ERROR;
        goto done;
    }

    if (opts.version != 0) {
        njs_printf("%s\n", NJS_VERSION);
        ret = NJS_OK;
        goto done;
    }

    ret = njs_main(&opts);

done:

    njs_options_free(&opts);

    return (ret == NJS_OK) ? EXIT_SUCCESS : opts.exit_code;
}


static njs_int_t
njs_options_parse(njs_opts_t *opts, int argc, char **argv)
{
    char        *p, *start;
    size_t      len;
    njs_int_t   i, ret;
    njs_uint_t  n;

    static const char  help[] =
        "njs [options] [-c string | script.js | -] [script args]\n"
        "\n"
        "Interactive shell: "
#ifdef NJS_HAVE_READLINE
        "enabled\n"
#else
        "disabled\n"
#endif
        "\n"
        "Options:\n"
        "  -a                print AST.\n"
        "  -c                specify the command to execute.\n"
        "  -d                print disassembled code.\n"
        "  -e <code>         set failure exit code.\n"
        "  -f                disabled denormals mode.\n"
#ifdef NJS_DEBUG_GENERATOR
        "  -g                enable generator debug.\n"
#endif
        "  -j <size>         set the maximum stack size in bytes.\n"
        "  -m                load as ES6 module (script is default).\n"
#ifdef NJS_HAVE_QUICKJS
        "  -n njs|QuickJS    set JS engine (njs is default)\n"
#endif
#ifdef NJS_DEBUG_OPCODE
        "  -o                enable opcode debug.\n"
#endif
        "  -p <path>         set path prefix for modules.\n"
        "  -q                disable interactive introduction prompt.\n"
        "  -r                ignore unhandled promise rejection.\n"
        "  -s                sandbox mode.\n"
        "  -v                print njs version and exit.\n"
        "  -u                disable \"unsafe\" mode.\n"
        "  script.js | -     run code from a file or stdin.\n";

    opts->denormals = 1;
    opts->can_block = 1;
    opts->exit_code = EXIT_FAILURE;
    opts->engine = NJS_ENGINE_NJS;
    opts->unhandled_rejection = 1;

    p = getenv("NJS_EXIT_CODE");
    if (p != NULL) {
        opts->exit_code = atoi(p);
    }

    p = getenv("NJS_CAN_BLOCK");
    if (p != NULL) {
        opts->can_block = atoi(p);
    }

    p = getenv("NJS_LOAD_AS_MODULE");
    if (p != NULL) {
        opts->module = 1;
    }

    p = getenv("NJS_ENGINE");
    if (p != NULL) {
        ret = njs_options_parse_engine(opts, p);
        if (ret != NJS_OK) {
            return NJS_ERROR;
        }
    }

    start = getenv("NJS_PATH");
    if (start != NULL) {
        for ( ;; ) {
            p = (char *) njs_strchr(start, ':');

            len = (p != NULL) ? (size_t) (p - start) : njs_strlen(start);

            ret = njs_options_add_path(opts, start, len);
            if (ret != NJS_OK) {
                njs_stderror("failed to add path\n");
                return NJS_ERROR;
            }

            if (p == NULL) {
                break;
            }

            start = p + 1;
        }
    }

    for (i = 1; i < argc; i++) {

        p = argv[i];

        if (p[0] != '-' || (p[0] == '-' && p[1] == '\0')) {
            opts->interactive = 0;
            opts->file = argv[i];
            goto done;
        }

        p++;

        switch (*p) {
        case '?':
        case 'h':
            njs_printf("%*s", njs_length(help), help);
            return NJS_DONE;

        case 'a':
            opts->ast = 1;
            break;

        case 'c':
            opts->interactive = 0;

            if (++i < argc) {
                opts->command.start = (u_char *) argv[i];
                opts->command.length = njs_strlen(argv[i]);
                goto done;
            }

            njs_stderror("option \"-c\" requires argument\n");
            return NJS_ERROR;

        case 'd':
            opts->disassemble = 1;
            break;

        case 'e':
            if (++i < argc) {
                opts->exit_code = atoi(argv[i]);
                break;
            }

            njs_stderror("option \"-e\" requires argument\n");
            return NJS_ERROR;

        case 'f':
            opts->denormals = 0;
            break;

#ifdef NJS_DEBUG_GENERATOR
        case 'g':
            opts->generator_debug = 1;
            break;
#endif
        case 'j':
            if (++i < argc) {
                opts->stack_size = atoi(argv[i]);
                break;
            }

            njs_stderror("option \"-j\" requires argument\n");
            return NJS_ERROR;

        case 'm':
            opts->module = 1;
            break;

        case 'n':
            if (++i < argc) {
                ret = njs_options_parse_engine(opts, argv[i]);
                if (ret != NJS_OK) {
                    return NJS_ERROR;
                }

                break;
            }

            njs_stderror("option \"-n\" requires argument\n");
            return NJS_ERROR;

#ifdef NJS_DEBUG_OPCODE
        case 'o':
            opts->opcode_debug = 1;
            break;
#endif

        case 'p':
            if (++i < argc) {
                ret = njs_options_add_path(opts, argv[i], njs_strlen(argv[i]));
                if (ret != NJS_OK) {
                    njs_stderror("failed to add path\n");
                    return NJS_ERROR;
                }

                break;
            }

            njs_stderror("option \"-p\" requires directory name\n");
            return NJS_ERROR;

        case 'q':
            opts->quiet = 1;
            break;

        case 'r':
            opts->unhandled_rejection = 0;
            break;

        case 's':
            opts->sandbox = 1;
            break;

        case 't':
            if (++i < argc) {
                if (strcmp(argv[i], "module") == 0) {
                    opts->module = 1;

                } else if (strcmp(argv[i], "script") != 0) {
                    njs_stderror("option \"-t\" unexpected source type: %s\n",
                                 argv[i]);
                    return NJS_ERROR;
                }

                break;
            }

            njs_stderror("option \"-t\" requires source type\n");
            return NJS_ERROR;
        case 'v':
        case 'V':
            opts->version = 1;
            break;

        case 'u':
            opts->safe = 1;
            break;

        default:
            njs_stderror("Unknown argument: \"%s\" "
                         "try \"%s -h\" for available options\n", argv[i],
                         argv[0]);
            return NJS_ERROR;
        }
    }

done:

#ifdef NJS_HAVE_QUICKJS
    if (opts->engine == NJS_ENGINE_QUICKJS) {
        if (opts->ast) {
            njs_stderror("option \"-a\" is not supported for quickjs\n");
            return NJS_ERROR;
        }

        if (opts->disassemble) {
            njs_stderror("option \"-d\" is not supported for quickjs\n");
            return NJS_ERROR;
        }

        if (opts->generator_debug) {
            njs_stderror("option \"-g\" is not supported for quickjs\n");
            return NJS_ERROR;
        }

        if (opts->opcode_debug) {
            njs_stderror("option \"-o\" is not supported for quickjs\n");
            return NJS_ERROR;
        }

        if (opts->sandbox) {
            njs_stderror("option \"-s\" is not supported for quickjs\n");
            return NJS_ERROR;
        }

        if (opts->safe) {
            njs_stderror("option \"-u\" is not supported for quickjs\n");
            return NJS_ERROR;
        }
    }
#endif

    opts->argc = njs_max(argc - i + 1, 2);
    opts->argv = malloc(sizeof(char*) * opts->argc);
    if (opts->argv == NULL) {
        njs_stderror("failed to alloc argv\n");
        return NJS_ERROR;
    }

    opts->argv[0] = argv[0];
    opts->argv[1] = (opts->file != NULL) ? opts->file : (char *) "";
    for (n = 2; n < opts->argc; n++) {
        opts->argv[n] = argv[i + n - 1];
    }

    return NJS_OK;
}


static njs_int_t
njs_options_parse_engine(njs_opts_t *opts, const char *engine)
{
    if (strncasecmp(engine, "njs", 3) == 0) {
        opts->engine = NJS_ENGINE_NJS;

#ifdef NJS_HAVE_QUICKJS
    } else if (strncasecmp(engine, "QuickJS", 7) == 0) {
        opts->engine = NJS_ENGINE_QUICKJS;
#endif

    } else {
        njs_stderror("unknown engine \"%s\"\n", engine);
        return NJS_ERROR;
    }

    return NJS_OK;
}


static njs_int_t
njs_options_add_path(njs_opts_t *opts, char *path, size_t len)
{
    njs_str_t  *paths;

    opts->n_paths++;

    paths = realloc(opts->paths, opts->n_paths * sizeof(njs_str_t));
    if (paths == NULL) {
        njs_stderror("failed to add path\n");
        return NJS_ERROR;
    }

    opts->paths = paths;
    opts->paths[opts->n_paths - 1].start = (u_char *) path;
    opts->paths[opts->n_paths - 1].length = len;

    return NJS_OK;
}


static void
njs_options_free(njs_opts_t *opts)
{
    if (opts->paths != NULL) {
        free(opts->paths);
    }

    if (opts->argv != NULL) {
        free(opts->argv);
    }
}


#else

int
LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
{
    u_char      *buf;
    njs_opts_t  opts;

    if (size == 0) {
        return 0;
    }

    buf = malloc(size + 1);
    if (buf == NULL) {
        return 0;
    }

    memcpy(buf, data, size);
    buf[size] = '\0';

    njs_memzero(&opts, sizeof(njs_opts_t));

    opts.file = (char *) "fuzzer";
    opts.command.start = buf;
    opts.command.length = size;
    opts.suppress_stdout = 1;

    (void) njs_main(&opts);

    free(buf);

    return 0;
}

#endif

static njs_int_t
njs_console_init(njs_opts_t *opts, njs_console_t *console)
{
    njs_memzero(console, sizeof(njs_console_t));

    njs_rbtree_init(&console->events, njs_event_rbtree_compare);
    njs_queue_init(&console->posted_events);
    njs_queue_init(&console->labels);

    console->interactive = opts->interactive;
    console->suppress_stdout = opts->suppress_stdout;
    console->module = opts->module;
    console->argv = opts->argv;
    console->argc = opts->argc;

#if (NJS_HAVE_QUICKJS)
    if (opts->engine == NJS_ENGINE_QUICKJS) {
        njs_queue_init(&console->agents);
        njs_queue_init(&console->reports);
        pthread_mutex_init(&console->report_mutex, NULL);
        pthread_mutex_init(&console->agent_mutex, NULL);
        pthread_cond_init(&console->agent_cond, NULL);

        console->process = JS_UNDEFINED;
    }
#endif

    return NJS_OK;
}


static njs_int_t
njs_function_bind(njs_vm_t *vm, const njs_str_t *name,
    njs_function_native_t native, njs_bool_t ctor)
{
    njs_function_t      *f;
    njs_opaque_value_t   value;

    f = njs_vm_function_alloc(vm, native, 1, ctor);
    if (f == NULL) {
        return NJS_ERROR;
    }

    njs_value_function_set(njs_value_arg(&value), f);

    return njs_vm_bind(vm, name, njs_value_arg(&value), 1);
}


static njs_int_t
njs_externals_init(njs_vm_t *vm)
{
    njs_int_t           ret, proto_id;
    njs_console_t       *console;
    njs_opaque_value_t  value, method;

    static const njs_str_t  console_name = njs_str("console");
    static const njs_str_t  dollar_262 = njs_str("$262");
    static const njs_str_t  print_name = njs_str("print");
    static const njs_str_t  console_log = njs_str("console.log");
    static const njs_str_t  set_timeout = njs_str("setTimeout");
    static const njs_str_t  set_immediate = njs_str("setImmediate");
    static const njs_str_t  clear_timeout = njs_str("clearTimeout");

    console = njs_vm_external_ptr(vm);

    njs_console_proto_id = njs_vm_external_prototype(vm, njs_ext_console,
                                         njs_nitems(njs_ext_console));
    if (njs_slow_path(njs_console_proto_id < 0)) {
        njs_stderror("failed to add \"console\" proto\n");
        return NJS_ERROR;
    }

    ret = njs_vm_external_create(vm, njs_value_arg(&value),
                                 njs_console_proto_id, console, 0);
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    ret = njs_vm_bind(vm, &console_name, njs_value_arg(&value), 0);
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    ret = njs_vm_value(vm, &console_log, njs_value_arg(&method));
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    ret = njs_vm_bind(vm, &print_name, njs_value_arg(&method), 0);
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    ret = njs_function_bind(vm, &set_timeout, njs_set_timeout, 0);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = njs_function_bind(vm, &set_immediate, njs_set_immediate, 0);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    ret = njs_function_bind(vm, &clear_timeout, njs_clear_timeout, 0);
    if (ret != NJS_OK) {
        return NJS_ERROR;
    }

    proto_id = njs_vm_external_prototype(vm, njs_ext_262,
                                         njs_nitems(njs_ext_262));
    if (njs_slow_path(proto_id < 0)) {
        njs_stderror("failed to add \"$262\" proto\n");
        return NJS_ERROR;
    }

    ret = njs_vm_external_create(vm,  njs_value_arg(&value), proto_id, NULL, 1);
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    ret = njs_vm_bind(vm, &dollar_262, njs_value_arg(&value), 1);
    if (njs_slow_path(ret != NJS_OK)) {
        return NJS_ERROR;
    }

    return NJS_OK;
}


static void
njs_rejection_tracker(njs_vm_t *vm, njs_external_ptr_t external,
    njs_bool_t is_handled, njs_value_t *promise, njs_value_t *reason)
{
    void                    *promise_obj;
    uint32_t                i, length;
    njs_console_t           *console;
    njs_rejected_promise_t  *rejected_promise;

    console = external;

    if (is_handled && console->rejected_promises != NULL) {
        rejected_promise = console->rejected_promises->start;
        length = console->rejected_promises->items;

        promise_obj = njs_value_ptr(promise);

        for (i = 0; i < length; i++) {
            if (njs_value_ptr(njs_value_arg(&rejected_promise[i].u.njs.promise))
                == promise_obj)
            {
                njs_arr_remove(console->rejected_promises,
                               &rejected_promise[i]);

                break;
            }
        }

        return;
    }

    if (console->rejected_promises == NULL) {
        console->rejected_promises = njs_arr_create(console->engine->pool, 4,
                                                sizeof(njs_rejected_promise_t));
        if (njs_slow_path(console->rejected_promises == NULL)) {
            return;
        }
    }

    rejected_promise = njs_arr_add(console->rejected_promises);
    if (njs_slow_path(rejected_promise == NULL)) {
        return;
    }

    njs_value_assign(&rejected_promise->u.njs.promise, promise);
    njs_value_assign(&rejected_promise->u.njs.message, reason);
}


static njs_int_t
njs_module_path(const njs_str_t *dir, njs_module_info_t *info)
{
    char        *p;
    size_t      length;
    njs_bool_t  trail;
    char        src[NJS_MAX_PATH + 1];

    trail = 0;
    length = info->name.length;

    if (dir != NULL) {
        length += dir->length;

        if (length == 0 || dir->length == 0) {
            return NJS_DECLINED;
        }

        trail = (dir->start[dir->length - 1] != '/');

        if (trail) {
            length++;
        }
    }

    if (njs_slow_path(length > NJS_MAX_PATH)) {
        return NJS_ERROR;
    }

    p = &src[0];

    if (dir != NULL) {
        p = (char *) njs_cpymem(p, dir->start, dir->length);

        if (trail) {
            *p++ = '/';
        }
    }

    p = (char *) njs_cpymem(p, info->name.start, info->name.length);
    *p = '\0';

    p = realpath(&src[0], &info->path[0]);
    if (p == NULL) {
        return NJS_DECLINED;
    }

    info->fd = open(&info->path[0], O_RDONLY);
    if (info->fd < 0) {
        return NJS_DECLINED;
    }

    info->file.start = (u_char *) &info->path[0];
    info->file.length = njs_strlen(info->file.start);

    return NJS_OK;
}


static njs_int_t
njs_module_lookup(njs_opts_t *opts, const njs_str_t *cwd,
    njs_module_info_t *info)
{
    njs_int_t   ret;
    njs_str_t   *path;
    njs_uint_t  i;

    if (info->name.start[0] == '/') {
        return njs_module_path(NULL, info);
    }

    ret = njs_module_path(cwd, info);

    if (ret != NJS_DECLINED) {
        return ret;
    }

    path = opts->paths;

    for (i = 0; i < opts->n_paths; i++) {
        ret = njs_module_path(&path[i], info);

        if (ret != NJS_DECLINED) {
            return ret;
        }
    }

    return NJS_DECLINED;
}


static njs_int_t
njs_module_read(njs_mp_t *mp, int fd, njs_str_t *text)
{
    ssize_t      n;
    struct stat  sb;

    text->start = NULL;

    if (fstat(fd, &sb) == -1) {
        goto fail;
    }

    if (!S_ISREG(sb.st_mode)) {
        goto fail;
    }

    text->length = sb.st_size;

    text->start = njs_mp_alloc(mp, text->length + 1);
    if (text->start == NULL) {
        goto fail;
    }

    n = read(fd, text->start, sb.st_size);

    if (n < 0 || n != sb.st_size) {
        goto fail;
    }

    text->start[text->length] = '\0';

    return NJS_OK;

fail:

    if (text->start != NULL) {
        njs_mp_free(mp, text->start);
    }

    return NJS_ERROR;
}


static void
njs_file_dirname(const njs_str_t *path, njs_str_t *name)
{
    const u_char  *p, *end;

    if (path->length == 0) {
        goto current_dir;
    }

    p = path->start + path->length - 1;

    /* Stripping basename. */

    while (p >= path->start && *p != '/') { p--; }

    end = p + 1;

    if (end == path->start) {
        goto current_dir;
    }

    /* Stripping trailing slashes. */

    while (p >= path->start && *p == '/') { p--; }

    p++;

    if (p == path->start) {
        p = end;
    }

    name->start = path->start;
    name->length = p - path->start;

    return;

current_dir:

    *name = njs_str_value(".");
}


static njs_int_t
njs_console_set_cwd(njs_console_t *console, njs_str_t *file)
{
    njs_str_t  cwd;

    njs_file_dirname(file, &cwd);

    console->cwd.start = njs_mp_alloc(console->engine->pool, cwd.length);
    if (njs_slow_path(console->cwd.start == NULL)) {
        return NJS_ERROR;
    }

    memcpy(console->cwd.start, cwd.start, cwd.length);
    console->cwd.length = cwd.length;

    return NJS_OK;
}


static njs_mod_t *
njs_module_loader(njs_vm_t *vm, njs_external_ptr_t external, njs_str_t *name)
{
    u_char             *start;
    njs_int_t          ret;
    njs_str_t          text, prev_cwd;
    njs_mod_t          *module;
    njs_opts_t         *opts;
    njs_console_t      *console;
    njs_module_info_t  info;

    opts = external;
    console = njs_vm_external_ptr(vm);

    njs_memzero(&info, sizeof(njs_module_info_t));

    info.name = *name;

    ret = njs_module_lookup(opts, &console->cwd, &info);
    if (njs_slow_path(ret != NJS_OK)) {
        return NULL;
    }

    ret = njs_module_read(console->engine->pool, info.fd, &text);

    (void) close(info.fd);

    if (njs_slow_path(ret != NJS_OK)) {
        njs_vm_internal_error(vm, "while reading \"%V\" module", &info.file);
        return NULL;
    }

    prev_cwd = console->cwd;

    ret = njs_console_set_cwd(console, &info.file);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_vm_internal_error(vm, "while setting cwd for \"%V\" module",
                              &info.file);
        return NULL;
    }

    start = text.start;

    module = njs_vm_compile_module(vm, &info.file, &start,
                                   &text.start[text.length]);

    njs_mp_free(console->engine->pool, console->cwd.start);
    console->cwd = prev_cwd;

    njs_mp_free(console->engine->pool, text.start);

    return module;
}


static njs_int_t
njs_engine_njs_init(njs_engine_t *engine, njs_opts_t *opts)
{
    njs_vm_t      *vm;
    njs_int_t     ret;
    njs_vm_opt_t  vm_options;

    njs_vm_opt_init(&vm_options);

    vm_options.file.start = (u_char *) opts->file;
    vm_options.file.length = njs_strlen(opts->file);

    vm_options.init = 1;
    vm_options.interactive = opts->interactive;
    vm_options.disassemble = opts->disassemble;
    vm_options.backtrace = 1;
    vm_options.quiet = opts->quiet;
    vm_options.sandbox = opts->sandbox;
    vm_options.unsafe = !opts->safe;
    vm_options.module = opts->module;
#ifdef NJS_DEBUG_GENERATOR
    vm_options.generator_debug = opts->generator_debug;
#endif
#ifdef NJS_DEBUG_OPCODE
    vm_options.opcode_debug = opts->opcode_debug;
#endif

    vm_options.addons = njs_console_addon_modules;
    vm_options.external = &njs_console;
    vm_options.argv = opts->argv;
    vm_options.argc = opts->argc;
    vm_options.ast = opts->ast;

    if (opts->stack_size != 0) {
        vm_options.max_stack_size = opts->stack_size;
    }

    vm = njs_vm_create(&vm_options);
    if (vm == NULL) {
        njs_stderror("failed to create vm\n");
        return NJS_ERROR;
    }

    if (opts->unhandled_rejection) {
        njs_vm_set_rejection_tracker(vm, njs_rejection_tracker,
                                     njs_vm_external_ptr(vm));
    }

    ret = njs_console_set_cwd(njs_vm_external_ptr(vm), &vm_options.file);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_stderror("failed to set cwd\n");
        return NJS_ERROR;
    }

    njs_vm_set_module_loader(vm, njs_module_loader, opts);

    engine->u.njs.vm = vm;

    return NJS_OK;
}


static njs_int_t
njs_engine_njs_destroy(njs_engine_t *engine)
{
    njs_int_t  ret;

    (void) njs_vm_call_exit_hook(engine->u.njs.vm);

    for ( ;; ) {
        ret = njs_vm_execute_pending_job(engine->u.njs.vm);
        if (ret == NJS_OK) {
            break;
        }
    }

    njs_vm_destroy(engine->u.njs.vm);
    njs_mp_destroy(engine->pool);

    return NJS_OK;
}


static njs_int_t
njs_engine_njs_eval(njs_engine_t *engine, njs_str_t *script)
{
     u_char     *start, *end;
     njs_int_t  ret;

     start = script->start;
     end = start + script->length;

     ret = njs_vm_compile(engine->u.njs.vm, &start, end);

     if (ret == NJS_OK && start == end) {
        return njs_vm_start(engine->u.njs.vm,
                           njs_value_arg(&engine->u.njs.value));
     }

     return NJS_ERROR;
}


static njs_int_t
njs_engine_njs_execute_pending_job(njs_engine_t *engine)
{
    return njs_vm_execute_pending_job(engine->u.njs.vm);
}


static njs_int_t
njs_engine_njs_output(njs_engine_t *engine, njs_int_t ret)
{
    njs_vm_t       *vm;
    njs_str_t      out;
    njs_console_t  *console;

    vm = engine->u.njs.vm;
    console = njs_vm_external_ptr(vm);

    if (ret == NJS_OK) {
        if (console->interactive) {
            if (njs_vm_value_dump(vm, &out, njs_value_arg(&engine->u.njs.value),
                                  0, 1)
                != NJS_OK)
            {
                njs_stderror("Shell:failed to get retval from VM\n");
                return NJS_ERROR;
            }

            njs_print(out.start, out.length);
            njs_print("\n", 1);
        }

    } else {
        njs_vm_exception_string(vm, &out);
        njs_stderror("Thrown:\n%V\n", &out);
    }

    return NJS_OK;
}


static njs_arr_t *
njs_object_completions(njs_vm_t *vm, njs_value_t *object, njs_str_t *expression)
{
    u_char              *prefix;
    size_t              len, prefix_len;
    int64_t             k, n, length;
    njs_int_t           ret;
    njs_arr_t           *array;
    njs_str_t           *completion, key;
    njs_value_t         *keys;
    njs_opaque_value_t  *start, retval, prototype;

    prefix = expression->start + expression->length;

    while (prefix > expression->start && *prefix != '.') {
        prefix--;
    }

    if (prefix != expression->start) {
        prefix++;
    }

    prefix_len = prefix - expression->start;
    len = expression->length - prefix_len;

    array = njs_arr_create(njs_vm_memory_pool(vm), 8, sizeof(njs_str_t));
    if (njs_slow_path(array == NULL)) {
        goto fail;
    }

    while (!njs_value_is_null(object)) {
        keys = njs_vm_value_enumerate(vm, object, NJS_ENUM_KEYS
                                      | NJS_ENUM_STRING,
                                      njs_value_arg(&retval));
        if (njs_slow_path(keys == NULL)) {
            goto fail;
        }

        (void) njs_vm_array_length(vm, keys, &length);

        start = (njs_opaque_value_t *) njs_vm_array_start(vm, keys);
        if (start == NULL) {
            goto fail;
        }


        for (n = 0; n < length; n++) {
            ret = njs_vm_value_to_string(vm, &key, njs_value_arg(start));
            if (njs_slow_path(ret != NJS_OK)) {
                goto fail;
            }

            start++;

            if (len > key.length || njs_strncmp(key.start, prefix, len) != 0) {
                continue;
            }

            for (k = 0; k < array->items; k++) {
                completion = njs_arr_item(array, k);

                if ((completion->length - prefix_len - 1) == key.length
                    && njs_strncmp(&completion->start[prefix_len],
                                   key.start, key.length)
                       == 0)
                {
                    break;
                }
            }

            if (k != array->items) {
                continue;
            }

            completion = njs_arr_add(array);
            if (njs_slow_path(completion == NULL)) {
                goto fail;
            }

            completion->length = prefix_len + key.length + 1;
            completion->start = njs_mp_alloc(njs_vm_memory_pool(vm),
                                             completion->length);
            if (njs_slow_path(completion->start == NULL)) {
                goto fail;
            }


            njs_sprintf(completion->start,
                        completion->start + completion->length,
                        "%*s%V%Z", prefix_len, expression->start, &key);
        }

        ret = njs_vm_prototype(vm, object, njs_value_arg(&prototype));
        if (njs_slow_path(ret != NJS_OK)) {
            goto fail;
        }

        object = njs_value_arg(&prototype);
    }

    return array;

fail:

    if (array != NULL) {
        njs_arr_destroy(array);
    }

    return NULL;
}


static njs_arr_t *
njs_engine_njs_complete(njs_engine_t *engine, njs_str_t *expression)
{
    u_char              *p, *start, *end;
    njs_vm_t            *vm;
    njs_int_t           ret;
    njs_bool_t          global;
    njs_opaque_value_t  value, key, retval;

    vm = engine->u.njs.vm;

    p = expression->start;
    end = p + expression->length;

    global = 1;
    (void) njs_vm_global(vm, njs_value_arg(&value));

    while (p < end && *p != '.') { p++; }

    if (p == end) {
        goto done;
    }

    p = expression->start;

    for ( ;; ) {

        start = (*p == '.' && p < end) ? ++p: p;

        if (p == end) {
            break;
        }

        while (p < end && *p != '.') { p++; }

        ret = njs_vm_value_string_create(vm, njs_value_arg(&key), start,
                                         p - start);
        if (njs_slow_path(ret != NJS_OK)) {
            return NULL;
        }

        ret = njs_value_property_val(vm, njs_value_arg(&value),
                                     njs_value_arg(&key),
                                     njs_value_arg(&retval));
        if (njs_slow_path(ret != NJS_OK)) {
            if (ret == NJS_DECLINED && !global) {
                goto done;
            }

            return NULL;
        }

        global = 0;
        njs_value_assign(&value, &retval);
    }

done:

    return njs_object_completions(vm, njs_value_arg(&value), expression);
}


static njs_int_t
njs_engine_njs_process_events(njs_engine_t *engine)
{
    njs_ev_t            *ev;
    njs_vm_t            *vm;
    njs_int_t           ret;
    njs_queue_t         *events;
    njs_console_t       *console;
    njs_queue_link_t    *link;
    njs_opaque_value_t  retval;

    vm = engine->u.njs.vm;
    console = njs_vm_external_ptr(vm);
    events = &console->posted_events;

    for ( ;; ) {
        link = njs_queue_first(events);

        if (link == njs_queue_tail(events)) {
            break;
        }

        ev = njs_queue_link_data(link, njs_ev_t, link);

        njs_queue_remove(&ev->link);
        njs_rbtree_delete(&console->events, &ev->node);

        ret = njs_vm_invoke(vm, ev->u.njs.function, ev->u.njs.args, ev->nargs,
                            njs_value_arg(&retval));
        if (ret == NJS_ERROR) {
            njs_engine_njs_output(engine, ret);

            if (!console->interactive) {
                return NJS_ERROR;
            }
        }
    }

    if (!njs_rbtree_is_empty(&console->events)) {
        return NJS_AGAIN;
    }

    return njs_vm_pending(vm) ? NJS_AGAIN: NJS_OK;
}


static njs_int_t
njs_engine_njs_unhandled_rejection(njs_engine_t *engine)
{
    njs_vm_t                *vm;
    njs_int_t               ret;
    njs_str_t               message;
    njs_console_t           *console;
    njs_rejected_promise_t  *rejected_promise;

    vm = engine->u.njs.vm;
    console = njs_vm_external_ptr(vm);

    if (console->rejected_promises == NULL
        || console->rejected_promises->items == 0)
    {
        return 0;
    }

    rejected_promise = console->rejected_promises->start;

    ret = njs_vm_value_to_string(vm, &message,
                               njs_value_arg(&rejected_promise->u.njs.message));
    if (njs_slow_path(ret != NJS_OK)) {
        return -1;
    }

    njs_vm_error(vm, "unhandled promise rejection: %V", &message);

    njs_arr_destroy(console->rejected_promises);
    console->rejected_promises = NULL;

    return 1;
}

#ifdef NJS_HAVE_QUICKJS

static JSValue
njs_qjs_console_log(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv, int magic)
{
    int         i;
    size_t      len;
    const char  *str;

    for (i = 0; i < argc; i++) {
        str = JS_ToCStringLen(ctx, &len, argv[i]);
        if (!str) {
            return JS_EXCEPTION;
        }

        njs_console_logger(magic, (const u_char*) str, len);
        JS_FreeCString(ctx, str);
    }

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_console_time(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    njs_str_t      name;
    const char     *str;
    njs_console_t  *console;

    static const njs_str_t  default_label = njs_str("default");

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    name = default_label;

    if (argc > 0 && !JS_IsUndefined(argv[0])) {
        str = JS_ToCStringLen(ctx, &name.length, argv[0]);
        if (str == NULL) {
            return JS_EXCEPTION;
        }

        name.start = njs_mp_alloc(console->engine->pool, name.length);
        if (njs_slow_path(name.start == NULL)) {
            JS_ThrowOutOfMemory(ctx);
            return JS_EXCEPTION;
        }

        (void) memcpy(name.start, str, name.length);

        JS_FreeCString(ctx, str);
    }

    if (njs_console_time(console, &name) != NJS_OK) {
        return JS_EXCEPTION;
    }

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_console_time_end(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    uint64_t       ns;
    njs_str_t      name;
    const char     *str;
    njs_console_t  *console;

    static const njs_str_t  default_label = njs_str("default");

    ns = njs_time();

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    name = default_label;

    if (argc > 0 && !JS_IsUndefined(argv[0])) {
        str = JS_ToCStringLen(ctx, &name.length, argv[0]);
        if (str == NULL) {
            return JS_EXCEPTION;
        }

        name.start = njs_mp_alloc(console->engine->pool, name.length);
        if (njs_slow_path(name.start == NULL)) {
            JS_ThrowOutOfMemory(ctx);
            return JS_EXCEPTION;
        }

        (void) memcpy(name.start, str, name.length);

        JS_FreeCString(ctx, str);
    }

    njs_console_time_end(console, &name, ns);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_set_timer(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv, int immediate)
{
    int            n;
    int64_t        delay;
    njs_ev_t       *ev;
    njs_uint_t     i;
    njs_console_t  *console;

    if (njs_slow_path(argc < 1)) {
        JS_ThrowTypeError(ctx, "too few arguments");
        return JS_EXCEPTION;
    }

    if (njs_slow_path(!JS_IsFunction(ctx, argv[0]))) {
        JS_ThrowTypeError(ctx, "first arg must be a function");
        return JS_EXCEPTION;
    }

    delay = 0;

    if (!immediate && argc >= 2 && JS_IsNumber(argv[1])) {
        JS_ToInt64(ctx, &delay, argv[1]);
    }

    if (delay != 0) {
        JS_ThrowInternalError(ctx, "njs_set_timer(): async timers unsupported");
        return JS_EXCEPTION;
    }

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    n = immediate ? 1 : 2;
    argc = (argc >= n) ? argc - n : 0;

    ev = njs_mp_alloc(console->engine->pool,
                      sizeof(njs_ev_t) + sizeof(njs_opaque_value_t) * argc);
    if (njs_slow_path(ev == NULL)) {
        JS_ThrowOutOfMemory(ctx);
        return JS_EXCEPTION;
    }

    ev->u.qjs.function = JS_DupValue(ctx, argv[0]);
    ev->u.qjs.args = (JSValue *) &ev[1];
    ev->nargs = (njs_uint_t) argc;
    ev->id = console->event_id++;

    if (ev->nargs != 0) {
        for (i = 0; i < ev->nargs; i++) {
            ev->u.qjs.args[i] = JS_DupValue(ctx, argv[i + n]);
        }
    }

    njs_rbtree_insert(&console->events, &ev->node);

    njs_queue_insert_tail(&console->posted_events, &ev->link);

    return JS_NewUint32(ctx, ev->id);
}


static void
njs_qjs_destroy_event(JSContext *ctx, njs_console_t *console, njs_ev_t *ev)
{
    njs_uint_t  i;

    JS_FreeValue(ctx, ev->u.qjs.function);

    if (ev->nargs != 0) {
        for (i = 0; i < ev->nargs; i++) {
            JS_FreeValue(ctx, ev->u.qjs.args[i]);
        }
    }

    njs_mp_free(console->engine->pool, ev);
}


static JSValue
njs_qjs_clear_timeout(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    njs_ev_t           ev_lookup, *ev;
    njs_console_t      *console;
    njs_rbtree_node_t  *rb;

    if (argc < 1 || !JS_IsNumber(argv[0])) {
        return JS_UNDEFINED;
    }

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    if (JS_ToUint32(ctx, &ev_lookup.id, argv[0])) {
        return JS_EXCEPTION;
    }

    rb = njs_rbtree_find(&console->events, &ev_lookup.node);
    if (njs_slow_path(rb == NULL)) {
        JS_ThrowTypeError(ctx, "failed to find timer");
        return JS_EXCEPTION;
    }

    ev = (njs_ev_t *) rb;
    njs_queue_remove(&ev->link);
    njs_rbtree_delete(&console->events, (njs_rbtree_part_t *) rb);

    njs_qjs_destroy_event(ctx, console, ev);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_process_getter(JSContext *ctx, JSValueConst this_val)
{
    JSValue         obj;
    njs_console_t  *console;

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    if (!JS_IsUndefined(console->process)) {
        return JS_DupValue(ctx, console->process);
    }

    obj = qjs_process_object(ctx, console->argc, (const char **) console->argv);
    if (JS_IsException(obj)) {
        return JS_EXCEPTION;
    }

    console->process = JS_DupValue(ctx, obj);

    return obj;
}


static njs_int_t njs_qjs_global_init(JSContext *ctx, JSValue global_obj);
static void njs_qjs_dump_error(JSContext *ctx);


static void
njs_qjs_dump_obj(JSContext *ctx, FILE *f, JSValueConst val, const char *prefix,
    const char *quote)
{
    njs_bool_t  is_str;
    const char  *str;

    is_str = JS_IsString(val);

    str = JS_ToCString(ctx, val);
    if (str) {
        fprintf(f, "%s%s%s%s\n", prefix, is_str ? quote : "",
                str, is_str ? quote : "");
        JS_FreeCString(ctx, str);

    } else {
        njs_qjs_dump_error(ctx);
    }
}


static void
njs_qjs_dump_error2(JSContext *ctx, JSValueConst exception)
{
    _Bool    is_error;
    JSValue  val;

    is_error = qjs_is_error(ctx, exception);

    njs_qjs_dump_obj(ctx, stderr, exception, "Thrown:\n", "");

    if (is_error) {
        val = JS_GetPropertyStr(ctx, exception, "stack");
        if (!JS_IsUndefined(val)) {
            njs_qjs_dump_obj(ctx, stderr, val, "", "");
        }

        JS_FreeValue(ctx, val);
    }
}


static void
njs_qjs_dump_error(JSContext *ctx)
{
    JSValue  exception;

    exception = JS_GetException(ctx);
    njs_qjs_dump_error2(ctx, exception);
    JS_FreeValue(ctx, exception);
}


static void *
njs_qjs_agent(void *arg)
{
    int            ret;
    JSValue        ret_val, global_obj;
    JSRuntime      *rt;
    JSContext      *ctx, *ctx1;
    njs_console_t  *console;
    JSValue        args[2];

    njs_262agent_t *agent = arg;
    console = agent->console;

    rt = JS_NewRuntime();
    if (rt == NULL) {
        njs_stderror("JS_NewRuntime failure\n");
        exit(1);
    }

    ctx = JS_NewContext(rt);
    if (ctx == NULL) {
        JS_FreeRuntime(rt);
        njs_stderror("JS_NewContext failure\n");
        exit(1);
    }

    JS_SetContextOpaque(ctx, agent);
    JS_SetRuntimeInfo(rt, "agent");
    JS_SetCanBlock(rt, 1);

    global_obj = JS_GetGlobalObject(ctx);

    ret = njs_qjs_global_init(ctx, global_obj);
    if (ret == -1) {
        JS_FreeContext(ctx);
        JS_FreeRuntime(rt);
        njs_stderror("njs_qjs_global_init failure\n");
        exit(1);
    }

    JS_FreeValue(ctx, global_obj);

    ret_val = JS_Eval(ctx, agent->script, strlen(agent->script),
                      "<evalScript>", JS_EVAL_TYPE_GLOBAL);

    free(agent->script);
    agent->script = NULL;

    if (JS_IsException(ret_val)) {
        njs_qjs_dump_error(ctx);
    }

    JS_FreeValue(ctx, ret_val);

    for (;;) {
        ret = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1);
        if (ret < 0) {
            njs_qjs_dump_error(ctx);
            break;

        } else if (ret == 0) {
            if (JS_IsUndefined(agent->broadcast_func)) {
                break;

            } else {
                pthread_mutex_lock(&console->agent_mutex);

                while (!agent->broadcast_pending) {
                    pthread_cond_wait(&console->agent_cond,
                                      &console->agent_mutex);
                }

                agent->broadcast_pending = 0;
                pthread_cond_signal(&console->agent_cond);
                pthread_mutex_unlock(&console->agent_mutex);

                args[0] = JS_NewArrayBuffer(ctx, agent->broadcast_sab_buf,
                                            agent->broadcast_sab_size,
                                            NULL, NULL, 1);
                args[1] = JS_NewInt32(ctx, agent->broadcast_val);

                ret_val = JS_Call(ctx, agent->broadcast_func, JS_UNDEFINED,
                                  2, (JSValueConst *)args);

                JS_FreeValue(ctx, args[0]);
                JS_FreeValue(ctx, args[1]);

                if (JS_IsException(ret_val)) {
                    njs_qjs_dump_error(ctx);
                }

                JS_FreeValue(ctx, ret_val);
                JS_FreeValue(ctx, agent->broadcast_func);
                agent->broadcast_func = JS_UNDEFINED;
            }
        }
    }

    JS_FreeValue(ctx, agent->broadcast_func);

    JS_FreeContext(ctx);
    JS_FreeRuntime(rt);

    return NULL;
}


static JSValue
njs_qjs_agent_start(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    const char      *script;
    njs_console_t   *console;
    njs_262agent_t  *agent;

    if (JS_GetContextOpaque(ctx) != NULL) {
        return JS_ThrowTypeError(ctx, "cannot be called inside an agent");
    }

    script = JS_ToCString(ctx, argv[0]);
    if (script == NULL) {
        return JS_EXCEPTION;
    }

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    agent = malloc(sizeof(*agent));
    if (agent == NULL) {
        return JS_ThrowOutOfMemory(ctx);
    }

    njs_memzero(agent, sizeof(*agent));

    agent->broadcast_func = JS_UNDEFINED;
    agent->broadcast_sab = JS_UNDEFINED;
    agent->script = strdup(script);
    if (agent->script == NULL) {
        return JS_ThrowOutOfMemory(ctx);
    }

    JS_FreeCString(ctx, script);

    agent->console = console;
    njs_queue_insert_tail(&console->agents, &agent->link);

    pthread_create(&agent->tid, NULL, njs_qjs_agent, agent);

    return JS_UNDEFINED;
}


static JSValue
njs_qjsr_agent_get_report(JSContext *ctx, JSValue this_val, int argc,
    JSValue *argv)
{
    JSValue             ret;
    njs_console_t       *console;
    njs_queue_link_t    *link;
    njs_agent_report_t  *rep;

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    pthread_mutex_lock(&console->report_mutex);

    rep = NULL;

    for ( ;; ) {
        link = njs_queue_first(&console->reports);

        if (link == njs_queue_tail(&console->reports)) {
            break;
        }

        rep = njs_queue_link_data(link, njs_agent_report_t, link);

        njs_queue_remove(&rep->link);
        break;
    }

    pthread_mutex_unlock(&console->report_mutex);

    if (rep != NULL) {
        ret = JS_NewString(ctx, rep->str);
        free(rep->str);
        free(rep);

    } else {
        ret = JS_NULL;
    }

    return ret;
}


static njs_bool_t
njs_qjs_broadcast_pending(njs_console_t *console)
{
    njs_262agent_t    *agent;
    njs_queue_link_t  *link;

    link = njs_queue_first(&console->agents);

    for ( ;; ) {
        if (link == njs_queue_tail(&console->agents)) {
            break;
        }

        agent = njs_queue_link_data(link, njs_262agent_t, link);

        if (agent->broadcast_pending) {
            return 1;
        }

        link = njs_queue_next(link);
    }

    return 0;
}

static JSValue
njs_qjs_agent_broadcast(JSContext *ctx, JSValue this_val, int argc,
    JSValue *argv)
{
    uint8_t           *buf;
    size_t            buf_size;
    int32_t           val;
    njs_console_t     *console;
    njs_262agent_t    *agent;
    njs_queue_link_t  *link;

    JSValueConst sab = argv[0];

    if (JS_GetContextOpaque(ctx) != NULL) {
        return JS_ThrowTypeError(ctx, "cannot be called inside an agent");
    }

    buf = JS_GetArrayBuffer(ctx, &buf_size, sab);
    if (buf == NULL) {
        return JS_EXCEPTION;
    }

    if (JS_ToInt32(ctx, &val, argv[1])) {
        return JS_EXCEPTION;
    }

    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    pthread_mutex_lock(&console->agent_mutex);

    link = njs_queue_first(&console->agents);

    for ( ;; ) {
        if (link == njs_queue_tail(&console->agents)) {
            break;
        }

        agent = njs_queue_link_data(link, njs_262agent_t, link);

        agent->broadcast_pending = 1;
        agent->broadcast_sab = JS_DupValue(ctx, sab);
        agent->broadcast_sab_buf = buf;
        agent->broadcast_sab_size = buf_size;
        agent->broadcast_val = val;

        link = njs_queue_next(link);
    }

    pthread_cond_broadcast(&console->agent_cond);

    while (njs_qjs_broadcast_pending(console)) {
        pthread_cond_wait(&console->agent_cond, &console->agent_mutex);
    }

    pthread_mutex_unlock(&console->agent_mutex);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_agent_receive_broadcast(JSContext *ctx, JSValue this_val, int argc,
    JSValue *argv)
{
    njs_262agent_t *agent = JS_GetContextOpaque(ctx);
    if (agent == NULL) {
        return JS_ThrowTypeError(ctx, "must be called inside an agent");
    }

    if (!JS_IsFunction(ctx, argv[0])) {
        return JS_ThrowTypeError(ctx, "expecting function");
    }

    JS_FreeValue(ctx, agent->broadcast_func);
    agent->broadcast_func = JS_DupValue(ctx, argv[0]);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_agent_report(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    const char          *str;
    njs_console_t       *console;
    njs_262agent_t      *agent;
    njs_agent_report_t  *rep;

    str = JS_ToCString(ctx, argv[0]);
    if (str == NULL) {
        return JS_EXCEPTION;
    }

    rep = malloc(sizeof(*rep));
    rep->str = strdup(str);
    JS_FreeCString(ctx, str);

    agent = JS_GetContextOpaque(ctx);
    console = agent->console;

    pthread_mutex_lock(&console->report_mutex);
    njs_queue_insert_tail(&console->reports, &rep->link);
    pthread_mutex_unlock(&console->report_mutex);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_agent_leaving(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    njs_262agent_t *agent = JS_GetContextOpaque(ctx);
    if (agent == NULL) {
        return JS_ThrowTypeError(ctx, "must be called inside an agent");
    }

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_agent_sleep(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    uint32_t duration;

    if (JS_ToUint32(ctx, &duration, argv[0])) {
        return JS_EXCEPTION;
    }

    usleep(duration * 1000);

    return JS_UNDEFINED;
}


static JSValue
njs_qjs_agent_monotonic_now(JSContext *ctx, JSValue this_val, int argc,
    JSValue *argv)
{
    return JS_NewInt64(ctx, njs_time() / 1000000);
}


static const JSCFunctionListEntry njs_qjs_agent_proto[] = {
    JS_CFUNC_DEF("start", 1, njs_qjs_agent_start),
    JS_CFUNC_DEF("getReport", 0, njs_qjsr_agent_get_report),
    JS_CFUNC_DEF("broadcast", 2, njs_qjs_agent_broadcast),
    JS_CFUNC_DEF("report", 1, njs_qjs_agent_report),
    JS_CFUNC_DEF("leaving", 0, njs_qjs_agent_leaving),
    JS_CFUNC_DEF("receiveBroadcast", 1, njs_qjs_agent_receive_broadcast),
    JS_CFUNC_DEF("sleep", 1, njs_qjs_agent_sleep),
    JS_CFUNC_DEF("monotonicNow", 0, njs_qjs_agent_monotonic_now),
};


static JSValue
njs_qjs_new_agent(JSContext *ctx)
{
    JSValue  agent;

    agent = JS_NewObject(ctx);
    if (JS_IsException(agent)) {
        return JS_EXCEPTION;
    }

    JS_SetPropertyFunctionList(ctx, agent, njs_qjs_agent_proto,
                               njs_nitems(njs_qjs_agent_proto));
    return agent;
}


static JSValue
njs_qjs_detach_array_buffer(JSContext *ctx, JSValueConst this_val, int argc,
    JSValueConst *argv)
{
    JS_DetachArrayBuffer(ctx, argv[0]);

    return JS_NULL;
}

static JSValue
njs_qjs_eval_script(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    size_t     len;
    JSValue    ret;
    const char *str;

    str = JS_ToCStringLen(ctx, &len, argv[0]);
    if (str == NULL) {
        return JS_EXCEPTION;
    }

    ret = JS_Eval(ctx, str, len, "<evalScript>", JS_EVAL_TYPE_GLOBAL);

    JS_FreeCString(ctx, str);

    return ret;
}


static JSValue
njs_qjs_create_realm(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    JSValue    ret_val, global_obj;
    njs_int_t  ret;
    JSContext  *ctx1;

    ctx1 = JS_NewContext(JS_GetRuntime(ctx));
    if (ctx1 == NULL) {
        return JS_ThrowOutOfMemory(ctx);
    }

    global_obj = JS_GetGlobalObject(ctx1);

    ret = njs_qjs_global_init(ctx1, global_obj);
    if (ret == -1) {
        JS_FreeContext(ctx1);
        return JS_EXCEPTION;
    }

    ret_val = JS_GetPropertyStr(ctx1, global_obj, "$262");

    JS_FreeValue(ctx1, global_obj);
    JS_FreeContext(ctx1);

    return ret_val;
}


static JSValue
njs_qjs_is_HTMLDDA(JSContext *ctx, JSValue this_val, int argc, JSValue *argv)
{
    return JS_NULL;
}


static const JSCFunctionListEntry njs_qjs_262_proto[] = {
    JS_CFUNC_DEF("detachArrayBuffer", 1, njs_qjs_detach_array_buffer),
    JS_CFUNC_DEF("evalScript", 1, njs_qjs_eval_script),
    JS_CFUNC_DEF("codePointRange", 2, js_string_codePointRange),
    JS_CFUNC_DEF("createRealm", 0, njs_qjs_create_realm),
};


static JSValue
njs_qjs_new_262(JSContext *ctx, JSValueConst this_val)
{
    JSValue  obj, obj262, global_obj;

    obj262 = JS_NewObject(ctx);
    if (JS_IsException(obj262)) {
        return JS_EXCEPTION;
    }

    JS_SetPropertyFunctionList(ctx, obj262, njs_qjs_262_proto,
                               njs_nitems(njs_qjs_262_proto));

    global_obj = JS_GetGlobalObject(ctx);
    JS_SetPropertyStr(ctx, obj262, "global", JS_DupValue(ctx, global_obj));
    JS_FreeValue(ctx, global_obj);

    obj = JS_NewCFunction(ctx, njs_qjs_is_HTMLDDA, "IsHTMLDDA", 0);
    JS_SetIsHTMLDDA(ctx, obj);
    JS_SetPropertyStr(ctx, obj262, "IsHTMLDDA", obj);

    JS_SetPropertyStr(ctx, obj262, "agent", njs_qjs_new_agent(ctx));

    return obj262;
}


static const JSCFunctionListEntry njs_qjs_global_proto[] = {
    JS_CFUNC_DEF("clearTimeout", 1, njs_qjs_clear_timeout),
    JS_CFUNC_MAGIC_DEF("print", 0, njs_qjs_console_log, NJS_LOG_INFO),
    JS_CGETSET_DEF("process", njs_qjs_process_getter, NULL),
    JS_CFUNC_MAGIC_DEF("setImmediate", 0, njs_qjs_set_timer, 1),
    JS_CFUNC_MAGIC_DEF("setTimeout", 0, njs_qjs_set_timer, 0),
};


static const JSCFunctionListEntry njs_qjs_console_proto[] = {
    JS_PROP_STRING_DEF("[Symbol.toStringTag]", "Console",
                       JS_PROP_CONFIGURABLE),
    JS_CFUNC_MAGIC_DEF("error", 0, njs_qjs_console_log, NJS_LOG_ERROR),
    JS_CFUNC_MAGIC_DEF("info", 0, njs_qjs_console_log, NJS_LOG_INFO),
    JS_CFUNC_MAGIC_DEF("log", 0, njs_qjs_console_log, NJS_LOG_INFO),
    JS_CFUNC_DEF("time", 0, njs_qjs_console_time),
    JS_CFUNC_DEF("timeEnd", 0, njs_qjs_console_time_end),
    JS_CFUNC_MAGIC_DEF("warn", 0, njs_qjs_console_log, NJS_LOG_WARN),
};


static njs_int_t
njs_qjs_global_init(JSContext *ctx, JSValue global_obj)
{
    JS_SetPropertyFunctionList(ctx, global_obj, njs_qjs_global_proto,
                               njs_nitems(njs_qjs_global_proto));

    return JS_SetPropertyStr(ctx, global_obj, "$262",
                             njs_qjs_new_262(ctx, global_obj));
}


static void
njs_qjs_rejection_tracker(JSContext *ctx, JSValueConst promise,
    JSValueConst reason, JS_BOOL is_handled, void *opaque)
{
    void                    *promise_obj;
    uint32_t                i, length;
    njs_console_t           *console;
    njs_rejected_promise_t  *rejected_promise;

    console = opaque;

    if (is_handled && console->rejected_promises != NULL) {
        rejected_promise = console->rejected_promises->start;
        length = console->rejected_promises->items;

        promise_obj = JS_VALUE_GET_PTR(promise);

        for (i = 0; i < length; i++) {
            if (JS_VALUE_GET_PTR(rejected_promise[i].u.qjs.promise)
                == promise_obj)
            {
                JS_FreeValue(ctx, rejected_promise[i].u.qjs.promise);
                JS_FreeValue(ctx, rejected_promise[i].u.qjs.message);
                njs_arr_remove(console->rejected_promises,
                               &rejected_promise[i]);

                break;
            }
        }

        return;
    }

    if (console->rejected_promises == NULL) {
        console->rejected_promises = njs_arr_create(console->engine->pool, 4,
                                                sizeof(njs_rejected_promise_t));
        if (njs_slow_path(console->rejected_promises == NULL)) {
            return;
        }
    }

    rejected_promise = njs_arr_add(console->rejected_promises);
    if (njs_slow_path(rejected_promise == NULL)) {
        return;
    }

    rejected_promise->u.qjs.promise = JS_DupValue(ctx, promise);
    rejected_promise->u.qjs.message = JS_DupValue(ctx, reason);
}


static njs_int_t
njs_has_suffix(const char *str, const char *suffix)
{
    size_t  len, slen;

    len = njs_strlen(str);
    slen = njs_strlen(suffix);

    return (len >= slen && memcmp(str + len - slen, suffix, slen) == 0);
}


static JSModuleDef *
njs_qjs_module_loader_so(JSContext *ctx, const char *module_name)
{
    void                *handle;
    char                *filename;
    JSModuleDef         *m;
    qjs_addon_init_pt   init;

    if (!strchr(module_name, '/')) {
        filename = js_malloc(ctx, njs_strlen(module_name) + 3);
        if (filename == NULL) {
            JS_ThrowOutOfMemory(ctx);
            return NULL;
        }

        strcpy(filename, "./");
        strcpy(filename + 2, module_name);

    } else {
        filename = (char *) module_name;
    }

    handle = dlopen(filename, RTLD_NOW | RTLD_LOCAL);
    if (filename != module_name) {
        js_free(ctx, filename);
    }

    if (handle == NULL) {
        JS_ThrowReferenceError(ctx, "could not load module filename '%s' as "
                               "shared library: %s", module_name, dlerror());
        return NULL;
    }

    init = dlsym(handle, "js_init_module");
    if (init == NULL) {
        JS_ThrowReferenceError(ctx, "could not load module filename '%s': "
                               "js_init_module not found", module_name);
        dlclose(handle);
        return NULL;
    }

    m = init(ctx, module_name);
    if (m == NULL) {
        JS_ThrowReferenceError(ctx, "could not load module filename '%s': "
                               "initialization error", module_name);
        dlclose(handle);
        return NULL;
    }

    return m;
}


static JSModuleDef *
njs_qjs_module_loader(JSContext *ctx, const char *module_name, void *opaque)
{
    JSValue            func_val;
    njs_int_t          ret;
    njs_str_t          text, prev_cwd;
    njs_opts_t         *opts;
    njs_console_t      *console;
    JSModuleDef        *m;
    njs_module_info_t  info;

    opts = opaque;
    console = JS_GetRuntimeOpaque(JS_GetRuntime(ctx));

    if (njs_has_suffix(module_name, ".so")) {
        return njs_qjs_module_loader_so(ctx, module_name);
    }

    njs_memzero(&info, sizeof(njs_module_info_t));

    info.name.start = (u_char *) module_name;
    info.name.length = njs_strlen(module_name);

    ret = njs_module_lookup(opts, &console->cwd, &info);
    if (njs_slow_path(ret != NJS_OK)) {
        JS_ThrowReferenceError(ctx, "could not load module filename '%s'",
                               module_name);
        return NULL;
    }

    ret = njs_module_read(console->engine->pool, info.fd, &text);

    (void) close(info.fd);

    if (njs_slow_path(ret != NJS_OK)) {
        JS_ThrowInternalError(ctx, "while reading \"%.*s\" module",
                              (int) info.file.length, info.file.start);
        return NULL;
    }

    prev_cwd = console->cwd;

    ret = njs_console_set_cwd(console, &info.file);
    if (njs_slow_path(ret != NJS_OK)) {
        JS_ThrowInternalError(ctx, "while setting cwd for \"%.*s\" module",
                              (int) info.file.length, info.file.start);
        return NULL;
    }

    func_val = JS_Eval(ctx, (char *) text.start, text.length, module_name,
                       JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY);

    njs_mp_free(console->engine->pool, console->cwd.start);
    console->cwd = prev_cwd;

    njs_mp_free(console->engine->pool, text.start);

    if (JS_IsException(func_val)) {
        return NULL;
    }

    m = JS_VALUE_GET_PTR(func_val);
    JS_FreeValue(ctx, func_val);

    return m;
}


static njs_int_t
njs_engine_qjs_init(njs_engine_t *engine, njs_opts_t *opts)
{
    JSValue    global_obj, obj;
    njs_int_t  ret;
    JSContext  *ctx;

    engine->u.qjs.rt = JS_NewRuntime();
    if (engine->u.qjs.rt == NULL) {
        njs_stderror("JS_NewRuntime() failed\n");
        return NJS_ERROR;
    }

    engine->u.qjs.ctx = qjs_new_context(engine->u.qjs.rt, NULL);
    if (engine->u.qjs.ctx == NULL) {
        njs_stderror("JS_NewContext() failed\n");
        return NJS_ERROR;
    }

    JS_SetRuntimeOpaque(engine->u.qjs.rt, &njs_console);

    engine->u.qjs.value = JS_UNDEFINED;

    ctx = engine->u.qjs.ctx;

    global_obj = JS_GetGlobalObject(ctx);

    ret = njs_qjs_global_init(ctx, global_obj);
    if (ret == -1) {
        njs_stderror("njs_qjs_global_init() failed\n");
        ret = NJS_ERROR;
        goto done;
    }

    obj = JS_NewObject(ctx);
    if (JS_IsException(obj)) {
        njs_stderror("JS_NewObject() failed\n");
        ret = NJS_ERROR;
        goto done;
    }

    JS_SetOpaque(obj, &njs_console);

    JS_SetPropertyFunctionList(ctx, obj, njs_qjs_console_proto,
                               njs_nitems(njs_qjs_console_proto));

    ret = JS_SetPropertyStr(ctx, global_obj, "console", obj);
    if (ret == -1) {
        njs_stderror("JS_SetPropertyStr() failed\n");
        ret = NJS_ERROR;
        goto done;
    }

    if (opts->unhandled_rejection) {
        JS_SetHostPromiseRejectionTracker(engine->u.qjs.rt,
                                         njs_qjs_rejection_tracker,
                                         JS_GetRuntimeOpaque(engine->u.qjs.rt));
    }

    JS_SetModuleLoaderFunc(engine->u.qjs.rt, NULL, njs_qjs_module_loader, opts);

    JS_SetCanBlock(engine->u.qjs.rt, opts->can_block);

    ret = NJS_OK;

done:

    JS_FreeValue(ctx, global_obj);

    return ret;
}


static njs_int_t
njs_engine_qjs_destroy(njs_engine_t *engine)
{
    uint32_t                i;
    njs_ev_t                *ev;
    njs_int_t               ret;
    JSContext               *cx;
    njs_queue_t             *events;
    njs_console_t           *console;
    njs_262agent_t          *agent;
    njs_queue_link_t        *link;
    njs_rejected_promise_t  *rejected_promise;

    (void) qjs_call_exit_hook(engine->u.qjs.ctx);

    for ( ;; ) {
        ret = JS_ExecutePendingJob(JS_GetRuntime(engine->u.qjs.ctx), &cx);
        if (ret == 0) {
            break;
        }
    }

    console = JS_GetRuntimeOpaque(engine->u.qjs.rt);

    if (console->rejected_promises != NULL) {
        rejected_promise = console->rejected_promises->start;

        for (i = 0; i < console->rejected_promises->items; i++) {
            JS_FreeValue(engine->u.qjs.ctx, rejected_promise[i].u.qjs.promise);
            JS_FreeValue(engine->u.qjs.ctx, rejected_promise[i].u.qjs.message);
        }
    }

    events = &console->posted_events;

    for ( ;; ) {
        link = njs_queue_first(events);

        if (link == njs_queue_tail(events)) {
            break;
        }

        ev = njs_queue_link_data(link, njs_ev_t, link);

        njs_queue_remove(&ev->link);
        njs_rbtree_delete(&console->events, &ev->node);

        njs_qjs_destroy_event(engine->u.qjs.ctx, console, ev);
    }

    for ( ;; ) {
        link = njs_queue_first(&console->agents);

        if (link == njs_queue_tail(&console->agents)) {
            break;
        }

        agent = njs_queue_link_data(link, njs_262agent_t, link);

        njs_queue_remove(&agent->link);

        pthread_join(agent->tid, NULL);
        JS_FreeValue(engine->u.qjs.ctx, agent->broadcast_sab);
        free(agent->script);
        free(agent);
    }

    JS_FreeValue(engine->u.qjs.ctx, console->process);
    JS_FreeValue(engine->u.qjs.ctx, engine->u.qjs.value);
    JS_FreeContext(engine->u.qjs.ctx);
    JS_FreeRuntime(engine->u.qjs.rt);

    return NJS_OK;
}


static njs_int_t
njs_engine_qjs_eval(njs_engine_t *engine, njs_str_t *script)
{
    int            flags;
    JSValue        code;
    njs_console_t  *console;

    flags = JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_STRICT
            | JS_EVAL_FLAG_COMPILE_ONLY;

    console = JS_GetRuntimeOpaque(engine->u.qjs.rt);

    if (console->module) {
        flags |= JS_EVAL_TYPE_MODULE;
    }

    code = JS_Eval(engine->u.qjs.ctx, (char *) script->start,
                   script->length, "<input>", flags);

    if (JS_IsException(code)) {
        return NJS_ERROR;
    }

    JS_FreeValue(engine->u.qjs.ctx, engine->u.qjs.value);

    engine->u.qjs.value = JS_EvalFunction(engine->u.qjs.ctx, code);

    return (JS_IsException(engine->u.qjs.value)) ? NJS_ERROR : NJS_OK;
}


static njs_int_t
njs_engine_qjs_execute_pending_job(njs_engine_t *engine)
{
    JSContext  *ctx1;

    return JS_ExecutePendingJob(engine->u.qjs.rt, &ctx1);
}


static njs_int_t
njs_engine_qjs_unhandled_rejection(njs_engine_t *engine)
{
    size_t                  len;
    uint32_t                i;
    JSContext               *ctx;
    const char              *str;
    njs_console_t           *console;
    njs_rejected_promise_t  *rejected_promise;

    ctx = engine->u.qjs.ctx;
    console = JS_GetRuntimeOpaque(engine->u.qjs.rt);

    if (console->rejected_promises == NULL
        || console->rejected_promises->items == 0)
    {
        return 0;
    }

    rejected_promise = console->rejected_promises->start;

    str = JS_ToCStringLen(ctx, &len, rejected_promise->u.qjs.message);
    if (njs_slow_path(str == NULL)) {
        return -1;
    }

    JS_ThrowTypeError(ctx, "unhandled promise rejection: %.*s", (int) len, str);
    JS_FreeCString(ctx, str);

    for (i = 0; i < console->rejected_promises->items; i++) {
        JS_FreeValue(ctx, rejected_promise[i].u.qjs.promise);
        JS_FreeValue(ctx, rejected_promise[i].u.qjs.message);
    }

    njs_arr_destroy(console->rejected_promises);
    console->rejected_promises = NULL;

    return 1;
}


static njs_int_t
njs_engine_qjs_process_events(njs_engine_t *engine)
{
    JSValue           ret;
    njs_ev_t          *ev;
    JSContext         *ctx;
    njs_queue_t       *events;
    njs_console_t     *console;
    njs_queue_link_t  *link;

    ctx = engine->u.qjs.ctx;
    console = JS_GetRuntimeOpaque(engine->u.qjs.rt);
    events = &console->posted_events;

    for ( ;; ) {
        link = njs_queue_first(events);

        if (link == njs_queue_tail(events)) {
            break;
        }

        ev = njs_queue_link_data(link, njs_ev_t, link);

        njs_queue_remove(&ev->link);
        njs_rbtree_delete(&console->events, &ev->node);

        ret = JS_Call(ctx, ev->u.qjs.function, JS_UNDEFINED, ev->nargs,
                      ev->u.qjs.args);

        njs_qjs_destroy_event(ctx, console, ev);

        if (JS_IsException(ret)) {
            engine->output(engine, NJS_ERROR);

            if (!console->interactive) {
                return NJS_ERROR;
            }
        }

        JS_FreeValue(ctx, ret);
    }

    if (!njs_rbtree_is_empty(&console->events)) {
        return NJS_AGAIN;
    }

    return JS_IsJobPending(engine->u.qjs.rt) ? NJS_AGAIN: NJS_OK;
}


static njs_int_t
njs_engine_qjs_output(njs_engine_t *engine, njs_int_t ret)
{
    JSContext      *ctx;
    njs_console_t  *console;

    ctx = engine->u.qjs.ctx;
    console = JS_GetRuntimeOpaque(engine->u.qjs.rt);

    if (ret == NJS_OK) {
        if (console->interactive) {
            njs_qjs_dump_obj(ctx, stdout, engine->u.qjs.value, "", "\'");
        }

    } else {
        njs_qjs_dump_error(ctx);
    }

    return NJS_OK;
}


static njs_arr_t *
njs_qjs_object_completions(njs_engine_t *engine, JSContext *ctx,
    JSValueConst object, njs_str_t *expression)
{
    u_char          *prefix;
    size_t          len, prefix_len;
    JSValue         prototype;
    uint32_t        k, n, length;
    njs_int_t       ret;
    njs_arr_t       *array;
    njs_str_t       *completion, key;
    JSPropertyEnum  *ptab;

    prefix = expression->start + expression->length;

    while (prefix > expression->start && *prefix != '.') {
        prefix--;
    }

    if (prefix != expression->start) {
        prefix++;
    }

    ptab = NULL;
    key.start = NULL;
    prefix_len = prefix - expression->start;
    len = expression->length - prefix_len;

    array = njs_arr_create(engine->pool, 8, sizeof(njs_str_t));
    if (njs_slow_path(array == NULL)) {
        goto fail;
    }

    while (!JS_IsNull(object)) {
        ret = JS_GetOwnPropertyNames(ctx, &ptab, &length, object,
                                     JS_GPN_STRING_MASK);
        if (ret < 0) {
            goto fail;
        }

        for (n = 0; n < length; n++) {
            key.start = (u_char *) JS_AtomToCString(ctx, ptab[n].atom);
            if (njs_slow_path(key.start == NULL)) {
                goto fail;
            }

            key.length = njs_strlen(key.start);

            if (len > key.length || njs_strncmp(key.start, prefix, len) != 0) {
                goto next;
            }

            for (k = 0; k < array->items; k++) {
                completion = njs_arr_item(array, k);

                if ((completion->length - prefix_len - 1) == key.length
                    && njs_strncmp(&completion->start[prefix_len],
                                   key.start, key.length)
                       == 0)
                {
                    goto next;
                }
            }

            completion = njs_arr_add(array);
            if (njs_slow_path(completion == NULL)) {
                goto fail;
            }

            completion->length = prefix_len + key.length + 1;
            completion->start = njs_mp_alloc(engine->pool, completion->length);
            if (njs_slow_path(completion->start == NULL)) {
                goto fail;
            }

            njs_sprintf(completion->start,
                        completion->start + completion->length,
                        "%*s%V%Z", prefix_len, expression->start, &key);

next:

            JS_FreeCString(ctx, (const char *) key.start);
        }

        qjs_free_prop_enum(ctx, ptab, length);

        prototype = JS_GetPrototype(ctx, object);
        if (JS_IsException(prototype)) {
            goto fail;
        }

        JS_FreeValue(ctx, object);
        object = prototype;
    }

    return array;

fail:

    if (array != NULL) {
        njs_arr_destroy(array);
    }

    if (key.start != NULL) {
        JS_FreeCString(ctx, (const char *) key.start);
    }

    if (ptab != NULL) {
        qjs_free_prop_enum(ctx, ptab, length);
    }

    JS_FreeValue(ctx, object);

    return NULL;
}


static njs_arr_t *
njs_engine_qjs_complete(njs_engine_t *engine, njs_str_t *expression)
{
    u_char      *p, *start, *end;
    JSAtom      key;
    JSValue     value, retval;
    njs_arr_t   *arr;
    JSContext   *ctx;
    njs_bool_t  global;

    ctx = engine->u.qjs.ctx;

    p = expression->start;
    end = p + expression->length;

    global = 1;
    value = JS_GetGlobalObject(ctx);

    while (p < end && *p != '.') { p++; }

    if (p == end) {
        goto done;
    }

    p = expression->start;

    for ( ;; ) {

        start = (*p == '.' && p < end) ? ++p: p;

        if (p == end) {
            break;
        }

        while (p < end && *p != '.') { p++; }

        key = JS_NewAtomLen(ctx, (char *) start, p - start);
        if (key == JS_ATOM_NULL) {
            goto fail;
        }

        retval = JS_GetProperty(ctx, value, key);

        JS_FreeAtom(ctx, key);

        if (JS_IsUndefined(retval)) {
            if (global) {
                goto fail;
            }

            goto done;
        }

        if (JS_IsException(retval)) {
            goto fail;
        }

        JS_FreeValue(ctx, value);
        value = retval;
        global = 0;
    }

done:

    arr = njs_qjs_object_completions(engine, ctx, JS_DupValue(ctx, value),
                                     expression);

    JS_FreeValue(ctx, value);

    return arr;

fail:

    JS_FreeValue(ctx, value);

    return NULL;
}

#endif


static njs_engine_t *
njs_create_engine(njs_opts_t *opts)
{
    njs_mp_t      *mp;
    njs_int_t     ret;
    njs_engine_t  *engine;

    mp = njs_mp_fast_create(2 * njs_pagesize(), 128, 512, 16);
    if (njs_slow_path(mp == NULL)) {
        return NULL;
    }

    engine = njs_mp_zalloc(mp, sizeof(njs_engine_t));
    if (njs_slow_path(engine == NULL)) {
        return NULL;
    }

    engine->pool = mp;

    njs_console.engine = engine;

    switch (opts->engine) {
    case NJS_ENGINE_NJS:
        ret = njs_engine_njs_init(engine, opts);
        if (njs_slow_path(ret != NJS_OK)) {
            njs_stderror("njs_engine_njs_init() failed\n");
            return NULL;
        }

        engine->type = NJS_ENGINE_NJS;
        engine->eval = njs_engine_njs_eval;
        engine->execute_pending_job = njs_engine_njs_execute_pending_job;
        engine->unhandled_rejection = njs_engine_njs_unhandled_rejection;
        engine->process_events = njs_engine_njs_process_events;
        engine->destroy = njs_engine_njs_destroy;
        engine->output = njs_engine_njs_output;
        engine->complete = njs_engine_njs_complete;
        break;

#ifdef NJS_HAVE_QUICKJS
    case NJS_ENGINE_QUICKJS:
        ret = njs_engine_qjs_init(engine, opts);
        if (njs_slow_path(ret != NJS_OK)) {
            njs_stderror("njs_engine_qjs_init() failed\n");
            return NULL;
        }

        engine->type = NJS_ENGINE_QUICKJS;
        engine->eval = njs_engine_qjs_eval;
        engine->execute_pending_job = njs_engine_qjs_execute_pending_job;
        engine->unhandled_rejection = njs_engine_qjs_unhandled_rejection;
        engine->process_events = njs_engine_qjs_process_events;
        engine->destroy = njs_engine_qjs_destroy;
        engine->output = njs_engine_qjs_output;
        engine->complete = njs_engine_qjs_complete;
        break;
#endif

    default:
        njs_stderror("unknown engine type\n");
        return NULL;
    }

    return engine;
}


static njs_int_t
njs_read_file(njs_opts_t *opts, njs_str_t *content)
{
    int          fd;
    char         *file;
    u_char       *p, *end, *start;
    size_t       size;
    ssize_t      n;
    njs_int_t    ret;
    struct stat  sb;

    file = opts->file;

    if (file[0] == '-' && file[1] == '\0') {
        fd = STDIN_FILENO;

    } else {
        fd = open(file, O_RDONLY);
        if (fd == -1) {
            njs_stderror("failed to open file: '%s' (%s)\n",
                         file, strerror(errno));
            return NJS_ERROR;
        }
    }

    if (fstat(fd, &sb) == -1) {
        njs_stderror("fstat(%d) failed while reading '%s' (%s)\n",
                     fd, file, strerror(errno));
        ret = NJS_ERROR;
        goto close_fd;
    }

    size = 4096;

    if (S_ISREG(sb.st_mode) && sb.st_size) {
        size = sb.st_size;
    }

    content->length = 0;
    content->start = realloc(NULL, size + 1);
    if (content->start == NULL) {
        njs_stderror("alloc failed while reading '%s'\n", file);
        ret = NJS_ERROR;
        goto close_fd;
    }

    p = content->start;
    end = p + size;

    for ( ;; ) {
        n = read(fd, p, end - p);

        if (n == 0) {
            break;
        }

        if (n < 0) {
            njs_stderror("failed to read file: '%s' (%s)\n",
                      file, strerror(errno));
            ret = NJS_ERROR;
            goto close_fd;
        }

        if (p + n == end) {
            size *= 2;

            start = realloc(content->start, size + 1);
            if (start == NULL) {
                njs_stderror("alloc failed while reading '%s'\n", file);
                ret = NJS_ERROR;
                goto close_fd;
            }

            content->start = start;

            p = content->start + content->length;
            end = content->start + size;
        }

        p += n;
        content->length += n;
    }

    content->start[content->length] = '\0';

    ret = NJS_OK;

close_fd:

    if (fd != STDIN_FILENO) {
        (void) close(fd);
    }

    return ret;
}


static njs_int_t
njs_process_file(njs_opts_t *opts)
{
    u_char        *p;
    njs_int_t     ret;
    njs_str_t     source, script;
    njs_engine_t  *engine;

    engine = NULL;
    source.start = NULL;

    ret = njs_read_file(opts, &source);
    if (ret != NJS_OK) {
        goto done;
    }

    script = source;

    /* shebang */

    if (script.length > 2 && memcmp(script.start, "#!", 2) == 0) {
        p = njs_strlchr(script.start, script.start + script.length, '\n');

        if (p != NULL) {
            script.length -= (p + 1 - script.start);
            script.start = p + 1;

        } else {
            script.length = 0;
        }
    }

    engine = njs_create_engine(opts);
    if (engine == NULL) {
        ret = NJS_ERROR;
        goto done;
    }

    ret = njs_process_script(engine, &njs_console, &script);
    if (ret != NJS_OK) {
        ret = NJS_ERROR;
        goto done;
    }

    ret = NJS_OK;

done:

    if (engine != NULL) {
        engine->destroy(engine);
    }

    if (source.start != NULL) {
        free(source.start);
    }

    return ret;
}


static njs_int_t
njs_process_script(njs_engine_t *engine, njs_console_t *console,
    njs_str_t *script)
{
    njs_int_t   ret;

    ret = engine->eval(engine, script);

    if (!console->suppress_stdout) {
        engine->output(engine, ret);
    }

    if (!console->interactive && ret == NJS_ERROR) {
        return NJS_ERROR;
    }

    for ( ;; ) {
        for ( ;; ) {
            ret = engine->execute_pending_job(engine);
            if (ret <= NJS_OK) {
                if (ret == NJS_ERROR) {
                    if (!console->suppress_stdout) {
                        engine->output(engine, ret);
                    }

                    if (!console->interactive) {
                         return NJS_ERROR;
                    }
                }

                break;
            }
        }

        ret = engine->process_events(engine);
        if (njs_slow_path(ret == NJS_ERROR)) {
            break;
        }

        if (engine->unhandled_rejection(engine)) {
            if (!console->suppress_stdout) {
                engine->output(engine, NJS_ERROR);
            }

            if (!console->interactive) {
                return NJS_ERROR;
            }
        }

        if (ret == NJS_OK) {
            break;
        }
    }

    return ret;
}


#if (!defined NJS_FUZZER_TARGET && defined NJS_HAVE_READLINE)


volatile sig_atomic_t njs_running;
volatile sig_atomic_t njs_sigint_count;
volatile sig_atomic_t njs_sigint_received;


static void
njs_cb_line_handler(char *line_in)
{
    njs_int_t  ret;
    njs_str_t  line;

    if (line_in == NULL) {
        njs_running = NJS_DONE;
        return;
    }

    line.start = (u_char *) line_in;
    line.length = njs_strlen(line.start);

    if (strcmp(line_in, ".exit") == 0) {
        njs_running = NJS_DONE;
        goto free_line;
    }

    njs_sigint_count = 0;

    if (line.length == 0) {
        rl_callback_handler_install(">> ", njs_cb_line_handler);
        goto free_line;
    }

    add_history((char *) line.start);

    ret = njs_process_script(njs_console.engine, &njs_console, &line);
    if (ret == NJS_ERROR) {
        njs_running = NJS_ERROR;
    }

    if (ret == NJS_OK) {
        rl_callback_handler_install(">> ", njs_cb_line_handler);
    }

free_line:

    free(line.start);
}


static njs_int_t
njs_interactive_shell(njs_opts_t *opts)
{
    int             flags;
    fd_set          fds;
    njs_int_t       ret;
    njs_engine_t    *engine;
    struct timeval  timeout;

    if (njs_editline_init() != NJS_OK) {
        njs_stderror("failed to init completions\n");
        return NJS_ERROR;
    }

    engine = njs_create_engine(opts);
    if (engine == NULL) {
        njs_stderror("njs_create_engine() failed\n");
        return NJS_ERROR;
    }

    if (!opts->quiet) {
        if (engine->type == NJS_ENGINE_NJS) {
            njs_printf("interactive njs (njs:%s)\n\n", NJS_VERSION);

#if (NJS_HAVE_QUICKJS)
        } else {
            njs_printf("interactive njs (QuickJS:%s)\n\n", NJS_QUICKJS_VERSION);
#endif
        }
    }

    rl_callback_handler_install(">> ", njs_cb_line_handler);

    flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);

    njs_running = NJS_OK;

    while (njs_running == NJS_OK) {
        FD_ZERO(&fds);
        FD_SET(STDIN_FILENO, &fds);
        timeout = (struct timeval) {1, 0};

        ret = select(FD_SETSIZE, &fds, NULL, NULL, &timeout);
        if (ret < 0 && errno != EINTR) {
            njs_stderror("select() failed\n");
            njs_running = NJS_ERROR;
            break;
        }

        if (njs_sigint_received) {
            if (njs_sigint_count > 1) {
                njs_running = NJS_DONE;
                break;
            }

            if (rl_end != 0) {
                njs_printf("\n");

                njs_sigint_count = 0;

            } else {
                njs_printf("(To exit, press Ctrl+C again or Ctrl+D "
                           "or type .exit)\n");

                njs_sigint_count = 1;
            }

            rl_point = rl_end = 0;
            rl_on_new_line();
            rl_redisplay();

            njs_sigint_received = 0;
        }

        if (ret < 0) {
            continue;
        }

        if (FD_ISSET(fileno(rl_instream), &fds)) {
            rl_callback_read_char();
        }
    }

    rl_callback_handler_remove();

    if (njs_running == NJS_DONE) {
        njs_printf("exiting\n");
    }

    engine->destroy(engine);

    return njs_running == NJS_DONE ? NJS_OK : njs_running;
}


static char **
njs_completion_handler(const char *text, int start, int end)
{
    rl_attempted_completion_over = 1;

    return rl_completion_matches(text, njs_completion_generator);
}


static void
njs_signal_handler(int signal)
{
    switch (signal) {
    case SIGINT:
        njs_sigint_received = 1;
        njs_sigint_count += 1;
        break;
    default:
        break;
    }
}


static njs_int_t
njs_editline_init(void)
{
    rl_completion_append_character = '\0';
    rl_attempted_completion_function = njs_completion_handler;
    rl_basic_word_break_characters = (char *) " \t\n\"\\'`@$><=;,|&{(";

    setlocale(LC_ALL, "");

    signal(SIGINT, njs_signal_handler);

    return NJS_OK;
}


static char *
njs_completion_generator(const char *text, int state)
{
    njs_str_t         expression, *suffix;
    njs_engine_t      *engine;
    njs_completion_t  *cmpl;

    engine = njs_console.engine;
    cmpl = &engine->completion;

    if (state == 0) {
        cmpl->index = 0;
        expression.start = (u_char *) text;
        expression.length = njs_strlen(text);

        cmpl->suffix_completions = engine->complete(engine, &expression);
        if (cmpl->suffix_completions == NULL) {
            return NULL;
        }
    }

    if (cmpl->index == cmpl->suffix_completions->items) {
        return NULL;
    }

    suffix = njs_arr_item(cmpl->suffix_completions, cmpl->index++);

    return strndup((char *) suffix->start, suffix->length);
}

#endif


static njs_int_t
njs_ext_console_log(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t magic, njs_value_t *retval)
{
    njs_str_t        msg;
    njs_uint_t       n;
    njs_log_level_t  level;

    n = 1;
    level = (njs_log_level_t) magic & NJS_LOG_MASK;

    while (n < nargs) {
        if (njs_vm_value_dump(vm, &msg, njs_argument(args, n), 1,
                              !!(magic & NJS_LOG_DUMP))
            == NJS_ERROR)
        {
            return NJS_ERROR;
        }

        njs_console_logger(level, msg.start, msg.length);

        n++;
    }

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
njs_ext_console_time(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_int_t      ret;
    njs_str_t      name;
    njs_value_t    *value;
    njs_console_t  *console;

    static const njs_str_t  default_label = njs_str("default");

    console = njs_vm_external(vm, njs_console_proto_id, njs_argument(args, 0));
    if (njs_slow_path(console == NULL)) {
        njs_vm_error(vm, "external value is expected");
        return NJS_ERROR;
    }

    name = default_label;

    value = njs_arg(args, nargs, 1);

    if (njs_slow_path(!njs_value_is_string(value))) {
        if (!njs_value_is_undefined(value)) {
            ret = njs_value_to_string(vm, value, value);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }

            njs_value_string_get(vm, value, &name);
        }

    } else {
        njs_value_string_get(vm, value, &name);
    }

    if (njs_console_time(console, &name) != NJS_OK) {
        njs_vm_error(vm, "failed to add timer");
        return NJS_ERROR;
    }

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
njs_ext_console_time_end(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    uint64_t       ns;
    njs_int_t      ret;
    njs_str_t      name;
    njs_value_t    *value;
    njs_console_t  *console;

    static const njs_str_t  default_label = njs_str("default");

    ns = njs_time();

    console = njs_vm_external(vm, njs_console_proto_id, njs_argument(args, 0));
    if (njs_slow_path(console == NULL)) {
        njs_vm_error(vm, "external value is expected");
        return NJS_ERROR;
    }

    name = default_label;

    value = njs_arg(args, nargs, 1);

    if (njs_slow_path(!njs_value_is_string(value))) {
        if (!njs_value_is_undefined(value)) {
            ret = njs_value_to_string(vm, value, value);
            if (njs_slow_path(ret != NJS_OK)) {
                return ret;
            }

            njs_value_string_get(vm, value, &name);
        }

    } else {
        njs_value_string_get(vm, value, &name);
    }

    njs_console_time_end(console, &name, ns);

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static njs_int_t
njs_set_timer(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_bool_t immediate, njs_value_t *retval)
{
    njs_ev_t       *ev;
    uint64_t       delay;
    njs_uint_t     n;
    njs_console_t  *console;

    console = njs_vm_external_ptr(vm);

    if (njs_slow_path(nargs < 2)) {
        njs_vm_type_error(vm, "too few arguments");
        return NJS_ERROR;
    }

    if (njs_slow_path(!njs_value_is_function(njs_argument(args, 1)))) {
        njs_vm_type_error(vm, "first arg must be a function");
        return NJS_ERROR;
    }

    delay = 0;

    if (!immediate && nargs >= 3
        && njs_value_is_number(njs_argument(args, 2)))
    {
        delay = njs_value_number(njs_argument(args, 2));
    }

    if (delay != 0) {
        njs_vm_internal_error(vm, "njs_set_timer(): async timers unsupported");
        return NJS_ERROR;
    }

    n = immediate ? 2 : 3;
    nargs = (nargs >= n) ? nargs - n : 0;

    ev = njs_mp_alloc(njs_vm_memory_pool(vm),
                      sizeof(njs_ev_t) + sizeof(njs_opaque_value_t) * nargs);
    if (njs_slow_path(ev == NULL)) {
        njs_vm_memory_error(vm);
        return NJS_ERROR;
    }

    ev->u.njs.function = njs_value_function(njs_argument(args, 1));
    ev->u.njs.args = (njs_value_t *) ((u_char *) ev + sizeof(njs_ev_t));
    ev->nargs = nargs;
    ev->id = console->event_id++;

    if (ev->nargs != 0) {
        memcpy(ev->u.njs.args, njs_argument(args, n),
               sizeof(njs_opaque_value_t) * ev->nargs);
    }

    njs_rbtree_insert(&console->events, &ev->node);

    njs_queue_insert_tail(&console->posted_events, &ev->link);

    njs_value_number_set(retval, ev->id);

    return NJS_OK;
}


static njs_int_t
njs_set_timeout(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    return njs_set_timer(vm, args, nargs, unused, 0, retval);
}


static njs_int_t
njs_set_immediate(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    return njs_set_timer(vm, args, nargs, unused, 1, retval);
}


static njs_int_t
njs_clear_timeout(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused, njs_value_t *retval)
{
    njs_ev_t           ev_lookup, *ev;
    njs_console_t      *console;
    njs_rbtree_node_t  *rb;

    if (nargs < 2 || !njs_value_is_number(njs_argument(args, 1))) {
        njs_value_undefined_set(retval);
        return NJS_OK;
    }

    console = njs_vm_external_ptr(vm);

    ev_lookup.id = njs_value_number(njs_argument(args, 1));

    rb = njs_rbtree_find(&console->events, &ev_lookup.node);
    if (njs_slow_path(rb == NULL)) {
        njs_vm_internal_error(vm, "failed to find timer");
        return NJS_ERROR;
    }

    ev = (njs_ev_t *) rb;
    njs_queue_remove(&ev->link);
    njs_rbtree_delete(&console->events, (njs_rbtree_part_t *) rb);

    njs_mp_free(njs_vm_memory_pool(vm), ev);

    njs_value_undefined_set(retval);

    return NJS_OK;
}


static void
njs_console_log(njs_log_level_t level, const char *fmt, ...)
{
    u_char   *p;
    va_list  args;
    u_char   buf[2048];

    va_start(args, fmt);
    p = njs_vsprintf(buf, buf + sizeof(buf), fmt, args);
    va_end(args);

    njs_console_logger(level, buf, p - buf);
}


static void
njs_console_logger(njs_log_level_t level, const u_char *start, size_t length)
{
    switch (level) {
    case NJS_LOG_WARN:
        njs_printf("W: ");
        break;
    case NJS_LOG_ERROR:
        njs_printf("E: ");
        break;
    case NJS_LOG_INFO:
        break;
    }

    njs_print(start, length);
    njs_print("\n", 1);
}


static njs_int_t
njs_console_time(njs_console_t *console, njs_str_t *name)
{
    njs_queue_t       *labels;
    njs_timelabel_t   *label;
    njs_queue_link_t  *link;

    labels = &console->labels;
    link = njs_queue_first(labels);

    while (link != njs_queue_tail(labels)) {
        label = njs_queue_link_data(link, njs_timelabel_t, link);

        if (njs_strstr_eq(name, &label->name)) {
            njs_console_log(NJS_LOG_INFO, "Timer \"%V\" already exists.",
                            name);
            return NJS_OK;
        }

        link = njs_queue_next(link);
    }

    label = njs_mp_alloc(console->engine->pool,
                         sizeof(njs_timelabel_t) + name->length);
    if (njs_slow_path(label == NULL)) {
        return NJS_ERROR;
    }

    label->name.start = (u_char *) label + sizeof(njs_timelabel_t);
    memcpy(label->name.start, name->start, name->length);
    label->name.length = name->length;
    label->time = njs_time();

    njs_queue_insert_tail(&console->labels, &label->link);

    return NJS_OK;
}


static void
njs_console_time_end(njs_console_t *console, njs_str_t *name, uint64_t ns)
{
    uint64_t          ms;
    njs_queue_t       *labels;
    njs_timelabel_t   *label;
    njs_queue_link_t  *link;

    labels = &console->labels;
    link = njs_queue_first(labels);

    for ( ;; ) {
        if (link == njs_queue_tail(labels)) {
            njs_console_log(NJS_LOG_INFO, "Timer \"%V\" doesn’t exist.",
                            name);
            return;
        }

        label = njs_queue_link_data(link, njs_timelabel_t, link);

        if (njs_strstr_eq(name, &label->name)) {
            njs_queue_remove(&label->link);
            break;
        }

        link = njs_queue_next(link);
    }

    ns = ns - label->time;

    ms = ns / 1000000;
    ns = ns % 1000000;

    njs_console_log(NJS_LOG_INFO, "%V: %uL.%06uLms", name, ms, ns);

    njs_mp_free(console->engine->pool, label);
}


static intptr_t
njs_event_rbtree_compare(njs_rbtree_node_t *node1, njs_rbtree_node_t *node2)
{
    njs_ev_t  *ev1, *ev2;

    ev1 = (njs_ev_t *) node1;
    ev2 = (njs_ev_t *) node2;

    if (ev1->id < ev2->id) {
        return -1;
    }

    if (ev1->id > ev2->id) {
        return 1;
    }

    return 0;
}


static uint64_t
njs_time(void)
{
#if (NJS_HAVE_CLOCK_MONOTONIC)
    struct timespec ts;

    clock_gettime(CLOCK_MONOTONIC, &ts);

    return (uint64_t) ts.tv_sec * 1000000000 + ts.tv_nsec;
#else
    struct timeval tv;

    gettimeofday(&tv, NULL);

    return (uint64_t) tv.tv_sec * 1000000000 + tv.tv_usec * 1000;
#endif
}
