Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Game Runner thread / subprocess improvements #571

Merged
merged 68 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
b9236e1
Work in Progress: add slf4j-test
keturn Apr 29, 2020
97a9cac
maintenance: convert TestGameRunner to using slf4j-test
keturn Apr 29, 2020
17a74e8
build.gradle: keep slf4j-api as an explicit dependency, but with flex…
keturn May 7, 2020
c6a7d6d
Work in Progress: RunGameTask comments
keturn Apr 28, 2020
bfed6b3
Work in Progress: RunGameTask tests?
keturn Apr 30, 2020
431bec3
Work in Progress: We're going to have a GameStarter after all.
keturn Apr 28, 2020
9e84bcd
extract an interface so we can provide a test implementation
keturn Apr 30, 2020
d3bbf91
Matcher `hasItemsFrom`
keturn Apr 28, 2020
f16526e
test: TestGameStarterWIP: junit4 → junit5
keturn May 3, 2020
fc8c7f5
TestRunGameTask: split junit4 vintage tests
keturn May 3, 2020
c5d6ec9
TestRunGameTask has learned a lot of things about tasks and processes!
keturn May 4, 2020
67a6fb6
TestRunGameTask streamlining
keturn May 4, 2020
0a38ce0
Work in Progress: TestRunGameTask thinking about cancellation and pro…
keturn May 4, 2020
16e2a5a
RunGameTask will throw an exception if the process fails to start or …
keturn May 5, 2020
e3c03e9
We don't have a well-defined spec for thread cancellation.
keturn May 5, 2020
d25b5f6
TestGameRunTask tidy
keturn May 5, 2020
6f4c4b6
RunGameTask sets its valuebean to True when it sees evidence of game …
keturn May 5, 2020
c28761d
Test process output parsing by providing a list of strings as an Inpu…
keturn May 6, 2020
d03adf0
Separate the in-memory mocks from the ones that spawn real processes …
keturn May 6, 2020
c369e98
idle thoughts and making checkstyle happier
keturn May 6, 2020
ae83f5e
RunGameTask: a START_MATCH that actually exists
keturn May 7, 2020
4caf3ba
swap out GameRunner for RunGameTask in ApplicationController
keturn May 7, 2020
020f7d3
done with the old GameStarter class
keturn May 8, 2020
c2b76d0
put new GameStarter in its place
keturn May 8, 2020
38fa977
removing old GameRunner and JUnit Vintage
keturn May 8, 2020
0883d01
hamcrest 1.x vs 2.x package naming conflicts
keturn May 8, 2020
3ee4bd7
upgrade game log output from TRACE to INFO
keturn May 8, 2020
ff45b5b
remove copied-from-superclass docstring
keturn May 8, 2020
397f7cd
push-validation: use monocle for headless TestFX
keturn May 9, 2020
f3bca0a
push-validation: is this how environment variables work?
keturn May 9, 2020
19bc1ed
GameService to get the nitty-gritty details of the thread factory out…
keturn May 9, 2020
2020f83
GradleGoo.prefers: provide Action<ResolutionStrategy> for a more grad…
keturn May 10, 2020
7d3eb77
ApplicationController: lift run result handlers to their own methods
keturn May 10, 2020
f89f1b7
ApplicationController: consider the run a success as soon as its valu…
keturn May 10, 2020
8d544c6
Service lets the Controller bind handlers just once.
keturn May 10, 2020
03bb551
GameService doesn't need to know about Package
keturn May 10, 2020
2655b32
GameService: simplify executor, as Service doesn't need it for its im…
keturn May 10, 2020
af7357f
build.grade: use more gradle-like prefers() signature
keturn May 10, 2020
ce704fa
Adopt FxTimer from ReactFx
keturn May 10, 2020
6b002a5
FxTimer: apply local style convention, remove unused methods
keturn May 10, 2020
c49c377
RunGameTask: declare success if the process outlives SURVIVAL_THRESHOLD.
keturn May 11, 2020
077ba1d
GameService: do the GameStarter construction here
keturn May 11, 2020
0082b7c
GameService: log exceptions
keturn May 11, 2020
a568d99
LogView: show logged exceptions
keturn May 11, 2020
d0cdae6
GameStarter: combine error stream with output stream
keturn May 11, 2020
71b3dbe
ApplicationController: select the log tab when the game doesn't start
keturn May 11, 2020
5250471
📓 docs for GameService
keturn May 11, 2020
8fd3151
📓 docs for GameStarter
keturn May 11, 2020
a964476
📓 docs for RunGameTask
keturn May 11, 2020
338ea23
📓 docs for StringIteratorInputStream
keturn May 11, 2020
a06a2c6
📓 docs for GameStarter had missing tags
keturn May 11, 2020
627d627
📓 ApplicationController#onSync's comment had become detached
keturn May 11, 2020
6e9d3e1
📓 FxTimer, in which I should never have deleted that <p>
keturn May 11, 2020
9efa232
maint(gradle): putting names on repos
keturn May 19, 2020
f2267df
doc(test): document Matcher.hasItemsFrom
keturn May 19, 2020
79fecfb
test(RunGameTask): muchly improve test comprehensibility!
keturn May 19, 2020
d43b8df
chore: no more need of MutableClock
keturn May 20, 2020
676da02
fix(RunGameTask): throw GameExitTooSoon if it quit early
keturn May 25, 2020
ec6efd7
test(RunGameTask): disable timers for not-timer-related tests
keturn May 25, 2020
4a82b4d
feat(RunGameTask): default message to string
keturn May 25, 2020
4df2589
fix(RunGameTask): clean up timer
keturn May 25, 2020
b91167d
fixup rebase on 01d42d7f8453e29f12e85d9d1656e05589dcbb23
keturn Jun 1, 2020
6667350
Merge remote-tracking branch 'origin/master' into game-start-snappy-5
keturn Sep 7, 2020
aa70a8b
Merge remote-tracking branch 'origin/master' into game-start-snappy-5
skaldarnar Oct 9, 2020
07cb1d8
Merge remote-tracking branch 'origin/master' into game-start-snappy-5
skaldarnar Oct 9, 2020
09859e7
build: set encoding to UTF-8
skaldarnar Oct 9, 2020
6693419
chore: use random UUID for "unavailable" command in tests
skaldarnar Oct 9, 2020
55c74d5
test: disable more tests on Windows
skaldarnar Oct 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/push-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ jobs:
java-version: 14
java-package: jdk
- name: Build and test with Gradle
env:
JAVA_TOOL_OPTIONS: -Dtestfx.robot=glass -Dtestfx.headless=true -Dprism.verbose=true
run: ./gradlew check
37 changes: 25 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,16 @@ repositories {
url "http://artifactory.terasology.org/artifactory/virtual-repo-live"
allowInsecureProtocol true
}
maven {
name "JitPack" // used by org.everit.json.schema
url "https://jitpack.io"
}
maven {
// for spf4j dependencies that haven't been merged upstream
// i.e. org.apache.avro:avro
name "spf4j dependencies"
url "https://dl.bintray.com/zolyfarkas/core"
}
maven {
name "JitPack" // used by org.everit.json.schema
url "https://jitpack.io"
}
}

