aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml7
-rw-r--r--README.md19
-rw-r--r--buildSrc/src/main/groovy/refinery-java-application.gradle4
-rw-r--r--buildSrc/src/main/groovy/refinery-java-conventions.gradle14
-rw-r--r--gradle/libs.versions.toml8
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java77
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java20
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java16
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java2
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java18
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java9
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java31
-rw-r--r--subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java10
15 files changed, 148 insertions, 102 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 80d6b4f3..62f74f13 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -21,10 +21,11 @@ jobs:
21 uses: actions/checkout@v2 21 uses: actions/checkout@v2
22 with: 22 with:
23 fetch-depth: ${{ !steps.check-secret.outputs.is_SONAR_TOKEN_set && 1 || 0 }} # Shallow clones should be disabled for a better relevancy of SonarCloud analysis 23 fetch-depth: ${{ !steps.check-secret.outputs.is_SONAR_TOKEN_set && 1 || 0 }} # Shallow clones should be disabled for a better relevancy of SonarCloud analysis
24 - name: Set up JDK 17 24 - name: Set up JDK 19
25 uses: actions/setup-java@v1 25 uses: actions/setup-java@v2
26 with: 26 with:
27 java-version: 17 27 java-version: 19
28 distribution: temurin
28 - name: Cache Gradle packages 29 - name: Cache Gradle packages
29 uses: actions/cache@v2 30 uses: actions/cache@v2
30 with: 31 with:
diff --git a/README.md b/README.md
index dfb4685e..33d89786 100644
--- a/README.md
+++ b/README.md
@@ -6,27 +6,28 @@
6 6
7### With Eclipse IDE 7### With Eclipse IDE
8 8
91. Download and install a _Java 17_ compatible JDK. For Windows, prefer OpenJDK builds from [Adoptium](https://adoptium.net/). 91. Download and install a _Java 19_ compatible JDK. For Windows, prefer OpenJDK builds from [Adoptium](https://adoptium.net/).
10 10
112. Download and extract the [Eclipse IDE for Java and DSL Developers 2021-12](https://www.eclipse.org/downloads/packages/release/2021-12/r/eclipse-ide-java-and-dsl-developers) package. 112. Download and extract the [Eclipse IDE for Java and DSL Developers 2022-09](https://www.eclipse.org/downloads/packages/release/2022-09/r/eclipse-ide-java-and-dsl-developers) package.
12 12
133. Launch Eclipse and create a new workspace. 133. Launch Eclipse and create a new workspace.
14 14
154. Open _Help > Install New Software..._ and install the following software from the _2021-12_ update site: 154. Open _Help > Install New Software..._ and install the following software from the _2022-09_ update site:
16 * _Modeling > Ecore Diagram Editor (SDK)_ 16 * _Modeling > Ecore Diagram Editor (SDK)_
17 17
185. Open _Help > Eclipse Marketplace_ and install the following software: 185. Open _Help > Eclipse Marketplace_ and install the following software:
19 * _EclEmma Java Code Coverage_ 19 * _EclEmma Java Code Coverage_
20 * _Java 19 Support for Eclipse 2022-09 (4.25)_
20 * _SonarLint_ 21 * _SonarLint_
21 22
226. Open _Window > Preferences_ and set the following preferences: 236. Open _Window > Preferences_ and set the following preferences:
23 * _General > Workspace > Text file encoding_ should be _UTF-8_. 24 * _General > Workspace > Text file encoding_ should be _UTF-8_.
24 * _General > Workspace > New text file line delimiter_ should be _Unix_. 25 * _General > Workspace > New text file line delimiter_ should be _Unix_.
25 * Add the JDK 17 to _Java > Installed JREs_. 26 * Add the JDK 19 to _Java > Installed JREs_.
26 * Make sure JDK 17 is selected for _JavaSE-17_ at _Java > Installed JREs > Execution Environments_. 27 * Make sure JDK 19 is selected for _JavaSE-19_ at _Java > Installed JREs > Execution Environments_.
27 * Set _Gradle > Java home_ to the `JAVA_HOME` directory (the directory which contains the `bin` directory) of JDK 17. Here, Buildship will show a yellow warning sign, which can be safely ignored. 28 * Set _Gradle > Java home_ to the `JAVA_HOME` directory (the directory which contains the `bin` directory) of JDK 17. Here, Buildship will show a yellow warning sign, which can be safely ignored.
28 * Set _Java > Compiler > JDK Compliance > Compiler compliance level_ to _17_. The warning about using Java 16 system libraries during compilation should disappear. 29 * Set _Java > Compiler > JDK Compliance > Compiler compliance level_ to _19_. The warning about using Java 16 system libraries during compilation should disappear.
29 30
307. Clone the project Git repository but do not import it into Eclipse yet. 317. Clone the project Git repository but do not import it into Eclipse yet.
31 32
328. Open a new terminal an run `./gradlew prepareEclipse` (`.\gradlew prepareEclipse` on Windows) in the cloned repository. 338. Open a new terminal an run `./gradlew prepareEclipse` (`.\gradlew prepareEclipse` on Windows) in the cloned repository.
@@ -36,11 +37,11 @@
36 37
379. Select _File > Import... > Gradle > Existing Gradle Project_ and import the cloned repository in Eclipse. 389. Select _File > Import... > Gradle > Existing Gradle Project_ and import the cloned repository in Eclipse.
38 * Make sure to select the root of the repository (containing this file) as the _Project root directory_ and that the _Gradle distribution_ is _Gradle wrapper_. 39 * Make sure to select the root of the repository (containing this file) as the _Project root directory_ and that the _Gradle distribution_ is _Gradle wrapper_.
39 * If you have previously imported the project into Eclipse, this step will likely fail. In that case, you should remove the projects from Eclipse, run `git clean -fxd` in the repository, and start over from step 8. 40 * If you have previously imported the project into Eclipse, this step will likely fail. In that case, you should remove the projects from Eclipse, run `git clean -fxd` in the repository, and start over from step 8.
40 41
41### With IntelliJ IDEA 42### With IntelliJ IDEA
42 43
43It is possible to import the project into IntelliJ IDEA, but it gives no editing help for Xtext (`*.xtext`), MWE2 (`*.mwe2`), and Xtend (`*.xtend`) and Ecore class diagrams (`*.aird`, `*.ecore`, `*.genmodel`). 44It is possible to import the project into IntelliJ IDEA, but it gives no editing help for Xtext (`*.xtext`), MWE2 (`*.mwe2`), and Xtend (`*.xtend`) and Ecore class diagrams (`*.aird`, `*.ecore`, `*.genmodel`).
44 45
45## License 46## License
46 47
diff --git a/buildSrc/src/main/groovy/refinery-java-application.gradle b/buildSrc/src/main/groovy/refinery-java-application.gradle
index c38ccdb3..9abfc2b3 100644
--- a/buildSrc/src/main/groovy/refinery-java-application.gradle
+++ b/buildSrc/src/main/groovy/refinery-java-application.gradle
@@ -4,6 +4,10 @@ plugins {
4 id 'refinery-java-conventions' 4 id 'refinery-java-conventions'
5} 5}
6 6
7application {
8 applicationDefaultJvmArgs += '--enable-preview'
9}
10
7for (taskName in ['distTar', 'distZip', 'shadowDistTar', 'shadowDistZip']) { 11for (taskName in ['distTar', 'distZip', 'shadowDistTar', 'shadowDistZip']) {
8 tasks.named(taskName) { 12 tasks.named(taskName) {
9 enabled = false 13 enabled = false
diff --git a/buildSrc/src/main/groovy/refinery-java-conventions.gradle b/buildSrc/src/main/groovy/refinery-java-conventions.gradle
index b95153ce..eedefdf8 100644
--- a/buildSrc/src/main/groovy/refinery-java-conventions.gradle
+++ b/buildSrc/src/main/groovy/refinery-java-conventions.gradle
@@ -21,7 +21,7 @@ dependencies {
21} 21}
22 22
23java.toolchain { 23java.toolchain {
24 languageVersion = JavaLanguageVersion.of(17) 24 languageVersion = JavaLanguageVersion.of(19)
25} 25}
26 26
27def jacocoTestReport = tasks.named('jacocoTestReport') 27def jacocoTestReport = tasks.named('jacocoTestReport')
@@ -53,6 +53,18 @@ tasks.named('jar') {
53 } 53 }
54} 54}
55 55
56tasks.withType(JavaCompile) {
57 options.compilerArgs += '--enable-preview'
58}
59
60tasks.withType(Test) {
61 jvmArgs += '--enable-preview'
62}
63
64tasks.withType(JavaExec) {
65 jvmArgs += '--enable-preview'
66}
67
56def generateEclipseSourceFolders = tasks.register('generateEclipseSourceFolders') 68def generateEclipseSourceFolders = tasks.register('generateEclipseSourceFolders')
57 69
58tasks.register('prepareEclipse') { 70tasks.register('prepareEclipse') {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d8b3d4d8..e8b7f5dd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
1[versions] 1[versions]
2eclipseCollections = "11.1.0" 2eclipseCollections = "11.1.0"
3jetty = "11.0.12" 3jetty = "12.0.0.alpha2"
4jmh = "1.36" 4jmh = "1.36"
5junit = "5.9.1" 5junit = "5.9.1"
6mockito = "4.9.0" 6mockito = "4.9.0"
@@ -19,9 +19,9 @@ gradlePlugin-shadow = { group = "gradle.plugin.com.github.johnrengelman", name =
19gradlePlugin-sonarqube = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version = "3.3" } 19gradlePlugin-sonarqube = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version = "3.3" }
20hamcrest = { group = "org.hamcrest", name = "hamcrest", version = "2.2" } 20hamcrest = { group = "org.hamcrest", name = "hamcrest", version = "2.2" }
21jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref = "jetty" } 21jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref = "jetty" }
22jetty-servlet = { group = "org.eclipse.jetty", name = "jetty-servlet", version.ref = "jetty" } 22jetty-servlet = { group = "org.eclipse.jetty.ee10", name = "jetty-ee10-servlet", version.ref = "jetty" }
23jetty-websocket-client = { group = "org.eclipse.jetty.websocket", name = "websocket-jetty-client", version.ref = "jetty" } 23jetty-websocket-client = { group = "org.eclipse.jetty.ee10.websocket", name = "jetty-ee10-websocket-jetty-client", version.ref = "jetty" }
24jetty-websocket-server = { group = "org.eclipse.jetty.websocket", name = "websocket-jetty-server", version.ref = "jetty" } 24jetty-websocket-server = { group = "org.eclipse.jetty.ee10.websocket", name = "jetty-ee10-websocket-jetty-server", version.ref = "jetty" }
25jmh-core = { group = "org.openjdk.jmh", name = "jmh-core", version.ref = "jmh" } 25jmh-core = { group = "org.openjdk.jmh", name = "jmh-core", version.ref = "jmh" }
26jmh-annprocess = { group = "org.openjdk.jmh", name = "jmh-generator-annprocess", version.ref = "jmh" } 26jmh-annprocess = { group = "org.openjdk.jmh", name = "jmh-generator-annprocess", version.ref = "jmh" }
27junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } 27junit-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ae04661e..f88321a2 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
1distributionBase=GRADLE_USER_HOME 1distributionBase=GRADLE_USER_HOME
2distributionPath=wrapper/dists 2distributionPath=wrapper/dists
3distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 3distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-rc-4-bin.zip
4zipStoreBase=GRADLE_USER_HOME 4zipStoreBase=GRADLE_USER_HOME
5zipStorePath=wrapper/dists 5zipStorePath=wrapper/dists
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
index ec55036f..706413a9 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebModule.java
@@ -3,12 +3,13 @@
3 */ 3 */
4package tools.refinery.language.web; 4package tools.refinery.language.web;
5 5
6import org.eclipse.xtext.ide.ExecutorServiceProvider;
6import org.eclipse.xtext.web.server.XtextServiceDispatcher; 7import org.eclipse.xtext.web.server.XtextServiceDispatcher;
7import org.eclipse.xtext.web.server.model.IWebDocumentProvider; 8import org.eclipse.xtext.web.server.model.IWebDocumentProvider;
8import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess; 9import org.eclipse.xtext.web.server.model.XtextWebDocumentAccess;
9import org.eclipse.xtext.web.server.occurrences.OccurrencesService; 10import org.eclipse.xtext.web.server.occurrences.OccurrencesService;
10
11import tools.refinery.language.web.occurrences.ProblemOccurrencesService; 11import tools.refinery.language.web.occurrences.ProblemOccurrencesService;
12import tools.refinery.language.web.xtext.VirtualThreadExecutorServiceProvider;
12import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher; 13import tools.refinery.language.web.xtext.server.push.PushServiceDispatcher;
13import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; 14import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess;
14import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider; 15import tools.refinery.language.web.xtext.server.push.PushWebDocumentProvider;
@@ -20,16 +21,20 @@ public class ProblemWebModule extends AbstractProblemWebModule {
20 public Class<? extends IWebDocumentProvider> bindIWebDocumentProvider() { 21 public Class<? extends IWebDocumentProvider> bindIWebDocumentProvider() {
21 return PushWebDocumentProvider.class; 22 return PushWebDocumentProvider.class;
22 } 23 }
23 24
24 public Class<? extends XtextWebDocumentAccess> bindXtextWebDocumentAccess() { 25 public Class<? extends XtextWebDocumentAccess> bindXtextWebDocumentAccess() {
25 return PushWebDocumentAccess.class; 26 return PushWebDocumentAccess.class;
26 } 27 }
27 28
28 public Class<? extends XtextServiceDispatcher> bindXtextServiceDispatcher() { 29 public Class<? extends XtextServiceDispatcher> bindXtextServiceDispatcher() {
29 return PushServiceDispatcher.class; 30 return PushServiceDispatcher.class;
30 } 31 }
31 32
32 public Class<? extends OccurrencesService> bindOccurrencesService() { 33 public Class<? extends OccurrencesService> bindOccurrencesService() {
33 return ProblemOccurrencesService.class; 34 return ProblemOccurrencesService.class;
34 } 35 }
36
37 public Class<? extends ExecutorServiceProvider> bindExecutorServiceProvider() {
38 return VirtualThreadExecutorServiceProvider.class;
39 }
35} 40}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
index 58c8ea4e..5da16850 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
@@ -5,20 +5,21 @@ package tools.refinery.language.web;
5 5
6import jakarta.servlet.DispatcherType; 6import jakarta.servlet.DispatcherType;
7import jakarta.servlet.SessionTrackingMode; 7import jakarta.servlet.SessionTrackingMode;
8import org.eclipse.jetty.ee10.servlet.DefaultServlet;
9import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
10import org.eclipse.jetty.ee10.servlet.ServletHolder;
11import org.eclipse.jetty.ee10.servlet.SessionHandler;
12import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
8import org.eclipse.jetty.server.Server; 13import org.eclipse.jetty.server.Server;
9import org.eclipse.jetty.server.session.SessionHandler; 14import org.eclipse.jetty.util.VirtualThreads;
10import org.eclipse.jetty.servlet.DefaultServlet;
11import org.eclipse.jetty.servlet.ServletContextHandler;
12import org.eclipse.jetty.servlet.ServletHolder;
13import org.eclipse.jetty.util.resource.Resource; 15import org.eclipse.jetty.util.resource.Resource;
14import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; 16import org.eclipse.jetty.util.resource.ResourceFactory;
15import org.slf4j.Logger; 17import org.slf4j.Logger;
16import org.slf4j.LoggerFactory; 18import org.slf4j.LoggerFactory;
17import tools.refinery.language.web.config.BackendConfigServlet; 19import tools.refinery.language.web.config.BackendConfigServlet;
18import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; 20import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
19 21
20import java.io.File; 22import java.io.File;
21import java.io.IOException;
22import java.net.InetSocketAddress; 23import java.net.InetSocketAddress;
23import java.net.URI; 24import java.net.URI;
24import java.net.URISyntaxException; 25import java.net.URISyntaxException;
@@ -42,13 +43,18 @@ public class ServerLauncher {
42 43
43 private final Server server; 44 private final Server server;
44 45
45 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, String[] allowedOrigins, 46 public ServerLauncher(InetSocketAddress bindAddress, String[] allowedOrigins, String webSocketUrl) {
46 String webSocketUrl) {
47 server = new Server(bindAddress); 47 server = new Server(bindAddress);
48 if (server.getThreadPool() instanceof VirtualThreads.Configurable virtualThreadsConfigurable) {
49 // Change this to setVirtualThreadsExecutor once
50 // https://github.com/eclipse/jetty.project/commit/83154b4ffe4767ef44981598d6c26e6a5d32e57c gets released.
51 virtualThreadsConfigurable.setUseVirtualThreads(VirtualThreads.areSupported());
52 }
48 var handler = new ServletContextHandler(); 53 var handler = new ServletContextHandler();
49 addSessionHandler(handler); 54 addSessionHandler(handler);
50 addProblemServlet(handler, allowedOrigins); 55 addProblemServlet(handler, allowedOrigins);
51 addBackendConfigServlet(handler, webSocketUrl); 56 addBackendConfigServlet(handler, webSocketUrl);
57 var baseResource = getBaseResource();
52 if (baseResource != null) { 58 if (baseResource != null) {
53 handler.setBaseResource(baseResource); 59 handler.setBaseResource(baseResource);
54 handler.setWelcomeFiles(new String[]{"index.html"}); 60 handler.setWelcomeFiles(new String[]{"index.html"});
@@ -95,6 +101,35 @@ public class ServerLauncher {
95 handler.addServlet(defaultServletHolder, "/"); 101 handler.addServlet(defaultServletHolder, "/");
96 } 102 }
97 103
104 private Resource getBaseResource() {
105 var factory = ResourceFactory.of(server);
106 var baseResourceOverride = System.getenv("BASE_RESOURCE");
107 if (baseResourceOverride != null) {
108 // If a user override is provided, use it.
109 return factory.newResource(baseResourceOverride);
110 }
111 var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html");
112 if (indexUrlInJar != null) {
113 // If the app is packaged in the jar, serve it.
114 URI webRootUri = null;
115 try {
116 webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/"));
117 } catch (URISyntaxException e) {
118 throw new IllegalStateException("Jar has invalid base resource URI", e);
119 }
120 return factory.newResource(webRootUri);
121 }
122 // Look for unpacked production artifacts (convenience for running from IDE).
123 var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack",
124 "production"};
125 var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents));
126 if (unpackedResourceDir.isDirectory()) {
127 return factory.newResource(unpackedResourceDir.toPath());
128 }
129 // Fall back to just serving a 404.
130 return null;
131 }
132
98 public void start() throws Exception { 133 public void start() throws Exception {
99 server.start(); 134 server.start();
100 LOG.info("Server started on {}", server.getURI()); 135 LOG.info("Server started on {}", server.getURI());
@@ -104,10 +139,9 @@ public class ServerLauncher {
104 public static void main(String[] args) { 139 public static void main(String[] args) {
105 try { 140 try {
106 var bindAddress = getBindAddress(); 141 var bindAddress = getBindAddress();
107 var baseResource = getBaseResource();
108 var allowedOrigins = getAllowedOrigins(); 142 var allowedOrigins = getAllowedOrigins();
109 var webSocketUrl = getWebSocketUrl(); 143 var webSocketUrl = getWebSocketUrl();
110 var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins, webSocketUrl); 144 var serverLauncher = new ServerLauncher(bindAddress, allowedOrigins, webSocketUrl);
111 serverLauncher.start(); 145 serverLauncher.start();
112 } catch (Exception exception) { 146 } catch (Exception exception) {
113 LOG.error("Fatal server error", exception); 147 LOG.error("Fatal server error", exception);
@@ -137,29 +171,6 @@ public class ServerLauncher {
137 return new InetSocketAddress(listenAddress, listenPort); 171 return new InetSocketAddress(listenAddress, listenPort);
138 } 172 }
139 173
140 private static Resource getBaseResource() throws IOException, URISyntaxException {
141 var baseResourceOverride = System.getenv("BASE_RESOURCE");
142 if (baseResourceOverride != null) {
143 // If a user override is provided, use it.
144 return Resource.newResource(baseResourceOverride);
145 }
146 var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html");
147 if (indexUrlInJar != null) {
148 // If the app is packaged in the jar, serve it.
149 var webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/"));
150 return Resource.newResource(webRootUri);
151 }
152 // Look for unpacked production artifacts (convenience for running from IDE).
153 var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack",
154 "production"};
155 var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents));
156 if (unpackedResourceDir.isDirectory()) {
157 return Resource.newResource(unpackedResourceDir);
158 }
159 // Fall back to just serving a 404.
160 return null;
161 }
162
163 private static String getPublicHost() { 174 private static String getPublicHost() {
164 var publicHost = System.getenv("PUBLIC_HOST"); 175 var publicHost = System.getenv("PUBLIC_HOST");
165 if (publicHost != null) { 176 if (publicHost != null) {
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java
new file mode 100644
index 00000000..ead98927
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/VirtualThreadExecutorServiceProvider.java
@@ -0,0 +1,20 @@
1package tools.refinery.language.web.xtext;
2
3import org.eclipse.xtext.ide.ExecutorServiceProvider;
4
5import java.util.concurrent.ExecutorService;
6import java.util.concurrent.Executors;
7
8public class VirtualThreadExecutorServiceProvider extends ExecutorServiceProvider {
9 private static final String THREAD_POOL_NAME = "xtextWeb";
10
11 @Override
12 protected ExecutorService createInstance(String key) {
13 var name = key == null ? THREAD_POOL_NAME : THREAD_POOL_NAME + "-" + key;
14 return Executors.newThreadPerTaskExecutor(Thread.ofVirtual()
15 .allowSetThreadLocals(true)
16 .inheritInheritableThreadLocals(false)
17 .name(name + "-", 0)
18 .factory());
19 }
20}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
index 82391d8b..1d9e0463 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
@@ -3,10 +3,10 @@ package tools.refinery.language.web.xtext.servlet;
3import com.google.gson.Gson; 3import com.google.gson.Gson;
4import com.google.gson.JsonIOException; 4import com.google.gson.JsonIOException;
5import com.google.gson.JsonParseException; 5import com.google.gson.JsonParseException;
6import org.eclipse.jetty.websocket.api.Session; 6import org.eclipse.jetty.ee10.websocket.api.Session;
7import org.eclipse.jetty.websocket.api.StatusCode; 7import org.eclipse.jetty.ee10.websocket.api.StatusCode;
8import org.eclipse.jetty.websocket.api.WriteCallback; 8import org.eclipse.jetty.ee10.websocket.api.WriteCallback;
9import org.eclipse.jetty.websocket.api.annotations.*; 9import org.eclipse.jetty.ee10.websocket.api.annotations.*;
10import org.eclipse.xtext.resource.IResourceServiceProvider; 10import org.eclipse.xtext.resource.IResourceServiceProvider;
11import org.eclipse.xtext.web.server.ISession; 11import org.eclipse.xtext.web.server.ISession;
12import org.slf4j.Logger; 12import org.slf4j.Logger;
@@ -17,7 +17,6 @@ import tools.refinery.language.web.xtext.server.TransactionExecutor;
17import tools.refinery.language.web.xtext.server.message.XtextWebRequest; 17import tools.refinery.language.web.xtext.server.message.XtextWebRequest;
18import tools.refinery.language.web.xtext.server.message.XtextWebResponse; 18import tools.refinery.language.web.xtext.server.message.XtextWebResponse;
19 19
20import java.io.IOException;
21import java.io.Reader; 20import java.io.Reader;
22 21
23@WebSocket 22@WebSocket
@@ -108,12 +107,7 @@ public class XtextWebSocket implements WriteCallback, ResponseHandler {
108 throw new ResponseHandlerException("Trying to send message when websocket is disconnected"); 107 throw new ResponseHandlerException("Trying to send message when websocket is disconnected");
109 } 108 }
110 var responseString = gson.toJson(response); 109 var responseString = gson.toJson(response);
111 try { 110 webSocketSession.getRemote().sendPartialString(responseString, true, this);
112 webSocketSession.getRemote().sendPartialString(responseString, true, this);
113 } catch (IOException e) {
114 throw new ResponseHandlerException(
115 "Cannot initiate async write to websocket " + webSocketSession.getRemoteAddress(), e);
116 }
117 } 111 }
118 112
119 @Override 113 @Override
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
index a2ad2943..9a32b937 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
@@ -2,7 +2,7 @@ package tools.refinery.language.web.xtext.servlet;
2 2
3import jakarta.servlet.ServletConfig; 3import jakarta.servlet.ServletConfig;
4import jakarta.servlet.ServletException; 4import jakarta.servlet.ServletException;
5import org.eclipse.jetty.websocket.server.*; 5import org.eclipse.jetty.ee10.websocket.server.*;
6import org.eclipse.xtext.resource.IResourceServiceProvider; 6import org.eclipse.xtext.resource.IResourceServiceProvider;
7import org.slf4j.Logger; 7import org.slf4j.Logger;
8import org.slf4j.LoggerFactory; 8import org.slf4j.LoggerFactory;
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
index 652fc13b..6dfce780 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/ProblemWebSocketServletIntegrationTest.java
@@ -1,17 +1,17 @@
1package tools.refinery.language.web; 1package tools.refinery.language.web;
2 2
3import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
4import org.eclipse.jetty.ee10.servlet.ServletHolder;
5import org.eclipse.jetty.ee10.websocket.api.Session;
6import org.eclipse.jetty.ee10.websocket.api.StatusCode;
7import org.eclipse.jetty.ee10.websocket.api.annotations.WebSocket;
8import org.eclipse.jetty.ee10.websocket.api.exceptions.UpgradeException;
9import org.eclipse.jetty.ee10.websocket.client.ClientUpgradeRequest;
10import org.eclipse.jetty.ee10.websocket.client.WebSocketClient;
11import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
3import org.eclipse.jetty.http.HttpHeader; 12import org.eclipse.jetty.http.HttpHeader;
4import org.eclipse.jetty.http.HttpStatus; 13import org.eclipse.jetty.http.HttpStatus;
5import org.eclipse.jetty.server.Server; 14import org.eclipse.jetty.server.Server;
6import org.eclipse.jetty.servlet.ServletContextHandler;
7import org.eclipse.jetty.servlet.ServletHolder;
8import org.eclipse.jetty.websocket.api.Session;
9import org.eclipse.jetty.websocket.api.StatusCode;
10import org.eclipse.jetty.websocket.api.annotations.WebSocket;
11import org.eclipse.jetty.websocket.api.exceptions.UpgradeException;
12import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
13import org.eclipse.jetty.websocket.client.WebSocketClient;
14import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
15import org.eclipse.xtext.testing.GlobalRegistries; 15import org.eclipse.xtext.testing.GlobalRegistries;
16import org.eclipse.xtext.testing.GlobalRegistries.GlobalStateMemento; 16import org.eclipse.xtext.testing.GlobalRegistries.GlobalStateMemento;
17import org.junit.jupiter.api.AfterEach; 17import org.junit.jupiter.api.AfterEach;
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java
index b70d0ed5..ebf36f13 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/AwaitTerminationExecutorServiceProvider.java
@@ -1,16 +1,15 @@
1package tools.refinery.language.web.tests; 1package tools.refinery.language.web.tests;
2 2
3import com.google.inject.Singleton;
4import org.eclipse.xtext.ide.ExecutorServiceProvider;
5
3import java.util.ArrayList; 6import java.util.ArrayList;
4import java.util.List; 7import java.util.List;
5import java.util.concurrent.ExecutorService; 8import java.util.concurrent.ExecutorService;
6 9
7import org.eclipse.xtext.ide.ExecutorServiceProvider;
8
9import com.google.inject.Singleton;
10
11@Singleton 10@Singleton
12public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider { 11public class AwaitTerminationExecutorServiceProvider extends ExecutorServiceProvider {
13 private List<RestartableCachedThreadPool> servicesToShutDown = new ArrayList<>(); 12 private final List<RestartableCachedThreadPool> servicesToShutDown = new ArrayList<>();
14 13
15 @Override 14 @Override
16 protected ExecutorService createInstance(String key) { 15 protected ExecutorService createInstance(String key) {
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
index 1468273d..8e5038ae 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/RestartableCachedThreadPool.java
@@ -1,45 +1,44 @@
1package tools.refinery.language.web.tests; 1package tools.refinery.language.web.tests;
2 2
3import java.util.Collection;
4import java.util.List;
5import java.util.concurrent.Callable;
6import java.util.concurrent.ExecutionException;
7import java.util.concurrent.ExecutorService;
8import java.util.concurrent.Executors;
9import java.util.concurrent.Future;
10import java.util.concurrent.TimeUnit;
11import java.util.concurrent.TimeoutException;
12
13import org.slf4j.Logger; 3import org.slf4j.Logger;
14import org.slf4j.LoggerFactory; 4import org.slf4j.LoggerFactory;
15 5
6import java.util.Collection;
7import java.util.List;
8import java.util.concurrent.*;
9
10@SuppressWarnings("NullableProblems")
16public class RestartableCachedThreadPool implements ExecutorService { 11public class RestartableCachedThreadPool implements ExecutorService {
17 private static final Logger LOG = LoggerFactory.getLogger(RestartableCachedThreadPool.class); 12 private static final Logger LOG = LoggerFactory.getLogger(RestartableCachedThreadPool.class);
18 13
19 private ExecutorService delegate; 14 private ExecutorService delegate;
20 15
21 public RestartableCachedThreadPool() { 16 public RestartableCachedThreadPool() {
22 delegate = createExecutorService(); 17 delegate = createExecutorService();
23 } 18 }
24 19
25 public void waitForAllTasksToFinish() { 20 public void waitForAllTasksToFinish() {
26 delegate.shutdown(); 21 delegate.shutdown();
27 waitForTermination(); 22 waitForTermination();
28 delegate = createExecutorService(); 23 delegate = createExecutorService();
29 } 24 }
30 25
31 public void waitForTermination() { 26 public void waitForTermination() {
27 boolean result = false;
32 try { 28 try {
33 delegate.awaitTermination(1, TimeUnit.SECONDS); 29 result = delegate.awaitTermination(1, TimeUnit.SECONDS);
34 } catch (InterruptedException e) { 30 } catch (InterruptedException e) {
35 LOG.warn("Interrupted while waiting for delegate executor to stop", e); 31 LOG.warn("Interrupted while waiting for delegate executor to stop", e);
36 } 32 }
33 if (!result) {
34 throw new IllegalStateException("Failed to shut down Xtext thread pool");
35 }
37 } 36 }
38 37
39 protected ExecutorService createExecutorService() { 38 protected ExecutorService createExecutorService() {
40 return Executors.newCachedThreadPool(); 39 return Executors.newCachedThreadPool();
41 } 40 }
42 41
43 @Override 42 @Override
44 public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException { 43 public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException {
45 return delegate.awaitTermination(arg0, arg1); 44 return delegate.awaitTermination(arg0, arg1);
diff --git a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java
index 74695c9a..f19c10ca 100644
--- a/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java
+++ b/subprojects/language-web/src/test/java/tools/refinery/language/web/tests/WebSocketIntegrationTestClient.java
@@ -1,10 +1,10 @@
1package tools.refinery.language.web.tests; 1package tools.refinery.language.web.tests;
2 2
3import org.eclipse.jetty.websocket.api.Session; 3import org.eclipse.jetty.ee10.websocket.api.Session;
4import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; 4import org.eclipse.jetty.ee10.websocket.api.annotations.OnWebSocketClose;
5import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; 5import org.eclipse.jetty.ee10.websocket.api.annotations.OnWebSocketConnect;
6import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; 6import org.eclipse.jetty.ee10.websocket.api.annotations.OnWebSocketError;
7import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; 7import org.eclipse.jetty.ee10.websocket.api.annotations.OnWebSocketMessage;
8 8
9import java.io.IOException; 9import java.io.IOException;
10import java.time.Duration; 10import java.time.Duration;