#include "InterpreterV8.h" #ifdef HAVE_V8 #include #include #include #include #ifdef HAVE_NOVA #include #include #include #endif #ifdef HAVE_SQL #include "Database.h" #endif #include #include "dim.h" #include "tools.h" #include "externals/izstream.h" using namespace std; using namespace v8; v8::Handle InterpreterV8::fTemplateLocal; v8::Handle InterpreterV8::fTemplateSky; v8::Handle InterpreterV8::fTemplateEvent; //v8::Handle InterpreterV8::fTemplateDatabase; // ========================================================================== // Some documentation // ========================================================================== // // Threads: // -------- // In most cases Js* and other calls to native C++ code could be wrapped // with an Unlocker to allow possible other JavaScipt 'threads' to run // during that time. However, all of these calls should take much less than // the preemption time of 10ms, so it would just be a waste of tim. // // Termination: // ------------ // Each thread running V8 code needs to be signalled individually for // termination. Therefor a list of V8 thread ids is created. // // If termination has already be signalled, no thread should start running // anymore (thy could, e.g., wait for their locking). So after locking // it has to be checked if the thread was terminated already. Note // that all calls to Terminate() must be locked to ensure that fThreadId // is correct when it is checked. // // The current thread id must be added to fThreadIds _before_ any // function is called after Locking and before execution is given // back to JavaScript, e.g. in script->Run(). So until the thread // is added to the list Terminate will not be executed. If Terminate // is then executed, it is ensured that the current thread is // already in the list. If terminate has been called before // the Locking, the check for the validiy of fThreadId ensures that // nothing is executed. // // Empty handles: // -------------- // If exceution is terminated, V8 calls might return with empty handles, // e.g. Date::New(). Therefore, the returned handles of these calls have to // be checked in all placed to avoid that V8 will core dump. // // HandleScope: // ------------ // A handle scope is a garbage collector and collects all handles created // until it goes out of scope. Handles which are not needed anymore are // then deleted. To return a handle from a HandleScope you need to use // Close(). E.g., String::AsciiValue does not create a new handle and // hence does not need a HandleScope. Any ::New will need a handle scope. // Forgetting the HandleScope could in principle fill your memory, // but everything is properly deleted by the global HandleScope at // script termination. // // ========================================================================== // Simple interface // ========================================================================== Handle InterpreterV8::FuncExit(const Arguments &) { V8::TerminateExecution(fThreadId); // we have to throw an excption to make sure that the // calling thread does not go on executing until it // has realized that it should terminate return ThrowException(Null()); } Handle InterpreterV8::FuncSleep(const Arguments& args) { if (args.Length()==0) { // Theoretically, the CPU usage can be reduced by maybe a factor // of four using a larger value, but this also means that the // JavaScript is locked for a longer time. const Unlocker unlock; usleep(1000); return Undefined(); } if (args.Length()!=1) return ThrowException(String::New("Number of arguments must be exactly 1.")); if (!args[0]->IsUint32()) return ThrowException(String::New("Argument 1 must be an uint32.")); // Using a Javascript function has the advantage that it is fully // interruptable without the need of C++ code const string code = "(function(){" "var t=new Date();" "while ((new Date()-t)<"+to_string(args[0]->Int32Value())+") v8.sleep();" "})();"; return ExecuteInternal(code); } void InterpreterV8::Thread(int &id, Persistent func, uint32_t ms) { const Locker lock; if (fThreadId<0) { id = -1; return; } id = V8::GetCurrentThreadId(); fThreadIds.insert(id); const HandleScope handle_scope; func->CreationContext()->Enter(); TryCatch exception; const bool rc = ms==0 || !ExecuteInternal("v8.sleep("+to_string(ms)+");").IsEmpty(); if (rc) func->Call(func, 0, NULL); func.Dispose(); fThreadIds.erase(id); if (!HandleException(exception, "thread")) V8::TerminateExecution(fThreadId); } Handle InterpreterV8::FuncThread(const Arguments& args) { if (!args.IsConstructCall()) return ThrowException(String::New("Thread must be called as constructor.")); if (args.Length()!=2) return ThrowException(String::New("Number of arguments must be two.")); if (!args[0]->IsUint32()) return ThrowException(String::New("Argument 0 not an uint32.")); if (!args[1]->IsFunction()) return ThrowException(String::New("Argument 1 not a function.")); //if (!args.IsConstructCall()) // return Constructor(args); const HandleScope handle_scope; Handle handle = Handle::Cast(args[1]); Persistent func = Persistent::New(handle); const uint32_t ms = args[0]->Uint32Value(); int id=-2; fThreads.push_back(thread(bind(&InterpreterV8::Thread, this, ref(id), func, ms))); { // Allow the thread to lock, so we can get the thread id. const Unlocker unlock; while (id==-2) usleep(1); } Handle self = args.This(); self->Set(String::New("id"), Integer::NewFromUnsigned(id), ReadOnly); self->Set(String::New("kill"), FunctionTemplate::New(WrapKill)->GetFunction(), ReadOnly); return Undefined(); } Handle InterpreterV8::FuncKill(const Arguments& args) { const uint32_t id = args.This()->Get(String::New("id"))->Uint32Value(); V8::TerminateExecution(id); return Boolean::New(fThreadIds.erase(id)); } Handle InterpreterV8::FuncSend(const Arguments& args) { if (args.Length()==0) return ThrowException(String::New("Number of arguments must be at least 1.")); if (!args[0]->IsString()) return ThrowException(String::New("Argument 1 must be a string.")); const String::AsciiValue str(args[0]); string command = *str; if (command.length()==0) return ThrowException(String::New("Server name empty.")); if (args.Length()==0) { if (command.find_first_of('/')==string::npos) command += "/"; } // Escape all string arguments. All others can be kept as they are. for (int i=1; iIsString()) { boost::replace_all(arg, "\\", "\\\\"); boost::replace_all(arg, "'", "\\'"); boost::replace_all(arg, "\"", "\\\""); } command += " "+arg; } try { return Boolean::New(JsSend(command)); } catch (const runtime_error &e) { return ThrowException(String::New(e.what())); } } // ========================================================================== // State control // ========================================================================== Handle InterpreterV8::FuncWait(const Arguments& args) { if (args.Length()!=2 && args.Length()!=3) return ThrowException(String::New("Number of arguments must be 2 or 3.")); if (!args[0]->IsString()) return ThrowException(String::New("Argument 1 not a string.")); if (!args[1]->IsInt32() && !args[1]->IsString()) return ThrowException(String::New("Argument 2 not an int32 and not a string.")); if (args.Length()==3 && !args[2]->IsInt32()) return ThrowException(String::New("Argument 3 not an int32.")); // Using a Javascript function has the advantage that it is fully // interruptable without the need of C++ code const string index = args[1]->IsInt32() ? "s.index" : "s.name"; const bool timeout = args.Length()==3; const string arg0 = *String::AsciiValue(args[0]); const string state = args[1]->IsString() ? *String::AsciiValue(args[1]) : ""; const string arg1 = args[1]->IsString() ? ("\""+state+"\"") : to_string(args[1]->Int32Value()); if (arg0.find_first_of("\"'")!=string::npos) return ThrowException(String::New("Server name must not contain quotation marks.")); if (args[1]->IsString()) if (state.find_first_of("\"'")!=string::npos) return ThrowException(String::New("State name must not contain quotation marks.")); string code = "(function(name,state,ms)" "{"; if (timeout) code += "var t = new Date();"; code += "while (1)" "{" "var s = dim.state(name);" "if(!s)throw new Error('Waitig for state "+arg1+" of server "+arg0+" failed.');" "if(state=="+index+")return true;"; if (timeout) code += "if((new Date()-t)>Math.abs(ms))break;"; code += "v8.sleep();" "}"; if (timeout) code += "if(ms>0)throw new Error('Waitig for state "+arg1+" of server "+arg0+" timed out.');"; code += "return false;" "})('"+arg0+"',"+arg1; if (timeout) code += "," + to_string(args[2]->Int32Value()); code += ");"; return ExecuteInternal(code); } Handle InterpreterV8::FuncState(const Arguments& args) { if (args.Length()!=1) return ThrowException(String::New("Number of arguments must be exactly 1.")); if (!args[0]->IsString()) return ThrowException(String::New("Argument 1 must be a string.")); // Return state.name/state.index const String::AsciiValue str(args[0]); const State rc = JsState(*str); if (rc.index<=-256) return Undefined(); HandleScope handle_scope; Handle obj = Object::New(); obj->Set(String::New("server"), String::New(*str), ReadOnly); obj->Set(String::New("index"), Integer::New(rc.index), ReadOnly); obj->Set(String::New("name"), String::New(rc.name.c_str()), ReadOnly); const Local date = Date::New(rc.time.JavaDate()); if (rc.index>-256 && !date.IsEmpty()) obj->Set(String::New("time"), date); return handle_scope.Close(obj); } Handle InterpreterV8::FuncNewState(const Arguments& args) { if (args.Length()<1 || args.Length()>3) return ThrowException(String::New("Number of arguments must be 1, 2 or 3.")); if (!args[0]->IsUint32()) return ThrowException(String::New("Argument 1 must be an uint32.")); if (args.Length()>1 && !args[1]->IsString()) return ThrowException(String::New("Argument 2 must be a string.")); if (args.Length()>2 && !args[2]->IsString()) return ThrowException(String::New("Argument 3 must be a string.")); const uint32_t index = args[0]->Int32Value(); const string name = *String::AsciiValue(args[1]); const string comment = *String::AsciiValue(args[2]); if (index<10 || index>255) return ThrowException(String::New("State must be in the range [10, 255].")); if (name.empty()) return ThrowException(String::New("State name must not be empty.")); if (name.find_first_of(':')!=string::npos || name.find_first_of('=')!=string::npos) return ThrowException(String::New("State name must not contain : or =.")); struct Find : State { Find(int idx, const string &n) : State(idx, n) { } bool operator()(const pair &p) { return index==p.first || name==p.second; } }; if (find_if(fStates.begin(), fStates.end(), Find(index, name))!=fStates.end()) { const string what = "State index ["+to_string(index)+"] or name ["+name+"] already defined."; return ThrowException(String::New(what.c_str())); } return Boolean::New(JsNewState(index, name, comment)); } Handle InterpreterV8::FuncSetState(const Arguments& args) { if (args.Length()!=1) return ThrowException(String::New("Number of arguments must be exactly 1.")); if (!args[0]->IsUint32() && !args[0]->IsString()) return ThrowException(String::New("Argument must be an unint32 or a string.")); int index = -2; if (args[0]->IsUint32()) { index = args[0]->Int32Value(); } else { const string name = *String::AsciiValue(args[0]); index = JsGetState(name); if (index==-2) return ThrowException(String::New(("State '"+name+"' not found.").c_str())); } if (index<10 || index>255) return ThrowException(String::New("State must be in the range [10, 255].")); return Boolean::New(JsSetState(index)); } Handle InterpreterV8::FuncGetState(const Arguments& args) { if (args.Length()>0) return ThrowException(String::New("getState must not take arguments.")); const State state = JsGetCurrentState(); HandleScope handle_scope; Handle rc = Object::New(); if (rc.IsEmpty()) return Undefined(); rc->Set(String::New("index"), Integer::New(state.index), ReadOnly); rc->Set(String::New("name"), String::New(state.name.c_str()), ReadOnly); rc->Set(String::New("description"), String::New(state.comment.c_str()), ReadOnly); return handle_scope.Close(rc); } // ========================================================================== // Internal functions // ========================================================================== // The callback that is invoked by v8 whenever the JavaScript 'print' // function is called. Prints its arguments on stdout separated by // spaces and ending with a newline. Handle InterpreterV8::FuncPrint(const Arguments& args) { for (int i=0; i InterpreterV8::FuncAlarm(const Arguments& args) { for (int i=0; i InterpreterV8::FuncOut(const Arguments& args) { for (int i=0; i InterpreterV8::FuncInclude(const Arguments& args) { for (int i=0; i InterpreterV8::FuncFile(const Arguments& args) { if (args.Length()!=1 && args.Length()!=2) return ThrowException(String::New("Number of arguments must be one or two.")); const String::AsciiValue file(args[0]); if (*file == NULL) return ThrowException(String::New("File name missing")); if (args.Length()==2 && !args[1]->IsString()) return ThrowException(String::New("Second argument must be a string.")); const string delim = args.Length()==2 ? *String::AsciiValue(args[1]) : ""; if (args.Length()==2 && delim.size()!=1) return ThrowException(String::New("Second argument must be a string of length 1.")); HandleScope handle_scope; izstream fin(*file); if (!fin) return ThrowException(String::New(errno!=0?strerror(errno):"Insufficient memory for decompression")); if (args.Length()==1) { string buffer; if (!getline(fin, buffer, '\0')) return ThrowException(String::New(strerror(errno))); Handle str = StringObject::New(String::New(buffer.c_str())); StringObject::Cast(*str)->Set(String::New("name"), String::New(*file)); return handle_scope.Close(str); } Handle arr = Array::New(); if (arr.IsEmpty()) return Undefined(); int i=0; string buffer; while (getline(fin, buffer, delim[0])) arr->Set(i++, String::New(buffer.c_str())); if ((fin.fail() && !fin.eof()) || fin.bad()) return ThrowException(String::New(strerror(errno))); arr->Set(String::New("name"), String::New(*file)); arr->Set(String::New("delim"), String::New(delim.c_str(), 1)); return handle_scope.Close(arr); } // ========================================================================== // Database // ========================================================================== Handle InterpreterV8::FuncDbClose(const Arguments &args) { void *ptr = External::Unwrap(args.This()->GetInternalField(0)); if (!ptr) return Boolean::New(false); #ifdef HAVE_SQL Database *db = reinterpret_cast(ptr); auto it = find(fDatabases.begin(), fDatabases.end(), db); fDatabases.erase(it); delete db; #endif HandleScope handle_scope; args.This()->SetInternalField(0, External::New(0)); return handle_scope.Close(Boolean::New(true)); } Handle InterpreterV8::FuncDbQuery(const Arguments &args) { if (args.Length()==0) return ThrowException(String::New("Arguments expected.")); void *ptr = External::Unwrap(args.This()->GetInternalField(0)); if (!ptr) return Undefined(); string query; for (int i=0; i(ptr); const mysqlpp::StoreQueryResult res = db->query(query).store(); Handle ret = Array::New(); if (ret.IsEmpty()) return Undefined(); ret->Set(String::New("table"), String::New(res.table()), ReadOnly); ret->Set(String::New("query"), String::New(query.c_str()), ReadOnly); Handle cols = Array::New(); if (cols.IsEmpty()) return Undefined(); int irow=0; for (vector::const_iterator it=res.begin(); it row = Object::New(); if (row.IsEmpty()) return Undefined(); const mysqlpp::FieldNames *list = it->field_list().list; for (size_t i=0; isize(); i++) { const Handle name = String::New((*list)[i].c_str()); if (irow==0) cols->Set(i, name); if ((*it)[i].is_null()) { row->Set(name, Undefined(), ReadOnly); continue; } const string sql_type = (*it)[i].type().sql_name(); const bool uns = sql_type.find("UNSIGNED")==string::npos; if (sql_type.find("BIGINT")!=string::npos) { if (uns) { const uint64_t val = (uint64_t)(*it)[i]; if (val>UINT32_MAX) row->Set(name, Number::New(val), ReadOnly); else row->Set(name, Integer::NewFromUnsigned(val), ReadOnly); } else { const int64_t val = (int64_t)(*it)[i]; if (valINT32_MAX) row->Set(name, Number::New(val), ReadOnly); else row->Set(name, Integer::NewFromUnsigned(val), ReadOnly); } continue; } // 32 bit if (sql_type.find("INT")!=string::npos) { if (uns) row->Set(name, Integer::NewFromUnsigned((uint32_t)(*it)[i]), ReadOnly); else row->Set(name, Integer::New((int32_t)(*it)[i]), ReadOnly); continue; } if (sql_type.find("BOOL")!=string::npos ) { row->Set(name, Boolean::New((bool)(*it)[i]), ReadOnly); continue; } if (sql_type.find("FLOAT")!=string::npos) { ostringstream val; val << setprecision(7) << (float)(*it)[i]; row->Set(name, Number::New(stod(val.str())), ReadOnly); continue; } if (sql_type.find("DOUBLE")!=string::npos) { row->Set(name, Number::New((double)(*it)[i]), ReadOnly); continue; } if (sql_type.find("CHAR")!=string::npos || sql_type.find("TEXT")!=string::npos) { row->Set(name, String::New((const char*)(*it)[i]), ReadOnly); continue; } time_t date = 0; if (sql_type.find("TIMESTAMP")!=string::npos) date = mysqlpp::Time((*it)[i]); if (sql_type.find("DATETIME")!=string::npos) date = mysqlpp::DateTime((*it)[i]); if (sql_type.find(" DATE ")!=string::npos) date = mysqlpp::Date((*it)[i]); if (date>0) { // It is important to catch the exception thrown // by Date::New in case of thread termination! const Local val = Date::New(date*1000); if (val.IsEmpty()) return Undefined(); row->Set(name, val, ReadOnly); } } ret->Set(irow++, row); } if (irow>0) ret->Set(String::New("cols"), cols, ReadOnly); return handle_scope.Close(ret); } catch (const exception &e) { return ThrowException(String::New(e.what())); } #endif } Handle InterpreterV8::FuncDatabase(const Arguments &args) { if (!args.IsConstructCall()) return ThrowException(String::New("Database must be called as constructor.")); if (args.Length()!=1) return ThrowException(String::New("Number of arguments must be 1.")); if (!args[0]->IsString()) return ThrowException(String::New("Argument 1 not a string.")); #ifdef HAVE_SQL try { HandleScope handle_scope; //if (!args.IsConstructCall()) // return Constructor(fTemplateDatabase, args); Database *db = new Database(*String::AsciiValue(args[0])); fDatabases.push_back(db); Handle self = args.This(); self->Set(String::New("user"), String::New(db->user.c_str()), ReadOnly); self->Set(String::New("server"), String::New(db->server.c_str()), ReadOnly); self->Set(String::New("database"), String::New(db->db.c_str()), ReadOnly); self->Set(String::New("port"), db->port==0?Undefined():Integer::NewFromUnsigned(db->port), ReadOnly); self->Set(String::New("query"), FunctionTemplate::New(WrapDbQuery)->GetFunction(), ReadOnly); self->Set(String::New("close"), FunctionTemplate::New(WrapDbClose)->GetFunction(), ReadOnly); self->SetInternalField(0, External::New(db)); return handle_scope.Close(self); } catch (const exception &e) { return ThrowException(String::New(e.what())); } #endif } // ========================================================================== // Services // ========================================================================== Handle InterpreterV8::Convert(char type, const char* &ptr) { // Dim values are always unsigned per (FACT++) definition switch (type) { case 'F': { // Remove the "imprecision" effect coming from casting a float to // a double and then showing it with double precision ostringstream val; val << setprecision(7) << *reinterpret_cast(ptr); ptr += 4; return Number::New(stod(val.str())); } case 'D': { Handle v=Number::New(*reinterpret_cast(ptr)); ptr+=8; return v; } case 'I': case 'L': { Handle v=Integer::NewFromUnsigned(*reinterpret_cast(ptr)); ptr += 4; return v; } case 'X': { const uint64_t val = *reinterpret_cast(ptr); ptr += 8; if (val>UINT32_MAX) return Number::New(val); return Integer::NewFromUnsigned(val); } case 'S': { Handle v=Integer::NewFromUnsigned(*reinterpret_cast(ptr)); ptr += 2; return v; } case 'C': { Handle v=Integer::NewFromUnsigned((uint16_t)*reinterpret_cast(ptr)); ptr += 1; return v; } } return Undefined(); } Handle InterpreterV8::FuncClose(const Arguments &args) { HandleScope handle_scope; //const void *ptr = Local::Cast(args.Holder()->GetInternalField(0))->Value(); const String::AsciiValue str(args.This()->Get(String::New("name"))); const auto it = fReverseMap.find(*str); if (it!=fReverseMap.end()) { it->second.Dispose(); fReverseMap.erase(it); } args.This()->Set(String::New("isOpen"), Boolean::New(false), ReadOnly); return handle_scope.Close(Boolean::New(JsUnsubscribe(*str))); } Handle InterpreterV8::ConvertEvent(const EventImp *evt, uint64_t counter, const char *str) { const vector vec = JsDescription(str); Handle ret = fTemplateEvent->GetFunction()->NewInstance();//Object::New(); if (ret.IsEmpty()) return Undefined(); const Local date = Date::New(evt->GetJavaDate()); if (date.IsEmpty()) return Undefined(); ret->Set(String::New("name"), String::New(str), ReadOnly); ret->Set(String::New("format"), String::New(evt->GetFormat().c_str()), ReadOnly); ret->Set(String::New("qos"), Integer::New(evt->GetQoS()), ReadOnly); ret->Set(String::New("size"), Integer::New(evt->GetSize()), ReadOnly); ret->Set(String::New("counter"), Integer::New(counter), ReadOnly); if (evt->GetJavaDate()>0) ret->Set(String::New("time"), date, ReadOnly); // If names are available data will also be provided as an // object. If an empty event was received, but names are available, // the object will be empty. Otherwise 'obj' will be undefined. // obj===undefined: no data received // obj!==undefined, length==0: names for event available // obj!==undefined, obj.length>0: names available, data received Handle named = Object::New(); if (vec.size()>0) ret->Set(String::New("obj"), named, ReadOnly); // If no event was received (usually a disconnection event in // the context of FACT++), no data is returned if (evt->IsEmpty()) return ret; // If valid data was received, but the size was zero, then // null is returned as data // data===undefined: no data received // data===null: event received, but no data // data.length>0: event received, contains data if (evt->GetSize()==0 || evt->GetFormat().empty()) { ret->Set(String::New("data"), Null(), ReadOnly); return ret; } typedef boost::char_separator separator; const boost::tokenizer tokenizer(evt->GetFormat(), separator(";:")); const vector tok(tokenizer.begin(), tokenizer.end()); Handle arr = tok.size()>1 ? Array::New() : ret; if (arr.IsEmpty()) return Undefined(); const char *ptr = evt->GetText(); const char *end = evt->GetText()+evt->GetSize(); try { size_t pos = 1; for (auto it=tok.begin(); it0) return Exception::Error(String::New(("Number of received bytes ["+to_string(evt->GetSize())+"] does not match format ["+evt->GetFormat()+"]").c_str())); // Check if format has a number attached. // If no number is attached calculate number of elements const uint32_t cnt = it==tok.end() ? (end-ptr)/sz : stoi(it->c_str()); // is_str: Array of type C but unknown size (String) // is_one: Array of known size, but size is 1 (I:1) const bool is_str = type=='C' && it==tok.end(); const bool is_one = cnt==1 && it!=tok.end(); Handle v; if (is_str) v = String::New(ptr); if (is_one) v = Convert(type, ptr); // Array of known (I:5) or unknown size (I), but no string if (!is_str && !is_one) { Handle a = Array::New(cnt); if (a.IsEmpty()) return Undefined(); for (uint32_t i=0; iSet(i, Convert(type, ptr)); v = a; } if (tok.size()>1) arr->Set(pos-1, v); else ret->Set(String::New("data"), v, ReadOnly); if (!name.empty()) { const Handle n = String::New(name.c_str()); named->Set(n, v); } } if (tok.size()>1) ret->Set(String::New("data"), arr, ReadOnly); return ret; } catch (...) { return Exception::Error(String::New(("Format string conversion '"+evt->GetFormat()+"' failed.").c_str())); } } /* Handle InterpreterV8::FuncGetData(const Arguments &args) { HandleScope handle_scope; const String::AsciiValue str(args.Holder()->Get(String::New("name"))); const pair p = JsGetEvent(*str); const EventImp *evt = p.second; if (!evt) return Undefined(); //if (counter==cnt) // return info.Holder();//Holder()->Get(String::New("data")); Handle ret = ConvertEvent(evt, p.first, *str); return ret->IsNativeError() ? ThrowException(ret) : handle_scope.Close(ret); } */ Handle InterpreterV8::FuncGetData(const Arguments &args) { if (args.Length()>2) return ThrowException(String::New("Number of arguments must not be greater than 2.")); if (args.Length()>=1 && !args[0]->IsInt32() && !args[0]->IsNull()) return ThrowException(String::New("Argument 1 not an uint32.")); if (args.Length()==2 && !args[1]->IsBoolean()) return ThrowException(String::New("Argument 2 not a boolean.")); // Using a Javascript function has the advantage that it is fully // interruptable without the need of C++ code const bool null = args.Length()>=1 && args[0]->IsNull(); const int32_t timeout = args.Length()>=1 ? args[0]->Int32Value() : 0; const bool named = args.Length()<2 || args[1]->BooleanValue(); HandleScope handle_scope; const Handle