diff --git a/src/com/sheepit/client/Client.java b/src/com/sheepit/client/Client.java index 975839e..2c2adbd 100644 --- a/src/com/sheepit/client/Client.java +++ b/src/com/sheepit/client/Client.java @@ -24,16 +24,21 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Observable; import java.util.Observer; +import java.util.Random; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import com.sheepit.client.Error.ServerCode; import com.sheepit.client.Error.Type; @@ -783,12 +788,12 @@ import lombok.Data; } protected Error.Type downloadSceneFile(Job ajob_) throws FermeExceptionNoSpaceLeftOnDevice { - return this.downloadFile(ajob_, ajob_.getSceneArchivePath(), ajob_.getSceneMD5(), + return this.downloadFile(ajob_, ajob_.getRequiredSceneArchivePath(), ajob_.getSceneMD5(), String.format("%s?type=job&job=%s", this.server.getPage("download-archive"), ajob_.getId()), "project"); } protected Error.Type downloadExecutable(Job ajob) throws FermeExceptionNoSpaceLeftOnDevice { - return this.downloadFile(ajob, ajob.getRendererArchivePath(), ajob.getRendererMD5(), + return this.downloadFile(ajob, ajob.getRequiredRendererArchivePath(), ajob.getRendererMD5(), String.format("%s?type=binary&job=%s", this.server.getPage("download-archive"), ajob.getId()), "renderer"); } @@ -796,9 +801,64 @@ import lombok.Data; File local_path_file = new File(local_path); String update_ui = "Downloading " + download_type; - if (local_path_file.exists() == true) { - this.gui.status("Reusing cached " + download_type); - return Type.OK; + int remaining = 1800000; // 30 minutes max timeout + + try { + // If the client is using a shared cache then introduce some random delay to minimise race conditions on the partial file creation on multiple + // instances of a client (when started with a script or rendering a recently downloaded scene) + if (configuration.getSharedDownloadsDirectory() != null) { + Thread.sleep((new Random().nextInt(9) + 1) * 1000); + } + + // For a maximum of 30 minutes + do { + // if the binary or scene already exists in the cache + if (local_path_file.exists() == true) { + this.gui.status("Reusing cached " + download_type); + return Type.OK; + } + // if the binary or scene is being downloaded by another client + else if (new File(local_path + ".partial").exists()) { + // Wait and check every second for file download completion but only update the GUI every 10 seconds to minimise CPU load + if (remaining % 10000 == 0) { + this.gui.status(String.format("Another client is downloading the %s. Cancel in %dmin %ds", + download_type, + TimeUnit.MILLISECONDS.toMinutes(remaining), + TimeUnit.MILLISECONDS.toSeconds(remaining) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(remaining)) + )); + } + } + else { + // The file doesn't yet existing not is being downloaded by another client, so immediately create the file with zero bytes to allow early + // detection by other concurrent clients and start downloading process + try { + File file = new File(local_path + ".partial"); + file.createNewFile(); + file.deleteOnExit(); // if the client crashes, the temporary file will be removed + } catch (IOException e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + this.log.error("Client::DownloadFile Unable to create .partial temp file for binary/scene " + local_path); + this.log.error("Client::DownloadFile Exception " + e + " stacktrace " + sw.toString()); + } + + break; + } + + // Reduce 1 second the waiting time + Thread.sleep(1000); + remaining -= 1000; + } while (remaining > 0); + } + catch (InterruptedException e) { + log.debug("Error in the thread wait. Exception " + e.getMessage()); + } + finally { + // If we have reached the timeout (30 minutes trying to download the client) delete the partial downloaded copy and try to download again + if (remaining <= 0) { + log.debug("ERROR while waiting for download to finish in another client. Deleting the partial file and downloading a fresh copy now!."); + new File(local_path + ".partial").delete(); + } } this.gui.status(String.format("Downloading %s", download_type), 0, 0); @@ -869,18 +929,28 @@ import lombok.Data; protected int prepareWorkingDirectory(Job ajob) throws FermeExceptionNoSpaceLeftOnDevice { int ret; + String bestRendererArchive = ajob.getRequiredRendererArchivePath(); String renderer_archive = ajob.getRendererArchivePath(); String renderer_path = ajob.getRendererDirectory(); File renderer_path_file = new File(renderer_path); - if (renderer_path_file.exists()) { - // Directory already exists -> do nothing + if (!new File(renderer_archive).exists()) { + this.gui.status("Copying renderer from shared downloads directory"); + + try { + Files.copy(Paths.get(bestRendererArchive), Paths.get(renderer_archive), StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) { + this.gui.error("Error while copying renderer from shared downloads directory to working dir"); + } } - else { - this.gui.status("Extracting renderer"); + + if (!renderer_path_file.exists()) { // we create the directory renderer_path_file.mkdir(); + this.gui.status("Extracting renderer"); + // unzip the archive ret = Utils.unzipFileIntoDirectory(renderer_archive, renderer_path, null, log); if (ret != 0) { @@ -899,18 +969,28 @@ import lombok.Data; } } + String bestSceneArchive = ajob.getRequiredSceneArchivePath(); String scene_archive = ajob.getSceneArchivePath(); String scene_path = ajob.getSceneDirectory(); File scene_path_file = new File(scene_path); - if (scene_path_file.exists()) { - // Directory already exists -> do nothing + if (!new File(scene_archive).exists()) { + this.gui.status("Copying scene from common directory"); + + try { + Files.copy(Paths.get(bestSceneArchive), Paths.get(scene_archive), StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) { + this.gui.error("Error while copying scene from common directory to working dir"); + } } - else { - this.gui.status("Extracting project"); + + if (!scene_path_file.exists()) { // we create the directory scene_path_file.mkdir(); + this.gui.status("Extracting project"); + // unzip the archive ret = Utils.unzipFileIntoDirectory(scene_archive, scene_path, ajob.getPassword(), log); if (ret != 0) { diff --git a/src/com/sheepit/client/Configuration.java b/src/com/sheepit/client/Configuration.java index af07aa5..237b886 100644 --- a/src/com/sheepit/client/Configuration.java +++ b/src/com/sheepit/client/Configuration.java @@ -43,6 +43,7 @@ import lombok.Data; private String configFilePath; private File workingDirectory; + private File sharedDownloadsDirectory; private File storageDirectory; // for permanent storage (binary archive) private boolean userHasSpecifiedACacheDir; private String static_exeDirName; @@ -87,6 +88,7 @@ import lombok.Data; this.userHasSpecifiedACacheDir = false; this.detectGPUs = true; this.workingDirectory = null; + this.sharedDownloadsDirectory = null; this.storageDirectory = null; this.setCacheDir(cache_dir_); this.printLog = false; @@ -147,6 +149,13 @@ import lombok.Data; this.storageDirectory.mkdirs(); } + if (this.sharedDownloadsDirectory != null) { + this.sharedDownloadsDirectory.mkdirs(); + + if (!this.sharedDownloadsDirectory.exists()) { + System.err.println("Configuration::setCacheDir Unable to create common directory " + this.sharedDownloadsDirectory.getAbsolutePath()); + } + } } public void setStorageDir(File dir) { @@ -253,6 +262,12 @@ import lombok.Data; files.addAll(Arrays.asList(filesInDirectory)); } } + if (this.sharedDownloadsDirectory != null) { + File[] filesInDirectory = this.sharedDownloadsDirectory.listFiles(); + if (filesInDirectory != null) { + files.addAll(Arrays.asList(filesInDirectory)); + } + } for (File file : files) { if (file.isFile()) { diff --git a/src/com/sheepit/client/Job.java b/src/com/sheepit/client/Job.java index af6694d..f426be2 100644 --- a/src/com/sheepit/client/Job.java +++ b/src/com/sheepit/client/Job.java @@ -141,6 +141,15 @@ import lombok.Getter; return configuration.getWorkingDirectory().getAbsolutePath() + File.separator + rendererMD5; } + public String getRequiredRendererArchivePath() { + if (configuration.getSharedDownloadsDirectory() != null) { + return configuration.getSharedDownloadsDirectory().getAbsolutePath() + File.separator + rendererMD5 + ".zip"; + } + else { + return getRendererArchivePath(); + } + } + public String getRendererPath() { return getRendererDirectory() + File.separator + OS.getOS().getRenderBinaryPath(); } @@ -149,6 +158,15 @@ import lombok.Getter; return configuration.getStorageDir().getAbsolutePath() + File.separator + rendererMD5 + ".zip"; } + public String getRequiredSceneArchivePath() { + if (configuration.getSharedDownloadsDirectory() != null) { + return configuration.getSharedDownloadsDirectory().getAbsolutePath() + File.separator + sceneMD5 + ".zip"; + } + else { + return getSceneArchivePath(); + } + } + public String getSceneDirectory() { return configuration.getWorkingDirectory().getAbsolutePath() + File.separator + sceneMD5; } diff --git a/src/com/sheepit/client/Server.java b/src/com/sheepit/client/Server.java index cda43e8..50313ad 100644 --- a/src/com/sheepit/client/Server.java +++ b/src/com/sheepit/client/Server.java @@ -433,7 +433,7 @@ public class Server extends Thread { } is = response.body().byteStream(); - output = new FileOutputStream(destination_); + output = new FileOutputStream(destination_ + ".partial"); long size = response.body().contentLength(); byte[] buffer = new byte[8 * 1024]; @@ -488,6 +488,17 @@ public class Server extends Thread { output.close(); } + File downloadedFile = new File(destination_ + ".partial"); + + if (downloadedFile.exists()) { + // Rename file (or directory) + boolean success = downloadedFile.renameTo(new File(destination_)); + + if (!success) { + this.log.debug(String.format("Server::HTTPGetFile Error trying to rename the downloaded file to final name (%s)", destination_)); + } + } + if (is != null) { is.close(); } @@ -629,6 +640,14 @@ public class Server extends Thread { File file_to_delete = new File(path + ".zip"); file_to_delete.delete(); Utils.delete(new File(path)); + + // If we are using a shared downloads directory, then delete the file from the shared downloads directory as well :) + if (this.user_config.getSharedDownloadsDirectory() != null) { + String commonCacheFile = this.user_config.getSharedDownloadsDirectory().getAbsolutePath() + File.separatorChar + fileMD5.getMd5(); + this.log.debug("Server::handleFileMD5DeleteDocument delete common file " + commonCacheFile + ".zip"); + file_to_delete = new File(commonCacheFile + ".zip"); + file_to_delete.delete(); + } } } } diff --git a/src/com/sheepit/client/standalone/Worker.java b/src/com/sheepit/client/standalone/Worker.java index 65417b2..78bec30 100644 --- a/src/com/sheepit/client/standalone/Worker.java +++ b/src/com/sheepit/client/standalone/Worker.java @@ -61,6 +61,8 @@ public class Worker { @Option(name = "-cache-dir", usage = "Cache/Working directory. Caution, everything in it not related to the render-farm will be removed", metaVar = "/tmp/cache", required = false) private String cache_dir = null; + @Option(name = "-shared-zip", usage = "Shared directory for downloaded binaries and scenes. Useful when running two or more clients in the same computer/network to download once and render many times. IMPORTANT: This option and value must be identical in ALL clients sharing the directory.", required = false) private String sharedDownloadsDir = null; + @Option(name = "-gpu", usage = "Name of the GPU used for the render, for example CUDA_0 for Nvidia or OPENCL_0 for AMD/Intel card", metaVar = "CUDA_0", required = false) private String gpu_device = null; @Option(name = "--no-gpu", usage = "Don't detect GPUs", required = false) private boolean no_gpu_detection = false; @@ -130,6 +132,15 @@ public class Worker { config.setUsePriority(priority); config.setDetectGPUs(!no_gpu_detection); + if (sharedDownloadsDir != null) { + File dir = new File(sharedDownloadsDir); + if (dir.exists() == false || dir.canWrite() == false) { + System.err.println("ERROR: The shared-zip directory must exist and be writeable"); + return; + } + config.setSharedDownloadsDirectory(dir); + } + if (cache_dir != null) { File a_dir = new File(cache_dir); a_dir.mkdirs();