#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <string.h> #include <unistd.h> #include <limits.h> #include <getopt.h> #include <zend.h> #include <zend_extensions.h> #include <zend_builtin_functions.h> #include <zend_exceptions.h> #include <embed/php_embed.h> #include <standard/php_var.h> #include <standard/info.h> #include "func.h" #include "hash.h" static volatile int is_perf = 0; #ifndef ZEND_CONSTANT_SET_FLAGS #define ZEND_CONSTANT_SET_FLAGS(z,c,n) do { \ (z)->flags = c; \ (z)->module_number = n; \ } while(0) #endif #if PHP_VERSION_ID < 70400 void zend_stream_init_filename(zend_file_handle *file_handle, const char *script_file) { int c; memset(file_handle, 0, sizeof(zend_file_handle)); file_handle->type = ZEND_HANDLE_FP; file_handle->opened_path = NULL; file_handle->free_filename = 0; if (!(file_handle->handle.fp = VCWD_FOPEN(script_file, "rb"))) { fprintf(stderr, "Could not open input file: %s\n", script_file); zend_bailout(); return; } file_handle->filename = script_file; CG(start_lineno) = 1; /* #!php support */ c = fgetc(file_handle->handle.fp); if (c == '#' && (c = fgetc(file_handle->handle.fp)) == '!') { while (c != '\n' && c != '\r' && c != EOF) { c = fgetc(file_handle->handle.fp); /* skip to end of line */ } /* handle situations where line is terminated by \r\n */ if (c == '\r') { if (fgetc(file_handle->handle.fp) != '\n') { zend_long pos = zend_ftell(file_handle->handle.fp); zend_fseek(file_handle->handle.fp, pos - 1, SEEK_SET); } } CG(start_lineno) = 2; } else { rewind(file_handle->handle.fp); } } #endif void cli_register_file_handles(void) { php_stream *s_in, *s_out, *s_err; php_stream_context *sc_in=NULL, *sc_out=NULL, *sc_err=NULL; zend_constant ic, oc, ec; s_in = php_stream_open_wrapper_ex("php://stdin", "rb", 0, NULL, sc_in); s_out = php_stream_open_wrapper_ex("php://stdout", "wb", 0, NULL, sc_out); s_err = php_stream_open_wrapper_ex("php://stderr", "wb", 0, NULL, sc_err); if (s_in==NULL || s_out==NULL || s_err==NULL) { if (s_in) php_stream_close(s_in); if (s_out) php_stream_close(s_out); if (s_err) php_stream_close(s_err); return; } /* do not close stdout and stderr */ s_in->flags |= PHP_STREAM_FLAG_NO_CLOSE; s_out->flags |= PHP_STREAM_FLAG_NO_CLOSE; s_err->flags |= PHP_STREAM_FLAG_NO_CLOSE; php_stream_to_zval(s_in, &ic.value); php_stream_to_zval(s_out, &oc.value); php_stream_to_zval(s_err, &ec.value); ZEND_CONSTANT_SET_FLAGS(&ic, CONST_CS, 0); ic.name = zend_string_init_interned("STDIN", sizeof("STDIN")-1, 0); zend_register_constant(&ic); ZEND_CONSTANT_SET_FLAGS(&oc, CONST_CS, 0); oc.name = zend_string_init_interned("STDOUT", sizeof("STDOUT")-1, 0); zend_register_constant(&oc); ZEND_CONSTANT_SET_FLAGS(&ec, CONST_CS, 0); ec.name = zend_string_init_interned("STDERR", sizeof("STDERR")-1, 0); zend_register_constant(&ec); } static void sapi_cli_register_variables(zval *var) { char *filename = SG(request_info).path_translated; if(!filename) filename = ""; //printf("filename = %s\n", filename); php_import_environment_variables(var); php_register_variable("PHP_SELF", filename, var); php_register_variable("SCRIPT_NAME", filename, var); php_register_variable("SCRIPT_FILENAME", filename, var); php_register_variable("PATH_TRANSLATED", filename, var); php_register_variable("DOCUMENT_ROOT", "", var); //php_var_dump(var, 0); } static int php_threadtask_startup(sapi_module_struct *sapi_module) { if(is_perf) { char *ret = NULL; if(asprintf(&ret, "%sopcache.jit=0\n", sapi_module->ini_entries) > 0) { free(sapi_module->ini_entries); sapi_module->ini_entries = ret; } } if (php_module_startup(sapi_module, &threadtask_module_entry, 1) == FAILURE) { return FAILURE; } return SUCCESS; } static int module_name_cmp(Bucket *f, Bucket *s) { return strcasecmp(((zend_module_entry *)Z_PTR(f->val))->name, ((zend_module_entry *)Z_PTR(s->val))->name); } static void print_modules(void) { HashTable sorted_registry; zend_module_entry *module; zend_hash_init(&sorted_registry, 50, NULL, NULL, 0); zend_hash_copy(&sorted_registry, &module_registry, NULL); #if PHP_VERSION_ID >= 80000 zend_hash_sort(&sorted_registry, module_name_cmp, 0); #else zend_hash_sort(&sorted_registry, (compare_func_t) module_name_cmp, 0); #endif ZEND_HASH_FOREACH_PTR(&sorted_registry, module) { php_printf("%s\n", module->name); } ZEND_HASH_FOREACH_END(); zend_hash_destroy(&sorted_registry); } static void print_extension_info(zend_extension *ext) { php_printf("%s\n", ext->name); } static int extension_name_cmp(const zend_llist_element **f, const zend_llist_element **s) { zend_extension *fe = (zend_extension*)(*f)->data; zend_extension *se = (zend_extension*)(*s)->data; return strcmp(fe->name, se->name); } static void print_extensions(void) { zend_llist sorted_exts; zend_llist_copy(&sorted_exts, &zend_extensions); sorted_exts.dtor = NULL; zend_llist_sort(&sorted_exts, extension_name_cmp); zend_llist_apply(&sorted_exts, (llist_apply_func_t) print_extension_info); zend_llist_destroy(&sorted_exts); } // ======================= begin perf ======================= static void (*_zend_execute_ex) (zend_execute_data *execute_data); static char *perf_basename(char *file) { char *pos = strrchr(file, '/'); if(pos) { return pos + 1; } else { return file; } } static char *perf_get_function_name(zend_execute_data *call) { zend_function *func; zend_string *name; char *ret = NULL; func = call->func; if (func && func->common.function_name) { if (Z_TYPE(call->This) == IS_OBJECT) { zend_object *object = Z_OBJ(call->This); /* $this may be passed into regular internal functions */ if (func->common.scope) { name = func->common.scope->name; #if PHP_VERSION_ID >= 70300 } else if (object->handlers->get_class_name == zend_std_get_class_name) { #else } else if (object->handlers->get_class_name == std_object_handlers.get_class_name) { #endif name = object->ce->name; } else { name = object->handlers->get_class_name(object); } if(func->op_array.fn_flags & ZEND_ACC_CLOSURE) { zend_spprintf(&ret, 1024, "%s->{closure}():%d", ZSTR_VAL(name), call->opline->lineno); } else { zend_spprintf(&ret, 1024, "%s->%s()", ZSTR_VAL(name), ZSTR_VAL(func->common.function_name)); } } else if (func->common.scope) { if(func->op_array.fn_flags & ZEND_ACC_CLOSURE) { zend_spprintf(&ret, 1024, "%s::{closure}():%d", ZSTR_VAL(func->common.scope->name), call->opline->lineno); } else { zend_spprintf(&ret, 1024, "%s::%s()", ZSTR_VAL(func->common.scope->name), ZSTR_VAL(func->common.function_name)); } } else if(func->op_array.fn_flags & ZEND_ACC_CLOSURE) { zend_spprintf(&ret, 1024, "{closure}():%s:%d", perf_basename(ZSTR_VAL(func->op_array.filename)), call->opline->lineno); } else { zend_spprintf(&ret, 1024, "%s()", ZSTR_VAL(func->common.function_name)); } } else { /* i know this is kinda ugly, but i'm trying to avoid extra cycles in the main execution loop */ bool build_filename_arg = 1; uint32_t include_kind = 0; if (func && ZEND_USER_CODE(func->common.type) && call->opline->opcode == ZEND_INCLUDE_OR_EVAL) { include_kind = call->opline->extended_value; } switch (include_kind) { case ZEND_EVAL: name = ZSTR_KNOWN(ZEND_STR_EVAL); build_filename_arg = 0; break; case ZEND_INCLUDE: name = ZSTR_KNOWN(ZEND_STR_INCLUDE); break; case ZEND_REQUIRE: name = ZSTR_KNOWN(ZEND_STR_REQUIRE); break; case ZEND_INCLUDE_ONCE: name = ZSTR_KNOWN(ZEND_STR_INCLUDE_ONCE); break; case ZEND_REQUIRE_ONCE: name = ZSTR_KNOWN(ZEND_STR_REQUIRE_ONCE); break; default: return NULL; } if (build_filename_arg && func && ZEND_USER_CODE(func->common.type)) { zend_spprintf(&ret, 1024, "%s(%s)", ZSTR_VAL(name), perf_basename(ZSTR_VAL(func->op_array.filename))); } else { zend_spprintf(&ret, 1024, "%s()", ZSTR_VAL(name)); } } return ret; } static ts_hash_table_t perf_ht; typedef struct { int n; float m; double t; } perf_t; static void perf_free(value_t *v) { } static void perf_execute_ex(zend_execute_data *execute_data) { char *func = perf_get_function_name(execute_data); if(func) { double t = microtime(); _zend_execute_ex(execute_data); t = microtime() - t; ts_hash_table_wr_lock(&perf_ht); { int size = strlen(func); zend_long h = zend_get_hash_value(func, size); perf_t v; if(hash_table_quick_find(&perf_ht.ht, func, size, h, (value_t*) &v) == FAILURE) { memset(&v, 0, sizeof(v)); } v.n ++; v.t += t; if(t > v.m) { v.m = t; } hash_table_quick_update(&perf_ht.ht, func, size, h, (value_t*) &v, NULL); } ts_hash_table_wr_unlock(&perf_ht); efree(func); } else { _zend_execute_ex(execute_data); } } static int perf_sort_func(const bucket_t *a, const bucket_t *b) { perf_t *ap = (perf_t*) &a->value; perf_t *bp = (perf_t*) &b->value; if(ap->t > bp->t) { return 1; } else if(ap->t < bp->t) { return -1; } else { return 0; } } int compare_key_nature(const bucket_t *a, const bucket_t *b) { if(a->nKeyLength == 0) { if(b->nKeyLength == 0) { if(a->h > b->h) { return 1; } else if(a->h < b->h) { return -1; } else { return 0; } } else { return 1; } } else if(b->nKeyLength == 0) { return -1; } else { return - strnatcmp(a->arKey, a->nKeyLength, b->arKey, b->nKeyLength, 0); } } static int perf_apply_avg_func(bucket_t *pDest) { perf_t *p = (perf_t*) &pDest->value; p->t = p->t / (double) p->n; return HASH_TABLE_APPLY_KEEP; } static int perf_apply_print_func(bucket_t *pDest, int *i) { perf_t *p = (perf_t*) &pDest->value; fprintf(stderr, "%04d %10d %10.6lf %10.6lf %s\n", *i, p->n, p->t, p->m, pDest->arKey); (*i) ++; return HASH_TABLE_APPLY_REMOVE; } // ======================= end perf ======================= static const char *options = "pkDd:t:rc:mivh"; static struct option OPTIONS[] = { {"perf", 0, 0, 'p' }, {"key", 0, 0, 'k' }, {"debug", 0, 0, 'D' }, {"delay", 1, 0, 'd' }, {"threads", 1, 0, 't' }, {"reload", 0, 0, 'r' }, {"config", 1, 0, 'c' }, {"modules", 0, 0, 'm' }, {"info", 0, 0, 'i' }, {"version", 0, 0, 'v' }, {"help", 0, 0, 'h' }, {NULL, 0, 0, 0 } }; int main(int argc, char *argv[]) { zend_file_handle file_handle; int opt, ind = 0; char path[PATH_MAX]; size_t sz = readlink("/proc/self/exe", path, PATH_MAX); path[sz] = '\0'; char *ini_path_override = NULL; int is_module_list = 0, is_print_info = 0; while((opt = getopt_long(argc, argv, options, OPTIONS, &ind)) != -1) { switch(opt) { case 'p': is_perf = 1; break; case 'k': is_perf = 2; break; case 'D': isDebug = 1; break; case 'd': delay = atoi(optarg); if(delay < 1) delay = 1; break; case 't': maxthreads = atoi(optarg); if(maxthreads < 1) maxthreads = 1; break; case 'r': isReload = 1; break; case 'c': ini_path_override = optarg; break; case 'm': is_module_list = 1; break; case 'i': is_print_info = 1; break; case 'v': printf("%s\n", PHP_VERSION); return 0; break; case 'h': case '?': default: goto usage; } } if(is_module_list == 0 && is_print_info == 0 && optind >= argc) { goto usage; } thread_init(); php_embed_module.executable_location = path; php_embed_module.php_ini_path_override = ini_path_override; #ifndef SAPI_NAME php_embed_module.name = "cli"; #else php_embed_module.name = SAPI_NAME; #endif php_embed_module.startup = php_threadtask_startup; php_embed_module.register_server_variables = sapi_cli_register_variables; php_embed_module.phpinfo_as_text = is_print_info; old_ub_write_handler = php_embed_module.ub_write; old_flush_handler = php_embed_module.flush; php_embed_module.ub_write = php_thread_ub_write_handler; php_embed_module.flush = php_thread_flush_handler; if(php_embed_init(argc-optind, argv+optind) == FAILURE) { fprintf(stderr, "php_embed_init failure\n"); return 1; } if(is_perf) { _zend_execute_ex = zend_execute_ex; zend_execute_ex = perf_execute_ex; // printf("value_t: %ld, perf_t: %ld\n", sizeof(value_t), sizeof(perf_t)); ts_hash_table_init_ex(&perf_ht, 128, perf_free); } zend_register_string_constant(ZEND_STRL("THREAD_TASK_NAME"), "main", CONST_CS, PHP_USER_CONSTANT); cli_register_file_handles(); if(is_module_list) { php_printf("[PHP Modules]\n"); print_modules(); php_printf("\n[Zend Modules]\n"); print_extensions(); php_printf("\n"); php_output_end_all(); EG(exit_status) = 0; goto out; } if(is_print_info) { php_print_info(PHP_INFO_ALL & ~PHP_INFO_CREDITS); php_output_end_all(); EG(exit_status) = 0; goto out; } thread_running(); #if PHP_VERSION_ID >= 70400 CG(skip_shebang) = 1; #endif SG(request_info).path_translated = argv[optind]; zend_register_long_constant(ZEND_STRL("THREAD_TASK_NUM"), maxthreads, CONST_CS, PHP_USER_CONSTANT); zend_register_long_constant(ZEND_STRL("THREAD_TASK_DELAY"), delay, CONST_CS, PHP_USER_CONSTANT); dprintf("BEGIN THREADTASK\n"); zend_first_try { zend_stream_init_filename(&file_handle, argv[optind]); php_execute_script(&file_handle); } zend_catch { if(EG(exit_status)) { isReload = 1; } } zend_end_try(); dprintf("END THREADTASK\n"); if(is_perf) { int i = 1; hash_table_apply(&perf_ht.ht, perf_apply_avg_func); hash_table_sort(&perf_ht.ht, is_perf == 1 ? perf_sort_func : compare_key_nature, 0); fprintf(stderr, "==================== PERF =========================================\n"); fprintf(stderr, " ID Times AVG MAX INFO\n"); fprintf(stderr, "-------------------------------------------------------------------\n"); hash_table_apply_with_argument(&perf_ht.ht, (hash_apply_func_arg_t) perf_apply_print_func, &i); ts_hash_table_destroy_ex(&perf_ht, 0); zend_execute_ex = _zend_execute_ex; } if(isReload) { dprintf("RELOAD THREADTASK\n"); char **args = (char**) malloc(sizeof(char*)*(argc+1)); memcpy(args, argv, sizeof(char*)*argc); args[argc] = NULL; execv(path, args); perror("execv"); } { zval func, retval, params[0]; ZVAL_LONG(¶ms[0], SIGINT); ZVAL_STRING(&func, "task_wait"); call_user_function(NULL, NULL, &func, &retval, 1, params); zval_ptr_dtor(&func); zval_ptr_dtor(&retval); } out: php_embed_shutdown(); thread_destroy(); return 0; usage: fprintf(stderr, "usage: %s [options] <phpfile> args...\n" " -h,--help This help text\n" " -p,--perf Perf info\n" " -k,--key Perf sort for key\n" " -D,--debug Debug info\n" " -d,--delay <delay> Delay seconds\n" " -t,--threads <threads> Max threads\n" " -r,--reload Auto reload\n" " -c,--config <path|file> Look for php.ini file in this directory\n" " -m,--modules PHP extension list\n" " -v,--version PHP Version\n" " -i,--info PHP information\n" , argv[0]); return 255; }