diff --git a/examples/c_contract/hotpocket_contract.h b/examples/c_contract/hotpocket_contract.h index eb62b3de..fbb18aab 100644 --- a/examples/c_contract/hotpocket_contract.h +++ b/examples/c_contract/hotpocket_contract.h @@ -11,13 +11,18 @@ #include #include "json.h" +// Private constants. #define __HP_MMAP_BLOCK_SIZE 4096 #define __HP_MMAP_BLOCK_ALIGN(x) (((x) + ((off_t)(__HP_MMAP_BLOCK_SIZE)-1)) & ~((off_t)(__HP_MMAP_BLOCK_SIZE)-1)) #define __HP_STREAM_MSG_HEADER_SIZE 4 #define __HP_SEQPKT_MAX_SIZE 131072 // 128KB to support SEQ_PACKET sockets. +const char *__HP_PATCH_FILE_PATH = "../patch.cfg"; + +// Public constants. #define HP_NPL_MSG_MAX_SIZE __HP_SEQPKT_MAX_SIZE #define HP_KEY_SIZE 66 // Hex pubkey size. (64 char key + 2 chars for key type prfix) #define HP_HASH_SIZE 64 +const char *HP_POST_EXEC_SCRIPT_NAME = "post_exec.sh"; #define __HP_ASSIGN_STRING(dest, elem) \ { \ @@ -174,8 +179,6 @@ struct __hp_contract size_t user_inmap_size; }; -const char *PATCH_FILE_PATH = "../patch.cfg"; - int hp_init_contract(); int hp_deinit_contract(); const struct hp_contract_context *hp_get_context(); @@ -438,7 +441,7 @@ int hp_read_npl_msg(void *msg_buf, char *pubkey_buf, const int timeout) */ struct hp_config *hp_get_config() { - const int fd = open(PATCH_FILE_PATH, O_RDONLY); + const int fd = open(__HP_PATCH_FILE_PATH, O_RDONLY); if (fd == -1) { fprintf(stderr, "Error opening patch.cfg file.\n"); @@ -512,7 +515,7 @@ int hp_update_config(const struct hp_config *config) config->round_limits.proc_cpu_seconds < 0 || config->round_limits.proc_mem_bytes < 0 || config->round_limits.proc_ofd_count < 0) __HP_UPDATE_CONFIG_ERROR("Invalid round limits."); - const int fd = open(PATCH_FILE_PATH, O_RDWR); + const int fd = open(__HP_PATCH_FILE_PATH, O_RDWR); if (fd == -1) __HP_UPDATE_CONFIG_ERROR("Error opening patch.cfg file."); diff --git a/examples/nodejs_contract/hp-contract-lib.js b/examples/nodejs_contract/hp-contract-lib.js index bfdad449..6769b3cc 100644 --- a/examples/nodejs_contract/hp-contract-lib.js +++ b/examples/nodejs_contract/hp-contract-lib.js @@ -16,6 +16,7 @@ const clientProtocols = { Object.freeze(clientProtocols); const PATCH_CONFIG_PATH = "../patch.cfg"; +const POST_EXEC_SCRIPT_NAME = "post_exec.sh"; class HotPocketContract { @@ -399,5 +400,6 @@ const errHandler = (err) => console.log(err); module.exports = { Contract: HotPocketContract, - clientProtocols + clientProtocols, + POST_EXEC_SCRIPT_NAME, } \ No newline at end of file diff --git a/src/sc/sc.cpp b/src/sc/sc.cpp index e404615a..63d830db 100644 --- a/src/sc/sc.cpp +++ b/src/sc/sc.cpp @@ -16,6 +16,7 @@ namespace sc constexpr int FILE_PERMS = 0644; constexpr const char *STDOUT_LOG = ".stdout.log"; constexpr const char *STDERR_LOG = ".stderr.log"; + constexpr const char *POST_EXEC_SCRIPT = "post_exec.sh"; constexpr uint32_t CONTRACT_FS_ID = 0; @@ -51,6 +52,7 @@ namespace sc contract_server.deinit(); contract_fs.deinit(); } + /** * Executes the contract process and passes the specified context arguments. * @return 0 on successful process creation. -1 on failure or contract process is already running. @@ -61,6 +63,24 @@ namespace sc if (start_hpfs_session(ctx) == -1) return -1; + // Set contract working directory. + ctx.working_dir = contract_fs.physical_path(ctx.args.hpfs_session_name, STATE_DIR_PATH); + + // Setup contract output log file paths. + if (conf::cfg.contract.log_output) + { + const time_t epoch = util::get_epoch_milliseconds() / 1000; + std::stringstream now_ss; + now_ss << std::put_time(std::localtime(&epoch), "%Y%m%dT%H%M%S"); + const std::string now = now_ss.str(); + + // For consensus execution, we keep appending logs to the same out/err files. + // For read request executions, independent log files are created based on read request session names. + const std::string prefix = ctx.args.readonly ? (ctx.args.hpfs_session_name + "_" + now) : ctx.args.hpfs_session_name; + ctx.stdout_file = conf::ctx.contract_log_dir + "/" + prefix + STDOUT_LOG; + ctx.stderr_file = conf::ctx.contract_log_dir + "/" + prefix + STDERR_LOG; + } + // Create the IO sockets for users, control channel and npl. // (Note: User socket will only be used for contract output only. For feeding user inputs we are using a memfd.) if (create_iosockets_for_fdmap(ctx.user_fds, ctx.args.userbufs) == -1 || @@ -133,8 +153,7 @@ namespace sc execv_args[j] = conf::cfg.contract.runtime_binexec_args[i].data(); execv_args[len - 1] = NULL; - const std::string current_dir = contract_fs.physical_path(ctx.args.hpfs_session_name, STATE_DIR_PATH); - chdir(current_dir.c_str()); + chdir(ctx.working_dir.c_str()); if (create_contract_log_files(ctx) == -1) { @@ -154,6 +173,10 @@ namespace sc cleanup_fds(ctx); + // If the consensus contact finished executing successfully, run the post-exec.sh script if it exists. + if (ctx.exit_success && !ctx.args.readonly && run_post_exec_script(ctx) == -1) + ret = -1; + if (stop_hpfs_session(ctx) == -1) ret = -1; @@ -214,12 +237,13 @@ namespace sc if (WIFEXITED(scstatus)) { + ctx.exit_success = true; LOG_DEBUG << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended normally."; return 1; } else { - LOG_ERROR << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended prematurely with code " << WEXITSTATUS(scstatus); + LOG_ERROR << "Contract process" << (ctx.args.readonly ? " (rdonly)" : "") << " ended prematurely. Exit code " << WEXITSTATUS(scstatus); return -1; } } @@ -438,6 +462,53 @@ namespace sc LOG_DEBUG << "Contract monitor stopped"; } + /** + * Runs the contract post execution script if exists. + */ + int run_post_exec_script(const execution_context &ctx) + { + // Check whether the post-exec script exists within contract state dir. + const std::string script_path = ctx.working_dir + "/" + POST_EXEC_SCRIPT; + if (!util::is_file_exists(script_path.c_str())) + return 0; + + LOG_INFO << "Running post-exec script..."; + + const std::string log_redirect = conf::cfg.contract.log_output ? (" >>" + ctx.stdout_file + " 2>>" + ctx.stderr_file + " ") : ""; + + // We set current working dir and pass lcl as command line arg to the script. + const std::string command = "(cd " + ctx.working_dir + " && ./" + POST_EXEC_SCRIPT + " " + ctx.args.lcl + log_redirect + ")"; + + const int ret = system(command.c_str()); + if (ret == -1) + { + LOG_ERROR << errno << ": Could not run post-exec script " << script_path; + return -1; + } + else + { + // If the script returns a code 0 or 3 to 125 we consider it a successful execition. + // If the script returns code 0, we consider script lifetime is over and delete the file. Otherwise we retain the file. + const int exit_code = WEXITSTATUS(ret); + if (WIFEXITED(ret) && (exit_code == 0 || (exit_code > 2 && exit_code < 126))) + { + LOG_INFO << "Post-exec script executed successfully. Exit code:" << exit_code; + // Exit code 0 means post-execution script can be deleted. + if (exit_code == 0 && util::remove_file(script_path) == -1) + { + LOG_ERROR << errno << ": Error deleting post-exec script after execution."; + return -1; + } + } + else + { + LOG_ERROR << "Post-exec script ended prematurely. Exit code:" << exit_code; + } + + return 0; + } + } + /** * Writes any hp input messages to the contract. */ @@ -729,29 +800,18 @@ namespace sc if (!conf::cfg.contract.log_output) return 0; - const time_t epoch = util::get_epoch_milliseconds() / 1000; - std::stringstream now_ss; - now_ss << std::put_time(std::localtime(&epoch), "%Y%m%dT%H%M%S"); - const std::string now = now_ss.str(); - - // For consensus execution, we keep appending logs to the same out/err files. - // For read request executions, independent log files are created based on read request session names. - const std::string prefix = ctx.args.readonly ? (ctx.args.hpfs_session_name + "_" + now) : ctx.args.hpfs_session_name; - const std::string stdout_file = conf::ctx.contract_log_dir + "/" + prefix + STDOUT_LOG; - const std::string stderr_file = conf::ctx.contract_log_dir + "/" + prefix + STDERR_LOG; - - const int outfd = open(stdout_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS); + const int outfd = open(ctx.stdout_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS); if (outfd == -1) { - std::cerr << errno << ": Error opening " << stdout_file << "\n"; + std::cerr << errno << ": Error opening " << ctx.stdout_file << "\n"; return -1; } - const int errfd = open(stderr_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS); + const int errfd = open(ctx.stderr_file.data(), O_CREAT | O_WRONLY | O_APPEND, FILE_PERMS); if (errfd == -1) { close(outfd); - std::cerr << errno << ": Error opening " << stderr_file << "\n"; + std::cerr << errno << ": Error opening " << ctx.stderr_file << "\n"; return -1; } @@ -759,7 +819,7 @@ namespace sc // to mark the start of each execution. if (!ctx.args.readonly) { - const std::string header = "Execution lcl " + ctx.args.lcl + " on " + now + "\n"; + const std::string header = "Execution lcl " + ctx.args.lcl + "\n"; if (write(outfd, header.data(), header.size()) == -1 || write(errfd, header.data(), header.size()) == -1) { diff --git a/src/sc/sc.hpp b/src/sc/sc.hpp index 66cbe2ab..c04dca2d 100644 --- a/src/sc/sc.hpp +++ b/src/sc/sc.hpp @@ -124,10 +124,20 @@ namespace sc size_t total_npl_output_size = 0; + // The path set as contract working directory. + std::string working_dir; + + // Full paths to std out/err log files for the contract execution. + std::string stdout_file; + std::string stderr_file; + // Indicates that the contract has sent termination control message. bool termination_signaled = false; - // Indicates that the deinit procedure has begun. + // Indicates whether the contract exited normally without any errors. + bool exit_success = false; + + // Indicates that the hpcore deinit procedure has begun. bool is_shutting_down = false; execution_context(util::buffer_store &user_input_store) : args(user_input_store) @@ -158,6 +168,8 @@ namespace sc void contract_monitor_loop(execution_context &ctx); + int run_post_exec_script(const execution_context &ctx); + int write_control_inputs(execution_context &ctx); int write_npl_messages(execution_context &ctx); diff --git a/test/bin/hpfs b/test/bin/hpfs index 3a8a1c91..e67bfc32 100755 Binary files a/test/bin/hpfs and b/test/bin/hpfs differ diff --git a/test/local-cluster/cluster-create.sh b/test/local-cluster/cluster-create.sh index d36d1329..4d7ad617 100755 --- a/test/local-cluster/cluster-create.sh +++ b/test/local-cluster/cluster-create.sh @@ -25,7 +25,7 @@ if [ "$CONTRACT" = "cecho" ]; then # C echo contract gcc echo_contract.c -o echo_contract popd > /dev/null 2>&1 copyfiles="$hpcore/examples/c_contract/echo_contract" - binary="/contract/bin/echo_contract" + binary="echo_contract" elif [ "$CONTRACT" = "nodefile" ]; then # nodejs file contract (uses BSON protocol) echo "Using nodejs file contract." @@ -34,13 +34,13 @@ elif [ "$CONTRACT" = "nodefile" ]; then # nodejs file contract (uses BSON protoc popd > /dev/null 2>&1 copyfiles="$hpcore/examples/nodejs_contract/{node_modules,package.json,hp-contract-lib.js,file_contract.js}" binary="/usr/local/bin/node" - binargs="/contract/bin/file_contract.js" + binargs="file_contract.js" else # nodejs echo contract (default) echo "Using nodejs echo contract." copyfiles="$hpcore/examples/nodejs_contract/{package.json,hp-contract-lib.js,echo_contract.js}" binary="/usr/local/bin/node" - binargs="/contract/bin/echo_contract.js" + binargs="echo_contract.js" fi if [ "$loglevel" = "" ]; then @@ -123,9 +123,8 @@ do popd > /dev/null 2>&1 # Copy the contract files and appbill. - mkdir ./node$n/bin - eval "cp -r $copyfiles ./node$n/bin/" - cp ../bin/appbill ./node$n/bin/ + eval "cp -r $copyfiles ./node$n/contract_fs/seed/state/" + cp ../bin/appbill ./node$n/contract_fs/seed/state/ done # Function to generate JSON array string while skiping a given index.