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  }