github.com/e154/smart-home@v0.17.2-0.20240311175135-e530a6e5cd45/system/scripts/javascript.go (about) 1 // This file is part of the Smart Home 2 // Program complex distribution https://github.com/e154/smart-home 3 // Copyright (C) 2016-2023, Filippov Alex 4 // 5 // This library is free software: you can redistribute it and/or 6 // modify it under the terms of the GNU Lesser General Public 7 // License as published by the Free Software Foundation; either 8 // version 3 of the License, or (at your option) any later version. 9 // 10 // This library is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 // Library General Public License for more details. 14 // 15 // You should have received a copy of the GNU Lesser General Public 16 // License along with this library. If not, see 17 // <https://www.gnu.org/licenses/>. 18 19 package scripts 20 21 import ( 22 "embed" 23 "fmt" 24 "strings" 25 "sync" 26 27 "github.com/pkg/errors" 28 "runtime/debug" 29 30 "github.com/dop251/goja" 31 . "github.com/e154/smart-home/common" 32 "github.com/e154/smart-home/common/apperr" 33 "github.com/e154/smart-home/system/scripts/eventloop" 34 ) 35 36 //go:embed typescript.js 37 //go:embed coffeescript.js 38 var scriptsAsset embed.FS 39 40 // Javascript ... 41 type Javascript struct { 42 engine *Engine 43 compiler string 44 vm *goja.Runtime 45 loop *eventloop.EventLoop 46 program *goja.Program 47 lockPrograms sync.Mutex 48 programs map[string]*goja.Program 49 } 50 51 // NewJavascript ... 52 func NewJavascript(engine *Engine) *Javascript { 53 return &Javascript{ 54 engine: engine, 55 56 programs: make(map[string]*goja.Program), 57 } 58 } 59 60 // Init ... 61 func (j *Javascript) Init() (err error) { 62 63 j.vm = goja.New() 64 j.vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) 65 //j.vm.SetFieldNameMapper(goja.UncapFieldNameMapper()) 66 j.loop = eventloop.NewEventLoop(j.vm) 67 68 j.bind() 69 70 if j.engine.model.Compiled == "" { 71 return 72 } 73 74 if j.program, err = goja.Compile("", j.engine.model.Compiled, false); err != nil { 75 log.Error(err.Error()) 76 } 77 78 return 79 } 80 81 // Compile ... 82 func (j *Javascript) Compile() (err error) { 83 84 if err = j.GetCompiler(); err != nil { 85 return 86 } 87 88 switch j.engine.model.Lang { 89 case ScriptLangTs: 90 var result goja.Value 91 result, err = j.tsCompile() 92 if err != nil { 93 return 94 } 95 96 j.engine.model.Compiled = result.String() 97 98 case ScriptLangCoffee: 99 var result goja.Value 100 result, err = j.coffeeCompile() 101 if err != nil { 102 return 103 } 104 105 j.engine.model.Compiled = result.String() 106 107 case ScriptLangJavascript: 108 j.engine.model.Compiled = j.engine.model.Source 109 110 } 111 112 j.program, err = goja.Compile("", j.engine.model.Compiled, false) 113 114 return 115 } 116 117 // GetCompiler ... 118 func (j *Javascript) GetCompiler() error { 119 120 switch j.engine.model.Lang { 121 case ScriptLangTs: 122 data, err := scriptsAsset.ReadFile("typescript.js") 123 if err != nil { 124 log.Error(err.Error()) 125 } 126 j.compiler = string(data) 127 128 case ScriptLangCoffee: 129 data, err := scriptsAsset.ReadFile("coffeescript.js") 130 if err != nil { 131 log.Error(err.Error()) 132 return err 133 } 134 j.compiler = string(data) 135 136 default: 137 138 } 139 140 return nil 141 } 142 143 func (j *Javascript) tsCompile() (result goja.Value, err error) { 144 145 if _, err = j.EvalString(j.compiler); err != nil { 146 return 147 } 148 149 // prepare script to inline 150 doc := strings.Join(strings.Split(j.engine.model.Source, "\n"), `\n`) 151 doc = strings.Replace(doc, `"`, `\"`, -1) 152 153 var SRC = fmt.Sprintf(` 154 function compileTypeScriptString(tsCode) { 155 const compilerOptions = { 156 target: 'es6', 157 newLine: 1, 158 module: ts.ModuleKind.CommonJS, 159 }; 160 161 const result = ts.transpileModule(tsCode, { compilerOptions }); 162 return result.outputText; 163 } 164 165 try { 166 compileTypeScriptString("%s"); 167 } catch (error) { 168 console.error(error.message); 169 } 170 `, doc) 171 172 // compile from typescript to native script 173 var program *goja.Program 174 if program, err = goja.Compile("", SRC, true); err != nil { 175 return 176 } 177 178 result, err = j.vm.RunProgram(program) 179 180 return 181 } 182 183 func (j *Javascript) coffeeCompile() (result goja.Value, err error) { 184 185 if _, err = j.EvalString(j.compiler); err != nil { 186 return 187 } 188 189 // prepare script to inline 190 doc := strings.Join(strings.Split(j.engine.model.Source, "\n"), `\n`) 191 doc = strings.Replace(doc, `"`, `\"`, -1) 192 193 var SRC = fmt.Sprintf(`CoffeeScript.compile("%s", {"bare":true})`, doc) 194 195 // compile from coffee to native script 196 var program *goja.Program 197 if program, err = goja.Compile("", SRC, true); err != nil { 198 return 199 } 200 201 result, err = j.vm.RunProgram(program) 202 203 return 204 } 205 206 // Do ... 207 func (j *Javascript) Do() (result string, err error) { 208 result, err = j.unsafeRun(j.program) 209 if err != nil { 210 if j.engine.ScriptId() != 0 { 211 err = errors.Wrapf(err, "script id:%d ", j.engine.ScriptId()) 212 } 213 } 214 return 215 } 216 217 // AssertFunction ... 218 func (j *Javascript) AssertFunction(f string, args ...interface{}) (result string, err error) { 219 defer func() { 220 if r := recover(); r != nil { 221 if j.engine.model != nil { 222 log.Warnf("Recovered script id: %d, %s", j.engine.model.Id, f) 223 } else { 224 log.Warnf("Recovered script %s", f) 225 } 226 //log.Debug(j.vm.Get(f).String()) 227 debug.PrintStack() 228 } 229 }() 230 if assertFunc, ok := goja.AssertFunction(j.vm.Get(f)); ok { 231 var value goja.Value 232 var gojaArgs []goja.Value 233 for _, arg := range args { 234 gojaArgs = append(gojaArgs, j.vm.ToValue(arg)) 235 } 236 if value, err = assertFunc(goja.Undefined(), gojaArgs...); err != nil { 237 return 238 } 239 result = value.String() 240 } 241 return 242 } 243 244 // PushStruct ... 245 func (j *Javascript) PushStruct(name string, s interface{}) { 246 _ = j.vm.Set(name, s) 247 } 248 249 // PushFunction ... 250 func (j *Javascript) PushFunction(name string, s interface{}) { 251 _ = j.vm.Set(name, s) 252 } 253 254 // EvalString ... 255 func (j *Javascript) EvalString(src string) (result string, err error) { 256 257 var program *goja.Program 258 if program, err = goja.Compile("", src, false); err != nil { 259 return 260 } 261 262 result, err = j.unsafeRun(program) 263 264 return 265 } 266 267 func (j *Javascript) bind() { 268 269 // 270 // print() 271 // console() 272 // hex2arr() 273 // marshal(obj) 274 // unmarshal(json) 275 // 276 277 _ = j.vm.Set("print", log.Info) 278 279 _, _ = j.vm.RunString(` 280 281 console = {log:print,warn:print,error:print,info:print}, 282 hex2arr = function (hexString) { 283 var result = []; 284 while (hexString.length >= 2) { 285 result.push(parseInt(hexString.substring(0, 2), 16)); 286 hexString = hexString.substring(2, hexString.length); 287 } 288 return result; 289 }; 290 unmarshal = function(j) { return JSON.parse(j); } 291 marshal = function(obj) { return JSON.stringify(obj); } 292 `) 293 294 j.engine.functions.Range(func(key, value interface{}) bool { 295 _ = j.vm.Set(key.(string), value) 296 return true 297 }) 298 299 j.engine.structures.Range(func(key, value interface{}) bool { 300 _ = j.vm.Set(key.(string), value) 301 return true 302 }) 303 } 304 305 // CreateProgram ... 306 func (j *Javascript) CreateProgram(name, source string) (err error) { 307 j.lockPrograms.Lock() 308 j.programs[name], err = goja.Compile("", source, false) 309 j.lockPrograms.Unlock() 310 return 311 } 312 313 // RunProgram ... 314 func (j *Javascript) RunProgram(name string) (result string, err error) { 315 j.lockPrograms.Lock() 316 defer j.lockPrograms.Unlock() 317 318 program, ok := j.programs[name] 319 if !ok { 320 err = errors.Wrap(apperr.ErrNotFound, fmt.Sprintf("name \"%s\"", name)) 321 return 322 } 323 324 result, err = j.unsafeRun(program) 325 326 return 327 } 328 329 func (j *Javascript) unsafeRun(program *goja.Program) (result string, err error) { 330 331 if program == nil { 332 return 333 } 334 335 defer func() { 336 if r := recover(); r != nil { 337 log.Warn("Recovered script: ", j.engine.model.Id) 338 debug.PrintStack() 339 } 340 }() 341 342 var value goja.Value 343 344 wg := sync.WaitGroup{} 345 wg.Add(1) 346 347 j.loop.Run(func(vm *goja.Runtime) { 348 defer wg.Done() 349 value, err = vm.RunProgram(program) 350 }) 351 352 wg.Wait() 353 354 if err != nil { 355 err = errors.Wrap(err, "unsafeRun") 356 return 357 } 358 359 result = value.String() 360 361 return 362 }