runtime.uc 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
  2. // Licensed to the public under the Apache License 2.0.
  3. import { access, basename } from 'fs';
  4. import { cursor } from 'uci';
  5. const template_directory = '/usr/share/ucode/luci/template';
  6. function cut_message(msg) {
  7. return trim(replace(msg, /\n--\n.*$/, ''));
  8. }
  9. function format_nested_exception(ex) {
  10. let msg = replace(cut_message(ex.message), /(\n+( \|[^\n]*(\n|$))+)/, (m, m1) => {
  11. m1 = replace(m1, /(^|\n) \| ?/g, '$1');
  12. m = match(m1, /^(.+?)\n(In.*line \d+, byte \d+:.+)$/);
  13. return `
  14. <div class="exception">
  15. <div class="message">${cut_message(m ? m[1] : m1)}</div>
  16. ${m ? `<pre class="context">${trim(m[2])}</pre>` : ''}
  17. </div>
  18. `;
  19. });
  20. return `
  21. <div class="exception">
  22. <div class="message">${cut_message(msg)}</div>
  23. <pre class="context">${trim(ex.stacktrace[0].context)}</pre>
  24. </div>
  25. `;
  26. }
  27. function format_lua_exception(ex) {
  28. let m = match(ex.message, /^(.+)\nstack traceback:\n(.+)$/);
  29. return `
  30. <div class="exception">
  31. <div class="message">${cut_message(m ? m[1] : ex.message)}</div>
  32. <pre class="context">${m ? trim(replace(m[2], /(^|\n)\t/g, '$1')) : ex.stacktrace[0].context}</pre>
  33. </div>
  34. `;
  35. }
  36. const Class = {
  37. init_lua: function(optional) {
  38. if (!this.L) {
  39. let bridge = this.env.dispatcher.load_luabridge(optional);
  40. if (bridge) {
  41. let http = this.env.http;
  42. this.L = bridge.create();
  43. this.L.set('L', proto({ write: (...args) => http.closed || print(...args) }, this.env));
  44. this.L.invoke('require', 'luci.ucodebridge');
  45. this.env.lua_active = true;
  46. }
  47. }
  48. return this.L;
  49. },
  50. is_ucode_template: function(path) {
  51. return access(`${template_directory}/${path}.ut`);
  52. },
  53. is_lua_template: function(path) {
  54. let vm = this.init_lua(true);
  55. return vm && access(`${vm.get('_G', 'luci', 'template', 'viewdir')}/${path}.htm`);
  56. },
  57. render_ucode: function(path, scope) {
  58. let tmplfunc = loadfile(path, { raw_mode: false });
  59. if (this.env.http.closed)
  60. render(call, tmplfunc, null, scope ?? {});
  61. else
  62. call(tmplfunc, null, scope ?? {});
  63. },
  64. render_lua: function(path, scope) {
  65. let vm = this.init_lua();
  66. let render = vm.get('_G', 'luci', 'ucodebridge', 'render');
  67. render.call(path, scope ?? {});
  68. },
  69. trycompile: function(path) {
  70. let ucode_path = `${template_directory}/${path}.ut`;
  71. if (access(ucode_path)) {
  72. try {
  73. loadfile(ucode_path, { raw_mode: false });
  74. }
  75. catch (ucode_err) {
  76. return `Unable to compile '${path}' as ucode template: ${format_nested_exception(ucode_err)}`;
  77. }
  78. }
  79. else {
  80. try {
  81. let vm = this.init_lua(true);
  82. if (vm)
  83. vm.get('_G', 'luci', 'ucodebridge', 'compile').call(path);
  84. else
  85. return `Unable to compile '${path}' as Lua template: Unable to load Lua runtime`;
  86. }
  87. catch (lua_err) {
  88. return `Unable to compile '${path}' as Lua template: ${format_lua_exception(lua_err)}`;
  89. }
  90. }
  91. return true;
  92. },
  93. render_any: function(path, scope) {
  94. let ucode_path = `${template_directory}/${path}.ut`;
  95. scope = proto(scope ?? {}, this.scopes[-1]);
  96. push(this.scopes, scope);
  97. try {
  98. if (access(ucode_path))
  99. this.render_ucode(ucode_path, scope);
  100. else
  101. this.render_lua(path, scope);
  102. }
  103. catch (ex) {
  104. pop(this.scopes);
  105. die(ex);
  106. }
  107. pop(this.scopes);
  108. },
  109. render: function(path, scope) {
  110. let self = this;
  111. this.env.http.write(render(() => self.render_any(path, scope)));
  112. },
  113. call: function(modname, method, ...args) {
  114. let vm = this.init_lua();
  115. let lcall = vm.get('_G', 'luci', 'ucodebridge', 'call');
  116. return lcall.call(modname, method, ...args);
  117. }
  118. };
  119. export default function(env) {
  120. const self = proto({ env: env ??= {}, scopes: [ proto(env, global) ], global }, Class);
  121. const uci = cursor();
  122. // determine theme
  123. let media = uci.get('luci', 'main', 'mediaurlbase');
  124. let status = self.trycompile(`themes/${basename(media)}/header`);
  125. if (status !== true) {
  126. media = null;
  127. self.env.media_error = status;
  128. for (let k, v in uci.get_all('luci', 'themes')) {
  129. if (substr(k, 0, 1) != '.') {
  130. status = self.trycompile(`themes/${basename(v)}/header`);
  131. if (status === true) {
  132. media = v;
  133. break;
  134. }
  135. }
  136. }
  137. if (!media)
  138. env.dispatcher.error500(`Unable to render any theme header template, last error was:\n${status}`);
  139. }
  140. self.env.media = media;
  141. self.env.theme = basename(media);
  142. self.env.resource = uci.get('luci', 'main', 'resourcebase');
  143. self.env.include = (...args) => self.render_any(...args);
  144. return self;
  145. };