/*
   +----------------------------------------------------------------------+
   | Copyright © The PHP Group and Contributors.                          |
   +----------------------------------------------------------------------+
   | This source file is subject to the Modified BSD License that is      |
   | bundled with this package in the file LICENSE, and is available      |
   | through the World Wide Web at <https://www.php.net/license/>.        |
   |                                                                      |
   | SPDX-License-Identifier: BSD-3-Clause                                |
   +----------------------------------------------------------------------+
   | Author: Anatol Belski <ab@php.net>                                   |
   +----------------------------------------------------------------------+
*/

#include "php.h"
#include "SAPI.h"

#include "win32/console.h"

/* true globals; only used from main thread and from kernel callback */
static zend_fcall_info_cache ctrl_handler;
static DWORD ctrl_evt = (DWORD)-1;
static zend_atomic_bool *vm_interrupt_flag = NULL;

static void (*orig_interrupt_function)(zend_execute_data *execute_data);

static void php_win32_signal_ctrl_interrupt_function(zend_execute_data *execute_data)
{/*{{{*/
	if (ZEND_FCC_INITIALIZED(ctrl_handler)) {
		zval params[1];

		ZVAL_LONG(&params[0], ctrl_evt);

		zend_call_known_fcc(&ctrl_handler, NULL, 1, params, NULL);
	}

	if (orig_interrupt_function) {
		orig_interrupt_function(execute_data);
	}
}/*}}}*/

PHP_WINUTIL_API void php_win32_signal_ctrl_handler_init(void)
{/*{{{*/
	/* We are in the main thread! */
	if (!php_win32_console_is_cli_sapi()) {
		return;
	}

	orig_interrupt_function = zend_interrupt_function;
	zend_interrupt_function = php_win32_signal_ctrl_interrupt_function;
	vm_interrupt_flag = &EG(vm_interrupt);
	ctrl_handler = empty_fcall_info_cache;

	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_EVENT_CTRL_C", CTRL_C_EVENT, CONST_PERSISTENT);
	REGISTER_MAIN_LONG_CONSTANT("PHP_WINDOWS_EVENT_CTRL_BREAK", CTRL_BREAK_EVENT, CONST_PERSISTENT);
}/*}}}*/

PHP_WINUTIL_API void php_win32_signal_ctrl_handler_shutdown(void)
{/*{{{*/
	if (!php_win32_console_is_cli_sapi()) {
		return;
	}

	zend_interrupt_function = orig_interrupt_function;
	orig_interrupt_function = NULL;
	vm_interrupt_flag = NULL;
}/*}}}*/

PHP_WINUTIL_API void php_win32_signal_ctrl_handler_request_shutdown(void)
{
	/* Must be initialized and in main thread */
	if (!vm_interrupt_flag) {
		return;
	}
#ifdef ZTS
	if (!tsrm_is_main_thread()) {
		return;
	}
#endif

	/* The ctrl_handler must be cleared between requests, otherwise we can crash
	 * due to accessing a previous request's memory. */
	if (ZEND_FCC_INITIALIZED(ctrl_handler)) {
		zend_fcc_dtor(&ctrl_handler);
	}
}

static BOOL WINAPI php_win32_signal_system_ctrl_handler(DWORD evt)
{/*{{{*/
	if (CTRL_C_EVENT != evt && CTRL_BREAK_EVENT != evt) {
		return FALSE;
	}

	zend_atomic_bool_store_ex(vm_interrupt_flag, true);

	ctrl_evt = evt;

	return TRUE;
}/*}}}*/

/* {{{ Assigns a CTRL signal handler to a PHP function */
PHP_FUNCTION(sapi_windows_set_ctrl_handler)
{
	zend_fcall_info fci;
	zend_fcall_info_cache fcc;
	bool add = 1;


	/* callable argument corresponds to the CTRL handler */
	if (zend_parse_parameters(ZEND_NUM_ARGS(), "F!|b", &fci, &fcc, &add) == FAILURE) {
		RETURN_THROWS();
	}

#ifdef ZTS
	if (!tsrm_is_main_thread()) {
		if (ZEND_FCC_INITIALIZED(fcc)) {
			zend_release_fcall_info_cache(&fcc);
		}
		zend_throw_error(NULL, "CTRL events can only be received on the main thread");
		RETURN_THROWS();
	}
#endif

	if (!php_win32_console_is_cli_sapi()) {
		if (ZEND_FCC_INITIALIZED(fcc)) {
			zend_release_fcall_info_cache(&fcc);
		}
		zend_throw_error(NULL, "CTRL events trapping is only supported on console");
		RETURN_THROWS();
	}

	if (!ZEND_FCC_INITIALIZED(fcc)) {
		if (ZEND_FCC_INITIALIZED(ctrl_handler)) {
			zend_fcc_dtor(&ctrl_handler);
		}
		RETURN_BOOL(SetConsoleCtrlHandler(NULL, add));
	}

	if (!SetConsoleCtrlHandler(NULL, FALSE) || !SetConsoleCtrlHandler(php_win32_signal_system_ctrl_handler, add)) {
		zend_string *func_name = zend_get_callable_name(&fci.function_name);
		php_error_docref(NULL, E_WARNING, "Unable to attach %s as a CTRL handler", ZSTR_VAL(func_name));
		zend_string_release_ex(func_name, 0);
		zend_release_fcall_info_cache(&fcc);
		RETURN_FALSE;
	}

	if (ZEND_FCC_INITIALIZED(ctrl_handler)) {
		zend_fcc_dtor(&ctrl_handler);
	}
	zend_fcc_dup(&ctrl_handler, &fcc);

	RETURN_TRUE;
}/*}}}*/

PHP_FUNCTION(sapi_windows_generate_ctrl_event)
{/*{{{*/
	zend_long evt, pid = 0;
	bool ret = 0;

	if (zend_parse_parameters(ZEND_NUM_ARGS(), "l|l", &evt, &pid) == FAILURE) {
		RETURN_THROWS();
	}

	if (!php_win32_console_is_cli_sapi()) {
		zend_throw_error(NULL, "CTRL events trapping is only supported on console");
		return;
	}

	SetConsoleCtrlHandler(NULL, TRUE);

	ret = (GenerateConsoleCtrlEvent(evt, pid) != 0);

	if (ZEND_FCC_INITIALIZED(ctrl_handler)) {
		ret = ret && SetConsoleCtrlHandler(php_win32_signal_system_ctrl_handler, TRUE);
	}

	RETURN_BOOL(ret);
}/*}}}*/
