Added HTTP basic auth, default octoprint config
Added HTTP basic auth to MJPG stream, so now you don't have to worry about creeps watching your printer Nginx now runs as www-data Frontend specific files are no longer put into /etc/skel, they are now only installed to the kiosk account OctoPrint now has a default config so that users don't have to configure system stuff (ex. You don't have to input /webcam/ into the stream box anymore) Restart Browser button now kills old Surf sessions The Raspberry Pi now starts with the Raspberry Pi cameras enabled on first boot OctoPrint can now shutdown, reboot and restart its own service using sudo without a password Kiosk account no longer has a password since you aren't supposed to login as kiosk anyways
This commit is contained in:
parent
09eac865c0
commit
c7f44ec412
18 changed files with 110 additions and 25 deletions
3
TODO.md
3
TODO.md
|
@ -4,9 +4,6 @@
|
||||||
- Add network configuration (replace nmtui)
|
- Add network configuration (replace nmtui)
|
||||||
- Add hostname configuration (replace nmtui)
|
- Add hostname configuration (replace nmtui)
|
||||||
|
|
||||||
## Nginx
|
|
||||||
- Add HTTP basic auth (especially to MJPG)
|
|
||||||
|
|
||||||
## Misc
|
## Misc
|
||||||
- More security
|
- More security
|
||||||
- Wiki
|
- Wiki
|
||||||
|
|
|
@ -66,3 +66,4 @@ max_framebuffers=2
|
||||||
[all]
|
[all]
|
||||||
#dtoverlay=vc4-fkms-v3d
|
#dtoverlay=vc4-fkms-v3d
|
||||||
arm_64bit=1
|
arm_64bit=1
|
||||||
|
start_x=1
|
||||||
|
|
|
@ -3,13 +3,8 @@
|
||||||
install -d "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d"
|
install -d "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d"
|
||||||
install -m 644 files/noclear.conf "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d/noclear.conf"
|
install -m 644 files/noclear.conf "${ROOTFS_DIR}/etc/systemd/system/getty@tty1.service.d/noclear.conf"
|
||||||
install -v -m 644 files/fstab "${ROOTFS_DIR}/etc/fstab"
|
install -v -m 644 files/fstab "${ROOTFS_DIR}/etc/fstab"
|
||||||
install -m 755 files/.xprofile "${ROOTFS_DIR}/etc/skel/.xprofile"
|
|
||||||
install -m 755 files/.browser.sh "${ROOTFS_DIR}/etc/skel/.browser.sh"
|
|
||||||
install -m 644 files/.dialogrc "${ROOTFS_DIR}/etc/skel/.dialogrc"
|
install -m 644 files/.dialogrc "${ROOTFS_DIR}/etc/skel/.dialogrc"
|
||||||
install -m 644 files/.dialogrc "${ROOTFS_DIR}/root/.dialogrc"
|
install -m 644 files/.dialogrc "${ROOTFS_DIR}/root/.dialogrc"
|
||||||
mkdir -p "${ROOTFS_DIR}/etc/skel/.config/openbox"
|
|
||||||
install -m 644 files/autostart "${ROOTFS_DIR}/etc/skel/.config/openbox/autostart"
|
|
||||||
install -m 644 files/menu.xml "${ROOTFS_DIR}/etc/skel/.config/openbox/menu.xml"
|
|
||||||
|
|
||||||
on_chroot << EOF
|
on_chroot << EOF
|
||||||
if ! id -u ${FIRST_USER_NAME} >/dev/null 2>&1; then
|
if ! id -u ${FIRST_USER_NAME} >/dev/null 2>&1; then
|
||||||
|
@ -22,6 +17,5 @@ if ! id -u kiosk >/dev/null 2>&1; then
|
||||||
adduser --disabled-password --shell /usr/sbin/nologin --gecos "" kiosk
|
adduser --disabled-password --shell /usr/sbin/nologin --gecos "" kiosk
|
||||||
fi
|
fi
|
||||||
echo "${FIRST_USER_NAME}:${FIRST_USER_PASS}" | chpasswd
|
echo "${FIRST_USER_NAME}:${FIRST_USER_PASS}" | chpasswd
|
||||||
echo "kiosk:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
|
||||||
echo "root:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
echo "root:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
||||||
EOF
|
EOF
|
||||||
|
|
|
@ -5,4 +5,4 @@ python-virtualenv
|
||||||
git
|
git
|
||||||
libyaml-dev
|
libyaml-dev
|
||||||
nginx
|
nginx
|
||||||
ffmpeg
|
ffmpeg
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
#!/bin/bash -e
|
#!/bin/bash -e
|
||||||
|
|
||||||
install -m 644 files/octoprint.service ${ROOTFS_DIR}/etc/systemd/system/octoprint.service
|
install -m 644 files/octoprint.service ${ROOTFS_DIR}/etc/systemd/system/octoprint.service
|
||||||
|
mkdir -p ${ROOTFS_DIR}/home/octoprint/.octoprint/
|
||||||
|
install -m 600 files/config.yaml ${ROOTFS_DIR}/home/octoprint/.octoprint/config.yaml
|
||||||
|
|
||||||
install -m 644 files/nginx.conf ${ROOTFS_DIR}/etc/nginx/nginx.conf
|
install -m 644 files/nginx.conf ${ROOTFS_DIR}/etc/nginx/nginx.conf
|
||||||
echo -e "listen 443;" > ${ROOTFS_DIR}/etc/nginx/listen.conf
|
echo -e "listen 443;" > ${ROOTFS_DIR}/etc/nginx/listen.conf
|
||||||
|
|
||||||
|
mkdir -p ${ROOTFS_DIR}/usr/local/bin
|
||||||
|
install -m 755 files/restart-octoprint ${ROOTFS_DIR}/usr/local/bin/restart-octoprint
|
||||||
|
|
||||||
|
# Yeah I could've used polkit, but this works fine so whatever
|
||||||
|
mkdir -p ${ROOTFS_DIR}/etc/sudoers.d
|
||||||
|
echo "octoprint ALL=NOPASSWD: /sbin/shutdown" > ${ROOTFS_DIR}/etc/sudoers.d/octoprint-shutdown
|
||||||
|
echo "octoprint ALL=NOPASSWD: /usr/local/bin/restart-octoprint" > ${ROOTFS_DIR}/etc/sudoers.d/octoprint-restart
|
||||||
|
|
||||||
on_chroot << EOF
|
on_chroot << EOF
|
||||||
# Package enables this when installed, won't start until first-time is run due to missing SSL certs
|
# Package enables this when installed, won't start until first-time is run due to missing SSL certs
|
||||||
systemctl disable nginx
|
systemctl disable nginx
|
||||||
|
|
||||||
# If OctoPrint already exists, skip this (for debugging)
|
# If OctoPrint already exists, skip this (for debugging)
|
||||||
if [[ ! -f /srv/octoprint/bin/octoprint ]]; then
|
if [[ ! -f /srv/octoprint/bin/octoprint ]]; then
|
||||||
cd /srv/ || exit 1
|
cd /srv/ || exit 1
|
||||||
|
@ -17,7 +28,6 @@ if [[ ! -f /srv/octoprint/bin/octoprint ]]; then
|
||||||
pip install octoprint || exit 1
|
pip install octoprint || exit 1
|
||||||
# Fix permissions
|
# Fix permissions
|
||||||
chown -R octoprint:octoprint /srv/octoprint
|
chown -R octoprint:octoprint /srv/octoprint
|
||||||
|
chown -R octoprint:octoprint /home/octoprint
|
||||||
fi
|
fi
|
||||||
# Enable the reverse proxy
|
|
||||||
systemctl enable nginx
|
|
||||||
EOF
|
EOF
|
||||||
|
|
10
stage2/04-octoprint/files/config.yaml
Normal file
10
stage2/04-octoprint/files/config.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
webcam:
|
||||||
|
stream: /webcam/
|
||||||
|
ffmpeg: /usr/bin/ffmpeg
|
||||||
|
discovery:
|
||||||
|
publicPort: 443
|
||||||
|
server:
|
||||||
|
commands:
|
||||||
|
systemShutdownCommand: sudo shutdown now
|
||||||
|
systemRestartCommand: sudo shutdown -r now
|
||||||
|
serverRestartCommand: sudo restart-octoprint
|
|
@ -1,4 +1,5 @@
|
||||||
worker_processes auto;
|
worker_processes auto;
|
||||||
|
user www-data;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
location /webcam/ {
|
location /webcam/ {
|
||||||
|
include /etc/nginx/auth.conf;
|
||||||
proxy_pass http://mjpg-streamer/;
|
proxy_pass http://mjpg-streamer/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ Wants=nginx.service
|
||||||
Type=simple
|
Type=simple
|
||||||
User=octoprint
|
User=octoprint
|
||||||
Group=octoprint
|
Group=octoprint
|
||||||
ExecStart=/srv/octoprint/bin/octoprint --host localhost --port 5000
|
ExecStart=/srv/octoprint/bin/octoprint serve --host localhost --port 5000
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
8
stage2/04-octoprint/files/restart-octoprint
Normal file
8
stage2/04-octoprint/files/restart-octoprint
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "This script needs to be run as root."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl restart octoprint
|
|
@ -1,6 +1,2 @@
|
||||||
cmake
|
|
||||||
libjpeg-dev
|
libjpeg-dev
|
||||||
v4l-utils
|
|
||||||
libv4l-dev
|
libv4l-dev
|
||||||
gcc
|
|
||||||
g++
|
|
||||||
|
|
|
@ -69,7 +69,6 @@ dialog --title "NOTICE" --nocancel --colors --msgbox "This collection of softwar
|
||||||
# Force the user to change the pi user's password before the RPi gets botnetted
|
# Force the user to change the pi user's password before the RPi gets botnetted
|
||||||
change_password
|
change_password
|
||||||
# Randomize the root and frontend password
|
# Randomize the root and frontend password
|
||||||
echo "kiosk:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
|
||||||
echo "root:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
echo "root:$(cat /dev/urandom | tr -dc _A-Z-a-z-0-9 | head -c40)" | chpasswd
|
||||||
|
|
||||||
dialog --title "Network Configuration" --nocancel --msgbox "Setup will now open nmtui, a program to help configure your ethernet/wireless interfaces. Hit Quit when you are done." 10 50
|
dialog --title "Network Configuration" --nocancel --msgbox "Setup will now open nmtui, a program to help configure your ethernet/wireless interfaces. Hit Quit when you are done." 10 50
|
||||||
|
@ -92,11 +91,12 @@ openssl req -x509 -nodes -days 36500 -newkey rsa:4096 -subj "/C=/ST=/L=/O=/OU=/C
|
||||||
|
|
||||||
# If OctoPrint/MJPG Streamer is running locally, ask if the user wants to change the default listening port/IP (optional)
|
# If OctoPrint/MJPG Streamer is running locally, ask if the user wants to change the default listening port/IP (optional)
|
||||||
if ( [[ -f /etc/systemd/system/multi-user.target.wants/octoprint.service ]] || [[ -f /etc/systemd/system/multi-user.target.wants/mjpg-streamer.service ]] ) && dialog --title "Nginx Config" --defaultno --yesno "Do you wish to change the default Nginx listening address and/or port?" 10 60; then
|
if ( [[ -f /etc/systemd/system/multi-user.target.wants/octoprint.service ]] || [[ -f /etc/systemd/system/multi-user.target.wants/mjpg-streamer.service ]] ) && dialog --title "Nginx Config" --defaultno --yesno "Do you wish to change the default Nginx listening address and/or port?" 10 60; then
|
||||||
nginx_config
|
nginx_listen
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If MJPG service is enabled, ask user which video device to use
|
# If MJPG service is enabled, ask the user to configure Nginx basic auth and the video device
|
||||||
if [[ -f /etc/systemd/system/multi-user.target.wants/mjpg-streamer.service ]]; then
|
if [[ -f /etc/systemd/system/multi-user.target.wants/mjpg-streamer.service ]]; then
|
||||||
|
nginx_auth
|
||||||
video_select
|
video_select
|
||||||
video_config
|
video_config
|
||||||
fi
|
fi
|
||||||
|
@ -111,4 +111,7 @@ fi
|
||||||
# Delete the autologin override and first-time setup utility
|
# Delete the autologin override and first-time setup utility
|
||||||
rm /etc/systemd/system/getty@tty1.service.d/override.conf
|
rm /etc/systemd/system/getty@tty1.service.d/override.conf
|
||||||
rm /etc/profile.d/first-time.sh
|
rm /etc/profile.d/first-time.sh
|
||||||
|
|
||||||
|
dialog --title "TouchPrint Config" --infobox "Rebooting..." 0 0
|
||||||
|
sleep 1
|
||||||
reboot
|
reboot
|
||||||
|
|
|
@ -22,7 +22,10 @@ main_menu () {
|
||||||
"2") change_password; main_menu; return 0;;
|
"2") change_password; main_menu; return 0;;
|
||||||
"3") services_menu; main_menu; return 0;;
|
"3") services_menu; main_menu; return 0;;
|
||||||
"4") screen_timeout; main_menu; return 0;;
|
"4") screen_timeout; main_menu; return 0;;
|
||||||
"5") dialog --title "Touchscreen Calibration" --infobox "Waiting for touchscreen calibrator to close..." 3 50; DISPLAY=:0 xinput_calibrator --no-timeout --output-filename /etc/X11/xorg.conf.d/99-calibration.conf; main_menu; return 0;;
|
"5") dialog --title "Touchscreen Calibration" --infobox "Waiting for touchscreen calibrator to close..." 3 50;
|
||||||
|
DISPLAY=:0 xinput_calibrator --no-timeout --output-filename /etc/X11/xorg.conf.d/99-calibration.conf;
|
||||||
|
main_menu;
|
||||||
|
return 0;;
|
||||||
"6") return 0;;
|
"6") return 0;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
@ -38,7 +41,7 @@ services_menu () {
|
||||||
case $SERVICESMENU in
|
case $SERVICESMENU in
|
||||||
"1") service_toggle; services_menu; return 0;;
|
"1") service_toggle; services_menu; return 0;;
|
||||||
"2") service_health; services_menu; return 0;;
|
"2") service_health; services_menu; return 0;;
|
||||||
"3") nginx_config; services_menu; return 0;;
|
"3") nginx_menu; services_menu; return 0;;
|
||||||
"4") video_menu; services_menu; return 0;;
|
"4") video_menu; services_menu; return 0;;
|
||||||
"5") return 0;;
|
"5") return 0;;
|
||||||
esac
|
esac
|
||||||
|
@ -67,6 +70,19 @@ service_health () {
|
||||||
\ZnSSH: \Zb$(if pgrep sshd >/dev/null; then echo '\Z2Online'; elif [[ ! -f /etc/systemd/system/multi-user.target.wants/sshd.service ]]; then echo "Disabled"; else echo '\Z1Offline'; fi)" 0 0
|
\ZnSSH: \Zb$(if pgrep sshd >/dev/null; then echo '\Z2Online'; elif [[ ! -f /etc/systemd/system/multi-user.target.wants/sshd.service ]]; then echo "Disabled"; else echo '\Z1Offline'; fi)" 0 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nginx_menu () {
|
||||||
|
local NGINXMENU=$(dialog --title "Nginx Config" --menu "" 10 50 0 \
|
||||||
|
"1" "Listening Settings" \
|
||||||
|
"2" "MJPG Authentication" \
|
||||||
|
"3" "Go Back" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
case $NGINXMENU in
|
||||||
|
"1") nginx_listen; nginx_menu; return 0;;
|
||||||
|
"2") nginx_auth; nginx_menu; return 0;;
|
||||||
|
"3") return 0;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
video_menu () {
|
video_menu () {
|
||||||
local VIDEOMENU=$(dialog --title "MJPG Config" --menu "" 10 50 0 \
|
local VIDEOMENU=$(dialog --title "MJPG Config" --menu "" 10 50 0 \
|
||||||
"1" "Camera Selection" \
|
"1" "Camera Selection" \
|
||||||
|
|
|
@ -57,6 +57,12 @@ service_toggle () {
|
||||||
raspi-config nonint do_camera 1 # Disables the camera
|
raspi-config nonint do_camera 1 # Disables the camera
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ $ENABLE_OCTO == true ]] && [[ $ENABLE_MJPG == true ]]; then
|
||||||
|
systemctl enable nginx
|
||||||
|
elif [[ $ENABLE_OCTO == false ]] && [[ $ENABLE_MJPG == false ]]; then
|
||||||
|
systemctl disable nginx
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $ENABLE_GUI == true ]]; then
|
if [[ $ENABLE_GUI == true ]]; then
|
||||||
systemctl set-default graphical.target
|
systemctl set-default graphical.target
|
||||||
else
|
else
|
||||||
|
@ -101,7 +107,7 @@ screen_timeout () {
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
nginx_config () {
|
nginx_listen () {
|
||||||
local LISTEN=""
|
local LISTEN=""
|
||||||
|
|
||||||
# Grab the variable from the nginx conf if it exists, otherwise use default
|
# Grab the variable from the nginx conf if it exists, otherwise use default
|
||||||
|
@ -118,7 +124,38 @@ nginx_config () {
|
||||||
# Write new value to nginx
|
# Write new value to nginx
|
||||||
echo "listen $LISTEN;" > /etc/nginx/listen.conf
|
echo "listen $LISTEN;" > /etc/nginx/listen.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nginx_auth () {
|
||||||
|
local NGINXAUTH_MENU=$(dialog --colors --nocancel --insecure --title "Nginx Config" --mixedform "Input desired username and password for the MJPG stream.\n\nLeave both fields blank if you do not want authentication \Zb\Z1(NOT RECOMMENDED)\Zn." 12 60 0\
|
||||||
|
"Username: " 1 1 "" 1 11 10 0 0 \
|
||||||
|
"Password: " 2 1 "" 2 11 30 0 1 3>&1 1>&2 2>&3 || return 0)
|
||||||
|
NGINXAUTH_MENU=($NGINXAUTH_MENU)
|
||||||
|
|
||||||
|
# If all the fields are blank, remove the auth stuff and exit
|
||||||
|
if [[ "${NGINXAUTH_MENU[*]}" == "" ]]; then
|
||||||
|
echo "" > /etc/nginx/auth.conf
|
||||||
|
rm /etc/nginx/.htpasswd
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If only one of them is blank, make the user start over
|
||||||
|
if [[ ${NGINXAUTH_MENU[0]} == "" ]] || [[ ${NGINXAUTH_MENU[1]} == "" ]]; then
|
||||||
|
dialog --title "Error" --msgbox "Invalid input!" 10 50
|
||||||
|
nginx_auth
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Write the auth config and password file to Nginx
|
||||||
|
echo -e "satisfy any;\nallow 127.0.0.1;\ndeny all;\nauth_basic \"TouchPrint MJPG Stream\";\nauth_basic_user_file /etc/nginx/.htpasswd;" > /etc/nginx/auth.conf
|
||||||
|
#htpasswd -b -B -c /etc/nginx/.htpasswd ${NGINXAUTH_MENU[0]} ${NGINXAUTH_MENU[1]}
|
||||||
|
echo "${NGINXAUTH_MENU[0]}:$(openssl passwd -apr1 ${NGINXAUTH_MENU[1]})" > /etc/nginx/.htpasswd
|
||||||
|
unset NGINXAUTH_MENU
|
||||||
|
|
||||||
|
# Set perms so that no one steals our precious password hashes
|
||||||
|
chown root:www-data /etc/nginx/.htpasswd
|
||||||
|
chmod 640 /etc/nginx/.htpasswd
|
||||||
|
}
|
||||||
|
|
||||||
video_select () {
|
video_select () {
|
||||||
# In the unlikely event that there are no video devices, don't continue
|
# In the unlikely event that there are no video devices, don't continue
|
||||||
if ! ls /dev/video* 2>&1 >/dev/null; then
|
if ! ls /dev/video* 2>&1 >/dev/null; then
|
||||||
|
@ -165,7 +202,7 @@ video_config () {
|
||||||
"Framerate: " 2 1 "$FRAMERATE" 2 12 3 0 3>&1 1>&2 2>&3)
|
"Framerate: " 2 1 "$FRAMERATE" 2 12 3 0 3>&1 1>&2 2>&3)
|
||||||
VIDEOCONFIG_MENU=($VIDEOCONFIG_MENU)
|
VIDEOCONFIG_MENU=($VIDEOCONFIG_MENU)
|
||||||
|
|
||||||
if [[ "$VIDEOCONFIG_MENU[0]" == "" ]] || [[ "$VIDEOCONFIG_MENU[1]" == "" ]]; then
|
if [[ "${VIDEOCONFIG_MENU[0]}" == "" ]] || [[ "${VIDEOCONFIG_MENU[1]}" == "" ]]; then
|
||||||
dialog --title "Error" --msgbox "Invalid input!" 10 50
|
dialog --title "Error" --msgbox "Invalid input!" 10 50
|
||||||
video_config
|
video_config
|
||||||
return 0
|
return 0
|
||||||
|
|
10
stage3/02-kiosk/00-run.sh
Executable file
10
stage3/02-kiosk/00-run.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
mkdir -p "${ROOTFS_DIR}/home/kiosk"
|
||||||
|
install -m 755 files/.xprofile "${ROOTFS_DIR}/home/kiosk/.xprofile"
|
||||||
|
install -m 755 files/.browser.sh "${ROOTFS_DIR}/home/kiosk/.browser.sh"
|
||||||
|
|
||||||
|
mkdir -p "${ROOTFS_DIR}/home/kiosk/.config/openbox"
|
||||||
|
install -m 644 files/autostart "${ROOTFS_DIR}/home/kiosk/.config/openbox/autostart"
|
||||||
|
install -m 644 files/menu.xml "${ROOTFS_DIR}/home/kiosk/.config/openbox/menu.xml"
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
<menu id="root-menu" label="Openbox 3">
|
<menu id="root-menu" label="Openbox 3">
|
||||||
<item label="Restart Browser">
|
<item label="Restart Browser">
|
||||||
|
<action name="Execute"><execute>killall surf</execute></action>
|
||||||
<action name="Execute"><execute>bash ~/.browser.sh</execute></action>
|
<action name="Execute"><execute>bash ~/.browser.sh</execute></action>
|
||||||
</item>
|
</item>
|
||||||
<separator />
|
<separator />
|
Loading…
Reference in a new issue