/* ; Project: Open Vehicle Monitor System ; Date: 14th March 2017 ; ; Changes: ; 1.0 Initial release ; ; (C) 2011 Michael Stegen / Stegen Electronics ; (C) 2011-2017 Mark Webb-Johnson ; (C) 2011 Sonny Chen @ EPRO/DX ; ; Permission is hereby granted, free of charge, to any person obtaining a copy ; of this software and associated documentation files (the "Software"), to deal ; in the Software without restriction, including without limitation the rights ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ; copies of the Software, and to permit persons to whom the Software is ; furnished to do so, subject to the following conditions: ; ; The above copyright notice and this permission notice shall be included in ; all copies or substantial portions of the Software. ; ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ; THE SOFTWARE. */ #include "ovms_log.h" static const char *TAG = "ovms-duk-vfs"; #include #include #include #include #include #include #include #include "ovms_malloc.h" #include "ovms_module.h" #include "ovms_duktape.h" #include "ovms_config.h" #include "ovms_command.h" #include "ovms_events.h" #include "console_async.h" #include "buffered_shell.h" #include "ovms_netmanager.h" #include "ovms_tls.h" #include "ovms_boot.h" #include "ovms_peripherals.h" //////////////////////////////////////////////////////////////////////////////// // DuktapeVFSLoad: load a file asynchronously // // This is following a high-level approach, limitation is the file contents needs to fit in RAM // twice, as the read buffer is converted into a JS string/buffer. // Partial loading / block operations may be added later as needed, e.g. based on arguments. // // Notes: // - Conditional synchronous operation doesn't make sense; async overhead is minimal // and the stat() on "/sd" already needs at least 45 ms. // - On /store a Load() takes ~ 25 ms base + 5 ms per KB. // - On /sd a Load() takes ~ 60 ms base + 30 ms per KB. // // Javascript API: // var request = VFS.Load({ // path: "…", // [binary: false,] // done: function(data) {}, // data: string|u8buf // fail: function(error) {}, // always: function() {}, // }); class DuktapeVFSLoad : public DuktapeObject { public: DuktapeVFSLoad(duk_context *ctx, int obj_idx); ~DuktapeVFSLoad() { /*ESP_LOGD(TAG, "~DuktapeVFSLoad");*/ } static duk_ret_t Create(duk_context *ctx); public: duk_ret_t CallMethod(duk_context *ctx, const char* method, void* data=NULL); protected: static void LoadTask(void *param); void Load(); protected: std::ifstream m_file; std::string m_path; struct stat m_stat; extram::string m_data; std::string m_error; bool m_binary = false; }; duk_ret_t DuktapeVFSLoad::Create(duk_context *ctx) { // var request = VFS.Load({ args }) DuktapeVFSLoad *request = new DuktapeVFSLoad(ctx, 0); request->Push(ctx); return 1; } DuktapeVFSLoad::DuktapeVFSLoad(duk_context *ctx, int obj_idx) : DuktapeObject(ctx, obj_idx) { // get args: duk_require_stack(ctx, 5); if (duk_get_prop_string(ctx, 0, "path")) m_path = duk_to_string(ctx, -1); duk_pop(ctx); if (duk_get_prop_string(ctx, 0, "binary")) m_binary = duk_to_boolean(ctx, -1); duk_pop(ctx); // check path: if (m_path.empty()) { m_error = "no path"; CallMethod(ctx, "fail"); return; } if (MyConfig.ProtectedPath(m_path)) { m_error = "protected path"; CallMethod(ctx, "fail"); return; } #ifdef CONFIG_OVMS_COMP_SDCARD // verify volume: if (startsWith(m_path, "/sd/") && (!MyPeripherals->m_sdcard || !MyPeripherals->m_sdcard->isavailable())) { m_error = "volume not mounted"; CallMethod(ctx, "fail"); return; } #endif // CONFIG_OVMS_COMP_SDCARD // start loader: Ref(); Register(ctx); TaskHandle_t task = NULL; if (xTaskCreatePinnedToCore(LoadTask, "DuktapeVFSLoad", 5*512, this, CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE_PRIORITY-1, &task, CORE(1)) != pdPASS) { Deregister(ctx); Unref(); m_error = "cannot start task"; CallMethod(ctx, "fail"); return; } AddTaskToMap(task); //ESP_LOGD(TAG, "DuktapeVFSLoad('%s'): started", m_path.c_str()); } void DuktapeVFSLoad::LoadTask(void *param) { // encapsulate local variables, as vTaskDelete() won't return: { DuktapeVFSLoad *me = (DuktapeVFSLoad*)param; me->Load(); me->Unref(); } vTaskDelete(NULL); } void DuktapeVFSLoad::Load() { // check file & size: if (stat(m_path.c_str(), &m_stat) != 0) { m_error = "file not found"; RequestCallback("fail"); return; } if (S_ISDIR(m_stat.st_mode)) { m_error = "not a file"; RequestCallback("fail"); return; } if (m_stat.st_size > heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)) { m_error = "file too large"; RequestCallback("fail"); return; } if (m_stat.st_size == 0) { m_error = ""; RequestCallback("done"); return; } // load data: m_file.open(m_path, std::ios::in | std::ios::binary); if (m_file.fail()) { m_error = "cannot open file"; RequestCallback("fail"); return; } m_data.resize(m_stat.st_size, '\0'); m_file.read(&m_data[0], m_stat.st_size); bool fail = m_file.fail(); m_file.close(); if (fail) { m_error = "read error"; RequestCallback("fail"); } else { m_error = ""; RequestCallback("done"); } } duk_ret_t DuktapeVFSLoad::CallMethod(duk_context *ctx, const char* method, void* data /*=NULL*/) { if (!ctx) { RequestCallback(method, data); return 0; } OvmsRecMutexLock lock(&m_mutex); if (!IsCoupled()) return 0; duk_require_stack(ctx, 3); int entry_top = duk_get_top(ctx); bool deregister = false; while (method) { const char* followup_method = NULL; // check method: int obj_idx = Push(ctx); duk_get_prop_string(ctx, obj_idx, method); bool callable = duk_is_callable(ctx, -1); duk_pop(ctx); if (callable) duk_push_string(ctx, method); int nargs = 0; // create results & method arguments: if (strcmp(method, "done") == 0) { // done(data): followup_method = "always"; //ESP_LOGD(TAG, "DuktapeVFSLoad('%s'): done size=%d", m_path.c_str(), m_data.size()); // clear request.error: duk_push_string(ctx, ""); duk_put_prop_string(ctx, obj_idx, "error"); // set data: if (m_binary) { void* p = duk_push_fixed_buffer(ctx, m_data.size()); memcpy(p, m_data.data(), m_data.size()); duk_dup(ctx, -1); duk_put_prop_string(ctx, obj_idx, "data"); } else { duk_push_lstring(ctx, m_data.data(), m_data.size()); duk_dup(ctx, -1); duk_put_prop_string(ctx, obj_idx, "data"); } m_data.clear(); m_data.shrink_to_fit(); nargs++; } else if (strcmp(method, "fail") == 0) { // fail(error): followup_method = "always"; //ESP_LOGD(TAG, "DuktapeVFSLoad('%s'): failed error='%s'", m_path.c_str(), m_error.c_str()); // set request.error: duk_push_string(ctx, m_error.c_str()); duk_dup(ctx, -1); duk_put_prop_string(ctx, obj_idx, "error"); nargs++; } else if (strcmp(method, "always") == 0) { // always(): deregister = true; } // call method: if (callable) { //ESP_LOGD(TAG, "DuktapeVFSLoad: calling method '%s' nargs=%d", method, nargs); if (duk_pcall_prop(ctx, obj_idx, nargs) != 0) { DukOvmsErrorHandler(ctx, -1); } } // clear stack: duk_pop_n(ctx, duk_get_top(ctx) - entry_top); // followup call: method = followup_method; } // while (method) // allow GC: if (deregister) Deregister(ctx); return 0; } //////////////////////////////////////////////////////////////////////////////// // DuktapeVFSSave: save a file asynchronously // // This is following a high-level approach, limitation is the file contents needs to fit in RAM // twice, as the JS buffer is copied for the saver. // Partial saving / block operations may be added later as needed, e.g. based on arguments. // // Javascript API: // var request = VFS.Save({ // data: string|u8buf // path: "…", // missing directories will be created automatically // [append: false,] // done: function() {}, // fail: function(error) {}, // always: function() {}, // }); class DuktapeVFSSave : public DuktapeObject { public: DuktapeVFSSave(duk_context *ctx, int obj_idx); ~DuktapeVFSSave() { /*ESP_LOGD(TAG, "~DuktapeVFSSave");*/ } static duk_ret_t Create(duk_context *ctx); public: duk_ret_t CallMethod(duk_context *ctx, const char* method, void* data=NULL); protected: static void SaveTask(void *param); void Save(); protected: std::ofstream m_file; std::string m_path; extram::string m_data; std::string m_error; bool m_append = false; }; duk_ret_t DuktapeVFSSave::Create(duk_context *ctx) { // var request = VFS.Save({ args }) DuktapeVFSSave *request = new DuktapeVFSSave(ctx, 0); request->Push(ctx); return 1; } DuktapeVFSSave::DuktapeVFSSave(duk_context *ctx, int obj_idx) : DuktapeObject(ctx, obj_idx) { // inhibit file I/O when system is about to reboot: if (MyBoot.IsShuttingDown()) { m_error = "shutting down"; CallMethod(ctx, "fail"); return; } // get args: duk_require_stack(ctx, 5); if (duk_get_prop_string(ctx, 0, "path")) m_path = duk_to_string(ctx, -1); duk_pop(ctx); if (duk_get_prop_string(ctx, 0, "append")) m_append = duk_to_boolean(ctx, -1); duk_pop(ctx); if (duk_get_prop_string(ctx, 0, "data")) { if (duk_is_buffer_data(ctx, -1)) { size_t size; void *data = duk_get_buffer_data(ctx, -1, &size); m_data.resize(size, '\0'); memcpy(&m_data[0], data, size); } else { m_data = duk_to_string(ctx, -1); } duk_pop(ctx); } else { duk_pop(ctx); m_error = "no data"; CallMethod(ctx, "fail"); return; } // check path: if (m_path.empty()) { m_error = "no path"; CallMethod(ctx, "fail"); return; } if (MyConfig.ProtectedPath(m_path)) { m_error = "protected path"; CallMethod(ctx, "fail"); return; } #ifdef CONFIG_OVMS_COMP_SDCARD // verify volume: if (startsWith(m_path, "/sd/") && (!MyPeripherals->m_sdcard || !MyPeripherals->m_sdcard->isavailable())) { m_error = "volume not mounted"; CallMethod(ctx, "fail"); return; } #endif // CONFIG_OVMS_COMP_SDCARD // start saver: Ref(); Register(ctx); TaskHandle_t task = NULL; if (xTaskCreatePinnedToCore(SaveTask, "DuktapeVFSSave", 5*512, this, CONFIG_OVMS_SC_JAVASCRIPT_DUKTAPE_PRIORITY-1, &task, CORE(1)) != pdPASS) { Deregister(ctx); Unref(); m_error = "cannot start task"; CallMethod(ctx, "fail"); return; } AddTaskToMap(task); //ESP_LOGD(TAG, "DuktapeVFSSave('%s'): started", m_path.c_str()); } void DuktapeVFSSave::SaveTask(void *param) { // encapsulate local variables, as vTaskDelete() won't return: { DuktapeVFSSave *me = (DuktapeVFSSave*)param; // listen for system shutdown: std::string tag; bool shuttingdown = false; tag = idtag("DuktapeVFSSave", me); MyEvents.RegisterEvent(tag, "system.shuttingdown", [&](std::string event, void* data) { MyBoot.ShutdownPending(tag.c_str()); shuttingdown = true; }); me->Save(); me->Unref(); MyEvents.DeregisterEvent(tag); if (shuttingdown) MyBoot.ShutdownReady(tag.c_str()); } vTaskDelete(NULL); } void DuktapeVFSSave::Save() { // create path: size_t n = m_path.rfind('/'); if (n != 0 && n != std::string::npos) { std::string dir = m_path.substr(0, n); if (!path_exists(dir) && mkpath(dir) != 0) { m_error = "cannot create path"; RequestCallback("fail"); return; } } // save data: if (m_append) m_file.open(m_path, std::ios::out | std::ios::binary | std::ios::app); else m_file.open(m_path, std::ios::out | std::ios::binary | std::ios::trunc); if (m_file.fail()) { m_error = "cannot open file"; RequestCallback("fail"); return; } m_file.write(&m_data[0], m_data.size()); bool wfail = m_file.fail(); m_file.close(); // free buffer: m_data.clear(); m_data.shrink_to_fit(); if (wfail || m_file.fail()) { m_error = "write error"; RequestCallback("fail"); } else { m_error = ""; RequestCallback("done"); } } duk_ret_t DuktapeVFSSave::CallMethod(duk_context *ctx, const char* method, void* data /*=NULL*/) { if (!ctx) { RequestCallback(method, data); return 0; } OvmsRecMutexLock lock(&m_mutex); if (!IsCoupled()) return 0; duk_require_stack(ctx, 3); int entry_top = duk_get_top(ctx); bool deregister = false; while (method) { const char* followup_method = NULL; // check method: int obj_idx = Push(ctx); duk_get_prop_string(ctx, obj_idx, method); bool callable = duk_is_callable(ctx, -1); duk_pop(ctx); if (callable) duk_push_string(ctx, method); int nargs = 0; // create results & method arguments: if (strcmp(method, "done") == 0) { // done(): followup_method = "always"; //ESP_LOGD(TAG, "DuktapeVFSSave('%s'): done", m_path.c_str()); // clear request.error: duk_push_string(ctx, ""); duk_put_prop_string(ctx, obj_idx, "error"); } else if (strcmp(method, "fail") == 0) { // fail(error): followup_method = "always"; //ESP_LOGD(TAG, "DuktapeVFSSave('%s'): failed error='%s'", m_path.c_str(), m_error.c_str()); // set request.error: duk_push_string(ctx, m_error.c_str()); duk_dup(ctx, -1); duk_put_prop_string(ctx, obj_idx, "error"); nargs++; } else if (strcmp(method, "always") == 0) { // always(): deregister = true; } // call method: if (callable) { //ESP_LOGD(TAG, "DuktapeVFSSave: calling method '%s' nargs=%d", method, nargs); if (duk_pcall_prop(ctx, obj_idx, nargs) != 0) { DukOvmsErrorHandler(ctx, -1); } } // clear stack: duk_pop_n(ctx, duk_get_top(ctx) - entry_top); // followup call: method = followup_method; } // while (method) // allow GC: if (deregister) Deregister(ctx); return 0; } //////////////////////////////////////////////////////////////////////////////// // DuktapeVFSInit registration class DuktapeVFSInit { public: DuktapeVFSInit(); } MyDuktapeVFSInit __attribute__ ((init_priority (1700))); DuktapeVFSInit::DuktapeVFSInit() { ESP_LOGI(TAG, "Installing DUKTAPE VFS (1710)"); DuktapeObjectRegistration* dt_vfs = new DuktapeObjectRegistration("VFS"); dt_vfs->RegisterDuktapeFunction(DuktapeVFSLoad::Create, 1, "Load"); dt_vfs->RegisterDuktapeFunction(DuktapeVFSSave::Create, 1, "Save"); MyDuktape.RegisterDuktapeObject(dt_vfs); }