// Primary dependencies definition
Expand Down Expand Up @@ -136,32 +137,44 @@ dependencies {
because "spf4j-slf4j-test:8.8 needs slf4j-api:1.x"
}

testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.6.+") {
because "we haven't yet replaced slf4j-test Runner with a junit5 Extension"
testImplementation("org.testfx:testfx-core:4.0.16-alpha") {
because "to test JavaFX Application"
// -alpha because that's the only kind of release they have?
}
testImplementation "org.testfx:testfx-junit5:4.0.16-alpha"

testImplementation("org.testfx:openjfx-monocle:jdk-12.0.1+2") {
// nobody's uploaded a jdk-14 build yet. does the jdk-12 one work?
because "CI builders are headless environments"
}
testImplementation "junit:junit:4.+"

// Config for our code analytics from: https://github.com/MovingBlocks/TeraConfig
codeMetrics group: 'org.terasology.config', name: 'codemetrics', version: '1.4.0', ext: 'zip'
}

configurations.matching({ it =~ ~/test(Runtime|Compile|Implementation)Classpath/ }).all {
resolutionStrategy(gradlegoo.prefers("logging", "jcl-api-capability", "jcl-over-slf4j", "jcl should prefer slf4j " +
"when available"));
resolutionStrategy(gradlegoo.prefers("logging", "jcl-api-capability", "jcl-over-slf4j",
"jcl should prefer slf4j when available"))
resolutionStrategy(gradlegoo.prefers("logging", "slf4j-impl-capability",
"spf4j-slf4j-test", "tests use slf4j-test"));
"spf4j-slf4j-test", "tests use slf4j-test"))

// old hamcrest versions exist only to spite us
// http://hamcrest.org/JavaHamcrest/distributables#upgrading-from-hamcrest-1x
exclude group: "org.hamcrest", module: "hamcrest-core"
exclude group: "org.hamcrest", module: "hamcrest-library"
}

// Set the expected module Java level (can use a higher Java to run, but should not use features from a higher Java)
sourceCompatibility = 1.11
targetCompatibility = 1.11

javafx {
version = "11.0.2"
version = "12.0.1"
modules = [
'javafx.graphics',
'javafx.fxml',
'javafx.web']
'javafx.web'
]
}

