diff --git a/CommonResources b/CommonResources index 71abd13..05a9a30 100755 --- a/CommonResources +++ b/CommonResources @@ -9,7 +9,6 @@ setupHelperDir="/data/SetupHelper" source "$setupHelperDir/EssentialResources" source "$setupHelperDir/LogHandler" source "$setupHelperDir/ServiceResources" -source "$setupHelperDir/UpdateResources" source "$setupHelperDir/DbusSettingsResources" # what action the script should take: @@ -36,8 +35,6 @@ runAgain=false filesUpdated=false restartGui=false - - # yesNoPrompt provides user prompting requesting a yes/no response # # $1 is the prompt displayed when pausing for user input @@ -84,11 +81,6 @@ yesNoPrompt () standardActionPrompt () { - if [ -f "$setupOptionsDir/optionsSet" ]; then - enableReinstall=true - else - enableReinstall=false - fi if [ $# -gt 0 ] && [ $1 == 'MORE_PROMPTS' ]; then updateScriptAction=false else @@ -98,7 +90,7 @@ standardActionPrompt () echo echo "Available actions:" echo " Install and activate (i)" - if $enableReinstall ; then + if $optionsSet ; then echo " Reinstall (r) based on options provided at last install" fi echo " Uninstall (u) and restores all files to stock" @@ -120,7 +112,7 @@ standardActionPrompt () break ;; [rR]*) - if $enableReinstall ; then + if $optionsSet ; then scriptAction='INSTALL' break fi @@ -130,7 +122,7 @@ standardActionPrompt () break ;; [qQ]*) - exit + exit $EXIT_SUCCESS ;; [lL]*) displayLog $packageLogFile @@ -298,6 +290,11 @@ _checkFileSets () return fi + # if incomplete file set exists - remove it and try again + if [ -f "$fileSet/INCOMPLETE" ]; then + rm -rf $fileSet + fi + # attempt to create file set if it doesn't exist if [ ! -d "$fileSet" ]; then logMessage "creating file set for $venusVersion" @@ -310,7 +307,7 @@ _checkFileSets () local baseName="" local file="" - rm -f "$pkgFileSets/INCOMPLETE" + rm -f "$fileSet/INCOMPLETE" for file in $fileList ; do baseName=$(basename "$file") @@ -369,6 +366,9 @@ _checkFileSets () done if $matchFound ;then + if [ -e "$fileSet/$baseName.orig" ]; then + rm -rf "$fileSet/$baseName.orig" + fi ln -s "../$otherVersion/$baseName.orig" "$fileSet/$baseName.orig" rm -f "$fileSet/$baseName.NO_ORIG" # if other file set contains a replacement file, link to it @@ -395,7 +395,7 @@ _checkFileSets () if [ -f "$fileSet/INCOMPLETE" ]; then logMessage "ERROR: incomplete file set for $venusVersion - can't continue" - exit + exit $EXIT_FILE_SET_ERROR fi } @@ -405,14 +405,9 @@ _checkFileSets () endScript () { - if $restartGui $$ !rebootNeeded ; then - logMessage "restarting GUI" - svc -t /service/gui - fi - if [ $scriptAction == 'INSTALL' ] ; then - # indicate installation was successful, so reinstalls can use the existing options in the future - touch "$setupOptionsDir/optionsSet" + # assume that if we get this far, any command line opitons have already been set + touch "$setupOptionsDir/optionsSet" # set up reinstallMods to run this script again after a VenusOS update if [ ! -f "$reinstallScriptsList" ] || [ $(grep -c "$fullScriptName" "$reinstallScriptsList") == 0 ]; then @@ -432,33 +427,15 @@ endScript () sed -i -e 's?/data/SetupHelper?nohup /data/SetupHelper?' -e 's?reinstallMods?reinstallMods > /dev/null \&?' "$rcLocal" fi - # installed flag is removed if script needs to run again + # if script needs to run again, installedVersionFile flag file is removed + # script should run again at boot time via reinstallMods if $runAgain ; then logMessage "script will run again at startup" - rm -f "$installedFlag" - # otherwise installed flag is set so script won't be run again at boot + rm -f "$installedVersionFile" + # otherwise, installation is complete - update installedVersion else - cp "$scriptDir/version" "$installedFlag" - fi - - # add package to packageList if not already there and the GitHub paths have been specified - if [ ! -z $packageGitHubUser ] && [ ! -z $packageGitHubBranch ]; then - # move from previous locaiton - oldPackageListFile="$setupOptionsRoot/SetupHelper/packageList" - if [ -f "$oldPackageListFile" ]; then - mv "$oldPackageListFile" "$packageListFile" - fi - - if [ ! -f "$packageListFile" ] || [ $(grep -c "^$packageName\s" "$packageListFile") == 0 ]; then - logMessage "adding $packageName to SetupHelper package list" - echo "$packageName $packageGitHubUser $packageGitHubBranch" >> "$packageListFile" - fi - fi - - # update package version for the gui - takes time so do in background - nohup "/data/SetupHelper/updatePackageVersions" > /dev/null & - - + cp "$scriptDir/version" "$installedVersionFile" + fi elif [ $scriptAction == 'UNINSTALL' ] ; then # remove this script from reinstallScriptsList to prevent further calls during boot if [ -f "$reinstallScriptsList" ] && [ ! $(grep -c "$fullScriptName" "$reinstallScriptsList") == 0 ]; then @@ -468,82 +445,100 @@ endScript () fi # clean up only - flag not used since package is being removed - rm -f "$installedFlag" - - # update package version for the gui - takes time so do in background - nohup "/data/SetupHelper/updatePackageVersions" > /dev/null & + rm -f "$installedFlag" # obsolete but remove it anyway + rm -f "$installedVersionFile" + else + logMessage "unexpected script action $scriptAction - did not install or uninstall" fi - # this script was called from reinstallMods - # set exit code based on actual code - if $runningAtBoot ; then - if $rebootNeeded ; then - logMessage "reboot pending" - exit $exitReboot - else - logMessage "completed" - exit $exitSuccess - fi - - # this script was run manually - else - # if reboot needed ask user if it should be done now - if $rebootNeeded ; then - if $deferReboot ; then - exit $exitReboot - else - yesNoPrompt "Reboot system now (y) or issue a reboot manually later (n): " - if $yesResponse ; then - echo "rebooting ..." - reboot - else - echo "system must be rebooted to finish installation and activate components" - fi - fi - - elif $runAgain ; then - echo "$shortScriptName NOT completed" - echo " run it again manually to finish" - echo " or reboot the system to finish automatically" - exit $exitSuccess - else - logMessage "completed" - exit $exitSuccess - fi - fi + # prioritize exit + if $versionNotCompatible ; then + logMessage "version not compatible - exiting" + exit $EXIT_INCOMPATIBLE_VERSION + elif $platformNotCompatible ; then + logMessage "platform not compatible - exiting" + exit $EXIT_INCOMPATIBLE_PLATFOM + elif $rebootNeeded ; then + if $logToConsole ; then + yesNoPrompt "Reboot system now (y) or do it manually later (n): " + if $yesResponse ; then + echo "rebooting ..." + reboot + else + echo "system must be rebooted to finish installation and activate components" + exit $EXIT_REBOOT + fi + else + echo "rebooting needed" + exit $EXIT_REBOOT + fi + elif $restartGui ; then + if $logToConsole ; then + yesNoPrompt "Restart the GUI now (y) or issue a do it manually later (n): " + if $yesResponse ; then + echo "restarting GUI ..." + svc -t /service/gui + exit $EXIT_SUCCESS + else + echo "GUI must be restarted activate components" + exit $EXIT_RESTART_GUI + fi + else + if $deferGuiRestart ; then + logMessage "GUI restart needed" + exit $EXIT_RESTART_GUI + # GUI restart NOT deferred - do it now + else + svc -t /service/gui + exit $EXIT_SUCCESS + fi + fi + else + logMessage "completed" + exit $EXIT_SUCCESS + fi } ######## this code is executed in-line when CommonResources is sourced # check for reinstall parameter # set $scriptAction to control work following the source command -# if "force" is also provided on the command line, then the installedFlag is not checked -# installedFlag contains the installed version (if any) +# if "force" is also provided on the command line, then the installedVersionFile is not checked +# installedVersionFile contains the installed version (if any) # it is compared to the version file in the package directory -# if installedFlag is missing or contents are different, the installation will proceed -# if the two versions matched, there is no need to reinstall the package +# if installedVersionFile is missing or contents are different, the installation will proceed +# if the two versions match, there is no need to reinstall the package # we assume a reinstall is always run without benefit of a console (runningAtBoot will be true) # so there will be no prompts and all actions will be automatic # -# "deferReboot" signals that endScript should not prompt for a user reboot, but return exitReboot -# This is used by the manual package install called from SetupHelper setup script +# "deferReboot" signals that endScript should not reboot the system, but return EXIT_REBOOT +# assuming the caller will evenutally reboot the system +# +# "deferGuiRestart" is similar for restarting the GUI # +# "install" causes the package to be installed silently # "uninstall" causes the package to be uninstalled silently # # command line parameters may appear in any order - -# make sure rootfs is mounted R/W -if [ -f /opt/victronenergy/swupdate-scripts/remount-rw.sh ]; then - /opt/victronenergy/swupdate-scripts/remount-rw.sh -fi - +# +# +# logToConsole is set to true in the LogHandler script +# It is set to false here if the command line parameters suggest +# the setup script is being run without the aid of the command line interface +# in which case the user will not be prompted and apprporiate actions are taken automatically +# exit code returned by the setup script indicates additional actions that +# need to be taken by the caller (if any) or the reason the script could not be completed + +# collect command line options reinstall=false force=false deferReboot=false +deferGuiRestart=false while [ $# -gt 0 ]; do case $1 in $reinstallParam) reinstall=true + logToConsole=false ;; "force") force=true @@ -551,6 +546,13 @@ while [ $# -gt 0 ]; do "deferReboot") deferReboot=true ;; + "deferGuiRestart") + deferGuiRestart=true + ;; + "install") + scriptAction='INSTALL' + logToConsole=false + ;; "uninstall") scriptAction='UNINSTALL' logToConsole=false @@ -560,27 +562,79 @@ while [ $# -gt 0 ]; do shift done +# make sure rootfs is mounted R/W +if [ -f /opt/victronenergy/swupdate-scripts/remount-rw.sh ]; then + /opt/victronenergy/swupdate-scripts/remount-rw.sh +fi + +# move old installedFlag ("...inInstalled...") +# to its new name ...installedVersionFile... +if [ ! -f "$installedVersionFile" ] && [ -f "$installedFlag" ]; then + installedVersion=$(cat "$installedFlag") + if [ -z $installedVersion ]; then + installedVersion="unknown" + else + echo $installedVersion > "$installedVersionFile" + fi +fi +rm -f "$installedFlag" + +# packages that require options to proceed unattended +# must include the optionsRequried flag file in their package directory +# if the flag is present and options haven't been previously set, +# SD/USB media will be checked for the package options directory +# and copy them into position + +opitonsRequiredFile="$scriptDir/optionsRequired" +optionsSet=false +if [ -f $opitonsRequiredFile ]; then + if [ -f "$setupOptionsDir/optionsSet" ]; then + optionsSet=true + # options not set - check media for options if doing a blind install + elif [ $scriptAction == 'INSTALL' ]; then + mediaList=($(ls /media)) + for dir in ${mediaList[@]} ; do + altSetupDir="/media/$dir/"$(basename $setupOptionsRoot)"/$packageName" + if [ -f "$altSetupDir/optionsSet" ]; then + cp -r "$altSetupDir" "$setupOptionsRoot" + if [ -f "$setupOptionsDir/optionsSet" ]; then + logMessage "options retrieved from SD/USB media" + optionsSet=true + fi + break + fi + done + fi + +# no command line options are needed - ok to reinstall even if +# setup was not run from the command line +else + optionsSet=true +fi + +# called from reinstallMods at boot time if $reinstall ; then + installedVersion=$(cat "$installedVersionFile") + versionStringToNumber $installedVersion + installedVersionNumber=$versionNumber + packageVersion=$(cat "$scriptDir/version") + versionStringToNumber $packageVersion + packageVersionNumber=$versionNumber + + runningAtBoot=true if $force ; then scriptAction='INSTALL' - elif [ ! -f "$installedFlag" ]; then + elif [ ! -f "$installedVersionFile" ]; then + scriptAction='INSTALL' + # trigger install if version numbers differ + elif (( installedVersionNumber != packageVersionNumber )); then scriptAction='INSTALL' else - # compare installed version with the one in the package directory - # nothing to do - # if they are not the same, continue with install - iVer=$(cat "$installedFlag") - pVer=$(cat "$scriptDir/version") - if [ "$iVer" = "$pVer" ]; then - exit - else - scriptAction='INSTALL' - fi + exit $EXIT_SUCCESS fi - runningAtBoot=true logToConsole=false -# not a reinstall, continue with MANUAL installation +# not running from reinstallMods else runningAtBoot=false fi @@ -590,32 +644,85 @@ fi versionStringToNumber $venusVersion venusVersionNumber=$versionNumber -# check to see if package is compatible with this Venus version -notCompatible=false -if [ -f "$scriptDir/obsoleteVersion" ]; then - versionStringToNumber $(cat "$scriptDir/obsoleteVersion") - obsoleteVersion=$versionNumber +# create obsolete version file if it does not already exist +# accommodates previous mechanism that used shell varaible +if [ ! -z $obsoleteVersion ] && [ ! -f "$scriptDir/obsoleteVersion" ]; then + echo $obsoleteVersion > "$scriptDir/obsoleteVersion" +fi - if (( $venusVersionNumber >= $obsoleteVersion )); then - notCompatible=true - fi +# prevent installing Raspberry Pi packages on other platforms +platformNotCompatible=false +if [ -f "$scriptDir/raspberryPiOnly" ]; then + if [ -f /etc/venus/machine ]; then + machine=$(cat /etc/venus/machine) + fi + if [ -z $machine ]; then + if $isInstalled ; then + logMessage "can't determine Venus device type - uninstalling" + scriptAction='UNINSTALL' + else + logMessage "can't determine Venus device type - exiting" + exit $INCOMPATIBLE_PLATFORM + fi + elif [ $machine != "raspberrypi2" ] && [ $machine != "raspberrypi4" ]; then + platformNotCompatible=true + if $isInstalled ; then + logMessage "$packageName not compatible with $machine - uninstalling" + scriptAction='UNINSTALL' + else + logMessage "$packageName not compatible with $machine - exiting" + exit $EXIT_INCOMPATIBLE_PLATFOM + fi + fi fi -if $notCompatible ; then - logMessage "$packageName not compatible with Venus $venusVersion" - scriptAction='UNINSTALL' +# check to see if package is compatible with this Venus version +versionNotCompatible=false +if [ -f "$scriptDir/firstCompatibleVersion" ]; then + firstCompatibleVersion=$(cat "$scriptDir/firstCompatibleVersion") +# no first compatible version specified - use the default else - if [ ! -d "$setupOptionsRoot" ]; then - logMessage "creating root setup options directory $setupOptionsRoot" - mkdir $setupOptionsRoot - fi - - if [ ! -d "$setupOptionsDir" ]; then - logMessage "creating package options directory to $setupOptionsDir" - mkdir $setupOptionsDir - fi + firstCompatibleVersion='v2.40' +fi +versionStringToNumber $firstCompatibleVersion +firstCompatibleVersionNumber=$versionNumber +if (( $venusVersionNumber < $firstCompatibleVersionNumber )); then + versionNotCompatible=true +elif [ -f "$scriptDir/obsoleteVersion" ]; then + versionStringToNumber $(cat "$scriptDir/obsoleteVersion") + obsoleteVersionNumber=$versionNumber + if (( $venusVersionNumber >= $obsoleteVersionNumber )); then + versionNotCompatible=true + fi +fi +if $versionNotCompatible ; then + # if not installed, log message and exit + if $isInstalled ; then + logMessage "$packageName not compatible with Venus $venusVersion - can't install" + exit $EXIT_INCOMPATIBLE_VERSION + else + logMessage "$packageName not compatible with Venus $venusVersion - uninstalling" + scriptAction='UNINSTALL' + fi +else + if [ ! -d "$setupOptionsRoot" ]; then + logMessage "creating root setup options directory $setupOptionsRoot" + mkdir $setupOptionsRoot + fi + + if [ ! -d "$setupOptionsDir" ]; then + logMessage "creating package options directory to $setupOptionsDir" + mkdir $setupOptionsDir + fi fi +# attempting an install without the comand line prompting +# and needed options have not been set yet +# can't continue +if [ $scriptAction == 'INSTALL' ] && ! $optionsSet ; then + logMessage "required options have not been set - can't install" + exit $EXIT_OPTIONS_NOT_SET +fi # if forcing an uninstall, skip file set checks if [ $scriptAction != 'UNINSTALL' ]; then _checkFileSets diff --git a/EssentialResources b/EssentialResources index 70e7c22..b642bab 100755 --- a/EssentialResources +++ b/EssentialResources @@ -11,6 +11,10 @@ fullScriptName="$scriptDir/$(basename "$0")" venusVersion="$(cat /opt/victronenergy/version | head -n 1)" +installedVersionPrefix="/etc/venus/installedVersion-" +installedVersionFile="$installedVersionPrefix"$packageName + +# obsolete - use installedVersion installedFlagPrefix="/etc/venus/inInstalled-" installedFlag="$installedFlagPrefix"$packageName @@ -25,8 +29,19 @@ fileSet="$pkgFileSets/$venusVersion" rcLocal="/data/rcS.local" # defined exit codes - must be consistent between all setup scripts and reinstallMods -exitReboot=123 -exitSuccess=0 +# and PackageManager.py +EXIT_SUCCESS=0 +EXIT_REBOOT=123 +EXIT_RESTART_GUI=124 +EXIT_ERROR=255 # unknown error +EXIT_INCOMPATIBLE_VERSION=254 +EXIT_INCOMPATIBLE_PLATFOM=253 +EXIT_FILE_SET_ERROR=252 +EXIT_OPTIONS_NOT_SET=251 +EXIT_RUN_AGAIN=250 +# old variables - keep for compatibility +exitReboot=$EXIT_REBOOT +exitSuccess=$EXIT_SUCCESS reinstallParam="reinstall" @@ -40,3 +55,74 @@ packageListFile="/data/packageList" qmlDir=/opt/victronenergy/gui/qml + + +# convert a version string in the form of vX.Y~Z-large-W to an integer to make comparisions easier +# the ~Z portion indicates a pre-release version so a version without it is later than a version with it +# the -W portion is like the ~Z for large builds +# the large portion is IGNORED !!!! +# note part[0] is always null because there is nothing before v which is used as a separator +# +# the incoming verison string is split on every non-digit character +# the first three of those pieces are combined into a single integer +# additional pieces are ignored +# +# for example v1.2~3 would be 1002003 +# for example v11.22.33 would be 11022033 +# for example v11.22-large-33 would be 11022999 +# an empty file or one that contains "unknown" or does not beging with 'v' +# has a version number = 0 +# +# the separators ~, b, a and d are given special significance +# indicating beta (~ and b), alpha or develompent releases +# which are prioritized: release is the highest, then beta, then alpha then development +# by spliting the third number range into individual ranges +# a released version is given the highest numerical value: 999 +# none for release -- 999 +# ~ or b for beta 0-398 600-998 +# a for alpha 0-299 300-599 +# d for development 0-299 0-299 + +function versionStringToNumber () +{ + local version + local type + local major + local minor + local preRelease + local number + version=$( echo ${1:1} | sed -e 's/-.*//') + read major minor preRelease <<< $(echo $version | sed 's/[.abd~]/ /g') + type=$(echo $version | sed 's/[^abd~]//g') + + # assemble core version number from major, minor and preRelease parts + (( number = 0 )) + if [ ! -z $major ]; then + (( number += major * 1000000 )) + fi + if [ ! -z $minor ]; then + (( number += minor * 1000 )) + fi + if [ ! -z $preRelease ]; then + (( number += preRelease )) + fi + + # adjust number for release + if [ -z $type ] || [ $type = '' ]; then + # if no preRelease part specified, the preRelease version needs to be it's maximum + # if a preRelease part is specified, use as is + # (should never specify a preRelease for a released version !!!) + if [ -z $preRelease ] || [ $preRelease = '' ]; then + (( number += 999 )) + fi + # adjust number for alpha + elif [ $type == 'a' ]; then + (( number += 300 )) + # adjust number for beta + elif [ $type == 'b' ] || [ $type == '~' ]; then + (( number += 600 )) + fi + versionNumber=$number +} + + diff --git a/FileSets/MbDisplayDefaultPackage.qml b/FileSets/MbDisplayDefaultPackage.qml new file mode 100644 index 0000000..5fc31c5 --- /dev/null +++ b/FileSets/MbDisplayDefaultPackage.qml @@ -0,0 +1,95 @@ +//////// new for PackageManager + +import QtQuick 1.1 +import com.victron.velib 1.0 +import "utils.js" as Utils + +MbItem { + id: root + + property int defaultIndex + property string servicePrefix + + VBusItem { id: packageName; bind: getServiceBind ("PackageName") } + + + onClicked: rootWindow.pageStack.push ("/opt/victronenergy/gui/qml/PageSettingsPackageAdd.qml", {defaultIndex: defaultIndex}) + + + function getServiceBind(param) + { + return Utils.path(servicePrefix, "/Default/", defaultIndex, "/", param) + } + + + MbRowSmall + { + description: "" + + anchors.verticalCenter: parent.verticalCenter + Column + { + width: root.width - gitHubUser.width - gitHubBranch.width - 20 + Text // puts a bit of space above package name + { + text: " " + font.pixelSize: 6 + } + Text + { + text:packageName.valid ? packageName.value : "" + font.pixelSize: 14 + horizontalAlignment: Text.AlignLeft + } + Text + { + text: "" + font.pixelSize: 10 + horizontalAlignment: Text.AlignLeft + } + } + Column + { + Text // puts a bit of space above version boxes + { + text: " " + font.pixelSize: 3 + } + Text + { + text: "GitHub User" + font.pixelSize: 10 + } + MbTextBlock + { + id: gitHubUser + item { bind: getServiceBind("GitHubUser") } + height: 20; width: 120 + } + Text // puts a bit of space below version boxes - only needed in one column + { + text: " " + font.pixelSize: 6 + } + } + Column + { + Text // puts a bit of space above version boxes + { + text: " " + font.pixelSize: 3 + } + Text + { + text: qsTr ("GitHub Tag") + font.pixelSize: 10 + } + MbTextBlock + { + id: gitHubBranch + item { bind: getServiceBind("GitHubBranch") } + height: 20; width: 120 + } + } + } +} diff --git a/FileSets/MbDisplayPackageVersion.qml b/FileSets/MbDisplayPackageVersion.qml index db69fe6..eb69112 100644 --- a/FileSets/MbDisplayPackageVersion.qml +++ b/FileSets/MbDisplayPackageVersion.qml @@ -1,3 +1,5 @@ +//////// new for PackageManager + import QtQuick 1.1 import com.victron.velib 1.0 import "utils.js" as Utils @@ -5,41 +7,159 @@ import "utils.js" as Utils MbItem { id: root - property int versionIndex - property string bindPrefix + property int packageIndex + property string servicePrefix + property string settingsPrefix + + VBusItem { id: packageName; bind: getSettingsBind ("PackageName") } + property VBusItem rebootNeededItem: VBusItem { bind: getServiceBind ( "RebootNeeded") } + property VBusItem guiRestartNeededItem: VBusItem { bind: getServiceBind ( "GuiRestartNeeded") } + property bool rebootNeeded: rebootNeededItem.valid && rebootNeededItem.value == 1 + property bool guiRestartNeeded: guiRestartNeededItem.valid && guiRestartNeededItem.value == 1 + + VBusItem { id: platformItem; bind: Utils.path("com.victronenergy.packageManager", "/Platform" ) } + VBusItem { id: incompatibleItem; bind: getServiceBind ( "Incompatible" ) } + property string incompatibleReason: incompatibleItem.valid ? incompatibleItem.value : "" + property bool compatible: incompatibleReason == "" + property string platform: platformItem.valid ? platformItem.value : "??" + + onClicked: rootWindow.pageStack.push ("/opt/victronenergy/gui/qml/PageSettingsPackageEdit.qml", {packageIndex: packageIndex}) - function getBind(param) + + function statusText () { - return Utils.path(bindPrefix, "/", versionIndex, "/", param) + if (rebootNeeded) + return qsTr (" REBOOT needed") + if (guiRestartNeeded) + return qsTr (" GUI restart needed") + else if (incompatibleReason == 'PLATFORM') + return qsTr ( " not compatible with " + platform ) + else if (incompatibleReason == 'VERSION') + return qsTr ( " not compatible with " + vePlatform.version ) + else if (incompatibleReason == 'CMDLINE' && installedVersion.item.value == "") + return qsTr ( " must install from command line" ) + else + return "" } - VBusItem { id: packageName; bind: getBind ("PackageName") } + function getSettingsBind(param) + { + return Utils.path(settingsPrefix, "/", packageIndex, "/", param) + } + function getServiceBind(param) + { + return Utils.path(servicePrefix, "/Package/", packageIndex, "/", param) + } + + function versionToNumber (item) + { + var parts=["x", "x", "x", "x", "x"] + var versionNumber = 0 + + if (item.valid && item.value.substring (0,1) == "v") + { + parts = item.value.split (/[v.~]+/ , 4) + { + if (parts.length >= 2) + versionNumber += parseInt(parts[1]) * 1000000 + if (parts.length >= 3) + versionNumber += parseInt(parts[2]) * 1000 + if (parts.length >= 4) + versionNumber += parseInt(parts[3]) + else + versionNumber += 999 + } + } + return versionNumber + } MbRowSmall { + description: "" + anchors.verticalCenter: parent.verticalCenter - height: 20 - - isCurrentItem: root.isCurrentItem - description: packageName.valid ? packageName.value : "" - MbTextBlock - { - id: gitUser - item { bind: getBind("GitHubUser") } - width: 100 - show: packageName.valid && item.valid - } - MbTextBlock - { - item { bind: getBind("GitHubBranch") } - show: packageName.valid && gitUser.item.valid - width: 80 - } - MbTextBlock - { - item { bind: getBind("PackageVersion") } - width: 80 - show: packageName.valid + Column + { + width: root.width - gitHubVersion.width - packageVersion.width - installedVersion.width - 20 + Text // puts a bit of space above package name + { + text: " " + font.pixelSize: 6 + } + Text + { + text:packageName.valid ? packageName.value : "" + font.pixelSize: 14 + horizontalAlignment: Text.AlignLeft + } + Text + { + text: statusText () + font.pixelSize: 10 + horizontalAlignment: Text.AlignLeft + } + } + Column + { + Text // puts a bit of space above version boxes + { + text: " " + font.pixelSize: 3 + } + Text + { + text: "GitHub" + font.pixelSize: 10 + } + MbTextBlock + { + id: gitHubVersion + item { bind: getServiceBind("GitHubVersion") } + height: 20; width: 80 + } + Text // puts a bit of space below version boxes - only needed in one column + { + text: " " + font.pixelSize: 6 + } } + Column + { + Text // puts a bit of space above version boxes + { + text: " " + font.pixelSize: 3 + } + Text + { + text: qsTr ("Stored") + font.pixelSize: 10 + } + MbTextBlock + { + id: packageVersion + item { bind: getServiceBind("PackageVersion") } + height: 20; width: 80 + } + } + Column + { + Text // puts a bit of space above version boxes + { + text: " " + font.pixelSize: 3 + } + Text + { + text: qsTr ("Installed") + font.pixelSize: 10 + } + MbTextBlock + { + id: installedVersion + item { bind: getServiceBind("InstalledVersion") } + height: 20; width: 80 + } + } } } diff --git a/FileSets/PageSettingsAddPackageList.qml b/FileSets/PageSettingsAddPackageList.qml new file mode 100644 index 0000000..7eea12b --- /dev/null +++ b/FileSets/PageSettingsAddPackageList.qml @@ -0,0 +1,24 @@ +/////// new menu for package version display + +import QtQuick 1.1 +import "utils.js" as Utils +import com.victron.velib 1.0 + +MbPage { + id: root + title: defaultCount.valid ? qsTr("Inactive packages (tap to activate) ") : qsTr ("Package manager not running") + + property string servicePrefix: "com.victronenergy.packageManager" + // use DefaultCount as an indication that PackageManager is running + property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, "/DefaultCount") } + + model: defaultCount.valid ? defaultCount.value : 0 + delegate: Component + { + MbDisplayDefaultPackage + { + servicePrefix: root.servicePrefix + defaultIndex: index + } + } +} diff --git a/FileSets/PageSettingsPackageAdd.qml b/FileSets/PageSettingsPackageAdd.qml new file mode 100644 index 0000000..372d22c --- /dev/null +++ b/FileSets/PageSettingsPackageAdd.qml @@ -0,0 +1,151 @@ +/////// new menu for package add edit + +import QtQuick 1.1 +import "utils.js" as Utils +import com.victron.velib 1.0 + +MbPage { + id: root + title: editActionItem.valid ? qsTr("Add package") : qsTr ("Package manager not running") + property string settingsPrefix: "com.victronenergy.settings/Settings/PackageManager" + property string servicePrefix: "com.victronenergy.packageManager" + property int defaultIndex:0 + property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, "/DefaultCount") } + property VBusItem editActionItem: VBusItem { bind: Utils.path(servicePrefix, "/GuiEditAction") } + property VBusItem editStatus: VBusItem { bind: Utils.path(servicePrefix, "/GuiEditStatus") } + property string packageName: packageNameBox.item.valid ? packageNameBox.item.value : "" + property string editAction: editActionItem.valid ? editActionItem.value : '' + + property bool showControls: editActionItem.valid + + property VBusItem defaultPackageName: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "PackageName" ) } + property VBusItem defaultGitHubUser: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "GitHubUser" ) } + property VBusItem defaultGitHubBranch: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "GitHubBranch" ) } + property VBusItem editPackageName: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "PackageName" ) } + property VBusItem editGitHubUser: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "GitHubUser" ) } + property VBusItem editGitHubBranch: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "GitHubBranch" ) } + property bool addPending: false + + Component.onCompleted: + { + updateEdit () + } + + onEditActionChanged: + { + if (addPending && editAction == '') + { + addPending = false + pageStack.pop() + } + } + + function getSettingsBind(param) + { + return Utils.path(settingsPrefix, "/Edit/", param) + } + function getServiceBind(param) + { + return Utils.path(servicePrefix, "/Default/", defaultIndex, "/", param) + } + + // copy a set of default package values to Edit area when changing indexes + function updateEdit () + { + bindPrefix = Utils.path(servicePrefix, "/Default/", defaultIndex ) + editPackageName.setValue ( defaultPackageName.valid ? defaultPackageName.value : "??" ) + editGitHubUser.setValue ( defaultGitHubUser.valid ? defaultGitHubUser.value : "??" ) + editGitHubBranch.setValue ( defaultGitHubBranch.valid ? defaultGitHubBranch.value : "??" ) + editStatus.setValue ("") + editActionItem.setValue ("") + addPending = false + } + + function cancelEdit () + { + addPending = false + if (editAction == '') + pageStack.pop() + else + { + editStatus.setValue ("") + editActionItem.setValue ("") + } + } + function confirm () + { + addPending = true + // provide local confirmation of action - takes PackageManager too long + editStatus.setValue ( "adding " + packageName) + editActionItem.setValue ('add:' + packageName) + } + model: VisualItemModel + { + MbEditBox + { + id: packageNameBox + description: qsTr ("Package name") + maximumLength: 30 + item.bind: getSettingsBind ("PackageName") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + show: showControls + } + MbEditBox + { + id: gitHubUser + description: qsTr ("GitHub user") + maximumLength: 20 + item.bind: getSettingsBind ("GitHubUser") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + show: showControls + } + MbEditBox + { + id: gitHubBranch + description: qsTr ("GitHub branch or tag") + maximumLength: 20 + item.bind: getSettingsBind ("GitHubBranch") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + show: showControls + } + MbOK + { + id: cancelButton + width: 90 + anchors { right: parent.right } + description: "" + value: editAction == '' ? qsTr("Cancel") : qsTr("OK") + onClicked: cancelEdit () + show: showControls + } + MbOK + { + id: proceedButton + width: 100 + anchors { right: cancelButton.left; bottom: cancelButton.bottom } + description: "" + value: qsTr ("Proceed") + onClicked: confirm () + show: showControls && editAction == '' + writeAccessLevel: User.AccessInstaller + } + Text + { + id: statusMessage + width: 250 + wrapMode: Text.WordWrap + anchors { left: parent.left; leftMargin: 10; bottom: cancelButton.bottom; bottomMargin: 5 } + font.pixelSize: 12 + text: + { + if (editStatus.valid && editStatus.value != "") + return editStatus.value + else + return ("add " + packageName + " ?") + } + } + } +} diff --git a/FileSets/PageSettingsPackageControl.qml b/FileSets/PageSettingsPackageControl.qml deleted file mode 100644 index 9b46b0f..0000000 --- a/FileSets/PageSettingsPackageControl.qml +++ /dev/null @@ -1,43 +0,0 @@ -/////// new menu for package version display - -import QtQuick 1.1 -import "utils.js" as Utils -import com.victron.velib 1.0 - -MbPage { - id: root - title: qsTr("Package Versions") - property string bindPrefix: "com.victronenergy.settings/Settings/PackageVersion" - VBusItem { id: checkingPackageItem; bind: Utils.path(bindPrefix, "/CheckingPackage") } - property string checkingPackage: checkingPackageItem.valid ? checkingPackageItem.value : "" - - model: VisualItemModel - { - MbSubMenu - { - description: qsTr("Package Version List") - subpage: Component { PageSettingsPackageVersions {} } - } - MbItemOptions - { - id: autoUpdate - description: qsTr ("Automatic Git Hub updates") - bind: Utils.path (bindPrefix, "/GitHubAutoUpdate") - possibleValues: - [ - MbOption { description: "Normal"; value: 1 }, - MbOption { description: "Fast one pass then Normal"; value: 2 }, - MbOption { description: "Check packages once (Fast)"; value: 3 }, - MbOption { description: "Disabled"; value: 0 } - ] - writeAccessLevel: User.AccessUser - } - MbItemText - { - text: checkingPackage - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - show: checkingPackage != "" - } - } -} diff --git a/FileSets/PageSettingsPackageEdit.qml b/FileSets/PageSettingsPackageEdit.qml new file mode 100644 index 0000000..6308676 --- /dev/null +++ b/FileSets/PageSettingsPackageEdit.qml @@ -0,0 +1,397 @@ +/////// new menu for package version edit + +import QtQuick 1.1 +import "utils.js" as Utils +import com.victron.velib 1.0 + +MbPage { + id: root + title: platform.valid ? qsTr("Package editor") : qsTr ("Package manager not running") + property string settingsPrefix: "com.victronenergy.settings/Settings/PackageManager" + property string servicePrefix: "com.victronenergy.packageManager" + property int packageIndex: 0 + property int defaultIndex:0 + property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, "/DefaultCount") } + property VBusItem packageCount: VBusItem { bind: Utils.path(settingsPrefix, "/Count") } + property VBusItem editAction: VBusItem { bind: Utils.path(servicePrefix, "/GuiEditAction") } + property VBusItem editStatus: VBusItem { bind: Utils.path(servicePrefix, "/GuiEditStatus") } + property string packageName: packageNameBox.item.valid ? packageNameBox.item.value : "" + property bool isSetupHelper: packageName == "SetupHelper" + + property VBusItem rebootNeeded: VBusItem { bind: getServiceBind ( "RebootNeeded") } + property VBusItem guiRestartNeeded: VBusItem { bind: getServiceBind ( "GuiRestartNeeded") } + property VBusItem incompatibleReason: VBusItem { bind: getServiceBind ( "Incompatible") } + property VBusItem platform: VBusItem { bind: Utils.path(servicePrefix, "/Platform") } + + property bool showControls: editAction.valid + property bool gitHubValid: gitHubVersion.item.valid && gitHubVersion.item.value.substring (0,1) === "v" + property bool packageValid: packageVersion.item.valid && packageVersion.item.value.substring (0,1) === "v" + property bool installedValid: installedVersion.item.valid && installedVersion.item.value.substring (0,1) === "v" + property bool downloadOk: gitHubValid && gitHubVersion.item.value != "" + property bool installOk: packageValid && packageVersion.item.value != "" && incompatibleReason.value == "" + property string requestedAction: '' + property bool actionPending: requestedAction != '' + property bool navigate: ! actionPending && ! waitForAction && showControls + property bool waitForAction: showControls && editAction.value != '' + property bool moreActions: showControls && (editAction.value == 'RebootNeeded' || editAction.value == 'GuiRestartNeeded') + + property VBusItem defaultPackageName: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "PackageName" ) } + property VBusItem defaultGitHubUser: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "GitHubUser" ) } + property VBusItem defaultGitHubBranch: VBusItem { bind: Utils.path ( servicePrefix, "/Default/", defaultIndex, "/", "GitHubBranch" ) } + property VBusItem editPackageName: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "PackageName" ) } + property VBusItem editGitHubUser: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "GitHubUser" ) } + property VBusItem editGitHubBranch: VBusItem { bind: Utils.path ( settingsPrefix, "/Edit/", "GitHubBranch" ) } + + + Component.onCompleted: + { + resetPackageIndex () + } + + function resetPackageIndex () + { + if (packageIndex < 0) + packageIndex = 0 + else if (packageIndex >= packageCount.value) + packageIndex = packageCount.value - 1 + } + + function getSettingsBind(param) + { + resetPackageIndex () + return Utils.path(settingsPrefix, "/", packageIndex, "/", param) + } + function getServiceBind(param) + { + resetPackageIndex () + return Utils.path(servicePrefix, "/Package/", packageIndex, "/", param) + } + + function nextIndex () + { + packageIndex += 1 + if (packageIndex >= packageCount.value) + packageIndex = packageCount.value - 1 + } + function previousIndex () + { + packageIndex -= 1 + if (packageIndex < 0) + packageIndex = 0 + } + function cancelEdit () + { + requestedAction = '' + editAction.setValue ( '' ) + editStatus.setValue ( '' ) + } + function confirm () + { + if (actionPending) + { + // provide local confirmation of action - takes PackageManager too long + editStatus.setValue ( (requestedAction == 'remove' ? "removing " : requestedAction + "ing ") + packageName) + editAction.setValue (requestedAction + ':' + packageName) + requestedAction = '' + } + } + function install () + { + requestedAction = 'install' + } + function uninstall () + { + requestedAction = 'uninstall' + } + function gitHubDownload () + { + requestedAction = 'download' + } + function remove () + { + requestedAction = 'remove' + } + function signalAdditionalAction () + { + if (editAction.value == 'RebootNeeded') + { + // provide local confirmation of action - takes PackageManager too long + editStatus.setValue ( "rebooting") + editAction.setValue ( 'reboot' ) + } + else if (editAction.value == 'GuiRestartNeeded') + { + // provide local confirmation of action - takes PackageManager too long + editStatus.setValue ( "restarting GUI") + editAction.setValue ( 'restartGui' ) + } + requestedAction = '' + } + + model: VisualItemModel + { + MbEditBox + { + id: packageNameBox + description: qsTr ("Package name") + maximumLength: 30 + item.bind: getSettingsBind ("PackageName") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + readonly: true + show: showControls + } + MbRowSmall + { + description: qsTr ("Versions") + height: 25 + Text + { + text: "GitHub:" + font.pixelSize: 10 + show: showControls + } + show: showControls + MbTextBlock + { + id: gitHubVersion + item { bind: getServiceBind("GitHubVersion") } + height: 25; width: 80 + show: showControls + } + Text + { + text: qsTr ("stored:") + font.pixelSize: 10 + show: showControls + } + MbTextBlock + { + id: packageVersion + item { bind: getServiceBind("PackageVersion") } + height: 25; width: 80 + show: showControls + } + Text + { + text: + { + if (rebootNeeded.value == 1) + return qsTr ("REBOOT:") + else if (guiRestartNeeded.value == 1) + return qsTr ("GUI\nRestart:") + else + return qsTr ("installed:") + } + horizontalAlignment: Text.AlignRight + width: 50 + font.pixelSize: 10 + show: showControls && incompatibleReason.value == "" + } + MbTextBlock + { + id: installedVersion + item { bind: getServiceBind("InstalledVersion") } + height: 25; width: 80 + show: showControls && incompatibleReason.value == "" + } + Text + { + id: incompatibleText + text: + { + if (incompatibleReason.value == 'PLATFORM') + return ( qsTr ("not compatible with\n") + platformItem.value ) + else if (incompatibleReason.value == 'VERSION') + return ( qsTr ("not compatible with\n") + vePlatform.version ) + else if (incompatibleReason.value == 'CMDLINE') + return qsTr ("must install\nfrom command line" ) + else + return qsTr ("compatible ???" ) // compatible for unknown reason + } + horizontalAlignment: Text.AlignHCenter + width: 50 + 80 + 3 + font.pixelSize: 10 + show: showControls && ! incompatibleReason.value == "" + } + } + MbEditBox + { + id: gitHubUser + description: qsTr ("GitHub user") + maximumLength: 20 + item.bind: getSettingsBind ("GitHubUser") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + show: showControls + } + MbEditBox + { + id: gitHubBranch + description: qsTr ("GitHub branch or tag") + maximumLength: 20 + item.bind: getSettingsBind ("GitHubBranch") + overwriteMode: false + writeAccessLevel: User.AccessInstaller + show: showControls + } + MbOK + { + id: removeButton + width: 170 + anchors { right: parent.right} + description: "" + value: qsTr("Remove package") + onClicked: remove () + writeAccessLevel: User.AccessInstaller + opacity: installedValid ? 0.0001 : 1.0 + show: navigate + } + MbOK + { + id: cancelButton + width: 90 + anchors { right: parent.right; bottom: removeButton.bottom } + description: "" + value: qsTr("Cancel") + onClicked: cancelEdit () + show: showControls && ! navigate && ! waitForAction + } + MbOK + { + id: dismissErrorButton + width: 90 + anchors { right: parent.right; bottom: removeButton.bottom } + description: "" + value: qsTr("OK") + onClicked: cancelEdit () + show: showControls && editAction.value == 'ERROR' + } + MbOK + { + id: laterButton + width: 90 + anchors { right: parent.right; bottom: removeButton.bottom } + description: "" + value: qsTr("Later") + onClicked: cancelEdit () + show: moreActions + } + MbOK + { + id: nowButton + width: 90 + anchors { right: laterButton.left; bottom: removeButton.bottom } + description: "" + value: qsTr("Now") + onClicked: signalAdditionalAction () + show: moreActions + } + MbOK + { + id: confirmButton + width: 375 + anchors { left: parent.left; bottom: removeButton.bottom } + description: "" + value: qsTr ("Proceed") + onClicked: confirm () + show: showControls && ! navigate && actionPending + writeAccessLevel: User.AccessInstaller + } + Text + { + id: statusMessage + width: 250 + wrapMode: Text.WordWrap + anchors { left: parent.left; leftMargin: 10; bottom: removeButton.bottom; bottomMargin: 5 } + font.pixelSize: 12 + color: actionPending && isSetupHelper ? "red" : "black" + text: + { + if (actionPending) + { + if (isSetupHelper && requestedAction == 'uninstall') + return qsTr ("WARNING: SetupHelper is required for these menus - uninstall anyway ?") + else + return (requestedAction + " " + packageName + " ?") + } + else if (editStatus.valid && editStatus.value != "") + return editStatus.value + else + return "" + } + show: waitForAction || actionPending + } + + // bottom row of buttons + MbOK + { + id: previousButton + width: 100 + anchors { left: parent.left ; top:removeButton.bottom } + description: "" + value: qsTr("Previous") + onClicked: previousIndex () + show: + { + if (! showControls) + return false + else if (packageIndex > 0) + return true + else + return false + } + } + MbOK + { + id: nextButton + width: 75 + anchors { left: previousButton.right; bottom: previousButton.bottom } + description: "" + value: qsTr("Next") + onClicked: nextIndex () + show: + { + if (! showControls) + return false + else if (packageIndex < packageCount.value - 1) + return true + else + return false + } + } + MbOK + { + id: downloadButton + width: 110 + anchors { right: installButton.left; bottom: previousButton.bottom } + description: "" + value: qsTr ("Download") + onClicked: gitHubDownload () + show: navigate && downloadOk + writeAccessLevel: User.AccessInstaller + } + MbOK + { + id: installButton + width: 90 + anchors { right: uninstallButton.left; bottom: previousButton.bottom } + description: "" + value: qsTr ("Install") + onClicked: install () + show: navigate && installOk + writeAccessLevel: User.AccessInstaller + } + MbOK + { + id: uninstallButton + width: 100 + anchors { right: parent.right; bottom: installButton.bottom } + description: "" + value: qsTr("Uninstall") + onClicked: uninstall () + show: navigate && installedValid + writeAccessLevel: User.AccessInstaller + } + } +} diff --git a/FileSets/PageSettingsPackageManager.qml b/FileSets/PageSettingsPackageManager.qml new file mode 100644 index 0000000..653f593 --- /dev/null +++ b/FileSets/PageSettingsPackageManager.qml @@ -0,0 +1,123 @@ +/////// new menu for package version display + +import QtQuick 1.1 +import "utils.js" as Utils +import com.victron.velib 1.0 + +MbPage { + id: root + title: qsTr("Package manager") + property string settingsPrefix: "com.victronenergy.settings/Settings/PackageManager" + property string servicePrefix: "com.victronenergy.packageManager" + VBusItem { id: downloadStatus; bind: Utils.path(servicePrefix, "/GitHubUpdateStatus") } + VBusItem { id: installStatus; bind: Utils.path(servicePrefix, "/InstallStatus") } + VBusItem { id: mediaStatus; bind: Utils.path(servicePrefix, "/MediaUpdateStatus") } + VBusItem { id: actionNeeded; bind: Utils.path(servicePrefix, "/ActionNeeded") } + VBusItem { id: editAction; bind: Utils.path(servicePrefix, "/GuiEditAction") } + property bool showInstallStatus: installStatus.valid && installStatus.value != "" + property bool showMediaStatus: mediaStatus.valid && mediaStatus.value != "" + property bool showControls: installStatus.valid + + model: VisualItemModel + { + MbItemText + { + id: status + text: + { + if (! showControls) + return"Package manager not running" + else if (installStatus.valid && installStatus.value != "") + return installStatus.value + else if (mediaStatus.valid && mediaStatus.value != "") + return mediaStatus.value + else if (downloadStatus.valid && downloadStatus.value != "") + return downloadStatus.value + else + return "idle" + } + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + MbItemOptions + { + id: autoDownload + description: qsTr ("Automatic GitHub downloads") + bind: Utils.path (settingsPrefix, "/GitHubAutoDownload") + possibleValues: + [ + MbOption { description: "Normal"; value: 1 }, + MbOption { description: "Fast, then Normal"; value: 2 }, + MbOption { description: "Once (Fast)"; value: 3 }, + MbOption { description: "Off"; value: 0 } + ] + writeAccessLevel: User.AccessInstaller + } + MbSwitch + { + id: autoInstall + bind: Utils.path (settingsPrefix, "/AutoInstall") + name: qsTr ("Auto install packages") + writeAccessLevel: User.AccessInstaller + } + MbSubMenu + { + description: qsTr("Active packages") + subpage: Component { PageSettingsPackageVersions {} } + show: showControls + } + MbSubMenu + { + description: qsTr("Inactive packages") + subpage: Component { PageSettingsAddPackageList {} } + show: showControls + } + MbOK + { + id: finishButton + description: + { + if (editAction.value == 'reboot') + return qsTr ("REBOOTING ...") + else if (editAction.value == 'guiRestart') + return qsTr ("restarting GUI ...") + else + return qsTr ("action to finish install/uninstall") + } + value: + { + if (! actionNeeded.valid) + return "" + else if (actionNeeded.value == 'reboot') + return qsTr ("Reboot") + else if (actionNeeded.value == 'guiRestart') + return qsTr ("Restart GUI") + else + return "" + } + onClicked: + { + if (actionNeeded.value == 'reboot') + { + // needs immediate update because GUI will be going down ASAP + finishButton.description = qsTr ("REBOOTING ...") + editAction.setValue ( 'reboot' ) + } + else if (actionNeeded.value == 'guiRestart') + { + // needs immediate update because GUI will be going down ASAP + finishButton.description = qsTr ("restarting GUI ...") + editAction.setValue ( 'restartGui' ) + } + } + show: actionNeeded.valid && actionNeeded.value != '' + writeAccessLevel: User.AccessInstaller + } + MbSubMenu + { + description: qsTr("Backup & restore settings") + subpage: Component { PageSettingsPmBackup {} } + show: showControls + } + } +} diff --git a/FileSets/PageSettingsPackageVersions.qml b/FileSets/PageSettingsPackageVersions.qml index 01b4b8a..ce28c0b 100644 --- a/FileSets/PageSettingsPackageVersions.qml +++ b/FileSets/PageSettingsPackageVersions.qml @@ -6,17 +6,21 @@ import com.victron.velib 1.0 MbPage { id: root - title: qsTr("Package Version List") - property string bindPrefix: "com.victronenergy.settings/Settings/PackageVersion" - property VBusItem count: VBusItem { bind: Utils.path(bindPrefix, "/Count") } + title: defaultCount.valid ? qsTr("Active packages (tap to edit) ") : qsTr ("Package manager not running") + property string servicePrefix: "com.victronenergy.packageManager" + property string settingsPrefix: "com.victronenergy.settings/Settings/PackageManager" + property VBusItem count: VBusItem { bind: Utils.path(settingsPrefix, "/Count") } + // use DefaultCount as an indication that PackageManager is running + property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, "/DefaultCount") } - model: count.valid ? count.value : 0 + model: defaultCount.valid ? count.valid ? count.value : 0 : 0 delegate: Component { MbDisplayPackageVersion { - bindPrefix: root.bindPrefix - versionIndex: index + servicePrefix: root.servicePrefix + settingsPrefix: root.settingsPrefix + packageIndex: index } } } diff --git a/FileSets/PageSettingsPmBackup.qml b/FileSets/PageSettingsPmBackup.qml new file mode 100644 index 0000000..dd652b9 --- /dev/null +++ b/FileSets/PageSettingsPmBackup.qml @@ -0,0 +1,62 @@ +/////// new menu for settings backup and restore + +import QtQuick 1.1 +import "utils.js" as Utils +import com.victron.velib 1.0 + +MbPage { + id: root + title: qsTr("Settings backup & restore") + property string settingsPrefix: "com.victronenergy.settings/Settings/PackageManager" + property string servicePrefix: "com.victronenergy.packageManager" + VBusItem { id: mediaAvailable; bind: Utils.path(servicePrefix, "/BackupMediaAvailable") } + VBusItem { id: settingsFileExists; bind: Utils.path(servicePrefix, "/BackupSettingsFileExist") } + VBusItem { id: backupProgressItem; bind: Utils.path(servicePrefix, "/BackupProgress") } + property int backupProgress: backupProgressItem.valid ? backupProgressItem.value : 0 + + model: VisualItemModel + { + MbItemText + { + id: info + text: qsTr ("Backup and restore SOME system settings\nthis is NOT the Victron mechanism currently under development") + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + MbItemText + { + id: status + text: + { + if (backupProgress == 1 || backupProgress == 3) + return qsTr ("backing up settings ... (may take a while)") + else if (backupProgress == 2 || backupProgress == 4) + return qsTr ("restoring settings ... (may take a while)") + else if ( ! mediaAvailable.valid || mediaAvailable.value == 0) + return qsTr ("No USB or SD media found - insert one to continue") + else if (settingsFileExists.valid && settingsFileExists.value == 1) + return qsTr ("Settings backup file found") + else + return "" + } + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + } + MbOK + { + description: qsTr("Backup settings") + value: qsTr("Press to backup settings") + onClicked: backupProgressItem.setValue (1) + show: mediaAvailable.valid && mediaAvailable.value == 1 && backupProgressItem.value == 0 + writeAccessLevel: User.AccessInstaller + } + MbOK + { + description: qsTr("Restore settings") + value: qsTr("Press to restore settings") + onClicked: backupProgressItem.setValue (2) + show: settingsFileExists.valid && settingsFileExists.value == 1 && backupProgressItem.value == 0 + writeAccessLevel: User.AccessInstaller + } + } +} diff --git a/FileSets/v2.42/PageSettings.qml b/FileSets/v2.42/PageSettings.qml index 5c5a575..0c75ba2 100644 --- a/FileSets/v2.42/PageSettings.qml +++ b/FileSets/v2.42/PageSettings.qml @@ -172,8 +172,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.58/PageSettings.qml b/FileSets/v2.58/PageSettings.qml index ddf5b63..dd744f4 100644 --- a/FileSets/v2.58/PageSettings.qml +++ b/FileSets/v2.58/PageSettings.qml @@ -179,8 +179,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.66/PageSettings.qml b/FileSets/v2.66/PageSettings.qml index 1541327..9a8359b 100644 --- a/FileSets/v2.66/PageSettings.qml +++ b/FileSets/v2.66/PageSettings.qml @@ -184,8 +184,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.73/PageSettings.qml b/FileSets/v2.73/PageSettings.qml index 328b74a..22a3ad8 100644 --- a/FileSets/v2.73/PageSettings.qml +++ b/FileSets/v2.73/PageSettings.qml @@ -175,8 +175,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~19/PageSettings.qml b/FileSets/v2.80~19/PageSettings.qml index cf8519e..4aaf3b4 100644 --- a/FileSets/v2.80~19/PageSettings.qml +++ b/FileSets/v2.80~19/PageSettings.qml @@ -180,8 +180,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~21-large-23/PageSettings.qml b/FileSets/v2.80~21-large-23/PageSettings.qml index 1ca0a3f..87e14f2 100644 --- a/FileSets/v2.80~21-large-23/PageSettings.qml +++ b/FileSets/v2.80~21-large-23/PageSettings.qml @@ -186,8 +186,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~21/PageSettings.qml b/FileSets/v2.80~21/PageSettings.qml index 1e54cd1..8146813 100644 --- a/FileSets/v2.80~21/PageSettings.qml +++ b/FileSets/v2.80~21/PageSettings.qml @@ -180,8 +180,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~21/PageSettings.qml 2.orig b/FileSets/v2.80~21/PageSettings.qml 2.orig new file mode 100644 index 0000000..37363e6 --- /dev/null +++ b/FileSets/v2.80~21/PageSettings.qml 2.orig @@ -0,0 +1,185 @@ +import QtQuick 1.1 +import com.victron.velib 1.0 +import net.connman 0.1 +import "utils.js" as Utils + +MbPage { + title: qsTr("Settings") + property string bindPrefix: "com.victronenergy.settings" + property VBusItem relay0Item: VBusItem {bind: "com.victronenergy.system/Relay/0/State"} + property bool hasRelay0: relay0Item.valid + + model: VisualItemModel { + MbSubMenu { + id: generalItem + description: qsTr("General") + subpage: Component { + PageSettingsGeneral { + title: generalItem.description + } + } + } + + MbSubMenu { + description: qsTr("Firmware") + subpage: Component { + PageSettingsFirmware { + title: qsTr("Firmware") + } + } + } + + MbSubMenu { + description: qsTr("Date & Time") + subpage: Component { + PageTzInfo { + title: qsTr("Date & Time") + } + } + } + + MbSubMenu { + description: qsTr("Remote Console") + subpage: Component { PageSettingsRemoteConsole {} } + } + + MbSubMenu { + id: systemSetupItem + description: qsTr("System setup") + subpage: Component { + PageSettingsSystem { + title: systemSetupItem.description + } + } + } + + MbSubMenu { + id: dvcc + description: qsTr("DVCC") + subpage: Component { + PageSettingsDVCC { + title: dvcc.description + } + } + } + + MbSubMenu { + id: displayItem + description: qsTr("Display & language") + subpage: Component { + PageSettingsDisplay { + title: displayItem.description + } + } + } + + MbSubMenu { + id: vrmLoggerItem + description: qsTr("VRM online portal") + subpage: Component { + PageSettingsLogger { + title: vrmLoggerItem.description + } + } + } + + MbSubMenu { + VBusItem { + id: systemType + bind: "com.victronenergy.system/SystemType" + } + description: systemType.value === "Hub-4" ? systemType.value : qsTr("ESS") + subpage: Component { PageSettingsHub4 {} } + } + + MbSubMenu { + description: qsTr("Energy meters") + subpage: Component { PageSettingsCGwacsOverview {} } + } + + MbSubMenu { + description: qsTr("PV inverters") + subpage: Component { PageSettingsFronius {} } + } + + MbSubMenu { + show: App.withQwacs + description: qsTr("Wireless AC sensors") + subpage: Component { PageSettingsQwacs {} } + } + + MbSubMenu { + description: qsTr("Modbus TCP devices") + subpage: Component { PageSettingsModbus {} } + } + + MbSubMenu { + id: ethernetItem + description: qsTr("Ethernet") + subpage: Component { PageSettingsTcpIp { showLinkLocal: true } } + } + + MbSubMenu { + description: qsTr("Wi-Fi") + subpage: vePlatform.hasHostAccessPoint ? wifiWithAP : wifiWithoutAP + Component { id: wifiWithoutAP; PageSettingsWifi {} } + Component { id: wifiWithAP; PageSettingsWifiWithAccessPoint {} } + } + + MbSubMenu { + description: qsTr("GSM modem") + subpage: Component { PageSettingsGsm {} } + } + + MbSubMenu { + description: qsTr("Bluetooth") + subpage: Component { PageSettingsBluetooth {} } + show: Connman.technologyList.indexOf("bluetooth") !== -1 + } + + MbSubMenu { + description: qsTr("GPS") + subpage: Component { PageSettingsGpsList {} } + } + + MbSubMenu { + description: qsTr("Generator start/stop") + subpage: Component { PageRelayGenerator {} } + show: hasRelay0 + } + + MbSubMenu { + description: qsTr("Tank pump") + subpage: Component { PageSettingsTankPump {} } + } + + MbSubMenu { + description: qsTr("Relay") + subpage: Component { PageSettingsRelay {} } + show: hasRelay0 + } + + MbSubMenu { + description: qsTr("Services") + subpage: Component { PageSettingsServices {} } + } + + MbSubMenu { + description: qsTr("I/O") + subpage: ioSettings + show: ioSettings.haveSubMenus + PageSettingsIo { id: ioSettings } + } + + MbSubMenu { + description: qsTr("Backup & Restore") + subpage: Component { PageSettingsBackup {} } + } + + MbSubMenu { + description: qsTr("Debug") + subpage: Component { PageDebug {} } + showAccessLevel: User.AccessService + } + } +} diff --git a/FileSets/v2.80~24/PageSettings.qml b/FileSets/v2.80~24/PageSettings.qml index 855dae2..f0a4a32 100644 --- a/FileSets/v2.80~24/PageSettings.qml +++ b/FileSets/v2.80~24/PageSettings.qml @@ -182,8 +182,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~29/PageSettings.qml b/FileSets/v2.80~29/PageSettings.qml index d89e7d3..1f9c8aa 100644 --- a/FileSets/v2.80~29/PageSettings.qml +++ b/FileSets/v2.80~29/PageSettings.qml @@ -182,8 +182,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/FileSets/v2.80~33-large-24/PageSettings.qml b/FileSets/v2.80~33-large-24/PageSettings.qml index 4d0da72..88b02ab 100644 --- a/FileSets/v2.80~33-large-24/PageSettings.qml +++ b/FileSets/v2.80~33-large-24/PageSettings.qml @@ -170,20 +170,12 @@ MbPage { subpage: ioSettings show: ioSettings.haveSubMenus PageSettingsIo { id: ioSettings } - } - - MbSubMenu { - description: qsTr("Backup & Restore") - subpage: Component { PageSettingsBackup {} } - } - /* MbSubMenu { description: qsTr("Backup & Restore") subpage: Component { PageSettingsBackup {} } } */ - MbSubMenu { description: qsTr("Venus OS Large features") subpage: Component { PageSettingsLarge {} } @@ -193,8 +185,8 @@ MbPage { //////// added for package control and version display MbSubMenu { - description: qsTr("Package Versions") - subpage: Component { PageSettingsPackageControl {} } + description: qsTr("Package manager") + subpage: Component { PageSettingsPackageManager {} } } MbSubMenu { diff --git a/PackageManager.py b/PackageManager.py new file mode 100755 index 0000000..6b47af2 --- /dev/null +++ b/PackageManager.py @@ -0,0 +1,3234 @@ +#!/usr/bin/env python +# +# PackageManager.py +# Kevin Windrem +# +# +# This program is responsible for +# downloading, installing and unstalling packages +# package monitor also checks SD cards and USB sticks for package archives +# either automatically or manually via the GUI +# providing the user with status on installed packages and any updates via the GUI +# +# It runs as /service/PackageManager +# +# Persistent storage for packageManager is stored in dbus Settings: +# +# com.victronenergy.Settings parameters for each package: +# /Settings/PackageManager/n/PackageName can not be edited by the GUI +# /Settings/PackageManager/n/GitHubUser can be edited by the GUI +# /Settings/PackageManager/n/GitHubBranch can be edited by the GUI +# /Settings/PackageManager/Count the number of ACTIVE packages (0 <= n < Count) +# /Settings/PackageManager/Edit/... GUI edit package set - all fields editable +# +# /Settings/PackageManager/GitHubAutoDownload set by the GUI to control automatic updates from GitHub +# 0 - no GitHub auto downloads (version checks still occur) +# 1 - normal updates - one download every 10 minutes +# 2 - fast updates - one download update every 10 seconds, then at the normal rate after one pass +# 3 - one update pass at the fast rate, then to no updates +# changing to one of the fast scans, starts from the first package +# +# if no download is needed, checks for downloads are fast: every 5 seconds, slow: every 2 minutes + +AUTO_DOWNLOADS_OFF = 0 +NORMAL_DOWNLOAD = 1 +FAST_DOWNLOAD = 2 +ONE_DOWNLOAD = 3 + +# /Settings/PackageManager/AutoInstall +# 0 - no automatic install +# 1 - automatic install after download from GitHub or SD/USB +# +# Additional (volatile) parameters linking packageManager and the GUI are provided in a separate dbus service: +# +# com.victronenergy.packageManager parameters +# /Package/n/GitHubVersion from GitHub +# /Package/n/PackageVersion from /data /version from the package directory +# /Package/n/InstalledVersion from /etc/venus/isInstalled- +# /Package/n/RebootNeeded indicates a reboot is needed to activate this package +# /Package/n/Incompatible indicates if package is or is not compatible with the system +# '' if compatible +# 'VERSION' if the system version is outside the package's acceptable range +# 'PLATFORM' package can not run on this platform +# 'CMDLINE' setup must be run from command line +# currently only for Raspberry PI packages only +# +# for both Settings and the the dbus service: +# n is a 0-based section used to reference a specific package +# +# a list of default packages that are not in the main package list +# these sets are used by the GUI to display a list of packages to be added to the system +# filled in from /data/SetupHelper/defaultPackageList, but eliminating any packages already in /data +# the first entry (m = 0) is "new" - for a new package +# "new" just displays in the packages to add list in the GUI +# all package additions are done through /Settings/PackageManager/Edit/... +# /Default/m/PackageName +# /Default/m/GitHubUser +# /Default/m/GitHubBranch +# /DefaultCount the number of default packages +# +# m is a 0-based section used to referene a specific default paclage +# +# /GuiEditAction is a text string representing the action +# set by the GUI to trigger an action in PackageManager +# 'install' - install package from /data to the Venus working directories +# 'uninstall' - uninstall package from the working directories +# 'download" - download package from GutHub to /data +# 'add' - add package to package list (after GUI sets .../Edit/... +# 'remove' - remove package from list TBD ????? +# 'reboot' - reboot +# 'restartGui' - restart the GUI +# +# the GUI must wait for PackageManager to signal completion of one operation before initiating another +# +# set by packageMonitor when the task is complete +# return codes - set by PackageManager +# '' - action completed without errors (idle) +# 'ERROR' - error during action - error reported in /GuiEditStatus: +# unknown error +# not compatible with this version +# not compatible with this platform +# no options present - must install from command line +# GUI choices: OK - closes "dialog" +# 'RebootNeeded' - reboot needed +# GUI choices: +# Do it now +# GUI sends 'reboot' command to PackageManager +# Defer +# GUI sets action to 0 +# 'GuiRestartNeeded' - GUI restart needed +# GUI choices: +# Do it now +# GUI sends 'restartGui' command to PackageManager +# Defer +# GUI sets action to 0 +# +# +# the following service parameters control settings backup and restore +# /BackupMediaAvailable True if suitable SD/USB media is detected by PackageManager +# /BackupSettingsFileExist True if PackageManager detected a settings backup file +# /BackupProgress used to trigger and provide status of an operation +# 0 nothing happening - set by PackageManager when operaiton completes +# 1 set by the GUI to trigger a backup operation +# 2 set by the GUI to trigger a restore operation +# 3 set by PackageManager to indicate a backup is in progress +# 4 set by PackageManager to indicate a restore is in progress +# +# setup script return codes and install states +EXIT_SUCCESS = 0 +EXIT_REBOOT = 123 +EXIT_RESTART_GUI = 124 +EXIT_INCOMPATIBLE_VERSION = 254 +EXIT_INCOMPATIBLE_PLATFOM = 253 +EXIT_FILE_SET_ERROR = 252 +EXIT_OPTIONS_NOT_SET = 251 +EXIT_RUN_AGAIN = 250 +EXIT_ERROR = 255 # generic error +# install states only +ERROR_NO_SETUP_FILE = 999 +# +# +# /GuiEditStatus a text message to report edit status to the GUI +# +# /GitHubUpdateStatus as above for automatic GitHub update +# +# /InstallStatus as above for automatic install/uninstall +# +# /MediaUpdateStatus as above for SD/USB media transfers +# +# /Platform a translated version of the platform (aka machine) +# machine Platform +# ccgx CCGX +# einstein Cerbo GX +# bealglebone Venus GX +# canvu500 CanVu 500 +# nanopi Multi/Easy Solar GX +# raspberrypi2 Raspberry Pi 2/3 +# raspberrypi4 Raspberry Pi 4 +# +# /ActionNeeded informs GUI if further action is needed following a manual operation +# the operator has the option to defer reboots and GUI restarts (by choosing "Later) +# '' no action needed +# 'reboot' reboot needed +# 'guiRestart' GUI restart needed +# +# the GUI can respond by setting /GuiEditAction to 'reboot' or 'restartGui' +# +# /Settings/PackageVersion/Edit/ is a section for the GUI to provide information about the a new package to be added +# +# /data/SetupHelper/defaultPackageList provides an initial list of packages +# It contains a row for each package with the following information: +# packageName gitHubUser gitHubBranch +# If present, packages listed will be ADDED to the package list in /Settings +# existing dbus Settings (GitHubUser and GitHubBranch) will not be changed +# +# this file is read at program start +# +# Package information is stored in the /data/ directory +# +# A version file within that directory identifies the version of that package stored on disk but not necessarily installed +# +# When a package is installed, the version in the package directory is written to an "installed flag" file +# /etc/venus/isInstalled- +# the contents of the file populate InstalledVersion (blank if the file doesn't exist or is empty) +# +# InstalledVersion is displayed to the user and used for tests for automatic updates +# +# GitHubVersion is read from the internet if a connection exists. +# To minimize local network traffic and GitHub server loads one package's GitHub version is +# read once every 5 seconds until all package versions have been retrieved +# then one package verison is read every 10 minutes. +# Addition of a package or change in GitHubUser or GitHubBranch will trigger a fast +# update of GitHub versions +# If the package on GitHub can't be accessed, GitHubVersion will be blank +# +# +# PackageManager downloads packages from GitHub based on the GitHub version and package (stored) versions: +# if the GitHub branch is a specific version, the download occurs if the versions differ +# otherwise the GitHub version must be newer. +# the archive file is unpacked to a directory in /data named +# -.tar.gz, then moved to /data/, replacing the original +# +# PackageManager installs the stored verion if the package (stored) and installed versions differ +# +# Manual downloads and installs triggered from the GUI ignore version checks completely +# +# In this context, "install" means replacing the working copy of Venus OS files with the modified ones +# or adding new files and system services +# +# Uninstalling means replacing the original Venus OS files to their working locations +# +# All operations that access the global package list must do so surrounded by a lock to avoid accessing changing data +# this is very important when scanning the package list +# so that packages within that list don't get moved, added or deleted +# +# Operations that take signficant time are handled in separate threads, decoupled from the package list +# Operaitons are placed on a queue with all the information a processing routine needs +# this is imporant because the package in the list involved in the operaiton +# may no longer be in the package list or be in a different location +# +# All operations that scan the package list must do so surrounded by +# DbusIf.LOCK () and DbusIf.UNLOCK () +# and must not consume significant time: no sleeping or actions taking seconds or minutes !!!! +# +# Operations that take little time can usually be done in-line (without queuing) +# +# PackageManager manages flag files in the package folder: +# DO_NOT_AUTO_ADD indicates the package was manually removed and PackageManager should not +# automaticlly add it +# DO_NOT_AUTO_INSTALL indicates the package was manually removed and PackageManager should not +# automatically install it +# +# these flags are stored in /data/setupOptions/ which is non-volatile and survives a package download +# +# PackageManager checks removable media (SD cards and USB sticks) for package upgrades or even as a new package +# File names must be in one of the following forms: +# -.tar.gz +# -install.tar.gz +# The portion determines where the package will be stored in /data +# and will be used as the package name when the package is added to the package list in Settings +# +# If all criteria are met, the archive is unpacked and the resulting directory replaces /data/ +# if not, the unpacked archive directory is deleted +# +# +# PackageManager scans /data looking for new packages +# directory names must not appear to be an archive +# (include a GitHub branch or version number) (see rejectList below for specifics) +# the directory must contain a valid version +# the package must not have been manually removed (DO_NOT_AUTO_ADD flag file set) +# the file name must be unique to all existing packages +# +# A new, verified package will be added to the package list and be ready for +# manual and automtic updates, installs, uninstalls +# +# This mechanism handles archives extracted from SD/USB media +# +# +# Packages may optionally include a file containg GitHub user and branch +# if the package diretory contains the file: gitHubInfo +# gitHubUser and gitHubBranch are set from the file's content when it is added to the package list +# making the new package ready for automatic GitHub updates +# gitHubInfo should have a single line of the form: gitHubUser:gitHubBranch, e.g, kwindrem:latest +# if the package is already in the package list, gitHubInfo is ignored +# if no GitHub information is contained in the package, the user must add it manually via the GUI +# in so automatic downloads from GitHub can occur +# +# classes/instances/methods: +# AddRemoveClass +# AddRemove (thread) +# StopThread () +# run () +# DbusIfClass +# DbusIf +# SetGuiEditAction () +# UpdateStatus () +# LocateRawDefaultPackage () +# handleGuiEditAction +# () +# UpdatePackageCount () +# various Gets and Sets for dbus parameters +# TransferOldDbusPackageInfo +# UpdateDefaultPackages () +# ReadDefaultPackagelist () +# LOCK () +# UNLOCK () +# PackageClass +# PackageList [] one per package +# UpdateDownloadPending () +# UpdateInstallPending () +# LocatePackage () +# RemoveDbusSettings () +# settingChangedHandler () +# various Gets and Sets +# AddPackagesFromDbus () +# AddStoredPackages () +# AddPackage () +# RemovePackage () +# UpdateFileFlagsAndVersions () +# GetAutoAddOk (class method) +# SetAutoAddOk (class method) +# AutoInstallOk (class method) +# SetAutoInstallOk () +# MoveFlagFiles () +# InstallVersionCheck () +# UpdateGitHubVersionClass +# UpdateGitHubVersion (thread) +# updateGitHubVersion () +# run () +# StopThread () +# DownloadGitHubPackagesClass +# DownloadGitHub (thread) +# GitHubDownload () +# DownloadVersionCheck () +# processDownloadQueue () +# run () +# StopThread () +# InstallPackagesClass +# InstallPackages (thread) +# InstallPackage () +# StopThread () +# run () +# MediaScanClass +# MediaScan (thread) +# transferPackage +# StopThread () +# settingsBackup +# settingsRestore +# run () +# +# global methods: +# PushAction () +# VersionToNumber () +# LocatePackagePath () +# AutoRebootCheck () + + +import platform +import argparse +import logging + +# set variables for logging levels: +CRITICAL = 50 +ERROR = 40 +WARNING = 30 +INFO = 20 +DEBUG = 10 + +import sys +import subprocess +import threading +import os +import shutil +import dbus +import time +import re +import glob + +PythonVersion = sys.version_info +# accommodate both Python 2 and 3 +if PythonVersion >= (3, 0): + import queue + from gi.repository import GLib +else: + import Queue as queue + import gobject as GLib + +# add the path to our own packages for import +sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'ext', 'velib_python')) +from vedbus import VeDbusService +from settingsdevice import SettingsDevice + +global DownloadGitHub +global InstallPackages +global AddRemove +global MediaScan +global DbusIf +global Platform +global VenusVersion +global VenusVersionNumber +global SystemReboot +global GuiRestart +global PackageIndex + +global AllVersionsRefreshed + + +# PushAction +# +# add an action to one of three queues: +# InstallPackages.InstallQueue for Install and Uninstall actions +# Download.Download for Download actions +# AddRemoveQueue +# commands are added to the queue from the GUI (dbus service change handler) +# and from the main loop (source = 'AUTO') +# the queue isolates command triggers from processing because processing +# can take seconds or minutes +# +# command is a string: action:packageName +# +# action is a text string: Install, Uninstall, Download, Add, Remove, etc +# packageName is the name of the package to receive the action +# for some actions this may be the null string +# +# the command and source are pushed on the queue as a tuple +# +# PushAction sets the ...Pending flag to prevent duplicate operations +# for a given package +# +# the 'Reboot' and 'restartGui' actions are NOT pushed on any queue +# they are handled in line since they just set a global flag +# to be handled in mainLoop + +def PushAction (command=None, source=None): + parts = command.split (":") + if len (parts) >= 1: + action = parts[0] + else: + action = "" + if len (parts) >= 2: + packageName = parts[1] + else: + packageName = "" + if action == 'download': + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + if package != None: + package.DownloadPending = True + DbusIf.UNLOCK () + queue = DownloadGitHub.DownloadQueue + queueText = "Download" + elif action == 'install' or action == 'uninstall': + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + if package != None: + package.InstallPending = True + DbusIf.UNLOCK () + queue = InstallPackages.InstallQueue + queueText = "Install" + elif action == 'add' or action == 'remove': + queue = AddRemove.AddRemoveQueue + queueText = "AddRemove" + elif action == 'reboot': + logging.warning ( "received Reboot request from " + source) + # set the flag - reboot is done in main_loop + global SystemReboot + SystemReboot = True + elif action == 'restartGui': + logging.warning ( "received GUI restart request from " + source) + # set the flag - reboot is done in main_loop + global GuiRestart + GuiRestart = True + # ignore blank action - this occurs when PackageManager changes the action on dBus to 0 + # which acknowledges a GUI action + elif action == '': + return + else: + logging.error ("PushAction received unrecognized command: " + command) + return + + try: + queue.put ( (command, source), block=False ) + except queue.Full: + logging.error ("command " + command + " from " + source + " lost - " + ququeText + " - queue full") + except: + logging.error ("command " + command + " from " + source + " lost - " + ququeText + " - other queue error") +# end PushAction + + +# VersionToNumber +# +# convert a version string in the form of vX.Y~Z-large-W to an integer to make comparisions easier +# the ~Z portion indicates a pre-release version so a version without it is later than a version with it +# the -W portion is like the ~Z for large builds +# the large portion is IGNORED !!!! +# note part[0] is always null because there is nothing before v which is used as a separator +# +# the incoming verison string is split on every non-digit character +# the first three of those pieces are combined into a single integer +# additional pieces are ignored +# +# for example v1.2~3 would be 1002003 +# for example v11.22.33 would be 11022033 +# for example v11.22-large-33 would be 11022999 +# an empty file or one that contains "unknown" or does not beging with 'v' +# has a version number = 0 +# +# the separators ~, b, a and d are given special significance +# indicating beta (~ and b), alpha or develompent releases +# which are prioritized: release is the highest, then beta, then alpha then development +# by spliting the third number range into individual ranges +# a released version is given the highest numerical value: 999 +# none for release -- 999 +# ~ or b for beta 0-398 600-998 +# a for alpha 0-299 300-599 +# d for development 0-299 0-299 +# +# returns the version number + +def VersionToNumber (version): + if version == None or version == "" or version[0] != 'v': + return 0 + + version = version.replace ("large","L") + parts = re.split ('\D', version[1:]) + + preReleasePart = False + # assemble core version number from major, minor and preRelease parts + versionNumber = 0 + if len(parts) >= 1: + if parts[0].isdigit(): + versionNumber += int ( parts[0] ) * 1000000 + if len(parts) >= 2: + if parts[1].isdigit(): + versionNumber += int ( parts[1] ) * 1000 + if len(parts) >= 3: + if parts[2].isdigit(): + versionNumber += int ( parts[2] ) + preReleasePart = True + + # adjust based on type + if '~' in version: + versionNumber += 600 + elif 'b' in version: + versionNumber += 600 + elif 'a' in version: + versionNumber += 300 + # a release version should not contain a third part, but use it as-is if present + elif not preReleasePart: + versionNumber += 999 + return versionNumber + + +# LocatePackagePath +# +# attempt to locate a package directory +# +# all directories at the current level are checked +# to see if they contain a file named 'version' +# indicating a package directory has been found +# +# further, the version file must begin with 'v' +# +# if so, that path is returned +# +# if a directory NOT containing 'version' is found +# this method is called again to look inside that directory +# +# if nothing is found, the method returns None +# +# all recursive calls will return with the located package or None +# so the original caller will have the path to the package or None + +def LocatePackagePath (origPath): + paths = os.listdir (origPath) + for path in paths: + newPath = origPath +'/' + path + if os.path.isdir(newPath): + # found version file, make sure it is "valid" + versionFile = newPath + "/version" + if os.path.isfile( versionFile ): + fd = open ( versionFile, 'r' ) + version = fd.readline().strip() + fd.close() + if version[0] == 'v': + return newPath + else: + logging.error ("version file not a valid version " + versionFile + " = " + version ) + else: + packageDir = locatePackagePath (newPath) + # found a package directory + if packageDir != None: + return packageDir + # nothing found - continue looking in this directory + else: + continue + return None + + +# AddRemoveClass +# Instances: +# AddRemove (a separate thread) +# +# Methods: +# run ( the thread ) +# StopThread () +# +# Install and Uninstall actions are processed by +# the InstallPackages thread +# Download actions are processed by +# the DownloadGitHub thread +# Add and Remove actions are processed in this thread +# +# a queue isolates the caller from processing time +# and interactions with the dbus object +# (can't update the dbus object from it's handler !) +# +# some actions called may take seconds or minutes (based on internet speed) !!!! +# +# the queue entries are: ("action":"packageName") +# this decouples the action from the current package list which could be changing +# allowing the operation to proceed without locking the list + +class AddRemoveClass (threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + self.AddRemoveQueue = queue.Queue (maxsize = 10) + self.threadRunning = True + + + + # AddRemove run (the thread), StopThread + # + # run is a thread that pulls actions from a queue and processes them + # Note: some processing times can be several seconds to a minute or more + # due to newtork activity + # + # run () checks the threadRunning flag and returns if it is False, + # essentially taking the thread off-line + # the main method should catch the tread with join () + # StopThread () is called to shut down the thread + + def StopThread (self): + logging.info ("attempting to stop AddRemove thread") + self.threadRunning = False + self.AddRemoveQueue.put ( ('STOP', ''), block=False ) + + # AddRemove run () + # + # process package Add/Remove actions + def run (self): + while self.threadRunning: + try: + command = self.AddRemoveQueue.get () + except: + logging.error ("pull from AddRemoveQueue failed") + continue + if len (command) == 0: + logging.error ("pull from AddRemove queue failed - empty comand") + time.sleep (5.0) + continue + # thread shutting down + if command [0] == 'STOP' or self.threadRunning == False: + return + + # separate command, source tuple + # and separate action and packageName + if len (command) >= 2: + parts = command[0].split (":") + if len (parts) >= 2: + action = parts[0].strip () + packageName = parts[1].strip () + else: + logging.error ("AddRemoveQueue - no action and/or package name - discarding", command) + time.sleep (5.0) + continue + source = command[1] + else: + logging.error ("AddRemoveQueue - no command and/or source - discarding", command) + time.sleep (5.0) + continue + + if action == 'add': + packageDir = "/data/" + packageName + if source == 'GUI': + user = DbusIf.EditPackage.GitHubUser + branch = DbusIf.EditPackage.GitHubBranch + else: + user = "" + branch = "" + # try to get GitHub info from package directory + if user == "": + if os.path.isdir (packageDir): + gitHubInfoFile = packageDir + "/gitHubInfo" + try: + fd = open (gitHubInfoFile, 'r') + parts = fd.readline().strip ().split (':') + fd.close() + except: + parts = "" + if len (parts) >= 2: + user = parts[0] + branch = parts[1] + # still nothing - try to get GitHub info from default package list + if user == "": + default = DbusIf.LocateRawDefaultPackage (packageName) + if default != None: + user = default[1] + branch = default[2] + + PackageClass.AddPackage (packageName = packageName, source=source, + gitHubUser=user, gitHubBranch=branch ) + + elif action == 'remove': + PackageClass.RemovePackage ( packageName ) + + else: + logging.warning ( "received invalid action " + command + " from " + source + " - discarding" ) + # end while True + # end run () +# end AddRemoveClass + + +# DbusIfClass +# Instances: +# DbusIf +# +# Methods: +# SetGuiEditAction +# UpdateStatus +# LocateRawDefaultPackage +# handleGuiEditAction +# UpdatePackageCount +# RemoveDbusSettings +# TransferOldDbusPackageInfo +# UpdateDefaultPackages () +# ReadDefaultPackagelist () +# various Gets and Sets for dbus parameters +# LOCK +# UNLOCK +# +# Globals: +# DbusSettings (for settings that are NOT part of a package) +# DbusService (for parameters that are NOT part of a package) +# EditPackage - the dbus Settings used by the GUI to hand off information about +# a new package +# DefaultPackages - list of default packages, each a tuple: +# ( packageName, gitHubUser, gitHubBranch) +# +# DbusIf manages the dbus Settings and packageManager dbus service parameters +# that are not associated with any spcific package +# +# unlike those managed in PackageClass which DO have a package association +# the dbus settings managed here don't have a package association +# however, the per-package parameters are ADDED to +# DbusSettings and dBusService created here !!!! +# +# DbusIf manages a lock to prevent data access in one thread +# while it is bein`g changed in another +# the same lock is used to protect data in PackageClass also +# this is more global than it needs to be but simplies the locking +# +# all methods that access must aquire this lock +# prior to accessing DbusIf or Package data +# then must release the lock +# +# default package info is fetched from a file and published to our dbus service +# for use by the GUI in adding new packages +# it the default info is also stored in DefaultPackages +# LocateRawDefaultPackage is used to retrieve the default from local storage +# rather than pulling from dbus or reading the file again + +class DbusIfClass: + + # RemoveDbusSettings + # remove the dbus Settings paths for package + # package Settings are removed + # this is called when removing a package + # settings to be removed are passed as a list (settingsList) + # this gets reformatted for the call to dbus + + @classmethod + def RemoveDbusSettings (cls, settingsList): + + # format the list of settings to be removed + i = 0 + while i < len (settingsList): + if i == 0: + settingsToRemove = '%[ "' + settingsList[i] + else: + settingsToRemove += '" , "' + settingsList[i] + i += 1 + settingsToRemove += '" ]' + + # remove the dbus Settings paths - via the command line + try: + proc = subprocess.Popen (['dbus', '-y', 'com.victronenergy.settings', '/', 'RemoveSettings', settingsToRemove ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + logging.error ("dbus RemoveSettings call failed") + else: + proc.wait() + # convert from binary to string + out, err = proc.communicate () + stdout = out.decode ().strip () + stderr = err.decode ().strip () + returnCode = proc.returncode + if returnCode != 0: + logging.error ("dbus RemoveSettings failed " + str (returnCode)) + logging.error ("stderr: " + stderr) + + + # TransferOldDbusPackageInfo + # PackageManager dbus storage was moved + # from ...PackageMonitor... to ...PackageManager... + # this method moves the info to the new location and deletes the old Settings + # this assumes the new dbus environment is already set up + # the transfer is only done if the new location has no packages + # should only be called from initialization so we don't LOCK while accessing the package list + + + @classmethod + def TransferOldDbusPackageInfo (cls): + bus = dbus.SystemBus() + oldPath = "/Settings/PackageMonitor" + newPath = "/Settings/PackageManager" + try: + oldCount = bus.get_object("com.victronenergy.settings", oldPath + "/Count").GetValue() + # nothing to tranfer/delete + except: + return + + + try: + newCount = bus.get_object("com.victronenergy.settings", newPath + "/Count").GetValue() + except: + logging.error ("PackageManager dbus Settings has no package count") + return + + # if the new dbus info has no packages, transfer them from the old location + if newCount == 0: + transferPackages = True + else: + transferPackages = False + + logging.warning ("moving PackageManager dbus settings from old location") + + # remove package-related Settings + i = 0 + while i < oldCount: + oldNamePath = oldPath + '/' + str (i) + "/PackageName" + oldUserPath = oldPath + '/' + str (i) + "/GitHubUser" + oldBranchPath = oldPath + '/' + str (i) + "/GitHubBranch" + + # create a new package and transfer old info to it + try: + name = bus.get_object("com.victronenergy.settings", oldNamePath).GetValue () + except: + name = None + if transferPackages and name != None: + logging.warning ("moving " + name + " settings") + + try: + user = bus.get_object("com.victronenergy.settings", (oldUserPath)).GetValue () + except: + user = "?" + try: + branch = bus.get_object("com.victronenergy.settings", oldBranchPath).GetValue () + except: + branch = "?" + + PackageClass.AddPackage (packageName=name, source='TRANSFER', + gitHubUser= user, gitHubBranch=branch ) + + # remove the old package-related dbus Settings + cls.RemoveDbusSettings ( [oldNamePath, oldUserPath, oldBranchPath ] ) + i += 1 + + # transfer and remove Settings not part of a package + if transferPackages: + + DbusIf.SetAutoInstall ( bus.get_object("com.victronenergy.settings", + oldPath + "/AutoInstall").GetValue () ) + DbusIf.SetAutoDownload ( bus.get_object("com.victronenergy.settings", + oldPath + "/GitHubAutoDownload").GetValue () ) + otherSettings = [oldPath + "/AutoInstall", + oldPath + "/GitHubAutoDownload", + oldPath + "/Edit/GitHubBranch", + oldPath + "/Edit/GitHubUser", + oldPath + "/Edit/PackageName", + oldPath + "/Count" + ] + cls.RemoveDbusSettings ( otherSettings ) + + + # UpdateStatus + # + # updates the status when the operation completes + # the GUI provides three different areas to show status + # where specifies which of these are updated + # 'Download' + # 'Install' + # 'Editor' + # 'Media' + # which determines where status is sent + # message is the text displayed + # if LogLevel is not 0, message is also written to the PackageManager log + # logging levels: (can use numeric value or these variables set at head of module + # CRITICAL = 50 + # ERROR = 40 + # WARNING = 30 + # INFO = 20 + # DEBUG = 10 + # if where = None, no GUI status areas are updated + + + def UpdateStatus ( self, message=None, where=None, logLevel=0 ): + + if logLevel != 0: + logging.log ( logLevel, message ) + + if where == 'Editor': + DbusIf.SetEditStatus ( message ) + elif where == 'Install': + DbusIf.SetInstallStatus ( message ) + elif where == 'Download': + DbusIf.SetGitHubUpdateStatus (message) + elif where == 'Media': + DbusIf.SetMediaStatus (message) + + + # handleGuiEditAction (internal use only) + # + # the GUI uses packageMonitor service /GuiEditAction + # to inform PackageManager of an action + # a command is formed as "action":"packageName" + # + # action is a text string: install, uninstall, download, etc + # packageName is the name of the package to receive the action + # for some acitons this may be the null string + # this handler disposes of the request quickly by pushing + # the command onto a queue for later processing + + def handleGuiEditAction (self, path, command): + global PushAction + PushAction ( command=command, source='GUI' ) + + return True # True acknowledges the dbus change - other wise dbus parameter does not change + + def UpdatePackageCount (self): + count = len(PackageClass.PackageList) + self.DbusSettings['packageCount'] = count + def GetPackageCount (self): + return self.DbusSettings['packageCount'] + def SetAutoDownload (self, value): + self.DbusSettings['autoDownload'] = value + def GetAutoDownloadMode (self): + return self.DbusSettings['autoDownload'] + def GetAutoInstall (self): + return self.DbusSettings['autoInstall'] + def SetAutoInstall (self, value): + self.DbusSettings['autoInstall'] = value + def SetGitHubUpdateStatus (self, value): + self.DbusService['/GitHubUpdateStatus'] = value + def SetInstallStatus (self, value): + self.DbusService['/InstallStatus'] = value + def SetMediaStatus (self, value): + self.DbusService['/MediaUpdateStatus'] = value + + def SetDefaultCount (self, value): + self.DbusService['/DefaultCount'] = value + def GetDefaultCount (self): + return self.DbusService['/DefaultCount'] + + def SetBackupMediaAvailable (self, value): + if value == True: + dbusValue = 1 + else: + dbusValue = 0 + self.DbusService['/BackupMediaAvailable'] = dbusValue + def GetBackupMediaAvailable (self): + if self.DbusService['/BackupMediaAvailable'] == 1: + return True + else: + return True + + def SetBackupSettingsFileExist (self, value): + if value == True: + dbusValue = 1 + else: + dbusValue = 0 + self.DbusService['/BackupSettingsFileExist'] = dbusValue + def GetBackupSettingsFileExist (self): + if self.DbusService['/BackupSettingsFileExist'] == 1: + return True + else: + return True + + def SetBackupProgress (self, value): + self.DbusService['/BackupProgress'] = value + def GetBackupProgress (self): + return self.DbusService['/BackupProgress'] + + + # SetGuiEditAction + # is part of the PackageManager to GUI communication + # the GUI set's an action triggering some processing here + # via the dbus change handler + # PM updates this dbus value when processing completes + # signaling either success or failure + + def SetGuiEditAction (self, value): + self.DbusService['/GuiEditAction'] = value + def GetGuiEditAction (self): + return self.DbusService['/GuiEditAction'] + def SetEditStatus (self, message): + self.DbusService['/GuiEditStatus'] = message + + def SetActionNeeded (self, message): + self.DbusService['/ActionNeeded'] = message + + # search RAW default package list for packageName + # and return the pointer if found + # otherwise return None + # + # Note: the raw default package list is built during init + # then never changes so LOCK/UNLOCK is NOT needed + # + # rawDefaultPackages is a list of tuples: + # (packageName, gitHubUser, gitHubBranch) + # + # if a packageName match is found, the tuple is returned + # otherwise None is retuned + + def LocateRawDefaultPackage (self, packageName): + + for default in self.rawDefaultPackages: + if packageName == default[0]: + return default + return None + + + # UpdateDefaultPackages + # + # refreshes the defaultPackageList to include only packages NOT be in PackageList + # this also updates the dbus default packages used by the GUI Add Package menu + # skip unless package was added/removed ???? + # alwaysRun forces processing for the init pass from main () + + def UpdateDefaultPackages (self, alwaysRun = False): + global AddRemove + if alwaysRun == False and self.RefreshDefaultPackages == False: + return + self.RefreshDefaultPackages = False + + DbusIf.LOCK () + # don't touch new entry (index 0) + index = 1 + oldDefaultCount = len (self.defaultPackageList) + for default in self.rawDefaultPackages: + # if not in the main package list, add it to the default package list + name = default[0] + if PackageClass.LocatePackage (name) == None: + user = default[1] + branch = default[2] + prefix = '/Default/' + str (index) + '/' + # this entry already exists - update it + if index < oldDefaultCount: + # name has changed, update the entry (local and dbus) + if (name != self.defaultPackageList[index][0]): + self.defaultPackageList[index] = default + self.DbusService[prefix + 'PackageName'] = name + self.DbusService[prefix + 'GitHubUser'] = user + self.DbusService[prefix + 'GitHubBranch'] = branch + # path doesn't yet exist, add it + else: + self.defaultPackageList.append (default) + self.DbusService.add_path (prefix + 'PackageName', name ) + self.DbusService.add_path (prefix + 'GitHubUser', user ) + self.DbusService.add_path (prefix + 'GitHubBranch', branch ) + + index += 1 + + self.DbusService['/DefaultCount'] = index + + # clear out any remaining path values + while index < oldDefaultCount: + prefix = '/Default/' + str (index) + '/' + self.defaultPackageList[index] = ( "", "", "" ) + self.DbusService[prefix + 'PackageName'] = "" + self.DbusService[prefix + 'GitHubUser'] = "" + self.DbusService[prefix + 'GitHubBranch'] = "" + index += 1 + + DbusIf.UNLOCK () + + + # ReadDefaultPackagelist + # + # read in the default packages list file and store info locally for faster access later + # this list is only used to populate the defaultPackageList which excludes packages that + # are in the main Packagelist + + def ReadDefaultPackagelist (self): + + try: + listFile = open ("/data/SetupHelper/defaultPackageList", 'r') + except: + logging.error ("no defaultPackageList " + listFileName) + else: + for line in listFile: + parts = line.split () + if len(parts) < 3 or line[0] == "#": + continue + self.rawDefaultPackages.append ( ( parts[0], parts[1], parts[2] ) ) + listFile.close () + + + # LOCK and UNLOCK - capitals used to make it easier to identify in the code + # + # these protect the package list from changing while the list is being accessed + + def LOCK (self): + self.lock.acquire () + def UNLOCK (self): + self.lock.release () + + + def __init__(self): + self.lock = threading.RLock() + + settingsList = {'packageCount': [ '/Settings/PackageManager/Count', 0, 0, 0 ], + 'autoDownload': [ '/Settings/PackageManager/GitHubAutoDownload', 0, 0, 0 ], + 'autoInstall': [ '/Settings/PackageManager/AutoInstall', 0, 0, 0 ], + } + self.DbusSettings = SettingsDevice(bus=dbus.SystemBus(), supportedSettings=settingsList, + timeout = 10, eventCallback=None ) + + self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus()) + self.DbusService.add_mandatory_paths ( + processname = 'PackageManager', processversion = 1.0, connection = 'none', + deviceinstance = 0, productid = 1, productname = 'Package Monitor', + firmwareversion = 1, hardwareversion = 0, connected = 1) + self.DbusService.add_path ( '/GitHubUpdateStatus', "", writeable = True ) + self.DbusService.add_path ( '/InstallStatus', "", writeable = True ) + self.DbusService.add_path ( '/MediaUpdateStatus', "", writeable = True ) + self.DbusService.add_path ( '/GuiEditStatus', "", writeable = True ) + + global Platform + self.DbusService.add_path ( '/Platform', Platform ) + + self.DbusService.add_path ( '/GuiEditAction', "", writeable = True, + onchangecallback = self.handleGuiEditAction ) + + # initialize default package list to empty - entries will be added later + self.DbusService.add_path ('/DefaultCount', 0 ) + + # a special package used for editing a package prior to adding it to Package list + self.EditPackage = PackageClass (section = "Edit") + + self.rawDefaultPackages = [] + self.defaultPackageList = [] + + # create first default package, place where a new package is entered from scratch + self.defaultPackageList.append ( ("new", "", "") ) + self.DbusService.add_path ( "/Default/0/PackageName", "new" ) + self.DbusService.add_path ( "/Default/0/GitHubUser", "" ) + self.DbusService.add_path ( "/Default/0/GitHubBranch", "" ) + + # used to notify the GUI that an action is required to complete a manual installation + # the operator has the option to defer reboot and GUI restart operations + # if they do, this parameter is set and a button appears on the main Package manager menu + + self.DbusService.add_path ( "/ActionNeeded", '' ) + + self.RefreshDefaultPackages = False + + self.DbusService.add_path ( '/BackupMediaAvailable', 0, writeable = True ) + self.DbusService.add_path ( '/BackupSettingsFileExist', 0, writeable = True ) + self.DbusService.add_path ( '/BackupProgress', 0, writeable = True ) + + + # RemoveDbusService + # deletes the dbus service + + def RemoveDbusService (self): + logging.warning ("shutting down com.victronenergy.packageManager dbus service") + self.DbusService.__del__() + +# end DbusIf + + +# PackageClass +# Instances: +# one per package +# +# Methods: +# LocatePackage +# various Gets and Sets +# UpdateDownloadPending () (class method) +# UpdateInstallPending () (class method) +# AddPackagesFromDbus (class method) +# AddStoredPackages (class method) +# AddPackage (class method) +# RemovePackage (class method) +# UpdateFileFlagsAndVersions () +# GetAutoAddOk (class method) +# SetAutoAddOk (class method) +# AutoInstallOk (class method) +# SetAutoInstallOk () +# MoveFlagFiles () +# InstallVersionCheck () +# +# Globals: +# DbusSettings (for per-package settings) +# DbusService (for per-package parameters) +# DownloadPending +# InstallPending +# PackageList - list instances of all packages +# +# a package consits of Settings and version parameters in the package monitor dbus service +# all Settings and parameters are accessible via set... and get... methods +# so that the caller does not need to understand dbus Settings and service syntax +# the packageName variable maintains a local copy of the dBus parameter for speed in loops +# section passed to init can be either a int or string ('Edit') +# an int is converted to a string to form the dbus setting paths +# +# the dbus settings and service parameters managed here are on a per-package basis +# unlike those managed in DbusIf which don't have a package association + +class PackageClass: + + # list of instantiated Packages + PackageList = [] + + # search PackageList for packageName + # and return the package pointer if found + # otherwise return None + # + # Note: this method should be called with LOCK () set + # and use the returned value before UNLOCK () + # to avoid unpredictable results + + @classmethod + def LocatePackage (cls, packageName): + for package in PackageClass.PackageList: + if packageName == package.PackageName: + t1 = time.time () + return package + t1 = time.time () + return None + + + # this set of methods manages the flag files that control + # automaticly adding and installing packages + # if a package is manually removed, it should not + # be readded automatically + # ditto for manual uninstall + # + + @classmethod + def GetAutoAddOk (cls, packageName): + if packageName == None: + logging.error ("GetAutoAddOk - no packageName") + return False + + flagFile = "/data/setupOptions/" + packageName + "/DO_NOT_AUTO_ADD" + if os.path.exists (flagFile): + return False + else: + return True + + + @classmethod + def SetAutoAddOk (cls, packageName, state): + if packageName == None: + logging.error ("SetAutoAddOk - no packageName") + return + + # if package options directory exists set/clear auto add flag + # directory may not exist if package was never downloaded or transferred from media + # or if package was added manually then never acted on + optionsDir = "/data/setupOptions/" + packageName + if os.path.exists (optionsDir): + flagFile = optionsDir + "/DO_NOT_AUTO_ADD" + # permit auto add + if state == True: + if os.path.exists (flagFile): + os.remove (flagFile) + # block auto add + else: + if not os.path.exists (flagFile): + # equivalent to unix touch command + open (flagFile, 'a').close() + + + def SetAutoInstallOk (self, state): + packageName = self.PackageName + if packageName == None: + logging.error ("SetAutoInstallOk - no packageName") + return + + # if package options directory exists set/clear auto add flag + # directory may not exist if package was never downloaded or transferred from media + # or if package was added manually then never acted on + optionsDir = "/data/setupOptions/" + packageName + if os.path.exists (optionsDir): + flagFile = optionsDir + "/DO_NOT_AUTO_INSTALL" + # permit auto installs + if state == True: + if os.path.exists (flagFile): + os.remove (flagFile) + # block auto install + else: + if not os.path.exists (flagFile): + open (flagFile, 'a').close() + + # move flag files that prevent automatic downloads or installs + # from to /setupOptions/ + # called only during init + + def MoveFlagFiles (self): + packageName = self.PackageName + if packageName == None: + logging.error ("MoveFlagFiles - no packageName") + return + + # nothing to do if package directory doesn't exist + packageDir = "/data/" + packageName + if not os.path.isdir (packageDir): + return + + # create setupOptions directory if it doesn't already exist + # this would happen if the package was being added / installed for the first time + optionsPath = "/data/setupOptions/" + packageName + if not os.path.isdir (optionsPath): + os.mkdir (optionsPath) + + # move auto add block flag + oldFlag = packageDir + "/REMOVED" + flagFile = optionsPath + "/DO_NOT_AUTO_ADD" + if os.path.exists (oldFlag): + open (flagFile, 'a').close() + os.remove (oldFlag) + + # move auto install block flag + oldFlag = packageDir + "/DO_NOT_AUTO_INSTALL" + flagFile = optionsPath + "/DO_NOT_AUTO_INSTALL" + if os.path.exists (oldFlag): + open (flagFile, 'a').close() + os.remove (oldFlag) + + + # InstallVersionCheck + # + # compares versions to determine if an install is needed + # returns True if an update is needed, False of not + # + # called from main loop + + def InstallVersionCheck (self): + + if self.Incompatible != "": + return False + + packageVersion = self.PackageVersion + # skip further checks if package version string isn't filled in + if packageVersion == "" or packageVersion[0] != 'v': + return False + + # skip install if versions are the same + if self.PackageVersionNumber == self.InstalledVersionNumber: + return False + else: + return True + + + def SetPackageName (self, newName): + self.DbusSettings['packageName'] = newName + self.PackageName = newName + + def SetInstalledVersion (self, version): + global VersionToNumber + self.InstalledVersion = version + self.InstalledVersionNumber = VersionToNumber (version) + if self.installedVersionPath != "": + DbusIf.DbusService[self.installedVersionPath] = version + + def SetPackageVersion (self, version): + global VersionToNumber + self.PackageVersion = version + self.PackageVersionNumber = VersionToNumber (version) + if self.packageVersionPath != "": + DbusIf.DbusService[self.packageVersionPath] = version + + def SetGitHubVersion (self, version): + global VersionToNumber + self.GitHubVersion = version + self.GitHubVersionNumber = VersionToNumber (version) + if self.gitHubVersionPath != "": + DbusIf.DbusService[self.gitHubVersionPath] = version + + def SetGitHubUser (self, user): + self.GitHubUser = user + self.DbusSettings['gitHubUser'] = user + + def SetGitHubBranch (self, branch): + self.GitHubBranch = branch + self.DbusSettings['gitHubBranch'] = branch + + def SetIncompatible (self, value): + self.Incompatible = value + if self.incompatiblePath != "": + DbusIf.DbusService[self.incompatiblePath] = value + + def SetRebootNeeded (self, value): + self.RebootNeeded = value + if self.rebootNeededPath != "": + if value == True: + DbusIf.DbusService[self.rebootNeededPath] = 1 + else: + DbusIf.DbusService[self.rebootNeededPath] = 0 + def SetGuiRestartNeeded (self, value): + self.GuiRestartNeeded = value + if self.guiRestartNeededPath != "": + if value == True: + DbusIf.DbusService[self.guiRestartNeededPath] = 1 + else: + DbusIf.DbusService[self.guiRestartNeededPath] = 0 + + + def settingChangedHandler (self, name, old, new): + # when GitHub information changes, need to refresh GitHub version for this package + if name == 'packageName': + self.PackageName = new + elif name == 'gitHubBranch': + self.GitHubBranch = new + if self.PackageName != None and self.PackageName != "": + UpdateGitHubVersion.SetPriorityGitHubVersion ( self.PackageName ) + elif name == 'gitHubUser': + self.GitHubUser = new + if self.PackageName != None and self.PackageName != "": + UpdateGitHubVersion.SetPriorityGitHubVersion ( self.PackageName ) + + def __init__( self, section, packageName = None ): + # add package versions if it's a real package (not Edit) + if section != 'Edit': + section = str (section) + self.gitHubVersionPath = '/Package/' + section + '/GitHubVersion' + self.packageVersionPath = '/Package/' + section + '/PackageVersion' + self.installedVersionPath = '/Package/' + section + '/InstalledVersion' + self.rebootNeededPath = '/Package/' + section + '/RebootNeeded' + self.guiRestartNeededPath = '/Package/' + section + '/GuiRestartNeeded' + self.incompatiblePath = '/Package/' + section + '/Incompatible' + + # create service paths if they don't already exist + try: + foo = DbusIf.DbusService[self.installedVersionPath] + except: + DbusIf.DbusService.add_path (self.installedVersionPath, "?" ) + try: + foo = DbusIf.DbusService[self.gitHubVersionPath] + except: + DbusIf.DbusService.add_path (self.gitHubVersionPath, "?" ) + try: + foo = DbusIf.DbusService[self.packageVersionPath] + except: + DbusIf.DbusService.add_path (self.packageVersionPath, "?" ) + try: + foo = DbusIf.DbusService[self.rebootNeededPath] + except: + DbusIf.DbusService.add_path (self.rebootNeededPath, False ) + try: + foo = DbusIf.DbusService[self.guiRestartNeededPath] + except: + DbusIf.DbusService.add_path (self.guiRestartNeededPath, False ) + try: + foo = DbusIf.DbusService[self.incompatiblePath] + except: + DbusIf.DbusService.add_path (self.incompatiblePath, "" ) + + + self.packageNamePath = '/Settings/PackageManager/' + section + '/PackageName' + self.gitHubUserPath = '/Settings/PackageManager/' + section + '/GitHubUser' + self.gitHubBranchPath = '/Settings/PackageManager/' + section + '/GitHubBranch' + + # mirror of dbus parameters to speed access + self.InstalledVersion = "?" + self.InstalledVersionNumber = 0 + self.PackageVersion = "?" + self.PackageVersionNumber = 0 + self.GitHubVersion = "?" + self.GitHubVersionNumber = 0 + self.GitHubUser = "?" + self.GitHubBranch = "?" + self.Incompatible = '' + self.RebootNeeded = '' + self.GuiRestartNeeded = '' + + + settingsList = {'packageName': [ self.packageNamePath, '', 0, 0 ], + 'gitHubUser': [ self.gitHubUserPath, '', 0, 0 ], + 'gitHubBranch': [ self.gitHubBranchPath, '', 0, 0 ], + } + self.DbusSettings = SettingsDevice(bus=dbus.SystemBus(), supportedSettings=settingsList, + eventCallback=self.settingChangedHandler, timeout = 10) + # if packageName specified on init, use that name + if packageName != None: + self.DbusSettings['packageName'] = packageName + self.PackageName = packageName + # otherwise pull name from dBus Settings + else: + self.PackageName = self.DbusSettings['packageName'] + self.GitHubUser = self.DbusSettings['gitHubUser'] + self.GitHubBranch = self.DbusSettings['gitHubBranch'] + + self.section = section + # these flags are used to insure multiple actions aren't executed on top of each other + self.DownloadPending = False + self.InstallPending = False + + + + # dbus Settings is the primary non-volatile storage for packageManager + # upon startup, PackageList [] is empty and we need to populate it + # from previous dBus Settings in /Settings/PackageManager/... + # this is a special case that can't use AddPackage below: + # we do not want to create any new Settings !! + # it should be "safe" to limit the serch to 0 to < packageCount + # we also don't specify any parameters other than the section (index) + # + # NOTE: this method is called before threads are created so do not LOCK + # + # returns False if couldn't get the package count from dbus + # otherwise returns True + # no package count on dbus is an error that would prevent continuing + # this should never happen since the DbusIf is instantiated before this call + # which creates /Count if it does not exist + + @classmethod + def AddPackagesFromDbus (cls): + global DbusIf + packageCount = DbusIf.GetPackageCount() + if packageCount == None: + logging.critical ("dbus PackageManager Settings not set up -- can't continue") + return False + i = 0 + while i < packageCount: + cls.PackageList.append(PackageClass (section = i)) + i += 1 + return True + + + # AddStoredPackages + # add packages stored in /data to the package list + # in order to qualify as a package: + # must be a directory + # name must not contain strings in the rejectList + # name must not include any spaces + # diretory must contain a file named version + # first character of version file must be 'v' + # name must be unique - that is not match any existing packages + + rejectList = [ "-current", "-latest", "-main", "-test", "-debug", "-beta", "-backup1", "-backup2", + "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9", " " ] + + @classmethod + def AddStoredPackages (cls): + for path in os.listdir ("/data"): + packageDir = "/data/" + path + if not os.path.isdir (packageDir): + continue + packageName = path + rejected = False + for reject in cls.rejectList: + if reject in packageName: + rejected = True + break + if rejected: + continue + + # skip if package was manually removed + if not PackageClass.GetAutoAddOk (packageName): + continue + # skip if package is for Raspberry PI only and platform is not + global Platform + if os.path.exists (packageDir + "/raspberryPiOnly") and Platform[0:4] != 'Rasp': + continue + + versionFile = packageDir + "/version" + try: + fd = open (versionFile, 'r') + version = fd.readline().strip() + fd.close () + except: + continue + if version[0] != 'v': + continue + + # continue only if package is unique + DbusIf.LOCK () + package = cls.LocatePackage (packageName) + DbusIf.UNLOCK () + if package == None: + PushAction (command='add:' + packageName, source='AUTO') + + # the DownloadPending and InstallPending flags prevent duplicate actions for the same package + # and holds off reboots and GUI resets until all actions are complete + # + # packageName rather than a package list reference (index) + # is used because the latter can change when packages are removed + # if you have a package pointer, set the parameter directly to save time + + @classmethod + def UpdateDownloadPending (self, packageName, state): + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + if package != None: + package.DownloadPending = state + DbusIf.UNLOCK () + + @classmethod + def UpdateInstallPending (cls, packageName, state): + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + if package != None: + package.InstallPending = state + DbusIf.UNLOCK () + + + + # AddPackage adds one package to the package list + # packageName must be specified + # the package names must be unique + # + # this method is called from the GUI add package command + + @classmethod + def AddPackage ( cls, packageName=None, gitHubUser=None, gitHubBranch=None, source=None ): + if source == 'GUI': + reportStatusTo = 'Editor' + # AUTO or DEFAULT source + else: + reportStatusTo = None + + if packageName == None or packageName == "": + DbusIf.UpdateStatus ( message="no package name for AddPackage - nothing done", + where=reportStatusTo, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + return False + + # insure packageName is unique before adding this new package + matchFound = False + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + + # new packageName is unique, OK to add it + if package == None: + DbusIf.UpdateStatus ( message="adding " + packageName, where='Editor', logLevel=WARNING ) + + section = len(cls.PackageList) + cls.PackageList.append( PackageClass ( section, packageName = packageName ) ) + DbusIf.UpdatePackageCount () + DbusIf.RefreshDefaultPackages = True + + # add user/branch from caller + package = PackageClass.LocatePackage (packageName) + if package != None: + if gitHubUser == None: + gitHubUser = "?" + if gitHubBranch == None: + gitHubBranch = "?" + package.SetGitHubUser (gitHubUser) + package.SetGitHubBranch (gitHubBranch) + + if source == 'GUI': + DbusIf.SetGuiEditAction ( '' ) + DbusIf.UpdateStatus ( message = "", where='Editor') + # package added from the GUI (aka, manually) + # allow auto adds + PackageClass.SetAutoAddOk (packageName, True) + else: + if source == 'GUI': + DbusIf.UpdateStatus ( message=packageName + " already exists - choose another name", where=reportStatusTo, logLevel=WARNING ) + DbusIf.SetGuiEditAction ( 'ERROR' ) + else: + DbusIf.UpdateStatus ( message=packageName + " already exists", where=reportStatusTo, logLevel=WARNING ) + + DbusIf.UNLOCK () + # end AddPackage + + # packages are removed as a request from the GUI + # to remove a package: + # 1) locate the entry matching package name (if any) + # 2) move all packages after that entry the previous slot (if any) + # 3) erase the last package slot to avoid confusion (by looking at dbus-spy) + # 3) remove the entry in PackageList (pop) + # 4) update the package count + # 5) set DO_NOT_AUTO_ADD flag file to prevent + # package from being re-added to the package list + # flag file is deleted when package is manually installed again + # + # returns True if package was removed, False if not + # + # this is all done while the package list is locked !!!! + + @classmethod + def RemovePackage (cls, packageName ): + if packageName == "SetupHelper": + DbusIf.UpdateStatus ( message="REMOVING SetupHelper" + packageName, where='Editor', logLevel=CRITICAL ) + else: + DbusIf.UpdateStatus ( message="removing " + packageName, where='Editor', logLevel=WARNING ) + DbusIf.LOCK () + packages = PackageClass.PackageList + if len (packages) == 0: + DbusIf.UNLOCK () + return + + # locate index of packageName + toIndex = 0 + listLength = len (packages) + matchFound = False + while toIndex < listLength: + if packageName == packages[toIndex].PackageName: + matchFound = True + break + toIndex += 1 + + if matchFound: + # move packages after the one to be remove down one slot (copy info) + # each copy overwrites the lower numbered package + fromIndex = toIndex + 1 + while fromIndex < listLength: + # dbus Settings + toPackage = packages[toIndex] + fromPackage = packages[fromIndex] + toPackage.SetPackageName (fromPackage.PackageName ) + toPackage.SetGitHubUser (fromPackage.GitHubUser ) + toPackage.SetGitHubBranch (fromPackage.GitHubBranch ) + + # dbus service params + toPackage.SetGitHubVersion (fromPackage.GitHubVersion ) + toPackage.SetPackageVersion (fromPackage.PackageVersion ) + toPackage.SetInstalledVersion (fromPackage.InstalledVersion ) + toPackage.SetRebootNeeded (fromPackage.RebootNeeded ) + toPackage.SetGuiRestartNeeded (fromPackage.GuiRestartNeeded ) + toPackage.SetIncompatible (fromPackage.Incompatible ) + + # package variables + toPackage.DownloadPending = fromPackage.DownloadPending + toPackage.InstallPending = fromPackage.InstallPending + + toIndex += 1 + fromIndex += 1 + + # here, toIndex points to the last package in the old list + toPackage = packages[toIndex] + + # can't actually remove service paths cleanly + # so just set contents to null/False + # they will disappear after PackageManager is started the next time + toPackage.SetGitHubVersion ("?") + toPackage.SetInstalledVersion ("?") + toPackage.SetPackageVersion ("?") + toPackage.SetRebootNeeded (False) + toPackage.SetGuiRestartNeeded (False) + toPackage.SetIncompatible (False) + + # remove the Settings and service paths for the package being removed + DbusIf.RemoveDbusSettings ( [toPackage.packageNamePath, toPackage.gitHubUserPath, toPackage.gitHubBranchPath] ) + + # remove entry from package list + packages.pop (toIndex) + + DbusIf.RefreshDefaultPackages = True + + # update package count + DbusIf.UpdatePackageCount () + + DbusIf.UNLOCK () + # this package was manually removed block automatic adds + # in the package directory + if matchFound: + # block automatic adds + PackageClass.SetAutoAddOk (packageName, False) + + DbusIf.UpdateStatus ( message="", where='Editor' ) + DbusIf.SetGuiEditAction ( '' ) + else: + DbusIf.UpdateStatus ( message=packageName + " not removed - name not found", where='Editor', logLevel=ERROR ) + DbusIf.SetGuiEditAction ( 'ERROR' ) + + + # UpdateFileFlagsAndVersions + # + # retrieves packages versions from the file system + # each package contains a file named version in it's root directory + # that becomes packageVersion + # the installedVersion-... file is associated with installed packages + # abesense of the file indicates the package is not installed + # presense of the file indicates the package is installed + # the content of the file is the actual version installed + # in prevous versions of the setup scripts, this file could be empty, + # so we show this as "unknown" + # + # also sets incompatible parameter and AutoInstallOk local variable to save time in other loops + # + # the single package variation is broken out so it can be called from other methods + # to insure version information is up to date before proceeding with an operaiton + # + # must be called while LOCKED !! + + def UpdateFileFlagsAndVersions (self): + + packageName = self.PackageName + + # fetch installed version + installedVersionFile = "/etc/venus/installedVersion-" + packageName + try: + versionFile = open (installedVersionFile, 'r') + except: + installedVersion = "" + else: + installedVersion = versionFile.readline().strip() + versionFile.close() + # if file is empty, an unknown version is installed + if installedVersion == "": + installedVersion = "unknown" + self.SetInstalledVersion (installedVersion) + + packageDir = "/data/" + packageName + + # no package directory - null out all params + if not os.path.isdir (packageDir): + self.SetPackageVersion ("") + self.AutoInstallOk = False + self.SetIncompatible ('') + return + + # fetch package version (the one in /data/packageName) + try: + versionFile = open (packageDir + "/version", 'r') + packageVersion = versionFile.readline().strip() + versionFile.close() + except: + packageVersion = "" + self.SetPackageVersion (packageVersion) + + # set the incompatible parameter + # to 'PLATFORM' or 'VERSION' + global Platform + incompatible = False + if os.path.exists (packageDir + "/raspberryPiOnly" ): + if Platform[0:4] != 'Rasp': + self.SetIncompatible ('PLATFORM') + incompatible = True + + # if package options directory exists set/clear auto install flag + # directory may not exist if package was never downloaded or transferred from media + # or if package was added manually then never acted on + flagFile = "/data/setupOptions/" + packageName + "/DO_NOT_AUTO_INSTALL" + if os.path.exists (flagFile): + self.AutoInstallOk = False + else: + self.AutoInstallOk = True + + # platform is OK, now check versions + if incompatible == False: + # check version compatibility + try: + fd = open (packageDir + "/firstCompatibleVersion", 'r') + except: + firstVersion = "v2.40" + else: + firstVersion = fd.readline().strip() + fd.close () + try: + fd = open (packageDir + "/obsoleteVersion", 'r') + except: + obsoleteVersion = None + else: + obsoleteVersion = fd.readline().strip() + + global VersionToNumber + global VenusVersion + global VenusVersionNumber + firstVersionNumber = VersionToNumber (firstVersion) + obsoleteVersionNumber = VersionToNumber (obsoleteVersion) + if VenusVersionNumber < firstVersionNumber: + self.SetIncompatible ('VERSION') + incompatible = True + elif obsoleteVersionNumber != 0 and VenusVersionNumber >= obsoleteVersionNumber: + self.SetIncompatible ('VERSION') + incompatible = True + + # platform and versions OK, check to see if command line is needed for install + # the optionsRequired flag in the package directory indicates options must be set before a blind install + # the optionsSet flag indicates the options HAVE been set already + # so if optionsRequired == True and optionsSet == False, can't install from GUI + if incompatible == False: + if os.path.exists ("/data/" + packageName + "/optionsRequired" ): + if not os.path.exists ( "/data/setupOptions/" + packageName + "/optionsSet"): + self.SetIncompatible ('CMDLINE') + incompatible = True + # end UpdateFileFlagsAndVersions +# end Package + + +# UpdateGitHubVersionClass +# +# downloads the GitHub version of all packages +# this work is done in a separate thread so network activity can be spaced out +# +# a queue is used to trigger a priority update for a specific package +# this is used when the operator changes GitHub user/branch so the version +# updates rapidly +# the queue is also used to wake the thread from a potentially long sleep period +# to exit or speed up the refresh rate +# +# Instances: +# UpdateGitHubVersion (a separate thread) +# +# Methods: +# updateGitHubVersion +# run () +# StopThread () +# + +class UpdateGitHubVersionClass (threading.Thread): + + # updateGitHubVersion + # + # fetches the GitHub version from the internet and stores it in the package + # + # this is called from the background thread run () below + # + # if the wget fails, the GitHub version is set to "" + # this will happen if the name, user or branch are not correct + # or if there is no internet connection + # + # the package GitHub version is upated + # but the version is also returned to the caller + # + # Instances: + # UpdateGitHubVersion (thread) + # + # Methods: + # updateGitHubVersion + # SetPriorityGitHubVersion + # run (the thread) + + def updateGitHubVersion (self, packageName, gitHubUser, gitHubBranch): + + url = "https://raw.githubusercontent.com/" + gitHubUser + "/" + packageName + "/" + gitHubBranch + "/version" + try: + proc = subprocess.Popen (["wget", "--timeout=10", "-qO", "-", url], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + logging.error ("wget for version failed " + packageName) + gitHubVersion = "" + else: + proc.wait() + # convert from binary to string + stdout, stderr = proc.communicate () + stdout = stdout.decode ().strip () + stderr = stderr.decode ().strip () + returnCode = proc.returncode + if proc.returncode == 0: + gitHubVersion = stdout + else: + gitHubVersion = "" + + # locate the package with this name and update it's GitHubVersion + # if not in the list discard the information + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + if package != None: + package.SetGitHubVersion (gitHubVersion) + DbusIf.UNLOCK () + return gitHubVersion + + + def __init__(self): + threading.Thread.__init__(self) + self.GitHubVersionQueue = queue.Queue (maxsize = 50) + self.threadRunning = True + # package needing immediate update + self.priorityPackageName = None + + + # SetPriorityGitHubVersion + # pushes a priority package version update onto our queue + + def SetPriorityGitHubVersion (self, packageName): + self.GitHubVersionQueue.put (packageName) + + + # UpdateGitHubVersion run () + # + # updates GitHub versions + # GitHub access is spaced out to minimize network traffic + # a priority update is pushed onto our que when GitHub info changes + # + # StopThread () is called to shut down the thread when PackageManager is quitting + # "STOP" is pushed on to the queue by StopThread to cause the run thread to detect + # detect the stop request immediately + # + # run () blocks on reading from our queue + # the timeout for the queue pull paces the version fetches + # normally, the timeout will occur with the queue empty + # in which case we update the next GitHub version for the next package + # the time between version fetches changes + # for the first pass, a shorter delay is used + # after all packages have been updated, the delay is increased + # the shorter delay is used again when we pull "FAST" off the queue + # + # when the que returns an item, it is checked to see if it is either a + # prioirty package to update it's GitHub version + # "STOP" - indicating run should return + # "FAST" - indicating the loop should speed up + # wake is used when download refresh mode/rates change + # checks the threadRunning flag and returns if it is False, + # when run returns, the main method should catch the tread with join () + + def StopThread (self): + logging.info ("attempting to stop UpdateGitHubVersion thread") + self.threadRunning = False + self.GitHubVersionQueue.put ( 'STOP', block=False ) + + + def run (self): + global AllVersionsRefreshed + + AllVersionsRefreshed = False + + gitHubVersionPackageIndex = 0 + delay = 10.0 + + while self.threadRunning: + command = "" + doBackgroundUpdate = True + # queue gets STOP and FAST commands or priority package name + # empty queue signals it's time for a background update + # queue timeout is used to pace background updates + try: + command = self.GitHubVersionQueue.get (timeout = delay) + except queue.Empty: # means get() timed out as expected - not an error + # timeout indicates it's time to do a background update + doBackgroundUpdate = True + except: + logging.error ("pull from GitHubVersionQueue failed") + doBackgroundUpdate = True + + if command == 'STOP' or self.threadRunning == False: + doBackgroundUpdate = False + return + + # the FAST command speeds up the loop and starts with the first package + if (command == 'FAST'): + delay = 10.0 + gitHubVersionPackageIndex = 0 + # command contains a package name for priority update + elif command != "": + priorityPackageName = command + DbusIf.LOCK () + package = PackageClass.LocatePackage (priorityPackageName) + if package != None: + user = package.GitHubUser + branch = package.GitHubBranch + DbusIf.UNLOCK () + if package != None: + # update the GigHub version here + self.updateGitHubVersion (priorityPackageName, user, branch) + doBackgroundUpdate = False + else: + logging.error ("can't fetch GitHub version - " + priorityPackageName + " not in package list") + # no priority update, do next in the list + if doBackgroundUpdate: + DbusIf.LOCK () + length = len (PackageClass.PackageList) + if length > 0: + # reached end of list - switch to slow scan + if gitHubVersionPackageIndex >= length: + # notify the main loop that all versions have been refreshed and + # download modes can now be changed if appropriate + AllVersionsRefreshed = True + gitHubVersionPackageIndex = 0 + delay = 60.0 + package = PackageClass.PackageList[gitHubVersionPackageIndex] + packageName = package.PackageName + user = package.GitHubUser + branch = package.GitHubBranch + gitHubVersionPackageIndex += 1 + else: + doBackgroundUpdate = False + DbusIf.UNLOCK () + + if doBackgroundUpdate: + self.updateGitHubVersion (packageName, user, branch) + + +# DownloadGitHubPackagesClass +# +# downloads packages from GitHub, replacing the existing package +# if versions indicate a newer version +# +# the GUI and auto download code (in main loop) push download +# actions onto our queue +# the thread blocks when the queue is empty +# +# a STOP command is also pushed onto the queue when PackageManager +# is shutting down. This unblocks this thread which immediately +# reads threadRunning. If false, run () returns +# +# Instances: +# DownloadGitHub (a separate thread) +# +# Methods: +# GitHubDownload +# DownloadVersionCheck +# processDownloadQueue +# run +# StopThread +# +# the run () thread is only responsible for pacing automatic downloads from the internet +# commands are pushed onto the processing queue (PushAction) +# + +class DownloadGitHubPackagesClass (threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + self.DownloadQueue = queue.Queue (maxsize = 50) + self.threadRunning = True + + + # this method downloads a package from GitHub + # it is called from run() below + # also, download requests are pushed for automatic downloads from mainloop + # and also for a manual download triggered from the GUI + # statusMethod provides text status to the caller + # callBack provides notificaiton of completion (or error) + # automatic downloads that fail are logged but otherwise not reported + + def GitHubDownload (self, packageName= None, source=None): + if source == 'GUI': + where = 'Editor' + elif source == 'AUTO': + where = 'Download' + else: + where = None + + # to avoid confilcts, create a temp directory that + # is unque to this program and this method + # and make sure it is empty + tempDirectory = "/var/run/packageManager" + str(os.getpid ()) + "GitHubDownload" + if os.path.exists (tempDirectory): + shutil.rmtree (tempDirectory) + os.mkdir (tempDirectory) + + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + gitHubUser = package.GitHubUser + gitHubBranch = package.GitHubBranch + DbusIf.UNLOCK () + + DbusIf.UpdateStatus ( message="downloading " + packageName, where=where, logLevel=WARNING ) + + # create temp directory specific to this thread + tempArchiveFile = tempDirectory + "/temp.tar.gz" + # download archive + if os.path.exists (tempArchiveFile): + os.remove ( tempArchiveFile ) + + url = "https://github.com/" + gitHubUser + "/" + packageName + "/archive/" + gitHubBranch + ".tar.gz" + try: + proc = subprocess.Popen ( ['wget', '--timeout=120', '-qO', tempArchiveFile, url ],\ + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + DbusIf.UpdateStatus ( message="could not access archive on GitHub " + packageName, + where=where, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + return False + else: + proc.wait() + stdout, stderr = proc.communicate () + # convert from binary to string + stdout = stdout.decode ().strip () + stderr = stderr.decode ().strip () + returnCode = proc.returncode + + if returnCode != 0: + DbusIf.UpdateStatus ( message="could not access " + packageName + ' ' + gitHubUser + ' '\ + + gitHubBranch + " on GitHub", where=where, logLevel=WARNING ) + logging.error ("returnCode" + str (returnCode) + "stderr" + stderr) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + PackageClass.UpdateDownloadPending (packageName, False) + shutil.rmtree (tempDirectory) + return False + try: + proc = subprocess.Popen ( ['tar', '-xzf', tempArchiveFile, '-C', tempDirectory ], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + DbusIf.UpdateStatus ( message="could not unpack " + packageName + ' ' + gitHubUser + ' ' + gitHubBranch, + where=where, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + PackageClass.UpdateDownloadPending (packageName, False) + shutil.rmtree (tempDirectory) + return False + + proc.wait() + stdout, stderr = proc.communicate () + # convert from binary to string + stdout = stdout.decode ().strip () + stderr = stderr.decode ().strip () + returnCode = proc.returncode + + if returnCode != 0: + DbusIf.UpdateStatus ( message="could not unpack " + packageName + ' ' + gitHubUser + ' ' + gitHubBranch, + where=where, logLevel=ERROR ) + logging.error ("stderr: " + stderr) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + PackageClass.UpdateDownloadPending (packageName, False) + shutil.rmtree (tempDirectory) + return False + + # attempt to locate a directory that contains a version file + # the first directory in the tree starting with tempDicrectory + # is returnd + unpackedPath = LocatePackagePath (tempDirectory) + if unpackedPath == None: + PackageClass.UpdateDownloadPending (packageName, False) + shutil.rmtree (tempDirectory) + logging.error ( "GitHubDownload: no archive path for " + packageName) + return False + + # move unpacked archive to package location + # LOCK this critical section of code to prevent others + # from accessing the directory while it's being updated + packagePath = "/data/" + packageName + tempPackagePath = packagePath + "-temp" + DbusIf.LOCK () + if os.path.exists (packagePath): + os.rename (packagePath, tempPackagePath) + shutil.move (unpackedPath, packagePath) + if os.path.exists (tempPackagePath): + shutil.rmtree (tempPackagePath, ignore_errors=True) # like rm -rf + DbusIf.UNLOCK () + PackageClass.UpdateDownloadPending (packageName, False) + DbusIf.UpdateStatus ( message="", where=where ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( '' ) + shutil.rmtree (tempDirectory) + return True + # end GitHubDownload + + + # DownloadVersionCheck + # + # compares versions to determine if a download is needed + # returns: True if a download is needed, False otherwise + # must be called with package list LOCKED !! + + + def DownloadVersionCheck (self, package): + gitHubUser = package.GitHubUser + gitHubBranch = package.GitHubBranch + gitHubVersion = package.GitHubVersion + packageVersion = package.PackageVersion + + # versions not initialized yet - don't allow the download + if gitHubVersion == None or gitHubVersion == "" or gitHubVersion[0] != 'v' or packageVersion == '?': + return False + + packageVersionNumber = package.PackageVersionNumber + gitHubVersionNumber = package.GitHubVersionNumber + + # if GitHubBranch is a version number, a download is needed if the versions differ + if gitHubBranch[0] == 'v': + if gitHubVersionNumber != packageVersionNumber: + return True + else: + return False + # otherwise the download is needed if the gitHubVersion is newer + else: + if gitHubVersionNumber > packageVersionNumber: + return True + else: + return False + + + # DownloadGitHub run (the thread) + # + # StopThread () is called to shut down the thread + + def StopThread (self): + logging.info ("attempting to stop DownloadGitHub thread") + self.threadRunning = False + self.DownloadQueue.put ( ('STOP', ''), block=False ) + + # DownloadGitHub run (the thread) + # + # updates GitHub versions + # a priority update triggered when GitHub info changes + # a backgroud update + # downloads packages from + # GUI requests + # a background loop + # the background loop always starts at the beginning of PackageList + # and stops when a package needing a download is found + # on the next pass, (hopefully) that package download will have + # been satisfied and a new one will be found + # when no updates are found, the download scan rate slows + # but a change in auto download mode will speed it up again + # + # run () checks the threadRunning flag and returns if it is False, + # essentially taking the thread off-line + # the main method should catch the tread with join () + + def run (self): + lastAutoDownloadTime = 0.0 + while self.threadRunning: # loop forever + # process one GUI download request + # if there was one, skip auto downloads until next pass + try: + command = self.DownloadQueue.get () # block forever + except: + logging.error ("pull from DownloadQueue queue failed") + time.sleep (5.0) + continue + if command[0] == 'STOP' or self.threadRunning == False: + return + + # separate command, source tuple + # and separate action and packageName + if len (command) >= 2: + parts = command[0].split (":") + if len (parts) >= 2: + action = parts[0].strip () + packageName = parts[1].strip () + else: + logging.error ("DownloadQueue - no action and/or package name - discarding", command) + time.sleep (5.0) + continue + source = command[1] + else: + logging.error ("DownloadQueue - no command and/or source - discarding", command) + time.sleep (5.0) + continue + + # invalid action for this queue + if action != 'download': + logging.error ("received invalid command from Install queue: ", command ) + time.sleep (5.0) + continue + + # set version refresh rate and download delay + currentMode = DbusIf.GetAutoDownloadMode () + if currentMode == ONE_DOWNLOAD or currentMode == FAST_DOWNLOAD: + downloadDelay = 10.0 + else: + downloadDelay = 600.0 + + # wait here until it is time to do the download + while True: + timeToGo = downloadDelay + lastAutoDownloadTime - time.time() + if timeToGo <= 0: + break + # it's not time to download yet - update status message with countdown + if timeToGo > 90: + statusMessage = packageName + " download begins in " + "%0.1f minutes" % ( timeToGo / 60 ) + elif timeToGo > 1.0: + statusMessage = packageName + " download begins in " + "%0.0f seconds" % ( timeToGo ) + if source == 'GUI': + DbusIf.UpdateStatus ( message=statusMessage, where='Editor' ) + else: + DbusIf.UpdateStatus ( message=statusMessage, where='Download' ) + + time.sleep (5.0) + + # do the download here + self.GitHubDownload (packageName=packageName, source=source ) + lastAutoDownloadTime = time.time() + # end while True + # end run +# end DownloadGitHubPackagesClass + + +# InstallPackagesClass +# Instances: +# InstallPackages (a separate thread) +# +# Methods: +# InstallPackage +# run (the thread) +# StopThread +# run +# +# install and uninstall packages +# if versions indicate a newer version +# runs as a separate thread since the operations can take a long time +# and we need to space them to avoid consuming all CPU resources +# +# packages are automatically installed only +# if the autoInstall Setting is active +# package version is newer than installed version +# or if nothing is installed +# +# a manual install is performed regardless of versions + +class InstallPackagesClass (threading.Thread): + + def __init__(self): + threading.Thread.__init__(self) + DbusIf.SetInstallStatus ("") + self.threadRunning = True + self.InstallQueue = queue.Queue (maxsize = 10) + + + # InstallPackage + # + # this method either installs or uninstalls a package + # the choice is the direction value: + # 'install' or 'uninstall' + # the operation can take many seconds + # i.e., the time it takes to run the package's setup script + # do not call from a thread that should not block + + def InstallPackage ( self, packageName=None, source=None , direction='install' ): + + # refresh versions, then check to see if an install is possible + DbusIf.LOCK () + package = PackageClass.LocatePackage (packageName) + + if source == 'GUI': + sendStatusTo = 'Editor' + # uninstall sets the uninstall flag file to prevent auto install + if direction == 'uninstall': + package.SetAutoInstallOk (False) + logging.warning (packageName + " was manually uninstalled - auto install for that package will be skipped") + # manual install removes the flag file + else: + package.SetAutoInstallOk (True) + logging.warning (packageName + " was manually installed - allowing auto install for that package") + elif source == 'AUTO': + sendStatusTo = 'Install' + + packageDir = "/data/" + packageName + if not os.path.isdir (packageDir): + logging.error ("InstallPackage - no package directory " + packageName) + package.InstallPending = False + DbusIf.UNLOCK () + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + return + + setupFile = packageDir + "/setup" + if os.path.isfile(setupFile): + if os.access(setupFile, os.X_OK) == False: + DbusIf.UpdateStatus ( message="setup file for " + packageName + " not executable", + where=sendStatusTo, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + package.InstallPending = False + DbusIf.UNLOCK () + return + else: + DbusIf.UpdateStatus ( message="setup file for " + packageName + " doesn't exist", + where=sendStatusTo, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + package.InstallPending = False + DbusIf.UNLOCK () + return + + DbusIf.UNLOCK () + + # provide an innitial status message for the action since it takes a while for PackageManager + # to fill in EditStatus + # this provides immediate user feedback that the button press was detected + DbusIf.UpdateStatus ( message=direction + "ing " + packageName, + where=sendStatusTo, logLevel=WARNING ) + try: + proc = subprocess.Popen ( [ setupFile, direction, 'deferReboot', 'deferGuiRestart' ], + stdout=subprocess.PIPE, stderr=subprocess.PIPE ) + proc.wait() + stdout, stderr = proc.communicate () + # convert from binary to string + stdout = stdout.decode ().strip () + stderr = stderr.decode ().strip () + returnCode = proc.returncode + setupRunFail = False + except: + setupRunFail = True + + # manage the result of the setup run while locked just in case + DbusIf.LOCK () + + package = PackageClass.LocatePackage (packageName) + package.InstallPending = False + + if setupRunFail: + DbusIf.UpdateStatus ( message="could not run setup file for " + packageName, + where=sendStatusTo, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + elif returnCode == EXIT_SUCCESS: + package.SetIncompatible ('') # this marks the package as compatible + DbusIf.UpdateStatus ( message="", where=sendStatusTo ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( '' ) + elif returnCode == EXIT_REBOOT: + # set package RebootNeeded so GUI can show the need - does NOT trigger a reboot + package.SetRebootNeeded (True) + + DbusIf.UpdateStatus ( message=packageName + " " + direction + " requires REBOOT", + where=sendStatusTo, logLevel=WARNING ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'RebootNeeded' ) + # auto install triggers a reboot by setting the global flag - reboot handled in main_loop + else: + global SystemReboot + SystemReboot = True + elif returnCode == EXIT_RESTART_GUI: + # set package GuiRestartNeeded so GUI can show the need - does NOT trigger a restart + package.SetGuiRestartNeeded (True) + + logging.warning ( packageName + " " + direction + " requires GUI restart" ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'GuiRestartNeeded' ) + # auto install triggers a GUI restart by setting the global flag - restart handled in main_loop + else: + global GuiRestart + GuiRestart = True + elif returnCode == EXIT_RUN_AGAIN: + if source == 'GUI': + DbusIf.UpdateStatus ( message=packageName + " run install again to complete install", + where=sendStatusTo, logLevel=WARNING ) + DbusIf.SetGuiEditAction ( 'ERROR' ) + else: + DbusIf.UpdateStatus ( message=packageName + " setup must be run again", + where=sendStatusTo, logLevel=WARNING ) + elif returnCode == EXIT_INCOMPATIBLE_VERSION: + global VenusVersion + package.SetIncompatible ('VERSION') + DbusIf.UpdateStatus ( message=packageName + " not compatible with Venus " + VenusVersion, + where=sendStatusTo, logLevel=WARNING ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + elif returnCode == EXIT_INCOMPATIBLE_PLATFOM: + global Platform + package.SetIncompatible ('PLATFORM') + DbusIf.UpdateStatus ( message=packageName + " " + direction + " not compatible with " + Platform, + where=sendStatusTo, logLevel=WARNING ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + elif returnCode == EXIT_OPTIONS_NOT_SET: + DbusIf.UpdateStatus ( message=packageName + " " + direction + " setup must be run from the command line", + where=sendStatusTo, logLevel=WARNING ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + elif returnCode == EXIT_FILE_SET_ERROR: + DbusIf.UpdateStatus ( message=packageName + " file set incomplete", + where=sendStatusTo, logLevel=ERROR ) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + # unknown error + elif returnCode != 0: + DbusIf.UpdateStatus ( message=packageName + " " + direction + " unknown error " + str (returnCode), + where=sendStatusTo, logLevel=ERROR ) + logging.error ("stderr: " + stderr) + if source == 'GUI': + DbusIf.SetGuiEditAction ( 'ERROR' ) + + DbusIf.UNLOCK () + # end InstallPackage () + + + # InstallPackage run (the thread) + # + # automatic install packages + # pushes request on queue for processing later in another thread + # this allows this to run quickly while the package list is locked + # + # run () checks the threadRunning flag and returns if it is False, + # essentially taking the thread off-line + # the main method should catch the tread with join () + # StopThread () is called to shut down the thread + + def StopThread (self): + logging.info ("attempting to stop InstallPackages thread") + self.threadRunning = False + self.InstallQueue.put ( ('STOP', ''), block=False ) + + def run (self): + while self.threadRunning: + try: + command = self.InstallQueue.get () + except: + logging.error ("pull from Install queue failed") + continue + if len (command) == 0: + logging.error ("pull from Install queue failed - empty comand") + time.sleep (5.0) + continue + # thread shutting down + if command[0] == 'STOP' or self.threadRunning == False: + return + + # separate command, source tuple + # and separate action and packageName + if len (command) >= 2: + parts = command[0].split (":") + if len (parts) >= 2: + action = parts[0].strip () + packageName = parts[1].strip () + else: + logging.error ("InstallQueue - no action and/or package name - discarding", command) + time.sleep (5.0) + continue + source = command[1] + else: + logging.error ("InstallQueue - no command and/or source - discarding", command) + time.sleep (5.0) + continue + + if action == 'install': + self.InstallPackage (packageName=packageName, source=source , direction='install' ) + time.sleep (5.0) + elif action == 'uninstall': + self.InstallPackage (packageName=packageName, source=source , direction='uninstall' ) + time.sleep (5.0) + # invalid action for this queue + else: + logging.error ("received invalid command from Install queue: ", command ) + time.sleep (5.0) + continue + # end run +# end InstallPackagesClass + + + +# MediaScanClass +# Instances: +# MediaScan (a separate thread) +# Methods: +# transferPackage +# StopThread +# settingsBackup +# settingsRestore +# run +# +# scan removable SD and USB media for packages to be installed +# +# run () is a separate thread that looks for removable +# SD cards and USB sticks that appear in /media as separate directories +# these directories come and go with the insertion and removable of the media +# +# when new media is detected, it is scanned once then ignored +# when media is removed, then reinserted, the scan begins again +# +# packages must be located in the root of the media (no subdirecoties are scanned) +# and must be an archive with a name ending in .tar.gz +# +# archives are unpacked to a temp directory in /var/run (a ram disk) +# verified, then moved into position in /data/ +# where the name comes from the unpacked directory name +# of the form - +# +# actual installation is handled in the InstallPackages run() thread + +class MediaScanClass (threading.Thread): + + # transferPackage unpacks the archive and moves it into postion in /data + # + # path is the full path to the archive + + def transferPackage (self, path): + packageName = os.path.basename (path).split ('-', 1)[0] + + # create an empty temp directory in ram disk + # for the following operations + # directory is unique to this process and thread + tempDirectory = "/var/run/packageManager" + str(os.getpid ()) + "Media" + if os.path.exists (tempDirectory): + shutil.rmtree (tempDirectory) + os.mkdir (tempDirectory) + + # unpack the archive - result is placed in tempDirectory + try: + proc = subprocess.Popen ( ['tar', '-xzf', path, '-C', tempDirectory ], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + DbusIf.UpdateStatus ( message="tar failed for " + packageName, + where='Media', logLevel=ERROR) + time.sleep (5.0) + DbusIf.UpdateStatus ( message="", where='Media') + return False + proc.wait() + stdout, stderr = proc.communicate () + # convert from binary to string + stdout = stdout.decode ().strip () + stderr = stderr.decode ().strip () + returnCode = proc.returncode + if returnCode != 0: + DbusIf.UpdateStatus ( message="could not unpack " + packageName + " from SD/USB media", + where='Media', logLevel=ERROR) + logging.error ("stderr: " + stderr) + shutil.rmtree (tempDirectory) + time.sleep (5.0) + DbusIf.UpdateStatus ( message="", where='Media') + return False + + # attempt to locate a package directory in the tree below tempDirectory + unpackedPath = LocatePackagePath (tempDirectory) + if unpackedPath == None: + logging.warning (packageName + " archive doesn't contain a package directory - rejected" ) + shutil.rmtree (tempDirectory) + time.sleep (5.0) + DbusIf.UpdateStatus ( message="", where='Media') + return False + + # move unpacked archive to package location + # LOCK this critical section of code to prevent others + # from accessing the directory while it's being updated + DbusIf.UpdateStatus ( message="transfering " + packageName + " from SD/USB", where='Media', logLevel=WARNING ) + packagePath = "/data/" + packageName + tempPackagePath = packagePath + "-temp" + DbusIf.LOCK () + if os.path.exists (tempPackagePath): + shutil.rmtree (tempPackagePath, ignore_errors=True) # like rm -rf + if os.path.exists (packagePath): + os.rename (packagePath, tempPackagePath) + shutil.move (unpackedPath, packagePath) + if os.path.exists (tempPackagePath): + shutil.rmtree (tempPackagePath, ignore_errors=True) # like rm -rf + DbusIf.UNLOCK () + shutil.rmtree (tempDirectory, ignore_errors=True) + time.sleep (5.0) + DbusIf.UpdateStatus ( message="", where='Media') + return True + # end transferPackage + + + def __init__(self): + threading.Thread.__init__(self) + self.MediaQueue = queue.Queue (maxsize = 10) # used only for STOP + self.threadRunning = True + + # + # settingsBackup + # settingsRestore + # + # extracts / restores dbus settings to/from a file + # + # settingsList contains the list of dbus /Settings parameters to save and restore + # + + def settingsBackup (self, backupPath): + settingsListFile = "/data/SetupHelper/settingsList" + backupFile = backupPath + "/settingsBackup" + if not os.path.exists (settingsListFile): + logging.error (settingsListFile + " does not exist - can't backup settings") + return + + # backup settings + backupSettings = open (backupFile, 'w') + settingsCount = 0 + bus = dbus.SystemBus() + with open (settingsListFile, 'r') as listFile: + for line in listFile: + setting = line.strip() + try: + value = bus.get_object("com.victronenergy.settings", setting).GetValue() + except: + continue + backupSettings.write ( setting + '=' + str(value) + '\n' ) + settingsCount += 1 + + backupSettings.close () + listFile.close () + + # backup logo overlays + overlaySourceDir = "/data/themes/overlay" + overlayDestDir = backupPath + "/logoBackup" + + + # remove any previous logo backups + if os.path.isdir (overlayDestDir): + shutil.rmtree (overlayDestDir) + + overlayCount = 0 + if os.path.isdir (overlaySourceDir): + overlayFiles = os.listdir (overlaySourceDir) + if len (overlayFiles) > 0: + # create overlay direcory on backkup device, then copy files + if not os.path.isdir (overlayDestDir): + os.mkdir (overlayDestDir) + for overlay in overlayFiles: + if overlay[0] == ".": + continue + shutil.copy ( overlaySourceDir + "/" + overlay, overlayDestDir ) + overlayCount += 1 + + logging.warning ("settings backup completed - " + str(settingsCount) + " settings and " + str (overlayCount) + " overlays") + + + def settingsRestore (self, backupPath): + backupFile = backupPath + "/settingsBackup" + if not os.path.exists (backupFile): + logging.error (backupFile + " does not exist - can't restore settings") + bus = dbus.SystemBus() + settingsCount = 0 + with open (backupFile, 'r') as fd: + for line in fd: + parts = line.strip().split ('=') + try: + bus.get_object("com.victronenergy.settings", parts[0]).SetValue(parts[1]) + except: + pass + settingsCount += 1 + + # restore logo overlays + overlaySourceDir = backupPath + "/logoBackup" + overlayDestDir = "/data/themes/overlay" + overlayCount = 0 + if os.path.isdir (overlaySourceDir): + overlayFiles = os.listdir (overlaySourceDir) + if len (overlayFiles) > 0: + # create overlay direcory in /data, then copy files + if not os.path.isdir (overlayDestDir): + os.mkdir (overlayDestDir) + for overlay in overlayFiles: + if overlay[0] == ".": + continue + shutil.copy ( overlaySourceDir + "/" + overlay, overlayDestDir ) + overlayCount += 1 + + logging.warning ("settings restore completed - " + str(settingsCount) + " settings and " + str (overlayCount) + " overlays") + + + # Media Scan run (the thread) + # + # run () checks the threadRunning flag and returns if it is False, + # essentially taking the thread off-line + # the main method should catch the tread with join () + # StopThread () is called to shut down the thread + + def StopThread (self): + logging.info ("attempting to stop MediaScan thread") + self.threadRunning = False + self.MediaQueue.put ( "STOP", block=False ) + + def run (self): + separator = '/' + root = "/media" + archiveSuffix = ".tar.gz" + + # list of accepted branch/version substrings + acceptList = [ "-current", "-latest", "-main", "-test", "-debug", "-beta", "-install", + "-0", "-1", "-2", "-3", "-4", "-5", "-6", "-7", "-8", "-9" ] + + # keep track of all media that's been scanned so it isn't scanned again + # media removal removes it from this list + alreadyScanned = [] + while self.threadRunning: + # use queue to receive stop command and also to space operations + command = "" + try: + command = self.MediaQueue.get (timeout = 5.0) + except queue.Empty: # queue empty is OK + # timeout indicates it's time to make one pass through the code below + pass + except: + logging.error ("pull from MediaQueue failed") + time.sleep (5.0) + if command == 'STOP' or self.threadRunning == False: + return + + try: + drives = os.listdir (root) + except: + drives = [] + + if len (drives) == 0: + DbusIf.SetBackupMediaAvailable (False) + DbusIf.SetBackupSettingsFileExist (False) + backupMediaExists = False + else: + DbusIf.SetBackupMediaAvailable (True) + backupMediaExists = True + + # if previously detected media is removed, + # allow it to be scanned again when reinserted + for scannedDrive in alreadyScanned: + if not scannedDrive in drives: + alreadyScanned.remove (scannedDrive) + + for drive in drives: + drivePath = separator.join ( [ root, drive ] ) + # process settings backup and restore + # check for settings backup file + settingsBackupPath = root + "/" + drive + settingsBackupFile = settingsBackupPath + "/settingsBackup" + if os.path.exists (settingsBackupFile): + DbusIf.SetBackupSettingsFileExist (True) + backupSettingsFileExists = True + else: + DbusIf.SetBackupSettingsFileExist (False) + backupSettingsFileExists = False + + if backupMediaExists: + backupProgress = DbusIf.GetBackupProgress () + # GUI triggered backup + if backupProgress == 1: + DbusIf.SetBackupProgress (3) + self.settingsBackup (settingsBackupPath) + DbusIf.SetBackupProgress (0) + elif backupProgress == 2: + if backupSettingsFileExists: + DbusIf.SetBackupProgress (4) + self.settingsRestore (settingsBackupPath) + DbusIf.SetBackupProgress (0) + + if drive in alreadyScanned: + continue + + # check any file name ending with the achive suffix + # all others are skipped + for path in glob.iglob (drivePath + "/*" + archiveSuffix): + accepted = False + if os.path.isdir (path): + continue + else: + accepted = False + baseName = os.path.basename (path) + # verify the file name contains one of the accepted branch/version identifiers + # if not found in the list, the archive is rejected + for accept in acceptList: + if accept in baseName: + accepted = True + break + # discovered what appears to be a valid archive + # unpack it, do further tests and move it to /data + if accepted: + self.transferPackage (path) + if self.threadRunning == False: + return + else: + logging.warning (path + " not a valid archive name - rejected") + # end for path + + # mark this drive so it won't get scanned again + # this prevents repeated installs + alreadyScanned.append (drive) + #end for drive + # end run () +# end MediaScanClass + +global noActionCount + +def mainLoop(): + global mainloop + global SystemReboot + global GuiRestart + global PackageIndex + global PushAction + global AllVersionsRefreshed # initialized in main - set in UpdateGitHubVersion + global lastDownloadMode # initialized in main + global currentDownloadMode # initialized in main + global noActionCount + + delayStart = 0.0 + + # detect download mode changes and switch back to fast scan + lastDownloadMode = currentDownloadMode + currentDownloadMode = DbusIf.GetAutoDownloadMode () + # UpdateGitHubVersion is responsible for fetching GitHub versions + # when it has checked all versions it sets AllVersionsRefreshed + # so we can update the download mode + # slowing or stopping the downloads + if AllVersionsRefreshed: + if currentDownloadMode == ONE_DOWNLOAD: + DbusIf.SetAutoDownload (AUTO_DOWNLOADS_OFF) + elif currentDownloadMode == FAST_DOWNLOAD: + DbusIf.SetAutoDownload (NORMAL_DOWNLOAD) + AllVersionsRefreshed = False + # signal mode change to the GitHub threads + if currentDownloadMode != lastDownloadMode: + if currentDownloadMode == ONE_DOWNLOAD or currentDownloadMode == FAST_DOWNLOAD: + UpdateGitHubVersion.GitHubVersionQueue.put ('FAST') + + PackageClass.AddStoredPackages () + + DbusIf.UpdateDefaultPackages () + + # process one package per pass of mainloop + DbusIf.LOCK () + packageListLength = len (PackageClass.PackageList) + if packageListLength > 0: + if PackageIndex >= packageListLength: + PackageIndex = 0 + package = PackageClass.PackageList [PackageIndex] + PackageIndex += 1 + packageName = package.PackageName + package.UpdateFileFlagsAndVersions () + # disallow operations on this package if anything is pending + packageOperationOk = not package.DownloadPending and not package.InstallPending + + if packageOperationOk and currentDownloadMode != AUTO_DOWNLOADS_OFF\ + and DownloadGitHub.DownloadVersionCheck (package): + PushAction ( command='download' + ':' + packageName, source='AUTO' ) + packageOperationOk = False # don't allow other operations if download was triggered + + if packageOperationOk and package.AutoInstallOk and package.Incompatible == ''\ + and DbusIf.GetAutoInstall () and package.InstallVersionCheck (): + PushAction ( command='install' + ':' + packageName, source='AUTO' ) + + # check all packages before looking for reboot or GUI restart + rebootNeeded = False + guiRestartNeeded = False + actionsPending = False + for package in PackageClass.PackageList: + if package.DownloadPending or package.InstallPending: + actionsPending = True + if package.GuiRestartNeeded: + guiRestartNeeded = True + if package.RebootNeeded: + rebootNeeded = True + DbusIf.UNLOCK () + if rebootNeeded: + DbusIf.SetActionNeeded ('reboot') + elif guiRestartNeeded: + DbusIf.SetActionNeeded ('guiRestart') + + if actionsPending: + noActionCount = 0 + else: + noActionCount += 1 + + # wait for two complete passes with nothing happening + # before triggerinng reboot or GUI restart + if noActionCount >= 2: + if SystemReboot: + # exit the main loop + mainloop.quit() + return False + elif GuiRestart: + logging.warning ("restarting GUI") + try: + proc = subprocess.Popen ( [ 'svc', '-t', '/service/gui' ] ) + except: + logging.critical ("GUI restart failed") + GuiRestart = False + DbusIf.SetInstallStatus ("") + DbusIf.SetEditStatus ("") + DbusIf.SetGuiEditAction ('') + # clear all package GUI restart needed flags + # that flag is only used by the GUI to show a restart is needed for that package + for package in PackageClass.PackageList: + package.SetGuiRestartNeeded (False) + # the ActionNeeded flag could be 'reboot' but that's addressed in main below anyway + DbusIf.SetActionNeeded ('') + + # continue the main loop + return True +# main +# +# ######## code begins here +# responsible for initialization and starting main loop and threads +# also deals with clean shutdown when main loop exits +# +def main(): + global mainloop + global SystemReboot + global GuiRestart + global PackageIndex + global noActionCount + + SystemReboot = False + GuiRestart = False + PackageIndex = 0 + noActionCount = 0 + + + # set logging level to include info level entries + logging.basicConfig( format='%(levelname)s:%(message)s', level=logging.WARNING ) + + # fetch installed version + installedVersionFile = "/etc/venus/installedVersion-SetupHelper" + try: + versionFile = open (installedVersionFile, 'r') + except: + installedVersion = "" + else: + installedVersion = versionFile.readline().strip() + versionFile.close() + # if file is empty, an unknown version is installed + if installedVersion == "": + installedVersion = "unknown" + + logging.warning (">>>> PackageManager " + installedVersion + " starting") + + from dbus.mainloop.glib import DBusGMainLoop + + # Have a mainloop, so we can send/receive asynchronous calls to and from dbus + DBusGMainLoop(set_as_default=True) + global PythonVersion + if PythonVersion < (3, 0): + GLib.threads_init() + + # get venus version + global VenusVersion + global VenusVersionNumber + global VersionToNumber + versionFile = "/opt/victronenergy/version" + try: + file = open (versionFile, 'r') + except: + VenusVersion = "" + else: + VenusVersion = file.readline().strip() + file.close() + VenusVersionNumber = VersionToNumber (VenusVersion) + + # get platform + global Platform + platformFile = "/etc/venus/machine" + try: + file = open (platformFile, 'r') + except: + Platform = "???" + else: + machine = file.readline().strip() + if machine == "einstein": + Platform = "Cerbo GX" + elif machine == "bealglebone": + Platform = "Venus GX" + elif machine == "ccgx": + Platform = "CCGX" + elif machine == "canvu500": + Platform = "CanVu 500" + elif machine == "nanopi": + Platform = "Multi/Easy Solar GX" + elif machine == "raspberrypi2": + Platform = "Raspberry Pi 2/3" + elif machine == "raspberrypi4": + Platform = "Raspberry Pi 4" + else: + Platform = machine + file.close() + + # initialze dbus Settings and com.victronenergy.packageManager + global DbusIf + DbusIf = DbusIfClass () + + PackageClass.AddPackagesFromDbus () + + DbusIf.TransferOldDbusPackageInfo () + + global UpdateGitHubVersion + UpdateGitHubVersion = UpdateGitHubVersionClass () + + global DownloadGitHub + DownloadGitHub = DownloadGitHubPackagesClass () + + global InstallPackages + InstallPackages = InstallPackagesClass () + + global AddRemove + AddRemove = AddRemoveClass () + + global MediaScan + MediaScan = MediaScanClass () + + # initialze package list + # and refresh versions before starting threads + # and the background loop + + DbusIf.ReadDefaultPackagelist () + + PackageClass.AddStoredPackages () + + DbusIf.UpdateDefaultPackages (alwaysRun = True) + + DbusIf.LOCK () + for package in PackageClass.PackageList: + package.MoveFlagFiles () + package.UpdateFileFlagsAndVersions () + DbusIf.UNLOCK () + + UpdateGitHubVersion.start() + DownloadGitHub.start() + InstallPackages.start() + AddRemove.start() + MediaScan.start () + + global lastDownloadMode + global currentDownloadMode + lastDownloadMode = AUTO_DOWNLOADS_OFF + currentDownloadMode = AUTO_DOWNLOADS_OFF + + # call the main loop - every 2 seconds + # this section of code loops until mainloop quits + GLib.timeout_add(2000, mainLoop) + mainloop = GLib.MainLoop() + mainloop.run() + + + # this section of code runs only after the mainloop quits + + # stop threads, remove service from dbus + if SystemReboot: + DbusIf.UpdateStatus ( message="REBOOTING ...", where='Download') + DbusIf.UpdateStatus ( message="REBOOTING ...", where='Editor' ) + logging.warning (">>>> REBOOTING: to complete package installation") + + + logging.warning ("stopping threads") + UpdateGitHubVersion.StopThread () + DownloadGitHub.StopThread () + InstallPackages.StopThread () + AddRemove.StopThread () + MediaScan.StopThread () + DbusIf.RemoveDbusService () + try: + UpdateGitHubVersion.join (timeout=30.0) + DownloadGitHub.join (timeout=30.0) + InstallPackages.join (timeout=10.0) + AddRemove.join (timeout=10.0) + except: + logging.critical ("attempt to join threads failed - one or more threads failed to exit") + pass + + # check for reboot + if SystemReboot: + try: + proc = subprocess.Popen ( [ 'shutdown', '-r', 'now', 'rebooting to complete package installation' ] ) + # for debug: proc = subprocess.Popen ( [ 'shutdown', '-k', 'now', 'simulated reboot - system staying up' ] ) + except: + logging.critical ("shutdown failed") + + # insure the package manager service doesn't restart when we exit + # it will start up again after the reboot + try: + proc = subprocess.Popen ( [ 'svc', '-o', '/service/PackageManager' ], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except: + logging.critical ("svc to shutdown PackageManager failed") + + logging.critical (">>>> PackageManager exiting") + + # program exits here + +# Always run our main loop so we can process updates +main() + + + + + diff --git a/ReadMe b/ReadMe index 3d1719e..37e10e9 100644 --- a/ReadMe +++ b/ReadMe @@ -1,494 +1,206 @@ -New: SetupHelper now includes an automatic install using SD/USB media - without the need for command line access - +New: SetupHelper includes a GUI based package mananager The SetupHelper package provides: - a set of utilities to simplfy installing modifications to Victron Venus Os - a mechanism to automatically reinstall them following a Venus OS update + a mechanism to automatically reinstall packages following a Venus OS update an automatic update mechanism to keep packages up to date from GitHub archives or USB stick - a manual package installation mechanism from GitHub archives or USB stick - an automatic install from SD/USB media + control of the automatic download and install from the GUI + add and remove packages from the system + manual download, install, uninstall from the GUI -NOTE: Starting with v2.80~10, the root partition is mounted read only - This package and those that use these resources rely on a writable /root partition - so CommonResources runs the remount-rw.sh script to make it writable + a "blind" install of SetupHelper from SD/USB media -Supported pacages are: - SetupHelper GeneratorConnector GuiMods GuiMods2 RpiDisplaySetup RpiGpioSetup ShutdownManager TankRepeater + backup and restore SOME settings from com.victronenergy.settings -More information is provided below. +SetupHelper is also required for my other packages + and must be installed BEFORE running the other package setup scripts -Setup: -There are three methods to fetch a package archive. +Blind Install: -New: You can install SetupHelper and GuiMods without need of command line access +By far, the easiest way to install SetupHelper is the "blind install" + which requires no command-line interaction. - 1) Download venus-data.tgz from the GitHub repo. + 1) Download venus-data.tgz from the SetupHelper GitHub repo. 2) copy it to the root of a freshly formatted SD card or USB memory stick 3) place the media in the GX device (Cerbo, CCGX, etc) - 4) reboot the GX device TWICE - after the second reboot, you should see the GUI restart and display - the enhanced flow overview from GuiMods2 - If GuiMods2 was previously installed, the Grid Parallel overview - is automatically selected - 5) remove the media from the GX device to prevent the next reboot from - starting the process all over again + 4) reboot the GX device TWICE - allowing the system to display the GUI each time + after the second reboot, you should find the Package Manager menus + at the bottom of the Settings menu + 5) REMOVE THE MEDIA from the GX device + VERY IMPORTANT to prevent the next reboot from starting the process all over again + failure to do so could disable reinstalls following a Venus OS firmware update !!! CAUTION: This mechanism overwrites /data/rcS.local !!!! If you are using rcS.local to perform boot-time activities, /data/rcS.local must be recreated following this "blind" install - Note that SetupHelper and GuiMods2 also use /data/rcS.local for + Note that SetupHelper also uses /data/rcS.local for reinstallation following a firmware update so use caution in recreating rcS.local. venus-data.tgz is available here: https://github.com/kwindrem/SetupHelper/raw/main/venus-data.tgz - and here: - https://github.com/kwindrem/GuiMods/raw/main/venus-data.tgz - One may be newer than the other. In any case, automatic updates - will update any package that is out of date following the blind install - - -If the Venus device has internet access is to run the following command: - -wget -qO - https://github.com/kwindrem/SetupHelper/archive/current.tar.gz | tar -xzf - -C /data - -If the Venus device does not have internet access, you will need to fetch the archive using a computer that does have internet access: - -click on this link in a web browser: -https://github.com/kwindrem/SetupHelper/archive/current.tar.gz + +Other mechanisms to install SetupHelper require command-line interaction with the GX device + and are no longer recommended for SetupHelper or my other packages -rename the resulting .tar.gz file to venus-data.tar.gz -copy the venus-data.tar.gz to a USB stick, -put the stick in the Venus device and reboot. -When Venus boots, it will unarchive the file to /data/SetupHelper-current - -Move the directory into it's active locaiton: -rm -rf /data/SetupHelper -mv /data/SetupHelper-current /data/SetupHelper - -Finally, run the setup script to complete the installation -/data/SetupHelper/setup - -Use the manual or automatic update mechnism for future updates +Once SetupHelper is installed, updates to it and other packages can be performed through the GUI +using the PackageManager menus. +CAUTION: + Package Manager allows uninstalling SetupHelper. + This can not be undone since the menus to control Package Manager will go away + You would need to use the Blind Install again to reinstall SetupHelper + + Note that removal does not actually remove the package so other setup scripts + will continue to function. Description: There are two parts to SetupHelper: -1) Install and update utilities help a user manage packages. +1) Package Manager, controls automatic and manual updates to pacakges -2) Resources to help writing scripts to perform the installation. +2) Utilities used by other packages' setup scripts. + These resources simplfy the task of writing install/uninstall scripts + and may be of help to others writing packages of their own. The latter is of concern only to those writing new Venus modificaitons or modifying an existing setup script. -These are described in detail later in the document. - -SetupHelper package management: - -The intent of this part of SetupHelper is to minimize the effort needed -to install and update packages. - -packageAutoUpdater handles automatic package updates. - -Automatic updates can be controlled via the Device List / Settings / Package Versions menu - or options in the SetupHelper setup script - Normal checks GitHub for a package once every 10 minutes - Fast checks GitHub for a package once every 10 seconds - Once checks GitHub for a package once every 10 minutes, but only once, then updates are turned off - Disable disables GitHub updates - -If enabled, automatic GitHub update checks will download and install the GitHub version - of all installed packages if it is newer than the current version. - - -If you are experimenting with modificaitons and wish to avoid GitHub updates overriding your work, -disable automatic updates. - - -USB updates are handy when the Venus device is not connected to the internet. - -To use the USB update process: - Navigate to the GitHub, repo, click on tags and select "current". - Choose the .tar.gz download link. - (Do not download from the Code tab or download the .zip file. These won't work.) - Copy the archive files to a USB memory stick. - Do NOT unpack the archive - Insert the stick in the Venus device. - -Regardless the update source (USB or GitHub), -packageAutoUpdater replaces the current copy on the Venus device with the archive contents - but only if the version number of the source is newer than the one that's installed. -Then the package's setup script is run to reinstall the modificaitons. -No user interaction is needed in this process. - -You may wish to dispalbe GitHub updates if you are updating from a USB/SD memory stick to avoid conflicts. -(GitHub and files on a USB stick could have different versions.) - -packageAutoUpdater runs in the background (as a service). - - -packageInstaller provides the opportunity to install packages for the first time, or reinstall them. -For each package, packageInstaller prompts the user to install that package. -Packages that are already installed will be skipped if automatic updates are enabled. - -Packages can be installed from either a USB memory stick/SD memory card or from GitHub. -After the package is copied to the Venus device it's setup script is run -and the user must answer the prompts to complete the instalation. - -The method of putting an archive on a USB stick for manaul install is the same as the one for automatic updates. - -As with package automatic updates, GitHub is the desired source if your Venus device has an internet connection. - -packageInstaller is called from the SetupHelper setup script by choosing the package (p) option at the main prompt. -The packageInstaller is a shell script and can also be run manually: -/data/SetupHelper/packageInstaller - - -Setup script aids: - -SetupHelper provides a number of functions and variables that simplfy scripts written to -install Venus OS mods. The remainder of this ReadMe describes the resources available and -how to incorporate them into a setup script. - -Read on if you are intersting in writing a package setup script (or modifying an existing one) -or understanding how the mechanism works. - -To use SetupHelper, the script must be written to include CommonResources, -then use the functions included in CommonResources to carry out the modiciations. - -#### following lines incorporate SetupHelper utilities into this script -# Refer to the SetupHelper ReadMe file for details. - -source "/data/SetupHelper/CommonResources" - -#### end of lines to include SetupHelper - -A Venus software update overwrites any modifications to the root partition. -It is therefore necessary to reinstall the modifications following the Venus update. - -Running setup for any package using SetupHelper will install the necessary code to reinstall packages -during system boot - -Typically, rcS.local is used because it runs prior to starting services - so conflicts with running services can be avoided - (services don't need to be restarted after they've been modified) - and if a reboot is required, it happens faster - -If your setup script needs resources launched later in boot, change the following line in CommonResources - -rcLocal="/data/rcS.local" - -to - -rcLocal="/data/rc.local" - -If you have already run any setup scripts and want to make this change, make sure to edit /data/rcS.local -to remove the call to reinstallMods or it will be called twice during boot! - -reinstallScriptsList is a list of setup scripts, one per line that will be called from reinstallMods. -Scripts in this list are called EVERY time the system boots. -The script must avoid repeating work if it can be avoided. -SetupHelper tools (CommonResources) handles this for the script write. - -reinstallScriptsList is hard-coded to reside in the /data directory. -The location must match in CommonResources, and in reinstallMods -The file is created by the first setup script to add to it. - -reinstallScriptsList should use full path names to avoid problems finding the script -Lines beginning with # or completely blank lines are ignored as is white space at begin and end - -When the setup script adds itself to the script list, it includes "reinstall" as the first parameter. -When reinstallMods calls the script, it passes "reinstall" to the setup script so it knows not to prompt the user - -When called from reinstallMods: -1) The setup script must not require user input since there will be no user interface -2) The setup script should not reboot the system directly -3) The setup script should avoid reapplying the modifications. -4) The setup script should avoid making calls to the internet as these could hang the system. - -reinstallMods tests the exit code of each setup script. -An exit code of 123 signals that reinstallMods should reboot the system after all scripts have been run. - -A "installed flag file" is used to make sure the automatic reinstallation occurs only once after a Venus OS update. - -The presence of the installed flag file signals no further work needs to be done to reinstall the package - -Running the script manually should typically ignore the installed flag, -but MUST create or remove it as appropriate to control future boot-time execution. - -The installed flag file needs to be removed by a Venus OS update so that the script can do it's work again. -Therefore, installed flag files are stored in /etc/venus since a Venus OS update overwrites the entire dirctory. - -The installed flag file should be tested early in the script to save time. - -Installation choice may be required to control the installation of optional parameters. -These options are stored in /data/setupOpitons/[package] -This maintains the options through a reinstallation of the package. -optionsSet is set after an installation is successful. This enables package reinstallation. - -A setup script needs to run manually to perform the initial installation or removal of the package. -In this case, user interaction is necessary at least to choose to install or uninstall. -The setup script also needs to run unattended when called from reinstallMods during system boot. -When run by a user, the scrit should prompt the user for neede information. -However, the call from reinstallMods also needs this information and the user won't be available. -Therefore the paremeters taken from user interaction need to be stored in persistent storage, -then recovered from that storage during the reinstall. -The script's directory is a convenient location for persistent storage since /data survives a Venus OS update. -The $scriptDir variable is set up in CommonResources (see discussion below). -$scriptDir may be prepended to any persistent storage files. - -CommonResources contains common functions and variables that can be used by all scripts, - saving the setup script writer from lots of tedious work and also hooks scripts into - the reinstall mechanism. - -The tools CommonResorces provides are described now. - -The following functions manage execution of the script and interfaces to reinstallMods which is run - at boot from one of the /data/rc scripts: - - Sourcing CommonResources checks the setup script command line and the installed flag - and sets $scriptAction based on what is found. - If this is a boot-time reinstall, AND the installed flag is set, - the script exits inside CommonResources. - It is no longer necessary to test scriptAction for EXIT. In fact that value will never be used - If this a boot-time reinstall but the installed flag is NOT set, - scriptAction is set to INSTALL. - - checkFileSets has been decommissioned - file set checks are now handled when CommonResources is sourced - Attempts to create a file set for the current Venus OS version if it does not exist yet. - If the original files match another version, the file set is created - so the setup script may continue normally - However, if the original files for this new version differ from all existing versions - the new partial file set must not be used - Flag files ares placed in the file set: - basename.NO_REPLACEMENT indicates a new replaement verion must be created manually - INCOMPLETE indiates the file set can not be used until issue are corrected - scriptAction is also set to EXIT - It is necessary for the user to create new replacement files manually - then rerun the setup script to install the package. - You can also revert to a previous Venus OS version until the package - is updated for the new version. - checkFileSets no longer needs to be called in the setup script - - endScript - Function to finish up, prompt the user (if not reinstalling) and exit the script - It updates the installed flag based on the $runAgain variable - This script is added/removed from the reinstallScriptsList if installing/uninstalling, respectively - If $runningAtBoot is true (false when CommonResources is sourced) - the script will exit with $exitReboot if $rebootNeeded is true - otherwise, the script will exit with $exitSuccess - endScript NEVER RETURNS to the caller - If $runningAtBoot is false (script was run manually), user interaction controls further action - If $rebootNeeded is true, the function asks if the user wishes to reboot now - If they respond yes, the system will be rebooted - The user may choose to not reboot now if additional installations need to be done first - If $rebootNeeded is false, the function notifies the user of any needed actions - If $restartGui is true (false when CommonResources is sourced) - the gui service will be restarted - - The following variables are available to control behavior: - $scriptAction provides direction for the setup script and has the following values: - NONE - setup script should prompt the user for the desired action - and set scriptAction accordingly - EXIT - the setup script should exit immediately - INSTALL - the setup script should execute code to install the package - UNINSTALL - the setup script should execute the code to restore the Venus files to stock - If installaiton errors occur within functions in CommonResources, scriptAction will be changed to UNINSTALL. - The setup script MUST retest scriptAction after all installation code has been executed - so the package can be removed, rather than leaving the system with a partially installed package. - - $rebootNeeded - true signifies a reboot is required after the script is run - if $runningAtBoot is also true, the reboot is actually performed in reinstallMods - $runAgain - true signifies startup script needs to be run again - - The following useful variables become available as well: - $scriptDir - the full path name to the startup script - the script's code can use this to identify the location of files that need to - persist between reboots and through Venus OS updates - It must be set in the setup script beause it is needed before sourcing CommonResources - $scriptName - the basename of the setup script - $reinstallScriptsList - the file containing a list of scripts to be run at boot to reinstall packages - after a Venus software update - $installedFlag - the name of the install flag files including it's full path - User code may use these variables but should not change their value! - It must be set in the setup script beause it is needed before sourcing CommonResources - $venusVersion - the version of VenusOS derived from /opt/victronenergy/version - $fileSets - the standard location for the replacement files - equivalent to #scriptDir/FileSets - Version-dependent replacements are stored in version subdirectories - Version-INDEPENDENT replacements are stored in FileSets - $pkgFileSets - is the locaiton of version-dependent files for the current Venus version - equivalent to $fileSets/$venusVersion - $runningAtBoot - true if the script was called from reinstallMods (at boot time) - signifying this is to be an unattended (automatic) installation - CommonResoures sets this variable based on command line options - $setupOptionsDir - the location of any files that control installation - These options are maintained in a separate directory so reinsalling the package - does not remove them so that a reinstall can proceed without prompting again - $obsoleteVersion - if the package is obsolete, it can be prevented from installation - at a specified Venus OS version - - $packageGitHubUser - specifies the GitHub user name to be used to fetch updates - $packageGitHubBranch - specifies the branch to pull the update from - Both of these need to be specified in the setup script to enable updates from GitHub and USB/SD media - -The following functions update or restore Venus files to activate a package -they are intended to simplify common tasks, generally reducing many lines -to a single line that is easier to read/understand - -The "active" file is the one used by Venus applications -It is backed up to [activeFile].orig in the same directory -Then a "replacement" file is copied into place and becomes the active file -Backups allow the stock functionality to be restored when the package is uninstalled - -Replacement files are expected in the FileSets directory within the script's directory -If the replacement file content differs with VenusOS version, -subdirectories for each version hold the replacement files -If the replacment is independent of version, it can be placed in FileSets -The version subdirectories are checked first. - -The version sub-directories also contain the stock files with their name ending in .orig. -These are used to look for a match within previous versions when a new Venus version is detected. -A new Venus version with matching files to a previous version updates the file sets automatically. -If the new version has different file content, replacement file(s) will need to be created MANUALLY. -This is typically easy by inspecting previous active and .orig files and the new .orig file. -The file set for the new version is flagged as INCOMPLETE and -will preven installation until the file set is complete and the INCOMPLETE flag is removed manually. - -fileList contains a list of Venus files to be managed by this package. -Packages may also contain files that do not exist in the stock Venus image. -These are NOT included in fileList! - -obsoleteVersion is a file optionally contained in the package directory. -It indicates a Venus OS version at which the package is no longer compatible. -CommonResources prevents the package from being installed for the specified version and all those following it. - -Use full paths/name for all files to avoid problems when running the script from other locations -such as the boot code, and quote them in case the names contain spaces. - -Two flags are set by these routines in order for the setup script to detect changes - $thisFileUpdated is true of the venus file was modified by the operation - It can be tested following each update, copy or restore operation - to determine the success/failure of that one operation - $filesUpdates is true if ANY file is modified by any of these functions - It can be tested at the end of the setup script to know if ANY file - was modified to trigger restarting service or rebooting - -Use updateActiveFile to replace the active file with a replacement from the package - - updateActiveFile replacementFile activeFile - or - updateActiveFile activeFile - First backs up the active file - then copies the replacement version into the active location - If called with two parameters - the first is replacement the file (source) - the second is the active file (destination) - If called with only one parameter, it specifies the active file - the replacement file is selected from FileSets - - restoreActiveFile activeFile - Moves the backup copy to the active location - The first parameter is the name of the active file (the one to be restored to stock) - The file with the same name with .orig at the end is moved - to the active file - If the backup copy doesn't exist BUT the noOrig flag is set - the active copy is deleted to restore the system to stock - - A failure in updateActiveFile and copyToActiveFile set scriptAction to UNINSTALL. - The setup script MUST then remove the package - to prevent system instability from a partially installed package - -The following functions simplify the task of getting user input - - standardActionPrompt displays a menu of actions and asks the user to choose - It sets scriptAction accordingly and returns - It also handles displaying setup and package logs then asks for an action again - It also handles quitting with no action - the fuction EXITS without returning in this case - The basic action prompt includes install, reinstall, quit, display logs (2 choices) - A reinstall option is enabled if the optionsSet option exists - When reinstall is enabled, selecting install, returns a scriptAction of NONE - indicating additional prompting may be needed to complete the install - At the end of these prompts, the main script should set scriptAction to INSTALL - If reinstall is selected, the script action is set to INSTALL and the main script - should then skip additional prompts and allow options set previously to control the install - - yesNoPrompt "question" - Asks the user to answer yes or no to the question - Any details regarding the question should be output before calling yesNoPrompt - yesNoPrompt sets $yesResponse to true if the answer was yes and false if the answer was no - -LogHandler is a logging and log display mechanism. It is sourced by CommonResources and also by reinstallMods - Some executions of the setup scripts are during system boot - where console messages are likely to go unnoticed (if they are visible at all). - Boot-time scripts that output to the console are diverted to /var/log/boot,var/log/messages or dmesg, - but these logs retain messages only from the last boot. - A Setup Helper log is used to make messages from these setup acivities that are more persistent and easier to find. - The setup helper log file is /var/log/SetupHelper (or $setupLogFile) - In addition, some packages have their own log file and logging utilities here write to these logs as well - Finally, when run from the command line, the console (stdout) is also available and provides the most immediate - interface to the user. - A tai64n timestamp is added to messages written to both log files. - This timestamp can be converted to human readable form for display tai64nlocal - The script name is also written to logs - - logMessage "message" - writes "message" the above places - - displayLog logfile - displays the last 100 lines of the log file - $1 is the log file to be displayed. Either: - $setupLogFile or - $packageLogFile - The latter must be initialized in setup script code - If no package log file exists, $packageLogFile shoudl remain null "" - - -Update handler - SetupHelper checks for updates to packages from GitHub or USB/SD media. - The GitHub updates can be automatic. - When a package is installed, it addes the GitHub user and branch info to a file - used by the automatic and manual updates. - The defaultPackageList file is included in the SetupHelper package. - The default is copied to the active location at /data/setupOptions/SetupHelper/packageList - if it does not already exist. - The active file can then be edited to control the updates. - Package setup scripts should add lines to this file during installation. - This is handled in endScript if the packageGitHubUser and packageGitHubBranch are defined in the script - Some packages are included in defaultPackageList - - Normally, the current or latest branch is used to check for updates, but specifying a different branch - or a specific version (of the form v2.3) can also be specified. - - A development branch should be used during package development to prevent affecting others using auto update. - - The repo version is compared with the installed version. - - If a version is specified for the branch, the package will be brought to that version - regardless of what is currently installed. - - If any other branch name is used, an update will occur if the repo verison is numerically greater than the installed version - -PackageList format - The file consists of one line per package naming the package and specifying the GitHub user and branch, e.g., - - SetupHelper kwindrem current - - Blank lines or those beginning with # are ignored - A package can be manually added to this file, or updates disabled by adding a # at the beginning of the line. - -packageVersions file - packageVersions are maintained in a file and also placed in dbus Settings for display in - the Package Versions List menu - The reason for the duplicaiton is dbus access from unix shell scripts is very slow - - Whenever a package is installed, it compares its current version to the one in - the /data/packageVersions file. If there is a difference dbus Settings are updated - - Do NOT edit this file!!! - -Version numbers use the same syntax as Venus OS: - v.~ - v2.45 would replace v2.45~68 as test versions lead up to an eventual release are considered +These are described in detail later in the SetupHelperDescription document. + +Package Manager: + +The Package Manager includes a set of menus on the GX device menu system + that allows the user to view package versions, + control automatic package updates + and manually install, uninstall, add and remove packages. + + A python program runs in the backgrouns (as a service) to do the actual + work and to interface with the menus. + + Package Manager menu: + + The first line of this menu provides status for Package Manager, + indicating what it is currenly doing + + Automatic GitHub downloads controls if packages are automatically downloaded + from GitHub. This occurs if a newer version is available. + Modes: + Normal checks GitHub for a package once every 5 minutes + and waits 10 minutes after a download has occured before checking another + Fast checks GitHub for a package once every 5 seconds (10 seconds after a download), + then after one pass through all packages, the downloads are switched to Normal rate + Once checks GitHub for a package at the fast rate, but only once, then dowloads are turned off + Disable disables GitHub downloads + + Auto install packages: + Controls whether new versions of a package are automatically installed. + Some users may wish to have the system automatically download new updates, + but install them manually. + In this case, automatic GitHub downloads may be turned on and Auto install packages turned off + + Auto install packages also influences whether packages transferred from SD/USB media + are automatically installed or just transferred to local storage + + Active packages: + displays all active packages + + Version information is displayed for each package: + Git Hub shows the version found on GitHub + Stored shows the verison stored on the GX device + Installed shows the version actually installed and running + + Tappingn on one of the entries leads to the Package editor menu + + Inactive packages: + displays all INACTIVE packages + I.e., default packages not yet activated or manually removed + The first entry is always "new" and allows the operator to enter package name, GitHub user and branch/tag + from scratch + Additional lines (if any) are default packages (from the defaultPackageList file) + If a package is already added to the version list, it will not appear in the Add Package list + + Tapping on one of the entries leads to the Add Package menu + + Backup & Restore settings: + saves settings to the settingsBackup file on removable SD/USB media + restores from same + /data/SetupHelper/settingsList is a complete list of settings saved to settingsBackup + GuiMods + SetupHelper + ShutdownManager + SOME Victron stock settings in the following sections + Alarms + CGwacs + DigitalInputs + Generators + Gui + Pump + Relay + System + SystemSetup + Vrmlogger + Any logo files in /data/themes/overlay + The parameters must exist to be saved + The parameters must exist to be updated during a restore + + Note: Victron is working on a more compresenhive mechanism but is not working reliably yet + This part of PackageManager is temporary and will be removed when the Victron functionality is working + + + Package editor: + This menu facilitates: + manual install, uninstall, package add and package remove + changing GitHub access information for each package + + Normally, you would want to download the latest released version + but you may also wish to try out a beta version or revert to a previous one. + Once the GitHub branch is changed, PackageManager will update the GitHub version + and, if enabled, download this alternate version. + + Remove Package + Packages that are of no interest to you may be removed from Package Manager. + Removed packages will no longer appear in the version list or be accessible from the Package Editor menu. + But you can add the package back in manually. + + Add package menu: + Allows the package name, GitHub user and GitHub branch or tag to be entered. + + Pressing Proceed initiates the package add. + The package will be added to the package list (and appear in the Package versions menu) + only IF the name is unique + + Pressing Cancel returns to the default package list + + All three fields must be set appropriately or you'll see -- for the GitHub version + + + Package Manager does not allow removing packages unless they are uninstalled first. + + Package Manager DOES permit uninstalling SetupHelper, + however this will remove the Package Manager itself. + Once removed, the Blind Install mechanism will be needed again !! + + +USB/SD updates: + + When the GX device is not connected to the internet, a USB flash drive or microSD card provides an install/upgrade path. + + To use the USB update process + Navigate to the GitHub, repo, click on tags and select the appropirate branch or specific version tag. + Choose the .tar.gz download link. + (Do not download from the Code tab or download the .zip file. These won't work.) + Copy the archive file to a USB memory stick or microSD card. + Do NOT unpack the archive + Repeat this for all packages you wish to install. + (These can all be placed on the same media along with the SetupHelper venus-data.tgz file) + Insert the stick in the GX device. + If SetupHelper has not yet been installed, follow the Blind Install process above. + Once Package Manager is running, it will transfer and unpack the archive files + and update the package list with the new packages. + If Auto install packages is turned on, the packages will then be installed + + NOTE: no vesion checks are made for packages found on SD/USB media! + Package Manager is quite content to transfer and install an older version! + So make sure you have the latest version especially if your GX device does not have internet access. + +If you are insterested in the inner workings of Setup Manager and Package Manager, read: SetupHelperDescription (to be provided later) diff --git a/Screen Shot Active packages menu.png b/Screen Shot Active packages menu.png new file mode 100644 index 0000000..4bd8bb3 Binary files /dev/null and b/Screen Shot Active packages menu.png differ diff --git a/Screen Shot Add package menu.png b/Screen Shot Add package menu.png new file mode 100644 index 0000000..5b97a50 Binary files /dev/null and b/Screen Shot Add package menu.png differ diff --git a/Screen Shot Inactive packages menu.png b/Screen Shot Inactive packages menu.png new file mode 100644 index 0000000..b7fb8ba Binary files /dev/null and b/Screen Shot Inactive packages menu.png differ diff --git a/Screen Shot Package editor menu.png b/Screen Shot Package editor menu.png new file mode 100644 index 0000000..6f15f3b Binary files /dev/null and b/Screen Shot Package editor menu.png differ diff --git a/Screen Shot Package manager menu.png b/Screen Shot Package manager menu.png new file mode 100644 index 0000000..e1f7274 Binary files /dev/null and b/Screen Shot Package manager menu.png differ diff --git a/Screen Shot SetupHelper uninstall warning.png b/Screen Shot SetupHelper uninstall warning.png new file mode 100644 index 0000000..f71e8c4 Binary files /dev/null and b/Screen Shot SetupHelper uninstall warning.png differ diff --git a/Screen Shot Uninstall confirmation.png b/Screen Shot Uninstall confirmation.png new file mode 100644 index 0000000..58a11de Binary files /dev/null and b/Screen Shot Uninstall confirmation.png differ diff --git a/ServiceResources b/ServiceResources index 6b918d6..765ce7c 100755 --- a/ServiceResources +++ b/ServiceResources @@ -1,6 +1,6 @@ # ServiceManager for SetupHelper # contains a functions to install, remove, start and stop a package's service - +# # managing a normal package's service is straight forward # # normally, services are connected via a symbolic link, but to avoid issues with @@ -36,16 +36,22 @@ fi _startService () { - local pkg=$1 - rm -f "$serviceDir/$pkg/down" + svc -d "/service/$1" + if [ -e "$serviceDir/$1/log" ]; then + svc -d "/service/$1/log" + fi + + rm -f "$serviceDir/$1/down" if $serviceOverlay ; then - rm -f "/service/$pkg/down" - svc -u "/service/$pkg" + rm -f "/service/$1/down" fi - svc -u "/service/$pkg" - if [ -e "$serviceDir/$pkg/log" ]; then - rm -f "$serviceDir/$pkg/log/down" - svc -u "/service/$pkg/log" + svc -u "/service/$1" + if [ -e "$serviceDir/$1/log" ]; then + rm -f "$serviceDir/$1/log/down" + if $serviceOverlay ; then + rm -f "/service/$1/log/down" + fi + svc -u "/service/$1/log" fi } @@ -55,23 +61,21 @@ startService () if [ $# -lt 1 ]; then return fi - local pkg=$1 - if [ -e "$serviceDir/$pkg" ]; then - logMessage "starting $pkg service" - _startService $pkg + if [ -e "$serviceDir/$1" ]; then + logMessage "starting $1 service" + _startService $1 fi } _stopService () { - local pkg=$1 - touch "$serviceDir/$pkg/down" - svc -d "/service/$pkg" - if [ -e "$serviceDir/$pkg/log" ]; then - touch "$serviceDir/$pkg/log/down" - svc -d "/service/$pkg/log" + touch "$serviceDir/$1/down" + svc -d "/service/$1" + if [ -e "$serviceDir/$1/log" ]; then + touch "$serviceDir/$1/log/down" + svc -d "/service/$1/log" fi } @@ -81,11 +85,10 @@ stopService () if [ $# -lt 1 ]; then return fi - local pkg=$1 - if [ -e "$serviceDir/$pkg" ]; then - logMessage "stopping $pkg service" - _stopService $pkg + if [ -e "$serviceDir/$1" ]; then + logMessage "stopping $1 service" + _stopService $1 fi } @@ -95,16 +98,15 @@ stopService () _removeService () { - local pkg=$1 # stop the service - _stopService $pkg + _stopService $1 # remove the service directory # removing the service in the overlayed service directory doesn't remove it from /service # it needs to be removed from the overlay work directory also - rm -rf "$serviceDir/$pkg" + rm -rf "$serviceDir/$1" if $serviceOverlay ; then - rm -rf "$overlayWorkDir/$pkg" + rm -rf "$overlayWorkDir/$1" fi } @@ -114,11 +116,10 @@ removeService () if [ $# -lt 1 ]; then return fi - local pkg=$1 - if [ -e "$serviceDir/$pkg" ]; then - logMessage "removing $pkg service" - _removeService $pkg + if [ -e "$serviceDir/$1" ]; then + logMessage "removing $1 service" + _removeService $1 fi } @@ -135,6 +136,7 @@ removeService () installService () { + local restartService=false # no package specified if [ $# -lt 1 ]; then return @@ -146,31 +148,37 @@ installService () local pkg=$1 - if [ -L "$serviceDir/$pkg" ]; then - logMessage "removing old $pkg service (was symbolic link)" - rm -f "$serviceDir/$pkg" + if [ -L "$serviceDir/$1" ]; then + logMessage "removing old $1 service (was symbolic link)" + rm -f "$serviceDir/$1" fi # service not yet installed, COPY service directory to the active locaiton - if [ ! -e "$serviceDir/$pkg" ]; then - logMessage "installing $pkg service" - cp -R "$scriptDir/service" "$serviceDir/$pkg" + if [ ! -e "$serviceDir/$1" ]; then + logMessage "installing $1 service" + cp -R "$scriptDir/service" "$serviceDir/$1" # service already installed - only copy changed files, then restart service else - logMessage "restarting $pkg service" + restartService=true + logMessage "restarting $1 service" if [ -f "$scriptDir/service/run" ]; then - cmp -s "$scriptDir/service/run" "$serviceDir/$pkg/run" > /dev/null + cmp -s "$scriptDir/service/run" "$serviceDir/$1/run" > /dev/null if [ $? != 0 ]; then - cp "$scriptDir/service/run" "$serviceDir/$pkg/run" + cp "$scriptDir/service/run" "$serviceDir/$1/run" fi fi if [ -f "$scriptDir/service/log/run" ]; then - cmp -s "$scriptDir/service/log/run" "$serviceDir/$pkg/log/run" > /dev/null + cmp -s "$scriptDir/service/log/run" "$serviceDir/$1/log/run" > /dev/null if [ $? != 0 ]; then - cp "$scriptDir/service/log/run" "$serviceDir/$pkg/log/run" + cp "$scriptDir/service/log/run" "$serviceDir/$1/log/run" fi fi fi - _startService $pkg + + # restart service if was previously installed + # a service will start when newly installed so we don't want to restart it + if $restartService ; then + _startService $1 + fi } diff --git a/UpdateResources b/UpdateResources deleted file mode 100755 index 8e3809f..0000000 --- a/UpdateResources +++ /dev/null @@ -1,253 +0,0 @@ - -# This file provides utilities for automatic and manual package updates/installation -# -# Include the following lines in the update scripts -# source "/data/SetupHelper/LogHandler" -# source "/data/SetupHelper/UpdateResources" - -setupHelperDir="/data/SetupHelper" -source "$setupHelperDir/EssentialResources" - -# converts a Venus version string to a version number -# -# beta numbers are handled in a special way. -# the released verison has no beta string -# it's number would be less than a beta version -# we bump the version number by 999 -# so the release is always greater than any beta -# -# the "-large-n" portion is discarded so those versions -# can be compared to non-large versions -# -# the version string is passed as $1 -# the number is returned in versionNumber - -function versionStringToNumber () -{ - local versionBeta="" - - read versionNumber versionBeta <<< $(echo $1 | sed -e 's/v//' -e 's/-.*$//' | \ - awk -v FS='[v.~-]' '{printf "%d%03d%03d %d\n", $1, $2, $3, $3}') - if (( $versionBeta == 0 )); then - ((versionNumber += 999)) - fi -} - -# the version number of the archive is compared to the installed version -# if a the branch specifies a spcific version number, the two versions must match -# for the installed package to be considered up to date -# for a branch label, the installed package is up to date if it's version -# is equal to or greater than the archive's verison -# -# function will return 1 if an update is required and 0 if no update is needed - -function checkVersions () -{ - # convert versions to numbers and compare them - versionStringToNumber $archiveVersion - archiveVersionNumber=$versionNumber - versionStringToNumber $installedVersion - # if version number specified, must match that exact value - if [[ ${gitHubBranch:0:1} == "v" ]] ; then - if (( $versionNumber == $archiveVersionNumber )); then - return 0 - else - return 1 - fi - else - if (( $versionNumber >= $archiveVersionNumber )); then - return 0 - else - return 1 - fi - fi - -} - - -# get the package from a USB file -# The package is left in $package-$gitHubBranch for processing later -# $1 is the name of the package -# returns 0 if updates should NOT occur or 1 if update is acceptable for update - -getFromUsb () -{ - local package=$1 - local packageArchive - local packageDir="/data/$package" - local lastUpdate="" - local fileSuffix - - # the unpacked folder for version tags don't inclue the v (e.g, v2.4 is 2.4 in archive name) - # so the gitHubBranch is reworked to create a file suffix - if [[ ${gitHubBranch:0:1} == "v" ]]; then - fileSuffix=${gitHubBranch:1:999} - else - fileSuffix=$gitHubBranch - fi - packageArchive="/media/$dir/$package"-$fileSuffix.tar.gz - - # archive not found on USB stick - if [ ! -f "$packageArchive" ]; then - return 0 - fi - - tar -xzf "$packageArchive" -C /data - if [ ! -d "$packageDir-$fileSuffix" ]; then - logMessage "ERROR: $packageArchive did not contain $package" - return 0 - fi - - # get the version from local copy of package - if [ -f "$packageDir/version" ]; then - installedVersion=$(cat "$packageDir/version") - else - installedVersion="" - fi - - # get archive version - archiveVersion=$(cat "/data/$package-$fileSuffix/version") - if [ ! -e "$packageDir" ]; then - if $logToConsole ; then - echo "$package not yet installed - proceeding" - fi - return 1 - elif [ -z $archiveVersion ]; then - logMessage "ERROR: No version in $package archive - can't update" - return 0 - elif [ -z $installedVersion ]; then - logMessage "WARNING: no version for $package current installation - proceeding" - return 1 - else - checkVersions - if [ $? -eq 0 ]; then - if $logToConsole ; then - echo "$package is up to date" - fi - return 0 - else - return 1 - fi - fi -} - - -# get the package from a GitHub -# The package is left in $package-$gitHubBranch for processing later -# $1 is the name of the package -# -# $2 is a boolean: when true archive download is skipped -# used by packageInstaller when a package is aready installed and auto updates are enabled -# if auto updates are enabled and update will be skipped anyway -# returns 0 if update should NOT occur or 1 if update is acceptable - -getFromGitHub () -{ - local package=$1 - local packageDir="/data/$package" - local lastUpdate="" - local skipDownload - if [ $# -gt 1 ] && $2 ; then - checkOnly=true - else - checkOnly=false - fi - - # get the version from local copy of package - if [ -f "$packageDir/version" ]; then - installedVersion=$(cat "$packageDir/version") - else - installedVersion="" - fi - - if [ ! -e "$packageDir" ]; then - if $logToConsole ; then - echo "$package not yet installed - proceeding" - fi - fi - - # fetch archive version - archiveVersion=$(wget -qO - https://raw.githubusercontent.com/$gitHubUser/$package/$gitHubBranch/version) - if [ -z $archiveVersion ]; then - logMessage "ERROR: no version for $package $gitHubUser $gitHubBranch on GitHub - can't continue" - return 0 - elif [ -z $installedVersion ]; then - logMessage "WARNING: no version for $package current installation - proceeding" - else - checkVersions - if [ $? -eq 0 ]; then - if $logToConsole ; then - echo "$package is up to date" - fi - return 0 - elif $checkOnly ; then - return 1 - fi - fi - # update the package and reinstall it - wget -qO - https://github.com/$gitHubUser/$package/archive/$gitHubBranch.tar.gz | tar -xzf - -C /data - if [ $? -eq 0 ]; then - return 1 - else - logMessage "ERROR: can't access $package $gitHubUser $gitHubBranch on GitHub" - return 0 - fi -} - - -# install the archive and run setup script -# -# $1 is the package name -# $2 is the github branch/version tag -# $3 is the flag to allow running the script with user interaction - -doUpdate () -{ - local package=$1 - local packageDir="/data/$package" - local gitHubBranch=$2 - local fileSuffix - - # the unpacked folder for version tags don't inclue the v (e.g, v2.4 is 2.4 in archive name) - if [[ ${gitHubBranch:0:1} == "v" ]]; then - fileSuffix=${gitHubBranch:1:999} - else - fileSuffix=$gitHubBranch - fi - - if [ $# -gt 2 ] && [ $3 == 'prompting' ]; then - installOk=true - else - installOk=false - fi - - # move new version into active position - if [ -d "$packageDir-$fileSuffix" ]; then - rm -rf "$packageDir" - mv "$packageDir-$fileSuffix" "$packageDir" - else - logMessage "$packageDir-$fileSuffix is missing - can't continue" - return - fi - - if [ -f "$packageDir/setup" ]; then - # if package is installed, reinstall automatically - if [ -f "$installedFlagPrefix$package" ]; then - logMessage "reinstalling $package" - "$packageDir/setup" "reinstall" "force" - # defer reboot until all updates and reinstalls have been done - if [ $? == $exitReboot ] ; then - rebootNeeded=true - fi - # no options set, run manual setup - but defer reboot until all scripts have been run - elif $installOk ; then - logMessage "running $package setup script - choose install to complete installation" - "$packageDir/setup" "deferReboot" - if [ $? == $exitReboot ] ; then - rebootNeeded=true - fi - else - logMessage "$package not currently active - skipping reinstall" - fi - fi -} diff --git a/blindInstall b/blindInstall index 29379c7..27f912c 100755 --- a/blindInstall +++ b/blindInstall @@ -1,25 +1,35 @@ #!/bin/bash -# this script is part of an autoInstall archive +# this script is part of an autoInstall archive which installs SetupHelper +# previous versions of blindInstall also installed GuiMods but has since been removed +# since PackageManager will install all package archives found on the SD/USB media so +# the result is the same (or actually better) +# # the archive makes use of the Venus OS update-data.sh script -# archives named "venus-data.tar" are unpacked during boot -# then for this archive, Venus must be rebooted, causing rcS.local to run, +# archives named "venus-data.tar.gz" are unpacked during boot +# overriting content in /data +# +# the archive unpacks to /data/SetupHelper-blind +# then blindInstall compares versions between /data/SetupHelper-blind anc /data/SetupHelper +# if blind is NEWER, SetupHelper-blind replaces SetupHelper, and the setup script is run +# +# for this archive, Venus must be rebooted a second time, causing this speial rcS.local to run, # which calls blindInstall as a background task. # -# Package install scripts will append to rcS.local, so it must be moved out of the way -# BEFORE running those scripts to force creation of a clean rcS.local +# a call to /data/SetupHelper/reinstallMods is appended to rcS.local by all setup scripts +# using SetupHelper CommonResources. +# That call is included in the blind install rcS.local so that if the media is left inserted +# subsequent reboots will still check for reinstalls # -# GitHub automatic updates are enabled to keep packages up to date. +# the rcS.local from the blindInstall is removed when SetupHelper/setup is run to keep things clean +# SetupHelper/setup creates a new one # -# the archive includes -# rcS.local and the packages themselves -# # the blindInstall script is run in the background so it can wait for dbus Settings resources # to become available before running the package install scripts. # -source "/data/SetupHelper/EssentialResources" -source "/data/SetupHelper/LogHandler" +source "/data/SetupHelper-blind/EssentialResources" +source "/data/SetupHelper-blind/LogHandler" # wait until dbus settings are active while [ $(dbus -y | grep -c "com.victronenergy.settings") == 0 ]; do @@ -29,28 +39,50 @@ done sleep 2 -logMessage "starting up" +setupHelperBlind='/data/SetupHelper-blind' +setupHelperInstalled='/data/SetupHelper' -# a package setup script normally prompts for user input -# reinstall force options insure the package is installed without user interaction -# -# currently VeCanSetup and RpiDisplaySetup require user responses during initial setup -# so can't be installed via this mechanism - -# all packages rely on SetupHelper so it must be present and installed first -if [ -f "/data/SetupHelper/setup" ]; then - # insure package reinstall doesn't get appended to the auto install rcS.local !!! - rm -f /data/rcS.local - - sync - logMessage "installing SetupHelper" - /data/SetupHelper/setup reinstall force deferReboot - if [ -f "/data/GuiMods/setup" ]; then - logMessage "installing GuiMods" - /data/GuiMods/setup reinstall force deferReboot - fi - - # enable GitHub automatic updates - dbus -y com.victronenergy.settings /Settings/PackageVersion/GitHubAutoUpdate SetValue 1 &> /dev/null +runSetup=false +if [ -d "$setupHelperBlind" ]; then + # compare versions + if [ -d "$setupHelperInstalled" ]; then + if [ -f "$setupHelperBlind/version" ]; then + versionStringToNumber $(cat "$setupHelperBlind/version") + blindVersion=$versionNumber + else + blindVersion=0 + fi + if [ -f "$setupHelperInstalled/version" ]; then + versionStringToNumber $(cat "$setupHelperInstalled/version") + packageVersion=$versionNumber + else + packageVersion=0 + fi + if (( blindVersion > packageVersion )); then + runSetup=true + fi + else + runSetup=true + fi fi +# get rid of the blind install rcS.local so it can't run again +# (unless the archive still exists on the media on next boot) +# SetupHelper/setup will install the proper one +rm -f /data/rcS.local + +if $runSetup ; then + logMessage "blind install SetupHelper" + + # replace SetupHelper with the one from the archive + rm -rf "$setupHelperInstalled" + mv "$setupHelperBlind" "$setupHelperInstalled" + + # eliminiate the blind install call in rcS.local + # SetupHelper/setup will create a new one rcS.local + "$setupHelperInstalled/setup" reinstall +# remove SetupHelper from archive - it wasn't newer +else + logMessage "blind install did NOT update SetupHelper (probably an older version)" + rm -rf "$setupHelperBlind" +fi diff --git a/changes b/changes new file mode 100644 index 0000000..7ed254f --- /dev/null +++ b/changes @@ -0,0 +1,172 @@ +v4.0: + beta test period ended + added running version to PackageManager sign-on + +v4.0~38: + fixed: backup/restore hangs + +v4.0~37: + changed the blind install process to minimize issues if venus-data.tgz is left mounted + SetupHelper now unpacks to /data/SetupHelper-blind, + then is moved to /data/SetupHelper and the setup script run + ONLY IF it is a newer version + +v4.0~36: + blind install was't updated for ~35 !!! + +v4.0~35: + reinstall now compares installed and package versions and installs if they differ + previously, booting to the alternate installed Venus version would not trigger + a package reinstall, possibly resulting in problems or at least out of date packages + +v4.0~34: + added image overlays to backup/restore + this includes custom logos for Mobile and Tile overviews + +v4.0~33: + fixed: PackageManager doesn't run on Venus versions prior to v2.80~10 (Python 2). + +v4.0~32: + fixed: version numbers not in the Victron format would crash PackageManager + also accommodate a other version string formats: + vX.Y.Z, vX.YdZ, vX.YaZ, vX.YbZ + +v4.0~31: + fixed crashes and bugs that prevented initial install on a system + that has no packages yet + +v4.0~30: + added settings backup/restore + note this is NOT the Victron mechanism + rather, it extracts SOME of the Settings parameters + and writes these to a file + care was taken to save/restore only those parameters that + should not cause conflicts + When Victron releases their mechanism, this one will be removed + +v4.0~26: + fixed: Large features not appearing + fixed: typo in 123SmartBMS-Venus in defaultPackageList + +v4.0~25: + handle nonexistant package directory + pull GitHub user/branch from package directory file + optimize AddStoredPackages so it can run all the time + (some updates were being missed) + changed default package name: smartbms-venus to 123SmartBMS-Venus + +v4.0~24: + add Reboot/GUI restart button to Package Manager main menu + to address deferred operations ("Later") + +v4.0~23: + fixed: GUI restart not happening after auto install + fixed: GUI restart notificaiton in menu not cleared after GUI restart +v4.0~22: not used +v4.0~21: + fixed: adding package didn't carry over GitHub user and branch + +v4.0~20: + rearranged package editor menus + Package version list -> Active packages + tapping on an entry leads to Package editor + added Inactive packages + which shows only packages that are not on the system yet (or manually removed) + tapping on entry leads to Add package menu + added separate Add package menu + Package Editor eliminated from main menu (access through Active packages only) + default packages are no longer automatically added to the active package list + +v4.0~19: + fixed: venus-data.tar.gz in v2.80~18 didn't include PackageManager.py + +v4.0~18: + fixed: packages auto add/install when PackageManager is restarted + even if REMOVED / DO_NOT_AUTO_INSTALL was set + more work on GUI getting "stuck" + bogus "unpack tar from GitHub failed" message - package downloads properly + major change to thread structure to make operations more responsive + and to minimize CPU consumption when idle + +v4.0~17: + fixed: package editor status did not always show package name + fixed?: occationally, Package Editor appears to get stuck when a PackageManager + action completes. + A missed property update from the dbus paramter may be yhe cause + Set a timer to refresh properties in the GUI + +v4.0~16: + fixed: packages auto auto-adding following manual removal + +v4.0~15: + fixed: auto download not working + moved dbus settings for PackageManager to /Settings/PackageManager + from /Settings/PackageMonitor + remove dbusSettings when package is removed + previously these were left in place + changed menu items and titles to conform to Victron standards: + only firt word capitalized + +v4.0~14: + fixed: selecting "Now" in GUI when reboot needed does nothing + fixed: blind install did not work + +v4.0~13: + added support for Venus OS v2.80~33-large-24 + +v4.0~12: + fixed: manually uninstalled packages would reinstall immediately + if auto install was on + accommodate Python 2.7 for Venus OS prior to v2.80~10 + fixed: a setup script run failure was not handled properly and + caused the install thread to hang +v4.0~11: + fixed bug that caused GUI to restart repeatedly if package was not + compatible with the current Venus version + e.g., a file set error + major rewrite to PackageManager download code + changes to GUI: + Add Package -> New Package + moved SetpHelper uninstall warning to status + moved action confirmaiton message to status, + Confirm ... button now reads Proceed + removed "can't remove" ... message +v4.0~10: + GitHub downloads and SD/USB transfers now scan the entire directory tree + searching for a package directory. This was done because of the 123 smartBMS + archive directory structure but there are other issues preventing integration + with PackageManger. + Fixed bug that showed a blank status line with the OK button after a download + Rhe OK button no longer appears and Package Editor menu returns + to the "navigation" mode + +v4.0~9: + fixed bug with firstCompatibleVersion + added try: / except: around all subprocess.run calls + so if the call fails, the program continues to run + +v4.0~8: + refresh GitHub version prior to download checks + new upadates to GitHub could be missed + reduced GitHub vesion refres delay + touching a row in Package Version List menu + leads to Package Editor menu + and < will return to the version list + +v4.0~5 - 7: + download bug fixes +v4.0~4: + add Package Manager & GUI + add setup script return codes for above + add optionsRequired flag file (VeCanSetup is only package that needs this now) + + add platform and version checks to CommonResources + add install opiton to CommonResources + better support installs without command line + ### TBD remove logging to package log files + + improve adding packages from SD/USB + + split auto download and auto install + + diff --git a/defaultPackageList b/defaultPackageList index af62a24..4c58baa 100644 --- a/defaultPackageList +++ b/defaultPackageList @@ -1,18 +1,19 @@ -# list of packages managed by SetupHelper +# the DEFAULT list of packages managed by SetupHelper +# actual list is based on what is stored on the system +# this list assists in adding new pacakges # lines beginning with # are ignored and can be used # to remove a package from auto and manual updates # or as comments # blank lines are ignored # incomplete lines are ignored -# SetupHelper should be first (and not commented out) -# so it updates resources used by other packages prior to their update -# packages should add themselves to this list during setup -# Package GitHubUser Version -SetupHelper kwindrem current -GuiMods kwindrem current -ShutdownMonitor kwindrem current -RpiDisplaySetup kwindrem current -RpiGpioSetup kwindrem current -TankRepeater kwindrem current +# Package GitHubUser Tag/branch/version +SetupHelper kwindrem latest +GuiMods kwindrem latest +GeneratorConnector kwindrem latest +ShutdownMonitor kwindrem latest +TankRepeater kwindrem latest +VeCanSetup kwindrem latest +RpiDisplaySetup kwindrem latest +RpiGpioSetup kwindrem latest 123SmartBMS-Venus 123electric latest diff --git a/ext/velib_python/LICENSE b/ext/velib_python/LICENSE new file mode 100755 index 0000000..1e21061 --- /dev/null +++ b/ext/velib_python/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Victron Energy BV + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ext/velib_python/README.md b/ext/velib_python/README.md new file mode 100755 index 0000000..f87b4e2 --- /dev/null +++ b/ext/velib_python/README.md @@ -0,0 +1,102 @@ +velib_python +============ + +[![Build Status](https://travis-ci.org/victronenergy/velib_python.svg?branch=master)](https://travis-ci.org/victronenergy/velib_python) + +This is the general python library within Victron. It contains code that is related to D-Bus and the Color +Control GX. See http://www.victronenergy.com/panel-systems-remote-monitoring/colorcontrol/ for more +infomation about that panel. + +Files busitem.py, dbusitem.py and tracing.py are deprecated. + +The main files are vedbus.py, dbusmonitor.py and settingsdevice.py. + +- Use VeDbusService to put your process on dbus and let other services interact with you. +- Use VeDbusItemImport to read a single value from other processes the dbus, and monitor its signals. +- Use DbusMonitor to monitor multiple values from other processes +- Use SettingsDevice to store your settings in flash, via the com.victronenergy.settings dbus service. See +https://github.com/victronenergy/localsettings for more info. + +Code style +========== + +Comply with PEP8, except: +- use tabs instead of spaces, since we use tabs for all projects within Victron. +- max line length = 110 + +Run this command to set git diff to tabsize is 4 spaces. Replace --local with --global to do it globally for the current +user account. + + git config --local core.pager 'less -x4' + +Run this command to check your code agains PEP8 + + pep8 --max-line-length=110 --ignore=W191 *.py + +D-Bus +===== + +D-Bus is an/the inter process communication bus used on Linux for many things. Victron uses it on the CCGX to have all the different processes exchange data. Protocol drivers publish data read from products (for example battery voltage) on the D-Bus, and other processes (the GUI for example) takes it from the D-Bus to show it on the display. + +Libraries that implement D-Bus connectivity are available in many programming languages (C, Python, etc). There are also many commandline tools available to talk to a running process via D-bus. See for example the dbuscli (executeable name dbus): http://code.google.com/p/dbus-tools/wiki/DBusCli, and also dbus-monitor and dbus-send. + +There are two sides in using the D-Bus, putting information on it (exporting as service with objects) and reading/writing to a process exporting a service. Note that instead of reading with GetValue, you can also subscribe to receive a signal when datachanges. Doing this saves unncessary context-switches in most cases. + +To get an idea of how to publish data on the dbus, run the example: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ python vedbusservice_example.py + vedbusservice_example.py starting up + /Position value is 5 + /Position value is now 10 + try changing our RPM by executing the following command from a terminal + + dbus-send --print-reply --dest=com.victronenergy.example /RPM com.victronenergy.BusItem.SetValue int32:1200 + Reply will be <> 0 for values > 1000: not accepted. And reply will be 0 for values < 1000: accepted. + +Leave that terminal open, start a second terminal, and interrogate above service from the commandline: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus + org.freedesktop.DBus + org.freedesktop.PowerManagement + com.victronenergy.example + org.xfce.Terminal5 + org.xfce.Xfconf + [and many more services in which we are not interested] + +To get more details, add the servicename: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example + / + /Float + /Int + /NegativeInt + /Position + /RPM + /String + +And get the value for the position: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM GetValue + 100 + +And setting the value is also possible, the % makes dbus evaluate what comes behind it, resulting in an int instead of the default (a string).: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %1 + 0 + +In this example, the 0 indicates succes. When trying an unsupported value, 2000, this is what happens: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %2000 + 2 + +Exporting services, and the object paths (/Float, /Position, /Group1/Value1, etcetera) is standard D-Bus functionality. At Victron we designed and implemented a D-Bus interface, called com.victronenergy.BusItem. Example showing all interfaces supported by an object: + + matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM + Interface org.freedesktop.DBus.Introspectable: + String Introspect() + + Interface com.victronenergy.BusItem: + Int32 SetValue(Variant newvalue) + String GetDescription(String language, Int32 length) + String GetText() + Variant GetValue() diff --git a/ext/velib_python/settingsdevice.py b/ext/velib_python/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/ext/velib_python/settingsdevice.py @@ -0,0 +1,118 @@ +import dbus +import logging +import time +from functools import partial + +# Local imports +from vedbus import VeDbusItemImport + +## Indexes for the setting dictonary. +PATH = 0 +VALUE = 1 +MINIMUM = 2 +MAXIMUM = 3 +SILENT = 4 + +## The Settings Device class. +# Used by python programs, such as the vrm-logger, to read and write settings they +# need to store on disk. And since these settings might be changed from a different +# source, such as the GUI, the program can pass an eventCallback that will be called +# as soon as some setting is changed. +# +# The settings are stored in flash via the com.victronenergy.settings service on dbus. +# See https://github.com/victronenergy/localsettings for more info. +# +# If there are settings in de supportSettings list which are not yet on the dbus, +# and therefore not yet in the xml file, they will be added through the dbus-addSetting +# interface of com.victronenergy.settings. +class SettingsDevice(object): + ## The constructor processes the tree of dbus-items. + # @param bus the system-dbus object + # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' + # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether + # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will + # be logged by localsettings. + # @param eventCallback function that will be called on changes on any of these settings + # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the + # interval if the localsettings D-Bus service has not appeared yet. + def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): + logging.debug("===== Settings device init starting... =====") + self._bus = bus + self._dbus_name = name + self._eventCallback = eventCallback + self._values = {} # stored the values, used to pass the old value along on a setting change + self._settings = {} + + count = 0 + while True: + if 'com.victronenergy.settings' in self._bus.list_names(): + break + if count == timeout: + raise Exception("The settings service com.victronenergy.settings does not exist!") + count += 1 + logging.info('waiting for settings') + time.sleep(1) + + # Add the items. + self.addSettings(supportedSettings) + + logging.debug("===== Settings device init finished =====") + + def addSettings(self, settings): + for setting, options in settings.items(): + silent = len(options) > SILENT and options[SILENT] + busitem = self.addSetting(options[PATH], options[VALUE], + options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) + self._settings[setting] = busitem + self._values[setting] = busitem.get_value() + + def addSetting(self, path, value, _min, _max, silent=False, callback=None): + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): + logging.debug("Setting %s found" % path) + else: + logging.info("Setting %s does not exist yet or must be adjusted" % path) + + # Prepare to add the setting. Most dbus types extend the python + # type so it is only necessary to additionally test for Int64. + if isinstance(value, (int, dbus.Int64)): + itemType = 'i' + elif isinstance(value, float): + itemType = 'f' + else: + itemType = 's' + + # Add the setting + # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface + settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) + setting_path = path.replace('/Settings/', '', 1) + if silent: + settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) + else: + settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) + + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + + return busitem + + def handleChangedSetting(self, setting, servicename, path, changes): + oldvalue = self._values[setting] if setting in self._values else None + self._values[setting] = changes['Value'] + + if self._eventCallback is None: + return + + self._eventCallback(setting, oldvalue, changes['Value']) + + def setDefault(self, path): + item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) + item.set_default() + + def __getitem__(self, setting): + return self._settings[setting].get_value() + + def __setitem__(self, setting, newvalue): + result = self._settings[setting].set_value(newvalue) + if result != 0: + # Trying to make some false change to our own settings? How dumb! + assert False diff --git a/ext/velib_python/ve_utils.py b/ext/velib_python/ve_utils.py new file mode 100644 index 0000000..e9a7fe3 --- /dev/null +++ b/ext/velib_python/ve_utils.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import sys +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +class NoVrmPortalIdError(Exception): + pass + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using GLib.idle_add and also GLib.timeout_add. +# Without this, the code will just keep running, since GLib does not stop the mainloop on an +# exception. +# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # The original definition of the VRM Portal ID is that it is the mac + # address of the onboard- ethernet port (eth0), stripped from its colons + # (:) and lower case. This may however differ between platforms. On Venus + # the task is therefore deferred to /sbin/get-unique-id so that a + # platform specific method can be easily defined. + # + # If /sbin/get-unique-id does not exist, then use the ethernet address + # of eth0. This also handles the case where velib_python is used as a + # package install on a Raspberry Pi. + # + # On a Linux host where the network interface may not be eth0, you can set + # the VRM_IFACE environment variable to the correct name. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + portal_id = None + + # First try the method that works if we don't have a data partition. This + # will fail when the current user is not root. + try: + portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() + if not portal_id: + raise NoVrmPortalIdError("get-unique-id returned blank") + __vrm_portal_id = portal_id + return portal_id + except CalledProcessError: + # get-unique-id returned non-zero + raise NoVrmPortalIdError("get-unique-id returned non-zero") + except OSError: + # File doesn't exist, use fallback + pass + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + except IOError: + raise NoVrmPortalIdError("ioctl failed for eth0") + + __vrm_portal_id = info[18:24].hex() + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception as ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def get_load_averages(): + c = read_file('/proc/loadavg') + return c.split(' ')[:3] + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip() + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception as ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return str(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([bytes(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/ext/velib_python/vedbus.py b/ext/velib_python/vedbus.py new file mode 100644 index 0000000..4e54203 --- /dev/null +++ b/ext/velib_python/vedbus.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + def _get_tree_dict(self, path, get_text=False): + logging.debug("_get_tree_dict called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + def _create_tree_export(self, bus, objectPath, get_value_handler): + return VeDbusTreeExport(bus, objectPath, get_value_handler) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match != None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, get_value_handler): + dbus.service.Object.__init__(self, bus, objectPath) + self._get_value_handler = get_value_handler + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.local_set_value(None) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + if self._value == newvalue: + return + + self._value = newvalue + + changes = {} + changes['Value'] = wrap_dbus_value(newvalue) + changes['Text'] = self.GetText() + self.PropertiesChanged(changes) + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/fileList b/fileList new file mode 100644 index 0000000..6d55f03 --- /dev/null +++ b/fileList @@ -0,0 +1 @@ +/opt/victronenergy/gui/qml/PageSettings.qml diff --git a/gitHubInfo b/gitHubInfo new file mode 100644 index 0000000..3435a5c --- /dev/null +++ b/gitHubInfo @@ -0,0 +1 @@ +kwindrem:latest diff --git a/packageAutoUpdater b/packageAutoUpdater deleted file mode 100755 index aa53004..0000000 --- a/packageAutoUpdater +++ /dev/null @@ -1,268 +0,0 @@ -#!/bin/bash - -# This script keeps Venus modification packages up to date -# It looks on GitHub for package updates -# The timestamp of the current tag on GitHub is compatred to the one that was last installed -# if GitHub is newer, the local copy is updated and the package is reinstalled -# -# This script also checks for updates on a USB stick and gives priority to that location -# -# An automatic update will only occur if the package is installed and the autoUpdate setup option is set -# -# the script runs as a service so any updates that also update this script will restart automatically - -# get the full, unambiguous path to this script -scriptDir="$( cd "$(dirname $0)" >/dev/null 2>&1 ; /bin/pwd -P )" -packageame=$(basename "$scriptDir") -shortScriptName=$(basename "$scriptDir")/$(basename "$0") - -source "/data/SetupHelper/EssentialResources" -source "/data/SetupHelper/LogHandler" - -# this flag is tested by LogHandler to determine if messages should be output to the console -logToConsole=false - -source "/data/SetupHelper/UpdateResources" -source "/data/SetupHelper/DbusSettingsResources" - -# updates status message on GUI -# $1 is the message identifier (not the actual message - -lastMessage='' -lastPackage='' - -updateStatus () -{ - updateDbus=false - checkPackage=false - if [[ $1 == 'IDLE' ]] ; then - message="Fast updates: 10 sec/pkg, Normal updates: 10 min/pkg" - elif [[ $1 == 'USB_DISABLED' ]] ; then - message="USB/SD updates disabled auto GitHub updates" - elif [[ $1 == 'CHECKING' ]] ; then - message="checking $package" - checkPackage=true - elif [[ $1 == 'WAIT' ]] ; then - message="waiting to check $package" - checkPackage=true - elif [[ $1 == 'USB_CHECK' ]] ; then - message="Checking USB/SD for updates" - else - message="" - fi - if [[ $1 != $lastMessage ]]; then - updateDbus=true - elif $checkPackage && [[ $package != $lastPackage ]]; then - updateDbus=true - fi - # update GUI status message - if $updateDbus ; then - updateDbusStringSetting "/Settings/PackageVersion/CheckingPackage" "$message" - fi - lastMessage=$1 - lastPackage=$package -} - - -#### main code starts here - -# wait until dbus settings are active -while [ $(dbus -y | grep -c "com.victronenergy.settings") == 0 ]; do - logMessage "waiting for dBus settings" - sleep 1 -done - -logMessage "starting up" - -usbCheck=false -mediaDetected=false -lastUpdateTime=0 -checkingPackage=false -updateSetupHelper=false - - -# 10 minutes between GitHub checks to minimize network traffic -gitHubSlowCheckDelay=600 -# 10 seconds for first pass -gitHubFastCheckDelay=10 -gitHubCheckDelay=0 - -lastGitHubUpdateSetting=0 - -# loop forever -while true ; do - rebootNeeded=false - restartFromFirstPackage=false - - # skip all processing if package list doesn't exist - # but keep checking - if [ ! -f "$packageListFile" ]; then - sleep 10 - continue - fi - - # loop through packages from package list - while read -u 9 package gitHubUser gitHubBranch; do - # skip comments - if [[ ${package:0:1} == "#" ]] ; then - continue - # skip blank/incomplete lines - elif [ -z $package ] || [ -z $gitHubUser ] || [ -z $gitHubBranch ] ; then - continue - fi - - packageDir="/data/$package" - setupOptionsDir="$setupOptionsRoot"/$package - - # skip uninstalled packages - if [ ! -f "$installedFlagPrefix"$package ]; then - continue - # package has been installed, check for updates - else - checkingPackage=true - fi - - # this loop permits detection of USB media during the long wait for the next GitHub check - while $checkingPackage ; do - doUpdate=false - - # pull Git Hub autoupdate mode from dbus - autoUpdateSetting=$(dbus-send --system --print-reply=literal --dest=com.victronenergy.settings /Settings/PackageVersion/GitHubAutoUpdate\ - com.victronenergy.BusItem.GetValue 2> /dev/null | awk '{print $3}') - if [ -z $autoUpdateSetting ]; then - autoUpdateSetting=0 - fi - - # check for USB / SD media - mediaList=($(ls /media)) - # no media - if [ -z $mediaList ] ; then - mediaDetected=false - usbCheck=false - # media first detected, enable USB checks and start loop over - elif ! $mediaDetected ; then - mediaDetected=true - usbCheck=true - updateStatus 'USB_CHECK' - checkingPackage=false - restartFromFirstPackage=true - break - fi - - # nothing to do - reset loop and wait - if (( $autoUpdateSetting == 0 )) && ! $usbCheck ; then - checkingPackage=false - restartFromFirstPackage=true - updateStatus 'IDLE' - break - fi - - # USB / SD updates - if $usbCheck ; then - for dir in ${mediaList[@]} ; do - getFromUsb $package - if [ $? -eq 1 ]; then - logMessage "found $package on USB" - doUpdate=true - updateStatus 'CHECKING' - break - fi - done - # done checking for this package, time to move on - checkingPackage=false - - # Git Hub updates - elif (( $autoUpdateSetting != 0 )); then - # if speeding up the loop, start package scan over - if (( $autoUpdateSetting >= 2 )) && (( $lastGitHubUpdateSetting <= 1 )); then - checkingPackage=false - restartFromFirstPackage=true - lastGitHubUpdateSetting=$autoUpdateSetting - break - fi - - # set update delay based on update mode - if (( $autoUpdateSetting >= 2 )) ; then - gitHubCheckDelay=$gitHubFastCheckDelay - else - gitHubCheckDelay=$gitHubSlowCheckDelay - fi - - currentTime=$(date '+%s') - # wait between GitHub updates to minimize traffic - if (( $currentTime >= $lastUpdateTime + $gitHubCheckDelay )) ; then - updateStatus 'CHECKING' - lastUpdateTime=$currentTime - getFromGitHub $package - if [ $? -eq 1 ]; then - logMessage "found $package on GitHub" - doUpdate=true - fi - checkingPackage=false - elif (( $autoUpdateSetting != 0 )); then - updateStatus 'WAIT' - fi - fi - if $doUpdate ; then - # do SetupHelper update now since other setup scripts depend on it's resources - # will end this script which will start up again via supervise - if [ $package == "SetupHelper" ]; then - updateSetupHelper=true - break - # reinstall the package without user interaction - else - # update via unattended reinstall only - logMessage "updating $package with $gitHubBranch" - doUpdate $package $gitHubBranch - if $usbCheck ; then - sleep 1 - fi - fi - elif (( $autoUpdateSetting == 0 )); then - rm -rf "$packageDir-$gitHubBranch" - fi - - # delay for inner loop - if $checkingPackage ; then - if $usbCheck ; then - usleep 200000 - else - sleep 1 - fi - fi - done # end while checkingPackage - - if $restartFromFirstPackage || $updateSetupHelper ; then - break - else - sleep 1 - fi - done 9< "$packageListFile" # end while read ... - -# if not restarting scan, update automatic update state - if ! $restartFromFirstPackage && ! $updateSetupHelper; then - # single pass - if (( $autoUpdateSetting == 3 )) ; then - setSetting 0 /Settings/PackageVersion/GitHubAutoUpdate - # end of first pass, switch to slow updates - elif (( $autoUpdateSetting == 2 )) ; then - setSetting 1 /Settings/PackageVersion/GitHubAutoUpdate - fi - fi - usbCheck=false - - # continue execution in packageAutoUpdateCleanup - # so that this script and the service can exit cleanly - if $updateSetupHelper || $rebootNeeded ; then - logMessage "continuing in cleanup script - $packageName exiting" - - nohup "$scriptDir/packageAutoUpdaterCleanup" $gitHubBranch $updateSetupHelper $rebootNeeded 2>&1 | awk '{print "packageAutoUpdaterCleanup " $0}'| tai64n >> /var/log/SetupHelper & - - # shutdown the service which runs this script - this will end this script - svc -d "/service/SetupHelper" - - # wait here for service to end this script - sleep 10000 - fi - sleep 1 -done diff --git a/packageAutoUpdaterCleanup b/packageAutoUpdaterCleanup deleted file mode 100755 index 8b2579b..0000000 --- a/packageAutoUpdaterCleanup +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# This script keeps cleans up at the end of packageAutoUpdater processing -# It is called as a nohup (no hang up) process so that the main script can terminate cleanly -# Mainly, this script will run SetupHelper's setup script to reinstall itself -# which can't be done from packageAutoUpdater. Doing so would abort the installation prematurely. -# This script also reboots the system if needed. - -# get parameters from command line -gitHubBranch=$1 -updateSetupHelper=$2 -rebootNeeded=$3 - -# get the full, unambiguous path to this script -scriptDir="$( cd "$(dirname $0)" >/dev/null 2>&1 ; /bin/pwd -P )" -shortScriptName=$(basename "$scriptDir")/$(basename "$0") - -source "/data/SetupHelper/LogHandler" - -# this flag is tested by LogHandler to determine if messages should be output to the console -logToConsole=false - -source "/data/SetupHelper/UpdateResources" - -# give calling script time to stop SetupHelper service -sleep 2 - -if $updateSetupHelper ; then - logMessage "running SetupHelper setup script to reinstall" - doUpdate "SetupHelper" $gitHubBranch -fi - -# reboot now if any script reboots were indicated -#if $rebootNeeded ; then -# logMessage "rebooting ..." -# reboot -#fi diff --git a/packageInstaller b/packageInstaller deleted file mode 100755 index dcdc816..0000000 --- a/packageInstaller +++ /dev/null @@ -1,150 +0,0 @@ -#!/bin/bash - -# This script prompts for manual installation of packages from the packageList file -# it is called from SetupHelper/setup or can be run from the command line -# Packages are installed either from GitHub or from a USB stick -# - -source "/data/SetupHelper/CommonResources" -source "/data/SetupHelper/UpdateResources" - -echo -echo "This script allows Venus modification packages to be installed" -echo "Previously installed packages are updated automatically, not from this script" -echo "If updating from USB stick, insert it now before proceeding" -echo -while true ; do - read -p "Select the package source GitHub (g) or USB (u): " response - case $response in - [gG]*) - updateFromUsb=false - break;; - [uU]*) - updateFromUsb=true - break;; - *) - ;; - esac -done - -if $updateFromUsb ; then - # wait up to 20 seconds for USB stick - timeoutCount=20 - /bin/echo -n "Waiting for USB media" - while true ; do - mediaList=($(ls /media)) - if [ ! -z $mediaList ] ; then - break; - elif ((timeoutCount-- == 0)); then - echo - echo "no usb media found - exiting" - exit - else - /bin/echo -n "." - sleep 1 - fi - done - echo -fi - -# allow reinstallations if auto updates are not enabled -autoUpdateSetting=$(dbus-send --system --print-reply=literal --dest=com.victronenergy.settings /Settings/PackageVersion/GitHubAutoUpdate\ - com.victronenergy.BusItem.GetValue 2> /dev/null | awk '{print $3}') -if [ -z $autoUpdateSetting ] || [ $autoUpdateSetting == 0 ]; then - autoUpdatesEnabled=false -else - autoUpdatesEnabled=true -fi - -rebootNeeded=false -updateSetupHelper=false - -# skip all processing if package list doesn't exist -if [ ! -f "$packageListFile" ]; then - echo "no packageList file found" - exit -fi - -# loop through packages from package list file -while read -u 9 package gitHubUser gitHubBranch; do - # skip comments - if [[ ${package:0:1} == "#" ]] ; then - continue - # skip blank/incomplete lines - elif [ -z $package ] || [ -z $gitHubUser ] || [ -z $gitHubBranch ] ; then - continue - fi - - doUpdate=false - packageDir="/data/$package" - - # if automatic updates are enabled, skip packages that are installed - checkOnly=false - if [ -e $packageDir ] && [ -f "$installedFlagPrefix"$package ]; then - installText="reinstall" - # update will be skipped so set a flag so getFromGitHub only checks versions - if ! $updateFromUsb ; then - checkOnly=true - fi - else - installText="install" - fi - - if $updateFromUsb ; then - for dir in ${mediaList[@]} ; do - getFromUsb $package - if [ $? -eq 1 ]; then - echo - yesNoPrompt "$installText $package from USB? (y/n): " - if $yesResponse ; then - doUpdate=true - else - rm -rf "$packageDir-$gitHubBranch" - fi - break; - fi - done - - # check GitHub - else - getFromGitHub $package $checkOnly - if [ $? -eq 1 ]; then - if $autoUpdatesEnabled ; then - echo "$package needs update but will be updated automatically - skipping" - continue - fi - echo - yesNoPrompt "$installText $package from GitHub? (y/n): " - if $yesResponse ; then - doUpdate=true - else - rm -rf "$packageDir-$gitHubBranch" - fi - fi - fi - if $doUpdate ; then - # defer running SetupHelper script - if [ $package == "SetupHelper" ]; then - updateSetupHelper=true - # update the package with user interaction if it is needed to do a full install (not reinstall) - else - doUpdate $package 'prompting' - fi - fi -done 9< "$packageListFile" # end while read ... - -# if an update was found for SetupHelper, run it's setup script now -# this prevents stepping on SetupHelper resources -# (this script will complete even if it's replaced) -if $updateSetupHelper ; then - doUpdate "SetupHelper" 'prompting' -fi -if $rebootNeeded ; then - echo - yesNoPrompt "Reboot needed to complete installation, do it now? (y/n): " - if $yesResponse ; then - echo "rebooting ..." - else - echo "You must reboot manually to complete installation" - fi -fi diff --git a/rcS.local b/rcS.local index 87bfa5c..900d80b 100755 --- a/rcS.local +++ b/rcS.local @@ -1,4 +1,6 @@ #!/bin/bash # SetupHelper reinstall all Venus mods -nohup /data/SetupHelper/reinstallMods > /dev/null & +if [ -f /data/SetupHelper/reinstallMods ]; then + nohup /data/SetupHelper/reinstallMods > /dev/null & +fi diff --git a/reinstallMods b/reinstallMods index 55f36b0..1c713c1 100755 --- a/reinstallMods +++ b/reinstallMods @@ -14,18 +14,19 @@ source "$scriptDir/LogHandler" # disable outputting log messages to console runningAtBoot=true +if [ ! -f "$reinstallScriptsList" ] ; then + logMessage "$reinstallScriptsList file not found" + exit +fi + +logMessage "reinstallMods starting" + # wait until dbus settings are active while [ $(dbus -y | grep -c "com.victronenergy.settings") == 0 ]; do logMessage "waiting for dBus settings" sleep 2 done - -if [ ! -f "$reinstallScriptsList" ] ; then - logMessage "$reinstallScriptsList file not found" - exit -fi - # read lines from script list file specified above # and call each script rebootNeeded=false @@ -50,4 +51,6 @@ done 9< "$reinstallScriptsList" if $rebootNeeded ; then logMessage "rebooting ..." reboot +else + logMessage "reinstallMods complete" fi diff --git a/removeManagerSettings b/removeManagerSettings new file mode 100755 index 0000000..6dad700 --- /dev/null +++ b/removeManagerSettings @@ -0,0 +1,48 @@ +#!/bin/bash + + +# remove Settings associated with previous versions of SetupHelper +# and those of the current verison +# run this script to make a clean start + +source /data/SetupHelper/DbusSettingsResources + +removeDbusSettings /Settings/PackageManager/Count \ + /Settings/PackageManager/GitHubAutoUpdate \ + /Settings/PackageManager/GitHubAutoDownload \ + /Settings/PackageManager/AutoInstall \ + /Settings/PackageManager/Edit/PackageName \ + /Settings/PackageManager/Edit/GitHubUser \ + /Settings/PackageManager/Edit/GitHubBranch \ + /Settings/PackageManager/0/PackageName \ + /Settings/PackageManager/0/GitHubUser \ + /Settings/PackageManager/0/GitHubBranch \ + /Settings/PackageManager/1/PackageName \ + /Settings/PackageManager/1/GitHubUser \ + /Settings/PackageManager/1/GitHubBranch \ + /Settings/PackageManager/2/PackageName \ + /Settings/PackageManager/2/GitHubUser \ + /Settings/PackageManager/2/GitHubBranch \ + /Settings/PackageManager/3/PackageName \ + /Settings/PackageManager/3/GitHubUser \ + /Settings/PackageManager/3/GitHubBranch \ + /Settings/PackageManager/4/PackageName \ + /Settings/PackageManager/4/GitHubUser \ + /Settings/PackageManager/4/GitHubBranch > /dev/null + +removeDbusSettings /Settings/PackageManager/5/PackageName \ + /Settings/PackageManager/5/GitHubUser \ + /Settings/PackageManager/5/GitHubBranch \ + /Settings/PackageManager/6/PackageName \ + /Settings/PackageManager/6/GitHubUser \ + /Settings/PackageManager/6/GitHubBranch \ + /Settings/PackageManager/7/PackageName \ + /Settings/PackageManager/7/GitHubUser \ + /Settings/PackageManager/7/GitHubBranch \ + /Settings/PackageManager/8/PackageName \ + /Settings/PackageManager/8/GitHubUser \ + /Settings/PackageManager/8/GitHubBranch \ + /Settings/PackageManager/9/PackageName \ + /Settings/PackageManager/9/GitHubUser \ + /Settings/PackageManager/9/GitHubBranch > /dev/null + diff --git a/removePkgSettings b/removePkgSettings new file mode 100755 index 0000000..95ce4d7 --- /dev/null +++ b/removePkgSettings @@ -0,0 +1,120 @@ +#!/bin/bash + + +# remove Settings associated with previous versions of SetupHelper +# and those of the current verison +# run this script to make a clean start + +source /data/SetupHelper/DbusSettingsResources + +removeDbusSettings /Settings/PackageMonitor/Count \ + /Settings/PackageMonitor/GitHubAutoUpdate \ + /Settings/PackageMonitor/GitHubAutoDownload \ + /Settings/PackageMonitor/AutoInstall \ + /Settings/PackageMonitor/Edit/PackageName \ + /Settings/PackageMonitor/Edit/GitHubUser \ + /Settings/PackageMonitor/Edit/GitHubBranch \ + /Settings/PackageMonitor/0/PackageName \ + /Settings/PackageMonitor/0/GitHubUser \ + /Settings/PackageMonitor/0/GitHubBranch \ + /Settings/PackageMonitor/1/PackageName \ + /Settings/PackageMonitor/1/GitHubUser \ + /Settings/PackageMonitor/1/GitHubBranch \ + /Settings/PackageMonitor/2/PackageName \ + /Settings/PackageMonitor/2/GitHubUser \ + /Settings/PackageMonitor/2/GitHubBranch \ + /Settings/PackageMonitor/3/PackageName \ + /Settings/PackageMonitor/3/GitHubUser \ + /Settings/PackageMonitor/3/GitHubBranch \ + /Settings/PackageMonitor/4/PackageName \ + /Settings/PackageMonitor/4/GitHubUser \ + /Settings/PackageMonitor/4/GitHubBranch > /dev/null + +removeDbusSettings /Settings/PackageMonitor/5/PackageName \ + /Settings/PackageMonitor/5/GitHubUser \ + /Settings/PackageMonitor/5/GitHubBranch \ + /Settings/PackageMonitor/6/PackageName \ + /Settings/PackageMonitor/6/GitHubUser \ + /Settings/PackageMonitor/6/GitHubBranch \ + /Settings/PackageMonitor/7/PackageName \ + /Settings/PackageMonitor/7/GitHubUser \ + /Settings/PackageMonitor/7/GitHubBranch \ + /Settings/PackageMonitor/8/PackageName \ + /Settings/PackageMonitor/8/GitHubUser \ + /Settings/PackageMonitor/8/GitHubBranch \ + /Settings/PackageMonitor/9/PackageName \ + /Settings/PackageMonitor/9/GitHubUser \ + /Settings/PackageMonitor/9/GitHubBranch > /dev/null + +removeDbusSettings /Settings/PackageMonitor/10/PackageName \ + /Settings/PackageMonitor/10/GitHubUser \ + /Settings/PackageMonitor/10/GitHubBranch \ + /Settings/PackageMonitor/11/PackageName \ + /Settings/PackageMonitor/11/GitHubUser \ + /Settings/PackageMonitor/11/GitHubBranch \ + /Settings/PackageMonitor/12/PackageName \ + /Settings/PackageMonitor/12/GitHubUser \ + /Settings/PackageMonitor/12/GitHubBranch \ + /Settings/PackageMonitor/13/PackageName \ + /Settings/PackageMonitor/13/GitHubUser \ + /Settings/PackageMonitor/13/GitHubBranch \ + /Settings/PackageMonitor/14/PackageName \ + /Settings/PackageMonitor/14/GitHubUser \ + /Settings/PackageMonitor/14/GitHubBranch > /dev/null +removeDbusSettings /Settings/PackageMonitor/15/PackageName \ + /Settings/PackageMonitor/15/GitHubUser \ + /Settings/PackageMonitor/15/GitHubBranch \ + /Settings/PackageMonitor/16/PackageName \ + /Settings/PackageMonitor/16/GitHubUser \ + /Settings/PackageMonitor/16/GitHubBranch \ + /Settings/PackageMonitor/17/PackageName \ + /Settings/PackageMonitor/17/GitHubUser \ + /Settings/PackageMonitor/17/GitHubBranch \ + /Settings/PackageMonitor/18/PackageName \ + /Settings/PackageMonitor/18/GitHubUser \ + /Settings/PackageMonitor/18/GitHubBranch \ + /Settings/PackageMonitor/19/PackageName \ + /Settings/PackageMonitor/19/GitHubUser \ + /Settings/PackageMonitor/19/GitHubBranch > /dev/null + +removeDbusSettings /Settings/PackageVersion/Count \ + /Settings/PackageVersion/New/GitHubUser \ + /Settings/PackageVersion/New/GitHubBranch \ + /Settings/PackageVersion/New/InstalledVersion \ + /Settings/PackageVersion/New/PackageName \ + /Settings/PackageVersion/New/PackageVersion \ + /Settings/PackageVersion/EditPackageIndex \ + /Settings/PackageVersion/GitHubAutoUpdate \ + /Settings/PackageVersion/GitHubAutoUpdate \ + /Settings/PackageVersion/EditAction \ + /Settings/PackageVersion/CheckingPackage \ + /Settings/PackageVersion/0/PackageName \ + /Settings/PackageVersion/0/GitHubUser \ + /Settings/PackageVersion/0/GitHubBranch \ + /Settings/PackageVersion/0/GitHubVersion \ + /Settings/PackageVersion/0/InstalledVersion \ + /Settings/PackageVersion/0/PackageVersion > /dev/null + +removeDbusSettings /Settings/PackageVersion/1/PackageName \ + /Settings/PackageVersion/1/GitHubUser \ + /Settings/PackageVersion/1/GitHubBranch \ + /Settings/PackageVersion/1/GitHubVersion \ + /Settings/PackageVersion/1/InstalledVersion \ + /Settings/PackageVersion/1/PackageVersion \ + /Settings/PackageVersion/2/PackageName \ + /Settings/PackageVersion/2/GitHubUser \ + /Settings/PackageVersion/2/GitHubBranch \ + /Settings/PackageVersion/2/GitHubVersion \ + /Settings/PackageVersion/2/InstalledVersion \ + /Settings/PackageVersion/2/PackageVersion \ + /Settings/PackageVersion/3/PackageName \ + /Settings/PackageVersion/3/GitHubUser \ + /Settings/PackageVersion/3/GitHubBranch \ + /Settings/PackageVersion/3/EditAction \ + /Settings/PackageVersion/3/GitHubVersion \ + /Settings/PackageVersion/3/InstalledVersion \ + /Settings/PackageVersion/3/PackageVersion > /dev/null + +removeDbusSettings /Settings/PackageMonitor/Edit/GitHubUserEdit/GitHubUser \ + /Settings/PackageVersion/4/PackageName \ + /Settings/PackageVersion/4/PackageVersion > /dev/null diff --git a/sampleSetupScript b/sampleSetupScript index ac51b54..b3340a5 100755 --- a/sampleSetupScript +++ b/sampleSetupScript @@ -18,13 +18,6 @@ # # key segments are marked with #### -# version that obsoletes this package -# package should not be run STARTING with this version -# CommonResources will set scriptAction to UNINSTALL -# and prevent installation if Venus version is at this version or later -# set to a blank string if package is not obsolete -obsoleteVersion="v2.70~12" - #### following lines incorporate SetupHelper utilities into this script # Refer to the SetupHelper ReadMe file for details. @@ -135,10 +128,6 @@ if [ $scriptAction == 'INSTALL' ] ; then restartAppsNeeded=true fi - # if some processing requires running this script on the next reboot, set the flag then process it later - if $something3 ; then - runAgain=true - fi fi # #### uninstalling - check scriptAction again @@ -159,7 +148,6 @@ if [ $scriptAction == 'UNINSTALL' ] ; then # set exit flags as needed rebootNeeded=false restartAppsNeeded=true - runAgain=false fi diff --git a/service/.DS_Store b/service/.DS_Store index 5ab65af..68347ed 100644 Binary files a/service/.DS_Store and b/service/.DS_Store differ diff --git a/service/log/run b/service/log/run new file mode 100755 index 0000000..4bc84bd --- /dev/null +++ b/service/log/run @@ -0,0 +1,3 @@ +#!/bin/sh +exec multilog t s25000 n4 /var/log/PackageManager + diff --git a/service/run b/service/run index efe8281..a672d0b 100755 --- a/service/run +++ b/service/run @@ -1,4 +1,4 @@ #!/bin/sh -#exec 2>&1 -exec /data/SetupHelper/packageAutoUpdater > /dev/null +exec 2>&1 +exec /data/SetupHelper/PackageManager.py diff --git a/settingsList b/settingsList new file mode 100644 index 0000000..11affe1 --- /dev/null +++ b/settingsList @@ -0,0 +1,305 @@ +/Settings/Alarm/Audible +/Settings/Alarm/System/GridLost +/Settings/Alarm/Vebus/HighDcCurrent +/Settings/Alarm/Vebus/HighDcRipple +/Settings/Alarm/Vebus/HighDcVoltage +/Settings/Alarm/Vebus/HighTemperature +/Settings/Alarm/Vebus/InverterOverload +/Settings/Alarm/Vebus/LowBattery +/Settings/Alarm/Vebus/TemperatureSenseError +/Settings/Alarm/Vebus/VeBusError +/Settings/Alarm/Vebus/VoltageSenseError +/Settings/Ble/Service/Pincode +/Settings/CGwacs/AcPowerSetPoint +/Settings/CGwacs/BatteryLife/DischargedTime +/Settings/CGwacs/BatteryLife/Flags +/Settings/CGwacs/BatteryLife/MinimumSocLimit +/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day +/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration +/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc +/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start +/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day +/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Duration +/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Soc +/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Start +/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Day +/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Duration +/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Soc +/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Start +/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Day +/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Duration +/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Soc +/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Start +/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Day +/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Duration +/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Soc +/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Start +/Settings/CGwacs/BatteryLife/SocLimit +/Settings/CGwacs/BatteryLife/State +/Settings/CGwacs/MaxChargePercentage +/Settings/CGwacs/MaxChargePower +/Settings/CGwacs/MaxDischargePercentage +/Settings/CGwacs/MaxDischargePower +/Settings/CGwacs/MaxFeedInPower +/Settings/CGwacs/OvervoltageFeedIn +/Settings/CGwacs/PreventFeedback +/Settings/CGwacs/RunWithoutGridMeter +/Settings/CanBms/SocketcanCan0/ProductId +/Settings/Canbus/can0/Profile +/Settings/Canbus/can1/Profile +/Settings/Devices/vebus_ttyUSB0/CustomName +/Settings/Devices/vebus_ttyUSB1/CustomName +/Settings/DigitalInput/1/AlarmSetting +/Settings/DigitalInput/1/Count +/Settings/DigitalInput/1/CustomName +/Settings/DigitalInput/1/InvertAlarm +/Settings/DigitalInput/1/InvertTranslation +/Settings/DigitalInput/1/Multiplier +/Settings/DigitalInput/1/Type +/Settings/DigitalInput/2/AlarmSetting +/Settings/DigitalInput/2/Count +/Settings/DigitalInput/2/CustomName +/Settings/DigitalInput/2/InvertAlarm +/Settings/DigitalInput/2/InvertTranslation +/Settings/DigitalInput/2/Multiplier +/Settings/DigitalInput/2/Type +/Settings/DigitalInput/3/AlarmSetting +/Settings/DigitalInput/3/Count +/Settings/DigitalInput/3/CustomName +/Settings/DigitalInput/3/InvertAlarm +/Settings/DigitalInput/3/InvertTranslation +/Settings/DigitalInput/3/Multiplier +/Settings/DigitalInput/3/Type +/Settings/DigitalInput/4/AlarmSetting +/Settings/DigitalInput/4/Count +/Settings/DigitalInput/4/CustomName +/Settings/DigitalInput/4/InvertAlarm +/Settings/DigitalInput/4/InvertTranslation +/Settings/DigitalInput/4/Multiplier +/Settings/DigitalInput/4/Type +/Settings/DigitalInput/5/AlarmSetting +/Settings/DigitalInput/5/Count +/Settings/DigitalInput/5/CustomName +/Settings/DigitalInput/5/InvertAlarm +/Settings/DigitalInput/5/InvertTranslation +/Settings/DigitalInput/5/Multiplier +/Settings/DigitalInput/5/Type +/Settings/DigitalInput/6/AlarmSetting +/Settings/DigitalInput/6/Count +/Settings/DigitalInput/6/CustomName +/Settings/DigitalInput/6/InvertAlarm +/Settings/DigitalInput/6/InvertTranslation +/Settings/DigitalInput/6/Multiplier +/Settings/DigitalInput/6/Type +/Settings/FischerPanda0/AcLoad/Enabled +/Settings/FischerPanda0/AcLoad/Measurement +/Settings/FischerPanda0/AcLoad/QuietHoursStartValue +/Settings/FischerPanda0/AcLoad/QuietHoursStopValue +/Settings/FischerPanda0/AcLoad/StartTimer +/Settings/FischerPanda0/AcLoad/StartValue +/Settings/FischerPanda0/AcLoad/StopTimer +/Settings/FischerPanda0/AcLoad/StopValue +/Settings/FischerPanda0/Alarms/NoGeneratorAtAcIn +/Settings/FischerPanda0/AutoStartEnabled +/Settings/FischerPanda0/BatteryCurrent/Enabled +/Settings/FischerPanda0/BatteryCurrent/QuietHoursStartValue +/Settings/FischerPanda0/BatteryCurrent/QuietHoursStopValue +/Settings/FischerPanda0/BatteryCurrent/StartTimer +/Settings/FischerPanda0/BatteryCurrent/StartValue +/Settings/FischerPanda0/BatteryCurrent/StopTimer +/Settings/FischerPanda0/BatteryCurrent/StopValue +/Settings/FischerPanda0/BatteryService +/Settings/FischerPanda0/BatteryVoltage/Enabled +/Settings/FischerPanda0/BatteryVoltage/QuietHoursStartValue +/Settings/FischerPanda0/BatteryVoltage/QuietHoursStopValue +/Settings/FischerPanda0/BatteryVoltage/StartTimer +/Settings/FischerPanda0/BatteryVoltage/StartValue +/Settings/FischerPanda0/BatteryVoltage/StopTimer +/Settings/FischerPanda0/BatteryVoltage/StopValue +/Settings/FischerPanda0/InverterHighTemp/Enabled +/Settings/FischerPanda0/InverterHighTemp/StartTimer +/Settings/FischerPanda0/InverterHighTemp/StopTimer +/Settings/FischerPanda0/InverterOverload/Enabled +/Settings/FischerPanda0/InverterOverload/StartTimer +/Settings/FischerPanda0/InverterOverload/StopTimer +/Settings/FischerPanda0/MinimumRuntime +/Settings/FischerPanda0/OnLossCommunication +/Settings/FischerPanda0/QuietHours/EndTime +/Settings/FischerPanda0/QuietHours/StartTime +/Settings/FischerPanda0/Soc/Enabled +/Settings/FischerPanda0/Soc/QuietHoursStartValue +/Settings/FischerPanda0/Soc/QuietHoursStopValue +/Settings/FischerPanda0/Soc/StartValue +/Settings/FischerPanda0/Soc/StopValue +/Settings/FischerPanda0/StopWhenAc1Available +/Settings/FischerPanda0/TestRun/Duration +/Settings/FischerPanda0/TestRun/Enabled +/Settings/FischerPanda0/TestRun/Interval +/Settings/FischerPanda0/TestRun/RunTillBatteryFull +/Settings/FischerPanda0/TestRun/SkipRuntime +/Settings/FischerPanda0/TestRun/StartDate +/Settings/FischerPanda0/TestRun/StartTime +/Settings/Generator0/AcLoad/Enabled +/Settings/Generator0/AcLoad/Measurement +/Settings/Generator0/AcLoad/QuietHoursStartValue +/Settings/Generator0/AcLoad/QuietHoursStopValue +/Settings/Generator0/AcLoad/StartTimer +/Settings/Generator0/AcLoad/StartValue +/Settings/Generator0/AcLoad/StopTimer +/Settings/Generator0/AcLoad/StopValue +/Settings/Generator0/Alarms/NoGeneratorAtAcIn +/Settings/Generator0/AutoStartEnabled +/Settings/Generator0/BatteryCurrent/Enabled +/Settings/Generator0/BatteryCurrent/QuietHoursStartValue +/Settings/Generator0/BatteryCurrent/QuietHoursStopValue +/Settings/Generator0/BatteryCurrent/StartTimer +/Settings/Generator0/BatteryCurrent/StartValue +/Settings/Generator0/BatteryCurrent/StopTimer +/Settings/Generator0/BatteryCurrent/StopValue +/Settings/Generator0/BatteryService +/Settings/Generator0/BatteryVoltage/Enabled +/Settings/Generator0/BatteryVoltage/QuietHoursStartValue +/Settings/Generator0/BatteryVoltage/QuietHoursStopValue +/Settings/Generator0/BatteryVoltage/StartTimer +/Settings/Generator0/BatteryVoltage/StartValue +/Settings/Generator0/BatteryVoltage/StopTimer +/Settings/Generator0/BatteryVoltage/StopValue +/Settings/Generator0/InverterHighTemp/Enabled +/Settings/Generator0/InverterHighTemp/StartTimer +/Settings/Generator0/InverterHighTemp/StopTimer +/Settings/Generator0/InverterOverload/Enabled +/Settings/Generator0/InverterOverload/StartTimer +/Settings/Generator0/InverterOverload/StopTimer +/Settings/Generator0/MinimumRuntime +/Settings/Generator0/OnLossCommunication +/Settings/Generator0/QuietHours/Enabled +/Settings/Generator0/QuietHours/EndTime +/Settings/Generator0/QuietHours/StartTime +/Settings/Generator0/Soc/Enabled +/Settings/Generator0/Soc/QuietHoursStartValue +/Settings/Generator0/Soc/QuietHoursStopValue +/Settings/Generator0/Soc/StartValue +/Settings/Generator0/Soc/StopValue +/Settings/Generator0/StopWhenAc1Available +/Settings/Generator0/TestRun/Duration +/Settings/Generator0/TestRun/Enabled +/Settings/Generator0/TestRun/Interval +/Settings/Generator0/TestRun/RunTillBatteryFull +/Settings/Generator0/TestRun/SkipRuntime +/Settings/Generator0/TestRun/StartDate +/Settings/Generator0/TestRun/StartTime +/Settings/Gps/Format +/Settings/Gps/SpeedUnit +/Settings/Gui/AutoBrightness +/Settings/Gui/Brightness +/Settings/Gui/DisplayOff +/Settings/Gui/Language +/Settings/Gui/MobileOverview +/Settings/Gui/StartWithMenuView +/Settings/Gui/TanksOverview +/Settings/GuiMods/AcCurrentLimit/Preset1 +/Settings/GuiMods/AcCurrentLimit/Preset2 +/Settings/GuiMods/AcCurrentLimit/Preset3 +/Settings/GuiMods/AcCurrentLimit/Preset4 +/Settings/GuiMods/EnhancedFlowCombineLoads +/Settings/GuiMods/GaugeLimits/AcOutputMaxPower +/Settings/GuiMods/GaugeLimits/AcOutputNonCriticalMaxPower +/Settings/GuiMods/GaugeLimits/BatteryMaxChargeCurrent +/Settings/GuiMods/GaugeLimits/BatteryMaxDischargeCurrent +/Settings/GuiMods/GaugeLimits/CautionPower +/Settings/GuiMods/GaugeLimits/ContiuousPower +/Settings/GuiMods/GaugeLimits/DcSystemMaxCharge +/Settings/GuiMods/GaugeLimits/DcSystemMaxLoad +/Settings/GuiMods/GaugeLimits/MaxChargerPower +/Settings/GuiMods/GaugeLimits/MaxFeedInPower +/Settings/GuiMods/GaugeLimits/PeakPower +/Settings/GuiMods/GaugeLimits/PvChargerMaxPower +/Settings/GuiMods/GaugeLimits/PvOnGridMaxPower +/Settings/GuiMods/GaugeLimits/PvOnOutputMaxPower +/Settings/GuiMods/MoveSettings +/Settings/GuiMods/ShortenTankNames +/Settings/GuiMods/ShowEnhancedFlowLoadsOnInput +/Settings/GuiMods/ShowEnhancedFlowOverviewTanks +/Settings/GuiMods/ShowEnhancedFlowOverviewTemps +/Settings/GuiMods/ShowGauges +/Settings/GuiMods/ShowInactiveFlowTiles +/Settings/GuiMods/ShowRelayOverview +/Settings/GuiMods/ShowTileOverview +/Settings/GuiMods/TemperatureScale +/Settings/GuiMods/TimeFormat +/Settings/GuiMods/UseEnhancedFlowOverview +/Settings/GuiMods/UseEnhancedGridParallelFlowOverview +/Settings/GuiMods/UseEnhancedMobileOverview +/Settings/PackageManager/AutoInstall +/Settings/PackageManager/Count +/Settings/PackageManager/GitHubAutoDownload +/Settings/Pump0/AutoStartEnabled +/Settings/Pump0/Mode +/Settings/Pump0/StartValue +/Settings/Pump0/StopValue +/Settings/Pump0/TankService +/Settings/Relay/0/CustomName +/Settings/Relay/0/InitialState +/Settings/Relay/0/Show +/Settings/Relay/1/CustomName +/Settings/Relay/1/Function +/Settings/Relay/1/InitialState +/Settings/Relay/1/Polarity +/Settings/Relay/1/Show +/Settings/Relay/2/CustomName +/Settings/Relay/2/InitialState +/Settings/Relay/2/Show +/Settings/Relay/3/CustomName +/Settings/Relay/3/InitialState +/Settings/Relay/3/Show +/Settings/Relay/4/CustomName +/Settings/Relay/4/InitialState +/Settings/Relay/4/Show +/Settings/Relay/5/CustomName +/Settings/Relay/5/InitialState +/Settings/Relay/5/Show +/Settings/Relay/6/InitialState +/Settings/Relay/Function +/Settings/Relay/Polarity +/Settings/Services/AccessPoint +/Settings/Services/BleSensors +/Settings/Services/Bluetooth +/Settings/Services/Bol +/Settings/Services/Console +/Settings/Services/FischerPandaAutoStartStop +/Settings/Services/Modbus +/Settings/Services/MqttLocal +/Settings/Services/MqttLocalInsecure +/Settings/Services/MqttN2k +/Settings/Services/MqttVrm +/Settings/Services/NodeRed +/Settings/Services/SignalK +/Settings/ShutdownMonitor/ExternalSwitch +/Settings/System/AccessLevel +/Settings/System/AutoUpdate +/Settings/System/ImageType +/Settings/System/LogLevel +/Settings/System/ReleaseType +/Settings/System/RemoteSupport +/Settings/System/SSHLocal +/Settings/System/TimeZone +/Settings/System/VncInternet +/Settings/System/VncLocal +/Settings/System/VolumeUnit +/Settings/SystemSetup/AcInput1 +/Settings/SystemSetup/AcInput2 +/Settings/SystemSetup/BatteryService +/Settings/SystemSetup/HasAcOutSystem +/Settings/SystemSetup/HasDcSystem +/Settings/SystemSetup/MaxChargeCurrent +/Settings/SystemSetup/MaxChargeVoltage +/Settings/SystemSetup/SharedTemperatureSense +/Settings/SystemSetup/SharedVoltageSense +/Settings/SystemSetup/SystemName +/Settings/SystemSetup/TemperatureService +/Settings/Vrmlogger/HttpsEnabled +/Settings/Vrmlogger/LogInterval +/Settings/Vrmlogger/Logmode +/Settings/Vrmlogger/RamDiskMode +/Settings/Vrmlogger/Url +/Settings/Watchdog/VrmTimeout diff --git a/setup b/setup index 710a11c..f64f1fd 100755 --- a/setup +++ b/setup @@ -13,20 +13,29 @@ source "/data/SetupHelper/CommonResources" # remove settings no longer used -cleanupDbusSettings () +cleanup () { + # cleanup from older versions + restoreActiveFile "$qmlDir/PageSettingsPackageControl.qml" + dbus-send --system --print-reply=literal --dest=com.victronenergy.settings /Settings/GuiMods/GitHubAutoUpdate\ com.victronenergy.BusItem.GetValue &> /dev/null if (( $? == 0 )); then - removeDbusSettings "/Settings/GuiMods/PackageVersions/GeneratorConnector" "/Settings/GuiMods/PackageVersions/GuiMods"\ - "/Settings/GuiMods/PackageVersions/GuiMods" "/Settings/GuiMods/PackageVersions/RpiDisplaySetup"\ - "/Settings/GuiMods/PackageVersions/RpiGpioSetup" "/Settings/GuiMods/PackageVersions/RpiTemp"\ - "/Settings/GuiMods/PackageVersions/SetupHelper" "/Settings/GuiMods/PackageVersions/TankRepeater",\ - "/Settings/GuiMods/PackageVersions/VeCanSetup" "/Settings/GuiMods/PackageVersions/ShutdownMonitor"\ - "/Settings/GuiMods/CheckingPackage" "/Settings/GuiMods/GitHubAutoUpdate" + removeDbusSettings /Settings/GuiMods/PackageVersions/GeneratorConnector /Settings/GuiMods/PackageVersions/GuiMods\ + /Settings/GuiMods/PackageVersions/GuiMods /Settings/GuiMods/PackageVersions/RpiDisplaySetup\ + /Settings/GuiMods/PackageVersions/RpiGpioSetup /Settings/GuiMods/PackageVersions/RpiTemp\ + /Settings/GuiMods/PackageVersions/SetupHelper /Settings/GuiMods/PackageVersions/TankRepeater,\ + /Settings/GuiMods/PackageVersions/VeCanSetup /Settings/GuiMods/PackageVersions/ShutdownMonitor\ + /Settings/GuiMods/CheckingPackage /Settings/GuiMods/GitHubAutoUpdate > /dev/null + removeDbusSettings /Settings/PackageVersion/Count /Settings/PackageVersion/New/GitHubUser \ + /Settings/PackageVersion/New/GitHubBranch /Settings/PackageVersion/New/InstalledVersion \ + /Settings/PackageVersion/New/PackageName /Settings/PackageVersion/New/PackageVersion \ + /Settings/PackageVersion/GitHubAutoUpdate /Settings/PackageVersion/EditAction \ + /Settings/PackageVersion/CheckingPackage > /dev/null fi } + #### running manually and OK to proceed - prompt for input if [ $scriptAction == 'NONE' ] ; then # create dbus Settings if they haven't been set previously @@ -38,165 +47,45 @@ if [ $scriptAction == 'NONE' ] ; then echo echo "This package provides support functions and utilities for Venus modification packages" echo "Packages are automatically reinstalled following a Venus OS update" - echo "Packages may also be automatically updated from GitHub" - echo " or a USB stick" - echo "Previously uninstalled packages can also be installed and configured" - echo " as an option from the menu either from GitHub or from a USB stick" - echo - echo "If internet access is not available, you can manually update/install from a USB stick or SD card" + echo "All actions are controlled via /Settings/PackageMonitor in the GUI" + echo " Packages may be automatically updated from GitHub or a USB stick" + echo " Previously uninstalled packages can also be downloaded an installed" echo - if [ -f "$setupOptionsDir/optionsSet" ]; then - enableReinstall=true - else - enableReinstall=false - fi - - response='' - fullPrompt=true - while true; do - if $fullPrompt ; then - echo - echo "Available actions:" - echo " Install and activate (i)" - if $enableReinstall ; then - echo " Reinstall (r) based on options provided at last install" - fi - echo " Uninstall (u) and restores all files to stock" - echo " Quit (q) without further action" - echo " Display setup log (s) outputs the last 100 lines of the log" - echo - echo " Enable/disable automatic GitHub package updates (g)" - echo " Manually install packages from GitHub or USB stick (p)" - echo - fullPrompt=false - fi - /bin/echo -n "Choose an action from the list above: " - read response - case $response in - [iI]*) - scriptAction='INSTALL' - break;; - [rR]*) - if $enableReinstall ; then - scriptAction='INSTALL' - break - fi - ;; - [uU]*) - scriptAction='UNINSTALL' - break - ;; - [qQ]*) - exit - ;; - [sS]*) - displayLog $setupLogFile - ;; - [gG]*) - autoUpdate=$(dbus-send --system --print-reply=literal --dest=com.victronenergy.settings\ - /Settings/PackageVersion/GitHubAutoUpdate\ - com.victronenergy.BusItem.GetValue 2> /dev/null | awk '{print $3}') - case $autoUpdate in - 1) - echo "Automatic GitHub updates are at normal rate" - ;; - 2) - echo "Automatic GitHub updates are at fast rate" - ;; - 3) - echo "Automatic GitHub updates will occur only once" - ;; - *) - echo "Automatic GitHub updates are currently disabled" - ;; - esac - echo "Available modes:" - echo " Normal (n) - one package is checked once every 10 minutes" - echo " Fast (f) - one package is checked once every 10 seconds" - echo " Once (o) - each package is checked once then auto updates are turned off" - echo " Disable (d) - auto updates are disabled" - - read -p "Choose a mode from the above list: (cr for no change): " response - if [ ! -z $response ]; then - case $response in - [nN]* | [eE]*) - setSetting 1 /Settings/PackageVersion/GitHubAutoUpdate - ;; - [fF]*) - setSetting 2 /Settings/PackageVersion/GitHubAutoUpdate - ;; - [oO]*) - setSetting 3 /Settings/PackageVersion/GitHubAutoUpdate - ;; - [dD]*) - setSetting 0 /Settings/PackageVersion/GitHubAutoUpdate - ;; - *) - esac - fi - ;; - [pP]*) - "$scriptDir/packageInstaller" - fullPrompt=true - ;; - *) - esac - done + standardActionPrompt fi if [ $scriptAction == 'INSTALL' ] ; then - if [ ! -f "$packageListFile" ]; then - logMessage "installing default packge list" - cp "$scriptDir/defaultPackageList" "$packageListFile" - fi - - # check both parameters to insure they are both created - dbus-send --system --print-reply=literal --dest=com.victronenergy.settings /Settings/PackageVersion/CheckingPackage\ - com.victronenergy.BusItem.GetValue &> /dev/null - if (( $? != 0 )); then - settingsInstalled=false - # first setting exists, check the second - else - dbus-send --system --print-reply=literal --dest=com.victronenergy.settings /Settings/PackageVersion/GitHubAutoUpdate\ - com.victronenergy.BusItem.GetValue &> /dev/null - if (( $? != 0 )); then - settingsInstalled=false - else - settingsInstalled=true - fi - fi - if ! $settingsInstalled ; then - logMessage "creating SetupHelper Settings" - dbus -y com.victronenergy.settings /Settings AddSettings\ - '%[ {"path": "/PackageVersion/GitHubAutoUpdate", "default":0},\ - {"path": "/PackageVersion/CheckingPackage", "default":""},]' > /dev/null - # relocate options and current values - moveSetting "$setupOptionsDir/autoGitHubUpdate" "/Settings/GuiMods/GitHubAutoUpdate" "/Settings/PackageVersion/GitHubAutoUpdate" - rm -f "$setupOptionsDir/autoGitHubUpdate" - moveSetting "" "/Settings/GuiMods/CheckingPackage" "/Settings/PackageVersion/CheckingPackage" - fi - updateActiveFile "$qmlDir/PageSettings.qml" - updateActiveFile "$qmlDir/PageSettingsPackageControl.qml" + updateActiveFile "$qmlDir/PageSettingsPackageManager.qml" updateActiveFile "$qmlDir/PageSettingsPackageVersions.qml" + updateActiveFile "$qmlDir/PageSettingsPackageEdit.qml" updateActiveFile "$qmlDir/MbDisplayPackageVersion.qml" + updateActiveFile "$qmlDir/PageSettingsAddPackageList.qml" + updateActiveFile "$qmlDir/PageSettingsPackageAdd.qml" + updateActiveFile "$qmlDir/MbDisplayDefaultPackage.qml" + updateActiveFile "$qmlDir/PageSettingsPmBackup.qml" - installService $packageName + installService PackageManager - cleanupDbusSettings + cleanup fi if [ $scriptAction == 'UNINSTALL' ] ; then restoreActiveFile "$qmlDir/PageSettings.qml" - restoreActiveFile "$qmlDir/PageSettingsPackageControl.qml" + restoreActiveFile "$qmlDir/PageSettingsPackageManager.qml" restoreActiveFile "$qmlDir/PageSettingsPackageVersions.qml" + restoreActiveFile "$qmlDir/PageSettingsPackageEdit.qml" restoreActiveFile "$qmlDir/MbDisplayPackageVersion.qml" + restoreActiveFile "$qmlDir/PageSettingsAddPackageList.qml" + restoreActiveFile "$qmlDir/PageSettingsPackageAdd.qml" + restoreActiveFile "$qmlDir/MbDisplayDefaultPackage.qml" + restoreActiveFile "$qmlDir/PageSettingsPmBackup.qml" - removeService $packageName + removeService PackageManager - cleanupDbusSettings + cleanup fi if $filesUpdated ; then diff --git a/updatePackageVersions b/updatePackageVersions deleted file mode 100755 index 0e6d2a3..0000000 --- a/updatePackageVersions +++ /dev/null @@ -1,119 +0,0 @@ -#!/bin/bash - -# This script keeps creates a list of installed package versions -# and updates dBus /Settings/PackageVersion/... -# -# this script should be called whenever a package is installed or uninstalled -# this is part of endScript () if used -# it can take time, so launch it as a background task: -# -# nohup /data/updatePackageVersions > /dev/null & -# -# dbus access is slow, so this script builds a file with the known values -# only changes are sent to dbus - -source "/data/SetupHelper/EssentialResources" -source "/data/SetupHelper/LogHandler" -source "/data/SetupHelper/DbusSettingsResources" - -versionsFile="/data/packageVersions" - -# this flag is tested by LogHandler to determine if messages should be output to the console -logToConsole=false - -# wait until dbus settings are active -while [ $(dbus -y | grep -c "com.victronenergy.settings") == 0 ]; do - logMessage "waiting for dBus settings" - sleep 1 -done - -newNames=() -newVersions=() -oldNames=() -oldVersions=() -# read in old values -if [ -f "$versionsFile" ]; then - index=0 - while read -u 8 oldPackage oldVersion; do - # skip comments - if [[ ${oldPackage:0:1} == "#" ]] ; then - continue - # skip blank/incomplete lines - elif [ -z $oldPackage ] ; then - continue - fi - - oldNames[$index]=$oldPackage - oldVersions[$index]=$oldVersion - ((index++)) - done 8< "$versionsFile" -fi -oldCount=${#oldNames[@]} - -# loop through packages from package list -index=0 -echo "# AUTO GENERATED DO NOT MODIFY" > "$versionsFile" -while read -u 9 package gitHubUser gitHubBranch; do - - # skip comments - if [[ ${package:0:1} == "#" ]] ; then - continue - # skip blank/incomplete lines - elif [ -z $package ] ; then - continue - fi - # skip uninstalled packages - if [ ! -f "$installedFlagPrefix"$package ]; then - continue - fi - - newNames[$index]=$package - if [ -f "/data/$package/version" ]; then - packageVersion=$(cat "/data/$package/version") - else - packageVersion="?" - fi - newVersions[$index]=$packageVersion - - # save in file for later comparison - echo $package $packageVersion >> "$versionsFile" - - ((index++)) - -done 9< "$packageListFile" -newCount=${#newNames[@]} - -if (( $oldCount > $newCount )); then - minCount=$newCount - maxCount=$oldCount -else - minCount=$oldCount - maxCount=$newCount -fi - -# compare new and old and update dbus Settings as needed -index=0 -while ((index < newCount )); do - if (( index > oldCount )); then - updateDbusStringSetting "/Settings/PackageVersion/$index/PackageName" ${newNames[$index]} - updateDbusStringSetting "/Settings/PackageVersion/$index/PackageVersion" ${newVersions[$index]} - else - if [ "${newNames[$index]}" != "${oldNames[$index]}" ]; then - updateDbusStringSetting "/Settings/PackageVersion/$index/PackageName" ${newNames[$index]} - fi - if [ "${newVersions[$index]}" != "${oldVersions[$index]}" ]; then - updateDbusStringSetting "/Settings/PackageVersion/$index/PackageVersion" ${newVersions[$index]} - fi - fi - ((index++)) -done - -index=$newCount -while (( $index < $oldCount )); do - removeDbusSettings "/Settings/PackageVersion/$index/PackageName"\ - "/Settings/PackageVersion/$index/PackageVersion" - ((index++)) -done - -updateDbusIntSetting "/Settings/PackageVersion/Count" $newCount - diff --git a/venus-data.tgz b/venus-data.tgz index c753353..0c980f9 100644 Binary files a/venus-data.tgz and b/venus-data.tgz differ diff --git a/version b/version index a54471b..1961bcc 100644 --- a/version +++ b/version @@ -1 +1 @@ -v3.15 +v4.0