Feature: allow the client to shut down the computer at or after a certain time (#249)

A new -shutdown <time> option has been created to specify the time when the client will stop asking for new jobs, will finish uploading the frames in the upload queue and will shutdown the computer. The time argument can have two different formats: an absolute date & time in the format yyyy-MM-ddThh:mm:ss (24h format) or a relative time in the format +m where m is the number of minutes from now.

The user can also select the shutdown-mode, where "wait" will wait for all the processes (render + pending uploads) to finish and "hard" will cancel all the pending jobs and will initiate the computer shutdown.
This commit is contained in:
Luis Uguina
2020-07-28 00:49:36 +10:00
committed by GitHub
parent 22e914dcc0
commit 9f5c35d02f
8 changed files with 235 additions and 11 deletions

View File

@@ -24,11 +24,16 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Observable;
import java.util.Observer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadLocalRandom;
@@ -65,6 +70,7 @@ import lombok.Data;
private boolean disableErrorSending;
private boolean running;
private boolean suspended;
private boolean shuttingdown;
private int maxDownloadFileAttempts = 5;
@@ -85,6 +91,7 @@ import lombok.Data;
this.disableErrorSending = false;
this.running = false;
this.suspended = false;
this.shuttingdown = false;
this.uploadQueueSize = 0;
this.uploadQueueVolume = 0;
@@ -126,6 +133,25 @@ import lombok.Data;
return -1;
}
// If the user has selected to shutdown the computer at any specific time, set a scheduled task
if (configuration.getShutdownTime() > 0) {
new Timer().schedule(new TimerTask() {
@Override public void run() {
shuttingdown = true;
log.debug("Initiating the computer's shutting down process");
if (configuration.getShutdownMode().equals("wait")) {
// Soft stop. Complete current render (if any), finish uploading frames and then shutdown the computer
askForStop();
}
else {
// Soft stop. Interrupt the current render, abort pending uploads, close the client and shutdown the computer
stop();
}
}
}, this.configuration.getShutdownTime());
}
this.startTime = new Date().getTime();
this.server.start(); // for staying alive
@@ -141,14 +167,16 @@ import lombok.Data;
do {
while (this.running == true) {
this.renderingJob = null;
synchronized (this) {
if (this.suspended) {
this.gui.status("Client paused", true);
}
while (this.suspended) {
while (this.suspended && !this.shuttingdown) {
wait();
}
}
step = this.log.newCheckPoint();
try {
Calendar next_request = this.nextJobRequest();
@@ -181,9 +209,9 @@ import lombok.Data;
catch (FermeExceptionSessionDisabled e) {
this.gui.error(Error.humanString(Error.Type.SESSION_DISABLED));
// should wait forever to actually display the message to the user
while (true) {
while (true && !shuttingdown) {
try {
Thread.sleep(100000);
Thread.sleep(1000);
}
catch (InterruptedException e1) {
}
@@ -192,9 +220,9 @@ import lombok.Data;
catch (FermeExceptionNoRendererAvailable e) {
this.gui.error(Error.humanString(Error.Type.RENDERER_NOT_AVAILABLE));
// should wait forever to actually display the message to the user
while (true) {
while (true && !shuttingdown) {
try {
Thread.sleep(100000);
Thread.sleep(1000);
}
catch (InterruptedException e1) {
}
@@ -214,8 +242,13 @@ import lombok.Data;
// wait
Date now = new Date();
this.gui.status(String.format("Waiting until %tR before requesting job", next_request));
long timeToSleep = next_request.getTimeInMillis() - now.getTime();
try {
Thread.sleep(next_request.getTimeInMillis() - now.getTime());
int timeSlept = 0;
while (timeSlept < timeToSleep && this.running && !this.shuttingdown) {
Thread.sleep(1000);
timeSlept += 1000;
}
}
catch (InterruptedException e3) {
@@ -224,6 +257,12 @@ import lombok.Data;
this.log.error("Client::run sleepB failed " + e3);
}
}
// if we have broken the wait loop because a stop or shutdown signal, go back to the main loop to exit
if (!this.running || this.shuttingdown) {
continue;
}
this.gui.status("Requesting Job");
this.renderingJob = this.server.requestJob();
}
@@ -238,7 +277,11 @@ import lombok.Data;
this.gui.status(String.format("Cannot connect to the server. Please check your connectivity. Will try again at %tR",
new Date(new Date().getTime() + time_sleep)));
try {
Thread.sleep(time_sleep);
int timeSlept = 0;
while (timeSlept < time_sleep && this.running && !this.shuttingdown) {
Thread.sleep(1000);
timeSlept += 1000;
}
}
catch (InterruptedException e1) {
return -3;
@@ -252,7 +295,11 @@ import lombok.Data;
this.gui.status(String.format("The server is overloaded and cannot allocate a job. Will try again at %tR",
new Date(new Date().getTime() + time_sleep)));
try {
Thread.sleep(time_sleep);
int timeSlept = 0;
while (timeSlept < time_sleep && this.running && !this.shuttingdown) {
Thread.sleep(1000);
timeSlept += 1000;
}
}
catch (InterruptedException e1) {
return -3;
@@ -266,7 +313,11 @@ import lombok.Data;
this.gui.status(String.format("The server is under maintenance and cannot allocate a job. Will try again at %tR",
new Date(new Date().getTime() + time_sleep)));
try {
Thread.sleep(time_sleep);
int timeSlept = 0;
while (timeSlept < time_sleep && this.running && !this.shuttingdown) {
Thread.sleep(1000);
timeSlept += 1000;
}
}
catch (InterruptedException e1) {
return -3;
@@ -279,7 +330,11 @@ import lombok.Data;
int time_sleep = 1000 * 60 * wait;
this.gui.status(String.format("Bad answer from the server. Will try again at %tR", new Date(new Date().getTime() + time_sleep)));
try {
Thread.sleep(time_sleep);
int timeSlept = 0;
while (timeSlept < time_sleep && this.running && !this.shuttingdown) {
Thread.sleep(1000);
timeSlept += 1000;
}
}
catch (InterruptedException e1) {
return -3;
@@ -306,7 +361,7 @@ import lombok.Data;
(retrySchemeInSeconds.length - 1)];
this.gui.status(String.format("No job available. Will try again at %tR", new Date(new Date().getTime() + time_sleep)));
int time_slept = 0;
while (time_slept < time_sleep && this.running == true) {
while (time_slept < time_sleep && this.running == true && !this.shuttingdown) {
try {
Thread.sleep(250);
}
@@ -412,6 +467,13 @@ import lombok.Data;
return -99; // the this.stop will be done after the return of this.run()
}
if (this.shuttingdown) {
// Shutdown the computer using the appropriate command for the host OS
this.log.debug("Shutting down the computer in 1 minute");
OS.getOS().shutdownComputer(1);
}
this.gui.stop();
return 0;
}

View File

@@ -60,6 +60,8 @@ import lombok.Data;
private boolean detectGPUs;
private boolean printLog;
private List<Pair<Calendar, Calendar>> requestTime;
private long shutdownTime;
private String shutdownMode;
private String extras;
private boolean autoSignIn;
private boolean useSysTray;
@@ -89,6 +91,8 @@ import lombok.Data;
this.setCacheDir(cache_dir_);
this.printLog = false;
this.requestTime = null;
this.shutdownTime = -1;
this.shutdownMode = "soft";
this.extras = "";
this.autoSignIn = false;
this.useSysTray = true;

View File

@@ -225,4 +225,15 @@ public class FreeBSD extends OS {
}
return hasNiceBinary;
}
@Override public void shutdownComputer(int delayInMinutes) {
try {
// Shutdown the computer waiting 1 minute to allow all SheepIt threads to close and exit the app
ProcessBuilder builder = new ProcessBuilder("shutdown", "-h", String.valueOf(delayInMinutes));
Process process = builder.inheritIO().start();
}
catch (IOException e) {
System.err.println(String.format("FreeBSD::shutdownComputer Unable to execute the 'shutdown -h 1' command. Exception %s", e.getMessage()));
}
}
}

View File

@@ -279,4 +279,15 @@ public class Linux extends OS {
return false;
}
@Override public void shutdownComputer(int delayInMinutes) {
try {
// Shutdown the computer waiting 1 minute to allow all SheepIt threads to close and exit the app
ProcessBuilder builder = new ProcessBuilder("shutdown", "-h", String.valueOf(delayInMinutes));
Process process = builder.inheritIO().start();
}
catch (IOException e) {
System.err.println(String.format("Linux::shutdownComputer Unable to execute the 'shutdown -h 1' command. Exception %s", e.getMessage()));
}
}
}

View File

@@ -214,4 +214,15 @@ public class Mac extends OS {
}
return hasNiceBinary;
}
@Override public void shutdownComputer(int delayInMinutes) {
try {
// Shutdown the computer waiting 1 minute to allow all SheepIt threads to close and exit the app
ProcessBuilder builder = new ProcessBuilder("shutdown", "-h", String.valueOf(delayInMinutes));
Process process = builder.inheritIO().start();
}
catch (IOException e) {
System.err.println(String.format("Mac::shutdownComputer Unable to execute the 'shutdown -h 1' command. Exception %s", e.getMessage()));
}
}
}

View File

@@ -45,6 +45,8 @@ public abstract class OS {
public abstract boolean checkNiceAvailability();
public abstract void shutdownComputer(int delayInMinutes);
public Process exec(List<String> command, Map<String, String> env) throws IOException {
ProcessBuilder builder = new ProcessBuilder(command);
builder.redirectErrorStream(true);

View File

@@ -227,4 +227,16 @@ public class Windows extends OS {
// In windows, nice is not required and therefore we return always true to show the slider in the Settings GUI
return true;
}
@Override public void shutdownComputer(int delayInMinutes) {
try {
// Shutdown the computer, waiting 60 seconds, force app closure and on the shutdown screen indicate that was initiated by SheepIt app
ProcessBuilder builder = new ProcessBuilder("shutdown", "/s", "/f", "/t", String.valueOf(delayInMinutes * 60), "/c", "\"SheepIt App has initiated this computer shutdown.\"");
Process process = builder.inheritIO().start();
}
catch (IOException e) {
System.err.println(
String.format("Windows::shutdownComputer Unable to execute the command 'shutdown /s /f /t 60...' command. Exception %s", e.getMessage()));
}
}
}

View File

@@ -28,8 +28,14 @@ import java.io.File;
import java.net.MalformedURLException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sheepit.client.Client;
import com.sheepit.client.Configuration;
@@ -71,6 +77,10 @@ public class Worker {
@Option(name = "-request-time", usage = "H1:M1-H2:M2,H3:M3-H4:M4 Use the 24h format. For example to request job between 2am-8.30am and 5pm-11pm you should do --request-time 2:00-8:30,17:00-23:00 Caution, it's the requesting job time to get a project, not the working time", metaVar = "2:00-8:30,17:00-23:00", required = false) private String request_time = null;
@Option(name = "-shutdown", usage = "Specify when the client will close and the host computer will shut down in a proper way. The time argument can have two different formats: an absolute date and time in the format yyyy-mm-ddThh:mm:ss (24h format) or a relative time in the format +m where m is the number of minutes from now.", metaVar = "DATETIME or +N", required = false) private String shutdown = null;
@Option(name = "-shutdown-mode", usage = "Indicates if the shutdown process waits for the upload queue to finish (wait) or interrupt all the pending tasks immediately (hard). The default shutdown mode is wait.", metaVar = "MODE", required = false) private String shutdownMode = null;
@Option(name = "-proxy", usage = "URL of the proxy", metaVar = "http://login:password@host:port", required = false) private String proxy = null;
@Option(name = "-extras", usage = "Extras data push on the authentication request", required = false) private String extras = null;
@@ -284,6 +294,96 @@ public class Worker {
config.setTheme(this.theme);
}
// Shutdown process block
if (shutdown != null) {
Pattern absoluteTimePattern = Pattern.compile("^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))T([01]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$");
Pattern relativeTimePattern = Pattern.compile("^\\+([0-9]{2,4})$");
LocalDateTime shutdownTime = null;
Matcher timeAbsolute = absoluteTimePattern.matcher(shutdown);
Matcher timeRelative = relativeTimePattern.matcher(shutdown);
if (timeAbsolute.find()) {
if ((shutdownTime = shutdownTimeParse(shutdown)) != null) {
long diffInMillies = ChronoUnit.MILLIS.between(LocalDateTime.now(), shutdownTime);
if (diffInMillies < 0) {
System.err.println(String
.format("\nERROR: The entered shutdown time (%s) is a date on the past. Shutdown time must be at least 30 minutes from now",
shutdown));
System.err.println("Aborting");
System.exit(2);
}
else if (diffInMillies < 10 * 60 * 1000) { // 10 minutes
System.err.println(String.format(
"\nERROR: The specified shutdown time (%s) is expected to happen in less than 10 minutes. Shutdown time must be at least 30 minutes from now",
shutdown));
System.err.println("Aborting");
System.exit(2);
}
config.setShutdownTime(diffInMillies);
}
else {
System.err.println(String.format(
"\nERROR: The format of the entered shutdown time (%s) is not correct.\nThe time argument can have two different formats: an absolute date and time in the format yyyy-mm-ddThh:mm:ss (24h format) or a relative time in the format +m where m is the number of minutes from now (min. +10 minutes, max. +9999 minutes)",
shutdown));
System.err.println("Aborting");
System.exit(2);
}
}
else if (timeRelative.find()) {
int minutesUntilShutdown = Integer.parseInt(timeRelative.group(1));
config.setShutdownTime(minutesUntilShutdown * 60 * 1000);
shutdownTime = LocalDateTime.now().plusMinutes(minutesUntilShutdown);
}
else {
System.err.println(String.format(
"\nERROR: The time especified (%s) is less than 10 minutes or the format is not correct.\nThe time argument can have two different formats: an absolute date and time in the format yyyy-mm-ddThh:mm:ss (24h format) or a relative time in the format +m where m is the number of minutes from now (min. +10 minutes, max. +9999 minutes)",
shutdown));
System.err.println("Aborting");
System.exit(2);
}
if (shutdownMode != null) {
if (shutdownMode.toLowerCase().equals("wait") || shutdownMode.toLowerCase().equals("hard")) {
config.setShutdownMode(shutdownMode.toLowerCase());
}
else {
System.err
.println(String.format("ERROR: The entered shutdown-mode (%s) is invalid. Please enter wait or hard shutdown mode.", shutdownMode));
System.err.println(" - Wait: the shutdown process is initiated once the current job and all the queued uploads are finished.");
System.err
.println(" - Hard: Then shutdown process is executed immediately. Any ongoing rendering process or upload queues will be cancelled.");
System.err.println("Aborting");
System.exit(2);
}
}
else {
// if no shutdown mode specified, then set "wait" mode by default
config.setShutdownMode("wait");
}
System.out.println("==============================================================================");
if (config.getShutdownMode().equals("wait")) {
System.out.println(String.format(
"WARNING!\n\nThe client will stop requesting new jobs at %s.\nTHE EFFECTIVE SHUTDOWN MIGHT OCCUR LATER THAN THE REQUESTED TIME AS THE UPLOAD\nQUEUE MUST BE FULLY UPLOADED BEFORE THE SHUTDOWN PROCESS STARTS.\n\nIf you want to shutdown the computer sharp at the specified time, please\ninclude the '-shutdown-mode hard' parameter in the application call",
shutdownTime));
}
else {
System.out.println(String.format(
"WARNING!\n\nThe client will initiate the shutdown process at %s.\nALL RENDERS IN PROGRESS AND UPLOAD QUEUES WILL BE CANCELED.\n\nIf you prefer to shutdown the computer once the pending jobs are completed,\nplease include the '-shutdown-mode wait' parameter in the application call",
shutdownTime));
}
System.out.println("==============================================================================\n");
}
else if (shutdown == null && shutdownMode != null) {
System.err.println(
"ERROR: The shutdown-mode parameter cannot be entered alone. Please make sure that you also enter a valid shutdown time (using -shutdown parameter)");
System.err.println("Aborting");
System.exit(2);
}
if (config_file != null) {
if (new File(config_file).exists() == false) {
System.err.println(
@@ -340,4 +440,15 @@ public class Worker {
System.err.println(String.format("ERROR: Unable to parse the provided parameter [%s]", e.getMessage()));
}
}
private LocalDateTime shutdownTimeParse(String shutdownTime) {
try {
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
return LocalDateTime.parse(shutdownTime, df);
}
catch (DateTimeParseException e) {
e.printStackTrace();
return null;
}
}
}