diff --git a/src/com/sheepit/client/Client.java b/src/com/sheepit/client/Client.java index 70ae8b5..0a5a87a 100644 --- a/src/com/sheepit/client/Client.java +++ b/src/com/sheepit/client/Client.java @@ -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; } diff --git a/src/com/sheepit/client/Configuration.java b/src/com/sheepit/client/Configuration.java index d87cd6a..af07aa5 100644 --- a/src/com/sheepit/client/Configuration.java +++ b/src/com/sheepit/client/Configuration.java @@ -60,6 +60,8 @@ import lombok.Data; private boolean detectGPUs; private boolean printLog; private List> 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; diff --git a/src/com/sheepit/client/os/FreeBSD.java b/src/com/sheepit/client/os/FreeBSD.java index ade4c3c..4d3500d 100644 --- a/src/com/sheepit/client/os/FreeBSD.java +++ b/src/com/sheepit/client/os/FreeBSD.java @@ -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())); + } + } } diff --git a/src/com/sheepit/client/os/Linux.java b/src/com/sheepit/client/os/Linux.java index 6aacfb1..131d8ad 100644 --- a/src/com/sheepit/client/os/Linux.java +++ b/src/com/sheepit/client/os/Linux.java @@ -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())); + } + } } diff --git a/src/com/sheepit/client/os/Mac.java b/src/com/sheepit/client/os/Mac.java index fdcdd41..3efbab4 100644 --- a/src/com/sheepit/client/os/Mac.java +++ b/src/com/sheepit/client/os/Mac.java @@ -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())); + } + } } diff --git a/src/com/sheepit/client/os/OS.java b/src/com/sheepit/client/os/OS.java index 04a337f..5b9eafc 100644 --- a/src/com/sheepit/client/os/OS.java +++ b/src/com/sheepit/client/os/OS.java @@ -45,6 +45,8 @@ public abstract class OS { public abstract boolean checkNiceAvailability(); + public abstract void shutdownComputer(int delayInMinutes); + public Process exec(List command, Map env) throws IOException { ProcessBuilder builder = new ProcessBuilder(command); builder.redirectErrorStream(true); diff --git a/src/com/sheepit/client/os/Windows.java b/src/com/sheepit/client/os/Windows.java index bc564ef..988cf86 100644 --- a/src/com/sheepit/client/os/Windows.java +++ b/src/com/sheepit/client/os/Windows.java @@ -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())); + } + } } diff --git a/src/com/sheepit/client/standalone/Worker.java b/src/com/sheepit/client/standalone/Worker.java index 073506c..e8ccbd6 100644 --- a/src/com/sheepit/client/standalone/Worker.java +++ b/src/com/sheepit/client/standalone/Worker.java @@ -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; + } + } }