/************************************************************************** * STRINGTOFILE HOOK * * Chunked non-blocking file write loop * * We need fast throughput for this function for our database interfacing * * It's also just nice to allow for faster file writing in general. * **************************************************************************/ /**************************************************************************** * TODO: * * Allow single depth EntFire calls * * Read the database for file ownership instead of comparing the metadata * ****************************************************************************/ local _stringtofile = StringToFile // local _entfire = EntFire // local _doentfire = DoEntFire // local _entfirebyhandle = EntFireByHandle local write_table = {} local start_time = Time() // write_table.setdelegate({ // _newslot = function(file, data) { // printl(file) // newthread(_stringtofile).call(file, data) // } // }) local banned_file_extensions = { "exe": false, //probably don't need to ban these "dll": false, "pdb": false, "so": false, "interface" : false, //false = default error message "contract" : false, } //Whitelisted behavior: // - skip writing metadata // - local whitelisted_files = { "contracts.nut" : true, } ::StringToFile <- function(name, str, metadata = {}) { // _stringtofile(name, str) // return if (typeof metadata == "string") compilestring(metadata)() if (!("src" in metadata)) metadata = getstackinfos(2) local name_split = split(name, ".") local name_split_len = name_split.len() local name_metadata = format("metadata/%s.metadata", name_split[0]) local filetostring_metadata = FileToString(name_metadata) //we don't use printf here because printl is hooked on potato to redirect to player console already //print, printf, and Msg still retain their vanilla behavior and print to server console only local Err = @(error = "", file_name = name) printl(format("StringToFile error in %s: %s\n", file_name, error)) if (name_split_len == 1) { Err("Invalid name! Cannot write files with no extension") return } else if (name_split_len > 2) { Err("Invalid name! Cannot have more than one period") return } else if (name_split_len > 1 && name_split[1] in banned_file_extensions && !(metadata.src in whitelisted_files)) { banned_file_extensions[name_split[1]] ? Err(banned_file_extensions[name_split[1]]) : Err(format("\"%s\" file extensions cannot be written.", name_split[1]), name_split[0]) return } else if (metadata.src == "InputRunScript") { //TODO: hook DoEntFire related functions to grab the src file. //people can nest EntFires forever, but single depth EntFire is common enough where we should support it Err("StringToFile cannot be used in InputRunScript!") return } if (filetostring_metadata) compilestring(filetostring_metadata)() local name_no_extension = name_split[0] local existing_source = "" if (name_no_extension in getroottable()) { existing_source = getroottable()[name_split[0]].src delete getroottable()[name_no_extension] } if (existing_source != "" && metadata.src != existing_source) { Err("This file is already owned by %s!", existing_source) return } //add to the write think write_table[name] <- str //don't write metadata for whitelisted files if (metadata.src in whitelisted_files) return // local file_metadata = format("::%s <- {\n", name_split[0]) local file_metadata = format("getroottable()[\"%s\"] <- {\n", name_split[0]) local time = {} LocalTime(time) // metadata.last_write_time <- GetUnixTimestamp(time) metadata.last_write_time <- format("%d-%d-%d %d:%d:%d", time.year, time.month, time.day, time.hour, time.minute, time.second) foreach(k, v in metadata) file_metadata += format("%s = \"%s\" \n", k.tostring(), v.tostring()) file_metadata += "}" write_table[name_metadata] <- file_metadata } // function GetUnixTimestamp(time) // { // local SECONDS_IN_DAY = 86400 // local SECONDS_IN_YEAR = 31536000 // local SECONDS_IN_LEAP_YEAR = 31622400 // local MONTH_DAYS = [null, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // local MONTH_DAYS_LEAP = [null, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // local EPOCH = { // year = 1970, // month = 1, // day = 1, // hour = 0, // minute = 0, // second = 0, // } // local timestamp = 0; // local time_year = time.year, epoch_year = EPOCH.year // // Years // for (local i = epoch_year; i < time_year; ++i) // timestamp += ((i % 4 == 0) ? SECONDS_IN_LEAP_YEAR : SECONDS_IN_YEAR) // // Months // for (local i = EPOCH.month; i < time.month; ++i) // { // if (time.year % 4 == 0) // timestamp += (MONTH_DAYS_LEAP[i] * SECONDS_IN_DAY) // else // timestamp += (MONTH_DAYS[i] * SECONDS_IN_DAY) // } // // Days // timestamp += ((time.day - EPOCH.day) * SECONDS_IN_DAY) // // The rest // timestamp += (time.hour * 3600) + (time.minute * 60) + time.second // return timestamp // } local function FileWriteGenerator(write_table) { while (true) { if (write_table.len() == 0) { yield false continue } local file = write_table.keys()[0] local file_nosuffix = split(file, ".")[0] local data = write_table[file].tostring() // printf("Writing file %s\n", file) _stringtofile(file, data) //crashes // newthread(_stringtofile.bindenv(_scope)).call(file, data) delete write_table[file] yield true } } // ::DoEntFire <- function(target, action, param, delay, activator, caller) { // local param_lower = param.tolower() // local action_lower = action.tolower() // local stringtofile_index = param_lower.find("stringtofile") // if (action_lower == "runscriptcode" && stringtofile_index) { // //create a global reference to the stack info // local stack_info_string = UniqueString() // local stack_info = getroottable()[stack_info_string] <- "{" // foreach(k, v in getstackinfos(2)) // stack_info += format("\"%s\" : \"%s\", ", k, v) // stack_info += "}" // local param_split = param_lower.split("(") // local file_name = split(param_split[1], ",")[0] // local file_data = split(param_split[2], ")")[0] // //remove stringtofile from the original param // // Remove the stringtofile function call from the original param // local stringtofile_end = param_lower.find(")", stringtofile_index) + 1 // param = param.slice(0, stringtofile_index) + param.slice(stringtofile_end) // // Trim any leading or trailing whitespace // param = strip(param) // // If param is not empty, add a semicolon to separate it from the new function call // if (param != "") { // param += "; " // } // param += format("StringToFile(\"%s\", \"%s\", %s)", file_name, file_data, stack_info) // } // typeof target == "string" ? _doentfire(target, action, param, delay, activator, caller) : _entfirebyhandle(target, action, param, delay, activator, caller) // delete getroottable()[stack_info_string] // } // ::EntFireByHandle <- @(target, action, param, delay, activator, caller) DoEntFire(target, action, param, delay, activator, null) // ::EntFire <- @(target, action, param = "", delay = -1, activator = null) DoEntFire(target, action, param, delay, activator, null) // ::_stringtofile_global_proxy <- function(file, data) { // local stack_info = getstackinfos(2) // if (stack_info.src != "serverinit.nut") return // _stringtofile(file, data) // } local filewrite_dummy_think_ent = Entities.CreateByClassname("move_rope") filewrite_dummy_think_ent.DispatchSpawn() filewrite_dummy_think_ent.ValidateScriptScope() // SetDestroyCallback(filewrite_dummy_think_ent, function() { // EntFire("worldspawn", "Kill") //user is trying to stop the file write think, abort and kill the server // }) local WRITE_CHUNK_SIZE = 12 //number of files to write per think. Anything higher than 12 will cause noticeable stuttering local WRITE_THINK_INTERVAL = -1 filewrite_dummy_think_ent.GetScriptScope().FileWriteThink <- function() { local write_table_len = write_table.len() // Nothing to write //block calling this function from anywhere but stringtofile.nut if (!write_table_len || getstackinfos(1).src != "stringtofile.nut") return WRITE_THINK_INTERVAL local write_generator = false local write_table_copy = {} foreach (k,v in write_table) write_table_copy[k] <- v if (!write_generator || write_generator.getstatus() == "dead") { write_generator = FileWriteGenerator(write_table_copy) } local start_time = Time() local files_written = 0 local chunk_size = write_table_len > WRITE_CHUNK_SIZE ? WRITE_CHUNK_SIZE : write_table_len while (files_written < chunk_size) { local result = resume write_generator if (result) { files_written++ } if (write_generator.getstatus() == "dead") { write_generator = FileWriteGenerator(write_table_copy) } } foreach (file in write_table.keys()) if (!(file in write_table_copy)) delete write_table[file] // printl("Remaining files: " + write_table_len) return WRITE_THINK_INTERVAL } AddThinkToEnt(filewrite_dummy_think_ent, "FileWriteThink")