test {
Expand Down
66 changes: 0 additions & 66 deletions src/main/java/org/terasology/launcher/game/GameRunner.java

This file was deleted.

164 changes: 164 additions & 0 deletions src/main/java/org/terasology/launcher/game/GameService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright 2020 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.terasology.launcher.game;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import javafx.concurrent.Service;
import javafx.concurrent.Worker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.launcher.settings.BaseLauncherSettings;

import java.nio.file.Path;
import java.util.concurrent.Executors;

import static com.google.common.base.Verify.verifyNotNull;

/**
* This service starts and monitors the game process.
* <p>
* Its {@linkplain #GameService() constructor} requires no arguments. Use {@link #start(Path, BaseLauncherSettings)} to
* start the game process; the zero-argument form of {@code start()} will not have enough information.
* <p>
* The Boolean value of this service is true when it believes the game process has started <em>successfully.</em>
* There will be some time between when this service is started and when that value is set. It can be observed on
* {@link #valueProperty()} or retrieved as {@link #getValue()}.
* <p>
* The {@link Worker} interface of this Service provides a proxy to its current {@link RunGameTask}.
* Many of the methods the Worker interface defines are not used by this task type. In particular, the
* {@link Worker#progressProperty() progress} and {@link Worker#workDoneProperty() workDone} properties have no
* information for you that reflect the state of the game process.
* <p>
* This service will be in {@link State#RUNNING RUNNING} state as long as the game process is live. It enters
* {@link State#SUCCEEDED SUCCEEDED} after the game process exits with no error code, or {@link State#FAILED FAILED} if
* the game failed to start or it terminates with an error. Whether it succeeded or failed, the service will then
* reset to {@link State#READY READY} to be started again.
* <p>
* This service {@linkplain #cancel() does not support cancellation}.
* <ul>
* <li>For details on how the arguments to the process are constructed, see the source for {@link GameStarter}.
* <li>For details on how output from the game process is treated, see {@link RunGameTask}.
* </ul>
*/
public class GameService extends Service<Boolean> {
private static final Logger logger = LoggerFactory.getLogger(GameService.class);

private Path gamePath;
private BaseLauncherSettings settings;
keturn marked this conversation as resolved.
Show resolved Hide resolved

public GameService() {
setExecutor(Executors.newSingleThreadExecutor(
new ThreadFactoryBuilder()
.setNameFormat("GameService-%d")
.setDaemon(true)
.setUncaughtExceptionHandler(this::exceptionHandler)
.build()
));
}

/**
* Start a new game process with these settings.
*
* @param gamePath the directory under which we will find libs/Teresology.jar, also used as the process's
* working directory
* @param settings supplies other settings relevant to configuring a process
*/
@SuppressWarnings("checkstyle:HiddenField")
keturn marked this conversation as resolved.
Show resolved Hide resolved
public void start(Path gamePath, BaseLauncherSettings settings) {
this.gamePath = gamePath;
this.settings = settings;

start();
}

/**
* Use {@link #start(Path, BaseLauncherSettings)} instead.
* <p>
* It is an error to call this method before providing the configuration.
*/
@Override
public void start() {
super.start();
skaldarnar marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Cancellation is unsupported. Do not attempt this method.
* <p>
* Rationale: We do not terminate a running game process, and we don't want to lose our thread keeping track of
* the process while it's still live. If we did, our “is a game already running?” logic would fail.
* <p>
* If you are using this with some kind of generic Service Manager that always invokes this method as part of an
* orderly shutdown, that could be a good reason to change this from throwing an exception to logging a warning.
* Until then, this fails loudly so you won't call this method thinking it does something and then become confused
* when nothing happens.
*
* @throws UnsupportedOperationException always
*/
@Override
public boolean cancel() {
throw new UnsupportedOperationException("GameService does not cancel.");
}

/**
* Restarting is unsupported. Do not attempt this method.
* <p>
* See {@link #cancel()} for rationale.
*/
@Override
public void restart() {
super.restart();
skaldarnar marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Creates a new task to run the game with the current settings.
* <p>
* This class's configuration fields <em>must</em> be set before this is called.
*
* @throws com.google.common.base.VerifyException when fields are unset
*/
@Override
protected RunGameTask createTask() {
verifyNotNull(settings);
var starter = new GameStarter(verifyNotNull(gamePath), settings.getGameDataDirectory(),
settings.getMaxHeapSize(), settings.getInitialHeapSize(),
settings.getUserJavaParameterList(), settings.getUserGameParameterList(),
settings.getLogLevel());
return new RunGameTask(starter);
}

/** After a task completes, reset to ready for the next. */
@Override
protected void succeeded() {
reset(); // Ready to go again!
}

/** Checks to see if the failure left any exceptions behind, then resets to ready. */
@Override
protected void failed() {
// "Uncaught" exceptions from javafx's Task are actually caught and kept in a property,
// so if we want them logged we have to explicitly dig them out.
var error = getException();
if (error != null) {
exceptionHandler(Thread.currentThread(), error);
}
reset(); // Ready to try again!
}

private void exceptionHandler(Thread thread, Throwable thrown) {
logger.error("Unhandled exception", thrown);
}
}
Loading