WebVM.svelte 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <script>
  2. import { onMount } from 'svelte';
  3. import { get } from 'svelte/store';
  4. import Nav from 'labs/packages/global-navbar/src/Nav.svelte';
  5. import SideBar from '$lib/SideBar.svelte';
  6. import '$lib/global.css';
  7. import '@xterm/xterm/css/xterm.css'
  8. import '@fortawesome/fontawesome-free/css/all.min.css'
  9. import { networkInterface, startLogin } from '$lib/network.js'
  10. import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js'
  11. import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js'
  12. import { displayConfig } from '$lib/anthropic.js'
  13. export let configObj = null;
  14. export let processCallback = null;
  15. export let cacheId = null;
  16. export let cpuActivityEvents = [];
  17. export let diskLatencies = [];
  18. export let activityEventsInterval = 0;
  19. var term = null;
  20. var cx = null;
  21. var fitAddon = null;
  22. var cxReadFunc = null;
  23. var blockCache = null;
  24. var processCount = 0;
  25. var curVT = 0;
  26. function writeData(buf, vt)
  27. {
  28. if(vt != 1)
  29. return;
  30. term.write(new Uint8Array(buf));
  31. }
  32. function readData(str)
  33. {
  34. if(cxReadFunc == null)
  35. return;
  36. for(var i=0;i<str.length;i++)
  37. cxReadFunc(str.charCodeAt(i));
  38. }
  39. function printMessage(msg)
  40. {
  41. for(var i=0;i<msg.length;i++)
  42. term.write(msg[i] + "\n");
  43. }
  44. function expireEvents(list, curTime, limitTime)
  45. {
  46. while(list.length > 1)
  47. {
  48. if(list[1].t < limitTime)
  49. {
  50. list.shift();
  51. }
  52. else
  53. {
  54. break;
  55. }
  56. }
  57. }
  58. function cleanupEvents()
  59. {
  60. var curTime = Date.now();
  61. var limitTime = curTime - 10000;
  62. expireEvents(cpuActivityEvents, curTime, limitTime);
  63. computeCpuActivity(curTime, limitTime);
  64. if(cpuActivityEvents.length == 0)
  65. {
  66. clearInterval(activityEventsInterval);
  67. activityEventsInterval = 0;
  68. }
  69. }
  70. function computeCpuActivity(curTime, limitTime)
  71. {
  72. var totalActiveTime = 0;
  73. var lastActiveTime = limitTime;
  74. var lastWasActive = false;
  75. for(var i=0;i<cpuActivityEvents.length;i++)
  76. {
  77. var e = cpuActivityEvents[i];
  78. // NOTE: The first event could be before the limit,
  79. // we need at least one event to correctly mark
  80. // active time when there is long time under load
  81. var eTime = e.t;
  82. if(eTime < limitTime)
  83. eTime = limitTime;
  84. if(e.state == "ready")
  85. {
  86. // Inactive state, add the time from lastActiveTime
  87. totalActiveTime += (eTime - lastActiveTime);
  88. lastWasActive = false;
  89. }
  90. else
  91. {
  92. // Active state
  93. lastActiveTime = eTime;
  94. lastWasActive = true;
  95. }
  96. }
  97. // Add the last interval if needed
  98. if(lastWasActive)
  99. {
  100. totalActiveTime += (curTime - lastActiveTime);
  101. }
  102. cpuPercentage.set(Math.ceil((totalActiveTime / 10000) * 100));
  103. }
  104. function hddCallback(state)
  105. {
  106. diskActivity.set(state != "ready");
  107. }
  108. function latencyCallback(latency)
  109. {
  110. diskLatencies.push(latency);
  111. if(diskLatencies.length > 30)
  112. diskLatencies.shift();
  113. // Average the latency over at most 30 blocks
  114. var total = 0;
  115. for(var i=0;i<diskLatencies.length;i++)
  116. total += diskLatencies[i];
  117. var avg = total / diskLatencies.length;
  118. diskLatency.set(Math.ceil(avg));
  119. }
  120. function cpuCallback(state)
  121. {
  122. cpuActivity.set(state != "ready");
  123. var curTime = Date.now();
  124. var limitTime = curTime - 10000;
  125. expireEvents(cpuActivityEvents, curTime, limitTime);
  126. cpuActivityEvents.push({t: curTime, state: state});
  127. computeCpuActivity(curTime, limitTime);
  128. // Start an interval timer to cleanup old samples when no further activity is received
  129. if(activityEventsInterval != 0)
  130. clearInterval(activityEventsInterval);
  131. activityEventsInterval = setInterval(cleanupEvents, 2000);
  132. }
  133. function computeXTermFontSize()
  134. {
  135. return parseInt(getComputedStyle(document.body).fontSize);
  136. }
  137. function setScreenSize(display)
  138. {
  139. var mult = 1.0;
  140. var displayWidth = display.offsetWidth;
  141. var displayHeight = display.offsetHeight;
  142. var minWidth = 1024;
  143. var minHeight = 768;
  144. if(displayWidth < minWidth)
  145. mult = minWidth / displayWidth;
  146. if(displayHeight < minHeight)
  147. mult = Math.max(mult, minHeight / displayHeight);
  148. var internalWidth = Math.floor(displayWidth * mult);
  149. var internalHeight = Math.floor(displayHeight * mult);
  150. cx.setKmsCanvas(display, internalWidth, internalHeight);
  151. // Track the state of the mouse as requested by the AI, to avoid losing the position due to user movement
  152. displayConfig.set({width: internalWidth, height: internalHeight, mouseX: 0, mouseY: 0});
  153. }
  154. var curInnerWidth = 0;
  155. var curInnerHeight = 0;
  156. function handleResize()
  157. {
  158. // Avoid spurious resize events caused by the soft keyboard
  159. if(curInnerWidth == window.innerWidth && curInnerHeight == window.innerHeight)
  160. return;
  161. curInnerWidth = window.innerWidth;
  162. curInnerHeight = window.innerHeight;
  163. term.options.fontSize = computeXTermFontSize();
  164. fitAddon.fit();
  165. const display = document.getElementById("display");
  166. if(display)
  167. setScreenSize(display);
  168. }
  169. async function initTerminal()
  170. {
  171. const { Terminal } = await import('@xterm/xterm');
  172. const { FitAddon } = await import('@xterm/addon-fit');
  173. const { WebLinksAddon } = await import('@xterm/addon-web-links');
  174. term = new Terminal({cursorBlink:true, convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700, fontSize: computeXTermFontSize()});
  175. fitAddon = new FitAddon();
  176. term.loadAddon(fitAddon);
  177. var linkAddon = new WebLinksAddon();
  178. term.loadAddon(linkAddon);
  179. const consoleDiv = document.getElementById("console");
  180. term.open(consoleDiv);
  181. term.scrollToTop();
  182. fitAddon.fit();
  183. window.addEventListener("resize", handleResize);
  184. term.focus();
  185. term.onData(readData);
  186. // Avoid undesired default DnD handling
  187. function preventDefaults (e) {
  188. e.preventDefault()
  189. e.stopPropagation()
  190. }
  191. consoleDiv.addEventListener("dragover", preventDefaults, false);
  192. consoleDiv.addEventListener("dragenter", preventDefaults, false);
  193. consoleDiv.addEventListener("dragleave", preventDefaults, false);
  194. consoleDiv.addEventListener("drop", preventDefaults, false);
  195. curInnerWidth = window.innerWidth;
  196. curInnerHeight = window.innerHeight;
  197. if(configObj.printIntro)
  198. printMessage(introMessage);
  199. try
  200. {
  201. await initCheerpX();
  202. }
  203. catch(e)
  204. {
  205. printMessage(unexpectedErrorMessage);
  206. printMessage([e.toString()]);
  207. return;
  208. }
  209. }
  210. function handleActivateConsole(vt)
  211. {
  212. if(curVT == vt)
  213. return;
  214. curVT = vt;
  215. if(vt != 7)
  216. return;
  217. // Raise the display to the foreground
  218. const display = document.getElementById("display");
  219. display.parentElement.style.zIndex = 5;
  220. plausible("Display activated");
  221. }
  222. function handleProcessCreated()
  223. {
  224. processCount++;
  225. if(processCallback)
  226. processCallback(processCount);
  227. }
  228. async function initCheerpX()
  229. {
  230. const CheerpX = await import('@leaningtech/cheerpx');
  231. var blockDevice = null;
  232. switch(configObj.diskImageType)
  233. {
  234. case "cloud":
  235. try
  236. {
  237. blockDevice = await CheerpX.CloudDevice.create(configObj.diskImageUrl);
  238. }
  239. catch(e)
  240. {
  241. // Report the failure and try again with plain HTTP
  242. var wssProtocol = "wss:";
  243. if(configObj.diskImageUrl.startsWith(wssProtocol))
  244. {
  245. // WebSocket protocol failed, try agin using plain HTTP
  246. plausible("WS Disk failure");
  247. blockDevice = await CheerpX.CloudDevice.create("https:" + configObj.diskImageUrl.substr(wssProtocol.length));
  248. }
  249. else
  250. {
  251. // No other recovery option
  252. throw e;
  253. }
  254. }
  255. break;
  256. case "bytes":
  257. blockDevice = await CheerpX.HttpBytesDevice.create(configObj.diskImageUrl);
  258. break;
  259. case "github":
  260. blockDevice = await CheerpX.GitHubDevice.create(configObj.diskImageUrl);
  261. break;
  262. default:
  263. throw new Error("Unrecognized device type");
  264. }
  265. blockCache = await CheerpX.IDBDevice.create(cacheId);
  266. var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, blockCache);
  267. var webDevice = await CheerpX.WebDevice.create("");
  268. var documentsDevice = await CheerpX.WebDevice.create("documents");
  269. var dataDevice = await CheerpX.DataDevice.create();
  270. var mountPoints = [
  271. // The root filesystem, as an Ext2 image
  272. {type:"ext2", dev:overlayDevice, path:"/"},
  273. // Access to files on the Web server, relative to the current page
  274. {type:"dir", dev:webDevice, path:"/web"},
  275. // Access to read-only data coming from JavaScript
  276. {type:"dir", dev:dataDevice, path:"/data"},
  277. // Automatically created device files
  278. {type:"devs", path:"/dev"},
  279. // Pseudo-terminals
  280. {type:"devpts", path:"/dev/pts"},
  281. // The Linux 'proc' filesystem which provides information about running processes
  282. {type:"proc", path:"/proc"},
  283. // The Linux 'sysfs' filesystem which is used to enumerate emulated devices
  284. {type:"sys", path:"/sys"},
  285. // Convenient access to sample documents in the user directory
  286. {type:"dir", dev:documentsDevice, path:"/home/user/documents"}
  287. ];
  288. try
  289. {
  290. cx = await CheerpX.Linux.create({mounts: mountPoints, networkInterface: networkInterface});
  291. }
  292. catch(e)
  293. {
  294. printMessage(errorMessage);
  295. printMessage([e.toString()]);
  296. return;
  297. }
  298. cx.registerCallback("cpuActivity", cpuCallback);
  299. cx.registerCallback("diskActivity", hddCallback);
  300. cx.registerCallback("diskLatency", latencyCallback);
  301. cx.registerCallback("processCreated", handleProcessCreated);
  302. term.scrollToBottom();
  303. cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
  304. const display = document.getElementById("display");
  305. if(display)
  306. {
  307. setScreenSize(display);
  308. cx.setActivateConsole(handleActivateConsole);
  309. }
  310. // Run the command in a loop, in case the user exits
  311. while (true)
  312. {
  313. await cx.run(configObj.cmd, configObj.args, configObj.opts);
  314. }
  315. }
  316. onMount(initTerminal);
  317. async function handleConnect()
  318. {
  319. const w = window.open("login.html", "_blank");
  320. await cx.networkLogin();
  321. w.location.href = await startLogin();
  322. }
  323. async function handleReset()
  324. {
  325. // Be robust before initialization
  326. if(blockCache == null)
  327. return;
  328. await blockCache.reset();
  329. location.reload();
  330. }
  331. async function handleTool(tool)
  332. {
  333. if(tool.command)
  334. {
  335. var sentinel = "# End of AI command";
  336. var buffer = term.buffer.active;
  337. // Get the current cursor position
  338. var marker = term.registerMarker();
  339. var startLine = marker.line;
  340. marker.dispose();
  341. var ret = new Promise(function(f, r)
  342. {
  343. var callbackDisposer = term.onWriteParsed(function()
  344. {
  345. var curLength = buffer.length;
  346. // Accumulate the output and see if the sentinel has been printed
  347. var output = "";
  348. for(var i=startLine + 1;i<curLength;i++)
  349. {
  350. var curLine = buffer.getLine(i).translateToString(true, 0, term.cols);;
  351. if(curLine.indexOf(sentinel) >= 0)
  352. {
  353. // We are done, cleanup and return
  354. callbackDisposer.dispose();
  355. return f(output);
  356. }
  357. output += curLine + "\n";
  358. }
  359. });
  360. });
  361. term.input(tool.command);
  362. term.input("\n");
  363. term.input(sentinel);
  364. term.input("\n");
  365. return ret;
  366. }
  367. else if(tool.action)
  368. {
  369. // Desktop control
  370. // TODO: We should have an explicit API to interact with CheerpX display
  371. switch(tool.action)
  372. {
  373. case "screenshot":
  374. {
  375. // TODO: Resize
  376. var display = document.getElementById("display");
  377. var dataUrl = display.toDataURL("image/png");
  378. // Remove prefix from the encoded data
  379. dataUrl = dataUrl.substring("data:image/png;base64,".length);
  380. var imageSrc = { type: "base64", media_type: "image/png", data: dataUrl };
  381. var contentObj = { type: "image", source: imageSrc };
  382. return [ contentObj ];
  383. }
  384. case "mouse_move":
  385. {
  386. var coords = tool.coordinate;
  387. var dc = get(displayConfig);
  388. dc.mouseX = coords[0];
  389. dc.mouseY = coords[1];
  390. var display = document.getElementById("display");
  391. var clientRect = display.getBoundingClientRect();
  392. var me = new MouseEvent('mousemove', { clientX: dc.mouseX + clientRect.left, clientY: dc.mouseY + clientRect.top });
  393. display.dispatchEvent(me);
  394. return null;
  395. }
  396. case "left_click":
  397. {
  398. var dc = get(displayConfig);
  399. var display = document.getElementById("display");
  400. var clientRect = display.getBoundingClientRect();
  401. var me = new MouseEvent('mousedown', { clientX: dc.mouseX + clientRect.left, clientY: dc.mouseY + clientRect.top });
  402. display.dispatchEvent(me);
  403. var me = new MouseEvent('mouseup', { clientX: dc.mouseX + clientRect.left, clientY: dc.mouseY + clientRect.top });
  404. display.dispatchEvent(me);
  405. return null;
  406. }
  407. default:
  408. {
  409. break;
  410. }
  411. }
  412. debugger;
  413. }
  414. else
  415. {
  416. debugger;
  417. }
  418. }
  419. </script>
  420. <main class="relative w-full h-full">
  421. <Nav />
  422. <div class="absolute top-10 bottom-0 left-0 right-0">
  423. <SideBar on:connect={handleConnect} on:reset={handleReset} needsDisplay={configObj.needsDisplay} handleTool={handleTool}>
  424. <slot></slot>
  425. </SideBar>
  426. {#if configObj.needsDisplay}
  427. <div class="absolute top-0 bottom-0 left-14 right-0">
  428. <canvas class="w-full h-full cursor-none" id="display"></canvas>
  429. </div>
  430. {/if}
  431. <div class="absolute top-0 bottom-0 left-14 right-0 p-1 scrollbar" id="console">
  432. </div>
  433. </div>
  434. </main>