Add managed local CV worker startup
This commit is contained in:
parent
9dc720b0cc
commit
e2f49e2e5a
|
|
@ -4,16 +4,17 @@ This service can use a local computer-vision worker for highlight visual analysi
|
|||
|
||||
## Default Behavior
|
||||
|
||||
By default the service uses the built-in heuristic provider:
|
||||
The packaged `application.yml` defaults to `local-cv` with heuristic fallback enabled:
|
||||
|
||||
```yaml
|
||||
video-clipping:
|
||||
editing:
|
||||
visual-analysis:
|
||||
provider: heuristic
|
||||
provider: local-cv
|
||||
fallback-to-heuristic: true
|
||||
```
|
||||
|
||||
This keeps the app runnable without Python, GPU drivers, model downloads, or a separate service.
|
||||
This keeps the app runnable when the local worker is unavailable, but visual analysis is stronger when the worker is running.
|
||||
|
||||
## Enable A Local CV Worker
|
||||
|
||||
|
|
@ -27,6 +28,10 @@ video-clipping:
|
|||
endpoint: http://127.0.0.1:8091/v1/analyze-visuals
|
||||
timeout-ms: 30000
|
||||
fallback-to-heuristic: true
|
||||
local-cv-worker:
|
||||
auto-start: false
|
||||
script: ./tools/run_local_cv_worker.sh
|
||||
startup-wait-ms: 0
|
||||
```
|
||||
|
||||
Equivalent environment variables:
|
||||
|
|
@ -36,6 +41,9 @@ VIDEO_EDITING_VISUAL_ANALYSIS_PROVIDER=local-cv
|
|||
VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT=http://127.0.0.1:8091/v1/analyze-visuals
|
||||
VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS=30000
|
||||
VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC=true
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START=false
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_SCRIPT=./tools/run_local_cv_worker.sh
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS=0
|
||||
```
|
||||
|
||||
This repository includes an optional starter worker:
|
||||
|
|
@ -60,6 +68,20 @@ Health check:
|
|||
curl http://127.0.0.1:8091/health
|
||||
```
|
||||
|
||||
## Spring-Managed Worker Startup
|
||||
|
||||
The Spring application can start the local CV worker for local runs:
|
||||
|
||||
```bash
|
||||
VIDEO_EDITING_VISUAL_ANALYSIS_PROVIDER=local-cv \
|
||||
VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START=true \
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
When `auto-start` is enabled, Spring starts `tools/run_local_cv_worker.sh`, derives `LOCAL_CV_HOST` and `LOCAL_CV_PORT` from the configured visual-analysis endpoint, streams worker output into application logs, and stops the worker process during application shutdown.
|
||||
|
||||
Keep `auto-start` disabled in production unless the app host is intended to own the Python worker lifecycle. The first run can take time because the script creates `.venv-local-cv`, installs Python dependencies, and may download/cache the configured YOLO model.
|
||||
|
||||
## Worker Request Contract
|
||||
|
||||
The Spring service sends:
|
||||
|
|
|
|||
|
|
@ -858,6 +858,8 @@ public class VideoClippingProperties {
|
|||
|
||||
private boolean fallbackToHeuristic = true;
|
||||
|
||||
private final LocalCvWorker localCvWorker = new LocalCvWorker();
|
||||
|
||||
public String getProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
|
@ -889,6 +891,43 @@ public class VideoClippingProperties {
|
|||
public void setFallbackToHeuristic(boolean fallbackToHeuristic) {
|
||||
this.fallbackToHeuristic = fallbackToHeuristic;
|
||||
}
|
||||
|
||||
public LocalCvWorker getLocalCvWorker() {
|
||||
return localCvWorker;
|
||||
}
|
||||
|
||||
public static class LocalCvWorker {
|
||||
private boolean autoStart = false;
|
||||
|
||||
private String script = "./tools/run_local_cv_worker.sh";
|
||||
|
||||
@Min(0)
|
||||
private long startupWaitMs = 0;
|
||||
|
||||
public boolean isAutoStart() {
|
||||
return autoStart;
|
||||
}
|
||||
|
||||
public void setAutoStart(boolean autoStart) {
|
||||
this.autoStart = autoStart;
|
||||
}
|
||||
|
||||
public String getScript() {
|
||||
return script;
|
||||
}
|
||||
|
||||
public void setScript(String script) {
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
public long getStartupWaitMs() {
|
||||
return startupWaitMs;
|
||||
}
|
||||
|
||||
public void setStartupWaitMs(long startupWaitMs) {
|
||||
this.startupWaitMs = startupWaitMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LocalDirector {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
package org.example.videoclips.editing;
|
||||
|
||||
import org.example.videoclips.config.VideoClippingProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "video-clipping.editing.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class LocalCvWorkerProcessManager implements SmartLifecycle {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalCvWorkerProcessManager.class);
|
||||
|
||||
private final VideoClippingProperties.Editing.VisualAnalysis visualAnalysis;
|
||||
private final ProcessLauncher processLauncher;
|
||||
private Process process;
|
||||
private Thread outputThread;
|
||||
private boolean running;
|
||||
|
||||
@Autowired
|
||||
public LocalCvWorkerProcessManager(VideoClippingProperties properties) {
|
||||
this(properties, ProcessBuilder::start);
|
||||
}
|
||||
|
||||
LocalCvWorkerProcessManager(
|
||||
VideoClippingProperties properties,
|
||||
ProcessLauncher processLauncher
|
||||
) {
|
||||
this.visualAnalysis = properties.getEditing().getVisualAnalysis();
|
||||
this.processLauncher = processLauncher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void start() {
|
||||
if (running || !shouldStart()) {
|
||||
return;
|
||||
}
|
||||
|
||||
VideoClippingProperties.Editing.VisualAnalysis.LocalCvWorker worker = visualAnalysis.getLocalCvWorker();
|
||||
Path script = Path.of(worker.getScript()).toAbsolutePath().normalize();
|
||||
if (!Files.isRegularFile(script)) {
|
||||
throw new IllegalStateException("Local CV worker script does not exist: " + script);
|
||||
}
|
||||
|
||||
Endpoint endpoint = endpoint();
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(script.toString())
|
||||
.redirectErrorStream(true);
|
||||
Map<String, String> environment = processBuilder.environment();
|
||||
environment.put("LOCAL_CV_HOST", endpoint.host());
|
||||
environment.put("LOCAL_CV_PORT", Integer.toString(endpoint.port()));
|
||||
|
||||
try {
|
||||
process = processLauncher.start(processBuilder);
|
||||
running = true;
|
||||
outputThread = outputReader(process.getInputStream(), script);
|
||||
outputThread.start();
|
||||
log.info("event=local_cv_worker_started script={} pid={} host={} port={}",
|
||||
script, process.pid(), endpoint.host(), endpoint.port());
|
||||
waitForStartupWindow(worker.getStartupWaitMs());
|
||||
} catch (IOException ex) {
|
||||
running = false;
|
||||
throw new IllegalStateException("Unable to start local CV worker script: " + script, ex);
|
||||
} catch (RuntimeException ex) {
|
||||
cleanupFailedStart();
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
if (process == null) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
long pid = process.pid();
|
||||
if (process.isAlive()) {
|
||||
process.destroy();
|
||||
try {
|
||||
if (!process.waitFor(Duration.ofSeconds(5).toMillis(), TimeUnit.MILLISECONDS)) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
running = false;
|
||||
log.info("event=local_cv_worker_stopped pid={}", pid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running && process != null && process.isAlive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAutoStartup() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPhase() {
|
||||
return Integer.MIN_VALUE + 100;
|
||||
}
|
||||
|
||||
private boolean shouldStart() {
|
||||
return "local-cv".equalsIgnoreCase(visualAnalysis.getProvider())
|
||||
&& visualAnalysis.getLocalCvWorker().isAutoStart();
|
||||
}
|
||||
|
||||
private void waitForStartupWindow(long startupWaitMs) {
|
||||
if (startupWaitMs <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(startupWaitMs);
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting for local CV worker startup", ex);
|
||||
}
|
||||
if (process != null && !process.isAlive()) {
|
||||
throw new IllegalStateException("Local CV worker exited during startup window");
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupFailedStart() {
|
||||
running = false;
|
||||
if (process != null && process.isAlive()) {
|
||||
process.destroyForcibly();
|
||||
}
|
||||
}
|
||||
|
||||
private Endpoint endpoint() {
|
||||
URI uri = URI.create(visualAnalysis.getEndpoint());
|
||||
String host = uri.getHost() == null || uri.getHost().isBlank() ? "127.0.0.1" : uri.getHost();
|
||||
int port = uri.getPort();
|
||||
if (port < 0) {
|
||||
port = "https".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||
}
|
||||
return new Endpoint(host, port);
|
||||
}
|
||||
|
||||
private Thread outputReader(InputStream inputStream, Path script) {
|
||||
Thread thread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
log.info("event=local_cv_worker_output script={} message={}", script, line);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
log.debug("event=local_cv_worker_output_closed script={} error_type={}",
|
||||
script, ex.getClass().getSimpleName());
|
||||
}
|
||||
}, "local-cv-worker-output");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
|
||||
record Endpoint(String host, int port) {
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ProcessLauncher {
|
||||
Process start(ProcessBuilder processBuilder) throws IOException;
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,10 @@ video-clipping:
|
|||
endpoint: ${VIDEO_EDITING_VISUAL_ANALYSIS_ENDPOINT:http://127.0.0.1:8091/v1/analyze-visuals}
|
||||
timeout-ms: ${VIDEO_EDITING_VISUAL_ANALYSIS_TIMEOUT_MS:30000}
|
||||
fallback-to-heuristic: ${VIDEO_EDITING_VISUAL_ANALYSIS_FALLBACK_TO_HEURISTIC:true}
|
||||
local-cv-worker:
|
||||
auto-start: ${VIDEO_EDITING_LOCAL_CV_WORKER_AUTO_START:false}
|
||||
script: ${VIDEO_EDITING_LOCAL_CV_WORKER_SCRIPT:./tools/run_local_cv_worker.sh}
|
||||
startup-wait-ms: ${VIDEO_EDITING_LOCAL_CV_WORKER_STARTUP_WAIT_MS:0}
|
||||
local-director:
|
||||
enabled: ${VIDEO_EDITING_LOCAL_DIRECTOR_ENABLED:true}
|
||||
source-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_SOURCE_DIRECTORY:./input/editing/source}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ class VideoClippingPropertiesTest {
|
|||
.isEqualTo("http://127.0.0.1:8091/v1/analyze-visuals");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(30000);
|
||||
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isTrue();
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().isAutoStart()).isFalse();
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getScript())
|
||||
.isEqualTo("./tools/run_local_cv_worker.sh");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs()).isZero();
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isTrue();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory())
|
||||
.isEqualTo("./input/editing/source");
|
||||
|
|
@ -101,6 +105,9 @@ class VideoClippingPropertiesTest {
|
|||
"video-clipping.editing.visual-analysis.endpoint=http://localhost:9000/analyze",
|
||||
"video-clipping.editing.visual-analysis.timeout-ms=12000",
|
||||
"video-clipping.editing.visual-analysis.fallback-to-heuristic=false",
|
||||
"video-clipping.editing.visual-analysis.local-cv-worker.auto-start=true",
|
||||
"video-clipping.editing.visual-analysis.local-cv-worker.script=/tmp/local-cv.sh",
|
||||
"video-clipping.editing.visual-analysis.local-cv-worker.startup-wait-ms=250",
|
||||
"video-clipping.editing.local-director.enabled=false",
|
||||
"video-clipping.editing.local-director.source-directory=/tmp/source",
|
||||
"video-clipping.editing.local-director.poll-interval-ms=9000",
|
||||
|
|
@ -140,6 +147,11 @@ class VideoClippingPropertiesTest {
|
|||
.isEqualTo("http://localhost:9000/analyze");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getTimeoutMs()).isEqualTo(12000);
|
||||
assertThat(properties.getEditing().getVisualAnalysis().isFallbackToHeuristic()).isFalse();
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().isAutoStart()).isTrue();
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getScript())
|
||||
.isEqualTo("/tmp/local-cv.sh");
|
||||
assertThat(properties.getEditing().getVisualAnalysis().getLocalCvWorker().getStartupWaitMs())
|
||||
.isEqualTo(250);
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isFalse();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory()).isEqualTo("/tmp/source");
|
||||
assertThat(properties.getEditing().getLocalDirector().getPollIntervalMs()).isEqualTo(9000);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
package org.example.videoclips.editing;
|
||||
|
||||
import org.example.videoclips.config.VideoClippingProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class LocalCvWorkerProcessManagerTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void doesNotStartWhenProviderIsNotLocalCv() {
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
properties.getEditing().getVisualAnalysis().setProvider("heuristic");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||
throw new AssertionError("worker should not start");
|
||||
});
|
||||
|
||||
manager.start();
|
||||
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotStartWhenAutoStartIsDisabled() {
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(false);
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||
throw new AssertionError("worker should not start");
|
||||
});
|
||||
|
||||
manager.start();
|
||||
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsWorkerScriptWithEndpointHostAndPort() throws Exception {
|
||||
Path script = tempDir.resolve("run-worker.sh");
|
||||
Files.writeString(script, "#!/usr/bin/env bash\n");
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||
properties.getEditing().getVisualAnalysis().setEndpoint("http://localhost:9005/v1/analyze-visuals");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(script.toString());
|
||||
AtomicReference<ProcessBuilder> captured = new AtomicReference<>();
|
||||
TestProcess process = new TestProcess();
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||
captured.set(processBuilder);
|
||||
return process;
|
||||
});
|
||||
|
||||
manager.start();
|
||||
|
||||
assertThat(manager.isRunning()).isTrue();
|
||||
assertThat(captured.get().command()).containsExactly(script.toAbsolutePath().normalize().toString());
|
||||
assertThat(captured.get().environment()).containsEntry("LOCAL_CV_HOST", "localhost");
|
||||
assertThat(captured.get().environment()).containsEntry("LOCAL_CV_PORT", "9005");
|
||||
|
||||
manager.stop();
|
||||
|
||||
assertThat(process.destroyed).isTrue();
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingWorkerScriptWhenAutoStartIsEnabled() {
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(tempDir.resolve("missing.sh").toString());
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> {
|
||||
throw new AssertionError("worker should not start");
|
||||
});
|
||||
|
||||
assertThatThrownBy(manager::start)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Local CV worker script does not exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetsRunningStateWhenWorkerExitsDuringStartupWindow() throws Exception {
|
||||
Path script = tempDir.resolve("run-worker.sh");
|
||||
Files.writeString(script, "#!/usr/bin/env bash\n");
|
||||
VideoClippingProperties properties = new VideoClippingProperties();
|
||||
properties.getEditing().getVisualAnalysis().setProvider("local-cv");
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setAutoStart(true);
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setScript(script.toString());
|
||||
properties.getEditing().getVisualAnalysis().getLocalCvWorker().setStartupWaitMs(1);
|
||||
TestProcess process = new TestProcess();
|
||||
process.alive = false;
|
||||
|
||||
LocalCvWorkerProcessManager manager = new LocalCvWorkerProcessManager(properties, processBuilder -> process);
|
||||
|
||||
assertThatThrownBy(manager::start)
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Local CV worker exited during startup window");
|
||||
assertThat(manager.isRunning()).isFalse();
|
||||
}
|
||||
|
||||
private static class TestProcess extends Process {
|
||||
private boolean alive = true;
|
||||
private boolean destroyed;
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return OutputStream.nullOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream("worker ready\n".getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
return InputStream.nullInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int waitFor() {
|
||||
alive = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean waitFor(long timeout, TimeUnit unit) {
|
||||
alive = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int exitValue() {
|
||||
return alive ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
destroyed = true;
|
||||
alive = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Process destroyForcibly() {
|
||||
destroyed = true;
|
||||
alive = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return alive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long pid() {
|
||||
return 1234;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue