-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKioskUpdate.py
More file actions
205 lines (170 loc) · 9.03 KB
/
Copy pathKioskUpdate.py
File metadata and controls
205 lines (170 loc) · 9.03 KB
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
#!/usr/bin/env python3
#**********************************************************************************************************************************
# BSD 3-Clause License for KioskForge - https://kioskforge.org:
#
# Copyright © 2024-2026 The KioskForge Team.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#**********************************************************************************************************************************
# This script is responsible for updating, upgrading, and cleaning the system (only if there is an active internet connection).
# Import Python v3.x's type hints as these are used extensively in order to allow MyPy to perform static checks on the code.
from typing import List
import glob
import os
import sys
import time
from kiosklib.actions import AptAction
from kiosklib.driver import KioskDriver
from kiosklib.errors import CommandError, KioskError
from kiosklib.invoke import invoke_text, invoke_text_safe
from kiosklib.kiosk import Kiosk
from kiosklib.logger import Logger
from kiosklib.network import internet_active
from kiosklib.signal import Signal
class KioskUpdate(KioskDriver):
"""This class implements the KioskUpdate.py script, which updates the system if and only if it is on the internet."""
def snap_cleanup(self, logger : Logger) -> None:
"""Removes all revisions of snaps to keep disk usage to the bare minimum."""
result = invoke_text("snap list --all --color=never --unicode=never")
if result.status != 0:
raise KioskError("Unable to get list of all snaps in system")
# Split the output of 'snap list --all' into lines
lines = result.output.strip().split(os.linesep)
del result
# Parse each line into six fields and remove all revisions of disabled snaps.
# NOTE: Logic borrowed from https://www.debugpoint.com/clean-up-snap/
for line in lines[1:]:
(name, version, revision, tracking, publisher, notes) = line.split()
del version
del tracking
del publisher
# The 'notes' field is a comma-separated list of flags, parse them.
flags = notes.split(",")
del notes
# We're only interested in disabled snaps - the rest are used by the system.
if "disabled" not in flags:
logger.write(f"Skipping active snap {name} revision {revision}.")
continue
# Remove the old revision of the current snap.
logger.write(f"Removing snap {name} revision {revision}.")
invoke_text_safe(f'snap remove "{name}" --revision="{revision}"')
# Empty the snapd cache as this may grow to MANY gigabytes over time.
for file in glob.glob("/var/lib/snapd/cache/*"):
if os.path.isfile(file):
logger.write(f"Removing snapd cache item {os.path.basename(file)}.")
os.unlink(file)
def _main(self, logger : Logger, origin : str, arguments : List[str]) -> None:
# Check that we're running on Linux.
if sys.platform != "linux":
raise KioskError("This script can only be run on a Linux kiosk machine")
# Check that we've got root privileges.
# pylint: disable-next=no-member
if os.geteuid() != 0: # pyrefly: ignore[missing-attribute]
raise KioskError("You must be root (use 'sudo') to run this script")
# Parse command-line arguments.
initial = False
if len(arguments) > 1:
raise CommandError('"KioskUpdate.py" [--initial]')
if len(arguments) == 1:
# 'KioskSetup.py' invokes this script with the '--initial' option to disable the signal synchronization code.
if arguments[0] != "--initial":
raise KioskError("Invalid argument: " + arguments[0])
initial = True
logger.write("Kiosk updater starting.")
# Load settings generated by KioskForge on the desktop machine.
kiosk = Kiosk(self.version)
kiosk.load_safe(logger, origin + os.sep + "KioskForge.kiosk")
# Not all kiosks are online so we need to handle the case that there's no internet gracefully.
if internet_active():
# Don't execute the code below if this script was invoked from the 'KioskSetup.py' script (to increase code sharing).
if not initial:
# Stop the desktop environment, and any child processes, using a signal, which is watched for by 'KioskDesktop.py'.
signal = Signal("KioskDesktop-shutdown", "kiosk")
signal.create()
logger.write("Signaled KioskDesktop.py to shut down and exit.")
# Wait for KioskDesktop.py to shut down, which means waiting until the signal has been removed.
while signal.exists:
time.sleep(1)
logger.write("KioskDesktop.py has shut down and exited.")
del signal
# NOTE: We don't start Chromium using 'snap run chromium', so don't use 'snap stop chromium'.
# invoke_text_safe("snap stop chromium")
# Stop X11 using "killall", the only way we have (we cannot kill the Python interpreter running this script...).
invoke_text_safe("killall Xorg")
# Ask snap to upgrade (refresh) all snaps.
logger.write("Upgrading all snaps.")
invoke_text_safe("snap refresh")
# Try to uninstall cups in case it got installed again by a refresh of the Chromium snap.
# NOTE: We simply ignore the return value, an instance of 'Result', as we're happy whether it fails or it succeeds.
logger.write("Purging Common Unix Printing System (CUPS) installed when Chromium is installed or upgraded.")
invoke_text("snap remove --purge cups")
# Remove all disabled snaps (prior snap versions) and empty the snap cache.
logger.write("Removing outdated snaps and clearing the snap cache.")
self.snap_cleanup(logger)
# Keep track of failures.
failed = False
# Purge all unused packages.
# NOTE: We purge unused packages PRIOR to updating to ensure we've rebooted before doing this so as to not accidentally
# NOTE: purge a running kernel, which may have catastrophic consequences as far as I know.
# NOTE: Using 'AptAction' to get automatic waiting for the 'apt' lock file to be released.
logger.write("Purging all unused packages.")
result = AptAction("Purging all unused packages.", "apt-get autoremove --purge").execute()
if result.status != 0:
logger.error("Unable to purge all unused packages.")
failed = True
del result
# Update all package lists.
logger.write("Updating package lists.")
result = AptAction("Updating package lists.", "apt-get update").execute()
if result.status != 0:
logger.error("Unable to update package lists.")
failed = True
del result
# Upgrade all packages.
# NOTE: Use "apt upgrade -y", not "apt-get dist-upgrade -y", to ensure that the system doesn't suddenly break down.
# NOTE: Use "apt upgrade -y", not "apt-get upgrade -y", because "apt-get" doesn't install new packages (incl. kernels).
logger.write("Upgrading all packages.")
result = AptAction("Upgrading all packages.", "apt upgrade -y").execute()
if result.status != 0:
logger.error("Unable to upgrade all packages.")
failed = True
del result
# Clean the apt cache (which may grow to many gigabytes in size).
logger.write("Cleaning the package cache.")
result = AptAction("Cleaning the package cache.", "apt-get clean").execute()
if result.status != 0:
logger.error("Unable to clean package cache.")
failed = True
del result
if not failed:
logger.write("Successfully purged, updated, upgraded, and cleaned all packages and all snaps.")
else:
logger.write("Unable to purge, update, upgrade, and clean system.")
logger.write("Kiosk updater stopping.")
# Execute the requested post-upgrade action (only if not invoked from 'KioskSetup.py').
if not initial:
match kiosk.upgrade_post.data:
case "reboot":
invoke_text_safe("reboot")
case "poweroff":
invoke_text_safe("poweroff")
case _:
raise KioskError(f"Invalid value in 'upgrade_post' option: {kiosk.upgrade_post.data}")
if __name__ == "__main__":
sys.exit(KioskUpdate().main(sys.argv))