-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathKioskStart.py
More file actions
212 lines (177 loc) · 9.1 KB
/
Copy pathKioskStart.py
File metadata and controls
212 lines (177 loc) · 9.1 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
206
207
208
209
210
211
212
#!/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 used to start the kiosk by configuring sound (if applicable), loading X11 or the user's app, etc.
# 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 os
import sys
import time
from kiosklib.builder import TextBuilder
from kiosklib.detect import pi_board_get, pulseaudio_soundcards_get
from kiosklib.driver import KioskDriver
from kiosklib.errors import CommandError, KioskError
from kiosklib.invoke import invoke_list_safe, invoke_text, invoke_text_safe, Result
from kiosklib.kiosk import Kiosk
from kiosklib.logger import Logger
from kiosklib.signal import Signal
from kiosklib.various import screen_clear, touchscreens_get
class KioskStart(KioskDriver):
"""Defines the KioskStart class, which is responsible for starting up the kiosk on every boot after it has been forged."""
def __init__(self) -> None:
KioskDriver.__init__(self)
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 don't have root privileges.
# pylint: disable-next=no-member
if os.geteuid() == 0: # pyrefly: ignore[missing-attribute]
raise KioskError("You may not be root when running this script")
# Parse command-line arguments.
if len(arguments) != 0: # pylint: disable=duplicate-code
raise CommandError('"KioskStart.py"')
# NOTE: We are running in a NON-interactive shell, just in case you need to know (I verified this programatically).
# Load settings generated by KioskForge on the desktop machine.
kiosk = Kiosk(self.version)
kiosk.load_safe(logger, origin + os.sep + "KioskForge.kiosk")
# Disallow redundant launches of this script (I can't get 'systemd(efunct)' to NOT launch it multiple times).
signal = Signal("kiosk-running", "kiosk")
if signal.exists:
return
logger.write("Starting kiosk.")
try:
# Create the signal that prevents this script from being launched multiple times by systemd.
signal.create()
# Configure audio subsystem, if applicable.
if kiosk.sound_card.data != "none":
# NOTE: I spent half a night trying out different solutions and none of them worked when done in 'KioskConfig.py',
# NOTE: so I finally resigned to accepting that 'wpctl' decides how I structure my scripts and put the code here.
# NOTE: The attempts involved using 'sudo' and lowering privileges to that of the user, none of them worked.
#if sys.platform == "linux":
# # pwd_item = pwd.getpwnam("kiosk")
# # NOTE: os.setegid() MUST be called before os.seteuid(), otherwise it raises a PermissionError exception.
# #os.setegid(pwd_item.pw_gid)
# os.setegid(29) # audio group
# os.seteuid(pwd_item.pw_uid)
# del pwd_item
# Grab PulseAudio control (pactl) output, keep repeating until we succeed.
# NOTE: Welcome to systemd... The PulseAudio server is NOT up when the condition 'After=pipewire.target' is true.
# NOTE: So we use 1970ish style code, where a little delay is inserted in the code, just to make the turd pretty.
time.sleep(5)
# Loop over calling 'wpctl status' until we get meaningful results.
# NOTE: If the 'time.sleep(5)' above is not present, we simply get an empty list of sinks. Hooray for systemd!
# NOTE: The next line is redundant but MyPy doesn't detect that 'result' is always assigned.
result = Result()
while True:
result = invoke_text("pactl list sinks")
if result.status == 0 and result.output.strip() != "":
break
logger.write("Waiting three seconds for 'pactl' to be ready for commands.")
time.sleep(3)
# Grab the list of active sinks (typically only one) from the 'wpctl status' output.
sound_cards = pulseaudio_soundcards_get(result.output)
del result
# Make sure we have at least one sink to set the volume of.
if len(sound_cards) == 0:
raise KioskError("Unable to detect any PulseAudio sinks")
# If the sound card is set to 'auto', choose 'usb' then 'jack' for Pi 4B and 'usb' then 'hdmi1' for Pi 5.
if kiosk.sound_card.data == "auto":
plug = ""
if sound_cards.get("usb", ""):
# Check if there is an USB sound card. If so, prefer it.
plug = "usb"
if not plug:
# If no USB sound card, use 'jack' on Pi4B and 'hdmi1' on Pi5.
match pi_board_get():
case "Pi 4B":
plug = "jack"
case "Pi 5":
plug = "hdmi1"
case _:
raise KioskError("Unable to detect model of Raspberry Pi")
kiosk.assign("sound_card", plug)
del plug
found = False
wanted_card = kiosk.sound_card.data
for sound_card in sound_cards:
if sound_card != wanted_card:
continue
wanted_id = sound_cards.get(wanted_card, 0)
if not wanted_id:
continue
found = True
# Select the default sink.
invoke_text_safe(f"pactl set-default-sink {wanted_id}")
# Set the audio level of the selected sink to the user-specified percentage on a logarithmic scale.
invoke_text_safe(f"wpctl set-volume {wanted_id} {kiosk.sound_level.data / 100.0:.2f}")
del wanted_id
del sound_cards
del wanted_card
if not found:
logger.error("Unable to locate sound card: " + kiosk.sound_card.data)
raise KioskError(f"Configuration of sound card '{kiosk.sound_card.data}' failed")
del found
# Go back to being root.
#if sys.platform == "linux":
# os.seteuid(0)
# os.setegid(0)
# Auto-configure the 'mouse' option depending on the presence of a touchscreen (present: disable, otherwise: enable).
if kiosk.mouse.data == "auto":
touchscreens = touchscreens_get()
if touchscreens:
# Disable mouse.
kiosk.assign("mouse", "false")
else:
# Enable mouse.
kiosk.assign("mouse", "true")
del touchscreens
if kiosk.type.data in [ "x11", "web" ]:
# Move ~/.xsession-errors to ~/.xsession-errors.old to avoid having it grow indefinitely forever.
if os.path.isfile("/home/kiosk/.xsession-errors"):
os.replace("/home/kiosk/.xsession-errors", "/home/kiosk/.xsession-errors.old")
# Only execute the request if $DISPLAY is undefined and $XDG_VTNR is equal to 1 (avoid starting X11 twice).
if not os.environ.get("DISPLAY") and os.environ.get("XDG_VTNR") == "1":
# Launch X11, which runs '.config/openbox/autostart', which launches Chromium in kiosk mode or the user app.
words = TextBuilder()
words += "startx"
if kiosk.mouse.data == "false":
words += "--"
words += "-nocursor"
invoke_list_safe(words.list)
elif kiosk.type.data == "web-wayland":
# TODO: Wayland: Support the 'mouse' option (https://gist.github.com/seffs/2395ca640d6d8d8228a19a9995418211).
if not os.environ.get("WAYLAND_DISPLAY"):
invoke_text_safe("/usr/bin/dbus-update-activation-environment --systemd WAYLAND_DISPLAY=wayland-0")
invoke_text_safe("/snap/bin/ubuntu-frame")
invoke_text_safe(f"/snap/bin/chromium --kiosk '{kiosk.command.data}'")
elif kiosk.type.data == "cli":
invoke_text_safe(kiosk.command.data)
else:
raise KioskError(f"Unknown kiosk type: {kiosk.type.data}")
screen_clear()
finally:
signal.remove()
if __name__ == "__main__":
sys.exit(KioskStart().main(sys.argv))