forked from kwindrem/SetupHelper
-
Notifications
You must be signed in to change notification settings - Fork 0
/
PackageManager.py
executable file
·4250 lines (3757 loc) · 149 KB
/
PackageManager.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/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 - updates enabled - one download update every 10 seconds, then one download every 10 minutes
# 3 - one update pass at the fast rate, then to no updates
# changing to one of the fast scans, starts from the first package
AUTO_DOWNLOADS_OFF = 0
NORMAL_DOWNLOAD = 1
HOURLY_DOWNLOAD = 2
DAILY_DOWNLOAD = 3
ONE_DOWNLOAD = 99
# /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 <packageName>/version from the package directory
# /Package/n/InstalledVersion from /etc/venus/isInstalled-<packageName>
# /Package/n/Incompatible indicates the reason the package not compatible with the system
# "" if compatible
# any other text if not compatible
# /Package/n/FileSetOK indicates if the file set for the current version is usable
# based on the INCOMPLETE flag in the file set
# the GUI uses this to enable/disable the Install button
# /Package/n/PackageConflicts (\n separated) list of reasons for a package conflict (\n separated)
#
# 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
# 'INITIALIZE' - install PackageManager's persistent storage (dbus Settings)
# so that the storage will be rebuilt when PackageManager restarts
# PackageManager will exit when this command is received
# 'RESTART_PM' - restart PackageManager
# 'gitHubScan' - trigger GitHub version update
# sent when entering the package edit menu or when changing packages within that menu
# also used to trigger a Git Hub version refresh of all packages when entering the Active packages menu
#
# the GUI must wait for PackageManager to signal completion of one operation before initiating another
#
# set by PackageManager 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"
#
# 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
# /BackupSettingsLocalFileExist True if PackageManager detected a settings backup file in /data
# /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 media
# 2 set by the GUI to trigger a restore operation media
# 3 set by PackageManager to indicate a backup to media is in progress
# 4 set by PackageManager to indicate a restore from media is in progress
# 21 set by the GUI to trigger a backup operation to /data
# 22 set by the GUI to trigger a restore operation from /data
# 23 set by PackageManager to indicate a backup is in progress to /data
# 24 set by PackageManager to indicate a restore from /data is in progress
#
# setup script return codes
EXIT_SUCCESS = 0
EXIT_REBOOT = 123
EXIT_RESTART_GUI = 124
EXIT_INCOMPATIBLE_VERSION = 254
EXIT_INCOMPATIBLE_PLATFORM = 253
EXIT_FILE_SET_ERROR = 252
EXIT_OPTIONS_NOT_SET = 251
EXIT_RUN_AGAIN = 250
EXIT_ROOT_FULL = 249
EXIT_DATA_FULL = 248
EXIT_NO_GUI_V1 = 247
EXIT_PACKAGE_CONFLICT = 246
EXIT_PATCH_ERROR = 245
EXIT_ERROR = 255 # generic error
#
#
# /GuiEditStatus a text message to report edit status to the GUI
#
# /PmStatus as above for main Package Manager status
#
# /MediaUpdateStatus as above for SD/USB media transfers
#
# /Platform a translated version of the platform (aka machine)
# machine Platform
# ccgx CCGX
# einstein Cerbo GX
# cerbosgx Cerbo SGX
# bealglebone Venus GX
# canvu500 CanVu 500
# nanopi Multi/Easy Solar GX
# raspberrypi2 Raspberry Pi 2/3
# raspberrypi4 Raspberry Pi 4
# ekrano Ekrano GX
#
# /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/<packageName> 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 version" file:
# /etc/venus/installedVersion-<packageName>
# this file does not exist if the package is not installed
# /etc/venus is chosen to store the installed version because it does NOT survive a firmware update
# this will trigger an automatic reinstall following a firmware update
#
# InstalledVersion is displayed to the user and used for tests for automatic updates
#
# GitHubVersion is read from the internet if a connection exists.
# Once the GitHub versions have been refreshed for all packages,
# the rate of refresh is reduced so that all packages are refreshed every 10 minutes.
# So if there are 10 packages, one refresh occurs every 60 seconds
# 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
# The GUI's Package editor menu will refresh the GitHub version of the current package
# when navagating to a new package. This insures the displayed version isn't out of date
# GitHub version information is erased 10 minutes after it was last refreshed.
# Entering the Package versions menu wil trigger a fast refresh, again to insure the info is up to date.
#
#
# PackageManager downloads packages from GitHub based on the GitHub version and package (stored) versions:
# if the GitHub branch is a specific version, automatic download occurs if the versions differ
# otherwise the GitHub version must be newer.
# the archive file is unpacked to a directory in /data named
# <packageName>-<gitHubBranch>.tar.gz, then moved to /data/<packageName>, replacing the original
#
# PackageManager automatically installs the stored verion if the package (stored) and installed versions differ
#
# Automatic downloads and installs can be enabled separately.
# Downloads checks can occur all the time, run once or be disabled
#
# Package reinstalls following a firmware update are handled as follows:
# During system boot, reinstallMods reinstalls SetupHelper if needed
# it then sets /etc/venus/REINSTALL_PACKAGES
# PackageManager tests this flag and if set, will reinstall all packages
# even if automatic installs are disabled.
#
# 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
#
# Removed packages won't be checked for automatic install or download
# and do not appear in the active package list in the GUI
#
# 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
#
# 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 !!!!
# information extracted from the package list must be used within LOCK / UNLOCK to insure
# that data is not changed by another thread.
#
# PackageManager manages flag files in the package's setup options folder:
# DO_NOT_AUTO_INSTALL indicates the package was manually removed and PackageManager should not
# automatically install it
# DO_NOT_AUTO_ADD indicates the package was manually removed and PackageManager should not
# automaticlly add it
# FORCE_REMOVE instructs PackageManager to remove the package from active packages list
# Used rarely, only case is GuiMods setup forcing GeneratorConnector to be removed
# this is done only at boot time.
#
# these flags are stored in /data/setupOptions/<packageName> which is non-volatile
# and survives a package download and firmware updates
#
# 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:
# <packageName>-<gitHubBranch or version>.tar.gz
# <packageName>-install.tar.gz
# The <packageName> 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/<packageName>
# 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 the gitHubInfo file containg GitHub user and branch
# gitHubInfo should have a single line of the form: gitHubUser:gitHubBranch, e.g, kwindrem:latest
# gitHubUser and gitHubBranch are set from the file's content when it is added to the package list
# if the package is already in the package list, gitHubInfo is ignored
# if no GitHub information is contained in the package,
# an attempt is made to extract it from the defaultPackages list
# failing that, the user must add it manually via the GUI
# without the GitHub info, no automatic or manual downloads are possible
#
# alternate user / branch info can be entered for a package
# user info probalby should not change
# branch info can be changed however to access specific tags/releases
# or other branches (e.g., a beta test branch)
#
# PackageManager has a mechnism for backing up and restoring settings:
# SOME dbus Settings
# custom icons
# backing up gui, SetupHelper and PackageManager logs
#
# PackageManager checks for several flag files on removable media:
# SETTINGS_AUTO_RESTORE
# Triggers automatically restore dbus Settings and custom icons
# A previous settings backup operation must have been performed
# This creates a settingsBackup fiile and icons folder on the removable media
# that is used by settings restore (manual or triggered by this flag
#
# AUTO_INSTALL_PACKAGES
# If present on the media, any packages found on the media will be automatically installed
#
# AUTO_UNINSTALL_PACKAGES
# As above but uninstalls INCLUDING SetupHelper !!!!
# Only applies if present on media (not in /data or a package direcory)
#
# AUTO_INSTALL
# If present in a package directory, the package is installed
# even if the automatic installs are disabled in the PackageManager menu
# DO_NOT_AUTO_INSTALL overrides this flag
#
# ONE_TIME_INSTALL
# If present in a package directory, the package is automatically installed
# even if automatic installs are diabled and the DO_NOT_AUTO_INSTALL flag is set
# This flag file is removed when the install is performed
# to prevent repeated installs
# Packages may be deployed with this flag set to insure it is installed
# when a new version is transferred from removable media or downloaded from GitHub
#
# AUTO_EJECT
# If present, all removable media is ejected after related "automatic" work is finished
#
# INITIALIZE_PACKAGE_MANAGER
# If present, the PackageManager's persistent storage (dbus Settings parameters) are initialized
# and PackageManager restarted
# On restart, PackageManager will rebuild the dbus Settings from packages found in /data
# Only custom Git Hub user and branch information is lost.
#
# A menu item with the same function as INITIALIZE_PACKAGE_MANAGER is also provided
#
# classes/instances:
# AddRemoveClass
# AddRemove runs as a separate thread
#
# DbusIfClass
# DbusIf
#
# PackageClass
# PackageList [] one per package
#
# UpdateGitHubVersionClass
# UpdateGitHubVersion runs as a separate thread
#
# DownloadGitHubPackagesClass
# DownloadGitHub runs as a separate thread
#
# InstallPackagesClass
# InstallPackages runs as a separate thread
#
# MediaScanClass
# MediaScan runs as a separate thread
#
# global methods:
# PushAction ()
# VersionToNumber ()
# LocatePackagePath ()
# AutoRebootCheck ()
import platform
import argparse
import logging
# constants for logging levels:
CRITICAL = 50
ERROR = 40
WARNING = 30
INFO = 20
DEBUG = 10
import sys
import signal
import subprocess
import threading
import os
import shutil
import dbus
import time
import re
import glob
# accommodate both Python 2 (prior to v2.80) and 3
# note subprocess.run and subprocess.DEVNULL do not exist in python 2
# so subprocess.Popen and subprocess.PIPE and subprocess.communicate ()
# are used in all subprodess calls even if process output is not needed
# or if it is not necessary to wait for the command to finish
PythonVersion = sys.version_info
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
# use an established Victron service to maintain compatiblity
sys.path.insert(1, os.path.join('/opt/victronenergy/dbus-systemcalc-py', '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 # initialized/used in main, set in mainloop
global GuiRestart # initialized in main, set in PushAction and InstallPackage, used in mainloop
global WaitForGitHubVersions # initialized in main, set in UpdateGitHubVersion used in mainLoop
global InitializePackageManager # initialized/used in main, set in PushAction, MediaScan run, used in mainloop
# PushAction
#
# some actions are pushed to one of three queues:
# InstallPackages.InstallQueue for install, uninstall, check and resolveConflicts actions
# Download.Download for download actions
# AddRemoveQueue for add and remove actions
# GitHubVersion for gitHubScan (GitHub version refresh requiests)
#
# other actions are handled in line since they just set a global flag
# (not pused on any queue)
# which is then handled elsewere
#
# 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
#
# returns True if command was accepted, False if not
def PushAction (command=None, source=None):
parts = command.split (":")
theQueue = None
if len (parts) >= 1:
action = parts[0]
else:
action = ""
if len (parts) >= 2:
packageName = parts[1]
else:
packageName = ""
if action == 'download':
DbusIf.LOCK ("PushAction 1")
package = PackageClass.LocatePackage (packageName)
if package != None:
package.DownloadPending = True
theQueue = DownloadGitHub.DownloadQueue
queueText = "Download"
# clear the install failure because package contents are changing
# this allows an auto install again
# but will be disableed if that install fails
if source == 'GUI':
DbusIf.UpdateStatus ( message=action + " pending " + packageName, where='Editor' )
else:
theQueue = None
queueText = ""
errorMessage = "PushAction Download: " + packageName + " not in package list"
logging.error (errorMessage)
if source == 'GUI':
DbusIf.UpdateStatus ( message=errorMessage, where='Editor' )
DbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )
DbusIf.UNLOCK ("PushAction 1")
elif action == 'install' or action == 'uninstall' or action == 'check':
DbusIf.LOCK ("PushAction 2")
package = PackageClass.LocatePackage (packageName)
# SetupHelper uninstall is processed later as PackageManager exists
if packageName == "SetupHelper" and action == 'uninstall':
global SetupHelperUninstall
SetupHelperUninstall = True
elif package != None:
package.InstallPending = True
theQueue = InstallPackages.InstallQueue
queueText = "Install"
if source == 'GUI':
DbusIf.UpdateStatus ( message=action + " pending " + packageName, where='Editor' )
else:
theQueue = None
queueText = ""
errorMessage = "PushAction Install: " + packageName + " not in package list"
logging.error (errorMessage)
if source == 'GUI':
DbusIf.UpdateStatus ( message=errorMessage, where='Editor' )
DbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )
DbusIf.UNLOCK ("PushAction 2")
elif action == 'resolveConflicts':
theQueue = InstallPackages.InstallQueue
queueText = "Install"
if source == 'GUI':
# note this message will be overwritten by the install and uninstall actions
# triggered by this action
DbusIf.UpdateStatus ( "resolving conflicts for " + packageName, where='Editor' )
elif action == 'add' or action == 'remove':
theQueue = AddRemove.AddRemoveQueue
queueText = "AddRemove"
if source == 'GUI':
DbusIf.UpdateStatus ( message=action + " pending " + packageName, where='Editor' )
elif action == 'gitHubScan':
theQueue = UpdateGitHubVersion.GitHubVersionQueue
queueText = "GitHubVersion"
# the remaining actions are handled here (not pushed on a queue)
elif action == 'reboot':
global SystemReboot
SystemReboot = True
logging.warning ( "received Reboot request from " + source)
if source == 'GUI':
DbusIf.UpdateStatus ( message=action + " pending " + packageName, where='Editor' )
# set the flag - reboot is done in main_loop
return True
elif action == 'restartGui':
# set the flag - reboot is done in main_loop
global GuiRestart
GuiRestart = True
logging.warning ( "received GUI restart request from " + source)
if source == 'GUI':
DbusIf.UpdateStatus ( "GUI restart pending " + packageName, where='Editor' )
return True
elif action == 'INITIALIZE_PM':
# set the flag - Initialize will quit the main loop, then work is done in main
global InitializePackageManager
InitializePackageManager = True
logging.warning ( "received PackageManager INITIALIZE request from " + source)
if source == 'GUI':
DbusIf.UpdateStatus ( "PackageManager INITIALIZE pending " + packageName, where='Editor' )
return True
elif action == 'RESTART_PM':
# set the flag - Initialize will quit the main loop, then work is done in main
global RestartPackageManager
RestartPackageManager = True
logging.warning ( "received PackageManager RESTART request from " + source)
if source == 'GUI':
DbusIf.UpdateStatus ( "PackageManager restart pending " + packageName, where='Editor' )
return True
else:
if source == 'GUI':
DbusIf.UpdateStatus ( message="unrecognized command '" + command + "'", where='Editor' )
DbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )
logging.error ("PushAction received unrecognized command from " + source + ": " + command)
return False
if theQueue != None:
try:
theQueue.put ( (command, source), block=False )
return True
except queue.Full:
logging.error ("command " + command + " from " + source + " lost - " + queueText + " - queue full")
return False
except:
logging.error ("command " + command + " from " + source + " lost - " + queueText + " - other queue error")
return False
else:
return False
# end PushAction
# convert a version string to an integer to make comparisions easier
# the Victron format for version numbers is: vX.Y~Z-large-W
# the ~Z portion indicates a pre-release version so a version without it is "newer" than a version with it
# the -W portion has been abandoned but was like the ~Z for large builds and is IGNORED !!!!
# large builds now have the same version number as the "normal" build
#
# the version string passed to this function allows for quite a bit of flexibility
# any alpha characters are permitted prior to the first digit
# up to 3 version parts PLUS a prerelease part are permitted
# each with up to 4 digits each -- MORE THAN 4 digits is indeterminate
# that is: v0.0.0d0 up to v9999.9999.9999b9999 and then v9999.9999.9999 as the highest priority
# any non-numeric character can be used to separate main versions
# special significance is assigned to single caracter separators between the numeric strings
# b or ~ indicates a beta release
# a indicates an alpha release
# d indicates an development release
# these offset the pre-release number so that b/~ has higher numeric value than any a
# and a has higher value than d separator
#
# a blank version or one without at least one number part is considered invalid
# alpha and beta seperators require at least two number parts
# if only one number part is found the prerelease seperator is IGNORED
#
# returns the version number or 0 if string does not parse into needed sections
def VersionToNumber (version):
version = version.replace ("large","L")
numberParts = re.split ('\D+', version)
otherParts = re.split ('\d+', version)
# discard blank elements
# this can happen if the version string starts with alpha characters (like "v")
# of if there are no numeric digits in the version string
try:
while numberParts [0] == "":
numberParts.pop(0)
except:
pass
numberPartsLength = len (numberParts)
if numberPartsLength == 0:
return 0
versionNumber = 0
releaseType='release'
if numberPartsLength >= 2:
if 'b' in otherParts or '~' in otherParts:
releaseType = 'beta'
versionNumber += 60000
elif 'a' in otherParts:
releaseType = 'alpha'
versionNumber += 30000
elif 'd' in otherParts:
releaseType = 'develop'
# if release all parts contribute to the main version number
# and offset is greater than all prerelease versions
if releaseType == 'release':
versionNumber += 90000
# if pre-release, last part will be the pre release part
# and others part will be part the main version number
else:
numberPartsLength -= 1
versionNumber += int (numberParts [numberPartsLength])
# include core version number
versionNumber += int (numberParts [0]) * 10000000000000
if numberPartsLength >= 2:
versionNumber += int (numberParts [1]) * 1000000000
if numberPartsLength >= 3:
versionNumber += int (numberParts [2]) * 100000
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
# 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 ):
return newPath
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, pulls from AddRemoveQueue)
# StopThread ()
#
# 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 = 50)
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 ()
#
# run () also serves as and idle loop to add packages found in /data (AddStoredPacakges)
# this is only called every 3 seconds
# and may push add commands onto the AddRemoveQueue
#
# StopThread () is called to shut down the thread
def StopThread (self):
self.threadRunning = False
self.AddRemoveQueue.put ( ('STOP', ''), block=False )
# AddRemove run ()
#
# process package Add/Remove actions
def run (self):
global RestartPackageManager
changes = False
while self.threadRunning:
# if package was added or removed, don't wait for queue empty
# so package lists can be updated immediately
if changes:
delay = 0.0
else:
delay = 3.0
try:
command = self.AddRemoveQueue.get (timeout = delay)
except queue.Empty:
# adds/removes since last queue empty
if changes:
DbusIf.UpdateDefaultPackages ()
# no changes so do idle processing:
# add packages in /data that aren't included in package list
else:
# restart package manager if a duplice name found in PackageList
# or if name is not valid
DbusIf.LOCK ("AddRemove run")
existingPackages = []
duplicateFound = False
for (index, package) in enumerate (PackageClass.PackageList):
packageName = package.PackageName
if packageName in existingPackages or not PackageClass.PackageNameValid (packageName):
duplicateFound = True
break
existingPackages.append (packageName)
del existingPackages
DbusIf.UNLOCK ("AddRemove run")
# exit this thread so no more package adds/removes are possible
# PackageManager will eventually reset
if duplicateFound:
logging.critical ("duplicate " + packageName + " found in package list - restarting PackageManager")
RestartPackageManager = True
return
PackageClass.AddStoredPackages ()
changes = False
continue
except:
logging.error ("pull from AddRemoveQueue failed")
continue
if len (command) == 0:
logging.error ("pull from AddRemove queue failed - empty comand")
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 or no package name - discarding", command)
continue
source = command[1]
else:
logging.error ("AddRemoveQueue - no command and/or source - discarding", command)
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]
if PackageClass.AddPackage (packageName = packageName, source=source,
gitHubUser=user, gitHubBranch=branch ):
changes = True
elif action == 'remove':
if PackageClass.RemovePackage ( packageName=packageName ):
changes = True
else:
logging.warning ( "received invalid action " + command + " from " + source + " - discarding" )
# end while True
# end run ()
# end AddRemoveClass
# DbusIfClass
# Instances:
# DbusIf
# Methods:
# RemoveDbusSettings (class method)
# UpdateStatus
# various Gets and Sets for dbus parameters
# handleGuiEditAction (dbus change handler)
# LocateRawDefaultPackage
# UpdateDefaultPackages ()
# ReadDefaultPackagelist ()
# LOCK ()
# UNLOCK ()
# RemoveDbusService ()
#
# 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
#
# the dbus settings managed here do NOT have a package association
# however, the per-package parameters from PackageClass are ADDED to
# DbusSettings and dBusService created here !!!!
#
# DbusIf manages a lock to prevent data access in one thread
# while it is being 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
# FALURE TO RELEASE THE LOCK WILL HANG OTHER THREADS !!!!!
#
# default package info is fetched from a file and published to our dbus service
# for use by the GUI in adding new packages
# the default info is also stored in defaultPackageList []
# LocateRawDefaultPackage is used to retrieve the default from local storage
# rather than pulling from dbus or reading the file again to save time
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 ],
bufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
_, stderr = proc.communicate ()
stderr = stderr.decode ().strip ()
returnCode = proc.returncode
except:
logging.error ("dbus RemoveSettings call failed")
else:
if returnCode != 0:
logging.error ("dbus RemoveSettings failed " + str (returnCode))
logging.error ("stderr: " + stderr)
# UpdateStatus
#
# updates the status when the operation completes
# the GUI provides three different areas to show status
# where specifies which of these are updated
# 'PmStatus'
# '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 == 'PmStatus':
DbusIf.SetPmStatus ( message )
elif where == 'Media':
DbusIf.SetMediaStatus (message)
def UpdatePackageCount (self):
count = len(PackageClass.PackageList)
self.DbusSettings['packageCount'] = count
def GetPackageCount (self):
return self.DbusSettings['packageCount']
def SetAutoDownloadMode (self, value):
self.DbusSettings['autoDownload'] = value
def GetAutoDownloadMode (self):
return self.DbusSettings['autoDownload']
def GetAutoInstall (self):
return self.DbusSettings['autoInstall'] == 1
def SetAutoInstall (self, value):
if value == True:
dbusValue = 1
else:
dbusValue = 0
self.DbusSettings['autoInstall'] = dbusValue
def SetPmStatus (self, value):
self.DbusService['/PmStatus'] = 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 SetBackupSettingsLocalFileExist (self, value):
if value == True:
dbusValue = 1
else:
dbusValue = 0