feat(editor): F4.6c the software installation on a computer's desktop

A new oracle, InstallRun, installs each of Java's programs on new
computers, all of them on one, each installed and removed again, and a
DNS and an e-mail server installed again after their removal; Java saves
the result as a project. A new program is switched off (a computer's
firewall is on), makes the folders and files it lacks (the web server's
start page in FILIUS' language and its picture, /peer2peer,
/mailserver/konten.txt, /dns/hosts), and removing it takes its settings
and leaves its files; a server installed again reads its records or
accounts from them. Java installs in the order of its list and starts
the program at once.

The document's setSoftware does the same, checked against Java's saved
installation. The core knows what a program makes in the files and
installs or removes a program while the simulation runs; the room
watches the document's programs and hands changes to the engine, so a
new command line works at once. The desktop offers Java's dialog with
two lists, arrows, double click and apply changes.

Also: Java's file may name a number it rolled by the result of any
getter (an idref, here a thread's priority); GetterDefaults now
measures the priority of the saved thread classes, which the import
read as 0 before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 15:03:35 +00:00
co-authored by Claude Opus 5.5
parent ac48e969f4
commit f0e7c73ec7
29 changed files with 1264 additions and 52 deletions
+1
View File
@@ -19,6 +19,7 @@ export * from './runtime/capture.js';
export * from './runtime/captureRows.js';
export * from './runtime/threads.js';
export * from './runtime/apps/program.js';
export * from './runtime/apps/installation.js';
export * from './runtime/apps/server.js';
export * from './runtime/apps/echo.js';
export * from './runtime/apps/web.js';
@@ -0,0 +1,122 @@
// What FILIUS' programs bring into a computer's files when the software
// installation installs them (measured with tools/java-oracle's InstallRun):
// a web server its folder /webserver with Java's start page and picture
// (WebServer.erzeugeStandardVerzeichnis, the page in the language FILIUS
// runs in), Gnutella its folder /peer2peer (PeerToPeerAnwendung), an e-mail
// server /mailserver with an empty konten.txt (EmailServer), a DNS server
// /dns with an empty hosts (DNSServer). Each is made only where nothing of
// the name is: a file left by a program removed before stays as it is, and
// the program reads it (a DNS server its records, an e-mail server its
// accounts). Removing a program leaves its files.
import type { InstallableProgram } from '../config.js';
import type { VirtualFileContent } from '../../model/virtualFileSystem.js';
import { javaBase64Decode } from '../../util/javaBase64.js';
import type { PageLanguage } from './routerPages.js';
export interface InstallationFile {
readonly path: string;
readonly content: VirtualFileContent;
}
/** tmpl/webserver_index_<language>.txt as Java reads it: each line ended by "\n". */
const WEB_SERVER_INDEX: Record<PageLanguage, string> = {
de: "<html>\n <head>\n <title>Standardseite</title>\n </head>\n <body bgcolor=\"#ccddff\" style=\"font-family:Verdana; text-align:center;\">\n <h2> FILIUS - Webserver </h2>\n\n <p>Herzlich Willkommen auf dem Webserver der Anwendung FILIUS!</p>\n\n <p> Diese Seite wurde automatisch mit der Installation des \n Webservers eingerichtet, es lassen sich jedoch auch \n eigene Seiten hier unterbringen. </p>\n\n <p align=\"center\"> <img src=\"splashscreen-mini.png\"> </p>\n\n <p> https://www.lernsoftware-filius.de </p>\n </body>\n</html>\n",
en: "<html>\n <head>\n <title>Standardseite</title>\n </head>\n <body bgcolor=\"#ccddff\" style=\"font-family:Verdana; text-align:center;\">\n <h2> FILIUS - Webserver </h2>\n\n <p>Welcome at FILIUS!</p>\n\n <p> This page has been delivered as default page.\n Feel free to add your own contents! </p>\n\n <p align=\"center\"> <img src=\"splashscreen-mini.png\"> </p>\n\n <p> https://www.lernsoftware-filius.de </p>\n </body>\n</html>\n",
};
/** gfx/allgemein/splashscreen-mini.png, the picture on the web server's start page. */
const SPLASH_PNG =
"iVBORw0KGgoAAAANSUhEUgAAAIAAAABgCAYAAADVenpJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUw" +
"AADqYAAAOpgAABdwnLpRPAAAAB90RVh0U29mdHdhcmUATWFjcm9tZWRpYSBGaXJld29ya3MgOLVo0ngAABebSURBVHhe7Z13sFRFFsYvWorigjxWBNct1/3D" +
"UmuroAxlSWHABVRETCgiKKJEwUTSV4gYQAQREAXEQBITKGYUMbEqggkzSRRFBcWAGZBwdn6N39iO83jzZu69b3jcruq6M307nvP1Oafjrfb6669bkLjtlwJv" +
"vvmmJX77pUHwxhtvWBheIAojr209D2iBZMVna0sx0SoIq/fT2FdeeSWRJr8zf9myZbZkyRIHgEwaz58/31599dWioFWwYMECK9TTwEWLFhmO37nkN2/evJzj" +
"5pJfrnFee+01R/xc4+cTDwZv2LDBfvjhh7+URdm//vqrffvtt0Zd8sk/zDShAACUg3YBABD4og+CICGoOO/eeustF/edd95Jh/OOOH5cNZRw0kEw5aMwpaE8" +
"nzD8Jy/SCJQQ/5tvvrHvv//eSSviKD/Vzf+vNvj1UP18oCseaSmDOOvWrbPvvvsuXb5f308++cQ+/PDDtHTw2yfJ4LfFp4vihgWCAGYU6qmMAMBvCPLFF184" +
"qUBj169fb1999ZUjDgxZs2aNAwDMIJ6YRRx6zurVq1086sWT/++//77rUcRBenz99df27rvvuncQG5FLPqSBAR9//LErF2aLWNRn8+bNtnHjRvvyyy9dGtKL" +
"mYsXL3b5kl5t+uyzz9JAIe5vv/3meq/y9NtKnpT50ksvuacAANDJl/JgIHl+9NFHLl/aRb7kR/t++eUXe++999x/tYV8ocuqVascvZYuXZp+XyjvgrffftsK" +
"9T4AqBCMhgByn376qfu5YsUK9w7G42AmYfRGmEgjpUpgHATinRxhMBYCy/3000+OITjKJg154sgLguJefvll958yYCKEhPE4mAGoeIcDWCqXuuu36rd27VoX" +
"j7L8tvJ+5cqVNnfu3DQAXnzxRRd306ZNaYlEPNpCWkAnB4NRDzjqw3vogqODiW4AgroXyjfShyIBqIwkgFBLxelpEEMMg+j0AAgsQvNfKgGxSKXEQAgv4kMc" +
"iALYFAbjYSyMwEFMPz550cNwCxcudOl//vln5/mNp47kQz1xMIfylSfSACmGk3imTBx1EnjJB2ZDC4GC99ABqUM8v3NIOgjw0I/6AEYc9aH+OHo8dCKM+gEA" +
"SbuCJQDiqVBPJagkjkpTWSQAhFUv4d3nn3/uCPTBBx+4uDxJK/DQOHqKeiKEhCg4ej5xqasfBiBEKB8AEJ285HmHOqGH4aXrASUORlBnxDMAWb58uasHZSkO" +
"TKR88kGKILIJ89squ0ZSQu2knnpHfNQgdRAApFIENvIVXQAxbYQeAA3JqfwK5V1oKsAHAAQC+TQShtOLBAAAQW/Eoet4zxNHQ2m4GO8zG8KI2QIAYRKFYqLe" +
"ATZ6qvIirQAAg2VjSEJQX9JIJMNAxD9MIhxH3oBQEkD2iNpK22CU2k9PFRCk033a+AAgHW3xAaCOAhBIJykVqgooFEGkp/ICgFBOD0HPSSRCQPQjhJAKoLfB" +
"fABCfBzgIExxBR4IIwngh6knkFbjbgiEQ5xSN8qhp8GgH3/80b0jXGW7gN8Bqbz5L4bKVsDe4Ld0Melhot/WzPYDFnotTiMdxfdHT6TjvQCgEQwSDKc26Xdo" +
"EgBmFOqpPPpbRIRIEBrLF0LTUBoNI4R0eg+O3qOZM40OCEdMapiGOEckUw51hegKEyMFADEAAMnBAIDFO5gmhgAYCEk9KU9qgbrDbMqnPNIhKQRSmEJaGZ3U" +
"m/bwn/jkSR6EaSKIdpIextIxYKhvPAsAgJb86fHkRXpGCeRH3ckD24J2F8o30gdhZEIe0v3KT0Mr/ZfRov8a4onIhGusr7G74sKYzAZnhmX+J3/CyIsyBB4B" +
"hHDlqTkAv+7UxacN6WTPKD/CsE1QEYDbj0/+hAECpRUNMDKxK0Q31BRheM0RyCCWcYgkkaqkM4QGAJCd+PxoAGPpnehqGY+yyjEQYSgMRLpsLRwgYG/gkQBI" +
"CM0/SFpJmpGv5g/C4FsAqhKfPw0kWeiRiGeNchD5UikM+ZgEgs6Z4TBcakYqirkLGccwGwkFs/U7TH4FoLcqeL835EIgP36h7YdBGIboddlD9FQxlHBJg8xw" +
"GcLUgXwYNWCwUj+/Xpn/C62z0gdhiJG48/AZLCJj5DGEYzSC/qTH0bvQ0b4nDE8c4pOO+tML/XxzbZOYDwAQ8+RBHWC09LRmPFEHmeG+7YAKYO5EE1i51qGQ" +
"eNuEBPAZQ++AeTAVg0kGGISUIeU/pVt5ZntPOjz5EId8yZ9yVG5ZvQ/QYJFjB8iYpWeRjkkkTe4w70CexM8WrokebAVAGVbvziWfogJAphind9KbYArM8Zns" +
"Mzbs3wKKQIHEoPdSP5jr1xOG0sMBAcNBjQQQ++h+ngzhJA2yhZMfbUD3Ey8XxoUVJ4DIlelFTBoEkSE2PZthUnm9O1uPjiJMEoI68RtAAkxf7fAftUM7CAcI" +
"klL8Fo3LCqft+Lh5EQsAMtEK0xF1iEXGtBoX+wyXaC6mJ/X06yNA0AYAkGm8SXVkMlX08MOzhcUBhgBG5OJp3NZ8WY0kDcShZ2+N2ZnELSbGl1WXzDoDYECB" +
"OKetADxbr86kYy70jypOzgCQiOJJw7JZ24huPAQQMXjKi5BRiOliytM3LEUHwqAL9EE1oC5ER5gLKOhEvrSsqATIZhcoDx90PpgCn0GF/t4Wem0x1DEbnZEc" +
"vi+EF6yDZOaXLX86TagAKKTSSdo/JGVctEAiBeUhJXn/554ZFT1kAEtlRFWOn28CgAyxGwfRyxLFzCWwCIT41qgo6vo4AMQlbpJy/irisUdgMhNITBYBANYC" +
"eGriS/MhYdOPfJ0NAOISHy8N1LPZiALz8awRwHimjQGB9hkChKj44wAQtZhJ8v/DhpB1znQwi0dMHzNdjPcBwKZVpoVZXtZOqrDpSF2QQAGLGYmPhwYwEVqj" +
"7+n9ZQEACaCNpoAFZkXBIweAqMRLku+f1Yr0PQzFlwUAej+qALAQD5DwOwp6OgCoQslzC2PC9vRc9DqzfzAxGwB8I1DDQTEfAJBH2PUiP8pKABAB031mwXRt" +
"RdcUuS8BYL56PEaZ5gESAETMmCh6VLY8AQB7Bu699163rxMRT68mHEOQJwtl/oqoL/6jlACUkxiBERvBOp+www472LBhw9JnFfihhSGeshEkAbTTSAAI2wiU" +
"cZmogAglDUwTAPbaay875ZRTnL/77rvTNgHLxgAACUBcjEMdE9fhmqikFeUFDEkSXzgNpNfR55wqYijHGJ4JHR1Rb9GihZ199tl20UUX2fPPP+9UgQCg+YCH" +
"H37YRo0a5aTFPffc4/IBFFHwCIAGVCLxFaeBGALDdaYfMT558mS77rrrbNCgQTZu3Di76aabnNjv0qWLe86ePdv69OljMBrGAgDyuv/++61Ro0Z2+umnW7t2" +
"7eycc85xz6OPPtqBKAoeOQBEgayqnKd6uno1jLviiits8ODBTrQjtjm3CNM0vw/ja9WqZeeff7479yeH6AcAMAJAnHrqqdaxY0fnO3XqZJ07d7YzzjjDHTBh" +
"u3jYdE0kQAWlH0yAgezmKS0tdaJaR8cZ66O76anaJazfMLxhw4Z28sknOyYDGPYLwgAAwPAQtdG8efOsAGCnMCoibCkAmBMVkCMIYDxbqXr16uXENU5Gms/w" +
"TCbxTtflkPakk05yNsCzzz7rGMoEEXmjMs4888z4AcBYNPFl04BejevRo0ea8YShv3OhGxM9/j1H9Pa77rrLXfaABAAcQ4cOtZYtW1qHDh3+BADshlatWrlL" +
"LrA1cimvInEAZ0BDEv9XGtA7dUWMDDiJ+IrQizSPPvqojRgxIq37H3/88fTVNcOHDzeNDgSA8847z/CohLFjx7rRREXKzDWuAwB6LfF/poFuALnlllvsjjvu" +
"cIwjLB86odufeeYZ69mzp+vhDO+efPJJd2/Brbfe6piMxd++fXv3HsafdtppzmDU3UT5ll1efQF5AoCMDiDm33DDDemrawphAHYClztcfPHF1rt3bzcchOHY" +
"AkwKnXXWWW5uAM+w74gjjrAHHnjAgQ71UUjZCQAqKN0gNkO422+/3eltxveFMgAmYujBeIzACy+80PVyejwMb9u2rWM+kgCQcCEEewEKLbc85vM+kQAZAKG3" +
"Pvfcc248j94NgwnkgZU/ffp0x2iAgHgXAJACRx11VPpeQhgTRrk5A6AiVmNVj4vYRVdL54fZXno2Ez/MHWDxN23a1M38XX/99a48bIUwyysvLwCCsZjYAL/3" +
"OJjA7FshBl95vY6hnC7BZHTAJFCU5ZVXHwcA7UrdXp8wBdHPGbqHHnrI7d6JkhZa9FG5/I+yvK3ljZTYLgEgojOfz2QMDr2Mq0yGxA2E7RYA+mADU7tc8MyJ" +
"Z2bokAT47QUEDgASSdvDE+bS67lYirl4lm7lWZ/nnh+OvbMGj45Gh1ZlutC+AF1UlT1iFQsbK5wez7LrbbfdZjfffLONHz/ejfmZ7WNWjufTTz9tc+bMSS/S" +
"AARt3KxqdHIAiFvvxFmeFnLYdHnfffcZU7usujH/zty8PEMzNm7gmQEEDE888YQxZ4+lzJxAVVQLVVoC0OtZ7JgyZYpbbbvssstswIABduWVV9rAgQPt6quv" +
"tmuuucZ5QMGGDjy7ea699lrr37+/TZs2ze688063fUuHNquSFADU26wK8CWJNlHqUKVu9WbuHTHft29ftwbPVCzbsfCE4fv16+fAcfnllztPHBZspk6dapMm" +
"TXLLtmycAFBVifm0xQEA4lWGh6DaRZNL+VSYeLp2Pb22mvpBOAsuM2bMcMun9Hg2bSAB6NGIdhjNalvXrl2te/fudsEFFzjPzB+e7VcAY8yYMc4+QPzjkRDc" +
"5cPIIZd6bktx6ESBhj5xPmE+Gyi/X73lTn/+Q7jMOqhn+zdmY8Shw+mlEydOdOKcMCx77dMTOFIfBLarrrrKgYF4MJPxfuvWrdMbL1mNY3UOsU+e2APER11g" +
"I7AuwB3/SJc4aRRHWXSqSgEAvenjxQtTH62uZ31Lr0rxa8sXxmCgepDEOAYcjKNXwhz/yxlMq2r1jLQQTcesye+SSy7hw9hWUlLiJAM6HjHPfj6mfdmBy04f" +
"7AF0PvljLPL7xhtvdMxnrMyunWwAjYNJUZbhAFAZIstss428dYoFTUotOKSjBf8+wfoMHGFrVm75YhZu7NhxzmB76qmn0mGAQtIil3qTEHUACPDNmjWzMbeM" +
"sV69e6VtApZgAQXxGCVg9D344IPpT+BUpLxc6lQscdRZYpcAGpo17XuH7dJ7jtXuMcNqdpxowQnXWtCgsx3Wuoe1adnMbOOWb/Oh8/PtfdgYuMcee8xJAUBQ" +
"rWQn69Sqg13U80InBZAS7NIdMmSIkxBY/nzmDmNQUinKXliZeUPXQN/Ri+3Jhxw3rLGa/eZZ3YlmdcettT2Gr7CSAa/Z7pfMtOodJljQYojVPOx0u2vClu1Y" +
"OBmNFa2n7AI2ZTRt8l8L/lHddhl2gB17bivr2qmrAwCjBdQCcXBasWNrlj56mW/5Fa1vnPFpU+wAWLduvX2zcK4FHWZZnSvmW50hH1rdMT9a/TvN9hy/3v4+" +
"eImV9Jtju3W+14Lm16WkQmsbPfrmgkCgmUAymTJpsh2x32HW6IQjrX279m5r1iOPPJKWNj6jCWTWUIc642ROHGUhfQIaHKe3zZtswuRpFhxTajVbD7VaHcan" +
"1MBDVqf0Zas7bLnVExAGLbI6fZ61oP1Uu3LA1Y5B+ppXvvWVSiCv2bNmuyGiPs2aLU+d62c4yZ59DE529+RbD8rPN22+bd5aOgeAOJDmlwHxTz6zmwX/amTV" +
"G7S0XRp1sBrH9bO/tR1ltbtPt5LSuVZ3xCrbc0JKPYxMbVvu9abNmT4+RfjN6c++Flpn1IKGloxIystPZ/8YGjJBhCsvjf+e9DCCiRftA6hI+qjixq4CKBAX" +
"BLUt2HFnq7bjTrZj/f2t+n+Os12P6mK7nTLIap0/xUr6Pm97DFlme0wy273P/2z1srdsbcoYjIoQueSrunPfP/MGOEC0tbS67Ut7/HQbGHkBQn3GtrLsCwcA" +
"KhKXl0E3ZmxKv9doaMEu9a1avQNsp/0aW/VDzrBdm19qNduMsN273e8kQcnIb63+OSNdsrjqWF45OubFCIL9/pIGfjoYq529uhKOWbdsANAxcoGlvPLDfA94" +
"A6Ewyqf07MyZM+3AAw9MrbaNdYSbNPUpC/Y/y4KG7WznJj1st5b9rWbbm6xWt/tShuALtvP1K+3s0i0GoN9joqxrLnlrZMFRb+0kYo6CtDxZZdOFUKwm8r8s" +
"AAAo7TnIpeww48QCABlOI0eOdHvfGVqxDXrfffdNjb0HO+Y+8Nh826HFqJTBN9lqdp9mtS992uoMeN2CEb/alJQVXmwAUG/VUW9UAlPR+uA0J3owHHWU3AeA" +
"f4EEoNDdgbpnIEwGl5dXZACAEHjG0yyosNDCggvz8ky9cv6dAxFHHnmk7b33P6308r4pHq+xaU8ssKDrHKs29CurOzq1XemGjbZ0znTbsHFTWl+W16jKeA9A" +
"X3jhBcd0rnrRZZDZAKBVRY6V6wJIXRQdd90dAMLUKRLTIJsGsg8ehh966KHWpk0bN7vGqhwLMxyQwDO8atKkidWrV996XtDVNn7xjo2f8YYF/VN32HWZafbb" +
"dynmry0aG6Asesk20AcpMiWAlpKhi24EAyACCzSrDDsgdBsAQuj2K4iBmIRoHIBo0KCBW4Vj9g3PXD/r9LoJg1Oy++zzL2vWpLF9tuAZ6z9wuBP/cfeMfMpD" +
"93ORg9QVvRtmAwR0PP+ZafSvgvcBwLQsUjOfsvNNA18Cieuwnhh8gIDdttykocsPWVnDscp28MEHu0ORSABdr8I6PWHHH3+82461cuUqW7pksVMjYdUtynxo" +
"Gyru3HPP1WDHPRH50IFDIPruoS5+zgQAtIuyjtnyDh0AKoTG0yNYzoW5derUsRo1arjVNhxLvIcffnj6dCx2AVOyHM6E6aAaAzJuguRbHm1ia1njxo3t2GOP" +
"dSd8YTiSgQ0l/ocvZSQiGXRrOCoibgBA44DeGpWHKBgagICvYbPRAuYyNMJBHIjGQUniQSTt+ImqTlHmS5u41Im7gHQKmDAkgD5di6jXV8ZZ6mbl0b9SLsr6" +
"ZeYNrSMFgNQBROA3e+6xlplNQz+CehxXoDCMwsVJgDDLou4sO+PYRMr2M80YAgDsIgw9lpy5+o1FKHYmARiMYL4zLDqFWa+t5QXwIgWAChcQNG7mXDoNZsMl" +
"u3Y1saJ4cREgzHJgHoYuKkAjAvQ/vwUApBxzILoJBLWHAcx9AYyWqiwAfCBIxPv35SESt2Xmi+HsIcSuwQZghxF3DehaOSQAq2+EY+zCdAGAJ3sSKgUAED7x" +
"hdMAcYpjf+ExxxzjJr66devmwmQDYNRySQQ2z9YAEBc/AG6g79Ikzy3f58nHQ0hGL1j1OCZ32GvAnIcAgPEL87UL2QcAv3UxBXnlU4d80kRuBIapY4s9Lxgt" +
"JjqupxzXw0msw3x2Ics41FVwPDEKWSjDxdlOQBOAzMQXRgNtYUfkw1BOFGHksgiEY+kYix8Q+AA48cQT3UyodiqRT5y8cCogzgKralkCAJY8x804ecQQkLuA" +
"sPphPnofz0wh6x/cEcQcAA71ETfzxYvEBshT72fqXKkAAMBZA2Y/se7p9TBcAGDMz2KYXJw6P5udkEiAkFQgDGXdAyNPZw3p7TCepW9mBxkeMhzEMTwsBokY" +
"MDRJfDg0gLGM9dlGzsomOp4NMAcddJBNmJDa5fq7KyZ6JwAIuQOkuZz6wfQ2B1h1/WwxMV51CTBAEh8+DXwgyNArRjonACjCDoB9IA9o+F0WePy4FUmn/BIA" +
"FBkA/DWSTCkS9n/KSgBQZACAySwisULKzaWzZs2yefPmuZu9WT5n7oAn3xxatGiRe8cOKqafWWbXVfOjR49277A/2IvAUjy3pHGTCquUHIHHBWWJkCT8DzEc" +
"Jy1gChdXsreAbXR8eo4nZxDYS8FNJjCR+QM2nXCcnUst+AgFTw6z4tiGzwYcbk8BKMxO6pN2bM7RgdcgbLGS5FecFGCOQsff/Rr+H44hBZr8Amz5AAAAAElF" +
"TkSuQmCC";
const folder = (path: string): InstallationFile => ({ path, content: { kind: 'directory' } });
const text = (path: string, value: string): InstallationFile => ({ path, content: { kind: 'text', text: value } });
/** The folders and files a program makes when it is installed, in the order Java makes them. */
export function installationFiles(program: InstallableProgram, language: PageLanguage): readonly InstallationFile[] {
switch (program) {
case 'webServer':
return [
folder('/webserver'),
text('/webserver/index.html', WEB_SERVER_INDEX[language]),
{ path: '/webserver/splashscreen-mini.png', content: { kind: 'image', data: javaBase64Decode(SPLASH_PNG)!, mediaType: 'image/png' } },
];
case 'gnutella':
return [folder('/peer2peer')];
case 'emailServer':
return [folder('/mailserver'), text('/mailserver/konten.txt', '')];
case 'dnsServer':
return [folder('/dns'), text('/dns/hosts', '')];
default:
return [];
}
}
+167 -23
View File
@@ -76,6 +76,7 @@ import {
FIREWALL_PROTOCOL,
firewallConfiguration,
interfaceKey,
type InstallableProgram,
} from './config.js';
import { formatIPv4, maskAddress, parseIPv4, sameSubnet } from './ipv4.js';
import { javaIpCheck, javaParseInt } from '../util/javaIp.js';
@@ -112,6 +113,7 @@ import { ICMP_TYPE_CODE } from './captureRows.js';
import { JavaCompatibleRandom } from './random.js';
import { routeTableRows, type RouteRow } from './routeTable.js';
import { EMPTY_TOPOLOGY_SNAPSHOT, withSnapshot, type NodeSnapshot, type TopologySnapshot } from './snapshot.js';
import { installationFiles } from './apps/installation.js';
import { RuntimeSpeed } from './speed.js';
import { ApplicationThreads, waitUntil, type Steps, type Wait } from './threads.js';
import { EchoServer, SimpleClient } from './apps/echo.js';
@@ -874,6 +876,10 @@ function getOrCreate<K, V>(map: Map<K, V>, key: K, create: () => V): V {
// ── Engine ─────────────────────────────────────────────────────────────────
/** The programs the engine runs (the others — file explorer, image viewer, text editor — are the editor's), in the order a start makes them. */
const RUNNING_PROGRAMS = ['echoServer', 'simpleClient', 'webServer', 'webBrowser', 'emailServer', 'commandPrompt', 'gnutella', 'emailClient'] as const;
type RunningProgram = (typeof RUNNING_PROGRAMS)[number];
export class NetworkRuntimeEngine {
private stateValue: RuntimeState;
private random: JavaCompatibleRandom;
@@ -1247,18 +1253,52 @@ export class NetworkRuntimeEngine {
this.terminalsByNodeId.clear();
this.gnutellaPeersByNodeId.clear();
for (const node of snapshot.nodes) {
const echoServer = snapshot.echoServersByNodeId.get(node.id);
if (echoServer !== undefined) this.echoServersByNodeId.set(node.id, new EchoServer(this, node.id, echoServer));
const simpleClient = snapshot.simpleClientsByNodeId.get(node.id);
if (simpleClient !== undefined) this.simpleClientsByNodeId.set(node.id, new SimpleClient(this, node.id, simpleClient));
const webServer = snapshot.webServersByNodeId.get(node.id);
if (webServer !== undefined) {
this.webServersByNodeId.set(node.id, new WebServer(this, node.id, webServer, snapshot.fileSystemsByNodeId.get(node.id)));
}
if (snapshot.webBrowserNodeIds.has(node.id)) this.webBrowsersByNodeId.set(node.id, new WebBrowser(this, node.id));
const emailServer = snapshot.emailServersByNodeId.get(node.id);
if (emailServer !== undefined) {
const nodeId = node.id;
for (const program of RUNNING_PROGRAMS) if (this.hasProgram(node.id, program)) this.startProgram(node.id, program);
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
}
}
/** Whether the snapshot gives a node a program the engine runs. */
private hasProgram(nodeId: Id, program: RunningProgram): boolean {
const snapshot = this.stateValue.topologySnapshot;
switch (program) {
case 'echoServer':
return snapshot.echoServersByNodeId.has(nodeId);
case 'simpleClient':
return snapshot.simpleClientsByNodeId.has(nodeId);
case 'webServer':
return snapshot.webServersByNodeId.has(nodeId);
case 'webBrowser':
return snapshot.webBrowserNodeIds.has(nodeId);
case 'emailServer':
return snapshot.emailServersByNodeId.has(nodeId);
case 'commandPrompt':
return snapshot.terminalNodeIds.has(nodeId);
case 'gnutella':
return snapshot.gnutellaByNodeId.has(nodeId);
case 'emailClient':
return snapshot.emailClientsByNodeId.has(nodeId);
}
}
/** Starts a node's program from the snapshot (Anwendung.starten); what it shows starts empty. */
private startProgram(nodeId: Id, program: RunningProgram): void {
const snapshot = this.stateValue.topologySnapshot;
switch (program) {
case 'echoServer':
this.echoServersByNodeId.set(nodeId, new EchoServer(this, nodeId, snapshot.echoServersByNodeId.get(nodeId)!));
return;
case 'simpleClient':
this.simpleClientsByNodeId.set(nodeId, new SimpleClient(this, nodeId, snapshot.simpleClientsByNodeId.get(nodeId)!));
return;
case 'webServer':
this.webServersByNodeId.set(nodeId, new WebServer(this, nodeId, snapshot.webServersByNodeId.get(nodeId)!, snapshot.fileSystemsByNodeId.get(nodeId)));
return;
case 'webBrowser':
this.webBrowsersByNodeId.set(nodeId, new WebBrowser(this, nodeId));
return;
case 'emailServer': {
const emailServer = snapshot.emailServersByNodeId.get(nodeId)!;
this.emailServersByNodeId.set(
nodeId,
new EmailServer(
@@ -1271,24 +1311,128 @@ export class NetworkRuntimeEngine {
),
);
this.writeEmailServerAccountsFile(nodeId, emailServer.domain, emailServer.accounts);
return;
}
if (snapshot.terminalNodeIds.has(node.id)) this.terminalsByNodeId.set(node.id, new Terminal(this, node.id, this.nodeFiles(node.id)));
const gnutella = snapshot.gnutellaByNodeId.get(node.id);
if (gnutella !== undefined) {
case 'commandPrompt':
this.terminalsByNodeId.set(nodeId, new Terminal(this, nodeId, this.nodeFiles(nodeId)));
return;
case 'gnutella': {
// PeerToPeerAnwendung.setSystemSoftware: the folder of the files to share.
const files = this.nodeFiles(node.id);
const files = this.nodeFiles(nodeId);
if (!files.contains(GNUTELLA_FOLDER)) files.createDirectory(GNUTELLA_FOLDER);
this.gnutellaPeersByNodeId.set(node.id, new GnutellaPeer(this, node.id, gnutella));
this.gnutellaPeersByNodeId.set(nodeId, new GnutellaPeer(this, nodeId, snapshot.gnutellaByNodeId.get(nodeId)!));
return;
}
const emailClient = snapshot.emailClientsByNodeId.get(node.id);
if (emailClient !== undefined) {
const nodeId = node.id;
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, emailClient, (folders) => this.setEmailClientFolders(nodeId, folders)));
}
if ((node.kind === 'router' || node.kind === 'gateway') && !this.swiftRouters) this.startRouterWebServer(node.id, node.kind);
case 'emailClient':
this.emailClientsByNodeId.set(nodeId, new EmailClient(this, nodeId, snapshot.emailClientsByNodeId.get(nodeId)!, (folders) => this.setEmailClientFolders(nodeId, folders)));
return;
}
}
/**
* A program installed while the simulation runs (Java's software
* installation: installAndStartAppIfAvailable): its settings, and the
* folders and files it makes where the node has none of the name, as
* `source` has them — the document's snapshot after the installation —,
* and the program started. A program installed already is left as it is.
*/
installProgram(nodeId: Id, program: InstallableProgram, source: TopologySnapshot): void {
const state = this.stateValue;
if (state.phase !== 'running' || !state.topologySnapshot.nodes.some((node) => node.id === nodeId)) return;
const running = (RUNNING_PROGRAMS as readonly string[]).includes(program) ? (program as RunningProgram) : undefined;
if (running !== undefined && this.hasProgram(nodeId, running)) return;
// The node's files first: a node without any gets them now, in the snapshot taken after.
const files = this.nodeFiles(nodeId);
const snapshot = state.topologySnapshot;
const from = source.fileSystemsByNodeId.get(nodeId);
for (const file of installationFiles(program, snapshot.language)) {
if (files.contains(file.path) || from?.contains(file.path) !== true) continue;
const content = from.entry(file.path).content;
if (content.kind === 'directory') files.createDirectory(file.path, true);
else if (content.kind === 'text') files.writeTextFile(file.path, content.text, false);
else if (content.kind === 'image') files.writeImageFile(file.path, content.data, content.mediaType, false);
else files.writeBinaryFile(file.path, content.data, content.mediaType, false);
}
const copied = <T>(own: ReadonlyMap<Id, T>, theirs: ReadonlyMap<Id, T>): ReadonlyMap<Id, T> => {
const value = theirs.get(nodeId);
return value === undefined ? own : new Map([...own, [nodeId, value]]);
};
const added = (own: ReadonlySet<Id>, theirs: ReadonlySet<Id>): ReadonlySet<Id> => (theirs.has(nodeId) ? new Set([...own, nodeId]) : own);
state.topologySnapshot = withSnapshot(snapshot, {
echoServersByNodeId: program === 'echoServer' ? copied(snapshot.echoServersByNodeId, source.echoServersByNodeId) : snapshot.echoServersByNodeId,
simpleClientsByNodeId: program === 'simpleClient' ? copied(snapshot.simpleClientsByNodeId, source.simpleClientsByNodeId) : snapshot.simpleClientsByNodeId,
webServersByNodeId: program === 'webServer' ? copied(snapshot.webServersByNodeId, source.webServersByNodeId) : snapshot.webServersByNodeId,
webBrowserNodeIds: program === 'webBrowser' ? added(snapshot.webBrowserNodeIds, source.webBrowserNodeIds) : snapshot.webBrowserNodeIds,
emailServersByNodeId: program === 'emailServer' ? copied(snapshot.emailServersByNodeId, source.emailServersByNodeId) : snapshot.emailServersByNodeId,
emailClientsByNodeId: program === 'emailClient' ? copied(snapshot.emailClientsByNodeId, source.emailClientsByNodeId) : snapshot.emailClientsByNodeId,
terminalNodeIds: program === 'commandPrompt' ? added(snapshot.terminalNodeIds, source.terminalNodeIds) : snapshot.terminalNodeIds,
gnutellaByNodeId: program === 'gnutella' ? copied(snapshot.gnutellaByNodeId, source.gnutellaByNodeId) : snapshot.gnutellaByNodeId,
dnsServerConfigurationsByNodeId: program === 'dnsServer' ? copied(snapshot.dnsServerConfigurationsByNodeId, source.dnsServerConfigurationsByNodeId) : snapshot.dnsServerConfigurationsByNodeId,
firewallConfigurationsByNodeId: program === 'firewall' ? copied(snapshot.firewallConfigurationsByNodeId, source.firewallConfigurationsByNodeId) : snapshot.firewallConfigurationsByNodeId,
});
if (running !== undefined && this.hasProgram(nodeId, running)) this.startProgram(nodeId, running);
this.applicationThreads.resume();
}
/**
* A program removed while the simulation runs (entferneSoftware): a server
* is switched off, the program and its settings go; its files stay.
*/
removeProgram(nodeId: Id, program: InstallableProgram): void {
const state = this.stateValue;
if (state.phase !== 'running') return;
const snapshot = state.topologySnapshot;
const without = <T>(own: ReadonlyMap<Id, T>): ReadonlyMap<Id, T> => new Map([...own].filter(([id]) => id !== nodeId));
const withoutId = (own: ReadonlySet<Id>): ReadonlySet<Id> => new Set([...own].filter((id) => id !== nodeId));
switch (program) {
case 'echoServer':
this.echoServersByNodeId.get(nodeId)?.setActive(false);
this.echoServersByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { echoServersByNodeId: without(snapshot.echoServersByNodeId) });
break;
case 'simpleClient':
this.simpleClientsByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { simpleClientsByNodeId: without(snapshot.simpleClientsByNodeId) });
break;
case 'webServer':
this.webServersByNodeId.get(nodeId)?.setActive(false);
this.webServersByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { webServersByNodeId: without(snapshot.webServersByNodeId) });
break;
case 'webBrowser':
this.webBrowsersByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { webBrowserNodeIds: withoutId(snapshot.webBrowserNodeIds) });
break;
case 'emailServer':
this.emailServersByNodeId.get(nodeId)?.setActive(false);
this.emailServersByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { emailServersByNodeId: without(snapshot.emailServersByNodeId) });
break;
case 'emailClient':
this.emailClientsByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { emailClientsByNodeId: without(snapshot.emailClientsByNodeId) });
break;
case 'commandPrompt':
this.terminalsByNodeId.get(nodeId)?.interrupt();
this.terminalsByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { terminalNodeIds: withoutId(snapshot.terminalNodeIds) });
break;
case 'gnutella':
this.gnutellaPeersByNodeId.delete(nodeId);
state.topologySnapshot = withSnapshot(snapshot, { gnutellaByNodeId: without(snapshot.gnutellaByNodeId) });
break;
case 'dnsServer':
state.topologySnapshot = withSnapshot(snapshot, { dnsServerConfigurationsByNodeId: without(snapshot.dnsServerConfigurationsByNodeId) });
break;
case 'firewall':
state.topologySnapshot = withSnapshot(snapshot, { firewallConfigurationsByNodeId: without(snapshot.firewallConfigurationsByNodeId) });
break;
default:
break;
}
this.applicationThreads.resume();
}
/**
* VermittlungsrechnerBetriebssystem, GatewayFirmware: a router and a home
* router switch on their web server on port 80 when they start, with