diff --git a/.classpath b/.classpath deleted file mode 100644 index c5a5a0fe..00000000 --- a/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4f21ac72 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 4 +indent_style = tab diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..84822bd5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Installed dependencies + run: | + sudo apt update + sudo apt install libxml2-utils + - name: Decrypt keystore + run: gpg --quiet --batch --yes --decrypt --passphrase="${{ secrets.KEYSTORE_ENCRYPTION_PASS }}" --output $HOME/keystore.jks keystore.gpg + - name: Build jar & exe + run: ./create_signed_release.sh $HOME/keystore.jks ${{ secrets.KEYSTORE_PASS }} ${{ secrets.CERT_ALIAS }} + env: + JAVA_HOME: /usr/lib/jvm/zulu-8-azure-amd64 + - name: Upload a Build Artifact + uses: actions/upload-artifact@v2 + with: + path: sheepit-*.exe + - name: Upload a Build Artifact + uses: actions/upload-artifact@v2 + with: + path: sheepit-client*.jar diff --git a/.gitignore b/.gitignore index 9eca9b6c..345f0149 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ +.idea/workspace.xml +.gradle build -bin +out + diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..e0788816 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +sheepit-client \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..1e989f09 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,44 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..95a88ae4 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 00000000..577be554 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 00000000..8da083f2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..e860487e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.project b/.project deleted file mode 100644 index 2f82e766..00000000 --- a/.project +++ /dev/null @@ -1,17 +0,0 @@ - - - SheepitClient - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff --git a/.travis.yml b/.travis.yml index 4d52d60d..e48d4a77 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ language: java -script: ant +dist: trusty os: - linux + # safelist branches: only: diff --git a/README.md b/README.md index bb267e24..fddc0a39 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,24 @@ SheepIt Render Farm Client is an *Open Source* client for the distributed render ## Compilation -You need Java 1.7 or more. (OpenJDK and Oracle are both supported). -You also need [ant](http://ant.apache.org/). -To create the jar file, simply type `ant` in the project's root directory. +You will need Java 1.7 or higher. (OpenJDK and Oracle are both supported). +To create the jar file, simply type `./gradlew shadowJar` on linux/OSX and `gradlew.bat shadowJar` on Windows in the project's root directory. ## Usage -Once you have a jar file, you can view the usage by running: +Once you have the jar file, you can see how to use it by running: - java -jar bin/sheepit-client.jar --help + java -jar build/libs/sheepit-client.jar --help When you are doing development work, you can use a mirror of the main site specially made for demo/dev. The mirror is located at **http://sandbox.sheepit-renderfarm.com**, and you can use it by passing `-server http://sandbox.sheepit-renderfarm.com` to your invocation of the client. -At the command line ui (-ui text / -ui oneLine) you could type in the following commands and press enter to controll the client: +At the command line ui (-ui text / -ui oneLine) you could type in the following commands and press enter to control the client: -* status: to get the current status of the client (paused, stoped, ...) -* priority : to set the renderer process priority -* block: to block the current project +* status: get the current status of the client (paused, stoped, etc.) +* priority : set the renderer process priority +* block: block the current project * pause: pause the client to request new jobs after the current frame has finished to render * resume: resume the client after it was paused * stop: stop the client after the current frame has finished * cancel: cancel the stop request -* quit: stops the client directly without finishing the current frame +* quit: stop the client directly without finishing the current frame diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..8e2f5c40 --- /dev/null +++ b/build.gradle @@ -0,0 +1,70 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.4' + } +} +apply plugin: 'java' +apply plugin: 'com.github.johnrengelman.shadow' + +sourceCompatibility = '1.7' +targetCompatibility = '1.7' + +compileJava { + // Suppress warnings about internal api usage - https://stackoverflow.com/a/19553686/6238618 + options.fork = true + options.forkOptions.executable = 'javac' + options.compilerArgs << '-XDignore.symbol.file' +} + +shadowJar { + exclude 'OSGI-OPT/' // args4j garbage +} + +repositories { + jcenter() + mavenCentral() +} + +dependencies { + compileOnly 'org.projectlombok:lombok:1.18.12' + annotationProcessor 'org.projectlombok:lombok:1.18.12' + + compile 'args4j:args4j:2.33' + compile 'net.lingala.zip4j:zip4j:1.3.3' + compile 'net.java.dev.jna:jna-platform:5.0.0' + compile 'org.simpleframework:simple-xml:2.7.1' + compile 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'com.formdev:flatlaf:0.30' + implementation 'com.squareup.okhttp3:okhttp:4.7.2' + implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.7.2' +} + +jar { + manifest { + attributes "Main-Class": "com.sheepit.client.standalone.Worker" + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +sourceSets { + main { + java { + srcDirs = ['src'] + } + resources { + srcDirs = ['resources'] + } + } + + test { + java { + srcDirs = ['test'] + } + } +} diff --git a/build.xml b/build.xml deleted file mode 100644 index cc22327b..00000000 --- a/build.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/code_style_formator.xml b/code_style_formator.xml deleted file mode 100644 index 54e6aefe..00000000 --- a/code_style_formator.xml +++ /dev/null @@ -1,291 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/create_signed_release.sh b/create_signed_release.sh new file mode 100755 index 00000000..0b5012ce --- /dev/null +++ b/create_signed_release.sh @@ -0,0 +1,71 @@ +#!/bin/bash -e + +# Requires xmllint (part of `libxml2-utils`), svn, gpg, git (duh), curl and p7zip +# JAVA_HOME must be set to a java 8 JDK +# +# Usage: ./create_signed_release.sh $PATH_TO_KEYSTORE $PASSWORD $CERTIFICATE_ALIAS + + + +echo "" +echo "### Setup ###" + +keystore="$1" +password="$2" +cert_alias="$3" +git_url="https://github.com/laurent-clouet/sheepit-client" +svn_trunk_url="$git_url/trunk" +pwd=`pwd` +tmp_dir=`mktemp -d` +jvm_name="jdk-11.0.6+10-jre" + +wrapper_dir="$tmp_dir/wrapper" +git clone $git_url $wrapper_dir +cd $wrapper_dir +git checkout wrapper +cd $pwd + +echo "" +echo "### Generate version ###" + +svn_info=`svn info $svn_trunk_url --xml` +echo "$svn_info" +echo "" +svn_revision=`echo $svn_info | xmllint --xpath "string(/info/entry/@revision)" -` +echo "Revision: $svn_revision" +version="6.$svn_revision.0" +echo "Version: $version" + + +echo "" +echo "### Create jar ###" + +signed_jar="$pwd/sheepit-client-$version.jar" +echo $version > ./resources/VERSION +./gradlew shadowJar +unsigned_jar="$pwd/build/libs/sheepit-client-all.jar" +echo "Unsigned .jar: $unsigned_jar" +echo "Signing" +jarsigner -tsa http://timestamp.digicert.com -keystore $keystore -storepass $password -signedjar $signed_jar $unsigned_jar $cert_alias + + +echo "" +echo "### Build exe ###" + +exe="$pwd/sheepit-$version.exe" +7z x -o$tmp_dir $wrapper_dir/$jvm_name.zip +mv $tmp_dir/$jvm_name $tmp_dir/jre +rm -rf $tmp_dir/jre/include $tmp_dir/jre/src.zip +cp -f $signed_jar $tmp_dir/jre/sheepit-client.jar + +cd $tmp_dir/jre +7z a $tmp_dir/application.7z . +cd $tmp_dir +cp $wrapper_dir/config.cfg config.cfg +cp $wrapper_dir/starter.sfx starter.sfx +cat starter.sfx config.cfg application.7z > $exe +cd $pwd + +echo "" +echo ".jar: $signed_jar" +echo ".exe: $exe" diff --git a/extern/args4j.jar b/extern/args4j.jar deleted file mode 100644 index e188b3df..00000000 Binary files a/extern/args4j.jar and /dev/null differ diff --git a/extern/jaxb-api.jar b/extern/jaxb-api.jar deleted file mode 100644 index 0817c083..00000000 Binary files a/extern/jaxb-api.jar and /dev/null differ diff --git a/extern/jaxb-core.jar b/extern/jaxb-core.jar deleted file mode 100644 index 864ba311..00000000 Binary files a/extern/jaxb-core.jar and /dev/null differ diff --git a/extern/jaxb-impl.jar b/extern/jaxb-impl.jar deleted file mode 100644 index 02f50ab5..00000000 Binary files a/extern/jaxb-impl.jar and /dev/null differ diff --git a/extern/jna.jar b/extern/jna.jar deleted file mode 100644 index 9038048d..00000000 Binary files a/extern/jna.jar and /dev/null differ diff --git a/extern/platform.jar b/extern/platform.jar deleted file mode 100644 index 4b3d567c..00000000 Binary files a/extern/platform.jar and /dev/null differ diff --git a/extern/zip4j.jar b/extern/zip4j.jar deleted file mode 100644 index 66a19e49..00000000 Binary files a/extern/zip4j.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..490fda85 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ff0aac17 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Apr 10 00:43:49 CST 2020 +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..2fe81a7d --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..62bd9b9c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/keystore.gpg b/keystore.gpg new file mode 100644 index 00000000..9f358217 Binary files /dev/null and b/keystore.gpg differ diff --git a/protocol.txt b/protocol.txt index f5ef32de..06f48e76 100644 --- a/protocol.txt +++ b/protocol.txt @@ -34,7 +34,6 @@ A path is provided for error, job request, job validation, download needed file, The maximum duration between two heartbeats in seconds is given by the attribute "max-period". - @@ -123,7 +122,7 @@ if fileformat != 'BMP' and fileformat != 'PNG' and fileformat != 'JPEG' and file === Job validation === -Url: use the request type "validate-job" from the configuration answer. +Url: use the url url of job's node for 'validationurl'. Parameter as GET or POST: * job: Job ID * frame: Job's frame number diff --git a/resources/frame_compute_method.jpg b/resources/frame_compute_method.jpg new file mode 100644 index 00000000..d65bd7fa Binary files /dev/null and b/resources/frame_compute_method.jpg differ diff --git a/resources/frame_power_detection.jpg b/resources/frame_power_detection.jpg new file mode 100644 index 00000000..caf1d75c Binary files /dev/null and b/resources/frame_power_detection.jpg differ diff --git a/resources/icon-sprites.png b/resources/icon-sprites.png new file mode 100644 index 00000000..b1a95222 Binary files /dev/null and b/resources/icon-sprites.png differ diff --git a/resources/icon.png b/resources/icon.png deleted file mode 100644 index 88a9d5e8..00000000 Binary files a/resources/icon.png and /dev/null differ diff --git a/resources/sheepit-logo.png b/resources/sheepit-logo.png new file mode 100644 index 00000000..bbaeff6e Binary files /dev/null and b/resources/sheepit-logo.png differ diff --git a/resources/title.png b/resources/title.png deleted file mode 100644 index e52fffde..00000000 Binary files a/resources/title.png and /dev/null differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..4a88bede --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'sheepit-client' diff --git a/src/com/sheepit/client/Client.java b/src/com/sheepit/client/Client.java index d8f82adb..00b18be7 100644 --- a/src/com/sheepit/client/Client.java +++ b/src/com/sheepit/client/Client.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -27,6 +27,8 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Date; +import java.util.Observable; +import java.util.Observer; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ThreadLocalRandom; @@ -43,18 +45,22 @@ import com.sheepit.client.exception.FermeExceptionServerOverloaded; import com.sheepit.client.exception.FermeExceptionSessionDisabled; import com.sheepit.client.exception.FermeServerDown; +import com.sheepit.client.hardware.cpu.CPU; import com.sheepit.client.os.OS; -public class Client { +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data public class Client { private Gui gui; private Server server; - private Configuration config; + private Configuration configuration; private Log log; private Job renderingJob; private Job previousJob; - private BlockingQueue jobsToValidate; + private BlockingQueue jobsToValidate; private boolean isValidatingJob; - private long start_time; + private long startTime; private boolean disableErrorSending; private boolean running; @@ -62,56 +68,40 @@ public class Client { private int maxDownloadFileAttempts = 5; - public Client(Gui gui_, Configuration config, String url_) { - this.config = config; - this.server = new Server(url_, this.config, this); - this.log = Log.getInstance(this.config); + private int uploadQueueSize; + private long uploadQueueVolume; + private int noJobRetryIter; + + public Client(Gui gui_, Configuration configuration, String url_) { + this.configuration = configuration; + this.server = new Server(url_, this.configuration, this); + this.log = Log.getInstance(this.configuration); this.gui = gui_; this.renderingJob = null; this.previousJob = null; - this.jobsToValidate = new ArrayBlockingQueue(1024); + this.jobsToValidate = new ArrayBlockingQueue(5); this.isValidatingJob = false; this.disableErrorSending = false; this.running = false; this.suspended = false; + + this.uploadQueueSize = 0; + this.uploadQueueVolume = 0; + this.noJobRetryIter = 0; } public String toString() { - return String.format("Client (config %s, server %s)", this.config, this.server); - } - - public Job getRenderingJob() { - return this.renderingJob; - } - - public Gui getGui() { - return this.gui; - } - - public Configuration getConfiguration() { - return this.config; - } - - public Server getServer() { - return this.server; - } - - public Log getLog() { - return this.log; - } - - public long getStartTime() { - return this.start_time; + return String.format("Client (configuration %s, server %s)", this.configuration, this.server); } public int run() { - if (this.config.checkOSisSupported() == false) { + if (this.configuration.checkOSisSupported() == false) { this.gui.error(Error.humanString(Error.Type.OS_NOT_SUPPORTED)); return -3; } - if (this.config.checkCPUisSupported() == false) { + if (this.configuration.checkCPUisSupported() == false) { this.gui.error(Error.humanString(Error.Type.CPU_NOT_SUPPORTED)); return -4; } @@ -123,7 +113,7 @@ public int run() { step = this.log.newCheckPoint(); this.gui.status("Starting"); - this.config.cleanWorkingDirectory(); + this.configuration.cleanWorkingDirectory(); Error.Type ret; ret = this.server.getConfiguration(); @@ -136,7 +126,7 @@ public int run() { return -1; } - this.start_time = new Date().getTime(); + this.startTime = new Date().getTime(); this.server.start(); // for staying alive // create a thread which will send the frame @@ -148,229 +138,270 @@ public void run() { Thread thread_sender = new Thread(runnable_sender); thread_sender.start(); - while (this.running == true) { - this.renderingJob = null; - synchronized (this) { - while (this.suspended) { - wait(); - } - } - step = this.log.newCheckPoint(); - try { - Calendar next_request = this.nextJobRequest(); - if (next_request != null) { - // wait - Date now = new Date(); - this.gui.status(String.format("Waiting until %tR before requesting job", next_request)); - try { - Thread.sleep(next_request.getTimeInMillis() - now.getTime()); + do { + while (this.running == true) { + this.renderingJob = null; + synchronized (this) { + if (this.suspended) { + this.gui.status("Client paused", true); + } + while (this.suspended) { + wait(); } - catch (InterruptedException e3) { + } + step = this.log.newCheckPoint(); + try { + Calendar next_request = this.nextJobRequest(); + if (next_request != null) { + // wait + Date now = new Date(); + this.gui.status(String.format("Waiting until %tR before requesting job", next_request)); + long wait = next_request.getTimeInMillis() - now.getTime(); + if (wait < 0) { + // it means the client has to wait until the next day + wait += 24 * 3600 * 1000; + } + try { + Thread.sleep(wait); + } + catch (InterruptedException e3) { + } + catch (IllegalArgumentException e3) { + this.log.error("Client::run sleepA failed " + e3); + } + } + this.gui.status("Requesting Job"); + this.renderingJob = this.server.requestJob(); + } + catch (FermeExceptionNoRightToRender e) { + this.gui.error("User does not have enough right to render scene"); + return -2; + } + 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) { + try { + Thread.sleep(100000); + } + catch (InterruptedException e1) { + } } - catch (IllegalArgumentException e3) { - this.log.error("Client::run sleepA failed " + e3); + } + 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) { + try { + Thread.sleep(100000); + } + catch (InterruptedException e1) { + } } } - this.gui.status("Requesting Job"); - this.renderingJob = this.server.requestJob(); - } - catch (FermeExceptionNoRightToRender e) { - this.gui.error("User does not have enough right to render scene"); - return -2; - } - 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) { + catch (FermeExceptionNoSession e) { + this.log.debug("User has no session and needs to re-authenticate"); + ret = this.server.getConfiguration(); + if (ret != Error.Type.OK) { + this.renderingJob = null; + } + else { + this.startTime = new Date().getTime(); // reset start session time because the server did it + try { + Calendar next_request = this.nextJobRequest(); + if (next_request != null) { + // wait + Date now = new Date(); + this.gui.status(String.format("Waiting until %tR before requesting job", next_request)); + try { + Thread.sleep(next_request.getTimeInMillis() - now.getTime()); + } + catch (InterruptedException e3) { + + } + catch (IllegalArgumentException e3) { + this.log.error("Client::run sleepB failed " + e3); + } + } + this.gui.status("Requesting Job"); + this.renderingJob = this.server.requestJob(); + } + catch (FermeException e1) { + this.renderingJob = null; + } + } + } + catch (FermeServerDown e) { + int wait = ThreadLocalRandom.current().nextInt(10, 30 + 1); // max is exclusive + int time_sleep = 1000 * 60 * wait; + 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(100000); + Thread.sleep(time_sleep); } catch (InterruptedException e1) { + return -3; } + this.log.removeCheckPoint(step); + continue; // go back to ask job } - } - 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) { + catch (FermeExceptionServerOverloaded e) { + int wait = ThreadLocalRandom.current().nextInt(10, 30 + 1); // max is exclusive + int time_sleep = 1000 * 60 * wait; + 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(100000); + Thread.sleep(time_sleep); } catch (InterruptedException e1) { + return -3; } + this.log.removeCheckPoint(step); + continue; // go back to ask job } - } - catch (FermeExceptionNoSession e) { - this.log.debug("User has no session need to re-authenticate"); - ret = this.server.getConfiguration(); - if (ret != Error.Type.OK) { - this.renderingJob = null; - } - else { - this.start_time = new Date().getTime(); // reset start session time because the server did it + catch (FermeExceptionServerInMaintenance e) { + int wait = ThreadLocalRandom.current().nextInt(20, 30 + 1); // max is exclusive + int time_sleep = 1000 * 60 * wait; + 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 { - Calendar next_request = this.nextJobRequest(); - if (next_request != null) { - // wait - Date now = new Date(); - this.gui.status(String.format("Waiting until %tR before requesting job", next_request)); - try { - Thread.sleep(next_request.getTimeInMillis() - now.getTime()); - } - catch (InterruptedException e3) { - - } - catch (IllegalArgumentException e3) { - this.log.error("Client::run sleepB failed " + e3); - } - } - this.gui.status("Requesting Job"); - this.renderingJob = this.server.requestJob(); + Thread.sleep(time_sleep); } - catch (FermeException e1) { - this.renderingJob = null; + catch (InterruptedException e1) { + return -3; } + this.log.removeCheckPoint(step); + continue; // go back to ask job } - } - catch (FermeServerDown e) { - int wait = ThreadLocalRandom.current().nextInt(10, 30 + 1); // max is exclusive - int time_sleep = 1000 * 60 * wait; - this.gui.status(String.format("Can not connect to server. Please check your connectivity. Will retry in %s minutes", wait)); - try { - Thread.sleep(time_sleep); + catch (FermeExceptionBadResponseFromServer e) { + int wait = ThreadLocalRandom.current().nextInt(15, 30 + 1); // max is exclusive + 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); + } + catch (InterruptedException e1) { + return -3; + } + this.log.removeCheckPoint(step); + continue; // go back to ask job } - catch (InterruptedException e1) { - return -3; + catch (FermeException e) { + this.gui.error("Client::run exception requestJob (1) " + e.getMessage()); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + this.log.debug("Client::run exception " + e + " stacktrace: " + sw.toString()); + this.sendError(step); + this.log.removeCheckPoint(step); + continue; } - continue; // go back to ask job - } - catch (FermeExceptionServerOverloaded e) { - int wait = ThreadLocalRandom.current().nextInt(10, 30 + 1); // max is exclusive - int time_sleep = 1000 * 60 * wait; - this.gui.status(String.format("Server is overloaded and cannot give frame to render. Will retry in %s minutes", wait)); - try { - Thread.sleep(time_sleep); + + if (this.renderingJob == null) { // no job + int[] retrySchemeInSeconds = { 300000, 480000, 720000, 900000, 1200000 }; // 5, 8, 12, 15 and 20 minutes + + int time_sleep = retrySchemeInSeconds[(this.noJobRetryIter < retrySchemeInSeconds.length) ? + this.noJobRetryIter++ : + (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) { + try { + Thread.sleep(250); + } + catch (InterruptedException e) { + return -3; + } + time_slept += 250; + } + this.log.removeCheckPoint(step); + continue; // go back to ask job } - catch (InterruptedException e1) { - return -3; + + this.log.debug("Got work to do id: " + this.renderingJob.getId() + " frame: " + this.renderingJob.getFrameNumber()); + + // As the server allocated a new job to this client, reset the no_job waiting algorithm + this.noJobRetryIter = 0; + + ret = this.work(this.renderingJob); + if (ret == Error.Type.RENDERER_KILLED) { + Job frame_to_reset = this.renderingJob; // copy it because the sendError will take ~5min to execute + this.renderingJob = null; + this.gui.error(Error.humanString(ret)); + this.sendError(step, frame_to_reset, ret); + this.log.removeCheckPoint(step); + continue; } - continue; // go back to ask job - } - catch (FermeExceptionServerInMaintenance e) { - int wait = ThreadLocalRandom.current().nextInt(20, 30 + 1); // max is exclusive - int time_sleep = 1000 * 60 * wait; - this.gui.status(String.format("Server is in maintenance and cannot give frame to render. Will retry in %s minutes", wait)); - try { - Thread.sleep(time_sleep); + + if (ret == Error.Type.NO_SPACE_LEFT_ON_DEVICE) { + Job frame_to_reset = this.renderingJob; // copy it because the sendError will take ~5min to execute + this.renderingJob = null; + this.gui.error(Error.humanString(ret)); + this.sendError(step, frame_to_reset, ret); + this.log.removeCheckPoint(step); + return -50; } - catch (InterruptedException e1) { - return -3; + + if (ret != Error.Type.OK) { + Job frame_to_reset = this.renderingJob; // copy it because the sendError will take ~5min to execute + this.renderingJob = null; + this.gui.error(Error.humanString(ret)); + this.sendError(step, frame_to_reset, ret); + this.log.removeCheckPoint(step); + continue; } - continue; // go back to ask job - } - catch (FermeExceptionBadResponseFromServer e) { - int wait = ThreadLocalRandom.current().nextInt(15, 30 + 1); // max is exclusive - int time_sleep = 1000 * 60 * wait; - this.gui.status(String.format("Bad answer from server. Will retry in %s minutes", wait)); - try { - Thread.sleep(time_sleep); + + if (this.renderingJob.isSynchronousUpload() == true) { // power or compute_method job, need to upload right away + this.gui.status(String.format("Uploading frame (%.2fMB)", (this.renderingJob.getOutputImageSize() / 1024.0 / 1024.0))); + + ret = confirmJob(this.renderingJob, step); + if (ret != Error.Type.OK) { + gui.error("Client::run problem with confirmJob (returned " + ret + ")"); + sendError(step, this.renderingJob, Error.Type.VALIDATION_FAILED); + } } - catch (InterruptedException e1) { - return -3; + else { + this.gui.status(String.format("Queuing frame for upload (%.2fMB)", (this.renderingJob.getOutputImageSize() / 1024.0 / 1024.0))); + + this.jobsToValidate.add(new QueuedJob(step, this.renderingJob)); + + this.uploadQueueSize++; + this.uploadQueueVolume += this.renderingJob.getOutputImageSize(); + this.gui.displayUploadQueueStats(uploadQueueSize, uploadQueueVolume); + + this.renderingJob = null; } - continue; // go back to ask job - } - catch (FermeException e) { - this.gui.error("Client::run exception requestJob (1) " + e.getMessage()); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Client::run exception " + e + " stacktrace: " + sw.toString()); - this.sendError(step); - continue; - } - - if (this.renderingJob == null) { // no job - int wait = ThreadLocalRandom.current().nextInt(10, 30 + 1); // max is exclusive - int time_sleep = 1000 * 60 * wait; - Date wakeup_time = new Date(new Date().getTime() + time_sleep); - this.gui.status(String.format("No job available. Sleeping for %d minutes (will wake up at %tR)", wait, wakeup_time)); - this.gui.displayStats(new Stats()); - this.suspended = true; - int time_slept = 0; - while (time_slept < time_sleep && this.running == true) { - try { - Thread.sleep(250); - } - catch (InterruptedException e) { - return -3; + + if (this.shouldWaitBeforeRender() == true) { + this.gui.status("Sending frames. Please wait"); + + while (this.shouldWaitBeforeRender() == true) { + try { + Thread.sleep(4000); // wait a little bit + } + catch (InterruptedException e3) { + } } - time_slept += 250; } - this.suspended = false; - continue; // go back to ask job - } - - this.log.debug("Got work to do id: " + this.renderingJob.getId() + " frame: " + this.renderingJob.getFrameNumber()); - - ret = this.work(this.renderingJob); - if (ret == Error.Type.RENDERER_KILLED) { this.log.removeCheckPoint(step); - continue; } - if (ret == Error.Type.NO_SPACE_LEFT_ON_DEVICE) { - Job frame_to_reset = this.renderingJob; // copy it because the sendError will take ~5min to execute - this.renderingJob = null; - this.gui.error(Error.humanString(ret)); - this.sendError(step, frame_to_reset, ret); - this.log.removeCheckPoint(step); - return -50; - } - - if (ret != Error.Type.OK) { - Job frame_to_reset = this.renderingJob; // copy it because the sendError will take ~5min to execute - this.renderingJob = null; - this.gui.error(Error.humanString(ret)); - this.sendError(step, frame_to_reset, ret); - this.log.removeCheckPoint(step); - continue; - } - - if (this.renderingJob.simultaneousUploadIsAllowed() == false) { // power or compute_method job, need to upload right away - ret = confirmJob(this.renderingJob); - if (ret != Error.Type.OK) { - gui.error("Client::run problem with confirmJob (returned " + ret + ")"); - sendError(step); - } - else { - gui.AddFrameRendered(); - } - } - else { - this.jobsToValidate.add(this.renderingJob); - this.renderingJob = null; - } - - while (this.shouldWaitBeforeRender() == true) { - try { - Thread.sleep(4000); // wait a little bit - } - catch (InterruptedException e3) { - } - } - this.log.removeCheckPoint(step); - } - - // not running but maybe still sending frame - while (this.jobsToValidate.isEmpty() == false) { + // If we reach this point is bc the main loop (the one that controls all the workflow) has exited + // due to user requesting to exit the App and we are just waiting for the upload queue to empty + // If the user cancels the exit, then this.running will be true and the main loop will take + // control again try { Thread.sleep(2300); // wait a little bit + this.gui.status("Uploading rendered frames before exiting. Please wait"); } catch (InterruptedException e3) { } + + // This loop will remain valid until all the background uploads have + // finished (unless the stop() method has been triggered) } + while (this.uploadQueueSize > 0); } catch (Exception e1) { // no exception should be raised in the actual launcher (applet or standalone) @@ -390,24 +421,30 @@ public synchronized int stop() { this.disableErrorSending = true; if (this.renderingJob != null) { + this.gui.status("Stopping"); + if (this.renderingJob.getProcessRender().getProcess() != null) { this.renderingJob.setAskForRendererKill(true); OS.getOS().kill(this.renderingJob.getProcessRender().getProcess()); } } - // this.config.workingDirectory.delete(); - this.config.removeWorkingDirectory(); + // this.configuration.workingDirectory.delete(); + this.configuration.removeWorkingDirectory(); if (this.server == null) { return 0; } - try { - this.server.HTTPRequest(this.server.getPage("logout")); - } - catch (IOException e) { - // nothing to do: if the logout failed that's ok + if (this.server.getPage("logout").isEmpty() == false) { + this.gui.status("Disconnecting from SheepIt server"); + + try { + this.server.HTTPRequest(this.server.getPage("logout")); + } + catch (IOException e) { + // nothing to do: if the logout failed that's ok + } } this.server.interrupt(); try { @@ -421,12 +458,9 @@ public synchronized int stop() { return 0; } - public boolean isSuspended() { - return this.suspended; - } - public void suspend() { suspended = true; + this.gui.status("Client will pause when the current job finishes", true); } public synchronized void resume() { @@ -444,30 +478,45 @@ public void cancelStop() { this.running = true; } - public boolean isRunning() { - return this.running; - } - public int senderLoop() { - int step = log.newCheckPoint(); - Error.Type ret; + int step = -1; + Error.Type ret = null; while (true) { - Job job_to_send; + QueuedJob queuedJob = null; try { - job_to_send = jobsToValidate.take(); - this.log.debug("will validate " + job_to_send); - //gui.status("Sending frame"); - ret = confirmJob(job_to_send); + queuedJob = jobsToValidate.take(); + step = queuedJob.checkpoint; // retrieve the checkpoint attached to the job + + this.log.debug(step, "will validate " + queuedJob.job); + + ret = confirmJob(queuedJob.job, step); if (ret != Error.Type.OK) { this.gui.error(Error.humanString(ret)); - this.log.debug("Client::senderLoop confirm failed, ret: " + ret); - sendError(step); - } - else { - gui.AddFrameRendered(); + this.log.debug(step, "Client::senderLoop confirm failed, ret: " + ret); } } catch (InterruptedException e) { + this.log.error(step, "Client::senderLoop Exception " + e.getMessage()); + } + finally { + if (ret != Error.Type.OK) { + if (queuedJob.job != null) { + sendError(step, queuedJob.job, ret); + } + else { + sendError(step); + } + } + + // Remove the checkpoint information + log.removeCheckPoint(step); + + this.uploadQueueSize--; + if (queuedJob.job != null) { + this.uploadQueueVolume -= queuedJob.job.getOutputImageSize(); + } + + this.gui.displayUploadQueueStats(this.uploadQueueSize, this.uploadQueueVolume); } } } @@ -489,6 +538,34 @@ protected void sendError(int step_, Job job_to_reset_, Error.Type error) { temp_file.deleteOnExit(); FileOutputStream writer = new FileOutputStream(temp_file); + // Create a header with the information summarised for easier admin error analysis + Configuration conf = this.configuration; + CPU cpu = OS.getOS().getCPU(); + + StringBuilder logHeader = new StringBuilder() + .append("====================================================================================================\n") + .append(String.format("%s / %s / %s / SheepIt v%s\n", conf.getLogin(), conf.getHostname(), OS.getOS().name(), conf.getJarVersion())) + .append(String.format("%s x%d %.1f GB RAM\n", cpu.name(), conf.getNbCores(), conf.getMaxMemory() / 1024.0 / 1024.0)); + + if (conf.getComputeMethod() == Configuration.ComputeType.GPU || conf.getComputeMethod() == Configuration.ComputeType.CPU_GPU) { + logHeader + .append(String.format("%s %.1f GB VRAM\n", conf.getGPUDevice().getModel(), conf.getGPUDevice().getMemory() / 1024.0 / 1024.0 / 1024.0)); + } + + logHeader.append("====================================================================================================\n"); + if (job_to_reset_ != null) { + logHeader.append(String.format("Project ::: %s\n", job_to_reset_.getName())).append(String.format("Project id: %s frame: %s\n", job_to_reset_.getId(), job_to_reset_.getFrameNumber())) + .append(String.format("blender ::: %s\n\n", job_to_reset_.getBlenderLongVersion())).append(String.format("ERROR Type :: %s\n", error)); + } + else { + logHeader.append("Project ::: No project allocated.\n") + .append(String.format("ERROR Type :: %s\n", (error != null ? error : "N/A"))); + } + logHeader.append("====================================================================================================\n\n"); + + // Insert the info at the beginning of the error log + writer.write(logHeader.toString().getBytes()); + ArrayList logs = this.log.getForCheckPoint(step_); for (String line : logs) { writer.write(line.getBytes()); @@ -498,12 +575,13 @@ protected void sendError(int step_, Job job_to_reset_, Error.Type error) { writer.close(); String args = "?type=" + (error == null ? "" : error.getValue()); if (job_to_reset_ != null) { - args += "&frame=" + job_to_reset_.getFrameNumber() + "&job=" + job_to_reset_.getId() + "&render_time=" + job_to_reset_.getProcessRender().getDuration() + "&memoryused=" + job_to_reset_.getProcessRender().getMemoryUsed(); + args += "&frame=" + job_to_reset_.getFrameNumber() + "&job=" + job_to_reset_.getId() + "&render_time=" + job_to_reset_.getProcessRender() + .getDuration() + "&memoryused=" + job_to_reset_.getProcessRender().getMemoryUsed(); if (job_to_reset_.getExtras() != null && job_to_reset_.getExtras().isEmpty() == false) { args += "&extras=" + job_to_reset_.getExtras(); } } - this.server.HTTPSendFile(this.server.getPage("error") + args, temp_file.getAbsolutePath()); + this.server.HTTPSendFile(this.server.getPage("error") + args, temp_file.getAbsolutePath(), step_); temp_file.delete(); } catch (Exception e) { @@ -514,7 +592,8 @@ protected void sendError(int step_, Job job_to_reset_, Error.Type error) { // no exception should be raised to actual launcher (applet or standalone) } - if (error != null && (error == Error.Type.RENDERER_CRASHED || error == Error.Type.RENDERER_KILLED_BY_USER || error == Error.Type.RENDERER_KILLED_BY_SERVER)) { + if (error != null && (error == Error.Type.RENDERER_CRASHED || error == Error.Type.RENDERER_KILLED_BY_USER + || error == Type.RENDERER_KILLED_BY_USER_OVER_TIME || error == Error.Type.RENDERER_KILLED_BY_SERVER)) { // do nothing, we can ask for a job right away } else { @@ -527,17 +606,16 @@ protected void sendError(int step_, Job job_to_reset_, Error.Type error) { } /** - * * @return the date of the next request, or null if there is not delay (null <=> now) */ public Calendar nextJobRequest() { - if (this.config.requestTime == null) { + if (this.configuration.getRequestTime() == null) { return null; } else { Calendar next = null; Calendar now = Calendar.getInstance(); - for (Pair interval : this.config.requestTime) { + for (Pair interval : this.configuration.getRequestTime()) { Calendar start = (Calendar) now.clone(); Calendar end = (Calendar) now.clone(); start.set(Calendar.SECOND, 00); @@ -551,15 +629,8 @@ public Calendar nextJobRequest() { if (start.before(now) && now.before(end)) { return null; } - if (start.after(now)) { - if (next == null) { - next = start; - } - else { - if (start.before(next)) { - next = start; - } - } + if (next == null || (start.before(next) && start.after(now))) { + next = start; } } @@ -567,7 +638,7 @@ public Calendar nextJobRequest() { } } - public Error.Type work(Job ajob) { + public Error.Type work(final Job ajob) { int ret; gui.setRenderingProjectName(ajob.getName()); @@ -599,65 +670,87 @@ public Error.Type work(Job ajob) { return Error.Type.NO_SPACE_LEFT_ON_DEVICE; } - File scene_file = new File(ajob.getScenePath()); + final File scene_file = new File(ajob.getScenePath()); File renderer_file = new File(ajob.getRendererPath()); if (scene_file.exists() == false) { gui.setRenderingProjectName(""); - this.log.error("Client::work job preparation failed (scene file '" + scene_file.getAbsolutePath() + "' does not exist)"); + this.log.error("Client::work job preparation failed (scene file '" + scene_file.getAbsolutePath() + + "' does not exist), cleaning directory in hope to recover"); + this.configuration.cleanWorkingDirectory(); return Error.Type.MISSING_SCENE; } if (renderer_file.exists() == false) { gui.setRenderingProjectName(""); - this.log.error("Client::work job preparation failed (renderer file '" + renderer_file.getAbsolutePath() + "' does not exist)"); - return Error.Type.MISSING_RENDER; + this.log.error("Client::work job preparation failed (renderer file '" + renderer_file.getAbsolutePath() + + "' does not exist), cleaning directory in hope to recover"); + this.configuration.cleanWorkingDirectory(); + return Error.Type.MISSING_RENDERER; } - Error.Type err = ajob.render(); + Observer removeSceneDirectoryOnceRenderHasStartedObserver = new Observer() { + @Override public void update(Observable observable, Object o) { + // only remove the .blend since it's most important data + // and it's the only file we are sure will not be needed anymore + scene_file.delete(); + } + }; + + Error.Type err = ajob.render(removeSceneDirectoryOnceRenderHasStartedObserver); gui.setRenderingProjectName(""); gui.setRemainingTime(""); gui.setRenderingTime(""); gui.setComputeMethod(""); if (err != Error.Type.OK) { this.log.error("Client::work problem with runRenderer (ret " + err + ")"); + if (err == Error.Type.RENDERER_CRASHED_PYTHON_ERROR) { + this.log.error("Client::work failed with python error, cleaning directory in hope to recover"); + this.configuration.cleanWorkingDirectory(); + } return err; } + removeSceneDirectory(ajob); + return Error.Type.OK; } protected int downloadSceneFile(Job ajob_) throws FermeExceptionNoSpaceLeftOnDevice { - return this.downloadFile(ajob_, ajob_.getSceneArchivePath(), ajob_.getSceneMD5(), String.format("%s?type=job&job=%s", this.server.getPage("download-archive"), ajob_.getId()), "project"); + return this.downloadFile(ajob_, ajob_.getSceneArchivePath(), ajob_.getSceneMD5(), + String.format("%s?type=job&job=%s", this.server.getPage("download-archive"), ajob_.getId()), "project"); } protected int downloadExecutable(Job ajob) throws FermeExceptionNoSpaceLeftOnDevice { - return this.downloadFile(ajob, ajob.getRendererArchivePath(), ajob.getRenderMd5(), String.format("%s?type=binary&job=%s", this.server.getPage("download-archive"), ajob.getId()), "renderer"); + return this.downloadFile(ajob, ajob.getRendererArchivePath(), ajob.getRendererMD5(), + String.format("%s?type=binary&job=%s", this.server.getPage("download-archive"), ajob.getId()), "renderer"); } private int downloadFile(Job ajob, String local_path, String md5_server, String url, String download_type) throws FermeExceptionNoSpaceLeftOnDevice { File local_path_file = new File(local_path); - String update_ui = "Downloading " + download_type + " %s %%"; + String update_ui = "Downloading " + download_type; if (local_path_file.exists() == true) { this.gui.status("Reusing cached " + download_type); return 0; } - this.gui.status("Downloading " + download_type); + this.gui.status(String.format("Downloading %s", download_type), 0, 0); // must download the archive int ret = this.server.HTTPGetFile(url, local_path, this.gui, update_ui); + + // Try to check the download file even if a download error has occurred (MD5 file check will delete the file if partially downloaded) boolean md5_check = this.checkFile(ajob, local_path, md5_server); int attempts = 1; while ((ret != 0 || md5_check == false) && attempts < this.maxDownloadFileAttempts) { if (ret != 0) { - this.gui.error("Client::downloadFile problem with Server.HTTPGetFile returned " + ret); + this.gui.error(String.format("Unable to download %s (error %d). Retrying now", download_type, ret)); this.log.debug("Client::downloadFile problem with Server.HTTPGetFile (return: " + ret + ") removing local file (path: " + local_path + ")"); } else if (md5_check == false) { - this.gui.error("Client::downloadFile problem with Client::checkFile mismatch on md5"); + this.gui.error(String.format("Verification of downloaded %s has failed. Retrying now", download_type)); this.log.debug("Client::downloadFile problem with Client::checkFile mismatch on md5, removing local file (path: " + local_path + ")"); } local_path_file.delete(); @@ -669,7 +762,8 @@ else if (md5_check == false) { attempts++; if ((ret != 0 || md5_check == false) && attempts >= this.maxDownloadFileAttempts) { - this.log.debug("Client::downloadFile failed after " + this.maxDownloadFileAttempts + " attempts, removing local file (path: " + local_path + "), stopping..."); + this.log.debug("Client::downloadFile failed after " + this.maxDownloadFileAttempts + " attempts, removing local file (path: " + local_path + + "), stopping..."); local_path_file.delete(); return -9; } @@ -689,13 +783,19 @@ private boolean checkFile(Job ajob, String local_path, String md5_server) { String md5_local = Utils.md5(local_path); if (md5_local.equals(md5_server) == false) { - this.log.error("Client::checkFile mismatch on md5 local: '" + md5_local + "' server: '" + md5_server + "' (local size: " + new File(local_path).length() + ")"); + this.log.error( + "Client::checkFile mismatch on md5 local: '" + md5_local + "' server: '" + md5_server + "' (local size: " + new File(local_path).length() + + ")"); return false; } return true; } + protected void removeSceneDirectory(Job ajob) { + Utils.delete(new File(ajob.getSceneDirectory())); + } + protected int prepareWorkingDirectory(Job ajob) throws FermeExceptionNoSpaceLeftOnDevice { int ret; String renderer_archive = ajob.getRendererArchivePath(); @@ -713,8 +813,10 @@ protected int prepareWorkingDirectory(Job ajob) throws FermeExceptionNoSpaceLeft // unzip the archive ret = Utils.unzipFileIntoDirectory(renderer_archive, renderer_path, null, log); if (ret != 0) { - this.log.error("Client::prepareWorkingDirectory, error(1) with Utils.unzipFileIntoDirectory(" + renderer_archive + ", " + renderer_path + ") returned " + ret); - this.gui.error("Client::prepareWorkingDirectory, error with Utils.unzipFileIntoDirectory of the renderer (returned " + ret + ")"); + this.log.error( + "Client::prepareWorkingDirectory, error(1) with Utils.unzipFileIntoDirectory(" + renderer_archive + ", " + renderer_path + ") returned " + + ret); + this.gui.error(String.format("Unable to extract the renderer (error %d)", ret)); return -1; } @@ -739,10 +841,12 @@ protected int prepareWorkingDirectory(Job ajob) throws FermeExceptionNoSpaceLeft scene_path_file.mkdir(); // unzip the archive - ret = Utils.unzipFileIntoDirectory(scene_archive, scene_path, ajob.getSceneArchivePassword(), log); + ret = Utils.unzipFileIntoDirectory(scene_archive, scene_path, ajob.getPassword(), log); if (ret != 0) { - this.log.error("Client::prepareWorkingDirectory, error(2) with Utils.unzipFileIntoDirectory(" + scene_archive + ", " + scene_path + ") returned " + ret); - this.gui.error("Client::prepareWorkingDirectory, error with Utils.unzipFileIntoDirectory of the scene (returned " + ret + ")"); + this.log.error( + "Client::prepareWorkingDirectory, error(2) with Utils.unzipFileIntoDirectory(" + scene_archive + ", " + scene_path + ") returned " + + ret); + this.gui.error(String.format("Unable to extract the scene (error %d)", ret)); return -2; } } @@ -750,35 +854,35 @@ protected int prepareWorkingDirectory(Job ajob) throws FermeExceptionNoSpaceLeft return 0; } - protected Error.Type confirmJob(Job ajob) { - String extras_config = ""; - RenderProcess process = ajob.getProcessRender(); - if (process != null && process.getCoresUsed() > 0) { - extras_config = "&cores=" + process.getCoresUsed(); - } - - String url_real = String.format("%s?job=%s&frame=%s&rendertime=%d&memoryused=%s&extras=%s%s", this.server.getPage("validate-job"), ajob.getId(), ajob.getFrameNumber(), ajob.getProcessRender().getDuration(), ajob.getProcessRender().getMemoryUsed(), ajob.getExtras(), extras_config); + protected Error.Type confirmJob(Job ajob, int checkpoint) { + String url_real = String.format("%s&rendertime=%d&memoryused=%s", ajob.getValidationUrl(), ajob.getProcessRender().getDuration(), + ajob.getProcessRender().getMemoryUsed()); + this.log.debug(checkpoint, "Client::confirmeJob url " + url_real); + this.log.debug(checkpoint, "path frame " + ajob.getOutputImagePath()); this.isValidatingJob = true; int nb_try = 1; int max_try = 3; ServerCode ret = ServerCode.UNKNOWN; + Type confirmJobReturnCode = Error.Type.OK; + retryLoop: while (nb_try < max_try && ret != ServerCode.OK) { - ret = this.server.HTTPSendFile(url_real, ajob.getOutputImagePath()); + ret = this.server.HTTPSendFile(url_real, ajob.getOutputImagePath(), checkpoint); switch (ret) { case OK: // no issue, exit the loop - nb_try = max_try; - break; + break retryLoop; case JOB_VALIDATION_ERROR_SESSION_DISABLED: case JOB_VALIDATION_ERROR_BROKEN_MACHINE: - return Type.SESSION_DISABLED; - + confirmJobReturnCode = Error.Type.SESSION_DISABLED; + break retryLoop; + case JOB_VALIDATION_ERROR_MISSING_PARAMETER: // no point to retry the request - return Error.Type.UNKNOWN; - + confirmJobReturnCode = Error.Type.UNKNOWN; + break retryLoop; + default: // do nothing, try to do a request on the next loop break; @@ -787,28 +891,28 @@ protected Error.Type confirmJob(Job ajob) { nb_try++; if (ret != ServerCode.OK && nb_try < max_try) { try { - this.log.debug("Sleep for 32s before trying to re-upload the frame"); + this.log.debug(checkpoint, "Sleep for 32s before trying to re-upload the frame"); Thread.sleep(32000); } catch (InterruptedException e) { - return Error.Type.UNKNOWN; + confirmJobReturnCode = Error.Type.UNKNOWN; } } } + this.isValidatingJob = false; + this.previousJob = ajob; + + if (confirmJobReturnCode == Error.Type.OK) { + gui.AddFrameRendered(); + } // we can remove the frame file File frame = new File(ajob.getOutputImagePath()); frame.delete(); ajob.setOutputImagePath(null); - this.isValidatingJob = false; - this.previousJob = ajob; - return Error.Type.OK; - } - - public Job getPreviousJob() { - return this.previousJob; + return confirmJobReturnCode; } protected boolean shouldWaitBeforeRender() { @@ -816,6 +920,16 @@ protected boolean shouldWaitBeforeRender() { if (this.isValidatingJob) { concurrent_job++; } - return (concurrent_job >= this.config.maxUploadingJob()); + return (concurrent_job >= this.configuration.getMaxUploadingJob()); + } + + /**************** + * Inner class that will hold the queued jobs. The constructor accepts two parameters: + * @int checkpoint - the checkpoint associated with the job (to add any additional log to the render output) + * @Job job - the job to be validated + */ + @AllArgsConstructor class QueuedJob { + final private int checkpoint; + final private Job job; } } diff --git a/src/com/sheepit/client/Configuration.java b/src/com/sheepit/client/Configuration.java index 172d703e..d87cd6aa 100644 --- a/src/com/sheepit/client/Configuration.java +++ b/src/com/sheepit/client/Configuration.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -34,35 +34,38 @@ import com.sheepit.client.hardware.cpu.CPU; import com.sheepit.client.hardware.gpu.GPUDevice; import com.sheepit.client.os.OS; +import lombok.Data; -public class Configuration { +@Data public class Configuration { public enum ComputeType { CPU_GPU, CPU, GPU } // accept job for ... private String configFilePath; - public File workingDirectory; - public File storageDirectory; // for permanent storage (binary archive) - public boolean userHasSpecifiedACacheDir; - public String static_exeDirName; + private File workingDirectory; + private File storageDirectory; // for permanent storage (binary archive) + private boolean userHasSpecifiedACacheDir; + private String static_exeDirName; private String login; private String password; private String proxy; private int maxUploadingJob; private int nbCores; - private int maxMemory; // max memory allowed for render + private long maxMemory; // max memory allowed for render private int maxRenderTime; // max render time per frame allowed private int priority; private ComputeType computeMethod; private GPUDevice GPUDevice; + private int renderbucketSize; private boolean detectGPUs; private boolean printLog; - public List> requestTime; + private List> requestTime; private String extras; private boolean autoSignIn; + private boolean useSysTray; private String UIType; - private int tileSize; private String hostname; + private String theme; public Configuration(File cache_dir_, String login_, String password_) { this.configFilePath = null; @@ -78,6 +81,7 @@ public Configuration(File cache_dir_, String login_, String password_) { this.priority = 19; // default lowest this.computeMethod = null; this.GPUDevice = null; + this.renderbucketSize = -1; this.userHasSpecifiedACacheDir = false; this.detectGPUs = true; this.workingDirectory = null; @@ -87,86 +91,14 @@ public Configuration(File cache_dir_, String login_, String password_) { this.requestTime = null; this.extras = ""; this.autoSignIn = false; + this.useSysTray = true; this.UIType = null; - this.tileSize = -1; // ie not set + this.theme = null; } - public String toString() { return String.format("Configuration (workingDirectory '%s')", this.workingDirectory.getAbsolutePath()); } - - public String getConfigFilePath() { - return this.configFilePath; - } - - public void setConfigPath(String val) { - this.configFilePath = val; - } - - public String login() { - return this.login; - } - - public void setLogin(String login_) { - this.login = login_; - } - - public String password() { - return this.password; - } - - public void setPassword(String password_) { - this.password = password_; - } - - public String getProxy() { - return this.proxy; - } - - public void setProxy(String url) { - this.proxy = url; - } - - public int maxUploadingJob() { - return this.maxUploadingJob; - } - - public GPUDevice getGPUDevice() { - return this.GPUDevice; - } - - public boolean getDetectGPUs() { - return this.detectGPUs; - } - - public void setMaxUploadingJob(int max) { - this.maxUploadingJob = max; - } - - public void setUseNbCores(int nbcores) { - this.nbCores = nbcores; - } - - public int getNbCores() { - return this.nbCores; - } - - public void setMaxMemory(int max) { - this.maxMemory = max; - } - - public int getMaxMemory() { - return this.maxMemory; - } - - public void setMaxRenderTime(int max) { - this.maxRenderTime = max; - } - - public int getMaxRenderTime() { - return this.maxRenderTime; - } public void setUsePriority(int priority) { if (priority > 19) @@ -178,38 +110,10 @@ public void setUsePriority(int priority) { } - public int getPriority() { - return this.priority; - } - - public void setPrintLog(boolean val) { - this.printLog = val; - } - - public boolean getPrintLog() { - return this.printLog; - } - public int computeMethodToInt() { return this.computeMethod.ordinal(); } - public ComputeType getComputeMethod() { - return this.computeMethod; - } - - public void setUseGPU(GPUDevice device) { - this.GPUDevice = device; - } - - public void setDetectGPUs(boolean val) { - this.detectGPUs = val; - } - - public void setComputeMethod(ComputeType meth) { - this.computeMethod = meth; - } - public void setCacheDir(File cache_dir_) { removeWorkingDirectory(); if (cache_dir_ == null) { @@ -221,7 +125,7 @@ public void setCacheDir(File cache_dir_) { this.workingDirectory.mkdir(); this.workingDirectory.deleteOnExit(); - // since there is no working directory and the client will be working in the system temp directory, + // since there is no working directory and the client will be working in the system temp directory, // we can also set up a 'permanent' directory for immutable files (like renderer binary) this.storageDirectory = new File(this.workingDirectory.getParent() + File.separator + "sheepit_binary_cache"); @@ -259,12 +163,8 @@ public File getStorageDir() { } } - public boolean getUserHasSpecifiedACacheDir() { - return this.userHasSpecifiedACacheDir; - } - public File getCacheDirForSettings() { - if (this.getUserHasSpecifiedACacheDir() == false) { + if (this.userHasSpecifiedACacheDir == false) { return null; } else { @@ -273,46 +173,6 @@ public File getCacheDirForSettings() { } } - public void setExtras(String str) { - this.extras = str; - } - - public String getExtras() { - return this.extras; - } - - public void setAutoSignIn(boolean v) { - this.autoSignIn = v; - } - - public boolean getAutoSignIn() { - return this.autoSignIn; - } - - public void setUIType(String ui) { - this.UIType = ui; - } - - public String getUIType() { - return this.UIType; - } - - public void setTileSize(int size) { - this.tileSize = size; - } - - public int getTileSize() { - return this.tileSize; - } - - public void setHostname(String hostname_) { - this.hostname = hostname_; - } - - public String getHostname() { - return this.hostname; - } - public String getDefaultHostname() { try { return InetAddress.getLocalHost().getHostName(); @@ -417,7 +277,7 @@ public String getJarVersion() { InputStream versionStream = Client.class.getResourceAsStream(versionPath); if (versionStream == null) { System.err.println("Configuration::getJarVersion Failed to get version file"); - return ""; + return "6.0.0"; } try { @@ -429,7 +289,7 @@ public String getJarVersion() { } catch (IOException ex) { System.err.println("Configuration::getJarVersion error while reading manifest file (" + versionPath + "): " + ex.getMessage()); - return ""; + return "6.0.0"; } } diff --git a/src/com/sheepit/client/Error.java b/src/com/sheepit/client/Error.java index b4ab5c4a..5389b811 100644 --- a/src/com/sheepit/client/Error.java +++ b/src/com/sheepit/client/Error.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -22,36 +22,14 @@ public class Error { public enum Type { // id have to be kept synchronised with the server side. - OK(0), - UNKNOWN(99), - WRONG_CONFIGURATION(1), - AUTHENTICATION_FAILED(2), - TOO_OLD_CLIENT(3), - SESSION_DISABLED(4), - RENDERER_NOT_AVAILABLE(5), - MISSING_RENDER(6), - MISSING_SCENE(7), - NOOUTPUTFILE(8), - DOWNLOAD_FILE(9), - CAN_NOT_CREATE_DIRECTORY(10), - NETWORK_ISSUE(11), - RENDERER_CRASHED(12), - RENDERER_CRASHED_PYTHON_ERROR(24), - RENDERER_OUT_OF_VIDEO_MEMORY(13), - RENDERER_OUT_OF_MEMORY(21), - RENDERER_KILLED(14), - RENDERER_KILLED_BY_USER(20), - RENDERER_KILLED_BY_USER_OVER_TIME(23), - RENDERER_KILLED_BY_SERVER(22), - RENDERER_MISSING_LIBRARIES(15), - FAILED_TO_EXECUTE(16), - OS_NOT_SUPPORTED(17), - CPU_NOT_SUPPORTED(18), - GPU_NOT_SUPPORTED(19), + OK(0), UNKNOWN(99), WRONG_CONFIGURATION(1), AUTHENTICATION_FAILED(2), TOO_OLD_CLIENT(3), SESSION_DISABLED(4), RENDERER_NOT_AVAILABLE( + 5), MISSING_RENDERER(6), MISSING_SCENE(7), NOOUTPUTFILE(8), DOWNLOAD_FILE(9), CAN_NOT_CREATE_DIRECTORY(10), NETWORK_ISSUE(11), RENDERER_CRASHED( + 12), RENDERER_CRASHED_PYTHON_ERROR(24), RENDERER_OUT_OF_VIDEO_MEMORY(13), RENDERER_OUT_OF_MEMORY(21), RENDERER_KILLED( + 14), RENDERER_KILLED_BY_USER(20), RENDERER_KILLED_BY_USER_OVER_TIME(23), RENDERER_KILLED_BY_SERVER(22), RENDERER_MISSING_LIBRARIES( + 15), FAILED_TO_EXECUTE(16), OS_NOT_SUPPORTED(17), CPU_NOT_SUPPORTED(18), GPU_NOT_SUPPORTED(19), VALIDATION_FAILED(25), // internal error handling - NO_SPACE_LEFT_ON_DEVICE(100), - ERROR_BAD_RESPONSE(101), + NO_SPACE_LEFT_ON_DEVICE(100), ERROR_BAD_RESPONSE(101), ; private final int id; @@ -66,36 +44,23 @@ public int getValue() { } public enum ServerCode { - OK(0), - UNKNOWN(999), + OK(0), UNKNOWN(999), - CONFIGURATION_ERROR_NO_CLIENT_VERSION_GIVEN(100), - CONFIGURATION_ERROR_CLIENT_TOO_OLD(101), - CONFIGURATION_ERROR_AUTH_FAILED(102), - CONFIGURATION_ERROR_WEB_SESSION_EXPIRED(103), - CONFIGURATION_ERROR_MISSING_PARAMETER(104), + CONFIGURATION_ERROR_NO_CLIENT_VERSION_GIVEN(100), CONFIGURATION_ERROR_CLIENT_TOO_OLD(101), CONFIGURATION_ERROR_AUTH_FAILED( + 102), CONFIGURATION_ERROR_WEB_SESSION_EXPIRED(103), CONFIGURATION_ERROR_MISSING_PARAMETER(104), - JOB_REQUEST_NOJOB(200), - JOB_REQUEST_ERROR_NO_RENDERING_RIGHT(201), - JOB_REQUEST_ERROR_DEAD_SESSION(202), - JOB_REQUEST_ERROR_SESSION_DISABLED(203), - JOB_REQUEST_ERROR_INTERNAL_ERROR(204), - JOB_REQUEST_ERROR_RENDERER_NOT_AVAILABLE(205), - JOB_REQUEST_SERVER_IN_MAINTENANCE(206), - JOB_REQUEST_SERVER_OVERLOADED(207), + JOB_REQUEST_NOJOB(200), JOB_REQUEST_ERROR_NO_RENDERING_RIGHT(201), JOB_REQUEST_ERROR_DEAD_SESSION(202), JOB_REQUEST_ERROR_SESSION_DISABLED( + 203), JOB_REQUEST_ERROR_INTERNAL_ERROR(204), JOB_REQUEST_ERROR_RENDERER_NOT_AVAILABLE(205), JOB_REQUEST_SERVER_IN_MAINTENANCE( + 206), JOB_REQUEST_SERVER_OVERLOADED(207), - JOB_VALIDATION_ERROR_MISSING_PARAMETER(300), - JOB_VALIDATION_ERROR_BROKEN_MACHINE(301), // in GPU the generated frame is black - JOB_VALIDATION_ERROR_FRAME_IS_NOT_IMAGE(302), - JOB_VALIDATION_ERROR_UPLOAD_FAILED(303), - JOB_VALIDATION_ERROR_SESSION_DISABLED(304), // missing heartbeat or broken machine + JOB_VALIDATION_ERROR_MISSING_PARAMETER(300), JOB_VALIDATION_ERROR_BROKEN_MACHINE(301), // in GPU the generated frame is black + JOB_VALIDATION_ERROR_FRAME_IS_NOT_IMAGE(302), JOB_VALIDATION_ERROR_UPLOAD_FAILED(303), JOB_VALIDATION_ERROR_SESSION_DISABLED( + 304), // missing heartbeat or broken machine KEEPMEALIVE_STOP_RENDERING(400), // internal error handling - ERROR_NO_ROOT(2), - ERROR_BAD_RESPONSE(3), - ERROR_REQUEST_FAILED(5); + ERROR_NO_ROOT(2), ERROR_BAD_RESPONSE(3), ERROR_REQUEST_FAILED(5); private final int id; @@ -128,15 +93,15 @@ public static Type ServerCodeToType(ServerCode sc) { return Type.TOO_OLD_CLIENT; case CONFIGURATION_ERROR_AUTH_FAILED: return Type.AUTHENTICATION_FAILED; - + case CONFIGURATION_ERROR_NO_CLIENT_VERSION_GIVEN: case CONFIGURATION_ERROR_WEB_SESSION_EXPIRED: return Type.WRONG_CONFIGURATION; - + case JOB_REQUEST_ERROR_SESSION_DISABLED: case JOB_VALIDATION_ERROR_SESSION_DISABLED: return Type.SESSION_DISABLED; - + case JOB_REQUEST_ERROR_RENDERER_NOT_AVAILABLE: return Type.RENDERER_NOT_AVAILABLE; @@ -181,6 +146,8 @@ public static String humanString(Type in) { return "The server has disabled your session. Your client may have generated a broken frame (GPU not compatible, not enough RAM/VRAM, etc)."; case RENDERER_NOT_AVAILABLE: return "No renderer are available on the server for your machine."; + case MISSING_RENDERER: + return "Unable to locate the Blender renderer within the binary download."; case OS_NOT_SUPPORTED: return "Operating System not supported."; case CPU_NOT_SUPPORTED: diff --git a/src/com/sheepit/client/Gui.java b/src/com/sheepit/client/Gui.java index dabaa223..71df53d5 100644 --- a/src/com/sheepit/client/Gui.java +++ b/src/com/sheepit/client/Gui.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -26,6 +26,14 @@ public interface Gui { public void status(String msg_); + public void status(String msg_, boolean overwriteSuspendedMsg); + + public void status(String msg_, int progress); + + public void status(String msg_, int progress, long size); + + public void updateTrayIcon(Integer percentage_); + public void setRenderingProjectName(String name_); public void setRemainingTime(String time_); @@ -34,10 +42,14 @@ public interface Gui { public void displayStats(Stats stats); + public void displayUploadQueueStats(int queueSize, long queueVolume); + public void error(String err_); public void AddFrameRendered(); + public void successfulAuthenticationEvent(String publickey); + public void setClient(Client cli); public void setComputeMethod(String computeMethod_); diff --git a/src/com/sheepit/client/Job.java b/src/com/sheepit/client/Job.java index a04c4de2..3939c9c3 100644 --- a/src/com/sheepit/client/Job.java +++ b/src/com/sheepit/client/Job.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -35,50 +35,65 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Observable; +import java.util.Observer; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; +import java.util.regex.Pattern; +import java.util.regex.Matcher; import com.sheepit.client.Configuration.ComputeType; import com.sheepit.client.Error.Type; import com.sheepit.client.hardware.gpu.GPUDevice; import com.sheepit.client.hardware.gpu.opencl.OpenCL; import com.sheepit.client.os.OS; +import lombok.Data; +import lombok.Getter; -public class Job { +@Data public class Job { public static final String UPDATE_METHOD_BY_REMAINING_TIME = "remainingtime"; public static final String UPDATE_METHOD_BLENDER_INTERNAL_BY_PART = "blenderinternal"; public static final String UPDATE_METHOD_BY_TILE = "by_tile"; - private String numFrame; + public static final int SHOW_BASE_ICON = -1; + + private String frameNumber; private String sceneMD5; private String rendererMD5; private String id; - private String pictureFilename; + private String outputImagePath; + private long outputImageSize; private String path; // path inside of the archive private String rendererCommand; + private String validationUrl; private String script; private boolean useGPU; private String name; private String password; private String extras; private String updateRenderingStatusMethod; + private String blenderShortVersion; + private String blenderLongVersion; private boolean synchronousUpload; private RenderProcess render; private boolean askForRendererKill; private boolean userBlockJob; private boolean serverBlockJob; private Gui gui; - private Configuration config; + private Configuration configuration; private Log log; - public Job(Configuration config_, Gui gui_, Log log_, String id_, String frame_, String path_, boolean use_gpu, String command_, String script_, String sceneMd5_, String rendererMd5_, String name_, String password_, String extras_, boolean synchronous_upload_, String update_method_) { - config = config_; + public Job(Configuration config_, Gui gui_, Log log_, String id_, String frame_, String path_, boolean use_gpu, String command_, String validationUrl_, + String script_, String sceneMd5_, String rendererMd5_, String name_, String password_, String extras_, boolean synchronous_upload_, + String update_method_) { + configuration = config_; id = id_; - numFrame = frame_; + frameNumber = frame_; path = path_; useGPU = use_gpu; rendererCommand = command_; + validationUrl = validationUrl_; sceneMD5 = sceneMd5_; rendererMD5 = rendererMd5_; name = name_; @@ -86,7 +101,8 @@ public Job(Configuration config_, Gui gui_, Log log_, String id_, String frame_, extras = extras_; synchronousUpload = synchronous_upload_; gui = gui_; - pictureFilename = null; + outputImagePath = null; + outputImageSize = 0; script = script_; updateRenderingStatusMethod = update_method_; askForRendererKill = false; @@ -94,6 +110,8 @@ public Job(Configuration config_, Gui gui_, Log log_, String id_, String frame_, serverBlockJob = false; log = log_; render = new RenderProcess(); + blenderShortVersion = null; + blenderLongVersion = null; } public void block() { @@ -110,83 +128,9 @@ public RenderProcess getProcessRender() { } public String toString() { - return String.format("Job (numFrame '%s' sceneMD5 '%s' rendererMD5 '%s' ID '%s' pictureFilename '%s' jobPath '%s' gpu %s name '%s' extras '%s' updateRenderingStatusMethod '%s' render %s)", numFrame, sceneMD5, rendererMD5, id, pictureFilename, path, useGPU, name, extras, updateRenderingStatusMethod, render); - } - - public String getId() { - return id; - } - - public String getFrameNumber() { - return numFrame; - } - - public String getExtras() { - return extras; - } - - public String getScript() { - return script; - } - - public String getSceneMD5() { - return sceneMD5; - } - - public String getRenderMd5() { - return rendererMD5; - } - - public String getPath() { - return path; - } - - public String getUpdateRenderingStatusMethod() { - return updateRenderingStatusMethod; - } - - public void setAskForRendererKill(boolean val) { - askForRendererKill = val; - } - - public boolean getAskForRendererKill() { - return askForRendererKill; - } - - public void setUserBlockJob(boolean val) { - userBlockJob = val; - } - - public boolean getUserBlockJob() { - return userBlockJob; - } - - public void setServerBlockJob(boolean val) { - serverBlockJob = val; - } - - public boolean getServerBlockJob() { - return serverBlockJob; - } - - public String getRenderCommand() { - return rendererCommand; - } - - public boolean getUseGPU() { - return useGPU; - } - - public String getName() { - return name; - } - - public void setOutputImagePath(String path) { - pictureFilename = path; - } - - public String getOutputImagePath() { - return pictureFilename; + return String + .format("Job (numFrame '%s' sceneMD5 '%s' rendererMD5 '%s' ID '%s' pictureFilename '%s' jobPath '%s' gpu %s name '%s' extras '%s' updateRenderingStatusMethod '%s' render %s)", + frameNumber, sceneMD5, rendererMD5, id, outputImagePath, path, useGPU, name, extras, updateRenderingStatusMethod, render); } public String getPrefixOutputImage() { @@ -194,7 +138,7 @@ public String getPrefixOutputImage() { } public String getRendererDirectory() { - return config.workingDirectory.getAbsolutePath() + File.separator + rendererMD5; + return configuration.getWorkingDirectory().getAbsolutePath() + File.separator + rendererMD5; } public String getRendererPath() { @@ -202,11 +146,11 @@ public String getRendererPath() { } public String getRendererArchivePath() { - return config.getStorageDir().getAbsolutePath() + File.separator + rendererMD5 + ".zip"; + return configuration.getStorageDir().getAbsolutePath() + File.separator + rendererMD5 + ".zip"; } public String getSceneDirectory() { - return config.workingDirectory.getAbsolutePath() + File.separator + sceneMD5; + return configuration.getWorkingDirectory().getAbsolutePath() + File.separator + sceneMD5; } public String getScenePath() { @@ -214,18 +158,10 @@ public String getScenePath() { } public String getSceneArchivePath() { - return config.workingDirectory.getAbsolutePath() + File.separator + sceneMD5 + ".zip"; - } - - public String getSceneArchivePassword() { - return password; - } - - public boolean simultaneousUploadIsAllowed() { - return synchronousUpload; + return configuration.getWorkingDirectory().getAbsolutePath() + File.separator + sceneMD5 + ".zip"; } - public Error.Type render() { + public Error.Type render(Observer renderStarted) { gui.status("Rendering"); RenderProcess process = getProcessRender(); Timer timerOfMaxRenderTime = null; @@ -233,26 +169,31 @@ public Error.Type render() { // When sending Ctrl+C to the terminal it also get's sent to all subprocesses e.g. also the render process. // The java program handles Ctrl+C but the renderer quits on Ctrl+C. // This script causes the renderer to ignore Ctrl+C. - String ignore_signal_script= "import signal\n" - + "def hndl(signum, frame):\n" - + " pass\n" - + "signal.signal(signal.SIGINT, hndl)\n"; - if (getUseGPU() && config.getGPUDevice() != null && config.getComputeMethod() != ComputeType.CPU) { - core_script = "sheepit_set_compute_device(\"" + config.getGPUDevice().getType() + "\", \"GPU\", \"" + config.getGPUDevice().getId() + "\")\n"; + String ignore_signal_script = "import signal\n" + "def hndl(signum, frame):\n" + " pass\n" + "signal.signal(signal.SIGINT, hndl)\n"; + if (isUseGPU() && configuration.getGPUDevice() != null && configuration.getComputeMethod() != ComputeType.CPU) { + // If using a GPU, check the proper tile size + int tileSize = configuration.getGPUDevice().getRenderbucketSize(); + + core_script = "sheepit_set_compute_device(\"" + configuration.getGPUDevice().getType() + "\", \"GPU\", \"" + configuration.getGPUDevice().getId() + + "\")\n"; + core_script += String.format("bpy.context.scene.render.tile_x = %1$d\nbpy.context.scene.render.tile_y = %1$d\n", tileSize); + + log.debug(String.format("Rendering bucket size set to %1$dx%1$d pixels", tileSize)); gui.setComputeMethod("GPU"); } else { + // Otherwise (CPU), fix the tile size to 32x32px core_script = "sheepit_set_compute_device(\"NONE\", \"CPU\", \"CPU\")\n"; + core_script += String.format("bpy.context.scene.render.tile_x = %1$d\nbpy.context.scene.render.tile_y = %1$d\n", 32); gui.setComputeMethod("CPU"); } core_script += ignore_signal_script; - core_script += String.format("bpy.context.scene.render.tile_x = %1$d\nbpy.context.scene.render.tile_y = %1$d\n", getTileSize()); File script_file = null; - String command1[] = getRenderCommand().split(" "); + String command1[] = getRendererCommand().split(" "); int size_command = command1.length + 2; // + 2 for script - if (config.getNbCores() > 0) { // user has specified something + if (configuration.getNbCores() > 0) { // user has specified something size_command += 2; } @@ -260,13 +201,14 @@ public Error.Type render() { Map new_env = new HashMap(); - new_env.put("BLENDER_USER_CONFIG", config.workingDirectory.getAbsolutePath().replace("\\", "\\\\")); - new_env.put("CORES", Integer.toString(config.getNbCores())); - new_env.put("PRIORITY", Integer.toString(config.getPriority())); + new_env.put("BLENDER_USER_CONFIG", configuration.getWorkingDirectory().getAbsolutePath().replace("\\", "\\\\")); + new_env.put("CORES", Integer.toString(configuration.getNbCores())); + new_env.put("PRIORITY", Integer.toString(configuration.getPriority())); new_env.put("PYTHONPATH", ""); // make sure blender is using the embedded python, if not it could create "Fatal Python error: Py_Initialize" new_env.put("PYTHONHOME", "");// make sure blender is using the embedded python, if not it could create "Fatal Python error: Py_Initialize" - if (getUseGPU() && config.getGPUDevice() != null && config.getComputeMethod() != ComputeType.CPU && OpenCL.TYPE.equals(config.getGPUDevice().getType())) { + if (isUseGPU() && configuration.getGPUDevice() != null && configuration.getComputeMethod() != ComputeType.CPU && OpenCL.TYPE + .equals(configuration.getGPUDevice().getType())) { new_env.put("CYCLES_OPENCL_SPLIT_KERNEL_TEST", "1"); this.updateRenderingStatusMethod = UPDATE_METHOD_BY_TILE; // don't display remaining time } @@ -278,7 +220,7 @@ public Error.Type render() { command.add("-P"); try { - script_file = File.createTempFile("script_", "", config.workingDirectory); + script_file = File.createTempFile("script_", "", configuration.getWorkingDirectory()); File file = new File(script_file.getAbsolutePath()); FileWriter txt; txt = new FileWriter(file); @@ -303,13 +245,13 @@ public Error.Type render() { case ".e": command.add(getRendererPath()); // the number of cores has to be put after the binary and before the scene arg - if (config.getNbCores() > 0) { + if (configuration.getNbCores() > 0) { command.add("-t"); - command.add(Integer.toString(config.getNbCores())); + command.add(Integer.toString(configuration.getNbCores())); } break; case ".o": - command.add(config.workingDirectory.getAbsolutePath() + File.separator + getPrefixOutputImage()); + command.add(configuration.getWorkingDirectory().getAbsolutePath() + File.separator + getPrefixOutputImage()); break; case ".f": command.add(getFrameNumber()); @@ -321,60 +263,91 @@ public Error.Type render() { } try { + renderStartedObservable event = new renderStartedObservable(renderStarted); String line; log.debug(command.toString()); OS os = OS.getOS(); - process.setCoresUsed(config.getNbCores()); + process.setCoresUsed(configuration.getNbCores()); process.start(); getProcessRender().setProcess(os.exec(command, new_env)); BufferedReader input = new BufferedReader(new InputStreamReader(getProcessRender().getProcess().getInputStream())); - if (config.getMaxRenderTime() > 0) { + if (configuration.getMaxRenderTime() > 0) { timerOfMaxRenderTime = new Timer(); timerOfMaxRenderTime.schedule(new TimerTask() { - @Override - public void run() { + @Override public void run() { RenderProcess process = getProcessRender(); if (process != null) { - long duration = (new Date().getTime() - process.getStartTime() ) / 1000; // in seconds - if (config.getMaxRenderTime() > 0 && duration > config.getMaxRenderTime()) { + long duration = (new Date().getTime() - process.getStartTime()) / 1000; // in seconds + if (configuration.getMaxRenderTime() > 0 && duration > configuration.getMaxRenderTime()) { + setAskForRendererKill(true); log.debug("Killing render because process duration"); OS.getOS().kill(process.getProcess()); - setAskForRendererKill(true); } } } - }, config.getMaxRenderTime() * 1000 + 2000); // +2s to be sure the delay is over + }, configuration.getMaxRenderTime() * 1000 + 2000); // +2s to be sure the delay is over } - long last_update_status = 0; log.debug("renderer output"); try { + int progress = -1; + + Pattern tilePattern = Pattern.compile(" (Rendered|Path Tracing Tile|Rendering) (\\d+)\\s?\\/\\s?(\\d+)( Tiles| samples|,)"); + + // Initialise the progress bar in the icon and the UI (0% completed at this time) + gui.updateTrayIcon(0); + gui.status("Preparing scene", 0); + while ((line = input.readLine()) != null) { log.debug(line); + // Process lines until the version is loaded (usually first or second line of log) + if (blenderLongVersion == null) { + Pattern blenderPattern = Pattern.compile("Blender (([0-9]{1,3}\\.[0-9]{0,3}).*)$"); + Matcher blendDetectedVersion = blenderPattern.matcher(line); + + if (blendDetectedVersion.find()) { + blenderLongVersion = blendDetectedVersion.group(1); + blenderShortVersion = blendDetectedVersion.group(2); + } + } + + progress = computeRenderingProgress(line, tilePattern, progress); + updateRenderingMemoryPeak(line); - if (config.getMaxMemory() != -1 && process.getMemoryUsed() > config.getMaxMemory()) { - log.debug("Blocking render because process ram used (" + process.getMemoryUsed() + "k) is over user setting (" + config.getMaxMemory() + "k)"); + if (configuration.getMaxMemory() != -1 && process.getMemoryUsed() > configuration.getMaxMemory()) { + log.debug("Blocking render because process ram used (" + process.getMemoryUsed() + "k) is over user setting (" + configuration + .getMaxMemory() + "k)"); OS.getOS().kill(process.getProcess()); process.finish(); if (script_file != null) { script_file.delete(); } + + // Once the process is finished (either finished successfully or with an error) move back to + // base icon (isolated S with no progress bar) + gui.updateTrayIcon(Job.SHOW_BASE_ICON); + return Error.Type.RENDERER_OUT_OF_MEMORY; } - if ((new Date().getTime() - last_update_status) > 2000) { // only call the update every two seconds - updateRenderingStatus(line); - last_update_status = new Date().getTime(); - } + updateRenderingStatus(line); Type error = detectError(line); if (error != Error.Type.OK) { if (script_file != null) { script_file.delete(); } + + // Put back base icon + gui.updateTrayIcon(Job.SHOW_BASE_ICON); + return error; } + + if (event.isStarted() == false && (process.getMemoryUsed() > 0 || process.getRemainingDuration() > 0)) { + event.doNotifyIsStarted(); + } } input.close(); } @@ -382,6 +355,10 @@ public void run() { // most likely The handle is invalid log.error("Job::render exception(B) (silent error) " + err1); } + + // Put back base icon + gui.updateTrayIcon(Job.SHOW_BASE_ICON); + log.debug("end of rendering"); } catch (Exception err) { @@ -413,24 +390,24 @@ public boolean accept(File dir, String name) { } }; - File[] files = config.workingDirectory.listFiles(textFilter); + File[] files = configuration.getWorkingDirectory().listFiles(textFilter); - if (getAskForRendererKill()) { + if (isAskForRendererKill()) { log.debug("Job::render been asked to end render"); - long duration = (new Date().getTime() - process.getStartTime() ) / 1000; // in seconds - if (config.getMaxRenderTime() > 0 && duration > config.getMaxRenderTime()) { - log.debug("Render killed because process duration (" + duration + "s) is over user setting (" + config.getMaxRenderTime() + "s)"); + long duration = (new Date().getTime() - process.getStartTime()) / 1000; // in seconds + if (configuration.getMaxRenderTime() > 0 && duration > configuration.getMaxRenderTime()) { + log.debug("Render killed because process duration (" + duration + "s) is over user setting (" + configuration.getMaxRenderTime() + "s)"); return Error.Type.RENDERER_KILLED_BY_USER_OVER_TIME; } if (files.length != 0) { new File(files[0].getAbsolutePath()).delete(); } - if (getServerBlockJob()) { + if (isServerBlockJob()) { return Error.Type.RENDERER_KILLED_BY_SERVER; } - if (getUserBlockJob()) { + if (isUserBlockJob()) { return Error.Type.RENDERER_KILLED_BY_USER; } return Error.Type.RENDERER_KILLED; @@ -446,7 +423,7 @@ public boolean accept(File dir, String name) { catch (Exception e) { e.printStackTrace(); } - File crash_file = new File(config.workingDirectory + File.separator + basename + ".crash.txt"); + File crash_file = new File(configuration.getWorkingDirectory() + File.separator + basename + ".crash.txt"); if (crash_file.exists()) { log.error("Job::render crash file found => the renderer crashed"); crash_file.delete(); @@ -462,7 +439,8 @@ public boolean accept(File dir, String name) { } else { setOutputImagePath(files[0].getAbsolutePath()); - log.debug("Job::render pictureFilename: '" + getOutputImagePath() + "'"); + this.outputImageSize = new File(getOutputImagePath()).length(); + log.debug(String.format("Job::render pictureFilename: %s, size: %d'", getOutputImagePath(), this.outputImageSize)); } File scene_dir = new File(getSceneDirectory()); @@ -476,6 +454,26 @@ public boolean accept(File dir, String name) { return Error.Type.OK; } + private int computeRenderingProgress(String line, Pattern tilePattern, int currentProgress) { + Matcher standardTileInfo = tilePattern.matcher(line); + int newProgress = currentProgress; + + if (standardTileInfo.find()) { + int tileJustProcessed = Integer.parseInt(standardTileInfo.group(2)); + int totalTilesInJob = Integer.parseInt(standardTileInfo.group(3)); + + newProgress = Math.abs((tileJustProcessed * 100) / totalTilesInJob); + } + + // Only update the tray icon and the screen if percentage has changed + if (newProgress != currentProgress) { + gui.updateTrayIcon(newProgress); + gui.status("Rendering", newProgress); + } + + return newProgress; + } + private void updateRenderingStatus(String line) { if (getUpdateRenderingStatusMethod() != null && getUpdateRenderingStatusMethod().equals(Job.UPDATE_METHOD_BLENDER_INTERNAL_BY_PART)) { String search = " Part "; @@ -483,7 +481,7 @@ private void updateRenderingStatus(String line) { if (index != -1) { String buf = line.substring(index + search.length()); String[] parts = buf.split("-"); - if (parts != null && parts.length == 2) { + if (parts.length == 2) { try { int current = Integer.parseInt(parts[0]); int total = Integer.parseInt(parts[1]); @@ -491,6 +489,7 @@ private void updateRenderingStatus(String line) { long end_render = (new Date().getTime() - this.render.getStartTime()) * total / current; Date date = new Date(end_render); gui.setRemainingTime(String.format("%s %% (%s)", (int) (100.0 - 100.0 * current / total), Utils.humanDuration(date))); + getProcessRender().setRemainingDuration((int) (date.getTime() / 1000)); return; } } @@ -533,13 +532,13 @@ else if (getUpdateRenderingStatusMethod() == null || getUpdateRenderingStatusMet } } } - else if (getUpdateRenderingStatusMethod() == null || getUpdateRenderingStatusMethod().equals(Job.UPDATE_METHOD_BY_TILE)) { + else if (getUpdateRenderingStatusMethod().equals(Job.UPDATE_METHOD_BY_TILE)) { String search = " Tile "; int index = line.lastIndexOf(search); if (index != -1) { String buf = line.substring(index + search.length()); String[] parts = buf.split("/"); - if (parts != null && parts.length == 2) { + if (parts.length == 2) { try { int current = Integer.parseInt(parts[0]); int total = Integer.parseInt(parts[1]); @@ -565,12 +564,12 @@ private void updateRenderingMemoryPeak(String line) { int end = element.indexOf(')'); if (end > 0) { try { - long mem = Utils.parseNumber(element.substring(1, end).trim()); + long mem = Utils.parseNumber(element.substring(1, end).trim()) / 1000; // internal use of ram is in kB if (mem > getProcessRender().getMemoryUsed()) { getProcessRender().setMemoryUsed(mem); } } - catch (IllegalStateException e) { + catch (IllegalStateException | NumberFormatException e) { // failed to parseNumber } } @@ -580,12 +579,12 @@ private void updateRenderingMemoryPeak(String line) { int end = element.indexOf('|'); if (end > 0) { try { - long mem = Utils.parseNumber(element.substring(1, end).trim()); + long mem = Utils.parseNumber(element.substring(1, end).trim()) / 1000; // internal use of ram is in kB if (mem > getProcessRender().getMemoryUsed()) { getProcessRender().setMemoryUsed(mem); } } - catch (IllegalStateException e) { + catch (IllegalStateException | NumberFormatException e) { // failed to parseNumber } } @@ -596,7 +595,7 @@ private void updateRenderingMemoryPeak(String line) { private Type detectError(String line) { - if (line.indexOf("CUDA error: Out of memory") != -1) { + if (line.contains("CUDA error: Out of memory")) { // Fra:151 Mem:405.91M (0.00M, Peak 633.81M) | Mem:470.26M, Peak:470.26M | Scene, RenderLayer | Updating Device | Writing constant memory // Fra:151 Mem:405.91M (0.00M, Peak 633.81M) | Mem:470.26M, Peak:470.26M | Scene, RenderLayer | Path Tracing Tile 0/135, Sample 0/200 // Fra:151 Mem:405.91M (0.00M, Peak 633.81M) | Mem:470.82M, Peak:470.82M | Scene, RenderLayer | Path Tracing Tile 1/135, Sample 0/200 @@ -615,7 +614,7 @@ private Type detectError(String line) { // Blender quit return Type.RENDERER_OUT_OF_VIDEO_MEMORY; } - else if (line.indexOf("CUDA error at cuCtxCreate: Out of memory") != -1) { + else if (line.contains("CUDA error at cuCtxCreate: Out of memory")) { // renderer output // CUDA error at cuCtxCreate: Out of memory // Refer to the Cycles GPU rendering documentation for possible solutions: @@ -638,7 +637,7 @@ else if (line.indexOf("CUDA error at cuCtxCreate: Out of memory") != -1) { // end of rendering return Type.RENDERER_OUT_OF_VIDEO_MEMORY; } - else if (line.indexOf("CUDA error: Launch exceeded timeout in") != -1) { + else if (line.contains("CUDA error: Launch exceeded timeout in")) { // Fra:420 Mem:102.41M (0.00M, Peak 215.18M) | Remaining:01:08.44 | Mem:176.04M, Peak:199.23M | Scene, RenderLayer | Path Tracing Tile 2/24, Sample 10/14 // Fra:420 Mem:102.41M (0.00M, Peak 215.18M) | Remaining:01:07.08 | Mem:175.48M, Peak:199.23M | Scene, RenderLayer | Path Tracing Tile 2/24, Sample 14/14 // Fra:420 Mem:102.41M (0.00M, Peak 215.18M) | Remaining:01:07.11 | Mem:176.04M, Peak:199.23M | Scene, RenderLayer | Path Tracing Tile 3/24, Sample 0/14 @@ -698,7 +697,7 @@ else if (line.indexOf("CUDA error: Launch exceeded timeout in") != -1) { // end of rendering return Type.RENDERER_OUT_OF_VIDEO_MEMORY; } - else if (line.indexOf("CUDA error: Invalid value in cuTexRefSetAddress(") != -1) { + else if (line.contains("CUDA error: Invalid value in cuTexRefSetAddress(")) { // Fra:83 Mem:1201.77M (0.00M, Peak 1480.94M) | Time:00:59.30 | Mem:894.21M, Peak:894.21M | color 3, RenderLayer | Updating Mesh | Copying Strands to device // Fra:83 Mem:1316.76M (0.00M, Peak 1480.94M) | Time:01:02.84 | Mem:1010.16M, Peak:1010.16M | color 3, RenderLayer | Cancel | CUDA error: Invalid value in cuTexRefSetAddress(NULL, texref, cuda_device_ptr(mem.device_pointer), size) // Error: CUDA error: Invalid value in cuTexRefSetAddress(NULL, texref, cuda_device_ptr(mem.device_pointer), size) @@ -709,7 +708,7 @@ else if (line.indexOf("CUDA error: Invalid value in cuTexRefSetAddress(") != -1) // http://www.blender.org/manual/render/cycles/gpu_rendering.html return Error.Type.RENDERER_OUT_OF_VIDEO_MEMORY; } - else if (line.indexOf("CUDA error: Launch failed in cuCtxSynchronize()") != -1) { + else if (line.contains("CUDA error: Launch failed in cuCtxSynchronize()")) { // Fra:60 Mem:278.24M (0.00M, Peak 644.01M) | Time:05:08.95 | Remaining:00:03.88 | Mem:210.79M, Peak:210.79M | Scene, W Laser | Path Tracing Tile 16/18, Sample 36/36 // Fra:60 Mem:278.24M (0.00M, Peak 644.01M) | Time:05:08.96 | Remaining:00:00.82 | Mem:211.04M, Peak:211.04M | Scene, W Laser | Path Tracing Tile 17/18, Sample 36/36 // Fra:60 Mem:278.24M (0.00M, Peak 644.01M) | Time:05:08.96 | Mem:211.11M, Peak:211.11M | Scene, W Laser | Path Tracing Tile 18/18 @@ -725,7 +724,19 @@ else if (line.indexOf("CUDA error: Launch failed in cuCtxSynchronize()") != -1) // CUDA error: Launch failed in cuMemFree(cuda_device_ptr(mem.device_pointer)), line 615 return Error.Type.RENDERER_OUT_OF_VIDEO_MEMORY; } - else if (line.indexOf("CUDA device supported only with compute capability") != -1) { + else if (line.contains("CUDA error: Illegal address in cuCtxSynchronize()")) { + // Fra:124 Mem:434.77M (0.00M, Peak 435.34M) | Time:25:50.81 | Remaining:01:10:05.16 | Mem:175.14M, Peak:265.96M | Scene, RenderLayer | Path Tracing Tile 34/135, Sample 800/800, Denoised 17 tiles + // Fra:124 Mem:432.71M (0.00M, Peak 435.34M) | Time:25:50.81 | Remaining:01:10:04.95 | Mem:264.84M, Peak:266.90M | Scene, RenderLayer | Path Tracing Tile 34/135, Sample 800/800, Denoised 18 tiles + // Fra:124 Mem:434.77M (0.00M, Peak 435.34M) | Time:25:50.82 | Remaining:01:07:20.83 | Mem:266.90M, Peak:266.90M | Scene, RenderLayer | Path Tracing Tile 35/135, Sample 800/800, Denoised 18 tiles + // Fra:124 Mem:432.71M (0.00M, Peak 435.34M) | Time:25:50.82 | Remaining:01:07:20.63 | Mem:356.60M, Peak:358.67M | Scene, RenderLayer | Path Tracing Tile 35/135, Sample 800/800, Denoised 19 tiles + // Fra:124 Mem:434.77M (0.00M, Peak 435.34M) | Time:25:50.82 | Remaining:01:04:45.63 | Mem:358.67M, Peak:358.67M | Scene, RenderLayer | Path Tracing Tile 36/135, Sample 800/800, Denoised 19 tiles + // Fra:124 Mem:432.71M (0.00M, Peak 435.34M) | Time:25:50.82 | Remaining:01:04:45.45 | Mem:448.37M, Peak:450.43M | Scene, RenderLayer | Path Tracing Tile 36/135, Sample 800/800, Denoised 20 tiles + // Fra:124 Mem:434.77M (0.00M, Peak 435.34M) | Time:25:50.83 | Remaining:01:02:18.83 | Mem:450.43M, Peak:450.43M | Scene, RenderLayer | Path Tracing Tile 37/135, Sample 800/800, Denoised 20 tiles + // CUDA error: Illegal address in cuCtxSynchronize(), line 1372 + // Refer to the Cycles GPU rendering documentation for possible solutions: + return Error.Type.RENDERER_OUT_OF_VIDEO_MEMORY; + } + else if (line.contains("CUDA device supported only with compute capability")) { // found bundled python: /tmp/xx/2.73/python // read blend: /tmp/xx/compute-method.blend // Fra:340 Mem:7.64M (0.00M, Peak 8.23M) | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Synchronizing object | Sun @@ -746,7 +757,7 @@ else if (line.indexOf("CUDA device supported only with compute capability") != - // Blender quit return Type.GPU_NOT_SUPPORTED; } - else if (line.indexOf("terminate called after throwing an instance of 'boost::filesystem::filesystem_error'") != -1) { + else if (line.contains("terminate called after throwing an instance of 'boost::filesystem::filesystem_error'")) { // Fra:2103 Mem:29.54M (0.00M, Peak 29.54M) | Time:00:00.24 | Mem:1.64M, Peak:1.64M | Scene, RenderLayer | Updating Mesh | Computing attributes // Fra:2103 Mem:29.54M (0.00M, Peak 29.54M) | Time:00:00.24 | Mem:1.64M, Peak:1.64M | Scene, RenderLayer | Updating Mesh | Copying Attributes to device // Fra:2103 Mem:29.54M (0.00M, Peak 29.54M) | Time:00:00.24 | Mem:1.97M, Peak:1.97M | Scene, RenderLayer | Updating Scene BVH | Building @@ -759,7 +770,7 @@ else if (line.indexOf("terminate called after throwing an instance of 'boost::fi // what(): boost::filesystem::create_directory: Permission denied: "/var/local/cache" return Error.Type.NOOUTPUTFILE; } - else if (line.indexOf("terminate called after throwing an instance of 'std::bad_alloc'") != -1) { + else if (line.contains("terminate called after throwing an instance of 'std::bad_alloc'")) { // Fra:80 Mem:1333.02M (0.00M, Peak 1651.23M) | Mem:780.37M, Peak:780.37M | Scene, RenderLayer | Updating Mesh BVH Plane.083 171/2 | Building BVH // Fra:80 Mem:1333.02M (0.00M, Peak 1651.23M) | Mem:780.37M, Peak:780.37M | Scene, RenderLayer | Updating Mesh BVH Mesh 172/2 | Building BVH // Fra:80 Mem:1333.02M (0.00M, Peak 1651.23M) | Mem:780.37M, Peak:780.37M | Scene, RenderLayer | Updating Mesh BVH Mesh 172/2 | Packing BVH triangles and strands @@ -770,26 +781,26 @@ else if (line.indexOf("terminate called after throwing an instance of 'std::bad_ // what(): std::bad_alloc return Error.Type.RENDERER_OUT_OF_MEMORY; } - else if (line.indexOf("what(): std::bad_alloc") != -1) { + else if (line.contains("what(): std::bad_alloc")) { // Fra:7 Mem:1247.01M (0.00M, Peak 1247.01M) | Time:00:28.84 | Mem:207.63M, Peak:207.63M | Scene, RenderLayer | Updating Scene BVH | Building BVH 93%, duplicates 0%terminate called recursively // terminate called after throwing an instance of 'St9bad_alloc' // what(): std::bad_alloc // scandir: Cannot allocate memory return Error.Type.RENDERER_OUT_OF_MEMORY; } - else if (line.indexOf("EXCEPTION_ACCESS_VIOLATION") != -1) { + else if (line.contains("EXCEPTION_ACCESS_VIOLATION")) { // Fra:638 Mem:342.17M (63.28M, Peak 735.33M) | Time:00:07.65 | Remaining:02:38.28 | Mem:246.91M, Peak:262.16M | scene_top_01_90, chip_top_view_scene_01 | Path Tracing Tile 57/2040, Denoised 0 tiles // Fra:638 Mem:342.32M (63.28M, Peak 735.33M) | Time:00:07.70 | Remaining:02:38.20 | Mem:247.05M, Peak:262.16M | scene_top_01_90, chip_top_view_scene_01 | Path Tracing Tile 58/2040, Denoised 0 tiles // Error: EXCEPTION_ACCESS_VIOLATION return Error.Type.RENDERER_CRASHED; } - else if (line.indexOf("Fatal Python error: Py_Initialize") != -1) { + else if (line.contains("Fatal Python error: Py_Initialize")) { // Fatal Python error: Py_Initialize: unable to load the file system codec // ImportError: No module named 'encodings' // Current thread 0x0000388c (most recent call first): return Error.Type.RENDERER_CRASHED_PYTHON_ERROR; } - else if (line.indexOf("Calloc returns null") != -1) { + else if (line.contains("Calloc returns null")) { // Fra:1 Mem:976.60M (0.00M, Peak 1000.54M) | Time:00:01.34 | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Synchronizing object | Left // Calloc returns null: len=7186416 in CDMLoopUV, total 2145859048 // Calloc returns null: len=7186416 in CDMLoopUV, total 2145859048 @@ -797,7 +808,7 @@ else if (line.indexOf("Calloc returns null") != -1) { // Writing: /home/user/.sheepit/LEFT packed.crash.txt return Error.Type.RENDERER_OUT_OF_MEMORY; } - else if (line.indexOf("Malloc returns null") != -1) { + else if (line.contains("Malloc returns null")) { // Fra:1 Mem:976.60M (0.00M, Peak 1000.54M) | Time:00:01.34 | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Synchronizing object | Left // Calloc returns null: len=7186416 in CDMLoopUV, total 2145859048 // Calloc returns null: len=7186416 in CDMLoopUV, total 2145859048 @@ -805,7 +816,7 @@ else if (line.indexOf("Malloc returns null") != -1) { // Writing: /home/user/.sheepit/LEFT packed.crash.txt return Error.Type.RENDERER_OUT_OF_MEMORY; } - else if (line.indexOf("CUDA kernel compilation failed") != -1 || line.indexOf("Refer to the Cycles GPU rendering documentation for possible solutions:" != -1) { + else if (line.contains("CUDA kernel compilation failed")) { // Fra:1 Mem:200.70M (0.00M, Peak 378.15M) | Time:00:01.02 | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Synchronizing object | Sun.001 // Fra:1 Mem:200.70M (0.00M, Peak 378.15M) | Time:00:01.02 | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Synchronizing object | Sun.002 // Fra:1 Mem:200.70M (0.00M, Peak 378.15M) | Time:00:01.02 | Mem:0.00M, Peak:0.00M | Scene, RenderLayer | Initializing @@ -827,17 +838,19 @@ else if (line.indexOf("CUDA kernel compilation failed") != -1 || line.indexOf("R return Type.OK; } - public int getTileSize() { - if (config.getTileSize() == -1) { // not set - int size = 32; // CPU - GPUDevice gpu = this.config.getGPUDevice(); - if (getUseGPU() && gpu != null) { - return gpu.getRecommandedTileSize(); - } - return size; + public static class renderStartedObservable extends Observable { + + @Getter private boolean isStarted; + + public renderStartedObservable(Observer observer) { + super(); + addObserver(observer); } - else { - return config.getTileSize(); + + public void doNotifyIsStarted() { + setChanged(); + notifyObservers(); + isStarted = true; } } } diff --git a/src/com/sheepit/client/Log.java b/src/com/sheepit/client/Log.java index 37d29456..6ef8d587 100644 --- a/src/com/sheepit/client/Log.java +++ b/src/com/sheepit/client/Log.java @@ -2,7 +2,7 @@ * Copyright (C) 2011-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -39,31 +39,52 @@ private Log(boolean print_) { this.printStdOut = print_; this.lastCheckPoint = 0; this.checkpoints.put(this.lastCheckPoint, new ArrayList()); - this.dateFormat = new SimpleDateFormat("dd-MM kk:mm:ss"); + this.dateFormat = new SimpleDateFormat("dd-MM HH:mm:ss"); } public void debug(String msg_) { - this.append("debug", msg_); + this.debug(-1, msg_); + } + + public void debug(int point_, String msg_) { + this.append(point_, "debug", msg_); } public void info(String msg_) { - this.append("info", msg_); + this.info(-1, msg_); + } + + public void info(int point_, String msg_) { + this.append(point_, "info", msg_); } public void error(String msg_) { - this.append("error", msg_); + this.error(-1, msg_); + } + + public void error(int point_, String msg_) { + this.append(point_, "error", msg_); } - private void append(String level_, String msg_) { - if (msg_.equals("") == false) { - String line = this.dateFormat.format(new java.util.Date()) + " (" + level_ + ") " + msg_; - if (this.checkpoints.containsKey(this.lastCheckPoint)) { - this.checkpoints.get(this.lastCheckPoint).add(line); - } - if (this.printStdOut == true) { - System.out.println(line); + private synchronized void append(int point_, String level_, String msg_) { + String line = null; + + try { + int checkpointToWrite = (point_ > 0 ? point_ : this.lastCheckPoint); + + if (msg_.equals("") == false) { + line = this.dateFormat.format(new java.util.Date()) + " (" + level_ + ") " + msg_; + if (this.checkpoints.containsKey(checkpointToWrite) && this.checkpoints.get(checkpointToWrite) != null) { + this.checkpoints.get(checkpointToWrite).add(line); + } + if (this.printStdOut == true) { + System.out.println(line); + } } } + catch (Exception e) { + // Nothing to do here. Just allow the thread to continue + } } public int newCheckPoint() { @@ -89,7 +110,7 @@ public static synchronized Log getInstance(Configuration config) { if (instance == null) { boolean print = false; if (config != null) { - print = config.getPrintLog(); + print = config.isPrintLog(); } instance = new Log(print); } diff --git a/src/com/sheepit/client/Pair.java b/src/com/sheepit/client/Pair.java index 47643d0b..5b4fee8e 100644 --- a/src/com/sheepit/client/Pair.java +++ b/src/com/sheepit/client/Pair.java @@ -19,35 +19,34 @@ import java.util.Objects; /** -* Container to ease passing around a pair of two objects. This object provides a sensible -* implementation of equals(), returning true if equals() is true on each of the contained -* objects. -*/ + * Container to ease passing around a pair of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ public class Pair { public final F first; public final S second; /** - * Constructor for a Pair. - * - * @param first the first object in the Pair - * @param second the second object in the pair - */ + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ public Pair(F first, S second) { this.first = first; this.second = second; } /** - * Checks the two objects for equality by delegating to their respective - * {@link Object#equals(Object)} methods. - * - * @param o the {@link Pair} to which this one is to be checked for equality - * @return true if the underlying objects of the Pair are both considered - * equal - */ - @Override - public boolean equals(Object o) { + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override public boolean equals(Object o) { if (!(o instanceof Pair)) { return false; } @@ -56,22 +55,21 @@ public boolean equals(Object o) { } /** - * Compute a hash code using the hash codes of the underlying objects - * - * @return a hashcode of the Pair - */ - @Override - public int hashCode() { + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override public int hashCode() { return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); } /** - * Convenience method for creating an appropriately typed pair. + * Convenience method for creating an appropriately typed pair. * - * @param a the first object in the Pair - * @param b the second object in the pair - * @return a Pair that is templatized with the types of a and b - */ + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ public static Pair create(A a, B b) { return new Pair(a, b); } diff --git a/src/com/sheepit/client/RenderProcess.java b/src/com/sheepit/client/RenderProcess.java index 108ea84d..105f4452 100644 --- a/src/com/sheepit/client/RenderProcess.java +++ b/src/com/sheepit/client/RenderProcess.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -19,82 +19,47 @@ package com.sheepit.client; +import lombok.Data; + import java.util.Date; -public class RenderProcess { - private long start; - private long end; - private int remainingDuration; +@Data public class RenderProcess { + private long startTime; + private long endTime; + private int remainingDuration; // in seconds private long memoryUsed; // in kB private int coresUsed; private Process process; public RenderProcess() { process = null; - start = -1; - end = -1; + startTime = -1; + endTime = -1; memoryUsed = 0; coresUsed = 0; remainingDuration = 0; } - public void setMemoryUsed(long val) { - memoryUsed = val; - } - - public long getMemoryUsed() { - return memoryUsed; - } - - public void setCoresUsed(int val) { - coresUsed = val; - } - - public int getCoresUsed() { - return coresUsed; - } - - public long getStartTime() { - return start; - } - - public long getEndTime() { - return end; - } - /** - * * @return duration in seconds */ public int getDuration() { - if (start != -1 && end != -1) { - return (int) ((end - start) / 1000); + if (startTime != -1 && endTime != -1) { + return (int) ((endTime - startTime) / 1000); } - else if (start != -1) { - return (int) ((new Date().getTime() - start) / 1000); + else if (startTime != -1) { + return (int) ((new Date().getTime() - startTime) / 1000); } return 0; } - /** - * - * @return duration in seconds - */ - public int getRemainingDuration() { - return remainingDuration; - } - - public void setRemainingDuration(int val) { - remainingDuration = val; - } - public void finish() { - end = new Date().getTime(); + endTime = new Date().getTime(); process = null; } public void start() { - start = new Date().getTime(); + startTime = new Date().getTime(); } public int exitValue() { @@ -118,12 +83,4 @@ public int exitValue() { } return value; } - - public void setProcess(Process val) { - process = val; - } - - public Process getProcess() { - return process; - } } diff --git a/src/com/sheepit/client/Server.java b/src/com/sheepit/client/Server.java index 05fb7b74..b5bbd1f5 100644 --- a/src/com/sheepit/client/Server.java +++ b/src/com/sheepit/client/Server.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -19,60 +19,46 @@ package com.sheepit.client; -import java.io.BufferedReader; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.NoRouteToHostException; -import java.net.URL; -import java.net.URLEncoder; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; +import java.net.*; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; +import lombok.Getter; +import org.simpleframework.xml.core.Persister; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; +import okhttp3.Call; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.JavaNetCookieJar; import com.sheepit.client.Configuration.ComputeType; import com.sheepit.client.Error.ServerCode; +import com.sheepit.client.datamodel.CacheFileMD5; +import com.sheepit.client.datamodel.FileMD5; +import com.sheepit.client.datamodel.HeartBeatInfos; +import com.sheepit.client.datamodel.JobInfos; +import com.sheepit.client.datamodel.JobValidation; +import com.sheepit.client.datamodel.RequestEndPoint; +import com.sheepit.client.datamodel.ServerConfig; import com.sheepit.client.exception.FermeException; import com.sheepit.client.exception.FermeExceptionBadResponseFromServer; import com.sheepit.client.exception.FermeExceptionNoRendererAvailable; @@ -85,11 +71,15 @@ import com.sheepit.client.exception.FermeServerDown; import com.sheepit.client.os.OS; -public class Server extends Thread implements HostnameVerifier, X509TrustManager { + +public class Server extends Thread { private String base_url; + private final OkHttpClient httpClient; + + @Getter private ServerConfig serverConfig; + private Configuration user_config; private Client client; - private HashMap pages; private Log log; private long lastRequestTime; private int keepmealive_duration; // time in ms @@ -99,13 +89,14 @@ public Server(String url_, Configuration user_config_, Client client_) { this.base_url = url_; this.user_config = user_config_; this.client = client_; - this.pages = new HashMap(); this.log = Log.getInstance(this.user_config); this.lastRequestTime = 0; this.keepmealive_duration = 15 * 60 * 1000; // default 15min - CookieManager cookies = new CookieManager(); - CookieHandler.setDefault(cookies); + // OkHttp performs best when we create a single OkHttpClient instance and reuse it for all of the HTTP calls. This is because each client holds its own + // connection pool and thread pools.Reusing connections and threads reduces latency and saves memory. Conversely, creating a client for each request + // wastes resources on idle pools. + this.httpClient = getOkHttpClient(); } public void run() { @@ -117,38 +108,44 @@ public void stayAlive() { long current_time = new Date().getTime(); if ((current_time - this.lastRequestTime) > this.keepmealive_duration) { try { - String args = ""; + HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(this.getPage("keepmealive"))).newBuilder(); + if (this.client != null && this.client.getRenderingJob() != null) { - args = "?frame=" + this.client.getRenderingJob().getFrameNumber() + "&job=" + this.client.getRenderingJob().getId(); - if (this.client.getRenderingJob().getExtras() != null && this.client.getRenderingJob().getExtras().isEmpty() == false) { - args += "&extras=" + this.client.getRenderingJob().getExtras(); + Job job = this.client.getRenderingJob(); + + urlBuilder.addQueryParameter("frame", job.getFrameNumber()).addQueryParameter("job", job.getId()); + if (job.getExtras() != null && !job.getExtras().isEmpty()) { + urlBuilder.addQueryParameter("extras", job.getExtras()); } - if (this.client.getRenderingJob().getProcessRender() != null) { - args += "&rendertime=" + this.client.getRenderingJob().getProcessRender().getDuration(); - args += "&remainingtime=" + this.client.getRenderingJob().getProcessRender().getRemainingDuration(); + + RenderProcess process = job.getProcessRender(); + if (process != null) { + urlBuilder.addQueryParameter("rendertime", String.valueOf(process.getDuration())) + .addQueryParameter("remainingtime", String.valueOf(process.getRemainingDuration())); } } - HttpURLConnection connection = this.HTTPRequest(this.getPage("keepmealive") + args); + Response response = this.HTTPRequest(urlBuilder); - if (connection.getResponseCode() == HttpURLConnection.HTTP_OK && connection.getContentType().startsWith("text/xml")) { - DataInputStream in = new DataInputStream(connection.getInputStream()); + if (response.code() == HttpURLConnection.HTTP_OK && response.body().contentType().toString().startsWith("text/xml")) { + String in = response.body().string(); + try { - Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); - ServerCode ret = Utils.statusIsOK(document, "keepmealive"); - if (ret == ServerCode.KEEPMEALIVE_STOP_RENDERING) { + HeartBeatInfos heartBeartInfos = new Persister().read(HeartBeatInfos.class, in); + ServerCode serverCode = ServerCode.fromInt(heartBeartInfos.getStatus()); + if (serverCode == ServerCode.KEEPMEALIVE_STOP_RENDERING) { this.log.debug("Server::stayAlive server asked to kill local render process"); // kill the current process, it will generate an error but it's okay - if (this.client != null && this.client.getRenderingJob() != null && this.client.getRenderingJob().getProcessRender().getProcess() != null) { + if (this.client != null && this.client.getRenderingJob() != null + && this.client.getRenderingJob().getProcessRender().getProcess() != null) { this.client.getRenderingJob().setServerBlockJob(true); this.client.getRenderingJob().setAskForRendererKill(true); OS.getOS().kill(this.client.getRenderingJob().getProcessRender().getProcess()); } } } - catch (SAXException e) { - } - catch (ParserConfigurationException e) { + catch (Exception e) { // for the read + this.log.debug("Server::stayAlive Exception " + e); } } } @@ -178,95 +175,61 @@ public void stayAlive() { } public String toString() { - return String.format("Server (base_url '%s', user_config %s, pages %s", this.base_url, this.user_config, this.pages); + return String.format("Server (base_url '%s', user_config %s", this.base_url, this.user_config); } public Error.Type getConfiguration() { OS os = OS.getOS(); - HttpURLConnection connection = null; + String publickey = null; try { - String url_remote = this.base_url + "/server/config.php"; - String parameters = String.format("login=%s&password=%s&cpu_family=%s&cpu_model=%s&cpu_model_name=%s&cpu_cores=%s&os=%s&ram=%s&bits=%s&version=%s&hostname=%s&ui=%s&extras=%s", - URLEncoder.encode(this.user_config.login(), "UTF-8"), - URLEncoder.encode(this.user_config.password(), "UTF-8"), - URLEncoder.encode(os.getCPU().family(), "UTF-8"), - URLEncoder.encode(os.getCPU().model(), "UTF-8"), - URLEncoder.encode(os.getCPU().name(), "UTF-8"), - ((this.user_config.getNbCores() == -1) ? os.getCPU().cores() : this.user_config.getNbCores()), - URLEncoder.encode(os.name(), "UTF-8"), - os.getMemory(), - URLEncoder.encode(os.getCPU().arch(), "UTF-8"), - this.user_config.getJarVersion(), - URLEncoder.encode(this.user_config.getHostname(), "UTF-8"), - this.client.getGui().getClass().getSimpleName(), - this.user_config.getExtras()); - this.log.debug("Server::getConfiguration url " + url_remote); + HttpUrl.Builder remoteURL = Objects.requireNonNull(HttpUrl.parse(this.base_url + "/server/config.php")).newBuilder(); + FormBody formBody = new FormBody.Builder() + .add("login", user_config.getLogin()) + .add("password", user_config.getPassword()) + .add("cpu_family", os.getCPU().family()) + .add("cpu_model", os.getCPU().model()) + .add("cpu_model_name", os.getCPU().name()) + .add("cpu_cores", String.valueOf(user_config.getNbCores() == -1 ? os.getCPU().cores() : user_config.getNbCores())) + .add("os", os.name()) + .add("ram", String.valueOf(os.getMemory())) + .add("bits", os.getCPU().arch()) + .add("version", user_config.getJarVersion()) + .add("hostname", user_config.getHostname()) + .add("ui", client.getGui().getClass().getSimpleName()) + .add("extras", user_config.getExtras()) + .build(); - connection = this.HTTPRequest(url_remote, parameters); - int r = connection.getResponseCode(); - String contentType = connection.getContentType(); + this.log.debug("Server::getConfiguration url " + remoteURL.build().toString()); + + Response response = this.HTTPRequest(remoteURL, formBody); + int r = response.code(); + String contentType = response.body().contentType().toString(); if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/xml")) { - DataInputStream in = new DataInputStream(connection.getInputStream()); - Document document = null; + String in = response.body().string(); + serverConfig = new Persister().read(ServerConfig.class, in); - try { - document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); - } - catch (SAXException e) { - this.log.error("getConfiguration error: failed to parse XML SAXException " + e); - return Error.Type.WRONG_CONFIGURATION; - } - catch (IOException e) { - this.log.error("getConfiguration error: failed to parse XML IOException " + e); - return Error.Type.WRONG_CONFIGURATION; - } - catch (ParserConfigurationException e) { - this.log.error("getConfiguration error: failed to parse XML ParserConfigurationException " + e); - return Error.Type.WRONG_CONFIGURATION; + if (ServerCode.fromInt(serverConfig.getStatus()) != ServerCode.OK) { + return Error.ServerCodeToType(ServerCode.fromInt(serverConfig.getStatus())); } - ServerCode ret = Utils.statusIsOK(document, "config"); - if (ret != ServerCode.OK) { - return Error.ServerCodeToType(ret); + publickey = serverConfig.getPublickey(); + if (publickey.isEmpty()) { + publickey = null; } - - Element config_node = null; - NodeList ns = null; - ns = document.getElementsByTagName("config"); - if (ns.getLength() == 0) { - this.log.error("getConfiguration error: failed to parse XML, no node 'config'"); - return Error.Type.WRONG_CONFIGURATION; + else { + this.user_config.setPassword(publickey); } - config_node = (Element) ns.item(0); - - ns = config_node.getElementsByTagName("request"); - if (ns.getLength() == 0) { - this.log.error("getConfiguration error: failed to parse XML, node 'config' has no child node 'request'"); - return Error.Type.WRONG_CONFIGURATION; - } - for (int i = 0; i < ns.getLength(); i++) { - Element element = (Element) ns.item(i); - if (element.hasAttribute("type") && element.hasAttribute("path")) { - this.pages.put(element.getAttribute("type"), element.getAttribute("path")); - if (element.getAttribute("type").equals("keepmealive") && element.hasAttribute("max-period")) { - this.keepmealive_duration = (Integer.parseInt(element.getAttribute("max-period")) - 120) * 1000; // put 2min of safety net - } - } - } - } - else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) { - return Error.Type.ERROR_BAD_RESPONSE; - } - else { - this.log.error("Server::getConfiguration: Invalid response " + contentType + " " + r); - return Error.Type.WRONG_CONFIGURATION; } } catch (ConnectException e) { this.log.error("Server::getConfiguration error ConnectException " + e); return Error.Type.NETWORK_ISSUE; } + catch (UnknownHostException e) { + this.log.error("Server::getConfiguration: exception UnknownHostException " + e); + return Error.Type.NETWORK_ISSUE; + } catch (UnsupportedEncodingException e) { this.log.error("Server::getConfiguration: exception UnsupportedEncodingException " + e); return Error.Type.UNKNOWN; @@ -275,11 +238,13 @@ else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) this.log.error("Server::getConfiguration: exception IOException " + e); return Error.Type.UNKNOWN; } - finally { - if (connection != null) { - connection.disconnect(); - } + catch (Exception e) { + this.log.error("Server::getConfiguration: exception Exception " + e); + return Error.Type.UNKNOWN; } + + this.client.getGui().successfulAuthenticationEvent(publickey); + return Error.Type.OK; } @@ -287,211 +252,110 @@ public Job requestJob() throws FermeException { this.log.debug("Server::requestJob"); String url_contents = ""; - HttpURLConnection connection = null; try { OS os = OS.getOS(); - int maxMemory = this.user_config.getMaxMemory(); - int freeMemory = os.getFreeMemory(); + long maxMemory = this.user_config.getMaxMemory(); + long freeMemory = os.getFreeMemory(); if (maxMemory < 0) { maxMemory = freeMemory; } else if (freeMemory > 0 && maxMemory > 0) { maxMemory = Math.min(maxMemory, freeMemory); } - String url = String.format("%s?computemethod=%s&cpu_cores=%s&ram_max=%s&rendertime_max=%s", this.getPage("request-job"), this.user_config.computeMethodToInt(), ((this.user_config.getNbCores() == -1) ? os.getCPU().cores() : this.user_config.getNbCores()), maxMemory, this.user_config.getMaxRenderTime()); - if (this.user_config.getComputeMethod() != ComputeType.CPU && this.user_config.getGPUDevice() != null) { - String gpu_model = ""; - try { - gpu_model = URLEncoder.encode(this.user_config.getGPUDevice().getModel(), "UTF-8"); - } - catch (UnsupportedEncodingException e) { - } - url += "&gpu_model=" + gpu_model + "&gpu_ram=" + this.user_config.getGPUDevice().getMemory() + "&gpu_type=" + this.user_config.getGPUDevice().getType(); - } - connection = this.HTTPRequest(url, this.generateXMLForMD5cache()); + HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(this.getPage("request-job"))).newBuilder() + .addQueryParameter("computemethod", String.valueOf(user_config.computeMethodToInt())) + .addQueryParameter("cpu_cores", String.valueOf(user_config.getNbCores() == -1 ? os.getCPU().cores() : user_config.getNbCores())) + .addQueryParameter("ram_max", String.valueOf(maxMemory)) + .addQueryParameter("rendertime_max", String.valueOf(user_config.getMaxRenderTime())); - int r = connection.getResponseCode(); - String contentType = connection.getContentType(); + if (user_config.getComputeMethod() != ComputeType.CPU && user_config.getGPUDevice() != null) { + urlBuilder.addQueryParameter("gpu_model", user_config.getGPUDevice().getModel()) + .addQueryParameter("gpu_ram", String.valueOf(user_config.getGPUDevice().getMemory())) + .addQueryParameter("gpu_type", user_config.getGPUDevice().getType()); + } + + Response response = this.HTTPRequest(urlBuilder, RequestBody.create(MediaType.parse("application/xml"), this.generateXMLForMD5cache())); + + int r = response.code(); + String contentType = response.body().contentType().toString(); if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/xml")) { - DataInputStream in = new DataInputStream(connection.getInputStream()); - Document document = null; - try { - document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); - } - catch (SAXException e) { - throw new FermeException("error requestJob: parseXML failed, SAXException " + e); - } - catch (IOException e) { - throw new FermeException("error requestJob: parseXML failed IOException " + e); - } - catch (ParserConfigurationException e) { - throw new FermeException("error requestJob: parseXML failed ParserConfigurationException " + e); - } + String in = response.body().string(); - ServerCode ret = Utils.statusIsOK(document, "jobrequest"); - if (ret != ServerCode.OK) { - if (ret == ServerCode.JOB_REQUEST_NOJOB) { - handleFileMD5DeleteDocument(document, "jobrequest"); - return null; - } - else if (ret == ServerCode.JOB_REQUEST_ERROR_NO_RENDERING_RIGHT) { - throw new FermeExceptionNoRightToRender(); - } - else if (ret == ServerCode.JOB_REQUEST_ERROR_DEAD_SESSION) { - throw new FermeExceptionNoSession(); - } - else if (ret == ServerCode.JOB_REQUEST_ERROR_RENDERER_NOT_AVAILABLE) { - throw new FermeExceptionNoRendererAvailable(); - } - else if (ret == ServerCode.JOB_REQUEST_ERROR_SESSION_DISABLED) { - throw new FermeExceptionSessionDisabled(); - } - else if (ret == ServerCode.JOB_REQUEST_SERVER_IN_MAINTENANCE) { - throw new FermeExceptionServerInMaintenance(); - } - else if (ret == ServerCode.JOB_REQUEST_SERVER_OVERLOADED) { - throw new FermeExceptionServerOverloaded(); - } - this.log.error("Server::requestJob: Utils.statusIsOK(document, 'jobrequest') -> ret " + ret); - throw new FermeException("error requestJob: status is not ok (it's " + ret + ")"); - } - - handleFileMD5DeleteDocument(document, "jobrequest"); + JobInfos jobData = new Persister().read(JobInfos.class, in); - Element a_node = null; - NodeList ns = null; - - ns = document.getElementsByTagName("stats"); - if (ns.getLength() == 0) { - throw new FermeException("error requestJob: parseXML failed, no 'frame' node"); - } - a_node = (Element) ns.item(0); + handleFileMD5DeleteDocument(jobData.getFileMD5s()); - int remaining_frames = 0; - int credits_earned = 0; - int credits_earned_session = 0; - int waiting_project = 0; - int connected_machine = 0; - if (a_node.hasAttribute("frame_remaining") && a_node.hasAttribute("credits_total") && a_node.hasAttribute("credits_session") && a_node.hasAttribute("waiting_project") && a_node.hasAttribute("connected_machine")) { - remaining_frames = Integer.parseInt(a_node.getAttribute("frame_remaining")); - credits_earned = Integer.parseInt(a_node.getAttribute("credits_total")); - credits_earned_session = Integer.parseInt(a_node.getAttribute("credits_session")); - waiting_project = Integer.parseInt(a_node.getAttribute("waiting_project")); - connected_machine = Integer.parseInt(a_node.getAttribute("connected_machine")); + if (jobData.getSessionStats() != null) { + this.client.getGui().displayStats( + new Stats(jobData.getSessionStats().getRemainingFrames(), jobData.getSessionStats().getPointsEarnedByUser(), + jobData.getSessionStats().getPointsEarnedOnSession(), jobData.getSessionStats().getRenderableProjects(), + jobData.getSessionStats().getWaitingProjects(), jobData.getSessionStats().getConnectedMachines())); } - ns = document.getElementsByTagName("job"); - if (ns.getLength() == 0) { - throw new FermeException("error requestJob: parseXML failed, no 'job' node"); - } - Element job_node = (Element) ns.item(0); - - ns = job_node.getElementsByTagName("renderer"); - if (ns.getLength() == 0) { - throw new FermeException("error requestJob: parseXML failed, node 'job' have no sub-node 'renderer'"); + ServerCode serverCode = ServerCode.fromInt(jobData.getStatus()); + if (serverCode != ServerCode.OK) { + switch (serverCode) { + case JOB_REQUEST_NOJOB: + return null; + case JOB_REQUEST_ERROR_NO_RENDERING_RIGHT: + throw new FermeExceptionNoRightToRender(); + case JOB_REQUEST_ERROR_DEAD_SESSION: + throw new FermeExceptionNoSession(); + case JOB_REQUEST_ERROR_SESSION_DISABLED: + throw new FermeExceptionSessionDisabled(); + case JOB_REQUEST_ERROR_INTERNAL_ERROR: + throw new FermeExceptionBadResponseFromServer(); + case JOB_REQUEST_ERROR_RENDERER_NOT_AVAILABLE: + throw new FermeExceptionNoRendererAvailable(); + case JOB_REQUEST_SERVER_IN_MAINTENANCE: + throw new FermeExceptionServerInMaintenance(); + case JOB_REQUEST_SERVER_OVERLOADED: + throw new FermeExceptionServerOverloaded(); + default: + throw new FermeException("error requestJob: status is not ok (it's " + serverCode + ")"); + } } - Element renderer_node = (Element) ns.item(0); String script = "import bpy\n"; - // blender 2.7x script += "try:\n"; - script += "\tbpy.context.user_preferences.filepaths.temporary_directory = \"" + this.user_config.workingDirectory.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; + script += "\tbpy.context.user_preferences.filepaths.temporary_directory = \"" + this.user_config.getWorkingDirectory().getAbsolutePath() + .replace("\\", "\\\\") + "\"\n"; script += "except AttributeError:\n"; script += "\tpass\n"; // blender 2.80 script += "try:\n"; - script += "\tbpy.context.preferences.filepaths.temporary_directory = \"" + this.user_config.workingDirectory.getAbsolutePath().replace("\\", "\\\\") + "\"\n"; + script += "\tbpy.context.preferences.filepaths.temporary_directory = \"" + this.user_config.getWorkingDirectory().getAbsolutePath() + .replace("\\", "\\\\") + "\"\n"; script += "except AttributeError:\n"; script += "\tpass\n"; - try { - ns = job_node.getElementsByTagName("script"); - if (ns.getLength() != 0) { - Element a_node3 = (Element) ns.item(0); - script += a_node3.getTextContent(); - } - } - catch (Exception e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::requestJob Exception " + e + " stacktrace: " + sw.toString()); - } + script += jobData.getRenderTask().getScript(); - String[] job_node_require_attribute = { "id", "archive_md5", "path", "use_gpu", "frame", "name", "extras", "password" }; - String[] renderer_node_require_attribute = { "md5", "commandline" }; + String validationUrl = URLDecoder.decode(jobData.getRenderTask().getValidationUrl(), "UTF-8"); - for (String e : job_node_require_attribute) { - if (job_node.hasAttribute(e) == false) { - throw new FermeException("error requestJob: parseXML failed, job_node have to attribute '" + e + "'"); - } - } - - for (String e : renderer_node_require_attribute) { - if (renderer_node.hasAttribute(e) == false) { - throw new FermeException("error requestJob: parseXML failed, renderer_node have to attribute '" + e + "'"); - } - } - - boolean use_gpu = (job_node.getAttribute("use_gpu").compareTo("1") == 0); - boolean synchronous_upload = true; - if (job_node.hasAttribute("synchronous_upload")) { - synchronous_upload = (job_node.getAttribute("synchronous_upload").compareTo("1") == 0); - } - - String frame_extras = ""; - if (job_node.hasAttribute("extras")) { - frame_extras = job_node.getAttribute("extras"); - } - - String update_method = null; - if (renderer_node.hasAttribute("update_method")) { - update_method = renderer_node.getAttribute("update_method"); - } - - Job a_job = new Job( - this.user_config, - this.client.getGui(), - this.client.getLog(), - job_node.getAttribute("id"), - job_node.getAttribute("frame"), - job_node.getAttribute("path").replace("/", File.separator), - use_gpu, - renderer_node.getAttribute("commandline"), - script, - job_node.getAttribute("archive_md5"), - renderer_node.getAttribute("md5"), - job_node.getAttribute("name"), - job_node.getAttribute("password"), - frame_extras, - synchronous_upload, - update_method - ); - - this.client.getGui().displayStats(new Stats(remaining_frames, credits_earned, credits_earned_session, waiting_project, connected_machine)); + Job a_job = new Job(this.user_config, this.client.getGui(), this.client.getLog(), jobData.getRenderTask().getId(), + jobData.getRenderTask().getFrame(), jobData.getRenderTask().getPath().replace("/", File.separator), + jobData.getRenderTask().getUseGpu() == 1, jobData.getRenderTask().getRendererInfos().getCommandline(), validationUrl, script, + jobData.getRenderTask().getArchive_md5(), jobData.getRenderTask().getRendererInfos().getMd5(), jobData.getRenderTask().getName(), + jobData.getRenderTask().getPassword(), jobData.getRenderTask().getExtras(), jobData.getRenderTask().getSynchronous_upload().equals("1"), + jobData.getRenderTask().getRendererInfos().getUpdate_method()); return a_job; } else { System.out.println("Server::requestJob url " + url_contents + " r " + r + " contentType " + contentType); - if (r == HttpURLConnection.HTTP_UNAVAILABLE || r == HttpURLConnection. HTTP_CLIENT_TIMEOUT) { + if (r == HttpURLConnection.HTTP_UNAVAILABLE || r == HttpURLConnection.HTTP_CLIENT_TIMEOUT) { // most likely varnish is up but apache down throw new FermeServerDown(); } else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) { throw new FermeExceptionBadResponseFromServer(); } - InputStream in = connection.getInputStream(); - String line; - BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8")); - while ((line = reader.readLine()) != null) { - System.out.print(line); - } - System.out.println(""); + System.out.println(response.body().string()); } } catch (FermeException e) { @@ -500,108 +364,96 @@ else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) catch (NoRouteToHostException e) { throw new FermeServerDown(); } + catch (UnknownHostException e) { + throw new FermeServerDown(); + } catch (Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); throw new FermeException("error requestJob: unknown exception " + e + " stacktrace: " + sw.toString()); } - finally { - if (connection != null) { - connection.disconnect(); - } - } throw new FermeException("error requestJob, end of function"); } - public HttpURLConnection HTTPRequest(String url_) throws IOException { - return this.HTTPRequest(url_, null); + public Response HTTPRequest(String url) throws IOException { + HttpUrl.Builder httpUrlBuilder = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder(); + return this.HTTPRequest(httpUrlBuilder, null); } - public HttpURLConnection HTTPRequest(String url_, String data_) throws IOException { - this.log.debug("Server::HTTPRequest url(" + url_ + ")"); - HttpURLConnection connection = null; - URL url = new URL(url_); - - connection = (HttpURLConnection) url.openConnection(); - connection.setDoInput(true); - connection.setDoOutput(true); - connection.setInstanceFollowRedirects(true); - connection.setRequestMethod("GET"); + public Response HTTPRequest(HttpUrl.Builder httpUrlBuilder) throws IOException { + return this.HTTPRequest(httpUrlBuilder, null); + } + + public Response HTTPRequest(HttpUrl.Builder httpUrlBuilder, RequestBody data_) throws IOException { + String url = httpUrlBuilder.build().toString(); + Request.Builder builder = new Request.Builder(); + builder.url(url); - if (url_.startsWith("https://")) { - try { - SSLContext sc; - sc = SSLContext.getInstance("SSL"); - sc.init(null, new TrustManager[] { this }, null); - SSLSocketFactory factory = sc.getSocketFactory(); - ((HttpsURLConnection) connection).setSSLSocketFactory(factory); - ((HttpsURLConnection) connection).setHostnameVerifier(this); - } - catch (NoSuchAlgorithmException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPRequest NoSuchAlgorithmException " + e + " stacktrace: " + sw.toString()); - return null; - } - catch (KeyManagementException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPRequest KeyManagementException " + e + " stacktrace: " + sw.toString()); - return null; - } - } + this.log.debug("Server::HTTPRequest url(" + url + ")"); if (data_ != null) { - connection.setRequestProperty("Content-type", "application/x-www-form-urlencoded"); - connection.setRequestMethod("POST"); - OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream()); - out.write(data_); - out.flush(); - out.close(); + builder.post(data_); } - // actually use the connection to, in case of timeout, generate an exception - connection.getResponseCode(); - - this.lastRequestTime = new Date().getTime(); + Request request = builder.build(); + Response response = null; - return connection; + try { + response = httpClient.newCall(request).execute(); + + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + this.lastRequestTime = new Date().getTime(); + return response; + } + catch (IOException e) { + throw new IOException("Unexpected response from HTTP Stack" + e.getMessage()); + } } public int HTTPGetFile(String url_, String destination_, Gui gui_, String status_) throws FermeExceptionNoSpaceLeftOnDevice { // the destination_ parent directory must exist try { - HttpURLConnection httpCon = this.HTTPRequest(url_); + Response response = this.HTTPRequest(url_); - InputStream inStrm = httpCon.getInputStream(); - if (httpCon.getResponseCode() != HttpURLConnection.HTTP_OK) { - this.log.error("Server::HTTPGetFile(" + url_ + ", ...) HTTP code is not " + HttpURLConnection.HTTP_OK + " it's " + httpCon.getResponseCode()); + if (response.code() != HttpURLConnection.HTTP_OK) { + this.log.error("Server::HTTPGetFile(" + url_ + ", ...) HTTP code is not " + HttpURLConnection.HTTP_OK + " it's " + response.code()); return -1; } - int size = httpCon.getContentLength(); + long start = new Date().getTime(); + InputStream is = response.body().byteStream(); + OutputStream output = new FileOutputStream(destination_); - FileOutputStream fos = new FileOutputStream(destination_); - byte[] ch = new byte[512 * 1024]; - int nb; + long size = response.body().contentLength(); + byte[] buffer = new byte[8 * 1024]; + int len = 0; long written = 0; - long last_gui_update = 0; // size in byte - while ((nb = inStrm.read(ch)) != -1) { - fos.write(ch, 0, nb); - written += nb; - if ((written - last_gui_update) > 1000000) { // only update the gui every 1MB - gui_.status(String.format(status_, (int) (100.0 * written / size))); - last_gui_update = written; + long lastUpd = 0; // last GUI progress update + + while ((len = is.read(buffer)) != -1) { + output.write(buffer, 0, len); + written += len; + + if ((written - lastUpd) > 1000000) { // only update the gui every 1MB + gui_.status(status_, (int) (100.0 * written / size), written); + lastUpd = written; } } - fos.close(); - inStrm.close(); + + output.flush(); + output.close(); + is.close(); + + gui_.status(status_, 100, size); + long end = new Date().getTime(); this.log.debug(String.format("File downloaded at %.1f kB/s, written %d B", ((float) (size / 1000)) / ((float) (end - start) / 1000), written)); this.lastRequestTime = new Date().getTime(); + return 0; } catch (Exception e) { @@ -613,331 +465,163 @@ public int HTTPGetFile(String url_, String destination_, Gui gui_, String status e.printStackTrace(new PrintWriter(sw)); this.log.error("Server::HTTPGetFile Exception " + e + " stacktrace " + sw.toString()); } - this.log.debug("Server::HTTPGetFile(" + url_ + ", ...) will failed (end of function)"); + this.log.debug(String.format("Server::HTTPGetFile(%s) did fail", url_)); return -2; } - public ServerCode HTTPSendFile(String surl, String file1) { - this.log.debug("Server::HTTPSendFile(" + surl + "," + file1 + ")"); - - HttpURLConnection conn = null; - DataOutputStream dos = null; - BufferedReader inStream = null; - - String exsistingFileName = file1; - File fFile2Snd = new File(exsistingFileName); - - String lineEnd = "\r\n"; - String twoHyphens = "--"; - String boundary = "***232404jkg4220957934FW**"; - - int bytesRead, bytesAvailable, bufferSize; - byte[] buffer; - int maxBufferSize = 1 * 1024 * 1024; - - String urlString = surl; + public ServerCode HTTPSendFile(String surl, String file1, int checkpoint) { + this.log.debug(checkpoint, "Server::HTTPSendFile(" + surl + "," + file1 + ")"); try { - FileInputStream fileInputStream = new FileInputStream(new File(exsistingFileName)); - URL url = new URL(urlString); + String fileMimeType = Files.probeContentType(Paths.get(file1)); + + MediaType MEDIA_TYPE = MediaType.parse(fileMimeType); // e.g. "image/png" - conn = (HttpURLConnection) url.openConnection(); - conn.setDoInput(true); - conn.setDoOutput(true); - conn.setInstanceFollowRedirects(true); - conn.setUseCaches(false); + RequestBody uploadContent = new MultipartBody.Builder().setType(MultipartBody.FORM) + .addFormDataPart("file", new File(file1).getName(), RequestBody.create(new File(file1), MEDIA_TYPE)).build(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Connection", "Keep-Alive"); - conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + Request request = new Request.Builder().url(surl).post(uploadContent).build(); - if (urlString.startsWith("https://")) { + Call call = httpClient.newCall(request); + Response response = call.execute(); + + int r = response.code(); + String contentType = response.body().contentType().toString(); + + if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/xml")) { try { - SSLContext sc; - sc = SSLContext.getInstance("SSL"); - sc.init(null, new TrustManager[] { this }, null); - SSLSocketFactory factory = sc.getSocketFactory(); - ((HttpsURLConnection) conn).setSSLSocketFactory(factory); - ((HttpsURLConnection) conn).setHostnameVerifier(this); - } - catch (NoSuchAlgorithmException e) { - this.log.error("Server::HTTPSendFile, exception NoSuchAlgorithmException " + e); - try { - fileInputStream.close(); - } - catch (Exception e1) { - + String in = response.body().string(); + JobValidation jobValidation = new Persister().read(JobValidation.class, in); + + this.lastRequestTime = new Date().getTime(); + + ServerCode serverCode = ServerCode.fromInt(jobValidation.getStatus()); + if (serverCode != ServerCode.OK) { + this.log.error(checkpoint, "Server::HTTPSendFile wrong status (is " + serverCode + ")"); + return serverCode; } - return ServerCode.UNKNOWN; } - catch (KeyManagementException e) { - this.log.error("Server::HTTPSendFile, exception KeyManagementException " + e); - try { - fileInputStream.close(); - } - catch (Exception e1) { - - } - return ServerCode.UNKNOWN; + catch (Exception e) { // for the .read + e.printStackTrace(); } + + return ServerCode.OK; } - - dos = new DataOutputStream(conn.getOutputStream()); - dos.writeBytes(twoHyphens + boundary + lineEnd); - dos.writeBytes("Content-Disposition: form-data; name=\"file\";" + " filename=\"" + fFile2Snd.getName() + "\"" + lineEnd); - dos.writeBytes(lineEnd); - - bytesAvailable = fileInputStream.available(); - bufferSize = Math.min(bytesAvailable, maxBufferSize); - buffer = new byte[bufferSize]; - - bytesRead = fileInputStream.read(buffer, 0, bufferSize); - - while (bytesRead > 0) { - dos.write(buffer, 0, bufferSize); - bytesAvailable = fileInputStream.available(); - bufferSize = Math.min(bytesAvailable, maxBufferSize); - bytesRead = fileInputStream.read(buffer, 0, bufferSize); + else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) { + return ServerCode.ERROR_BAD_RESPONSE; + } + else { + this.log.error(String.format("Server::HTTPSendFile Unknown response received from server: %s", response.body().string())); } - dos.writeBytes(lineEnd); - dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd); - fileInputStream.close(); - dos.flush(); - dos.close(); - } - catch (MalformedURLException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.error("Server::HTTPSendFile, MalformedURLException " + e + " stacktrace " + sw.toString()); return ServerCode.UNKNOWN; } catch (IOException e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); - this.log.error("Server::HTTPSendFile, IOException " + e + " stacktrace " + sw.toString()); + this.log.error(checkpoint, String.format("Server::HTTPSendFile Error in upload process. Exception %s stacktrace ", e.getMessage(), sw.toString())); return ServerCode.UNKNOWN; } catch (OutOfMemoryError e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); - this.log.error("Server::HTTPSendFile, OutOfMemoryError " + e + " stacktrace " + sw.toString()); + this.log.error(checkpoint, "Server::HTTPSendFile, OutOfMemoryError " + e + " stacktrace " + sw.toString()); return ServerCode.JOB_VALIDATION_ERROR_UPLOAD_FAILED; } catch (Exception e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); e.printStackTrace(pw); - this.log.error("Server::HTTPSendFile, Exception " + e + " stacktrace " + sw.toString()); - return ServerCode.UNKNOWN; - } - - int r; - try { - r = conn.getResponseCode(); - } - catch (IOException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPSendFile IOException " + e + " stacktrace: " + sw.toString()); + this.log.error(checkpoint, "Server::HTTPSendFile, Exception " + e + " stacktrace " + sw.toString()); return ServerCode.UNKNOWN; } - String contentType = conn.getContentType(); - - if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/xml")) { - DataInputStream in; - try { - in = new DataInputStream(conn.getInputStream()); - } - catch (IOException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPSendFile IOException " + e + " stacktrace: " + sw.toString()); - return ServerCode.UNKNOWN; - } - Document document = null; + } + + private String generateXMLForMD5cache() { + List md5s = new ArrayList<>(); + for (File local_file : this.user_config.getLocalCacheFiles()) { try { - document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); - } - catch (SAXException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPSendFile SAXException " + e + " stacktrace: " + sw.toString()); - return ServerCode.UNKNOWN; - } - catch (IOException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPSendFile IOException " + e + " stacktrace: " + sw.toString()); - return ServerCode.UNKNOWN; - } - catch (ParserConfigurationException e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::HTTPSendFile ParserConfigurationException " + e + " stacktrace: " + sw.toString()); - return ServerCode.UNKNOWN; + String extension = local_file.getName().substring(local_file.getName().lastIndexOf('.')).toLowerCase(); + String name = local_file.getName().substring(0, local_file.getName().length() - 1 * extension.length()); + if (extension.equals(".zip")) { + // node_file.setAttribute("md5", name); + FileMD5 fileMD5 = new FileMD5(); + fileMD5.setMd5(name); + + md5s.add(fileMD5); + } } - - this.lastRequestTime = new Date().getTime(); - - ServerCode ret1 = Utils.statusIsOK(document, "jobvalidate"); - if (ret1 != ServerCode.OK) { - this.log.error("Server::HTTPSendFile wrong status (is " + ret1 + ")"); - return ret1; + catch (StringIndexOutOfBoundsException e) { // because the file does not have an . its path } - return ServerCode.OK; } - else if (r == HttpURLConnection.HTTP_OK && contentType.startsWith("text/html")) { - return ServerCode.ERROR_BAD_RESPONSE; + + CacheFileMD5 cache = new CacheFileMD5(); + cache.setMd5s(md5s); + + final Persister persister = new Persister(); + try (StringWriter writer = new StringWriter()) { + persister.write(cache, writer); + return writer.toString(); } - else { - try { - inStream = new BufferedReader(new InputStreamReader(conn.getInputStream())); - - String str; - while ((str = inStream.readLine()) != null) { - System.out.println(str); - System.out.println(""); + catch (final Exception e) { + log.debug("Failed to dump md5s " + e); + return ""; + } + } + + private void handleFileMD5DeleteDocument(List fileMD5s) { + if (fileMD5s != null && fileMD5s.isEmpty() == false) { + for (FileMD5 fileMD5 : fileMD5s) { + if ("delete".equals(fileMD5.getAction()) && fileMD5.getMd5() != null && fileMD5.getMd5().isEmpty() == false) { + String path = this.user_config.getWorkingDirectory().getAbsolutePath() + File.separatorChar + fileMD5.getMd5(); + this.log.debug("Server::handleFileMD5DeleteDocument delete old file " + path); + File file_to_delete = new File(path + ".zip"); + file_to_delete.delete(); + Utils.delete(new File(path)); } - inStream.close(); - } - catch (IOException ioex) { } } - return ServerCode.UNKNOWN; } - public byte[] getLastRender() { - try { - HttpURLConnection httpCon = this.HTTPRequest(this.getPage("last-render-frame")); - - InputStream inStrm = httpCon.getInputStream(); - if (httpCon.getResponseCode() != HttpURLConnection.HTTP_OK) { - this.log.debug("Server::getLastRender code not ok " + httpCon.getResponseCode()); - return null; - } - int size = httpCon.getContentLength(); - - if (size <= 0) { - this.log.debug("Server::getLastRender size is negative (size: " + size + ")"); - return null; - } - - byte[] ret = new byte[size]; - byte[] ch = new byte[512 * 1024]; - int n = 0; - int i = 0; - while ((n = inStrm.read(ch)) != -1) { - System.arraycopy(ch, 0, ret, i, n); - i += n; + public String getPage(String key) { + if (this.serverConfig != null) { + RequestEndPoint endpoint = this.serverConfig.getRequestEndPoint(key); + if (endpoint != null) { + return this.base_url + endpoint.getPath(); } - inStrm.close(); - return ret; - } - catch (Exception e) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - this.log.debug("Server::getLastRender Exception " + e + " stacktrace: " + sw.toString()); } - return null; + return ""; } - private String generateXMLForMD5cache() { - String xml_str = null; + private OkHttpClient getOkHttpClient() { try { - DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); - Document document_cache = docBuilder.newDocument(); + OkHttpClient.Builder builder = new OkHttpClient.Builder(); - Element rootElement = document_cache.createElement("cache"); - document_cache.appendChild(rootElement); + CookieManager cookieManager = new CookieManager(); + cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); + builder.cookieJar(new JavaNetCookieJar(cookieManager)); // Cookie store to maintain the session across calls - List local_files = this.user_config.getLocalCacheFiles(); - for (File local_file : local_files) { - Element node_file = document_cache.createElement("file"); - rootElement.appendChild(node_file); - try { - String extension = local_file.getName().substring(local_file.getName().lastIndexOf('.')).toLowerCase(); - String name = local_file.getName().substring(0, local_file.getName().length() - 1 * extension.length()); - if (extension.equals(".zip")) { - node_file.setAttribute("md5", name); - } - } - catch (StringIndexOutOfBoundsException e) { // because the file does not have an . his path - } - } + builder.connectTimeout(30, TimeUnit.SECONDS); // Cancel the HTTP Request if the connection to server takes more than 10 seconds + builder.writeTimeout(60, TimeUnit.SECONDS); // Cancel the upload if the client cannot send any byte in 60 seconds - TransformerFactory tf = TransformerFactory.newInstance(); - Transformer transformer = tf.newTransformer(); - transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); - StringWriter writer = new StringWriter(); - transformer.transform(new DOMSource(document_cache), new StreamResult(writer)); - xml_str = writer.getBuffer().toString(); - } - catch (TransformerConfigurationException e) { - this.log.debug("Server::generateXMLForMD5cache " + e); - } - catch (TransformerException e) { - this.log.debug("Server::generateXMLForMD5cache " + e); - } - catch (ParserConfigurationException e) { - this.log.debug("Server::generateXMLForMD5cache " + e); - } - - return xml_str; - } - - private void handleFileMD5DeleteDocument(Document document, String root_nodename) { - NodeList ns = document.getElementsByTagName(root_nodename); - if (ns.getLength() > 0) { - Element root_node = (Element) ns.item(0); - ns = root_node.getElementsByTagName("file"); - if (ns.getLength() > 0) { - for (int i = 0; i < ns.getLength(); ++i) { - Element file_node = (Element) ns.item(i); - if (file_node.hasAttribute("md5") && file_node.hasAttribute("action") && file_node.getAttribute("action").equals("delete")) { - String path = this.user_config.workingDirectory.getAbsolutePath() + File.separatorChar + file_node.getAttribute("md5"); - this.log.debug("Server::handleFileMD5DeleteDocument delete old file " + path); - File file_to_delete = new File(path + ".zip"); - file_to_delete.delete(); - Utils.delete(new File(path)); - } - } + // If the user has selected a proxy, then we must increase the download timeout. Reason being the way proxies work. To download a large file (i.e. + // a 500MB job), the proxy must first download the file to the proxy cache and then the information is sent fast to the SheepIt client. From a client + // viewpoint, the HTTP connection will make the CONNECT step really fast but then the time until the fist byte is received (the time measured by + // readTimeout) will be really long (minutes). Without a proxy in the middle, a connection that does receive nothing in 60 seconds might be + // considered a dead connection. + if (this.user_config.getProxy() != null) { + builder.readTimeout(10, TimeUnit.MINUTES); // Proxy enabled - 10 minutes + } + else { + builder.readTimeout(1, TimeUnit.MINUTES); // No proxy - 60 seconds max } + + return builder.build(); } - } - - public String getPage(String key) { - if (this.pages.containsKey(key)) { - return this.base_url + this.pages.get(key); + catch (Exception e) { + throw new RuntimeException(e); } - return ""; - } - - @Override - public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - @Override - public boolean verify(String arg0, SSLSession arg1) { - return true; // trust every ssl certificate } } diff --git a/src/com/sheepit/client/SettingsLoader.java b/src/com/sheepit/client/SettingsLoader.java index 553bc87c..445462bd 100644 --- a/src/com/sheepit/client/SettingsLoader.java +++ b/src/com/sheepit/client/SettingsLoader.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -32,28 +32,32 @@ import java.util.Properties; import java.util.Set; -import com.sheepit.client.Configuration; import com.sheepit.client.Configuration.ComputeType; import com.sheepit.client.hardware.gpu.GPU; import com.sheepit.client.hardware.gpu.GPUDevice; +import lombok.Setter; public class SettingsLoader { private String path; private String login; - private String password; + + @Setter private String password; + private String proxy; private String hostname; private String computeMethod; private String gpu; + private String renderbucketSize; private String cores; private String ram; private String renderTime; private String cacheDir; private String autoSignIn; + private String useSysTray; private String ui; - private String tileSize; - private int priority; + private String theme; + private int priority; public SettingsLoader(String path_) { if (path_ == null) { @@ -64,7 +68,9 @@ public SettingsLoader(String path_) { } } - public SettingsLoader(String path_, String login_, String password_, String proxy_, String hostname_, ComputeType computeMethod_, GPUDevice gpu_, int cores_, int maxRam_, int maxRenderTime_, String cacheDir_, boolean autoSignIn_, String ui_, String tileSize_, int priority_) { + public SettingsLoader(String path_, String login_, String password_, String proxy_, String hostname_, ComputeType computeMethod_, GPUDevice gpu_, + int renderbucketSize_, int cores_, long maxRam_, int maxRenderTime_, String cacheDir_, boolean autoSignIn_, boolean useSysTray_, String ui_, + String theme_, int priority_) { if (path_ == null) { path = getDefaultFilePath(); } @@ -77,14 +83,16 @@ public SettingsLoader(String path_, String login_, String password_, String prox hostname = hostname_; cacheDir = cacheDir_; autoSignIn = String.valueOf(autoSignIn_); + useSysTray = String.valueOf(useSysTray_); ui = ui_; - tileSize = tileSize_; priority = priority_; + theme = theme_; + if (cores_ > 0) { cores = String.valueOf(cores_); } if (maxRam_ > 0) { - ram = String.valueOf(maxRam_); + ram = String.valueOf(maxRam_) + "k"; } if (maxRenderTime_ > 0) { renderTime = String.valueOf(maxRenderTime_); @@ -100,6 +108,10 @@ public SettingsLoader(String path_, String login_, String password_, String prox if (gpu_ != null) { gpu = gpu_.getId(); } + + if (renderbucketSize_ >= 32) { + renderbucketSize = String.valueOf(renderbucketSize_); + } } public static String getDefaultFilePath() { @@ -129,6 +141,10 @@ public void saveFile() { prop.setProperty("compute-gpu", gpu); } + if (renderbucketSize != null) { + prop.setProperty("renderbucket-size", renderbucketSize); + } + if (cores != null) { prop.setProperty("cores", cores); } @@ -161,12 +177,16 @@ public void saveFile() { prop.setProperty("auto-signin", autoSignIn); } + if (useSysTray != null) { + prop.setProperty("use-systray", useSysTray); + } + if (ui != null) { prop.setProperty("ui", ui); } - if (tileSize != null) { - prop.setProperty("tile-size", tileSize); + if (theme != null) { + prop.setProperty("theme", theme); } prop.store(output, null); @@ -208,13 +228,15 @@ public void loadFile() { this.hostname = null; this.computeMethod = null; this.gpu = null; + this.renderbucketSize = null; this.cacheDir = null; this.autoSignIn = null; + this.useSysTray = null; this.ui = null; - this.tileSize = null; this.priority = 19; // must be the same default as Configuration this.ram = null; this.renderTime = null; + this.theme = null; if (new File(path).exists() == false) { return; @@ -238,6 +260,10 @@ public void loadFile() { this.gpu = prop.getProperty("compute-gpu"); } + if (prop.containsKey("renderbucket-size")) { + this.renderbucketSize = prop.getProperty("renderbucket-size"); + } + if (prop.containsKey("cpu-cores")) { // backward compatibility this.cores = prop.getProperty("cpu-cores"); } @@ -274,12 +300,16 @@ public void loadFile() { this.autoSignIn = prop.getProperty("auto-signin"); } + if (prop.containsKey("use-systray")) { + this.useSysTray = prop.getProperty("use-systray"); + } + if (prop.containsKey("ui")) { this.ui = prop.getProperty("ui"); } - if (prop.containsKey("tile-size")) { - this.tileSize = prop.getProperty("tile-size"); + if (prop.containsKey("theme")) { + this.theme = prop.getProperty("theme"); } if (prop.containsKey("priority")) { @@ -312,10 +342,10 @@ public void merge(Configuration config) { loadFile(); - if (config.login().isEmpty() && login != null) { + if (config.getLogin().isEmpty() && login != null) { config.setLogin(login); } - if (config.password().isEmpty() && password != null) { + if (config.getPassword().isEmpty() && password != null) { config.setPassword(password); } @@ -331,7 +361,8 @@ public void merge(Configuration config) { config.setUsePriority(priority); } try { - if ((config.getComputeMethod() == null && computeMethod != null) || (computeMethod != null && config.getComputeMethod() != ComputeType.valueOf(computeMethod))) { + if ((config.getComputeMethod() == null && computeMethod != null) || (computeMethod != null && config.getComputeMethod() != ComputeType + .valueOf(computeMethod))) { config.setComputeMethod(ComputeType.valueOf(computeMethod)); } } @@ -342,22 +373,41 @@ public void merge(Configuration config) { if (config.getGPUDevice() == null && gpu != null) { GPUDevice device = GPU.getGPUDevice(gpu); if (device != null) { - config.setUseGPU(device); + config.setGPUDevice(device); + + // If the user has indicated a render bucket size at least 32x32 px, overwrite the config file value + if (config.getRenderbucketSize() >= 32) { + config.getGPUDevice().setRenderbucketSize(config.getRenderbucketSize()); // Update size + } + else { + // If the configuration file does have any value + if (renderbucketSize != null) { + config.getGPUDevice().setRenderbucketSize(Integer.valueOf(renderbucketSize)); + } + else { + // Don't do anything here as the GPU get's a default value when it's initialised + // The configuration will take the default GPU value + } + } + + // And now update the client configuration with the new value + config.setRenderbucketSize(config.getGPUDevice().getRenderbucketSize()); } } + if (config.getNbCores() == -1 && cores != null) { - config.setUseNbCores(Integer.valueOf(cores)); + config.setNbCores(Integer.valueOf(cores)); } if (config.getMaxMemory() == -1 && ram != null) { - config.setMaxMemory(Integer.valueOf(ram)); + config.setMaxMemory(Utils.parseNumber(ram) / 1000); // internal ram value is in kB } if (config.getMaxRenderTime() == -1 && renderTime != null) { config.setMaxRenderTime(Integer.valueOf(renderTime)); } - if (config.getUserHasSpecifiedACacheDir() == false && cacheDir != null) { + if (config.isUserHasSpecifiedACacheDir() == false && cacheDir != null) { config.setCacheDir(new File(cacheDir)); } @@ -365,15 +415,27 @@ public void merge(Configuration config) { config.setUIType(ui); } - if (config.getTileSize() == -1 && tileSize != null) { - config.setTileSize(Integer.valueOf(tileSize)); + if (config.getTheme() == null) { + if (this.theme != null) { + config.setTheme(this.theme); + } + else { + config.setTheme("light"); + } + } + + // if the user has invoked the app with --no-systray, then we just overwrite the existing configuration with (boolean)false. If no parameter has been + // specified and the settings file contains use-systray=false, then deactivate as well. + if (!config.isUseSysTray() || (config.isUseSysTray() && useSysTray != null && useSysTray.equals("false"))) { + config.setUseSysTray(false); } - config.setAutoSignIn(Boolean.valueOf(autoSignIn)); + config.setAutoSignIn(Boolean.parseBoolean(autoSignIn)); } - @Override - public String toString() { - return "SettingsLoader [path=" + path + ", login=" + login + ", password=" + password + ", computeMethod=" + computeMethod + ", gpu=" + gpu + ", cacheDir=" + cacheDir + "priority="+priority+"]"; + @Override public String toString() { + return String.format( + "SettingsLoader [path=%s, login=%s, password=%s, computeMethod=%s, gpu=%s, renderbucket-size=%s, cacheDir=%s, theme=%s, priority=%d, autosign=%s, usetray=%s]", + path, login, password, computeMethod, gpu, renderbucketSize, cacheDir, theme, priority, autoSignIn, useSysTray); } } diff --git a/src/com/sheepit/client/ShutdownHook.java b/src/com/sheepit/client/ShutdownHook.java index 7b8ac75a..79744a8a 100644 --- a/src/com/sheepit/client/ShutdownHook.java +++ b/src/com/sheepit/client/ShutdownHook.java @@ -2,7 +2,7 @@ * Copyright (C) 2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -28,8 +28,7 @@ public ShutdownHook(Client client_) { public void attachShutDownHook() { Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { + @Override public void run() { _client.stop(); } }); diff --git a/src/com/sheepit/client/Stats.java b/src/com/sheepit/client/Stats.java index 5b3c66b8..c14d81ae 100644 --- a/src/com/sheepit/client/Stats.java +++ b/src/com/sheepit/client/Stats.java @@ -2,7 +2,7 @@ * Copyright (C) 2016 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -23,13 +23,15 @@ public class Stats { private int remainingFrame; private int creditsEarned; private int creditsEarnedSession; + private int renderableProject; private int waitingProject; private int connectedMachine; - public Stats(int frame, int credits, int creditsSession, int waitings, int machines) { + public Stats(int frame, int credits, int creditsSession, int renderables, int waitings, int machines) { remainingFrame = frame; creditsEarned = credits; creditsEarnedSession = creditsSession; + renderableProject = renderables; waitingProject = waitings; connectedMachine = machines; } @@ -38,6 +40,7 @@ public Stats() { remainingFrame = 0; creditsEarned = 0; creditsEarnedSession = 0; + renderableProject = 0; waitingProject = 0; connectedMachine = 0; } @@ -54,6 +57,10 @@ public int getCreditsEarned() { return creditsEarned; } + public int getRenderableProject() { + return renderableProject; + } + public int getWaitingProject() { return waitingProject; } @@ -62,8 +69,8 @@ public int getConnectedMachine() { return connectedMachine; } - @Override - public String toString() { - return "Stats [remainingFrame=" + remainingFrame + ", creditsEarned=" + creditsEarned + ", creditsEarnedSession=" + creditsEarnedSession + ", waitingProject=" + waitingProject + ", connectedMachine=" + connectedMachine + "]"; + @Override public String toString() { + return "Stats [remainingFrame=" + remainingFrame + ", creditsEarned=" + creditsEarned + ", creditsEarnedSession=" + creditsEarnedSession + + ", renderableProject=" + renderableProject + ", waitingProject=" + waitingProject + ", connectedMachine=" + connectedMachine + "]"; } } diff --git a/src/com/sheepit/client/Utils.java b/src/com/sheepit/client/Utils.java index 5ada0196..cb28cb14 100644 --- a/src/com/sheepit/client/Utils.java +++ b/src/com/sheepit/client/Utils.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -20,8 +20,6 @@ package com.sheepit.client; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; @@ -51,7 +49,8 @@ import com.sheepit.client.exception.FermeExceptionNoSpaceLeftOnDevice; public class Utils { - public static int unzipFileIntoDirectory(String zipFileName_, String destinationDirectory, String password, Log log) throws FermeExceptionNoSpaceLeftOnDevice { + public static int unzipFileIntoDirectory(String zipFileName_, String destinationDirectory, String password, Log log) + throws FermeExceptionNoSpaceLeftOnDevice { try { ZipFile zipFile = new ZipFile(zipFileName_); UnzipParameters unzipParameters = new UnzipParameters(); @@ -146,6 +145,10 @@ public static void delete(File file) { file.delete(); } + /** + * Parse a number string to a number. + * Input can be as "32", "10k", "100K", "100G", "1.3G", "0.4T" + */ public static long parseNumber(String in) { in = in.trim(); in = in.replaceAll(",", "."); @@ -158,18 +161,22 @@ public static long parseNumber(String in) { m.find(); int scale = 1; switch (m.group(2).charAt(0)) { + case 'T': + case 't': + scale = 1000 * 1000 * 1000 * 1000; + break; case 'G': - scale *= 1000; case 'g': - scale *= 1000; + scale = 1000 * 1000 * 1000; + break; case 'M': - scale *= 1000; case 'm': - scale *= 1000; + scale = 1000 * 1000; + break; case 'K': + case 'k': + scale = 1000; break; - default: - return 0; } return Math.round(Double.parseDouble(m.group(1)) * scale); } @@ -184,10 +191,10 @@ public static String humanDuration(Date date) { String output = ""; if (hours > 0) { - output += hours + "h"; + output += hours + "h "; } if (minutes > 0) { - output += minutes + "min"; + output += minutes + "min "; } if (seconds > 0) { output += seconds + "s"; diff --git a/src/com/sheepit/client/datamodel/CacheFileMD5.java b/src/com/sheepit/client/datamodel/CacheFileMD5.java new file mode 100644 index 00000000..8cd91d60 --- /dev/null +++ b/src/com/sheepit/client/datamodel/CacheFileMD5.java @@ -0,0 +1,16 @@ +package com.sheepit.client.datamodel; + +import lombok.Data; +import lombok.ToString; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +@Root(strict = false, name = "cache") @Data @ToString public class CacheFileMD5 { + + @ElementList(inline = true) private List md5s; + + public CacheFileMD5() { + } +} diff --git a/src/com/sheepit/client/datamodel/FileMD5.java b/src/com/sheepit/client/datamodel/FileMD5.java new file mode 100644 index 00000000..7469e17d --- /dev/null +++ b/src/com/sheepit/client/datamodel/FileMD5.java @@ -0,0 +1,16 @@ +package com.sheepit.client.datamodel; + +import lombok.Data; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "file") @Data @ToString public class FileMD5 { + + @Attribute private String md5; + + @Attribute(required = false) private String action; + + public FileMD5() { + } +} diff --git a/src/com/sheepit/client/datamodel/HeartBeatInfos.java b/src/com/sheepit/client/datamodel/HeartBeatInfos.java new file mode 100644 index 00000000..950f135d --- /dev/null +++ b/src/com/sheepit/client/datamodel/HeartBeatInfos.java @@ -0,0 +1,10 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "keepmealive") @ToString public class HeartBeatInfos { + @Attribute @Getter private int status; +} diff --git a/src/com/sheepit/client/datamodel/JobInfos.java b/src/com/sheepit/client/datamodel/JobInfos.java new file mode 100644 index 00000000..b4c879aa --- /dev/null +++ b/src/com/sheepit/client/datamodel/JobInfos.java @@ -0,0 +1,24 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +@Root(strict = false, name = "jobrequest") @ToString public class JobInfos { + + @Attribute @Getter private int status; + + @Element(name = "stats", required = false) @Getter private SessionStats sessionStats; + + @Element(name = "job", required = false) @Getter() private RenderTask renderTask; + + @ElementList(name = "file", inline = true, required = false) @Getter private List fileMD5s; + + public JobInfos() { + } +} diff --git a/src/com/sheepit/client/datamodel/JobValidation.java b/src/com/sheepit/client/datamodel/JobValidation.java new file mode 100644 index 00000000..01749a5b --- /dev/null +++ b/src/com/sheepit/client/datamodel/JobValidation.java @@ -0,0 +1,14 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "jobvalidate") @ToString public class JobValidation { + + @Attribute @Getter private int status; + + public JobValidation() { + } +} diff --git a/src/com/sheepit/client/datamodel/RenderTask.java b/src/com/sheepit/client/datamodel/RenderTask.java new file mode 100644 index 00000000..c1e34384 --- /dev/null +++ b/src/com/sheepit/client/datamodel/RenderTask.java @@ -0,0 +1,38 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "job") @ToString public class RenderTask { + + @Attribute(name = "id") @Getter private String id; + + @Attribute(name = "use_gpu") @Getter private int useGpu; + + @Attribute(name = "archive_md5") @Getter private String archive_md5; + + @Attribute(name = "path") @Getter private String path; + + @Attribute(name = "frame") @Getter private String frame; + + @Attribute(name = "synchronous_upload") @Getter private String synchronous_upload; + + @Attribute(name = "extras") @Getter private String extras; + + @Attribute(name = "validation_url") @Getter private String validationUrl; + + @Attribute(name = "name") @Getter private String name; + + @Attribute(name = "password") @Getter private String password; + + @Element(name = "renderer") @Getter private RendererInfos rendererInfos; + + @Element(name = "script", data = true) @Getter private String script; + + public RenderTask() { + + } +} diff --git a/src/com/sheepit/client/datamodel/RendererInfos.java b/src/com/sheepit/client/datamodel/RendererInfos.java new file mode 100644 index 00000000..c352a9ba --- /dev/null +++ b/src/com/sheepit/client/datamodel/RendererInfos.java @@ -0,0 +1,19 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "renderer") @ToString public class RendererInfos { + + @Attribute(name = "md5") @Getter private String md5; + + @Attribute(name = "commandline") @Getter private String commandline; + + @Attribute(name = "update_method") @Getter private String update_method; + + public RendererInfos() { + + } +} diff --git a/src/com/sheepit/client/datamodel/RequestEndPoint.java b/src/com/sheepit/client/datamodel/RequestEndPoint.java new file mode 100644 index 00000000..6c512924 --- /dev/null +++ b/src/com/sheepit/client/datamodel/RequestEndPoint.java @@ -0,0 +1,18 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "request") @ToString public class RequestEndPoint { + + @Attribute @Getter private String type; + + @Attribute @Getter private String path; + + @Attribute(name = "max-period", required = false) @Getter private int maxPeriod; + + public RequestEndPoint() { + } +} diff --git a/src/com/sheepit/client/datamodel/ServerConfig.java b/src/com/sheepit/client/datamodel/ServerConfig.java new file mode 100644 index 00000000..71b84be8 --- /dev/null +++ b/src/com/sheepit/client/datamodel/ServerConfig.java @@ -0,0 +1,32 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +@Root(strict = false, name = "config") @ToString public class ServerConfig { + + @Attribute @Getter private int status; + + @Attribute(required = false) @Getter private String publickey; + + @ElementList(name = "request", inline = true, required = false) private List requestEndPoints; + + public ServerConfig() { + } + + public RequestEndPoint getRequestEndPoint(String type) { + if (requestEndPoints != null) { + for (RequestEndPoint endPoint : requestEndPoints) { + if (type.equals(endPoint.getType())) { + return endPoint; + } + } + } + return null; + } +} diff --git a/src/com/sheepit/client/datamodel/SessionStats.java b/src/com/sheepit/client/datamodel/SessionStats.java new file mode 100644 index 00000000..ae03c199 --- /dev/null +++ b/src/com/sheepit/client/datamodel/SessionStats.java @@ -0,0 +1,25 @@ +package com.sheepit.client.datamodel; + +import lombok.Getter; +import lombok.ToString; +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; + +@Root(strict = false, name = "stats") @ToString public class SessionStats { + + @Attribute(name = "credits_session") @Getter private int pointsEarnedOnSession; + + @Attribute(name = "credits_total") @Getter private int pointsEarnedByUser; + + @Attribute(name = "frame_remaining") @Getter private int remainingFrames; + + @Attribute(name = "waiting_project") @Getter private int waitingProjects; + + @Attribute(name = "renderable_project", required = false) @Getter private int renderableProjects; + + @Attribute(name = "connected_machine") @Getter private int connectedMachines; + + public SessionStats() { + + } +} diff --git a/src/com/sheepit/client/exception/FermeException.java b/src/com/sheepit/client/exception/FermeException.java index 6eda6828..c0799b93 100644 --- a/src/com/sheepit/client/exception/FermeException.java +++ b/src/com/sheepit/client/exception/FermeException.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionBadResponseFromServer.java b/src/com/sheepit/client/exception/FermeExceptionBadResponseFromServer.java index 010aeed1..1e5347f2 100644 --- a/src/com/sheepit/client/exception/FermeExceptionBadResponseFromServer.java +++ b/src/com/sheepit/client/exception/FermeExceptionBadResponseFromServer.java @@ -2,7 +2,7 @@ * Copyright (C) 2016 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionNoRendererAvailable.java b/src/com/sheepit/client/exception/FermeExceptionNoRendererAvailable.java index 054ef0f6..7682444b 100644 --- a/src/com/sheepit/client/exception/FermeExceptionNoRendererAvailable.java +++ b/src/com/sheepit/client/exception/FermeExceptionNoRendererAvailable.java @@ -2,7 +2,7 @@ * Copyright (C) 2017 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionNoRightToRender.java b/src/com/sheepit/client/exception/FermeExceptionNoRightToRender.java index 3d5ec6ed..fdfad57c 100644 --- a/src/com/sheepit/client/exception/FermeExceptionNoRightToRender.java +++ b/src/com/sheepit/client/exception/FermeExceptionNoRightToRender.java @@ -2,7 +2,7 @@ * Copyright (C) 2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionNoSession.java b/src/com/sheepit/client/exception/FermeExceptionNoSession.java index 4b6e17e6..08eaf3dd 100644 --- a/src/com/sheepit/client/exception/FermeExceptionNoSession.java +++ b/src/com/sheepit/client/exception/FermeExceptionNoSession.java @@ -2,7 +2,7 @@ * Copyright (C) 2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionNoSpaceLeftOnDevice.java b/src/com/sheepit/client/exception/FermeExceptionNoSpaceLeftOnDevice.java index 71c048e4..abd557f3 100644 --- a/src/com/sheepit/client/exception/FermeExceptionNoSpaceLeftOnDevice.java +++ b/src/com/sheepit/client/exception/FermeExceptionNoSpaceLeftOnDevice.java @@ -2,7 +2,7 @@ * Copyright (C) 2016 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionServerInMaintenance.java b/src/com/sheepit/client/exception/FermeExceptionServerInMaintenance.java index 6779ef8a..13804cea 100644 --- a/src/com/sheepit/client/exception/FermeExceptionServerInMaintenance.java +++ b/src/com/sheepit/client/exception/FermeExceptionServerInMaintenance.java @@ -2,7 +2,7 @@ * Copyright (C) 2016 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionServerOverloaded.java b/src/com/sheepit/client/exception/FermeExceptionServerOverloaded.java index a41a67e4..22d6eba4 100644 --- a/src/com/sheepit/client/exception/FermeExceptionServerOverloaded.java +++ b/src/com/sheepit/client/exception/FermeExceptionServerOverloaded.java @@ -2,7 +2,7 @@ * Copyright (C) 2016 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeExceptionSessionDisabled.java b/src/com/sheepit/client/exception/FermeExceptionSessionDisabled.java index 3997ec1b..81733c14 100644 --- a/src/com/sheepit/client/exception/FermeExceptionSessionDisabled.java +++ b/src/com/sheepit/client/exception/FermeExceptionSessionDisabled.java @@ -2,7 +2,7 @@ * Copyright (C) 2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/exception/FermeServerDown.java b/src/com/sheepit/client/exception/FermeServerDown.java index 00f3295f..9ccd534b 100644 --- a/src/com/sheepit/client/exception/FermeServerDown.java +++ b/src/com/sheepit/client/exception/FermeServerDown.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -21,7 +21,6 @@ /** * Server down (server side error) or unreachable (client side error) - * */ public class FermeServerDown extends FermeException { public FermeServerDown() { diff --git a/src/com/sheepit/client/hardware/cpu/CPU.java b/src/com/sheepit/client/hardware/cpu/CPU.java index 0d4ea7c4..49f59721 100644 --- a/src/com/sheepit/client/hardware/cpu/CPU.java +++ b/src/com/sheepit/client/hardware/cpu/CPU.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/hardware/gpu/GPU.java b/src/com/sheepit/client/hardware/gpu/GPU.java index 9866d42d..08dd2757 100644 --- a/src/com/sheepit/client/hardware/gpu/GPU.java +++ b/src/com/sheepit/client/hardware/gpu/GPU.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -63,7 +63,7 @@ public static List listModels() { public static List listDevices(Configuration config) { if (devices == null) { - if (config.getDetectGPUs()) { + if (config.isDetectGPUs()) { generate(); } else { @@ -74,8 +74,8 @@ public static List listDevices(Configuration config) { return devices; } - public static GPUDevice getGPUDevice(String device_model) { - if (device_model == null) { + public static GPUDevice getGPUDevice(String deviceId) { + if (deviceId == null) { return null; } @@ -88,7 +88,7 @@ public static GPUDevice getGPUDevice(String device_model) { } for (GPUDevice dev : devices) { - if (device_model.equals(dev.getId()) || device_model.equals(dev.getModel())) { + if (deviceId.equals(dev.getId()) || deviceId.equals(dev.getOldId())) { return dev; } } diff --git a/src/com/sheepit/client/hardware/gpu/GPUDevice.java b/src/com/sheepit/client/hardware/gpu/GPUDevice.java index b138a517..58d1513a 100644 --- a/src/com/sheepit/client/hardware/gpu/GPUDevice.java +++ b/src/com/sheepit/client/hardware/gpu/GPUDevice.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -19,24 +19,36 @@ package com.sheepit.client.hardware.gpu; +import com.sheepit.client.hardware.gpu.nvidia.Nvidia; +import com.sheepit.client.hardware.gpu.opencl.OpenCL; + public class GPUDevice { private String type; private String model; private long memory; // in B + private int renderBucketSize; private String id; + private String oldId; // for backward compatibility + public GPUDevice(String type, String model, long ram, String id) { this.type = type; this.model = model; this.memory = ram; this.id = id; + this.renderBucketSize = 32; + } + + public GPUDevice(String type, String model, long ram, String id, String oldId) { + this(type, model, ram, id); + this.oldId = oldId; } public String getType() { return type; } - + public void setType(String type) { this.type = type; } @@ -65,15 +77,50 @@ public void setId(String id) { this.id = id; } - @Override - public String toString() { - return "GPUDevice [type=" + type + ", model='" + model + "', memory=" + memory + ", id=" + id + "]"; + public String getOldId() { + return oldId; } - public int getRecommandedTileSize() { - // GPU - // if the vram is lower than 1G reduce the size of tile to avoid black output - return (getMemory() > 1073741824L) ? 256 : 128; + public void setOldId(String id) { + this.oldId = id; } + public int getRenderbucketSize() { + return this.renderBucketSize; + } + + public void setRenderbucketSize(int proposedRenderbucketSize) { + int renderBucketSize = 32; + GPULister gpu; + + if (type.equals("CUDA")) { + gpu = new Nvidia(); + } + else if (type.equals("OPENCL")) { + gpu = new OpenCL(); + } + else { + // If execution takes this branch is because we weren't able to detect the proper GPU technology or + // because is a new one (different from CUDA and OPENCL). In that case, move into the safest position + // of 32x32 pixel tile sizes + System.out.println("GPUDevice::setRenderbucketSize Unable to detect GPU technology. Render bucket size set to 32x32 pixels"); + this.renderBucketSize = 32; + return; + } + + if (proposedRenderbucketSize >= 32) { + if (proposedRenderbucketSize <= gpu.getMaximumRenderBucketSize(getMemory())) { + renderBucketSize = proposedRenderbucketSize; + } + else { + renderBucketSize = gpu.getRecommendedRenderBucketSize(getMemory()); + } + } + + this.renderBucketSize = renderBucketSize; + } + + @Override public String toString() { + return "GPUDevice [type=" + type + ", model='" + model + "', memory=" + memory + ", id=" + id + ", renderbucketSize=" + renderBucketSize + "]"; + } } diff --git a/src/com/sheepit/client/hardware/gpu/GPULister.java b/src/com/sheepit/client/hardware/gpu/GPULister.java index 8ef034c3..9aecafa3 100644 --- a/src/com/sheepit/client/hardware/gpu/GPULister.java +++ b/src/com/sheepit/client/hardware/gpu/GPULister.java @@ -4,4 +4,8 @@ public interface GPULister { public abstract List getGpus(); + + public abstract int getRecommendedRenderBucketSize(long memory); + + public abstract int getMaximumRenderBucketSize(long memory); } diff --git a/src/com/sheepit/client/hardware/gpu/nvidia/CUDA.java b/src/com/sheepit/client/hardware/gpu/nvidia/CUDA.java index fbf17f85..38129f16 100644 --- a/src/com/sheepit/client/hardware/gpu/nvidia/CUDA.java +++ b/src/com/sheepit/client/hardware/gpu/nvidia/CUDA.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -27,17 +27,18 @@ public interface CUDA extends Library { public int cuInit(int flags); /* - * @return: CUDA_SUCCESS, CUDA_ERROR_DEINITIALIZED, CUDA_ERROR_NOT_INITIALIZED, CUDA_ERROR_INVALID_CONTEXT, CUDA_ERROR_INVALID_VALUE - */ + * @return: CUDA_SUCCESS, CUDA_ERROR_DEINITIALIZED, CUDA_ERROR_NOT_INITIALIZED, CUDA_ERROR_INVALID_CONTEXT, CUDA_ERROR_INVALID_VALUE + */ public int cuDeviceGetCount(IntByReference count); public int cuDeviceGetName(byte[] name, int len, int dev); - public int cuDeviceGet (IntByReference device, int ordinal); + public int cuDeviceGet(IntByReference device, int ordinal); - public int cuDeviceGetAttribute (IntByReference pi, int attrib, int dev ); + public int cuDeviceGetAttribute(IntByReference pi, int attrib, int dev); public int cuDeviceTotalMem_v2(LongByReference bytes, int dev); + public int cuDeviceTotalMem(LongByReference bytes, int dev); } diff --git a/src/com/sheepit/client/hardware/gpu/nvidia/CUDeviceAttribute.java b/src/com/sheepit/client/hardware/gpu/nvidia/CUDeviceAttribute.java index 2529e352..20256d5d 100644 --- a/src/com/sheepit/client/hardware/gpu/nvidia/CUDeviceAttribute.java +++ b/src/com/sheepit/client/hardware/gpu/nvidia/CUDeviceAttribute.java @@ -2,7 +2,7 @@ * Copyright (C) 2018 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -28,12 +28,12 @@ public class CUDeviceAttribute { * PCI bus ID of the device */ public static final int CU_DEVICE_ATTRIBUTE_PCI_BUS_ID = 33; - + /** * PCI device ID of the device */ public static final int CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID = 34; - + /** * PCI domain ID of the device */ diff --git a/src/com/sheepit/client/hardware/gpu/nvidia/CUresult.java b/src/com/sheepit/client/hardware/gpu/nvidia/CUresult.java index cca5d4c3..70c42b9c 100644 --- a/src/com/sheepit/client/hardware/gpu/nvidia/CUresult.java +++ b/src/com/sheepit/client/hardware/gpu/nvidia/CUresult.java @@ -66,18 +66,18 @@ public class CUresult { /** * This indicates profiling APIs are called while application is running - * in visual profiler mode. + * in visual profiler mode. */ public static final int CUDA_ERROR_PROFILER_DISABLED = 5; /** - * This indicates profiling has not been initialized for this context. - * Call cuProfilerInitialize() to resolve this. + * This indicates profiling has not been initialized for this context. + * Call cuProfilerInitialize() to resolve this. * - * @deprecated This error return is deprecated as of CUDA 5.0. - * It is no longer an error to attempt to enable/disable the - * profiling via ::cuProfilerStart or ::cuProfilerStop without - * initialization. + * @deprecated This error return is deprecated as of CUDA 5.0. + * It is no longer an error to attempt to enable/disable the + * profiling via ::cuProfilerStart or ::cuProfilerStop without + * initialization. */ public static final int CUDA_ERROR_PROFILER_NOT_INITIALIZED = 6; @@ -85,8 +85,8 @@ public class CUresult { * This indicates profiler has already been started and probably * cuProfilerStart() is incorrectly called. * - * @deprecated This error return is deprecated as of CUDA 5.0. - * It is no longer an error to call cuProfilerStart() when + * @deprecated This error return is deprecated as of CUDA 5.0. + * It is no longer an error to call cuProfilerStart() when * profiling is already enabled. */ public static final int CUDA_ERROR_PROFILER_ALREADY_STARTED = 7; @@ -95,8 +95,8 @@ public class CUresult { * This indicates profiler has already been stopped and probably * cuProfilerStop() is incorrectly called. * - * @deprecated This error return is deprecated as of CUDA 5.0. - * It is no longer an error to call cuProfilerStop() when + * @deprecated This error return is deprecated as of CUDA 5.0. + * It is no longer an error to call cuProfilerStop() when * profiling is already disabled. */ public static final int CUDA_ERROR_PROFILER_ALREADY_STOPPED = 8; @@ -203,7 +203,7 @@ public class CUresult { /** * This indicates that the ::CUcontext passed to the API call can - * only be bound to a single CPU thread at a time but is already + * only be bound to a single CPU thread at a time but is already * bound to a CPU thread. */ public static final int CUDA_ERROR_CONTEXT_ALREADY_IN_USE = 216; @@ -307,7 +307,7 @@ public class CUresult { /** * This error indicates that a call to ::cuMemPeerRegister is trying to * register memory from a context which has not had peer access - * enabled yet via ::cuCtxEnablePeerAccess(), or that + * enabled yet via ::cuCtxEnablePeerAccess(), or that * ::cuCtxDisablePeerAccess() is trying to disable peer access * which has not been enabled yet. */ @@ -347,15 +347,15 @@ public class CUresult { /** * A device-side assert triggered during kernel execution. The context - * cannot be used anymore, and must be destroyed. All existing device - * memory allocations from this context are invalid and must be + * cannot be used anymore, and must be destroyed. All existing device + * memory allocations from this context are invalid and must be * reconstructed if the program is to continue using CUDA. */ public static final int CUDA_ERROR_ASSERT = 710; /** * This error indicates that the hardware resources required to enable - * peer access have been exhausted for one or more of the devices + * peer access have been exhausted for one or more of the devices * passed to ::cuCtxEnablePeerAccess(). */ public static final int CUDA_ERROR_TOO_MANY_PEERS = 711; diff --git a/src/com/sheepit/client/hardware/gpu/nvidia/Nvidia.java b/src/com/sheepit/client/hardware/gpu/nvidia/Nvidia.java index 0d98b960..a146027b 100644 --- a/src/com/sheepit/client/hardware/gpu/nvidia/Nvidia.java +++ b/src/com/sheepit/client/hardware/gpu/nvidia/Nvidia.java @@ -1,11 +1,8 @@ package com.sheepit.client.hardware.gpu.nvidia; -import java.util.HashMap; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; -import java.util.Map; -import com.sheepit.client.hardware.gpu.nvidia.CUDeviceAttribute; import com.sheepit.client.hardware.gpu.GPUDevice; import com.sheepit.client.hardware.gpu.GPULister; import com.sheepit.client.os.OS; @@ -16,8 +13,7 @@ public class Nvidia implements GPULister { public static String TYPE = "CUDA"; - @Override - public List getGpus() { + @Override public List getGpus() { OS os = OS.getOS(); String path = os.getCUDALib(); if (path == null) { @@ -25,7 +21,7 @@ public List getGpus() { } CUDA cudalib = null; try { - cudalib = (CUDA) Native.loadLibrary(path, CUDA.class); + cudalib = (CUDA) Native.load(path, CUDA.class); } catch (java.lang.UnsatisfiedLinkError e) { return null; @@ -64,24 +60,37 @@ public List getGpus() { return null; } - List devices = new LinkedList(); + List devices = new ArrayList<>(count.getValue()); - HashMap devicesWithPciId = new HashMap(count.getValue()); for (int num = 0; num < count.getValue(); num++) { IntByReference aDevice = new IntByReference(); - result = cudalib.cuDeviceGet(aDevice, num); + result = cudalib.cuDeviceGet(aDevice, num); if (result != CUresult.CUDA_SUCCESS) { System.out.println("Nvidia::getGpus cuDeviceGet failed (ret: " + CUresult.stringFor(result) + ")"); continue; } + IntByReference pciDomainId = new IntByReference(); IntByReference pciBusId = new IntByReference(); - result = cudalib.cuDeviceGetAttribute(pciBusId, CUDeviceAttribute.CU_DEVICE_ATTRIBUTE_PCI_BUS_ID, aDevice.getValue()); + IntByReference pciDeviceId = new IntByReference(); + result = cudalib.cuDeviceGetAttribute(pciDomainId, CUDeviceAttribute.CU_DEVICE_ATTRIBUTE_PCI_DOMAIN_ID, aDevice.getValue()); + if (result != CUresult.CUDA_SUCCESS) { + System.out + .println("Nvidia::getGpus cuDeviceGetAttribute for CU_DEVICE_ATTRIBUTE_PCI_DOMAIN_ID failed (ret: " + CUresult.stringFor(result) + ")"); + continue; + } + result = cudalib.cuDeviceGetAttribute(pciBusId, CUDeviceAttribute.CU_DEVICE_ATTRIBUTE_PCI_BUS_ID, aDevice.getValue()); if (result != CUresult.CUDA_SUCCESS) { System.out.println("Nvidia::getGpus cuDeviceGetAttribute for CU_DEVICE_ATTRIBUTE_PCI_BUS_ID failed (ret: " + CUresult.stringFor(result) + ")"); continue; } + result = cudalib.cuDeviceGetAttribute(pciDeviceId, CUDeviceAttribute.CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID, aDevice.getValue()); + if (result != CUresult.CUDA_SUCCESS) { + System.out + .println("Nvidia::getGpus cuDeviceGetAttribute for CU_DEVICE_ATTRIBUTE_PCI_DEVICE_ID failed (ret: " + CUresult.stringFor(result) + ")"); + continue; + } byte name[] = new byte[256]; @@ -105,19 +114,23 @@ public List getGpus() { return null; } - devicesWithPciId.put(pciBusId.getValue(), new GPUDevice(TYPE, new String(name).trim(), ram.getValue(), "FAKE")); + String blenderId = String + .format("CUDA_%s_%04x:%02x:%02x", new String(name).trim(), pciDomainId.getValue(), pciBusId.getValue(), pciDeviceId.getValue()); + GPUDevice gpu = new GPUDevice(TYPE, new String(name).trim(), ram.getValue(), blenderId); + // for backward compatibility generate a CUDA_N id + gpu.setOldId(TYPE + "_" + num); + devices.add(gpu); } - // generate proper cuda id - // in theory a set to environment "CUDA_DEVICE_ORDER=PCI_BUS_ID" should be enough but it didn't work - int i = 0; - for (Map.Entry entry : devicesWithPciId.entrySet()){ - GPUDevice aDevice = entry.getValue(); - aDevice.setId(TYPE + "_" + Integer.toString(i)); - devices.add(aDevice); - i++; - } return devices; } + @Override public int getRecommendedRenderBucketSize(long memory) { + // Optimal CUDA-based GPUs Renderbucket algorithm + return (memory > 1073741824L) ? 256 : 128; + } + + @Override public int getMaximumRenderBucketSize(long memory) { + return (memory > 1073741824L) ? 512 : 128; + } } diff --git a/src/com/sheepit/client/hardware/gpu/opencl/OpenCL.java b/src/com/sheepit/client/hardware/gpu/opencl/OpenCL.java index 294a05a7..e9fd1490 100644 --- a/src/com/sheepit/client/hardware/gpu/opencl/OpenCL.java +++ b/src/com/sheepit/client/hardware/gpu/opencl/OpenCL.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -34,13 +34,12 @@ public class OpenCL implements GPULister { public static String TYPE = "OPENCL"; - @Override - public List getGpus() { + @Override public List getGpus() { OpenCLLib lib = null; String path = "OpenCL"; try { - lib = (OpenCLLib) Native.loadLibrary(path, OpenCLLib.class); + lib = (OpenCLLib) Native.load(path, OpenCLLib.class); } catch (java.lang.UnsatisfiedLinkError e) { System.out.println("OpenCL::getGpus failed(A) to load OpenCL lib (path: " + path + ")"); @@ -105,13 +104,16 @@ public List getGpus() { } for (int j = 0; j < device_count.getValue(); j++) { - String platform_vendor = getInfoPlatform(lib, plateforms[i], OpenCLLib.CL_PLATFORM_VENDOR); - if (platform_vendor != null && platform_vendor.toLowerCase().equals("advanced micro devices, inc.")) { // opencl is only used for amd gpus - String name = getInfodeviceString(lib, devices[j], OpenCLLib.CL_DEVICE_BOARD_NAME_AMD); - long vram = getInfodeviceLong(lib, devices[j], OpenCLLib.CL_DEVICE_GLOBAL_MEM_SIZE); - if (name != null && vram > 0) { - available_devices.add(new GPUDevice(TYPE, name, vram, TYPE + "_" + id)); + String name = getInfodeviceString(lib, devices[j], OpenCLLib.CL_DEVICE_BOARD_NAME_AMD); + String platform_name = getInfoPlatform(lib, plateforms[i], OpenCLLib.CL_PLATFORM_NAME); + long vram = getInfodeviceLong(lib, devices[j], OpenCLLib.CL_DEVICE_GLOBAL_MEM_SIZE); + if (name != null && vram > 0) { + if (name.equals("Radeon RX Vega")) { + name += " " + getInfodeviceLong(lib, devices[j], OpenCLLib.CL_DEVICE_MAX_COMPUTE_UNITS); } + GPUDevice gpu = new GPUDevice(TYPE, name, vram, getBlenderId(lib, devices[j], platform_name, name)); + gpu.setOldId(TYPE + "_" + id); + available_devices.add(gpu); } id++; } @@ -120,6 +122,15 @@ public List getGpus() { return available_devices; } + @Override public int getRecommendedRenderBucketSize(long memory) { + // Optimal CUDA-based GPUs Renderbucket algorithm + return (memory > 1073741824L) ? 256 : 128; + } + + @Override public int getMaximumRenderBucketSize(long memory) { + return (memory > 1073741824L) ? 2048 : 128; + } + private static String getInfodeviceString(OpenCLLib lib, CLDeviceId.ByReference device, int type) { byte name[] = new byte[256]; @@ -158,4 +169,16 @@ private static String getInfoPlatform(OpenCLLib lib, CLPlatformId.ByReference pl return new String(name).trim(); } + + private static String getBlenderId(OpenCLLib lib, CLDeviceId.ByReference device, String platform, String name) { + byte topology[] = new byte[24]; + + int status = lib.clGetDeviceInfo(device, lib.CL_DEVICE_TOPOLOGY_AMD, 24, topology, null); + if (status != OpenCLLib.CL_SUCCESS) { + System.out.println("OpenCL::getBlenderId failed(I) status: " + status); + return ""; + } + + return String.format("%s_%s_%s_%02x:%02x.%01x", TYPE, platform, name, topology[21], topology[22], topology[23]); + } } diff --git a/src/com/sheepit/client/hardware/gpu/opencl/OpenCLLib.java b/src/com/sheepit/client/hardware/gpu/opencl/OpenCLLib.java index f9c23cc0..5ef585ba 100644 --- a/src/com/sheepit/client/hardware/gpu/opencl/OpenCLLib.java +++ b/src/com/sheepit/client/hardware/gpu/opencl/OpenCLLib.java @@ -2,7 +2,7 @@ * Copyright (C) 2013-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -32,6 +32,7 @@ public interface OpenCLLib extends Library { public static final int CL_DEVICE_NOT_FOUND = -1; public static final int CL_PLATFORM_VENDOR = 0x0903; + public static final int CL_PLATFORM_NAME = 0x0902; // cl_device_type public static final int CL_DEVICE_TYPE_DEFAULT = (1 << 0); @@ -43,14 +44,19 @@ public interface OpenCLLib extends Library { // cl_device_info public static final int CL_DEVICE_NAME = 0x102B; + public static final int CL_DEVICE_VENDOR = 0x102C; + public static final int CL_DEVICE_VERSION = 0x102D; + public static final int CL_DEVICE_MAX_COMPUTE_UNITS = 0x1002; public static final int CL_DEVICE_GLOBAL_MEM_SIZE = 0x101F; public static final int CL_DEVICE_BOARD_NAME_AMD = 0x4038; + public static final int CL_DEVICE_TOPOLOGY_AMD = 0x4037; public int clGetPlatformIDs(int num_entries, CLPlatformId.ByReference[] platforms, IntByReference num_platforms); public int clGetPlatformInfo(CLPlatformId.ByReference platform, int param_name, long param_value_size, byte[] destination, long size_ret[]); - public int clGetDeviceIDs(CLPlatformId.ByReference platform, int param_name, int num_entries, CLDeviceId.ByReference[] devices, IntByReference device_count); + public int clGetDeviceIDs(CLPlatformId.ByReference platform, int param_name, int num_entries, CLDeviceId.ByReference[] devices, + IntByReference device_count); public int clGetDeviceInfo(CLDeviceId.ByReference device, int param_name, long param_value_size, byte[] destination, long size_ret[]); @@ -60,8 +66,7 @@ public static class ByReference extends CLPlatformId implements Structure.ByRefe public int id; - @Override - protected List getFieldOrder() { + @Override protected List getFieldOrder() { return Arrays.asList(new String[] { "id" }); } } @@ -72,8 +77,7 @@ public static class ByReference extends CLDeviceId implements Structure.ByRefere public int id; - @Override - protected List getFieldOrder() { + @Override protected List getFieldOrder() { return Arrays.asList(new String[] { "id" }); } } diff --git a/src/com/sheepit/client/network/Proxy.java b/src/com/sheepit/client/network/Proxy.java index 4a580dc2..ecf70125 100644 --- a/src/com/sheepit/client/network/Proxy.java +++ b/src/com/sheepit/client/network/Proxy.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -22,8 +22,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.Authenticator; -import com.sheepit.client.network.Proxy; -import com.sheepit.client.network.ProxyAuthenticator; public class Proxy { diff --git a/src/com/sheepit/client/network/ProxyAuthenticator.java b/src/com/sheepit/client/network/ProxyAuthenticator.java index 196cba40..ca2aca1e 100644 --- a/src/com/sheepit/client/network/ProxyAuthenticator.java +++ b/src/com/sheepit/client/network/ProxyAuthenticator.java @@ -2,7 +2,7 @@ * Copyright (C) 2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/os/FreeBSD.java b/src/com/sheepit/client/os/FreeBSD.java index 43f0f28b..ade4c3c3 100644 --- a/src/com/sheepit/client/os/FreeBSD.java +++ b/src/com/sheepit/client/os/FreeBSD.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.io.BufferedReader; +import java.io.InputStream; import java.io.InputStreamReader; import com.sheepit.client.Log; @@ -31,24 +32,21 @@ public class FreeBSD extends OS { private final String NICE_BINARY_PATH = "nice"; - private Boolean hasNiceBinary; + private final String ID_COMMAND_INVOCATION = "id -u"; public FreeBSD() { super(); - this.hasNiceBinary = null; } public String name() { return "freebsd"; } - @Override - public String getRenderBinaryPath() { + @Override public String getRenderBinaryPath() { return "rend.exe"; } - @Override - public CPU getCPU() { + @Override public CPU getCPU() { CPU ret = new CPU(); try { Runtime r = Runtime.getRuntime(); @@ -110,8 +108,7 @@ public CPU getCPU() { return ret; } - @Override - public int getMemory() { + @Override public long getMemory() { try { Runtime r = Runtime.getRuntime(); Process p = r.exec("sysctl -n hw.usermem"); @@ -124,7 +121,7 @@ public int getMemory() { return 0; } Long mem_byte = Long.parseLong(line.trim()); - return (int) (mem_byte / Long.valueOf(1024)); + return mem_byte / Long.valueOf(1024); } catch (IOException e) { Log.getInstance(null).debug("OS::FreeBSD::getMemory exception " + e); @@ -133,18 +130,15 @@ public int getMemory() { return 0; } - @Override - public int getFreeMemory() { + @Override public long getFreeMemory() { return -1; } - @Override - public String getCUDALib() { + @Override public String getCUDALib() { return "cuda"; } - @Override - public Process exec(List command, Map env_overight) throws IOException { + @Override public Process exec(List command, Map env_overight) throws IOException { // the renderer have a lib directory so add to the LD_LIBRARY_PATH // (even if we are not sure that it is the renderer who is launch @@ -161,10 +155,7 @@ public Process exec(List command, Map env_overight) thro } List actual_command = command; - if (this.hasNiceBinary == null) { - this.checkNiceAvailability(); - } - if (this.hasNiceBinary.booleanValue()) { + if (checkNiceAvailability()) { // launch the process in lowest priority if (env_overight != null) { actual_command.add(0, env_overight.get("PRIORITY")); @@ -189,17 +180,42 @@ public Process exec(List command, Map env_overight) thro return builder.start(); } - private void checkNiceAvailability() { + @Override public boolean getSupportHighPriority() { + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command("bash", "-c", ID_COMMAND_INVOCATION); + builder.redirectErrorStream(true); + + Process process = builder.start(); + InputStream is = process.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + + String userLevel = null; + if ((userLevel = reader.readLine()) != null) { + // Root user in *ix systems -independently of the alias used to login- has a id value of 0. On top of being a user with root capabilities, + // to support changing the priority the nice tool must be accessible from the current user + return (userLevel.equals("0")) & checkNiceAvailability(); + } + } + catch (IOException e) { + System.err.println(String.format("ERROR FreeBSD::getSupportHighPriority Unable to execute id command. IOException %s", e.getMessage())); + } + + return false; + } + + @Override public boolean checkNiceAvailability() { ProcessBuilder builder = new ProcessBuilder(); builder.command(NICE_BINARY_PATH); builder.redirectErrorStream(true); + Process process = null; + boolean hasNiceBinary = false; try { process = builder.start(); - this.hasNiceBinary = true; + hasNiceBinary = true; } catch (IOException e) { - this.hasNiceBinary = false; Log.getInstance(null).error("Failed to find low priority binary, will not launch renderer in normal priority (" + e + ")"); } finally { @@ -207,5 +223,6 @@ private void checkNiceAvailability() { process.destroy(); } } + return hasNiceBinary; } } diff --git a/src/com/sheepit/client/os/Linux.java b/src/com/sheepit/client/os/Linux.java index e9f8a5a6..6aacfb1a 100644 --- a/src/com/sheepit/client/os/Linux.java +++ b/src/com/sheepit/client/os/Linux.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -18,7 +18,10 @@ */ package com.sheepit.client.os; +import java.io.BufferedReader; import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -30,24 +33,21 @@ public class Linux extends OS { private final String NICE_BINARY_PATH = "nice"; - private Boolean hasNiceBinary; + private final String ID_COMMAND_INVOCATION = "id -u"; public Linux() { super(); - this.hasNiceBinary = null; } public String name() { return "linux"; } - @Override - public String getRenderBinaryPath() { + @Override public String getRenderBinaryPath() { return "rend.exe"; } - @Override - public CPU getCPU() { + @Override public CPU getCPU() { CPU ret = new CPU(); try { String filePath = "/proc/cpuinfo"; @@ -87,8 +87,7 @@ public CPU getCPU() { return ret; } - @Override - public int getMemory() { + @Override public long getMemory() { try { String filePath = "/proc/meminfo"; Scanner scanner = new Scanner(new File(filePath)); @@ -116,8 +115,7 @@ public int getMemory() { return 0; } - @Override - public int getFreeMemory() { + @Override public long getFreeMemory() { try { String filePath = "/proc/meminfo"; Scanner scanner = new Scanner(new File(filePath)); @@ -136,7 +134,8 @@ public int getFreeMemory() { scanner.close(); } catch (java.lang.NoClassDefFoundError e) { - System.err.println("OS::Linux::getFreeMemory error " + e + " mostly because Scanner class was introducted by Java 5 and you are running a lower version"); + System.err.println( + "OS::Linux::getFreeMemory error " + e + " mostly because Scanner class was introducted by Java 5 and you are running a lower version"); } catch (Exception e) { e.printStackTrace(); @@ -145,33 +144,31 @@ public int getFreeMemory() { return 0; } - @Override - public String getCUDALib() { + @Override public String getCUDALib() { return "cuda"; } - @Override - public Process exec(List command, Map env_overight) throws IOException { - // the renderer have a lib directory so add to the LD_LIBRARY_PATH - // (even if we are not sure that it is the renderer who is launch - + @Override public Process exec(List command, Map env_overight) throws IOException { Map new_env = new HashMap(); new_env.putAll(java.lang.System.getenv()); // clone the env - Boolean has_ld_library_path = new_env.containsKey("LD_LIBRARY_PATH"); - String lib_dir = (new File(command.get(0))).getParent() + File.separator + "lib"; - if (has_ld_library_path == false) { - new_env.put("LD_LIBRARY_PATH", lib_dir); - } - else { - new_env.put("LD_LIBRARY_PATH", new_env.get("LD_LIBRARY_PATH") + ":" + lib_dir); + // if Blender is already loading an OpenGL library, don't need to load Blender's default one (it will + // create system incompatibilities). If no OpenGL library is found, then load the one included in the binary + // zip file + if (isOpenGLAlreadyInstalled(command.get(0)) == false) { + Boolean has_ld_library_path = new_env.containsKey("LD_LIBRARY_PATH"); + + String lib_dir = (new File(command.get(0))).getParent() + File.separator + "lib"; + if (has_ld_library_path == false) { + new_env.put("LD_LIBRARY_PATH", lib_dir); + } + else { + new_env.put("LD_LIBRARY_PATH", new_env.get("LD_LIBRARY_PATH") + ":" + lib_dir); + } } List actual_command = command; - if (this.hasNiceBinary == null) { - this.checkNiceAvailability(); - } - if (this.hasNiceBinary.booleanValue()) { + if (checkNiceAvailability()) { // launch the process in lowest priority if (env_overight != null) { actual_command.add(0, env_overight.get("PRIORITY")); @@ -196,30 +193,42 @@ public Process exec(List command, Map env_overight) thro return builder.start(); } - @Override - public boolean getSupportHighPriority() { - // only the root user can create process with high (negative nice) value - String logname = System.getenv("LOGNAME"); - String user = System.getenv("USER"); - - if ((logname != null && logname.equals("root")) || (user != null && user.equals("root"))) { - return true; + @Override public boolean getSupportHighPriority() { + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command("bash", "-c", ID_COMMAND_INVOCATION); + builder.redirectErrorStream(true); + + Process process = builder.start(); + InputStream is = process.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + + String userLevel = null; + if ((userLevel = reader.readLine()) != null) { + // Root user in *ix systems -independently of the alias used to login- has a id value of 0. On top of being a user with root capabilities, + // to support changing the priority the nice tool must be accessible from the current user + return (userLevel.equals("0")) & checkNiceAvailability(); + } + } + catch (IOException e) { + System.err.println(String.format("ERROR Linux::getSupportHighPriority Unable to execute id command. IOException %s", e.getMessage())); } return false; } - protected void checkNiceAvailability() { + @Override public boolean checkNiceAvailability() { ProcessBuilder builder = new ProcessBuilder(); builder.command(NICE_BINARY_PATH); builder.redirectErrorStream(true); + Process process = null; + boolean hasNiceBinary = false; try { process = builder.start(); - this.hasNiceBinary = true; + hasNiceBinary = true; } catch (IOException e) { - this.hasNiceBinary = false; Log.getInstance(null).error("Failed to find low priority binary, will not launch renderer in normal priority (" + e + ")"); } finally { @@ -227,5 +236,47 @@ protected void checkNiceAvailability() { process.destroy(); } } + return hasNiceBinary; + } + + protected boolean isOpenGLAlreadyInstalled(String pathToRendEXE) { + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command("bash", "-c", "ldd '" + pathToRendEXE + "'"); // support for paths with an space + processBuilder.redirectErrorStream(true); + + try { + Process process = processBuilder.start(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + String line; + StringBuilder screenOutput = new StringBuilder(); + while ((line = reader.readLine()) != null) { + // check the shared libraries that Blender is loading at run time. If it already loads an existing + // version of OpenGL (ie the one shipped with NVIDIA drivers) then return false to avoid the client + // replacing them (and glitching the EEVEE render). Otherwise return true and load the /lib folder + // to ensure that Blender works correctly + if (line.toLowerCase().contains("libgl.so")) { + return !line.toLowerCase().contains("not found"); + } + + // In case of error we can later check the screen output from ldd + screenOutput.append(line); + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + System.err.println(String.format("ERROR Linux::isOpenGLAlreadyInstalled Unable to execute ldd command. Exit code %d", exitCode)); + System.err.println(String.format("Screen output from ldd execution: %s", screenOutput.toString())); + } + } + catch (IOException e) { + System.err.println(String.format("ERROR Linux::isOpenGLAreadyInstalled Unable to execute ldd command. IOException %s", e.getMessage())); + } + catch (InterruptedException e) { + System.err.println(String.format("ERROR Linux::isOpenGLAreadyInstalled Unable to execute ldd command. InterruptedException %s", e.getMessage())); + } + + return false; } } diff --git a/src/com/sheepit/client/os/Mac.java b/src/com/sheepit/client/os/Mac.java index 0d1bd340..fdcdd41a 100644 --- a/src/com/sheepit/client/os/Mac.java +++ b/src/com/sheepit/client/os/Mac.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -21,6 +21,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; import java.util.Map; @@ -30,24 +31,21 @@ public class Mac extends OS { private final String NICE_BINARY_PATH = "nice"; - private Boolean hasNiceBinary; + private final String ID_COMMAND_INVOCATION = "id -u"; public Mac() { super(); - this.hasNiceBinary = null; } public String name() { return "mac"; } - @Override - public String getRenderBinaryPath() { + @Override public String getRenderBinaryPath() { return "Blender" + File.separator + "blender.app" + File.separator + "Contents" + File.separator + "MacOS" + File.separator + "blender"; } - @Override - public CPU getCPU() { + @Override public CPU getCPU() { CPU ret = new CPU(); String command = "sysctl machdep.cpu.family machdep.cpu.brand_string"; @@ -97,8 +95,7 @@ public CPU getCPU() { return ret; } - @Override - public int getMemory() { + @Override public long getMemory() { String command = "sysctl hw.memsize"; Process p = null; @@ -113,7 +110,7 @@ public int getMemory() { if (line.startsWith(option)) { String memory = line.substring(option.length()).trim(); // memory in bytes - return (int) (Long.parseLong(memory) / 1024); + return Long.parseLong(memory) / 1024; } } input.close(); @@ -140,18 +137,13 @@ public int getMemory() { return -1; } - @Override - public int getFreeMemory() { + @Override public long getFreeMemory() { return -1; } - @Override - public Process exec(List command, Map env) throws IOException { + @Override public Process exec(List command, Map env) throws IOException { List actual_command = command; - if (this.hasNiceBinary == null) { - this.checkNiceAvailability(); - } - if (this.hasNiceBinary.booleanValue()) { + if (checkNiceAvailability()) { // launch the process in lowest priority if (env != null) { actual_command.add(0, env.get("PRIORITY")); @@ -173,22 +165,46 @@ public Process exec(List command, Map env) throws IOExce return builder.start(); } - @Override - public String getCUDALib() { + @Override public String getCUDALib() { return "/usr/local/cuda/lib/libcuda.dylib"; } - protected void checkNiceAvailability() { + @Override public boolean getSupportHighPriority() { + try { + ProcessBuilder builder = new ProcessBuilder(); + builder.command("bash", "-c", ID_COMMAND_INVOCATION); + builder.redirectErrorStream(true); + + Process process = builder.start(); + InputStream is = process.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + + String userLevel = null; + if ((userLevel = reader.readLine()) != null) { + // Root user in *ix systems -independently of the alias used to login- has a id value of 0. On top of being a user with root capabilities, + // to support changing the priority the nice tool must be accessible from the current user + return (userLevel.equals("0")) & checkNiceAvailability(); + } + } + catch (IOException e) { + System.err.println(String.format("ERROR Mac::getSupportHighPriority Unable to execute id command. IOException %s", e.getMessage())); + } + + return false; + } + + @Override public boolean checkNiceAvailability() { ProcessBuilder builder = new ProcessBuilder(); builder.command(NICE_BINARY_PATH); builder.redirectErrorStream(true); + Process process = null; + boolean hasNiceBinary = false; try { process = builder.start(); - this.hasNiceBinary = true; + hasNiceBinary = true; } catch (IOException e) { - this.hasNiceBinary = false; Log.getInstance(null).error("Failed to find low priority binary, will not launch renderer in normal priority (" + e + ")"); } finally { @@ -196,5 +212,6 @@ protected void checkNiceAvailability() { process.destroy(); } } + return hasNiceBinary; } } diff --git a/src/com/sheepit/client/os/OS.java b/src/com/sheepit/client/os/OS.java index 5430398d..04a337f2 100644 --- a/src/com/sheepit/client/os/OS.java +++ b/src/com/sheepit/client/os/OS.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -31,9 +31,9 @@ public abstract class OS { public abstract CPU getCPU(); - public abstract int getMemory(); + public abstract long getMemory(); - public abstract int getFreeMemory(); + public abstract long getFreeMemory(); public abstract String getRenderBinaryPath(); @@ -41,9 +41,9 @@ public String getCUDALib() { return null; } - public boolean getSupportHighPriority() { - return true; - } + public abstract boolean getSupportHighPriority(); + + public abstract boolean checkNiceAvailability(); public Process exec(List command, Map env) throws IOException { ProcessBuilder builder = new ProcessBuilder(command); diff --git a/src/com/sheepit/client/os/Windows.java b/src/com/sheepit/client/os/Windows.java index 905f0383..bc564ef8 100644 --- a/src/com/sheepit/client/os/Windows.java +++ b/src/com/sheepit/client/os/Windows.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -37,13 +37,11 @@ public String name() { return "windows"; } - @Override - public String getRenderBinaryPath() { + @Override public String getRenderBinaryPath() { return "rend.exe"; } - @Override - public CPU getCPU() { + @Override public CPU getCPU() { CPU ret = new CPU(); try { String[] identifier = java.lang.System.getenv("PROCESSOR_IDENTIFIER").split(" "); @@ -88,12 +86,11 @@ public CPU getCPU() { return ret; } - @Override - public int getMemory() { + @Override public long getMemory() { try { MEMORYSTATUSEX _memory = new MEMORYSTATUSEX(); if (Kernel32.INSTANCE.GlobalMemoryStatusEx(_memory)) { - return (int) (_memory.ullTotalPhys.longValue() / 1024); // size in KB + return _memory.ullTotalPhys.longValue() / 1024; // size in KB } } catch (Exception e) { @@ -102,32 +99,29 @@ public int getMemory() { return 0; } - @Override - public int getFreeMemory() { + @Override public long getFreeMemory() { try { MEMORYSTATUSEX _memory = new MEMORYSTATUSEX(); if (Kernel32.INSTANCE.GlobalMemoryStatusEx(_memory)) { - return (int) (_memory.ullAvailPhys.longValue() / 1024); // size in KB + return _memory.ullAvailPhys.longValue() / 1024; // size in KB } } catch (Exception e) { e.printStackTrace(); - } - + } + return -1; } - @Override - public String getCUDALib() { + @Override public String getCUDALib() { return "nvcuda"; } - @Override - public Process exec(List command, Map env) throws IOException { + @Override public Process exec(List command, Map env) throws IOException { // disable a popup because the renderer might crash (seg fault) Kernel32Lib kernel32lib = null; try { - kernel32lib = (Kernel32Lib) Native.loadLibrary(Kernel32Lib.path, Kernel32Lib.class); + kernel32lib = (Kernel32Lib) Native.load(Kernel32Lib.path, Kernel32Lib.class); kernel32lib.SetErrorMode(Kernel32Lib.SEM_NOGPFAULTERRORBOX); } catch (java.lang.UnsatisfiedLinkError e) { @@ -216,8 +210,7 @@ int getPriorityClass(int priority) { return process_class; } - @Override - public boolean kill(Process process) { + @Override public boolean kill(Process process) { if (process != null) { WinProcess wproc = new WinProcess(process); wproc.kill(); @@ -225,4 +218,13 @@ public boolean kill(Process process) { } return false; } + + @Override public boolean getSupportHighPriority() { + return true; + } + + @Override public boolean checkNiceAvailability() { + // In windows, nice is not required and therefore we return always true to show the slider in the Settings GUI + return true; + } } diff --git a/src/com/sheepit/client/os/windows/Kernel32Lib.java b/src/com/sheepit/client/os/windows/Kernel32Lib.java index 51a30d4a..78805c9b 100644 --- a/src/com/sheepit/client/os/windows/Kernel32Lib.java +++ b/src/com/sheepit/client/os/windows/Kernel32Lib.java @@ -1,6 +1,6 @@ /* This file was originally taken from JNA project (https://github.com/twall/jna) * filename: contrib/platform/src/com/sun/jna/platform/win32/Tlhelp32.java - * + * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either @@ -61,7 +61,8 @@ public interface Kernel32Lib extends Library { /** * Includes all processes and threads in the system, plus the heaps and modules of the process specified in th32ProcessID. */ - WinDef.DWORD TH32CS_SNAPALL = new WinDef.DWORD((TH32CS_SNAPHEAPLIST.intValue() | TH32CS_SNAPPROCESS.intValue() | TH32CS_SNAPTHREAD.intValue() | TH32CS_SNAPMODULE.intValue())); + WinDef.DWORD TH32CS_SNAPALL = new WinDef.DWORD( + (TH32CS_SNAPHEAPLIST.intValue() | TH32CS_SNAPPROCESS.intValue() | TH32CS_SNAPTHREAD.intValue() | TH32CS_SNAPMODULE.intValue())); /** * Indicates that the snapshot handle is to be inheritable. @@ -151,9 +152,10 @@ public PROCESSENTRY32(Pointer memory) { */ public char[] szExeFile = new char[WinDef.MAX_PATH]; - @Override - protected List getFieldOrder() { - return Arrays.asList(new String[] { "dwSize", "cntUsage", "th32ProcessID", "th32DefaultHeapID", "th32ModuleID", "cntThreads", "th32ParentProcessID", "pcPriClassBase", "dwFlags", "szExeFile" }); + @Override protected List getFieldOrder() { + return Arrays + .asList("dwSize", "cntUsage", "th32ProcessID", "th32DefaultHeapID", "th32ModuleID", "cntThreads", "th32ParentProcessID", "pcPriClassBase", + "dwFlags", "szExeFile"); } } diff --git a/src/com/sheepit/client/os/windows/WinProcess.java b/src/com/sheepit/client/os/windows/WinProcess.java index 78ed2a8e..36c61230 100644 --- a/src/com/sheepit/client/os/windows/WinProcess.java +++ b/src/com/sheepit/client/os/windows/WinProcess.java @@ -2,7 +2,7 @@ * Copyright (C) 2013 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -49,7 +49,7 @@ public WinProcess() { this.pid = -1; this.kernel32lib = null; try { - this.kernel32lib = (Kernel32Lib) Native.loadLibrary(Kernel32Lib.path, Kernel32Lib.class); + this.kernel32lib = (Kernel32Lib) Native.load(Kernel32Lib.path, Kernel32Lib.class); } catch (java.lang.UnsatisfiedLinkError e) { System.out.println("WinProcess::construct " + e); @@ -64,7 +64,7 @@ public WinProcess() { private static boolean processHasGetPid() { try { - if (Process.class.getMethod("pid", null) != null) { + if (Process.class.getMethod("pid") != null) { return true; } } @@ -75,7 +75,7 @@ private static boolean processHasGetPid() { private static long getPid(Process process) { try { - return (long) Process.class.getMethod("pid", null).invoke(process, null); + return (long) Process.class.getMethod("pid").invoke(process); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { } @@ -84,13 +84,14 @@ private static long getPid(Process process) { private static WinNT.HANDLE getHandleByPid(int pid_) throws IOException { WinNT.HANDLE handle = Kernel32.INSTANCE.OpenProcess(0x0400 | // PROCESS_QUERY_INFORMATION - 0x0800 | // PROCESS_SUSPEND_RESUME - 0x0001 | // PROCESS_TERMINATE - 0x0200 | // PROCESS_SET_INFORMATION - 0x00100000, // SYNCHRONIZE + 0x0800 | // PROCESS_SUSPEND_RESUME + 0x0001 | // PROCESS_TERMINATE + 0x0200 | // PROCESS_SET_INFORMATION + 0x00100000, // SYNCHRONIZE false, pid_); if (handle == null) { - throw new IOException("OpenProcess failed: " + Kernel32Util.formatMessageFromLastErrorCode(Kernel32.INSTANCE.GetLastError()) + " (pid: " + pid_ + ")"); + throw new IOException( + "OpenProcess failed: " + Kernel32Util.formatMessageFromLastErrorCode(Kernel32.INSTANCE.GetLastError()) + " (pid: " + pid_ + ")"); } return handle; } @@ -130,8 +131,7 @@ public WinProcess(int pid_) throws IOException { this.pid = pid_; } - @Override - protected void finalize() throws Throwable { + @Override protected void finalize() throws Throwable { if (this.handle != null) { // Kernel32.INSTANCE.CloseHandle(this.handle); // do not close the handle because the parent Process object might still be alive this.handle = null; diff --git a/src/com/sheepit/client/standalone/GuiSwing.java b/src/com/sheepit/client/standalone/GuiSwing.java index 6fdaaaf8..b4e15ce3 100644 --- a/src/com/sheepit/client/standalone/GuiSwing.java +++ b/src/com/sheepit/client/standalone/GuiSwing.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -25,29 +25,39 @@ import java.awt.MenuItem; import java.awt.PopupMenu; import java.awt.SystemTray; -import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowEvent; import java.awt.event.WindowStateListener; +import java.awt.image.BufferedImage; import java.net.URL; import java.util.Timer; import java.util.TimerTask; +import java.io.IOException; + import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.border.EmptyBorder; +import javax.imageio.ImageIO; import com.sheepit.client.Client; import com.sheepit.client.Configuration; import com.sheepit.client.Gui; +import com.sheepit.client.SettingsLoader; import com.sheepit.client.Stats; import com.sheepit.client.standalone.swing.activity.Settings; import com.sheepit.client.standalone.swing.activity.Working; +import lombok.Getter; +import lombok.Setter; + +import com.formdev.flatlaf.FlatLightLaf; // Required for dark & light mode +import com.formdev.flatlaf.FlatDarkLaf; +import com.formdev.flatlaf.FlatLaf; public class GuiSwing extends JFrame implements Gui { public static final String type = "swing"; @@ -69,6 +79,11 @@ public enum ActivityType { private boolean waitingForAuthentication; private Client client; + private BufferedImage iconSprites; + private BufferedImage[] trayIconSprites; + + @Getter @Setter private SettingsLoader settingsLoader; + private ThreadClient threadClient; public GuiSwing(boolean useSysTray_, String title_) { @@ -78,8 +93,7 @@ public GuiSwing(boolean useSysTray_, String title_) { waitingForAuthentication = true; new Timer().scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { + @Override public void run() { if (activityWorking != null) { activityWorking.updateTime(); } @@ -87,15 +101,7 @@ public void run() { }, 2 * 1000, 2 * 1000); } - @Override - public void start() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } - catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e1) { - e1.printStackTrace(); - } - + @Override public void start() { if (useSysTray) { try { sysTray = SystemTray.getSystemTray(); @@ -114,14 +120,23 @@ public void windowStateChanged(WindowEvent e) { } } - URL iconUrl = getClass().getResource("/icon.png"); - if (iconUrl != null) { - ImageIcon img = new ImageIcon(iconUrl); - setIconImage(img.getImage()); + // load the images sprite and split into individual images + URL spriteSequenceUrl = getClass().getResource("/icon-sprites.png"); + + if (spriteSequenceUrl != null) { + try { + iconSprites = ImageIO.read(spriteSequenceUrl); + trayIconSprites = new BufferedImage[101 * 1]; // sprite sheet has 101 images in 1 column + + setIconImage(extractImageFromSprite(-1)); // sprite 0 is standard Sheep It! icon + } + catch (IOException e) { + e.printStackTrace(); + } } setTitle(title); - setSize(520, 680); + setSize(520, 760); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -135,6 +150,21 @@ public void windowStateChanged(WindowEvent e) { this.showActivity(ActivityType.SETTINGS); + try { + if (client.getConfiguration().getTheme().equals("light")) { + UIManager.setLookAndFeel(new FlatLightLaf()); + } + else if (client.getConfiguration().getTheme().equals("dark")) { + UIManager.setLookAndFeel(new FlatDarkLaf()); + } + + // Apply the selected theme to swing components + FlatLaf.updateUI(); + } + catch (UnsupportedLookAndFeelException e1) { + e1.printStackTrace(); + } + while (waitingForAuthentication) { try { synchronized (this) { @@ -147,46 +177,53 @@ public void windowStateChanged(WindowEvent e) { } } - @Override - public void stop() { + @Override public void stop() { System.exit(0); } - @Override - public void status(String msg_) { + @Override public void status(String msg_) { + status(msg_, false); + } + + @Override public void status(String msg_, boolean overwriteSuspendedMsg) { if (activityWorking != null) { - this.activityWorking.setStatus(msg_); + this.activityWorking.setStatus(msg_, overwriteSuspendedMsg); } } - @Override - public void setRenderingProjectName(String name_) { + @Override public void status(String msg, int progress) { + if (activityWorking != null) { + this.activityWorking.setStatus(String.format("%s %d%%", msg, progress)); + } + } + + @Override public void status(String msg, int progress, long size) { + this.status(msg, progress); + } + + @Override public void setRenderingProjectName(String name_) { if (activityWorking != null) { this.activityWorking.setRenderingProjectName(name_); } } - @Override - public void error(String msg_) { - status(msg_); + @Override public void error(String msg_) { + status(msg_, true); } - @Override - public void setRemainingTime(String time_) { + @Override public void setRemainingTime(String time_) { if (activityWorking != null) { this.activityWorking.setRemainingTime(time_); } } - @Override - public void setRenderingTime(String time_) { + @Override public void setRenderingTime(String time_) { if (activityWorking != null) { this.activityWorking.setRenderingTime(time_); } } - @Override - public void AddFrameRendered() { + @Override public void AddFrameRendered() { framesRendered++; if (activityWorking != null) { @@ -197,25 +234,27 @@ public void AddFrameRendered() { } } - @Override - public void displayStats(Stats stats) { + @Override public void displayStats(Stats stats) { if (activityWorking != null) { this.activityWorking.displayStats(stats); } } - @Override - public Client getClient() { + @Override public void displayUploadQueueStats(int queueSize, long queueVolume) { + if (activityWorking != null) { + this.activityWorking.displayUploadQueueStats(queueSize, queueVolume); + } + } + + @Override public Client getClient() { return client; } - @Override - public void setClient(Client cli) { + @Override public void setClient(Client cli) { client = cli; } - @Override - public void setComputeMethod(String computeMethod) { + @Override public void setComputeMethod(String computeMethod) { this.activityWorking.setComputeMethod(computeMethod); } @@ -223,6 +262,15 @@ public Configuration getConfiguration() { return client.getConfiguration(); } + @Override public void successfulAuthenticationEvent(String publickey) { + if (settingsLoader != null) { + if (publickey != null) { + settingsLoader.setPassword(publickey); + } + settingsLoader.saveFile(); + } + } + public void setCredentials(String contentLogin, String contentPassword) { client.getConfiguration().setLogin(contentLogin); client.getConfiguration().setPassword(contentPassword); @@ -287,14 +335,13 @@ public void restoreFromTray() { public TrayIcon getTrayIcon() { final PopupMenu trayMenu = new PopupMenu(); - URL iconUrl = getClass().getResource("/icon.png"); - Image img = Toolkit.getDefaultToolkit().getImage(iconUrl); + // on start, show the base icon + Image img = extractImageFromSprite(-1); final TrayIcon icon = new TrayIcon(img); MenuItem exit = new MenuItem("Exit"); exit.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { System.exit(0); } }); @@ -302,8 +349,7 @@ public void actionPerformed(ActionEvent e) { MenuItem open = new MenuItem("Open..."); open.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { restoreFromTray(); } }); @@ -311,8 +357,7 @@ public void actionPerformed(ActionEvent e) { MenuItem settings = new MenuItem("Settings..."); settings.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { restoreFromTray(); showActivity(ActivityType.SETTINGS); } @@ -324,8 +369,7 @@ public void actionPerformed(ActionEvent e) { icon.setToolTip("SheepIt! Client"); icon.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { restoreFromTray(); } }); @@ -334,9 +378,36 @@ public void actionPerformed(ActionEvent e) { } + private Image extractImageFromSprite(int spriteNumber) { + // Sprite structure + // Image 0: base sprite + // Images 1-101: progress bar percentage from 0 to 100 + // + // Always add +1 to the icon requested. + // -1 turns into 0 (base sprite with no progress bar) + // 0 to 101 turns into 1 to 101 (progress sequence starts in sprite 1 and ends on sprite 101) + ImageIcon img = new ImageIcon(iconSprites.getSubimage(0, (spriteNumber + 1) * 114, 114, 114)); + + return img.getImage(); + } + + @Override public void updateTrayIcon(Integer percentage) { + // update the app icon on the app bar + Image img = extractImageFromSprite(percentage); + setIconImage(img); + + // if the app supports the system tray, update as well + if (sysTray != null && SystemTray.isSupported()) { + if (trayIcon != null) { + trayIcon.setImage(img); + trayIcon.setImageAutoSize(true); // use this method to ensure that icon is refreshed when on + // the tray + } + } + } + public class ThreadClient extends Thread { - @Override - public void run() { + @Override public void run() { if (GuiSwing.this.client != null) { GuiSwing.this.client.run(); } diff --git a/src/com/sheepit/client/standalone/GuiText.java b/src/com/sheepit/client/standalone/GuiText.java index ad116e17..d65a8c8c 100644 --- a/src/com/sheepit/client/standalone/GuiText.java +++ b/src/com/sheepit/client/standalone/GuiText.java @@ -2,7 +2,7 @@ * Copyright (C) 2010-2014 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -21,7 +21,6 @@ import com.sheepit.client.Client; import com.sheepit.client.Gui; -import com.sheepit.client.Job; import com.sheepit.client.Log; import com.sheepit.client.Stats; import com.sheepit.client.standalone.text.CLIInputActionHandler; @@ -30,24 +29,31 @@ import sun.misc.Signal; import sun.misc.SignalHandler; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; + public class GuiText implements Gui { public static final String type = "text"; private int framesRendered; private int sigIntCount = 0; - private Log log; + private DateFormat df; + private String eta; private Client client; public GuiText() { this.framesRendered = 0; this.log = Log.getInstance(null); + this.df = new SimpleDateFormat("MMM dd HH:mm:ss"); + this.eta = ""; } - @Override - public void start() { + @Override public void start() { if (client != null) { CLIInputObserver cli_input_observer = new CLIInputObserver(client); @@ -56,8 +62,7 @@ public void start() { cli_input_observer_thread.start(); Signal.handle(new Signal("INT"), new SignalHandler() { - @Override - public void handle(Signal signal) { + @Override public void handle(Signal signal) { sigIntCount++; if (sigIntCount == 4) { @@ -84,65 +89,123 @@ else if (client.isRunning() && client.isSuspended() == false) { } } - @Override - public void stop() { + @Override public void stop() { Runtime.getRuntime().halt(0); } - @Override - public void status(String msg_) { - System.out.println(msg_); + @Override public void updateTrayIcon(Integer percentage) { + } + + @Override public void status(String msg_) { + status(msg_, false); + } + + @Override public void status(String msg_, boolean overwriteSuspendedMsg) { log.debug("GUI " + msg_); + + if (client != null && client.isSuspended()) { + if (overwriteSuspendedMsg) { + System.out.println(String.format("%s %s", this.df.format(new Date()), msg_)); + } + } + else { + System.out.println(String.format("%s %s", this.df.format(new Date()), msg_)); + } + } + + @Override public void status(String msg, int progress) { + this.status(msg, progress, 0); + } + + @Override public void status(String msg, int progress, long size) { + System.out.print("\r"); + System.out.print(String.format("%s %s", this.df.format(new Date()), showProgress(msg, progress, size))); } - @Override - public void error(String err_) { - System.out.println("Error " + err_); + @Override public void error(String err_) { + System.out.println(String.format("ERROR: %s %s", this.df.format(new Date()), err_)); log.error("Error " + err_); } - @Override - public void AddFrameRendered() { + @Override public void AddFrameRendered() { this.framesRendered += 1; - System.out.println("Frames rendered: " + this.framesRendered); + System.out.println(String.format("%s Frames rendered: %d", this.df.format(new Date()), this.framesRendered)); } - @Override - public void displayStats(Stats stats) { - System.out.println("Frames remaining: " + stats.getRemainingFrame()); - System.out.println("Credits earned: " + stats.getCreditsEarnedDuringSession()); + @Override public void displayStats(Stats stats) { + System.out.println(String.format("%s Frames remaining: %d", this.df.format(new Date()), stats.getRemainingFrame())); + System.out.println(String.format("%s Credits earned: %d", this.df.format(new Date()), stats.getCreditsEarnedDuringSession())); } - @Override - public void setRenderingProjectName(String name_) { + @Override public void displayUploadQueueStats(int queueSize, long queueVolume) { + // No need to check if the queue is not empty to show the volume bc this line is always shown at the end + // of the render process in text GUI (unless an error occurred, where the file is uploaded synchronously) + System.out.println(String.format("%s Queued uploads: %d (%.2fMB)", this.df.format(new Date()), queueSize, (queueVolume / 1024.0 / 1024.0))); + } + + @Override public void setRenderingProjectName(String name_) { if (name_ != null && name_.isEmpty() == false) { - System.out.println("Rendering project \"" + name_ + "\""); + System.out.println(String.format("%s Rendering project \"%s\"", this.df.format(new Date()), name_)); } } - @Override - public void setRemainingTime(String time_) { - System.out.println("Rendering (remaining " + time_ + ")"); + @Override public void setRemainingTime(String time_) { + this.eta = time_; } - @Override - public void setRenderingTime(String time_) { - System.out.println("Rendering " + time_); + @Override public void setRenderingTime(String time_) { + System.out.println(String.format("%s Rendering %s", this.df.format(new Date()), time_)); } - @Override - public void setClient(Client cli) { + @Override public void setClient(Client cli) { client = cli; } - @Override - public void setComputeMethod(String computeMethod) { - System.out.println("Compute method: " + computeMethod); + @Override public void setComputeMethod(String computeMethod) { + System.out.println(String.format("%s Compute method: %s", this.df.format(new Date()), computeMethod)); } - @Override - public Client getClient() { + @Override public Client getClient() { return client; } + @Override public void successfulAuthenticationEvent(String publickey) { + + } + + private String showProgress(String message, int progress, long size) { + StringBuilder progressBar = new StringBuilder(140); + + if (progress < 100) { + progressBar + .append(message) + .append(" ") + .append(String.join("", Collections.nCopies(progress == 0 ? 2 : 2 - (int) (Math.log10(progress)), " "))) + .append(String.format("%d%% [", progress)) + .append(String.join("", Collections.nCopies((int)(progress/5), "="))) + .append('>') + .append(String.join("", Collections.nCopies(20 - (int)(progress / 5), " "))) + .append(']'); + + if (size > 0) { + progressBar.append(String.format(" %dMB", (size / 1024 / 1024))); + } + + if (!this.eta.equals("")) { + progressBar.append(String.format(" ETA %s", this.eta)); + } + + progressBar.append(String.join("", Collections.nCopies(60 - progressBar.length(), " "))); + } + // If progress has reached 100% + else { + progressBar + .append(message) + .append(" done") + .append(String.join("", Collections.nCopies(60 - progressBar.length(), " "))) + .append("\n"); + } + + return progressBar.toString(); + } } diff --git a/src/com/sheepit/client/standalone/GuiTextOneLine.java b/src/com/sheepit/client/standalone/GuiTextOneLine.java index 044d5d82..0c6e8d2d 100644 --- a/src/com/sheepit/client/standalone/GuiTextOneLine.java +++ b/src/com/sheepit/client/standalone/GuiTextOneLine.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -21,7 +21,6 @@ import com.sheepit.client.Client; import com.sheepit.client.Gui; -import com.sheepit.client.Job; import com.sheepit.client.Stats; import com.sheepit.client.standalone.text.CLIInputActionHandler; import com.sheepit.client.standalone.text.CLIInputObserver; @@ -29,6 +28,11 @@ import sun.misc.Signal; import sun.misc.SignalHandler; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; + public class GuiTextOneLine implements Gui { public static final String type = "oneLine"; @@ -37,10 +41,15 @@ public class GuiTextOneLine implements Gui { private int remaining; private String creditsEarned; private int sigIntCount = 0; + private DateFormat df; private String computeMethod; private String status; private String line; + private String eta; + + private int uploadQueueSize; + private long uploadQueueVolume; private boolean exiting = false; @@ -54,10 +63,13 @@ public GuiTextOneLine() { status = ""; computeMethod = ""; line = ""; + uploadQueueSize = 0; + uploadQueueVolume = 0; + df = new SimpleDateFormat("MMM dd HH:mm:ss"); + eta = ""; } - @Override - public void start() { + @Override public void start() { if (client != null) { CLIInputObserver cli_input_observer = new CLIInputObserver(client); @@ -66,8 +78,7 @@ public void start() { cli_input_observer_thread.start(); Signal.handle(new Signal("INT"), new SignalHandler() { - @Override - public void handle(Signal signal) { + @Override public void handle(Signal signal) { sigIntCount++; if (sigIntCount == 5) { @@ -90,82 +101,141 @@ else if (client.isRunning() && client.isSuspended() == false) { } } - @Override - public void stop() { + @Override public void stop() { Runtime.getRuntime().halt(0); } - @Override - public void status(String msg_) { - status = msg_; + @Override public void updateTrayIcon(Integer percentage) { + } + + @Override public void status(String msg_) { + status(msg_, false); + } + + @Override public void status(String msg_, boolean overwriteSuspendedMsg) { + if (client != null && client.isSuspended()) { + if (overwriteSuspendedMsg) { + status = msg_; + updateLine(); + } + } + else { + status = msg_; + updateLine(); + } + } + + @Override public void status(String msg, int progress) { + this.status(msg, progress, 0); + } + + @Override public void status(String msg, int progress, long size) { + status = showProgress(msg, progress, size); updateLine(); } - @Override - public void setRenderingProjectName(String name_) { + @Override public void setRenderingProjectName(String name_) { if (name_ == null || name_.isEmpty()) { project = ""; } else { - project = "Project \"" + name_ + "\" |"; + project = name_ + " |"; } updateLine(); } - @Override - public void error(String msg_) { + @Override public void error(String msg_) { status = "Error " + msg_; updateLine(); } - @Override - public void AddFrameRendered() { + @Override public void AddFrameRendered() { rendered += 1; updateLine(); } - @Override - public void displayStats(Stats stats) { + @Override public void displayStats(Stats stats) { remaining = stats.getRemainingFrame(); creditsEarned = String.valueOf(stats.getCreditsEarnedDuringSession()); updateLine(); } - @Override - public void setRemainingTime(String time_) { - status = "(remaining " + time_ + ")"; + @Override public void displayUploadQueueStats(int queueSize, long queueVolume) { + this.uploadQueueSize = queueSize; + this.uploadQueueVolume = queueVolume; + } + + @Override public void setRemainingTime(String time_) { + this.eta = time_; updateLine(); } - @Override - public void setRenderingTime(String time_) { + @Override public void setRenderingTime(String time_) { status = "Rendering " + time_; updateLine(); } - @Override - public void setClient(Client cli) { + @Override public void setClient(Client cli) { client = cli; } - @Override - public void setComputeMethod(String computeMethod_) { + @Override public void setComputeMethod(String computeMethod_) { computeMethod = computeMethod_; } - @Override - public Client getClient() { + @Override public Client getClient() { return client; } + @Override public void successfulAuthenticationEvent(String publickey) { + + } + private void updateLine() { int charToRemove = line.length(); System.out.print("\r"); - line = String.format("Frames: %d Points: %s | %s %s %s", rendered, creditsEarned != null ? creditsEarned : "unknown", project, computeMethod, status + (exiting ? " (Exiting after this frame)" : "")); + + line = String.format("%s Frames: %d Points: %s | Upload Queue: %d%s | %%s %s %s", df.format(new Date()), rendered, + creditsEarned != null ? creditsEarned : "unknown", this.uploadQueueSize, + (this.uploadQueueSize > 0 ? String.format(" (%.2fMB)", (this.uploadQueueVolume / 1024.0 / 1024.0)) : ""), computeMethod, + status + (exiting ? " (Exiting after all frames are uploaded)" : "")); + + if (line.length() + project.length() > 120) { + // If the line without the project name is already >120 characters (might happen if the user has thousands of frames and millions of points in the + // session + is exiting after all frames are uploaded) then set the line to 117c to avoid a negative number exception in substring function + int lineLength = (line.length() >= 120 ? 117 : line.length()); + line = String.format(line, project.substring(0, 117 - lineLength) + "..."); + } + else { + line = String.format(line, project); + } + System.out.print(line); for (int i = line.length(); i <= charToRemove; i++) { System.out.print(" "); } } + + private String showProgress(String message, int progress, long size) { + StringBuilder progressBar = new StringBuilder(140); + progressBar + .append(message) + .append(String.join("", Collections.nCopies(progress == 0 ? 2 : 2 - (int) (Math.log10(progress)), " "))) + .append(String.format(" %d%%%% [", progress)) + .append(String.join("", Collections.nCopies((int) (progress / 10), "="))) + .append('>') + .append(String.join("", Collections.nCopies(10 - (int) (progress / 10), " "))) + .append(']'); + + if (size > 0) { + progressBar.append(String.format(" %dMB", (size / 1024 / 1024))); + } + + if (!this.eta.equals("")) { + progressBar.append(String.format(" ETA %s", this.eta)); + } + + return progressBar.toString(); + } } diff --git a/src/com/sheepit/client/standalone/ListGpuParameterHandler.java b/src/com/sheepit/client/standalone/ListGpuParameterHandler.java index 86f88846..2f62b08d 100644 --- a/src/com/sheepit/client/standalone/ListGpuParameterHandler.java +++ b/src/com/sheepit/client/standalone/ListGpuParameterHandler.java @@ -2,7 +2,7 @@ * Copyright (C) 2017 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -37,12 +37,12 @@ public ListGpuParameterHandler(CmdLineParser parser, OptionDef option, Setter gpus = GPU.listDevices(new Configuration(null, null, null)); if (gpus != null) { for (GPUDevice gpu : gpus) { - System.out.println("Id : " + gpu.getId()); + System.out.println("GPU_ID : " + gpu.getOldId()); + System.out.println("Long ID : " + gpu.getId()); System.out.println("Model : " + gpu.getModel()); System.out.println("Memory, MB: " + (int) (gpu.getMemory() / (1024 * 1024))); System.out.println(); @@ -53,8 +53,7 @@ public int parseArguments(Parameters params) throws CmdLineException { return 0; } - @Override - public String getDefaultMetaVariable() { + @Override public String getDefaultMetaVariable() { return null; } } diff --git a/src/com/sheepit/client/standalone/VersionParameterHandler.java b/src/com/sheepit/client/standalone/VersionParameterHandler.java index e1de0ac8..501b3841 100644 --- a/src/com/sheepit/client/standalone/VersionParameterHandler.java +++ b/src/com/sheepit/client/standalone/VersionParameterHandler.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -33,16 +33,14 @@ public VersionParameterHandler(CmdLineParser parser, OptionDef option, Setter * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -21,8 +21,8 @@ import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; -import static org.kohsuke.args4j.ExampleMode.REQUIRED; import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.OptionHandlerFilter; import java.io.File; import java.net.MalformedURLException; @@ -30,6 +30,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.LinkedList; + import com.sheepit.client.Client; import com.sheepit.client.Configuration; import com.sheepit.client.Configuration.ComputeType; @@ -38,6 +39,7 @@ import com.sheepit.client.Pair; import com.sheepit.client.SettingsLoader; import com.sheepit.client.ShutdownHook; +import com.sheepit.client.Utils; import com.sheepit.client.hardware.gpu.GPU; import com.sheepit.client.hardware.gpu.GPUDevice; import com.sheepit.client.hardware.gpu.nvidia.Nvidia; @@ -45,71 +47,52 @@ import com.sheepit.client.network.Proxy; public class Worker { - @Option(name = "-server", usage = "Render-farm server, default https://client.sheepit-renderfarm.com", metaVar = "URL", required = false) - private String server = "https://client.sheepit-renderfarm.com"; + @Option(name = "-server", usage = "Render-farm server, default https://client.sheepit-renderfarm.com", metaVar = "URL", required = false) private String server = "https://client.sheepit-renderfarm.com"; - @Option(name = "-login", usage = "User's login", metaVar = "LOGIN", required = false) - private String login = ""; + @Option(name = "-login", usage = "User's login", metaVar = "LOGIN", required = false) private String login = ""; - @Option(name = "-password", usage = "User's password", metaVar = "PASSWORD", required = false) - private String password = ""; + @Option(name = "-password", usage = "User's password", metaVar = "PASSWORD", required = false) private String password = ""; - @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 = "-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 = "-max-uploading-job", usage = "", metaVar = "1", required = false) - private int max_upload = -1; + @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 = "-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; - @Option(name = "--no-gpu", usage = "Don't detect GPUs", required = false) - private boolean no_gpu_detection = false; + @Option(name = "-compute-method", usage = "CPU: only use cpu, GPU: only use gpu, CPU_GPU: can use cpu and gpu (not at the same time) if -gpu is not use it will not use the gpu", metaVar = "CPU", required = false) private String method = null; - @Option(name = "-compute-method", usage = "CPU: only use cpu, GPU: only use gpu, CPU_GPU: can use cpu and gpu (not at the same time) if -gpu is not use it will not use the gpu", metaVar = "CPU", required = false) - private String method = null; + @Option(name = "-cores", usage = "Number of cores/threads to use for the render", metaVar = "3", required = false) private int nb_cores = -1; - @Option(name = "-cores", usage = "Number of cores/threads to use for the render", metaVar = "3", required = false) - private int nb_cores = -1; + @Option(name = "-memory", usage = "Maximum memory allow to be used by renderer, number with unit (800M, 2G, ...)", required = false) private String max_ram = null; - @Option(name = "-memory", usage = "Maximum memory allow to be used by renderer (in MB)", required = false) - private int max_ram = -1; + @Option(name = "-rendertime", usage = "Maximum time allow for each frame (in minutes)", required = false) private int max_rendertime = -1; - @Option(name = "-rendertime", usage = "Maximum time allow for each frame (in minute)", required = false) - private int max_rendertime = -1; + @Option(name = "--verbose", usage = "Display log", required = false) private boolean print_log = false; - @Option(name = "--verbose", usage = "Display log", required = false) - private boolean print_log = false; + @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 = "-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 = "-proxy", usage = "URL of the proxy", metaVar = "http://login:password@host:port", required = false) private String proxy = 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; - @Option(name = "-extras", usage = "Extras data push on the authentication request", required = false) - private String extras = null; + @Option(name = "-ui", usage = "Specify the user interface to use, default '" + GuiSwing.type + "', available '" + GuiTextOneLine.type + "', '" + + GuiText.type + "', '" + GuiSwing.type + "' (graphical)", required = false) private String ui_type = null; - @Option(name = "-ui", usage = "Specify the user interface to use, default '" + GuiSwing.type + "', available '" + GuiTextOneLine.type + "', '" + GuiText.type + "', '" + GuiSwing.type + "' (graphical)", required = false) - private String ui_type = null; + @Option(name = "-config", usage = "Specify the configuration file", required = false) private String config_file = null; - @Option(name = "-config", usage = "Specify the configuration file", required = false) - private String config_file = null; + @Option(name = "--version", usage = "Display application version", required = false, handler = VersionParameterHandler.class) private VersionParameterHandler versionHandler; - @Option(name = "--version", usage = "Display application version", required = false, handler = VersionParameterHandler.class) - private VersionParameterHandler versionHandler; + @Option(name = "--show-gpu", usage = "Print available CUDA devices and exit", required = false, handler = ListGpuParameterHandler.class) private ListGpuParameterHandler listGpuParameterHandler; - @Option(name = "--show-gpu", usage = "Print available CUDA devices and exit", required = false, handler = ListGpuParameterHandler.class) - private ListGpuParameterHandler listGpuParameterHandler; + @Option(name = "--no-systray", usage = "Don't use SysTray", required = false) private boolean useSysTray = false; - @Option(name = "--no-systray", usage = "Don't use systray", required = false) - private boolean no_systray = false; + @Option(name = "-priority", usage = "Set render process priority (19 lowest to -19 highest)", required = false) private int priority = 19; - @Option(name = "-priority", usage = "Set render process priority (19 lowest to -19 highest)", required = false) - private int priority = 19; - - @Option(name = "-title", usage = "Custom title for the GUI Client", required = false) - private String title = "SheepIt Render Farm"; + @Option(name = "-title", usage = "Custom title for the GUI Client", required = false) private String title = "SheepIt Render Farm"; + + @Option(name = "-theme", usage = "Specify the theme to use for the graphical client, default 'light', available 'light', 'dark'", required = false) private String theme = null; + + @Option(name = "-renderbucket-size", usage = "Set a custom GPU renderbucket size (32 for 32x32px, 64 for 64x64px, and so on). NVIDIA GPUs support a maximum renderbucket size of 512x512 pixel, while AMD GPUs support a maximum 2048x2048 pixel renderbucket size. Minimum renderbucket size is 32 pixels for all GPUs", required = false) private int renderbucketSize = -1; public static void main(String[] args) { new Worker().doMain(args); @@ -125,7 +108,7 @@ public void doMain(String[] args) { System.err.println("Usage: "); parser.printUsage(System.err); System.err.println(); - System.err.println("Example: java " + this.getClass().getName() + " " + parser.printExample(REQUIRED)); + System.err.println("Example: java " + this.getClass().getName() + " " + parser.printExample(OptionHandlerFilter.REQUIRED)); return; } @@ -133,7 +116,7 @@ public void doMain(String[] args) { Configuration config = new Configuration(null, login, password); config.setPrintLog(print_log); config.setUsePriority(priority); - config.setDetectGPUs(! no_gpu_detection); + config.setDetectGPUs(!no_gpu_detection); if (cache_dir != null) { File a_dir = new File(cache_dir); @@ -143,45 +126,37 @@ public void doMain(String[] args) { } } - if (max_upload != -1) { - if (max_upload <= 0) { - System.err.println("Error: max upload should be a greater than zero"); - return; - } - config.setMaxUploadingJob(max_upload); - } + // We set a hard limit of 3 concurrent uploads. As the server doesn't allocate more than 3 concurrent jobs to + // a single session to avoid any client taking too many frames and not validating them, we throttle the uploads. + // If we don't set this limit, in a computer with slow uploads the server will return a "no job available" when + // the 4th concurrent job is requested and that will put the client in "wait" mode for some random time. To + // avoid that situation we set this limit. + config.setMaxUploadingJob(3); + + // Store the SysTray preference from the user. Please note that we must ! the value of the variable because the way args4j works. If the --no-systray + // parameter is detected, args4j will store (boolean)true in the useSysTray variable but we want to store (boolean)false in the configuration class + // for further checks. + config.setUseSysTray(!useSysTray); if (gpu_device != null) { if (gpu_device.startsWith(Nvidia.TYPE) == false && gpu_device.startsWith(OpenCL.TYPE) == false) { - System.err.println("CUDA_DEVICE should look like '" + Nvidia.TYPE + "_X' or '" + OpenCL.TYPE + "_X' where X is a number"); - return; - } - String family = ""; - try { - if (gpu_device.startsWith(Nvidia.TYPE)) { - family = Nvidia.TYPE; - } - else if (gpu_device.startsWith(OpenCL.TYPE)) { - family = OpenCL.TYPE; - } - Integer.parseInt(gpu_device.substring(family.length() + 1)); // for the _ - } - catch (NumberFormatException en) { - System.err.println("Gpu device code should look like '" + family + "_X' where X is a number"); - return; + System.err.println("ERROR: The entered GPU_ID is invalid. The GPU_ID should look like '" + Nvidia.TYPE + "_#' or '" + OpenCL.TYPE + + "_#'. Please use the proper GPU_ID from the GPU list below\n"); + showGPUList(parser); } + GPUDevice gpu = GPU.getGPUDevice(gpu_device); if (gpu == null) { - System.err.println("GPU unknown"); - System.exit(2); + System.err.println("ERROR: The entered GPU_ID is invalid. Please use the proper GPU_ID from the GPU list below\n"); + showGPUList(parser); } - config.setUseGPU(gpu); + config.setGPUDevice(gpu); } if (request_time != null) { String[] intervals = request_time.split(","); if (intervals != null) { - config.requestTime = new LinkedList>(); + config.setRequestTime(new LinkedList>()); SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm"); for (String interval : intervals) { @@ -195,15 +170,17 @@ else if (gpu_device.startsWith(OpenCL.TYPE)) { end.setTime(timeFormat.parse(times[1])); } catch (ParseException e) { - System.err.println("Error: wrong format in request time"); + System.err.println(String.format( + "ERROR: The entered time slot (-request-time parameter) doesn't seem to be valid. Please check the format is correct [%s]", + e.getMessage())); System.exit(2); } if (start.before(end)) { - config.requestTime.add(new Pair(start, end)); + config.getRequestTime().add(new Pair(start, end)); } else { - System.err.println("Error: wrong request time " + times[0] + " is after " + times[1]); + System.err.println(String.format("ERROR: The start (%s) time must be earlier than the finish (%s) time", times[0], times[1])); System.exit(2); } } @@ -212,15 +189,22 @@ else if (gpu_device.startsWith(OpenCL.TYPE)) { } if (nb_cores < -1 || nb_cores == 0) { // -1 is the default - System.err.println("Error: use-number-core should be a greater than zero"); + System.err.println("ERROR: The entered number of CPU cores (-cores parameter) is not valid. Please enter a number greater than zero"); return; } else { - config.setUseNbCores(nb_cores); + config.setNbCores(nb_cores); } - if (max_ram > 0) { - config.setMaxMemory(max_ram * 1000); + if (max_ram != null) { + try { + config.setMaxMemory(Utils.parseNumber(max_ram) / 1000); // internal value are in kB + } + catch (java.lang.IllegalStateException e) { + System.err.println( + String.format("ERROR: The entered value of maximum memory (-memory parameter) doesn't seem to be a valid number [%s]", e.getMessage())); + return; + } } if (max_rendertime > 0) { @@ -232,7 +216,9 @@ else if (gpu_device.startsWith(OpenCL.TYPE)) { compute_method = ComputeType.valueOf(method); } catch (IllegalArgumentException e) { - System.err.println("Error: compute-method unknown"); + System.err.println(String.format( + "ERROR: The entered compute method (-compute-method parameter) is not valid. Available values are CPU, GPU or CPU_GPU [%s]", + e.getMessage())); System.exit(2); } } @@ -250,8 +236,8 @@ else if (gpu_device.startsWith(OpenCL.TYPE)) { Proxy.set(proxy); } catch (MalformedURLException e) { - System.err.println("Error: wrong url for proxy"); - System.err.println(e); + System.err.println(String.format("ERROR: The entered proxy URL (-proxy parameter) doesn't seem to have the right format. Please check it [%s]", + e.getMessage())); System.exit(2); } } @@ -261,40 +247,54 @@ else if (gpu_device.startsWith(OpenCL.TYPE)) { } if (compute_method == ComputeType.CPU && config.getGPUDevice() != null) { - System.err.println("You choose to only use the CPU but a GPU was also provided. You can not do both."); - System.err.println("Aborting"); + System.err.println( + "ERROR: The compute method is set to use CPU only, but a GPU has also been specified. Change the compute method to CPU_GPU or remove the GPU"); System.exit(2); } else if (compute_method == ComputeType.CPU_GPU && config.getGPUDevice() == null) { - System.err.println("You choose to only use the CPU and GPU but no GPU device was provided."); - System.err.println("Aborting"); + System.err.println( + "ERROR: The compute method is set to use both CPU and GPU, but no GPU has been specified. Change the compute method to CPU or add a GPU (via -gpu parameter)"); System.exit(2); } else if (compute_method == ComputeType.GPU && config.getGPUDevice() == null) { - System.err.println("You choose to only use the GPU but no GPU device was provided."); - System.err.println("Aborting"); + System.err.println("ERROR: The compute method is set to use GPU only, but not GPU has been specified. Please add a GPU (via -gpu parameter)"); System.exit(2); } else if (compute_method == ComputeType.CPU) { - config.setUseGPU(null); // remove the GPU + config.setGPUDevice(null); // remove the GPU } config.setComputeMethod(compute_method); + // Change the default configuration if the user has specified a minimum renderbucket size of 32 + if (renderbucketSize >= 32) { + // Send the proposed renderbucket size and check if viable + config.setRenderbucketSize(renderbucketSize); + } + if (ui_type != null) { config.setUIType(ui_type); } + if (theme != null) { + if (!theme.equals("light") && !theme.equals("dark")) { + System.err.println("ERROR: The entered theme (-theme parameter) doesn't exist. Please choose either 'light' or 'dark'"); + System.exit(2); + } + + config.setTheme(this.theme); + } + if (config_file != null) { if (new File(config_file).exists() == false) { - System.err.println("Configuration file not found."); - System.err.println("Aborting"); + System.err.println( + "ERROR: The entered configuration file (-config parameter) cannot be loaded. Please check that you've entered an existing filename"); System.exit(2); } - config.setConfigPath(config_file); - new SettingsLoader(config_file).merge(config); + config.setConfigFilePath(config_file); } + new SettingsLoader(config_file).merge(config); Log.getInstance(config).debug("client version " + config.getJarVersion()); Gui gui; @@ -304,8 +304,9 @@ else if (compute_method == ComputeType.CPU) { } switch (type) { case GuiTextOneLine.type: - if (config.getPrintLog()) { - System.out.println("OneLine UI can not be used if verbose mode is enabled"); + if (config.isPrintLog()) { + System.err.println( + "ERROR: The oneLine UI and the --verbose parameter cannot be used at the same time. Please either change the ui to text or remove the verbose mode"); System.exit(2); } gui = new GuiTextOneLine(); @@ -316,11 +317,11 @@ else if (compute_method == ComputeType.CPU) { default: case GuiSwing.type: if (java.awt.GraphicsEnvironment.isHeadless()) { - System.out.println("Graphical ui can not be launch."); - System.out.println("You should set a DISPLAY or use a text ui (with -ui " + GuiTextOneLine.type + " or -ui " + GuiText.type + ")."); + System.err.println("ERROR: Your current configuration doesn't support graphical UI."); + System.err.println("Please use one of the text-based UIs provided (using -ui " + GuiTextOneLine.type + " or -ui " + GuiText.type + ")"); System.exit(3); } - gui = new GuiSwing(no_systray == false, title); + gui = new GuiSwing(config.isUseSysTray(), title); break; } Client cli = new Client(gui, config, server); @@ -331,4 +332,13 @@ else if (compute_method == ComputeType.CPU) { gui.start(); } + + private void showGPUList(CmdLineParser parser) { + try { + parser.parseArgument("--show-gpu"); + } + catch (CmdLineException e) { + System.err.println(String.format("ERROR: Unable to parse the provided parameter [%s]", e.getMessage())); + } + } } diff --git a/src/com/sheepit/client/standalone/swing/activity/Activity.java b/src/com/sheepit/client/standalone/swing/activity/Activity.java index 537ad139..15a996f2 100644 --- a/src/com/sheepit/client/standalone/swing/activity/Activity.java +++ b/src/com/sheepit/client/standalone/swing/activity/Activity.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/standalone/swing/activity/Settings.java b/src/com/sheepit/client/standalone/swing/activity/Settings.java index 263fbb89..80c236c0 100644 --- a/src/com/sheepit/client/standalone/swing/activity/Settings.java +++ b/src/com/sheepit/client/standalone/swing/activity/Settings.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -19,6 +19,7 @@ package com.sheepit.client.standalone.swing.activity; +import java.awt.Graphics; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; @@ -26,15 +27,18 @@ import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; +import java.awt.image.BufferedImage; import java.io.File; import java.net.MalformedURLException; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; +import javax.imageio.ImageIO; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBox; @@ -43,20 +47,31 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPasswordField; +import javax.swing.JRadioButton; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; import javax.swing.SpinnerNumberModel; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.FlatDarkLaf; + import com.sheepit.client.Configuration; import com.sheepit.client.Configuration.ComputeType; import com.sheepit.client.SettingsLoader; import com.sheepit.client.hardware.cpu.CPU; import com.sheepit.client.hardware.gpu.GPU; import com.sheepit.client.hardware.gpu.GPUDevice; +import com.sheepit.client.hardware.gpu.GPULister; +import com.sheepit.client.hardware.gpu.nvidia.Nvidia; +import com.sheepit.client.hardware.gpu.opencl.OpenCL; import com.sheepit.client.network.Proxy; import com.sheepit.client.os.OS; import com.sheepit.client.standalone.GuiSwing; +import com.sheepit.client.standalone.swing.components.CollapsibleJPanel; public class Settings implements Activity { private static final String DUMMY_CACHE_DIR = "Auto detected"; @@ -70,6 +85,9 @@ public class Settings implements Activity { private JFileChooser cacheDirChooser; private JCheckBox useCPU; private List useGPUs; + private JCheckBox useSysTray; + private JLabel renderbucketSizeLabel; + private JSlider renderbucketSize; private JSlider cpuCores; private JSlider ram; private JSpinner renderTime; @@ -77,15 +95,16 @@ public class Settings implements Activity { private JTextField proxy; private JTextField hostname; + private ButtonGroup themeOptionsGroup; + private JRadioButton lightMode; + private JRadioButton darkMode; + private JCheckBox saveFile; private JCheckBox autoSignIn; JButton saveButton; private boolean haveAutoStarted; - - private JTextField tileSizeValue; - private JLabel tileSizeLabel; - private JCheckBox customTileSize; + private boolean useSysTrayPrevState; public Settings(GuiSwing parent_) { parent = parent_; @@ -94,19 +113,39 @@ public Settings(GuiSwing parent_) { haveAutoStarted = false; } - @Override - public void show() { + @Override public void show() { Configuration config = parent.getConfiguration(); new SettingsLoader(config.getConfigFilePath()).merge(config); + useSysTrayPrevState = config.isUseSysTray(); + + applyTheme(config.getTheme()); // apply the proper theme (light/dark) List gpus = GPU.listDevices(config); + useGPUs.clear(); // Empty the auxiliary list (used in the list of checkboxes) GridBagConstraints constraints = new GridBagConstraints(); int currentRow = 0; - ImageIcon image = new ImageIcon(getClass().getResource("/title.png")); + + JLabel labelImage; + try { + // Include the version of the app as a watermark in the SheepIt logo. + final BufferedImage watermark = ImageIO.read(getClass().getResource("/sheepit-logo.png")); + + Graphics gph = watermark.getGraphics(); + gph.setFont(gph.getFont().deriveFont(12f)); + gph.drawString("v" + config.getJarVersion(), 335, 120); + gph.dispose(); + + labelImage = new JLabel(new ImageIcon(watermark)); + } + catch (Exception e) { + // If something fails, we just show the SheepIt logo (without any watermark) + ImageIcon image = new ImageIcon(getClass().getResource("/sheepit-logo.png")); + labelImage = new JLabel(image); + } + constraints.fill = GridBagConstraints.CENTER; - JLabel labelImage = new JLabel(image); constraints.gridwidth = 2; constraints.gridx = 0; constraints.gridy = currentRow; @@ -114,18 +153,23 @@ public void show() { ++currentRow; + constraints.gridy = currentRow; + parent.getContentPane().add(new JLabel(" "), constraints); // Add a separator between logo and first panel + + currentRow++; + // authentication - JPanel authentication_panel = new JPanel(new GridLayout(2, 2)); + CollapsibleJPanel authentication_panel = new CollapsibleJPanel(new GridLayout(2, 2)); authentication_panel.setBorder(BorderFactory.createTitledBorder("Authentication")); JLabel loginLabel = new JLabel("Username:"); login = new JTextField(); - login.setText(parent.getConfiguration().login()); + login.setText(parent.getConfiguration().getLogin()); login.setColumns(20); login.addKeyListener(new CheckCanStart()); JLabel passwordLabel = new JLabel("Password:"); password = new JPasswordField(); - password.setText(parent.getConfiguration().password()); + password.setText(parent.getConfiguration().getPassword()); password.setColumns(10); password.addKeyListener(new CheckCanStart()); @@ -140,13 +184,43 @@ public void show() { constraints.fill = GridBagConstraints.HORIZONTAL; parent.getContentPane().add(authentication_panel, constraints); + // Theme selection panel + CollapsibleJPanel themePanel = new CollapsibleJPanel(new GridLayout(1, 3)); + themePanel.setBorder(BorderFactory.createTitledBorder("Theme")); + + themeOptionsGroup = new ButtonGroup(); + + lightMode = new JRadioButton("Light"); + lightMode.setActionCommand("light"); + lightMode.setSelected(config.getTheme().equals("light")); + lightMode.addActionListener(new ApplyThemeAction()); + + darkMode = new JRadioButton("Dark"); + darkMode.setActionCommand("dark"); + darkMode.setSelected(config.getTheme().equals("dark")); + darkMode.addActionListener(new ApplyThemeAction()); + + themePanel.add(lightMode); + themePanel.add(darkMode); + + // Group both radio buttons to allow only one selected + themeOptionsGroup.add(lightMode); + themeOptionsGroup.add(darkMode); + + currentRow++; + constraints.gridx = 0; + constraints.gridy = currentRow; + constraints.gridwidth = 2; + + parent.getContentPane().add(themePanel, constraints); + // directory - JPanel directory_panel = new JPanel(new GridLayout(1, 3)); + CollapsibleJPanel directory_panel = new CollapsibleJPanel(new GridLayout(1, 3)); directory_panel.setBorder(BorderFactory.createTitledBorder("Cache")); JLabel cacheLabel = new JLabel("Working directory:"); directory_panel.add(cacheLabel); String destination = DUMMY_CACHE_DIR; - if (config.getUserHasSpecifiedACacheDir()) { + if (config.isUserHasSpecifiedACacheDir()) { destination = config.getCacheDirForSettings().getName(); } @@ -175,7 +249,7 @@ public void show() { // compute devices GridBagLayout gridbag = new GridBagLayout(); GridBagConstraints compute_devices_constraints = new GridBagConstraints(); - JPanel compute_devices_panel = new JPanel(gridbag); + CollapsibleJPanel compute_devices_panel = new CollapsibleJPanel(gridbag); compute_devices_panel.setBorder(BorderFactory.createTitledBorder("Compute devices")); @@ -206,34 +280,87 @@ else if (method == ComputeType.GPU) { gridbag.setConstraints(useCPU, compute_devices_constraints); compute_devices_panel.add(useCPU); - for (GPUDevice gpu : gpus) { - JCheckBoxGPU gpuCheckBox = new JCheckBoxGPU(gpu); - gpuCheckBox.setToolTipText(gpu.getId()); - if (gpuChecked) { - GPUDevice config_gpu = config.getGPUDevice(); - if (config_gpu != null && config_gpu.getId().equals(gpu.getId())) { - gpuCheckBox.setSelected(gpuChecked); + if (gpus.size() > 0) { + renderbucketSizeLabel = new JLabel("Renderbucket size:"); + renderbucketSize = new JSlider(); + renderbucketSize.setMajorTickSpacing(1); + renderbucketSize.setMinorTickSpacing(1); + renderbucketSize.setPaintTicks(true); + renderbucketSize.setPaintLabels(true); + + renderbucketSizeLabel.setVisible(false); + renderbucketSize.setVisible(false); + + for (GPUDevice gpu : gpus) { + JCheckBoxGPU gpuCheckBox = new JCheckBoxGPU(gpu); + gpuCheckBox.setToolTipText(gpu.getId()); + if (gpuChecked) { + GPUDevice config_gpu = config.getGPUDevice(); + if (config_gpu != null && config_gpu.getId().equals(gpu.getId())) { + gpuCheckBox.setSelected(gpuChecked); + renderbucketSizeLabel.setVisible(true); + renderbucketSize.setVisible(true); + } } + gpuCheckBox.addActionListener(new GpuChangeAction()); + + compute_devices_constraints.gridy++; + gridbag.setConstraints(gpuCheckBox, compute_devices_constraints); + compute_devices_panel.add(gpuCheckBox); + useGPUs.add(gpuCheckBox); } - gpuCheckBox.addActionListener(new GpuChangeAction()); + // Initialisation values will apply if we are not able to detect the proper GPU technology or + // because is a new one (different from CUDA and OPENCL). In that case, move into a safe position + // of 32x32 pixel render bucket and a maximum of 128x128 pixel for the "unknown GPU" + int maxRenderbucketSize = 128; + int recommendedBucketSize = 32; + + if (config.getComputeMethod() == ComputeType.GPU || config.getComputeMethod() == ComputeType.CPU_GPU) { + GPULister gpu; + + if (config.getGPUDevice().getType().equals("CUDA")) { + gpu = new Nvidia(); + maxRenderbucketSize = gpu.getMaximumRenderBucketSize(config.getGPUDevice().getMemory()); + recommendedBucketSize = gpu.getRecommendedRenderBucketSize(config.getGPUDevice().getMemory()); + } + else if (config.getGPUDevice().getType().equals("OPENCL")) { + gpu = new OpenCL(); + maxRenderbucketSize = gpu.getMaximumRenderBucketSize(config.getGPUDevice().getMemory()); + recommendedBucketSize = gpu.getRecommendedRenderBucketSize(config.getGPUDevice().getMemory()); + } + } + + buildRenderBucketSizeSlider(maxRenderbucketSize, config.getRenderbucketSize() != -1 ? + ((int) (Math.log(config.getRenderbucketSize()) / Math.log(2))) - 5 : + ((int) (Math.log(recommendedBucketSize) / Math.log(2))) - 5); + + compute_devices_constraints.weightx = 1.0 / gpus.size(); + compute_devices_constraints.gridx = 0; compute_devices_constraints.gridy++; - gridbag.setConstraints(gpuCheckBox, compute_devices_constraints); - compute_devices_panel.add(gpuCheckBox); - useGPUs.add(gpuCheckBox); + + gridbag.setConstraints(renderbucketSizeLabel, compute_devices_constraints); + compute_devices_panel.add(renderbucketSizeLabel); + + compute_devices_constraints.gridx = 1; + compute_devices_constraints.weightx = 1.0; + + gridbag.setConstraints(renderbucketSize, compute_devices_constraints); + compute_devices_panel.add(new JLabel(" "), compute_devices_constraints); // Add a space between lines + compute_devices_panel.add(renderbucketSize); } CPU cpu = new CPU(); if (cpu.cores() > 1) { // if only one core is available, no need to show the choice double step = 1; - double display = (double)cpu.cores() / step; + double display = (double) cpu.cores() / step; while (display > 10) { step += 1.0; - display = (double)cpu.cores() / step; + display = (double) cpu.cores() / step; } cpuCores = new JSlider(1, cpu.cores()); - cpuCores.setMajorTickSpacing((int)(step)); + cpuCores.setMajorTickSpacing((int) (step)); cpuCores.setMinorTickSpacing(1); cpuCores.setPaintTicks(true); cpuCores.setPaintLabels(true); @@ -251,18 +378,19 @@ else if (method == ComputeType.GPU) { compute_devices_constraints.weightx = 1.0; gridbag.setConstraints(cpuCores, compute_devices_constraints); + compute_devices_panel.add(new JLabel(" "), compute_devices_constraints); // Add a space between lines compute_devices_panel.add(cpuCores); } // max ram allowed to render OS os = OS.getOS(); - int all_ram = os.getMemory(); + int all_ram = (int) os.getMemory(); ram = new JSlider(0, all_ram); int step = 1000000; - double display = (double)all_ram / (double)step; + double display = (double) all_ram / (double) step; while (display > 10) { step += 1000000; - display = (double)all_ram / (double)step; + display = (double) all_ram / (double) step; } Hashtable labelTable = new Hashtable(); for (int g = 0; g < all_ram; g += step) { @@ -272,7 +400,7 @@ else if (method == ComputeType.GPU) { ram.setLabelTable(labelTable); ram.setPaintTicks(true); ram.setPaintLabels(true); - ram.setValue(config.getMaxMemory() != -1 ? config.getMaxMemory() : os.getMemory()); + ram.setValue((int) (config.getMaxMemory() != -1 ? config.getMaxMemory() : os.getMemory())); JLabel ramLabel = new JLabel("Memory:"); compute_devices_constraints.weightx = 1.0 / gpus.size(); @@ -286,6 +414,7 @@ else if (method == ComputeType.GPU) { compute_devices_constraints.weightx = 1.0; gridbag.setConstraints(ram, compute_devices_constraints); + compute_devices_panel.add(new JLabel(" "), compute_devices_constraints); // Add a space between lines compute_devices_panel.add(ram); parent.getContentPane().add(compute_devices_panel, constraints); @@ -298,7 +427,11 @@ else if (method == ComputeType.GPU) { priority.setPaintTicks(true); priority.setPaintLabels(true); priority.setValue(config.getPriority()); - JLabel priorityLabel = new JLabel(high_priority_support ? "Priority (High <-> Low):" : "Priority (Normal <-> Low):" ); + JLabel priorityLabel = new JLabel(high_priority_support ? "Priority (High <-> Low):" : "Priority (Normal <-> Low):"); + + boolean showPrioritySlider = os.checkNiceAvailability(); + priority.setVisible(showPrioritySlider); + priorityLabel.setVisible(showPrioritySlider); compute_devices_constraints.weightx = 1.0 / gpus.size(); compute_devices_constraints.gridx = 0; @@ -320,9 +453,16 @@ else if (method == ComputeType.GPU) { parent.getContentPane().add(compute_devices_panel, constraints); // other - JPanel advanced_panel = new JPanel(new GridLayout(5, 2)); + CollapsibleJPanel advanced_panel = new CollapsibleJPanel(new GridLayout(4, 2)); advanced_panel.setBorder(BorderFactory.createTitledBorder("Advanced options")); + JLabel useSysTrayLabel = new JLabel("Minimize to SysTray"); + + useSysTray = new JCheckBox(); + useSysTray.setSelected(config.isUseSysTray()); + advanced_panel.add(useSysTrayLabel); + advanced_panel.add(useSysTray); + JLabel proxyLabel = new JLabel("Proxy:"); proxyLabel.setToolTipText("http://login:password@host:port"); proxy = new JTextField(); @@ -345,35 +485,11 @@ else if (method == ComputeType.GPU) { if (parent.getConfiguration().getMaxRenderTime() > 0) { val = parent.getConfiguration().getMaxRenderTime() / 60; } - renderTime = new JSpinner(new SpinnerNumberModel(val,0,1000,1)); + renderTime = new JSpinner(new SpinnerNumberModel(val, 0, 1000, 1)); advanced_panel.add(renderTimeLabel); advanced_panel.add(renderTime); - JLabel customTileSizeLabel = new JLabel("Custom render tile size:"); - customTileSize = new JCheckBox("", config.getTileSize() != -1); - advanced_panel.add(customTileSizeLabel); - advanced_panel.add(customTileSize); - - customTileSize.addActionListener(new TileSizeChange()); - tileSizeLabel = new JLabel("Tile Size:"); - - tileSizeValue = new JTextField(); - int fromConfig = parent.getConfiguration().getTileSize(); - if (fromConfig == -1) { - if (parent.getConfiguration().getGPUDevice() != null) { - fromConfig = parent.getConfiguration().getGPUDevice().getRecommandedTileSize(); - } - else { - fromConfig = 32; - } - } - tileSizeValue.setText(Integer.toString(fromConfig)); - hideCustomTileSize(config.getTileSize() != -1, false); - - advanced_panel.add(tileSizeLabel); - advanced_panel.add(tileSizeValue); - currentRow++; constraints.gridx = 0; constraints.gridy = currentRow; @@ -386,7 +502,7 @@ else if (method == ComputeType.GPU) { saveFile = new JCheckBox("Save settings", true); general_panel.add(saveFile); - autoSignIn = new JCheckBox("Auto sign in", config.getAutoSignIn()); + autoSignIn = new JCheckBox("Auto sign in", config.isAutoSignIn()); autoSignIn.addActionListener(new AutoSignInChangeAction()); general_panel.add(autoSignIn); @@ -396,6 +512,11 @@ else if (method == ComputeType.GPU) { constraints.gridwidth = 2; parent.getContentPane().add(general_panel, constraints); + currentRow++; + constraints.gridy = currentRow; + parent.getContentPane().add(new JLabel(" "), constraints); // Add a separator between last checkboxes and button + + currentRow++; String buttonText = "Start"; if (parent.getClient() != null) { if (parent.getClient().isRunning()) { @@ -410,20 +531,38 @@ else if (method == ComputeType.GPU) { constraints.gridx = 0; constraints.gridy = currentRow; parent.getContentPane().add(saveButton, constraints); + + // Increase the size of the app Window to ensure it shows all the information with the Advanced Options panel opened. + parent.setSize(520,850); - if (haveAutoStarted == false && config.getAutoSignIn() && checkDisplaySaveButton()) { + if (haveAutoStarted == false && config.isAutoSignIn() && checkDisplaySaveButton()) { // auto start haveAutoStarted = true; new SaveAction().actionPerformed(null); } } - public void hideCustomTileSize(boolean hidden, boolean displayWarning) { - tileSizeValue.setVisible(hidden); - tileSizeLabel.setVisible(hidden); - if (customTileSize.isSelected() == true && displayWarning) { - JOptionPane.showMessageDialog(parent.getContentPane(), "These settings should only be changed if you are an advanced user.
Improper settings may lead to invalid and slower renders!", "Warning: Advanced Users Only!", JOptionPane.WARNING_MESSAGE); + private void buildRenderBucketSizeSlider(int maxRenderbucketSize, int selectedBucketSize) { + Hashtable renderbucketSizeTable = new Hashtable(); + + // We "take logs" to calculate the exponent to fill the slider. The logarithm, or log, of a number reflects + // what power you need to raise a certain base to in order to get that number. In this case, as we are + // offering increments of 2^n, the formula will be: + // + // log(tile size in px) + // exponent = -------------------- + // log(2) + // + int steps = (int) (Math.log(maxRenderbucketSize) / Math.log(2)); + + for (int i = 5; i <= steps; i++) { + renderbucketSizeTable.put((i - 5), new JLabel(String.format("%.0f", Math.pow(2, i)))); } + + renderbucketSize.setMinimum(0); + renderbucketSize.setMaximum(renderbucketSizeTable.size() - 1); + renderbucketSize.setLabelTable(renderbucketSizeTable); + renderbucketSize.setValue(selectedBucketSize); } public boolean checkDisplaySaveButton() { @@ -437,23 +576,33 @@ public boolean checkDisplaySaveButton() { selected = false; } - if (customTileSize.isSelected()) { - try { - Integer.parseInt(tileSizeValue.getText().replaceAll(",", "")); + saveButton.setEnabled(selected); + return selected; + } + + private void applyTheme(String theme_) { + try { + if (theme_.equals("light")) { + UIManager.setLookAndFeel(new FlatLightLaf()); } - catch (NumberFormatException e) { - selected = false; + else if (theme_.equals("dark")) { + UIManager.setLookAndFeel(new FlatDarkLaf()); } + + // Apply the new theme + FlatLaf.updateUI(); + } + catch (UnsupportedLookAndFeelException e1) { + e1.printStackTrace(); } - saveButton.setEnabled(selected); - return selected; } class ChooseFileAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent arg0) { - JOptionPane.showMessageDialog(parent.getContentPane(), "The working directory has to be dedicated directory.
Caution, everything not related to SheepIt-Renderfarm will be removed.
You should create a directory specifically for it.", "Warning: files will be removed!", JOptionPane.WARNING_MESSAGE); + @Override public void actionPerformed(ActionEvent arg0) { + JOptionPane.showMessageDialog(parent.getContentPane(), + "The working directory has to be dedicated directory.
Caution, everything not related to SheepIt-Renderfarm will be removed.
You should create a directory specifically for it.", + "Warning: files will be removed!", JOptionPane.WARNING_MESSAGE); int returnVal = cacheDirChooser.showOpenDialog(parent.getContentPane()); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = cacheDirChooser.getSelectedFile(); @@ -465,20 +614,51 @@ public void actionPerformed(ActionEvent arg0) { class CpuChangeAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { checkDisplaySaveButton(); } } class GpuChangeAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { + renderbucketSizeLabel.setVisible(false); + renderbucketSize.setVisible(false); + + int counter = 0; for (JCheckBox box : useGPUs) { + if (!box.isSelected()) { + box.setSelected(false); + } + else { + GPULister gpu; + int maxRenderbucketSize = 128; // Max default render bucket size + int recommendedBucketSize = 32; // Default recommended render bucket size + + if (useGPUs.get(counter).getGPUDevice().getType().equals("CUDA")) { + gpu = new Nvidia(); + maxRenderbucketSize = gpu.getMaximumRenderBucketSize(useGPUs.get(counter).getGPUDevice().getMemory()); + recommendedBucketSize = gpu.getRecommendedRenderBucketSize(useGPUs.get(counter).getGPUDevice().getMemory()); + } + else if (useGPUs.get(counter).getGPUDevice().getType().equals("OPENCL")) { + gpu = new OpenCL(); + maxRenderbucketSize = gpu.getMaximumRenderBucketSize(useGPUs.get(counter).getGPUDevice().getMemory()); + recommendedBucketSize = gpu.getRecommendedRenderBucketSize(useGPUs.get(counter).getGPUDevice().getMemory()); + } + + buildRenderBucketSizeSlider(maxRenderbucketSize, ((int) (Math.log(recommendedBucketSize) / Math.log(2))) - 5); + + renderbucketSizeLabel.setVisible(true); + renderbucketSize.setVisible(true); + } + + // Simulate a radio button behavior with check buttons while only 1 GPU + // can be selected at a time if (box.equals(e.getSource()) == false) { box.setSelected(false); } + + counter++; } checkDisplaySaveButton(); } @@ -486,27 +666,22 @@ public void actionPerformed(ActionEvent e) { class AutoSignInChangeAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { if (autoSignIn.isSelected()) { saveFile.setSelected(true); } } } - class TileSizeChange implements ActionListener { - - @Override - public void actionPerformed(ActionEvent e) { - boolean custom = customTileSize.isSelected(); - hideCustomTileSize(custom, true); + class ApplyThemeAction implements ActionListener { + @Override public void actionPerformed(ActionEvent e) { + applyTheme(themeOptionsGroup.getSelection().getActionCommand()); } } class SaveAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { if (parent == null) { return; } @@ -516,6 +691,9 @@ public void actionPerformed(ActionEvent e) { return; } + if (themeOptionsGroup.getSelection().getActionCommand() != null) + config.setTheme(themeOptionsGroup.getSelection().getActionCommand()); + if (cacheDir != null) { File fromConfig = config.getStorageDir(); if (fromConfig != null && fromConfig.getAbsolutePath().equals(cacheDir.getAbsolutePath()) == false) { @@ -546,7 +724,12 @@ else if (useCPU.isSelected() && selected_gpu != null) { config.setComputeMethod(method); if (selected_gpu != null) { - config.setUseGPU(selected_gpu); + config.setGPUDevice(selected_gpu); + } + + int renderbucket_size = -1; + if (renderbucketSize != null) { + renderbucket_size = (int) Math.pow(2, (renderbucketSize.getValue() + 5)); } int cpu_cores = -1; @@ -555,10 +738,10 @@ else if (useCPU.isSelected() && selected_gpu != null) { } if (cpu_cores > 0) { - config.setUseNbCores(cpu_cores); + config.setNbCores(cpu_cores); } - int max_ram = -1; + long max_ram = -1; if (ram != null) { max_ram = ram.getValue(); } @@ -569,7 +752,7 @@ else if (useCPU.isSelected() && selected_gpu != null) { int max_rendertime = -1; if (renderTime != null) { - max_rendertime = (Integer)renderTime.getValue() * 60; + max_rendertime = (Integer) renderTime.getValue() * 60; config.setMaxRenderTime(max_rendertime); } @@ -588,26 +771,10 @@ else if (useCPU.isSelected() && selected_gpu != null) { } } - String tile = null; - if (customTileSize.isSelected() && tileSizeValue != null) { - try { - tile = tileSizeValue.getText().replaceAll(",", ""); - config.setTileSize(Integer.parseInt(tile)); - } - catch (NumberFormatException e1) { - System.err.println("Error: wrong tile format"); - System.err.println(e1); - System.exit(2); - } - } - else { - config.setTileSize(-1); // no tile - } - parent.setCredentials(login.getText(), new String(password.getPassword())); String cachePath = null; - if (config.getUserHasSpecifiedACacheDir() && config.getCacheDirForSettings() != null) { + if (config.isUserHasSpecifiedACacheDir() && config.getCacheDirForSettings() != null) { cachePath = config.getCacheDirForSettings().getAbsolutePath(); } @@ -617,7 +784,20 @@ else if (useCPU.isSelected() && selected_gpu != null) { } if (saveFile.isSelected()) { - new SettingsLoader(config.getConfigFilePath(), login.getText(), new String(password.getPassword()), proxyText, hostnameText, method, selected_gpu, cpu_cores, max_ram, max_rendertime, cachePath, autoSignIn.isSelected(), GuiSwing.type, tile, priority.getValue()).saveFile(); + parent.setSettingsLoader( + new SettingsLoader(config.getConfigFilePath(), login.getText(), new String(password.getPassword()), proxyText, hostnameText, method, + selected_gpu, renderbucket_size, cpu_cores, max_ram, max_rendertime, cachePath, autoSignIn.isSelected(), useSysTray.isSelected(), + GuiSwing.type, themeOptionsGroup.getSelection().getActionCommand(), priority.getValue())); + + // wait for successful authentication (to store the public key) + // or do we already have one? + if (parent.getClient().getServer().getServerConfig() != null && parent.getClient().getServer().getServerConfig().getPublickey() != null) { + parent.getSettingsLoader().saveFile(); + } + + if (useSysTrayPrevState != useSysTray.isSelected()) { + JOptionPane.showMessageDialog(null, "You must restart the SheepIt app for the SysTray change to take effect"); + } } } } @@ -637,17 +817,14 @@ public GPUDevice getGPUDevice() { public class CheckCanStart implements KeyListener { - @Override - public void keyPressed(KeyEvent arg0) { + @Override public void keyPressed(KeyEvent arg0) { } - @Override - public void keyReleased(KeyEvent arg0) { + @Override public void keyReleased(KeyEvent arg0) { checkDisplaySaveButton(); } - @Override - public void keyTyped(KeyEvent arg0) { + @Override public void keyTyped(KeyEvent arg0) { } } diff --git a/src/com/sheepit/client/standalone/swing/activity/Working.java b/src/com/sheepit/client/standalone/swing/activity/Working.java index 15e05056..dea04405 100644 --- a/src/com/sheepit/client/standalone/swing/activity/Working.java +++ b/src/com/sheepit/client/standalone/swing/activity/Working.java @@ -2,7 +2,7 @@ * Copyright (C) 2015 Laurent CLOUET * Author Laurent CLOUET * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -27,9 +27,9 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; +import java.awt.Image; +import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.text.DecimalFormat; import java.util.Date; @@ -39,17 +39,17 @@ import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; +import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.UIManager; import javax.swing.Spring; import javax.swing.SpringLayout; import com.sheepit.client.Client; import com.sheepit.client.Job; -import com.sheepit.client.RenderProcess; -import com.sheepit.client.Server; +import com.sheepit.client.Log; import com.sheepit.client.Stats; import com.sheepit.client.Utils; -import com.sheepit.client.os.OS; import com.sheepit.client.standalone.GuiSwing; import com.sheepit.client.standalone.GuiSwing.ActivityType; @@ -57,6 +57,7 @@ public class Working implements Activity { private GuiSwing parent; private JLabel statusContent; + private String previousStatus; private JLabel renderedFrameContent; private JLabel remainingFrameContent; private JLabel lastRenderTime; @@ -69,15 +70,19 @@ public class Working implements Activity { private JLabel currrent_project_progression_value; private JLabel current_project_compute_method_value; private JLabel user_info_points_total_value; + private JLabel renderable_projects_value; private JLabel waiting_projects_value; private JLabel connected_machines_value; private JLabel user_info_total_rendertime_this_session_value; + private JLabel userInfoQueuedUploadsAndSizeValue; + private String currentTheme; + private Log log; public Working(GuiSwing parent_) { parent = parent_; statusContent = new JLabel("Init"); - renderedFrameContent = new JLabel(""); + renderedFrameContent = new JLabel("0"); remainingFrameContent = new JLabel(""); creditEarned = new JLabel(""); current_project_name_value = new JLabel(""); @@ -85,15 +90,47 @@ public Working(GuiSwing parent_) { currrent_project_progression_value = new JLabel(""); current_project_compute_method_value = new JLabel(""); user_info_points_total_value = new JLabel(""); + renderable_projects_value = new JLabel(""); waiting_projects_value = new JLabel(""); connected_machines_value = new JLabel(""); user_info_total_rendertime_this_session_value = new JLabel(""); lastRenderTime = new JLabel(""); lastRender = new JLabel(""); + userInfoQueuedUploadsAndSizeValue = new JLabel("0"); + currentTheme = UIManager.getLookAndFeel().getName(); // Capture the theme on component instantiation + previousStatus = ""; + log = Log.getInstance(parent_.getConfiguration()); } - @Override - public void show() { + @Override public void show() { + // If the stored theme and the UIManager's theme doesn't match is bc the user has changed it + if (!currentTheme.equals(UIManager.getLookAndFeel().getName())) { + // And, as the user has changed the theme, then we must recreate all the UI elements with session data + // Reason being they are defined as class variables and therefore created once when the object + // is created the first time. + // As the Java swing engine applies the "look & feel" at creation time, we need to "re-create" the + // objects to ensure they have the right theme colors. + statusContent = new JLabel(statusContent.getText()); + renderedFrameContent = new JLabel(renderedFrameContent.getText()); + remainingFrameContent = new JLabel(remainingFrameContent.getText()); + creditEarned = new JLabel(creditEarned.getText()); + current_project_name_value = new JLabel(current_project_name_value.getText()); + current_project_duration_value = new JLabel(current_project_duration_value.getText()); + currrent_project_progression_value = new JLabel(currrent_project_progression_value.getText()); + current_project_compute_method_value = new JLabel(current_project_compute_method_value.getText()); + user_info_points_total_value = new JLabel(user_info_points_total_value.getText()); + renderable_projects_value = new JLabel(renderable_projects_value.getText()); + waiting_projects_value = new JLabel(waiting_projects_value.getText()); + connected_machines_value = new JLabel(connected_machines_value.getText()); + user_info_total_rendertime_this_session_value = new JLabel(user_info_total_rendertime_this_session_value.getText()); + lastRenderTime = new JLabel(lastRenderTime.getText()); + lastRender = new JLabel(lastRender.getText()); + userInfoQueuedUploadsAndSizeValue = new JLabel(userInfoQueuedUploadsAndSizeValue.getText()); + + // set the new theme as the current one + currentTheme = UIManager.getLookAndFeel().getName(); + } + // current project JPanel current_project_panel = new JPanel(new SpringLayout()); current_project_panel.setBorder(BorderFactory.createTitledBorder("Project")); @@ -125,7 +162,9 @@ public void show() { JLabel user_info_credits_this_session = new JLabel("Points earned: ", JLabel.TRAILING); JLabel user_info_total_rendertime_this_session = new JLabel("Duration: ", JLabel.TRAILING); + JLabel user_info_pending_uploads_and_size = new JLabel("Queued uploads: ", JLabel.TRAILING); JLabel user_info_rendered_frame_this_session = new JLabel("Rendered frames: ", JLabel.TRAILING); + JLabel global_static_renderable_project = new JLabel("Renderable projects: ", JLabel.TRAILING); session_info_panel.add(user_info_credits_this_session); session_info_panel.add(creditEarned); @@ -133,6 +172,12 @@ public void show() { session_info_panel.add(user_info_rendered_frame_this_session); session_info_panel.add(renderedFrameContent); + session_info_panel.add(user_info_pending_uploads_and_size); + session_info_panel.add(userInfoQueuedUploadsAndSizeValue); + + session_info_panel.add(global_static_renderable_project); + session_info_panel.add(renderable_projects_value); + session_info_panel.add(user_info_total_rendertime_this_session); session_info_panel.add(user_info_total_rendertime_this_session_value); @@ -142,7 +187,7 @@ public void show() { JLabel global_stats_machine_connected = new JLabel("Machines connected: ", JLabel.TRAILING); JLabel global_stats_remaining_frame = new JLabel("Remaining frames: ", JLabel.TRAILING); - JLabel global_stats_waiting_project = new JLabel("Remaining projects: ", JLabel.TRAILING); + JLabel global_stats_waiting_project = new JLabel("Active projects: ", JLabel.TRAILING); JLabel global_stats_user_points = new JLabel("User's points: ", JLabel.TRAILING); global_stats_panel.add(global_stats_waiting_project); @@ -160,14 +205,14 @@ public void show() { // last frame JPanel last_frame_panel = new JPanel(); last_frame_panel.setLayout(new BoxLayout(last_frame_panel, BoxLayout.Y_AXIS)); - last_frame_panel.setBorder(BorderFactory.createTitledBorder("Last rendered frame")); + last_frame_panel.setBorder(BorderFactory.createTitledBorder("Last uploaded frame")); lastRender.setIcon(new ImageIcon(new BufferedImage(200, 120, BufferedImage.TYPE_INT_ARGB))); lastRender.setAlignmentX(Component.CENTER_ALIGNMENT); lastRenderTime.setAlignmentX(Component.CENTER_ALIGNMENT); last_frame_panel.add(lastRenderTime); last_frame_panel.add(lastRender); - ImageIcon image = new ImageIcon(getClass().getResource("/title.png")); + ImageIcon image = new ImageIcon(getClass().getResource("/sheepit-logo.png")); JLabel labelImage = new JLabel(image); labelImage.setAlignmentX(Component.CENTER_ALIGNMENT); parent.getContentPane().add(labelImage); @@ -177,10 +222,10 @@ public void show() { JButton settingsButton = new JButton("Settings"); settingsButton.addActionListener(new SettingsAction()); - pauseButton = new JButton("Pause"); + pauseButton = new JButton("Pause getting new jobs"); Client client = parent.getClient(); if (client != null && client.isSuspended()) { - pauseButton.setText("Resume"); + pauseButton.setText("Resume getting jobs"); } pauseButton.addActionListener(new PauseAction()); @@ -188,7 +233,7 @@ public void show() { JButton blockJob = new JButton("Block this project"); blockJob.addActionListener(new blockJobAction()); - exitAfterFrame = new JButton("Exit after this frame"); + exitAfterFrame = new JButton("Exit"); exitAfterFrame.addActionListener(new ExitAfterAction()); buttonsPanel.add(settingsButton); @@ -202,22 +247,44 @@ public void show() { global_constraints.weightx = 1; global_constraints.gridx = 0; + parent.getContentPane().add(new JLabel(" "), global_constraints); // Add a separator between logo and first panel parent.getContentPane().add(current_project_panel, global_constraints); parent.getContentPane().add(global_stats_panel, global_constraints); parent.getContentPane().add(session_info_panel, global_constraints); parent.getContentPane().add(last_frame_panel, global_constraints); + parent.getContentPane().add(new JLabel(" "), global_constraints); // Add a separator between last panel and buttons parent.getContentPane().add(buttonsPanel, global_constraints); Spring widthLeftColumn = getBestWidth(current_project_panel, 4, 2); widthLeftColumn = Spring.max(widthLeftColumn, getBestWidth(global_stats_panel, 4, 2)); - widthLeftColumn = Spring.max(widthLeftColumn, getBestWidth(session_info_panel, 3, 2)); + widthLeftColumn = Spring.max(widthLeftColumn, getBestWidth(session_info_panel, 4, 2)); alignPanel(current_project_panel, 5, 2, widthLeftColumn); alignPanel(global_stats_panel, 4, 2, widthLeftColumn); - alignPanel(session_info_panel, 3, 2, widthLeftColumn); + alignPanel(session_info_panel, 5, 2, widthLeftColumn); + + // Set the proper size for the Working (if coming from Settings screen, the window size will be too big for the content!) + parent.setSize(520, 760); } public void setStatus(String msg_) { - statusContent.setText("" + msg_ + ""); + setStatus(msg_, false); + } + + public void setStatus(String msg_, boolean overwriteSuspendedMsg) { + Client client = parent.getClient(); + + if (!msg_.equals("")) { + this.previousStatus = msg_; + } + + if (client != null && client.isSuspended()) { + if (overwriteSuspendedMsg) { + statusContent.setText("" + msg_ + ""); + } + } + else { + statusContent.setText("" + msg_ + ""); + } } public void setRenderingProjectName(String msg_) { @@ -241,18 +308,41 @@ public void displayStats(Stats stats) { remainingFrameContent.setText(df.format(stats.getRemainingFrame())); creditEarned.setText(df.format(stats.getCreditsEarnedDuringSession())); user_info_points_total_value.setText(df.format(stats.getCreditsEarned())); + renderable_projects_value.setText(df.format(stats.getRenderableProject())); waiting_projects_value.setText(df.format(stats.getWaitingProject())); connected_machines_value.setText(df.format(stats.getConnectedMachine())); + updateTime(); } + public void displayUploadQueueStats(int queueSize, long queueVolume) { + userInfoQueuedUploadsAndSizeValue.setText( + String.format("%d%s%s", queueSize, (queueSize > 0 ? String.format(" (%.2fMB) ", (queueVolume / 1024.0 / 1024.0)) : ""), + (queueSize == this.parent.getConfiguration().getMaxUploadingJob() ? "- Queue full!" : ""))); + + // If the user has requested to exit, then we need to update the JButton with the queue size + if (this.exitAfterFrame.getText().startsWith("Cancel")) { + Client client = parent.getClient(); + + if (client != null) { + if (client.isRunning()) { + queueSize++; + } + } + + exitAfterFrame.setText(String.format("Cancel exit (%s frame%s to go)", queueSize, (queueSize > 1 ? "s" : ""))); + } + } + public void updateTime() { if (this.parent.getClient().getStartTime() != 0) { - user_info_total_rendertime_this_session_value.setText(Utils.humanDuration(new Date((new Date().getTime() - this.parent.getClient().getStartTime())))); + user_info_total_rendertime_this_session_value + .setText(Utils.humanDuration(new Date((new Date().getTime() - this.parent.getClient().getStartTime())))); } Job job = this.parent.getClient().getRenderingJob(); if (job != null && job.getProcessRender() != null && job.getProcessRender().getStartTime() > 0) { - current_project_duration_value.setText("" + Utils.humanDuration(new Date((new Date().getTime() - job.getProcessRender().getStartTime()))) + ""); + current_project_duration_value + .setText("" + Utils.humanDuration(new Date((new Date().getTime() - job.getProcessRender().getStartTime()))) + ""); } else { current_project_duration_value.setText(""); @@ -268,26 +358,43 @@ public void showLastRender() { Client client = parent.getClient(); if (client != null) { Job lastJob = client.getPreviousJob(); - Server server = client.getServer(); - if (server != null) { - byte[] data = server.getLastRender(); - if (data != null) { - InputStream is = new ByteArrayInputStream(data); + if (lastJob != null) { + ImageIcon icon = null; + int idInt = Integer.parseInt(lastJob.getId()); + if (idInt == 1) { + icon = new ImageIcon(getClass().getResource("/frame_compute_method.jpg")); + } + else if (idInt < 20) { + icon = new ImageIcon(getClass().getResource("/frame_power_detection.jpg")); + } + else { try { - BufferedImage image = ImageIO.read(is); - if (image != null) { - lastRender.setIcon(new ImageIcon(image)); - if (lastJob != null) { - // don't use lastJob.getProcessRender().getDuration() due to timezone - if (lastJob.getProcessRender().getDuration() > 1) { - lastRenderTime.setText("Render time : " + Utils.humanDuration(new Date(lastJob.getProcessRender().getEndTime() - lastJob.getProcessRender().getStartTime()))); - } - } + String path = lastJob.getOutputImagePath(); + + BufferedImage img = ImageIO.read(new File(path)); + float width = img.getWidth(); + float height = img.getHeight(); + float factor = 1.0f; + if (height > 200) { + factor = 200f / height; } + if (width * factor > 200) { + factor = Math.min(factor, 200f / width); + } + icon = new ImageIcon(img.getScaledInstance((int) (width * factor), (int) (height * factor), Image.SCALE_FAST)); } catch (IOException e) { - System.out.println("Working::showLastRender() exception " + e); - e.printStackTrace(); + log.error(String.format("Working::showLastRender() Unable to load/preview rendered frame [%s]. Exception %s", + lastJob.getOutputImagePath(), e.getMessage())); + } + } + + if (icon != null) { + lastRender.setIcon(icon); + // don't use lastJob.getProcessRender().getDuration() due to timezone + if (lastJob.getProcessRender().getDuration() > 1) { + lastRenderTime.setText("Render time : " + Utils + .humanDuration(new Date(lastJob.getProcessRender().getEndTime() - lastJob.getProcessRender().getStartTime()))); } } } @@ -355,25 +462,25 @@ private SpringLayout.Constraints getConstraintsForCell(int row, int col, Contain class PauseAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { Client client = parent.getClient(); if (client != null) { if (client.isSuspended()) { - pauseButton.setText("Pause"); + pauseButton.setText("Pause getting new jobs"); client.resume(); + setStatus(previousStatus); } else { - pauseButton.setText("Resume"); + pauseButton.setText("Resume getting jobs"); client.suspend(); + setStatus(""); } } } } class SettingsAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { if (parent != null) { parent.showActivity(ActivityType.SETTINGS); } @@ -381,16 +488,32 @@ public void actionPerformed(ActionEvent e) { } class ExitAfterAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { Client client = parent.getClient(); if (client != null) { if (client.isRunning()) { - exitAfterFrame.setText("Cancel exit"); - client.askForStop(); + String[] exitJobOptions = { "Exit after current Jobs", "Exit Immediately", "Do Nothing" }; + int jobsQueueSize = client.getUploadQueueSize() + (client.isRunning() ? 1 : 0); + + int userDecision = JOptionPane.showOptionDialog(null, String.format( + "You have %d frame%s being uploaded or rendered. Do you want to finish the jobs or exit now?.\n\n", + jobsQueueSize, // Add the current frame to the total count ONLY if the client is running + (jobsQueueSize > 1 ? "s" : ""), (jobsQueueSize > 1 ? (jobsQueueSize + " ") : ""), (jobsQueueSize > 1 ? "s" : "")), + "Exit Now or Later", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, exitJobOptions, + exitJobOptions[2]); // Make the "Do nothing" button the default one to avoid mistakes + + if (userDecision == 0) { + exitAfterFrame.setText(String.format("Cancel exit (%s frame%s to go)", jobsQueueSize, (jobsQueueSize > 1 ? "s" : ""))); + + client.askForStop(); + } + else if (userDecision == 1) { + client.stop(); + System.exit(0); + } } else { - exitAfterFrame.setText("Exit after this frame"); + exitAfterFrame.setText("Exit"); client.cancelStop(); } } @@ -398,8 +521,7 @@ public void actionPerformed(ActionEvent e) { } class blockJobAction implements ActionListener { - @Override - public void actionPerformed(ActionEvent e) { + @Override public void actionPerformed(ActionEvent e) { Client client = parent.getClient(); if (client != null) { Job job = client.getRenderingJob(); @@ -409,5 +531,4 @@ public void actionPerformed(ActionEvent e) { } } } - } diff --git a/src/com/sheepit/client/standalone/swing/components/CollapsibleJPanel.java b/src/com/sheepit/client/standalone/swing/components/CollapsibleJPanel.java new file mode 100644 index 00000000..facd97f5 --- /dev/null +++ b/src/com/sheepit/client/standalone/swing/components/CollapsibleJPanel.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2015 Laurent CLOUET + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; version 2 + * of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package com.sheepit.client.standalone.swing.components; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.LayoutManager; +import java.awt.event.MouseListener; +import java.awt.event.MouseEvent; +import javax.swing.BorderFactory; + +import javax.swing.JPanel; +import javax.swing.border.Border; +import javax.swing.border.TitledBorder; + +public class CollapsibleJPanel extends JPanel { + + private boolean isCompnentsVisible = true; + private int originalHeight; + private String borderTitle = ""; + private int COLLAPSED_HEIGHT = 22; + private boolean[] originalVisibilty; + + public CollapsibleJPanel(LayoutManager layoutManager) { + setLayout(layoutManager); + addMouseListener(new onClickHandler()); + } + + public void setCollapsed(boolean aFlag) { + if (aFlag) { + hideComponents(); + } + else { + showComponents(); + } + } + + public void toggleCollapsed() { + if (isCompnentsVisible) { + setCollapsed(true); + } + else { + setCollapsed(false); + } + } + + private void hideComponents() { + + Component[] components = getComponents(); + + originalVisibilty = new boolean[components.length]; + + // Hide all componens on panel + for (int i = 0; i < components.length; i++) { + originalVisibilty[i] = components[i].isVisible(); + components[i].setVisible(false); + } + + setHeight(COLLAPSED_HEIGHT); + + // Add '+' char to end of border title + //setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(), borderTitle + " + ")); + setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), " + " + borderTitle)); + + // Update flag + isCompnentsVisible = false; + } + + private void showComponents() { + + Component[] components = getComponents(); + + // Set all components in panel to visible + for (int i = 0; i < components.length; i++) { + components[i].setVisible(originalVisibilty[i]); + } + + setHeight(originalHeight); + + // Add '-' char to end of border title + setBorder(BorderFactory.createTitledBorder(" - " + borderTitle)); + + // Update flag + isCompnentsVisible = true; + } + + private void setHeight(int height) { + setPreferredSize(new Dimension(getPreferredSize().width, height)); + setMinimumSize(new Dimension(getMinimumSize().width, height)); + setMaximumSize(new Dimension(getMaximumSize().width, height)); + } + + @Override public Component add(Component component) { // Need this to get the original height of panel + + Component returnComponent = super.add(component); + + originalHeight = getPreferredSize().height; + + return returnComponent; + } + + @Override public void setBorder(Border border) { // Need this to get the border title + + if (border instanceof TitledBorder && (borderTitle == "")) { + borderTitle = ((TitledBorder) border).getTitle(); + + ((TitledBorder) border).setTitle(" - " + borderTitle); + } + + super.setBorder(border); + } + + public class onClickHandler implements MouseListener { + + @Override public void mouseClicked(MouseEvent e) { + } + + @Override public void mousePressed(MouseEvent e) { + if (e.getPoint().y < COLLAPSED_HEIGHT) { // Only if click is on top of panel + ((CollapsibleJPanel) e.getComponent()).toggleCollapsed(); + } + } + + @Override public void mouseEntered(MouseEvent e) { + } + + @Override public void mouseExited(MouseEvent e) { + } + + @Override public void mouseReleased(MouseEvent e) { + } + + } +} diff --git a/src/com/sheepit/client/standalone/text/CLIInputActionHandler.java b/src/com/sheepit/client/standalone/text/CLIInputActionHandler.java index f5d4d84c..039bb69b 100644 --- a/src/com/sheepit/client/standalone/text/CLIInputActionHandler.java +++ b/src/com/sheepit/client/standalone/text/CLIInputActionHandler.java @@ -2,7 +2,7 @@ * Copyright (C) 2017 Laurent CLOUET * Author Rolf Aretz Lap * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. @@ -25,8 +25,7 @@ public class CLIInputActionHandler implements CLIInputListener { - @Override - public void commandEntered(Client client, String command) { + @Override public void commandEntered(Client client, String command) { int priorityLength = "priority".length(); //prevent Null Pointer at next step @@ -89,7 +88,7 @@ void changePriority(Client client, String newPriority) { } } - + void displayStatus(Client client) { if (client.isSuspended()) { System.out.println("Status: paused"); diff --git a/src/com/sheepit/client/standalone/text/CLIInputListener.java b/src/com/sheepit/client/standalone/text/CLIInputListener.java index 89a67b81..090aacd6 100644 --- a/src/com/sheepit/client/standalone/text/CLIInputListener.java +++ b/src/com/sheepit/client/standalone/text/CLIInputListener.java @@ -2,7 +2,7 @@ * Copyright (C) 2017 Laurent CLOUET * Author Rolf Aretz Lap * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License. diff --git a/src/com/sheepit/client/standalone/text/CLIInputObserver.java b/src/com/sheepit/client/standalone/text/CLIInputObserver.java index ea70f5fd..593e3ec4 100644 --- a/src/com/sheepit/client/standalone/text/CLIInputObserver.java +++ b/src/com/sheepit/client/standalone/text/CLIInputObserver.java @@ -2,7 +2,7 @@ * Copyright (C) 2017 Laurent CLOUET * Author Rolf Aretz Lap * - * This program is free software; you can redistribute it and/or + * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License.