From f548bafcfb29b2f3f3cbdd412d2487950108f6ba Mon Sep 17 00:00:00 2001 From: Burak Yavuz Date: Tue, 23 Sep 2025 18:34:41 +0200 Subject: [PATCH 01/44] Translated using Weblate (Turkish) Currently translated at 100.0% (1879 of 1879 strings) --- plinth/locale/tr/LC_MESSAGES/django.po | 38 +++++++++----------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/plinth/locale/tr/LC_MESSAGES/django.po b/plinth/locale/tr/LC_MESSAGES/django.po index 12d91f24c..014df9156 100644 --- a/plinth/locale/tr/LC_MESSAGES/django.po +++ b/plinth/locale/tr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-10 04:01+0000\n" +"PO-Revision-Date: 2025-09-24 03:01+0000\n" "Last-Translator: Burak Yavuz \n" "Language-Team: Turkish \n" @@ -1642,6 +1642,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Bu uygulama ayrıca {box_name} hizmetleri için günlükleri gösterir." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -10492,13 +10494,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the logs to the bug report." msgstr "" "Bu bir iç hatadır ve sizin neden olduğunuz veya düzeltebileceğiniz bir şey " -"değildir. Lütfen düzeltebilmemiz için hata izleyicide hatayı bildirin. " -"Ayrıca, lütfen hata raporuna durum günlüğünü ekleyin." +"değildir. Lütfen düzeltebilmemiz için hata " +"izleyicide hatayı bildirin. Ayrıca, lütfen hata raporuna günlükleri ekleyin." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Kurulum" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Bunlar, bu web arayüzü için durum günlüğünün son %(num_lines)s satırıdır. " -"Eğer bir hata bildirmek istiyorsanız, lütfen hata izleyiciyi " -"kullanın ve bu durum günlüğünü hata raporuna ekleyin." +"Bunlar, bu uygulamada yer alan hizmetler için günlüklerin son satırlarıdır. " +"Eğer bir hata bildirmek istiyorsanız, lütfen hata " +"izleyiciyi kullanın ve bu günlüğü hata raporuna ekleyin." #: plinth/templates/app-logs.html:26 msgid "" @@ -10792,10 +10782,8 @@ msgid "Clear all tags" msgstr "Tüm etiketleri temizle" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Günlükler" +msgstr "Günlükleri görüntüle" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" From 4fe7a72cc13e4e89d4a2890be75be5496ff08b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A7=E7=8E=8B=E5=8F=AB=E6=88=91=E6=9D=A5=E5=B7=A1?= =?UTF-8?q?=E5=B1=B1?= Date: Tue, 23 Sep 2025 04:04:09 +0200 Subject: [PATCH 02/44] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 61.4% (1155 of 1879 strings) --- plinth/locale/zh_Hans/LC_MESSAGES/django.po | 28 ++++++--------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/plinth/locale/zh_Hans/LC_MESSAGES/django.po b/plinth/locale/zh_Hans/LC_MESSAGES/django.po index 3a42b5058..ff3814618 100644 --- a/plinth/locale/zh_Hans/LC_MESSAGES/django.po +++ b/plinth/locale/zh_Hans/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: Plinth\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-10 04:01+0000\n" +"PO-Revision-Date: 2025-09-24 03:02+0000\n" "Last-Translator: 大王叫我来巡山 " "\n" "Language-Team: Chinese (Simplified Han script) bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"这是一个内部错误,不是你造成的或可以修复。 请报告到 bug 追踪器 上这样我" -"们就可以修复该错误。同时请附加状态日志到 " +"这是一个内部错误,不是你造成的或可以修复的。 请报告到 bug 追踪器 上,让我们可以修复该错误。同时请附加 日志到 " "Bug 报告里。" #: plinth/templates/app-header.html:26 @@ -9470,21 +9464,15 @@ msgid "Installation" msgstr "安装" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"这些是此 Web 界面状态日志的最后 %(num_lines)s 行。如果想报 Bug,请通过 bug 追踪器 " -"并附上此状态日志。" +"这些是本应用涉及到的服务的日志的最后几行。如果想报 Bug,请通过 bug 追踪器 " +"并附上此日志。" #: plinth/templates/app-logs.html:26 msgid "" From c69e870420f442b657fb15b2b52626ce6aaaf2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9C=D0=B0=D0=BA=D1=81=D0=B8=D0=BC=20=D0=93=D0=BE=D1=80?= =?UTF-8?q?=D0=BF=D0=B8=D0=BD=D1=96=D1=87?= Date: Tue, 23 Sep 2025 08:45:38 +0200 Subject: [PATCH 03/44] Translated using Weblate (Ukrainian) Currently translated at 100.0% (1879 of 1879 strings) --- plinth/locale/uk/LC_MESSAGES/django.po | 41 +++++++++----------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/plinth/locale/uk/LC_MESSAGES/django.po b/plinth/locale/uk/LC_MESSAGES/django.po index 9ec68ec56..f757cc802 100644 --- a/plinth/locale/uk/LC_MESSAGES/django.po +++ b/plinth/locale/uk/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-10 04:01+0000\n" +"PO-Revision-Date: 2025-09-24 03:02+0000\n" "Last-Translator: Максим Горпиніч \n" "Language-Team: Ukrainian \n" @@ -1651,6 +1651,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Ця програма також показує журнали для сервісів " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -10483,47 +10485,34 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"Це внутрішня помилка і не те, що Ви спричинили чи можете виправити. Будь " -"ласка, повідомте про помилку на сторінці відстеження недоліків, щоб ми могли " -"виправити її. Також, будь ласка, прикріпіть до звіту про недолік журнал стану." +"Це внутрішня помилка, яку ви не спричинили і не можете виправити. Будь " +"ласка, повідомте про помилку на bug tracker, щоб ми могли її виправити. Також, будь ласка, додайте журнали до звіту про помилку." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Встановлення" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Це останні %(num_lines)s рядків із журналу стану для цього вебінтерфейсу. " -"Якщо Ви бажаєте повідомити про недолік, будь ласка використовуйте сторінку " -"відстеження недоліків і прикріпіть цей журнал стану до звіту " -"про недолік." +"Це останні рядки журналів для служб, пов'язаних із цією програмою. Якщо ви " +"хочете повідомити про помилку, скористайтеся системою " +"відстеження помилок і додайте цей журнал до звіту про помилку." #: plinth/templates/app-logs.html:26 msgid "" @@ -10783,10 +10772,8 @@ msgid "Clear all tags" msgstr "Очистити всі теги" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Журнали" +msgstr "Переглянути журнали" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" From 84a79d923d111e8ca246aa3dd12cfe764925b128 Mon Sep 17 00:00:00 2001 From: 109247019824 <109247019824@users.noreply.hosted.weblate.org> Date: Tue, 23 Sep 2025 07:38:05 +0200 Subject: [PATCH 04/44] Translated using Weblate (Bulgarian) Currently translated at 56.1% (1055 of 1879 strings) --- plinth/locale/bg/LC_MESSAGES/django.po | 64 ++++++++------------------ 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/plinth/locale/bg/LC_MESSAGES/django.po b/plinth/locale/bg/LC_MESSAGES/django.po index b3b637a8e..79e1ed77a 100644 --- a/plinth/locale/bg/LC_MESSAGES/django.po +++ b/plinth/locale/bg/LC_MESSAGES/django.po @@ -8,9 +8,9 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-01 17:02+0000\n" -"Last-Translator: 109247019824 " -"<109247019824@users.noreply.hosted.weblate.org>\n" +"PO-Revision-Date: 2025-09-24 03:02+0000\n" +"Last-Translator: 109247019824 <109247019824@users.noreply.hosted.weblate.org>" +"\n" "Language-Team: Bulgarian \n" "Language: bg\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.13.1-dev\n" +"X-Generator: Weblate 5.14-dev\n" #: plinth/config.py:103 #, python-brace-format @@ -705,24 +705,14 @@ msgid "Restore data from" msgstr "Възстановяване на данни от" #: plinth/modules/backups/templates/backups_upload.html:17 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Upload a backup file downloaded from another %(box_name)s to " -#| "restore its\n" -#| " contents. You can choose the apps you wish to restore after " -#| "uploading a\n" -#| " backup file.\n" -#| " " +#, python-format msgid "" "Upload a backup file downloaded from another %(box_name)s to restore its " "contents. You can choose the apps you wish to restore after uploading a " "backup file." msgstr "" -"\n" -" Качете резервно копие от %(box_name)s, за да го възстановите. Ще " -"можете да изберете приложенията, които да бъдат възстановени.\n" -" " +"Качете резервно копие от %(box_name)s, за да го възстановите. Ще можете да " +"изберете приложенията, които да бъдат възстановени." #: plinth/modules/backups/templates/backups_upload.html:31 #, python-format @@ -8457,15 +8447,7 @@ msgstr "Нови възможности" #: plinth/modules/upgrades/templates/backports-firstboot.html:14 #: plinth/modules/upgrades/templates/upgrades_configure.html:69 -#, fuzzy, python-format -#| msgid "" -#| "Frequent feature updates allow the %(box_name)s Service, plus a very " -#| "limited set of software, to receive new features more frequently (from " -#| "the backports repository). This results in receiving some new features " -#| "within weeks, instead of only once every 2 years or so. Note that " -#| "software with frequent feature updates does not have support from the " -#| "Debian Security Team. Instead, they are maintained by contributors to " -#| "Debian and the %(box_name)s community." +#, python-format msgid "" "Frequent feature updates allow the %(box_name)s Service, plus a very limited " "set of software, to receive new features more frequently (from the backports " @@ -8477,11 +8459,11 @@ msgid "" msgstr "" "Честото обновяване на възможности помага на услугата на %(box_name)s, както " "и на много ограничен набор от софтуер да получават по-често нови възможности " -"(от хранилището backports). Това води до получаване на някои нови " -"възможности в рамките на седмици вместо веднъж на около 2 години. Обърнете " -"внимание, че софтуерът с често обновяване на възможностите не се ползва от " -"подкрепата на екипа по сигурността на Дебиан. Вместо това той се поддържа от " -"сътрудници на Дебиан и общността на %(box_name)s." +"(от хранилището backports или unstable). Това води до получаване на някои " +"нови възможности в рамките на седмици вместо веднъж на около 2 години. " +"Обърнете внимание, че софтуерът с често обновяване на възможностите не се " +"ползва от подкрепата на екипа по сигурността на Дебиан. Вместо това той се " +"поддържа от сътрудници на Дебиан и общността на %(box_name)s." #: plinth/modules/upgrades/templates/backports-firstboot.html:26 msgid "" @@ -9068,13 +9050,7 @@ msgid "The following administrator accounts exist in the system." msgstr "В системата съществуват следните администраторски профили." #: plinth/modules/users/templates/users_firstboot.html:56 -#, fuzzy, python-format -#| msgid "" -#| "Delete these accounts from command line and refresh the page to create an " -#| "account that is usable with %(box_name)s. On the command line run the " -#| "command \"echo '{\"args\": [\"USERNAME\", \"PASSWORD\"], \"kwargs\": {}}' " -#| "| sudo /usr/share/plinth/actions/actions users remove_user\". If an " -#| "account is already usable with %(box_name)s, skip this step." +#, python-format msgid "" "Delete these accounts from command line and refresh the page to create an " "account that is usable with %(box_name)s. On the command line run the " @@ -9084,10 +9060,10 @@ msgid "" msgstr "" "За да създадете профил, който може да бъде използван с %(box_name)s, " "премахнете тези профили от командния ред и презаредете страницата. От " -"команднен ред изпълнете командата „echo '{\"args\": [\"USERNAME\", " -"\"PASSWORD\"], \"kwargs\": {}}' | sudo /usr/share/plinth/actions/actions " -"users remove_user“. Ако профилът вече може да се използва с %(box_name)s, " -"прескочете тази стъпка." +"команднен ред изпълнете командата „echo '{\"args\": " +"[\"USERNAME\", \"AUTH_USER\", \"AUTH_PASSWORD\"], \"kwargs\": {}}' | sudo " +"freedombox-cmd users remove_user“. Ако профилът вече може да се използва с " +"%(box_name)s, прескочете тази стъпка." #: plinth/modules/users/templates/users_firstboot.html:69 msgid "Skip this step" @@ -10016,10 +9992,8 @@ msgid "Clear all tags" msgstr "Изчистване на всички етикети" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Дневник" +msgstr "Преглед на дневника" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" From 9c3776b03def20bafddabae7d57268aa922cbec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Podhoreck=C3=BD?= Date: Sat, 27 Sep 2025 08:07:00 +0200 Subject: [PATCH 05/44] Translated using Weblate (Czech) Currently translated at 100.0% (1879 of 1879 strings) --- plinth/locale/cs/LC_MESSAGES/django.po | 37 +++++++++----------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/plinth/locale/cs/LC_MESSAGES/django.po b/plinth/locale/cs/LC_MESSAGES/django.po index 0fc48189c..2594f902b 100644 --- a/plinth/locale/cs/LC_MESSAGES/django.po +++ b/plinth/locale/cs/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-20 19:02+0000\n" +"PO-Revision-Date: 2025-09-28 07:02+0000\n" "Last-Translator: Jiří Podhorecký \n" "Language-Team: Czech \n" @@ -1640,6 +1640,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Tato aplikace také zobrazuje protokoly pro služby " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -10437,13 +10439,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the logs to the bug report." msgstr "" "Jedná se o interní chybu, kterou jste nezpůsobili ani ji nemůžete opravit. " -"Nahlaste prosím chybu na sledovači chyb, abychom ji mohli opravit. K hlášení " -"chyby prosím připojte také status log." +"Nahlaste prosím chybu na sledovači " +"chyb, abychom ji mohli opravit. K hlášení chyby prosím připojte také protokoly." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Instalace" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Toto je posledních %(num_lines)s řádků stavového protokolu pro toto webové " -"rozhraní. Pokud chcete nahlásit chybu, použijte prosím prohlížeč chyb a " -"připojte tento stavový protokol k hlášení chyby." +"Toto jsou poslední řádky protokolů služeb souvisejících s touto aplikací. " +"Pokud chcete nahlásit chybu, použijte prosím bug tracker a připojte tento protokol k hlášení o chybě." #: plinth/templates/app-logs.html:26 msgid "" @@ -10736,10 +10727,8 @@ msgid "Clear all tags" msgstr "Vymazat všechny štítky" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Protokoly" +msgstr "Zobrazit Protokoly" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" From 996596ddc049d97c77c20fad659971b184f2350a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 15:22:20 -0700 Subject: [PATCH 06/44] glib: Add schedule parameter for setting interval in develop mode Tests: - In development mode, diagnostics task runs after about 180 seconds (with jitter). - In production mode, diagnostics task does not run after 180 seconds. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/glib.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plinth/glib.py b/plinth/glib.py index 5ef5fbec8..f4612d2a4 100644 --- a/plinth/glib.py +++ b/plinth/glib.py @@ -51,8 +51,13 @@ def _run(): def schedule(interval, method, data=None, in_thread=True, repeat=True, - add_jitter=True): - """Schedule a recurring call to a method with fixed interval.""" + add_jitter=True, develop_interval=None): + """Schedule a recurring call to a method with fixed interval. + + develop_interval is number of seconds to schedule the task after when + running in development mode. A value of 180 seconds assumed if the value is + set to None or 0. + """ def _runner(): """Run the target method and log and exceptions.""" @@ -74,8 +79,8 @@ def schedule(interval, method, data=None, in_thread=True, repeat=True, # When running in development mode, reduce the interval for tasks so that # they are triggered quickly and frequently to facilitate debugging. - if cfg.develop and interval > 180: - interval = 180 + if cfg.develop: + interval = min(interval, develop_interval or 180) if add_jitter: # Add or subtract 5% random jitter to given interval to avoid many From d512a8b645ed87342c65911f52f03b19248e52f8 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 15:43:55 -0700 Subject: [PATCH 07/44] diagnostics: In development mode, run diagnostics more rarely Due the frequency and length of execution of diagnostics, the service does not restart when files are modified. The operation also makes other testing tasks wait until completed. It also makes functional tests slower. So, reduce the frequency of execution. It can always be temporarily changed when debugging diagnostics operations is necessary. Tests: - Change the development interval to 18 seconds and notice that new interval is effective in development mode but not in production mode. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/diagnostics/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plinth/modules/diagnostics/__init__.py b/plinth/modules/diagnostics/__init__.py index 42b5652b6..0b3d21d65 100644 --- a/plinth/modules/diagnostics/__init__.py +++ b/plinth/modules/diagnostics/__init__.py @@ -86,8 +86,9 @@ class DiagnosticsApp(app_module.App): # Check periodically for low RAM space glib.schedule(3600, _warn_about_low_ram_space) - # Run diagnostics once a day - glib.schedule(24 * 3600, _daily_diagnostics_run, in_thread=False) + # Run diagnostics once a day or every 30 minutes in development mode. + glib.schedule(24 * 3600, _daily_diagnostics_run, in_thread=False, + develop_interval=1800) def setup(self, old_version): """Install and configure the app.""" From 904e5935cb59ac5dc3a90d39d6d4a290c3256635 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 21 Aug 2025 14:36:14 -0700 Subject: [PATCH 08/44] backups: Ignore a typing error with mypy Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/backups/repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plinth/modules/backups/repository.py b/plinth/modules/backups/repository.py index ec1c642cb..ec40a3ed1 100644 --- a/plinth/modules/backups/repository.py +++ b/plinth/modules/backups/repository.py @@ -237,7 +237,7 @@ class RootBorgRepository(BaseBorgRepository): UUID = 'root' PATH = '/var/lib/freedombox/borgbackup' - storage_type = 'root' + storage_type = 'root' # type: ignore name = format_lazy(_('{box_name} storage'), box_name=_(cfg.box_name)) borg_path = PATH sort_order = 10 @@ -252,7 +252,7 @@ class RootBorgRepository(BaseBorgRepository): class BorgRepository(BaseBorgRepository): """General Borg repository implementation.""" known_credentials = ['encryption_passphrase'] - storage_type = 'disk' + storage_type = 'disk' # type: ignore sort_order = 20 flags = {'removable': True} @@ -273,7 +273,7 @@ class SshBorgRepository(BaseBorgRepository): known_credentials = [ 'ssh_keyfile', 'ssh_password', 'encryption_passphrase' ] - storage_type = 'ssh' + storage_type = 'ssh' # type: ignore sort_order = 30 flags = {'removable': True, 'mountable': True} From e227e9a919f403e9a5253675c368f8636df0a136 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sat, 16 Aug 2025 18:29:19 -0700 Subject: [PATCH 09/44] Makefile: Move privileged daemon to /usr/lib/freedombox Tests: - Running make install installs to /usr/lib/freedombox. Non-privileged users don't find it in the path. root user does. - New service file contains path to /usr/lib/freedombox/. Actions works as expected. - Build and install the debian package. Privileged daemon runs as expected and first setup steps complete as expected. First wizard works as expected. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- Makefile | 3 ++- data/usr/lib/systemd/system/freedombox-privileged.service | 2 +- debian/freedombox.lintian-overrides | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index fecc2c3ba..db908fbf1 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ DIRECTORIES_TO_CREATE := \ STATIC_FILES_DIRECTORY := $(DESTDIR)/usr/share/plinth/static BIN_DIR := $(DESTDIR)/usr/bin +LIB_DIR := $(DESTDIR)/usr/lib FIND_ARGS := \ -not -iname "*.log" \ @@ -102,7 +103,7 @@ install: rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/COPYING.md && \ rm -f $(DESTDIR)$${lib_dir}/plinth*.dist-info/direct_url.json && \ $(INSTALL) -D -t $(BIN_DIR) bin/plinth - $(INSTALL) -D -t $(BIN_DIR) bin/freedombox-privileged + $(INSTALL) -D -t $(LIB_DIR)/freedombox bin/freedombox-privileged $(INSTALL) -D -t $(BIN_DIR) bin/freedombox-cmd # Static web server files diff --git a/data/usr/lib/systemd/system/freedombox-privileged.service b/data/usr/lib/systemd/system/freedombox-privileged.service index 9cce91e34..fe255c94c 100644 --- a/data/usr/lib/systemd/system/freedombox-privileged.service +++ b/data/usr/lib/systemd/system/freedombox-privileged.service @@ -8,7 +8,7 @@ StartLimitIntervalSec=0 [Service] Type=notify -ExecStart=/usr/bin/freedombox-privileged +ExecStart=/usr/lib/freedombox/freedombox-privileged TimeoutSec=300s User=root Group=root diff --git a/debian/freedombox.lintian-overrides b/debian/freedombox.lintian-overrides index 1bda5b605..b3a57a7da 100644 --- a/debian/freedombox.lintian-overrides +++ b/debian/freedombox.lintian-overrides @@ -19,3 +19,8 @@ freedombox binary: web-application-works-only-with-apache # Not documentation freedombox: package-contains-documentation-outside-usr-share-doc [usr/share/plinth/static/jslicense.html] freedombox: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/plinth-*.dist-info/top_level.txt] + +# This executable is meant to executed from systemd service file and is not +# meant for user. However, don't install to /usr/libexec and follow systemd +# convention instead. +freedombox: executable-in-usr-lib [usr/lib/freedombox/freedombox-privileged] From c8f89e3ca5404bc30b3feac55c78e9d9850a43c4 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 17 Aug 2025 08:47:43 -0700 Subject: [PATCH 10/44] action_utils: Handle capture_output argument in run wrapper Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index e7db636a1..8bed5ca0a 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -825,8 +825,10 @@ def run_as_user(command, username, **kwargs): def run(command, **kwargs): """Run subprocess.run but capture stdout and stderr in thread storage.""" - collect_stdout = ('stdout' not in kwargs) - collect_stderr = ('stderr' not in kwargs) + collect_stdout = ('stdout' not in kwargs + and 'capture_output' not in kwargs) + collect_stderr = ('stderr' not in kwargs + and 'capture_output' not in kwargs) if collect_stdout: kwargs['stdout'] = subprocess.PIPE From c2d5d1d3c81374a1a45f27d9ec874bc179c16623 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 16 Sep 2025 14:10:08 -0700 Subject: [PATCH 11/44] privileged_daemon: Fix showing errors for freedombox-cmd command Tests: - When arguments are not provided to freedombox-cmd it shows errors on the console. - When a command is successfully executed, the output is printed on the console. - The output of the privileged daemon goes to the journald. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/log.py | 9 ++++++--- plinth/privileged_daemon.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plinth/log.py b/plinth/log.py index e34a02f47..d77a66d5d 100644 --- a/plinth/log.py +++ b/plinth/log.py @@ -90,13 +90,16 @@ def _capture_warnings(): warnings.filterwarnings('default', '', ImportWarning) -def action_init(): +def action_init(console: bool = False): """Initialize logging for action scripts.""" _capture_warnings() configuration = get_configuration() - # Don't log to console - configuration['root']['handlers'] = ['journal'] + if console: + configuration['root']['handlers'] = ['console'] + else: + configuration['root']['handlers'] = ['journal'] + logging.config.dictConfig(configuration) diff --git a/plinth/privileged_daemon.py b/plinth/privileged_daemon.py index 7cfd256a5..66102718e 100644 --- a/plinth/privileged_daemon.py +++ b/plinth/privileged_daemon.py @@ -207,7 +207,7 @@ class Server(socketserver.ThreadingUnixStreamServer): def client_main() -> None: """Parse arguments for the client for privileged daemon.""" - log.action_init() + log.action_init(console=True) parser = argparse.ArgumentParser() parser.add_argument('module', help='Module to trigger action in') From 7c0fa00536e24e8638fd8f1d8b40f231beac803b Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 16 Sep 2025 14:40:55 -0700 Subject: [PATCH 12/44] doc: Add manual page for freedombox-cmd Tests: - 'make -C doc' succeeds. 'man doc/freedombox-cmd.1' shows the manual page. - Building and install .deb package installs the manual page to appropriate location. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .gitignore | 2 +- debian/freedombox.manpages | 1 + doc/Makefile | 2 +- doc/freedombox-cmd.xml | 160 +++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 doc/freedombox-cmd.xml diff --git a/.gitignore b/.gitignore index 812d5c4ad..9440cf52b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ doc/manual/*/*.pdf doc/manual/*/*.html doc/manual/*/*.xml -doc/plinth.1 +doc/*.1 doc/dev/_build \#* .#* diff --git a/debian/freedombox.manpages b/debian/freedombox.manpages index 1b517d232..f96cd2ce6 100644 --- a/debian/freedombox.manpages +++ b/debian/freedombox.manpages @@ -1 +1,2 @@ +./doc/freedombox-cmd.1 ./doc/plinth.1 diff --git a/doc/Makefile b/doc/Makefile index 95489391b..37ccf8465 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -13,7 +13,7 @@ SCRIPTS_DIR=scripts manual-pdfs=$(foreach lang,$(MANUAL_LANGUAGES),manual/$(lang)/freedombox-manual.pdf) manual-xmls=$(patsubst %.pdf,%.xml,$(manual-pdfs)) -OUTPUTS=$(manual-pdfs) plinth.1 +OUTPUTS=$(manual-pdfs) plinth.1 freedombox-cmd.1 INSTALL_OPTS=-D --mode=644 diff --git a/doc/freedombox-cmd.xml b/doc/freedombox-cmd.xml new file mode 100644 index 000000000..64eb6d3f4 --- /dev/null +++ b/doc/freedombox-cmd.xml @@ -0,0 +1,160 @@ + + + + + + freedombox-cmd + 1 + FreedomBox Command Line Utility + + + + + freedombox-cmd + + command line utility to perform FreedomBox operations + + + + + + freedombox-cmd + + module + action + + + + + + Description + + FreedomBox is a community project to develop, design and promote + personal servers running free software for private, personal + communications. It is a networking appliance designed to allow + interfacing with the rest of the Internet under conditions of + protected privacy and data security. It hosts applications such + as blog, wiki, website, social network, email, web proxy and a + Tor relay on a device that can replace a wireless router so that + data stays with the users. + + + freedombox-cmd is a command line interface to some of the operations + performed by FreedomBox. It is typically not needed by the end users who + use FreedomBox's web interface. The command may be used in some cases + while debugging problems, especially where the web interface is not + accessible or when a piece of functionality that is not provided in the + web interface needs to be triggered. + + + The command is simply a client to the FreedomBox's privileged daemon and + relays user's request to it. It waits for the request to complete and + prints the output of the operation or an error message collected form the + daemon. The daemon only allows connections from an pre-allowed list of + user accounts. So, be sure to run the command as 'root' superuser. + + + + + Options + + + + + + Name of the module from which to execute an action. + + + + + + + + Name of the action to execute. It should found in the provided + module. + + + + + + + + Don't try to read the arguments to the command on the standard + input. Instead, assume that the operation does not have any + arguments and execute the method without arguments. + + + + + + + + Show brief help about arguments allowed for this command. + + + + + + + + Examples + + + Re-run FreedomBox network setup + $ sudo freedombox-cmd networks setup --no-args + + When FreedomBox starts for the first time, it will setup Network Manager + connections suitable for the hardware found. If you wish to re-create + these connections at a later time, you can re-run setup for the Networks + app using the web interface or run this command on a terminal. + + + + + Delete a user account from LDAP database + $ echo '{"args": ["USERNAME", "AUTH_USER", "AUTH_PASSWORD"], "kwargs": {}}' | sudo freedombox-cmd users remove_user + + USERNAME is the name of the user account that must be removed. AUTH_USER + is name of the user account that is authorizing this operation. + AUTH_PASSWORD is the password for user account that is authorizing this + operation. This operation may be needed if FreedomBox's sqlite3 database + is wiped, removing the user accounts in the database but the + corresponding entries from LDAP database are not removed. A new user + with that name can't be created until the LDAP account is also removed. + + + + + Set the logging mode to persistent + $ echo '{"args": ["persistent"], "kwargs": {}}' | sudo freedombox-cmd config set_logging_mode + + By default, FreedomBox sets up systemd-journald to 'volatile' logging. + This means that logs will not be stored on the disk and will be lost + after a reboot. If you are tackling a problem and wish to store the logs + persistently, you can change the setting in the web interface or run + this command. + + + + + + Bugs + + See FreedomBox + issue tracker for a full list of known issues and TODO items. + + + + + Author + + + FreedomBox Developers + Original author + + + + From 288b58e0b552987e6deb6b3b1bcdfaf5ff8af396 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 16 Sep 2025 14:50:23 -0700 Subject: [PATCH 13/44] storage: Fix disk usage checking with disconnected SSH mounts - When disconnected sshfs mounts are present, then df command prints the disk usage for the remaining disks but prints a warning to the stderr and return a non-zero return code. Accommodate this case and parse the information for all the available disks. Tests: - Create a remote backup location and mount it. When the SSH process is killed, it leaves a mount point that is not properly connected. View the storage page to see that disk usage for other partitions is shown properly. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/storage/privileged.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/modules/storage/privileged.py b/plinth/modules/storage/privileged.py index e5422eced..dc156fa3a 100644 --- a/plinth/modules/storage/privileged.py +++ b/plinth/modules/storage/privileged.py @@ -325,7 +325,7 @@ def usage_info() -> str: 'df', '--exclude-type=tmpfs', '--exclude-type=devtmpfs', '--block-size=1', '--output=source,fstype,size,used,avail,pcent,target' ] - return subprocess.check_output(command).decode() + return action_utils.run(command, check=False).stdout.decode() @privileged From 636b4cabd89cbd6c2de77dd54461049d587b6c02 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Sep 2025 13:46:17 -0700 Subject: [PATCH 14/44] actions: Work with older privileged daemon - Older privileged daemon before 25.10 did not return the stdout/stderr properties as part of an exception. During upgrade, there is a 5 minute time window (longer if the privileged daemon is continuously used) when privileged daemon is the old version and the service is the newer version. During this time any exception in the privileged task will cause this problem. - Our goal is not to always provide backward compatibility to old version of privileged daemon as the web interface and privileged daemon are expected to be upgraded at the same time. However, this one is easy and is complementary to a separate fix that addresses the core problem. Tests: - Perform an operation that raises an Exception in a privileged method. The error is properly shown as an HTML message but without stdout and stderr. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plinth/actions.py b/plinth/actions.py index 0f195da73..45dbc228b 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -170,8 +170,8 @@ def _wait_for_server_response(func, module_name, action_name, args, kwargs, module = importlib.import_module(return_value['exception']['module']) exception_class = getattr(module, return_value['exception']['name']) exception = exception_class(*return_value['exception']['args']) - exception.stdout = return_value['exception']['stdout'].encode() - exception.stderr = return_value['exception']['stderr'].encode() + exception.stdout = return_value['exception'].get('stdout', b'').encode() + exception.stderr = return_value['exception'].get('stderr', b'').encode() def _get_html_message(): """Return an HTML format error that can be shown in messages.""" From 0fdf59b9f058789c3acf73bc7e95520655f9718a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Sep 2025 13:52:14 -0700 Subject: [PATCH 15/44] privileged_daemon: Implement handling termination signal - And gracefully terminate the process after finishing the current requests underway. Tests: - Trigger a long operation such as an app installation. While the operation is underway, run 'systemctl stop freedombox-privilved.service'. Journal will show that the SIGTERM is handled and shutdown is more or less immediately complete. However, the whole process will wait until the ongoing request is complete and then exit. - During the wait period, no new requests are accepted as experienced with 'freedombox-cmd plinth is_package_manager_busy --no-args' command. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/privileged_daemon.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/plinth/privileged_daemon.py b/plinth/privileged_daemon.py index 66102718e..3b31ec900 100644 --- a/plinth/privileged_daemon.py +++ b/plinth/privileged_daemon.py @@ -8,10 +8,12 @@ import logging import os import pathlib import pwd +import signal import socket import socketserver import struct import sys +import threading import time import systemd.daemon @@ -28,6 +30,8 @@ FREEDOMBOX_PROCESS_USER = 'plinth' MAX_REQUEST_LENGTH = 1_000_000 +_server = None + idle_shutdown_time: int | None = 5 * 60 # 5 minutes freedombox_develop = False @@ -245,6 +249,25 @@ def client_main() -> None: sys.exit(1) +def _on_sigterm(signal_number: int, frame) -> None: + """Handle SIGTERM signal. Issue server shutdown.""" + threading.Thread(target=_shutdown_server).start() + + +def _shutdown_server() -> None: + """Issue a shutdown request to the server. + + This must be run in a thread separate from the server.serve_forever() + otherwise it will deadlock waiting for the shutdown to complete. + """ + global _server + logger.info('SIGTERM received, shutting down the server.') + if _server: + _server.shutdown() + + logger.info('Shutdown complete, some requests may be running.') + + def main() -> None: """Start the server, listen on socket, and serve forever.""" global freedombox_develop, idle_shutdown_time @@ -263,10 +286,15 @@ def main() -> None: if not systemd.daemon.listen_fds(unset_environment=False): idle_shutdown_time = None + signal.signal(signal.SIGTERM, _on_sigterm) + module_loader.load_modules() app_module.apps_init() with Server(str(address), RequestHandler) as server: + global _server + _server = server # Reference needed to shutdown the server. + # systemd will wait until notification to proceed with other processes. # We have service Type=notify. systemd.daemon.notify('READY=1') @@ -283,6 +311,11 @@ def main() -> None: else: logger.info('FreedomBox privileged daemon exiting.') + # Exit the context manager. This calls server.close() which waits on + # all pending request threads to complete. + + logger.info('All requested completed. Exit.') + if __name__ == '__main__': main() From 1ad48ecad87921ea8f4b84e2821e3dd41c9c71c8 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Sep 2025 14:19:22 -0700 Subject: [PATCH 16/44] d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib Since debhelper 13.11.6, we don't need this hack as dh_installsystemd recognizes the files in /usr/lib/systemd/ directory in addition to /lib/systemd/. Tests: - After build package with gbp. Notice that postinst script has code inserted by dh_installsystemd for starting/restarting the service. - Install the deb package starts service. Reinstalling the package restarts the service. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- debian/rules | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/debian/rules b/debian/rules index 50a2cc0c1..de56899b6 100755 --- a/debian/rules +++ b/debian/rules @@ -29,10 +29,6 @@ ifneq ($(FBX_VERSION),$(DEB_VERSION)) endif override_dh_installsystemd: - # Do not enable or start any service other than FreedomBox service. Use - # of --tmpdir is a hack to workaround an issue with dh_installsystemd - # (as of debhelper 13.5.2) that still has hardcoded search path of - # /lib/systemd/system for searching systemd services. See #987989 and - # reversion of its changes. - dh_installsystemd --tmpdir=debian/tmp/usr --package=freedombox \ - plinth.service freedombox-privileged.socket + # Do not enable or start any service other than FreedomBox service. + dh_installsystemd --package=freedombox plinth.service \ + freedombox-privileged.socket From daca4d1d9c22c999f9a3ab66b7aa408c4109b041 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 17 Sep 2025 20:16:35 -0700 Subject: [PATCH 17/44] actions: Log method arguments in privileged daemon - This change means that when invalid module or action name is provided, the log message is not printed. However, this is acceptable as those cases are rare in production and are logged properly on the client side. Tests: - Run diagnostics for an app and notice that arguments are printed in privileged daemon's journald logs. - Remove a password from bepasty app and notice that the password argument is not logged. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plinth/actions.py b/plinth/actions.py index 45dbc228b..0634e3dd2 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -431,8 +431,6 @@ def privileged_handle_json_request( try: request = _parse_request() - logger.info('Received request for %s..%s(..)', request['module'], - request['action']) arguments = {'args': request['args'], 'kwargs': request['kwargs']} _setup_thread_storage() return_value = _privileged_call(request['module'], request['action'], @@ -495,6 +493,8 @@ def _privileged_call(module_name, action_name, arguments): _privileged_assert_valid_arguments(func, arguments) + _log_action(func, module_name, action_name, arguments['args'], + arguments['kwargs'], run_in_background=False) try: return_values = func(*arguments['args'], **arguments['kwargs']) if isinstance(return_values, io.BufferedReader): From 647e72516c65c3a37a2967d53f4218c9022772ea Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Sep 2025 13:03:38 -0700 Subject: [PATCH 18/44] backups: Fix robust handling of known errors During functional tests, it was noticed that getattr() failed at the following line. The original intent of the code is to ensure that there are no failures when 'stdout'/'stderr' attribute are not present or when they return None. stdout = (getattr(err, 'stdout') or b'').decode() Tests: - Make the UI raise incorrect password error. Notice that the error is shown properly. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/backups/privileged.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index ced04bc8c..f7fbdb64f 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -118,8 +118,8 @@ def reraise_known_errors(privileged_func): def _reraise_known_errors(err): """Look whether the caught error is known and reraise it accordingly""" - stdout = (getattr(err, 'stdout') or b'').decode() - stderr = (getattr(err, 'stderr') or b'').decode() + stdout = (getattr(err, 'stdout', b'') or b'').decode() + stderr = (getattr(err, 'stderr', b'') or b'').decode() caught_error = str((err, err.args, stdout, stderr)) for known_error in KNOWN_ERRORS: for error in known_error['errors']: From 5566f05caddbf56af94a047a809c9cdf0806b33b Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Wed, 24 Sep 2025 16:22:41 -0700 Subject: [PATCH 19/44] config: Set home page to FreedomBox for invalid values - When attempting to set an invalid shortcut ID or invalid user's directory as home page, set FreedomBox UI as home page. - Simplify the tests somewhat and avoid failure first time and skipping the test next time. Tests: - Run unit tests as 'root' and 'fbx' users. - Set home page to apache default, FreedomBox, user home page and a shortcut. The set value is retained. The change works when visiting / with browser. The value is as expected in Apache configuration. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/config/__init__.py | 8 ++-- plinth/modules/config/tests/test_config.py | 45 ++++++---------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/plinth/modules/config/__init__.py b/plinth/modules/config/__init__.py index 75e1c6b1b..c0b82f459 100644 --- a/plinth/modules/config/__init__.py +++ b/plinth/modules/config/__init__.py @@ -109,23 +109,23 @@ def home_page_url2scid(url: str | None): def _home_page_scid2url(shortcut_id: str) -> str | None: """Return the url for the given home page shortcut ID.""" + url: str | None = '/plinth/' if shortcut_id == 'plinth': - url = '/plinth/' + pass elif shortcut_id == 'apache-default': url = None elif shortcut_id.startswith('uws-'): user = shortcut_id[4:] if user in get_users_with_website(): url = uws_url_of_user(user) - else: - url = None else: shortcuts = frontpage.Shortcut.list() aux = [ shortcut.url for shortcut in shortcuts if shortcut_id == shortcut.component_id ] - url = aux[0] if 1 == len(aux) else None + if 1 == len(aux): + url = aux[0] return url diff --git a/plinth/modules/config/tests/test_config.py b/plinth/modules/config/tests/test_config.py index dd7d192d1..5453fed1a 100644 --- a/plinth/modules/config/tests/test_config.py +++ b/plinth/modules/config/tests/test_config.py @@ -4,11 +4,13 @@ Tests for config module. """ import os -from unittest.mock import MagicMock, patch +import pathlib +from unittest.mock import Mock, patch import pytest from plinth import __main__ as plinth_main +from plinth import utils from plinth.modules.apache import uws_directory_of_user, uws_url_of_user from plinth.modules.config import (_home_page_scid2url, change_home_page, get_home_page, home_page_url2scid) @@ -61,23 +63,14 @@ def test_homepage_mapping_skip_ci(): # AC: Return None if it doesn't: os.rmdir(uws_directory) - assert _home_page_scid2url(uws_scid) is None + assert _home_page_scid2url(uws_scid) == '/plinth/' -class Dict2Obj: - """Mock object made out of any dict.""" - - def __init__(self, a_dict): - self.__dict__ = a_dict - - -@patch('plinth.frontpage.Shortcut.list', - MagicMock(return_value=[ - Dict2Obj({ - 'url': 'url/for/' + id, - 'component_id': id - }) for id in ('a', 'b') - ])) +@patch( + 'plinth.frontpage.Shortcut.list', + Mock(return_value=[ + Mock(url='url/for/' + id, component_id=id) for id in ('a', 'b') + ])) @pytest.mark.usefixtures('needs_root') def test_homepage_field(): """Test homepage changes. @@ -104,23 +97,14 @@ def test_homepage_field(): Currently they share the same test case. - Search for another valid user apart from fbx. """ - user = 'fbx' + user = 'test_' + utils.random_string(size=12) uws_directory = uws_directory_of_user(user) uws_url = uws_url_of_user(user) uws_scid = home_page_url2scid(uws_url) default_home_page = 'plinth' original_home_page = get_home_page() or default_home_page - - # Check test's preconditions: - if original_home_page not in (default_home_page, None): - reason = "Unexpected home page {}.".format(original_home_page) - pytest.skip(reason) - - if os.path.exists(uws_directory): - # Don't blindly remove a pre-existing directory. Just skip the test. - reason = "UWS directory {} exists already.".format(uws_directory) - pytest.skip(reason) + change_home_page(default_home_page) # Set to known value explicitly # AC: invalid changes fall back to default: for scid in ('uws-unexisting', uws_scid, 'missing_app'): @@ -128,12 +112,7 @@ def test_homepage_field(): assert get_home_page() == default_home_page # AC: valid changes actually happen: - try: - os.mkdir(uws_directory) - except Exception: - reason = "Needs access to ~/ directory. " \ - + "CI sandboxed workspace doesn't provide it." - pytest.skip(reason) + pathlib.Path(uws_directory).mkdir(parents=True) for scid in ('b', 'a', uws_scid, 'apache-default', 'plinth'): change_home_page(scid) assert get_home_page() == scid From a43082308d7f9028c33a25b9fafedb13042925ad Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 15:04:40 -0700 Subject: [PATCH 20/44] actions: Log full exception from privileged daemon on error - This make it easy to find issues when looking at either main service logs or privileged daemon logs. Tests: - Raise an exception in one of the privileged actions. Notice that the exception is printed along with module name, action_name, stdout, stderr and traceback. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/actions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plinth/actions.py b/plinth/actions.py index 0634e3dd2..5762d3b99 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -503,6 +503,11 @@ def _privileged_call(module_name, action_name, arguments): return_value = {'result': 'success', 'return': return_values} except Exception as exception: return_value = get_return_value_from_exception(exception) + logger.exception( + 'Error running action: %s..%s(..): %s\nstdout:\n%s\nstderr:\n%s\n', + module_name, action_name, exception, + return_value['exception']['stdout'], + return_value['exception']['stderr']) return return_value From 2fbaea191fb9f72867d1cf91dd80f2a0d3f13fea Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 15:12:00 -0700 Subject: [PATCH 21/44] setup: Log full exception traceback when setup fails - When an error occurs during setup thread execution and the error is not due a failed privileged action, we are left with very little information about what went run. On the other than when a privileged action fails, we will be logging the exception twice. But this is okay. Tests: - Increment the setup version of one of installed apps and raise an exception in setup() method. Notice that exception traceback in the logged message. - Increment the setup version of one of installed apps and raise an exception in setup's privileged action. Notice that exception traceback in the logged message twice. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/setup.py b/plinth/setup.py index a90e3f3f4..9676eadcb 100644 --- a/plinth/setup.py +++ b/plinth/setup.py @@ -350,7 +350,7 @@ def run_setup_on_apps(app_ids, allow_install=True): else: setup_apps(app_ids, allow_install=allow_install) except Exception as exception: - logger.error('Error running setup - %s', exception) + logger.exception('Error running setup - %s', exception) raise From f559870d3eae5e37235c29c1cbf7236a04d3b059 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 12:27:51 -0700 Subject: [PATCH 22/44] actions: Fix lifetime of thread local storage - A local storage object must exist globally shared by all threads. Then object.__dict__ is the thread specific storage. Absent this, when multiple actions run in parallel, one will erase the thread local object of another. Tests: - When an error is raised in a privileged method, then the HTML error shown contains stdout and stderr of the involved processes. - Running functional tests on a lot of apps does not show this error anymore. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 4 ++-- plinth/actions.py | 18 ++++++------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 8bed5ca0a..5e5e87f68 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -837,10 +837,10 @@ def run(command, **kwargs): kwargs['stderr'] = subprocess.PIPE process = subprocess.run(command, **kwargs) - if collect_stdout and actions.thread_storage: + if collect_stdout and hasattr(actions.thread_storage, 'stdout'): actions.thread_storage.stdout += process.stdout - if collect_stderr and actions.thread_storage: + if collect_stderr and hasattr(actions.thread_storage, 'stderr'): actions.thread_storage.stderr += process.stderr return process diff --git a/plinth/actions.py b/plinth/actions.py index 5762d3b99..d045c4000 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) socket_path = '/run/freedombox/privileged.socket' -thread_storage = None +thread_storage = threading.local() # An alias for 'str' to mark some strings as sensitive. Sensitive strings are @@ -364,7 +364,6 @@ class JSONEncoder(json.JSONEncoder): def _setup_thread_storage(): """Setup collection of stdout/stderr from any process in this thread.""" global thread_storage - thread_storage = threading.local() thread_storage.stdout = b'' thread_storage.stderr = b'' @@ -376,14 +375,13 @@ def _clear_thread_storage(): cleaned up after a thread terminates. """ global thread_storage - if thread_storage: - thread_storage.stdout = None - thread_storage.stderr = None - thread_storage = None + thread_storage.stdout = None + thread_storage.stderr = None def get_return_value_from_exception(exception): """Return the value to return from server when an exception is raised.""" + global thread_storage return_value = { 'result': 'exception', 'exception': { @@ -391,14 +389,10 @@ def get_return_value_from_exception(exception): 'name': type(exception).__name__, 'args': exception.args, 'traceback': traceback.format_tb(exception.__traceback__), - 'stdout': '', - 'stderr': '' + 'stdout': getattr(thread_storage, 'stdout', b'').decode(), + 'stderr': getattr(thread_storage, 'stderr', b'').decode(), } } - if thread_storage: - return_value['exception']['stdout'] = thread_storage.stdout.decode() - return_value['exception']['stderr'] = thread_storage.stderr.decode() - return return_value From 355812c9f21b387fe9cb18be0d13534ded897e3a Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 25 Sep 2025 22:22:04 -0700 Subject: [PATCH 23/44] actions_utils: Fix issue with collecting stdout/stderr - When an exception is raised in subprocess.run(), for that call the stdout and stderr are not being collected. Any previous successful calls are being collected. - This also fixes issues with adding an existing backup repository back after removal. Capturing stderr is essential for raising the proper exceptions and working correctly. Tests: - Remove an existing backup repository and add it back again. It fails with the patches and succeeds with the patches. - Remove an existing encrypted backup repository and add it back again with the wrong password. A proper error message is shown 'Incorrect encryption passphrase'. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 5e5e87f68..0b7800cef 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -836,11 +836,20 @@ def run(command, **kwargs): if collect_stderr: kwargs['stderr'] = subprocess.PIPE - process = subprocess.run(command, **kwargs) - if collect_stdout and hasattr(actions.thread_storage, 'stdout'): - actions.thread_storage.stdout += process.stdout + try: + process = subprocess.run(command, **kwargs) + if collect_stdout and hasattr(actions.thread_storage, 'stdout'): + actions.thread_storage.stdout += process.stdout - if collect_stderr and hasattr(actions.thread_storage, 'stderr'): - actions.thread_storage.stderr += process.stderr + if collect_stderr and hasattr(actions.thread_storage, 'stderr'): + actions.thread_storage.stderr += process.stderr + except subprocess.CalledProcessError as exception: + if exception.stdout and hasattr(actions.thread_storage, 'stdout'): + actions.thread_storage.stdout += exception.stdout + + if exception.stderr and hasattr(actions.thread_storage, 'stderr'): + actions.thread_storage.stderr += exception.stderr + + raise exception return process From 61ff15a04fc78fcc1a8700a2bb5d519b3fde3dd2 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 14 Aug 2025 21:33:17 -0700 Subject: [PATCH 24/44] *: Use action_utils.run instead of subprocess.run - This is to capture stdout and stderr and transmit that from privileged daemon back to the service to be displayed in HTML. Tests: - Unit tests and code checks pass. - Some of the modified actions work as expected. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 61 ++++++++----------- plinth/db/mariadb.py | 4 +- plinth/db/postgres.py | 3 +- plinth/modules/apache/privileged.py | 7 +-- plinth/modules/backups/privileged.py | 2 +- plinth/modules/bepasty/privileged.py | 9 ++- plinth/modules/datetime/privileged.py | 3 +- plinth/modules/email/postfix.py | 4 +- plinth/modules/email/privileged/dkim.py | 3 +- plinth/modules/email/privileged/home.py | 5 +- plinth/modules/email/privileged/spam.py | 9 +-- plinth/modules/firewall/privileged.py | 18 +++--- plinth/modules/ikiwiki/privileged.py | 19 +++--- plinth/modules/infinoted/privileged.py | 9 +-- plinth/modules/letsencrypt/privileged.py | 6 +- plinth/modules/mediawiki/privileged.py | 10 +-- plinth/modules/minidlna/privileged.py | 3 +- plinth/modules/mumble/privileged.py | 4 +- plinth/modules/names/privileged.py | 5 +- plinth/modules/networks/privileged.py | 7 ++- plinth/modules/nextcloud/privileged.py | 17 +++--- plinth/modules/openvpn/privileged.py | 8 +-- plinth/modules/snapshot/privileged.py | 33 +++++----- plinth/modules/sogo/privileged.py | 8 +-- plinth/modules/storage/privileged.py | 18 +++--- plinth/modules/syncthing/privileged.py | 5 +- plinth/modules/tor/privileged.py | 3 +- plinth/modules/torproxy/privileged.py | 3 +- plinth/modules/upgrades/distupgrade.py | 15 +++-- plinth/modules/upgrades/privileged.py | 7 ++- .../upgrades/tests/test_distupgrade.py | 25 +++++--- plinth/modules/users/privileged.py | 42 +++++++------ plinth/modules/wordpress/privileged.py | 23 ++++--- plinth/modules/zoph/privileged.py | 37 +++++------ plinth/privileged/packages.py | 7 +-- plinth/tests/test_daemon.py | 59 ++++++++++-------- 36 files changed, 257 insertions(+), 244 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 0b7800cef..119c842de 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -33,20 +33,19 @@ def is_systemd_running(): def systemd_get_default() -> str: """Return the default target that systemd will boot into.""" - process = subprocess.run(['systemctl', 'get-default'], - stdout=subprocess.PIPE, check=True) + process = run(['systemctl', 'get-default'], stdout=subprocess.PIPE, + check=True) return process.stdout.decode().strip() def systemd_set_default(target: str): """Set the default target that systemd will boot into.""" - subprocess.run(['systemctl', 'set-default', target], check=True) + run(['systemctl', 'set-default', target], check=True) def service_daemon_reload(): """Reload systemd to ensure that newer unit files are read.""" - subprocess.run(['systemctl', 'daemon-reload'], check=True, - stdout=subprocess.DEVNULL) + run(['systemctl', 'daemon-reload'], check=True, stdout=subprocess.DEVNULL) def service_is_running(servicename): @@ -55,8 +54,8 @@ def service_is_running(servicename): Does not need to run as root. """ try: - subprocess.run(['systemctl', 'status', servicename], check=True, - stdout=subprocess.DEVNULL) + run(['systemctl', 'status', servicename], check=True, + stdout=subprocess.DEVNULL) return True except subprocess.CalledProcessError: # If a service is not running we get a status code != 0 and @@ -102,9 +101,8 @@ def service_is_enabled(service_name, strict_check=False): """ try: - process = subprocess.run(['systemctl', 'is-enabled', service_name], - check=True, stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL) + process = run(['systemctl', 'is-enabled', service_name], check=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) if not strict_check: return True @@ -115,13 +113,13 @@ def service_is_enabled(service_name, strict_check=False): def service_enable(service_name: str, check: bool = False): """Enable and start a service in systemd.""" - subprocess.run(['systemctl', 'enable', service_name], check=check) + run(['systemctl', 'enable', service_name], check=check) service_start(service_name, check=check) def service_disable(service_name: str, check: bool = False): """Disable and stop service in systemd.""" - subprocess.run(['systemctl', 'disable', service_name], check=check) + run(['systemctl', 'disable', service_name], check=check) try: service_stop(service_name, check=check) except subprocess.CalledProcessError: @@ -130,12 +128,12 @@ def service_disable(service_name: str, check: bool = False): def service_mask(service_name: str, check: bool = False): """Mask a service""" - subprocess.run(['systemctl', 'mask', service_name], check=check) + run(['systemctl', 'mask', service_name], check=check) def service_unmask(service_name: str, check: bool = False): """Unmask a service""" - subprocess.run(['systemctl', 'unmask', service_name], check=check) + run(['systemctl', 'unmask', service_name], check=check) def service_start(service_name: str, check: bool = False): @@ -181,14 +179,14 @@ def service_get_logs(service_name: str) -> str: command = [ 'journalctl', '--no-pager', '--lines=200', '--unit', service_name ] - process = subprocess.run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False, stdout=subprocess.PIPE) return process.stdout.decode() def service_show(service_name: str) -> dict[str, str]: """Return the status of the service in dictionary format.""" command = ['systemctl', 'show', service_name] - process = subprocess.run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False, stdout=subprocess.PIPE) status = {} for line in process.stdout.decode().splitlines(): parts = line.partition('=') @@ -199,8 +197,8 @@ def service_show(service_name: str) -> dict[str, str]: def service_action(service_name: str, action: str, check: bool = False): """Perform the given action on the service_name.""" - subprocess.run(['systemctl', action, service_name], - stdout=subprocess.DEVNULL, check=check) + run(['systemctl', action, service_name], stdout=subprocess.DEVNULL, + check=check) def webserver_is_enabled(name, kind='config'): @@ -440,7 +438,7 @@ Owners: {package} env['DEBCONF_DB_OVERRIDE'] = 'File{' + override_file.name + \ ' readonly:true}' env['DEBIAN_FRONTEND'] = 'noninteractive' - subprocess.run(['dpkg-reconfigure', package], env=env, check=False) + run(['dpkg-reconfigure', package], env=env, check=False) try: os.remove(override_file.name) @@ -454,7 +452,7 @@ def debconf_set_selections(presets): # Workaround Debian Bug #487300. In some situations, debconf complains # it can't find the question being answered even though it is supposed # to create a dummy question for it. - subprocess.run(['/usr/share/debconf/fix_db.pl'], check=True) + run(['/usr/share/debconf/fix_db.pl'], check=True) except (FileNotFoundError, PermissionError): pass @@ -481,8 +479,8 @@ def run_apt_command(arguments, stdout=subprocess.DEVNULL, env['DEBIAN_FRONTEND'] = 'noninteractive' if not enable_triggers: env['FREEDOMBOX_INVOKED'] = 'true' - process = subprocess.run(command, stdin=subprocess.DEVNULL, stdout=stdout, - env=env, check=False) + process = run(command, stdin=subprocess.DEVNULL, stdout=stdout, env=env, + check=False) return process.returncode @@ -506,8 +504,7 @@ def apt_hold(packages): current_hold = subprocess.check_output( ['apt-mark', 'showhold', package]) if not current_hold: - process = subprocess.run(['apt-mark', 'hold', package], - check=False) + process = run(['apt-mark', 'hold', package], check=False) if process.returncode == 0: # success held_packages.append(package) @@ -539,9 +536,8 @@ def apt_hold_freedombox(): def apt_unhold_freedombox(): """Remove any hold on freedombox package, and clear flag.""" - subprocess.run(['apt-mark', 'unhold', 'freedombox'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False) + run(['apt-mark', 'unhold', 'freedombox'], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=False) if apt_hold_flag.exists(): apt_hold_flag.unlink() @@ -568,15 +564,14 @@ def podman_create(container_name: str, image_name: str, volume_name: str, service_stop(container_name) # Data is kept - subprocess.run(['podman', 'volume', 'rm', '--force', volume_name], - check=False) + run(['podman', 'volume', 'rm', '--force', volume_name], check=False) directory = pathlib.Path('/etc/containers/systemd') directory.mkdir(parents=True, exist_ok=True) # Fetch the image before creating the container. The systemd service for # the container won't timeout due to slow internet connectivity. - subprocess.run(['podman', 'image', 'pull', image_name], check=True) + run(['podman', 'image', 'pull', image_name], check=True) pathlib.Path(volume_path).mkdir(parents=True, exist_ok=True) # Create storage volume @@ -735,10 +730,8 @@ def podman_disable(container_name: str): def podman_uninstall(container_name: str, volume_name: str, image_name: str, volume_path: str): """Remove a podman container's components and systemd unit.""" - subprocess.run(['podman', 'volume', 'rm', '--force', volume_name], - check=True) - subprocess.run(['podman', 'image', 'rm', '--ignore', image_name], - check=True) + run(['podman', 'volume', 'rm', '--force', volume_name], check=True) + run(['podman', 'image', 'rm', '--ignore', image_name], check=True) volume_file = pathlib.Path( '/etc/containers/systemd/') / f'{volume_name}.volume' volume_file.unlink(missing_ok=True) diff --git a/plinth/db/mariadb.py b/plinth/db/mariadb.py index d7680c34e..c5f52433e 100644 --- a/plinth/db/mariadb.py +++ b/plinth/db/mariadb.py @@ -6,12 +6,14 @@ Uses utilities from 'mysql-client' package such as 'mysql' and 'mysqldump'. import subprocess +from .. import action_utils + def run_query(database_name: str, query: str) -> subprocess.CompletedProcess: """Run a database query using 'root' user. Does not ensure that the database server is running. """ - return subprocess.run( + return action_utils.run( ['mysql', '--user=root', '--database', database_name], input=query.encode('utf-8'), check=True) diff --git a/plinth/db/postgres.py b/plinth/db/postgres.py index 1170c4608..1ddf618d6 100644 --- a/plinth/db/postgres.py +++ b/plinth/db/postgres.py @@ -6,7 +6,6 @@ Uses utilities from 'postgres' package such as 'psql' and 'pg_dump'. import os import pathlib -import subprocess from plinth import action_utils @@ -14,7 +13,7 @@ from plinth import action_utils def _run_as(command, **kwargs): """Run a command as 'postgres' user.""" command = ['sudo', '--user', 'postgres'] + command - return subprocess.run(command, check=True, **kwargs) + return action_utils.run(command, check=True, **kwargs) def run_query(query): diff --git a/plinth/modules/apache/privileged.py b/plinth/modules/apache/privileged.py index 2568aa82e..f58a0e791 100644 --- a/plinth/modules/apache/privileged.py +++ b/plinth/modules/apache/privileged.py @@ -5,7 +5,6 @@ import glob import os import pathlib import re -import subprocess from plinth import action_utils from plinth.actions import privileged @@ -62,14 +61,14 @@ def setup(old_version: int): # version of Apache FreedomBox app and setting up for the first time don't # regenerate. if action_utils.is_disk_image() and old_version == 0: - subprocess.run([ + action_utils.run([ 'make-ssl-cert', 'generate-default-snakeoil', '--force-overwrite' ], check=True) # In case the certificate has been removed after ssl-cert is installed # on a fresh Debian machine. elif not os.path.exists('/etc/ssl/certs/ssl-cert-snakeoil.pem'): - subprocess.run(['make-ssl-cert', 'generate-default-snakeoil'], - check=True) + action_utils.run(['make-ssl-cert', 'generate-default-snakeoil'], + check=True) with action_utils.WebserverChange() as webserver: # Disable mod_php as we have switched to mod_fcgi + php-fpm. Disable diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index f7fbdb64f..132cd4f27 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -194,7 +194,7 @@ def _is_mounted(mountpoint): cmd = ['mountpoint', '-q', mountpoint] # mountpoint exits with status non-zero if it didn't find a mountpoint try: - subprocess.run(cmd, check=True) + action_utils.run(cmd, check=True) return True except subprocess.CalledProcessError: return False diff --git a/plinth/modules/bepasty/privileged.py b/plinth/modules/bepasty/privileged.py index 7e6a186b2..11a09c5e6 100644 --- a/plinth/modules/bepasty/privileged.py +++ b/plinth/modules/bepasty/privileged.py @@ -10,7 +10,6 @@ import pwd import secrets import shutil import string -import subprocess import augeas @@ -71,13 +70,13 @@ def setup(domain_name: str): try: grp.getgrnam('bepasty') except KeyError: - subprocess.run(['addgroup', '--system', 'bepasty'], check=True) + action_utils.run(['addgroup', '--system', 'bepasty'], check=True) # Create bepasty user if needed. try: pwd.getpwnam('bepasty') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'bepasty', '--home', '/var/lib/bepasty', '--gecos', 'bepasty file sharing', 'bepasty' ], check=True) @@ -174,5 +173,5 @@ def uninstall(): """Remove bepasty user, group and data.""" shutil.rmtree(DATA_DIR, ignore_errors=True) CONF_FILE.unlink(missing_ok=True) - subprocess.run(['deluser', 'bepasty'], check=False) - subprocess.run(['delgroup', 'bepasty'], check=False) + action_utils.run(['deluser', 'bepasty'], check=False) + action_utils.run(['delgroup', 'bepasty'], check=False) diff --git a/plinth/modules/datetime/privileged.py b/plinth/modules/datetime/privileged.py index 45790405e..8e435bfcd 100644 --- a/plinth/modules/datetime/privileged.py +++ b/plinth/modules/datetime/privileged.py @@ -3,6 +3,7 @@ import subprocess +from plinth import action_utils from plinth.actions import privileged @@ -10,4 +11,4 @@ from plinth.actions import privileged def set_timezone(timezone: str): """Set time zone with timedatectl.""" command = ['timedatectl', 'set-timezone', timezone] - subprocess.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, stdout=subprocess.DEVNULL, check=True) diff --git a/plinth/modules/email/postfix.py b/plinth/modules/email/postfix.py index 1a04731cb..2f6b4a57c 100644 --- a/plinth/modules/email/postfix.py +++ b/plinth/modules/email/postfix.py @@ -11,6 +11,8 @@ import re import subprocess from dataclasses import dataclass +from plinth import action_utils + @dataclass class Service: # NOQA, pylint: disable=too-many-instance-attributes @@ -109,7 +111,7 @@ def _run(args): Raise a RuntimeError on non-zero exit codes. """ try: - result = subprocess.run(args, check=True, capture_output=True) + result = action_utils.run(args, check=True, capture_output=True) return result.stdout.decode() except subprocess.SubprocessError as subprocess_error: raise RuntimeError('Subprocess failed') from subprocess_error diff --git a/plinth/modules/email/privileged/dkim.py b/plinth/modules/email/privileged/dkim.py index d3b9273c6..62195a5d9 100644 --- a/plinth/modules/email/privileged/dkim.py +++ b/plinth/modules/email/privileged/dkim.py @@ -9,6 +9,7 @@ import re import shutil import subprocess +from plinth import action_utils from plinth.actions import privileged from plinth.privileged import service as service_privileged @@ -54,7 +55,7 @@ def setup_dkim(domain: str): # Ed25519 is widely *not* accepted as of 2022-01. See: # https://serverfault.com/questions/1023674 - subprocess.run([ + action_utils.run([ 'rspamadm', 'dkim_keygen', '-t', 'rsa', '-b', '2048', '-s', 'dkim', '-d', domain, '-k', (str(key_file)) ], check=True) diff --git a/plinth/modules/email/privileged/home.py b/plinth/modules/email/privileged/home.py index 544643dc3..05a9f974e 100644 --- a/plinth/modules/email/privileged/home.py +++ b/plinth/modules/email/privileged/home.py @@ -6,8 +6,7 @@ See: https://doc.dovecot.org/configuration_manual/authentication/user_databases_userdb/ """ -import subprocess - +from plinth import action_utils from plinth.actions import privileged @@ -19,4 +18,4 @@ def setup_home(): Dovecot creates new directories with the same permissions as the parent directory. Ensure that 'others' can't access /var/mail/. """ - subprocess.run(['chmod', 'o-rwx', '/var/mail'], check=True) + action_utils.run(['chmod', 'o-rwx', '/var/mail'], check=True) diff --git a/plinth/modules/email/privileged/spam.py b/plinth/modules/email/privileged/spam.py index c42331393..6205f2d7f 100644 --- a/plinth/modules/email/privileged/spam.py +++ b/plinth/modules/email/privileged/spam.py @@ -10,8 +10,8 @@ For testing DKIM signatures: https://www.mail-tester.com/ import pathlib import re -import subprocess +from plinth import action_utils from plinth.actions import privileged from plinth.modules.email import postfix @@ -31,10 +31,11 @@ def setup_spam(): def _compile_sieve(): """Compile all .sieve script to binary format for performance.""" - sieve_dirs = ['/etc/dovecot/freedombox-sieve-after/', - '/etc/dovecot/freedombox-sieve'] + sieve_dirs = [ + '/etc/dovecot/freedombox-sieve-after/', '/etc/dovecot/freedombox-sieve' + ] for sieve_dir in sieve_dirs: - subprocess.run(['sievec', sieve_dir], check=True) + action_utils.run(['sievec', sieve_dir], check=True) def _setup_rspamd(): diff --git a/plinth/modules/firewall/privileged.py b/plinth/modules/firewall/privileged.py index 605c7e948..33f0dd7bf 100644 --- a/plinth/modules/firewall/privileged.py +++ b/plinth/modules/firewall/privileged.py @@ -36,10 +36,10 @@ def _flush_iptables_rules(): iptables_rules += rule_template.format(table=table) ip6tables_rules += rule_template.format(table=table) - subprocess.run(['iptables-restore'], input=iptables_rules.encode(), - check=True) - subprocess.run(['ip6tables-restore'], input=iptables_rules.encode(), - check=True) + action_utils.run(['iptables-restore'], input=iptables_rules.encode(), + check=True) + action_utils.run(['ip6tables-restore'], input=iptables_rules.encode(), + check=True) def set_firewall_backend(backend): @@ -67,8 +67,8 @@ def set_firewall_backend(backend): def _run_firewall_cmd(args): """Run firewall-cmd command, discard output and check return value.""" - subprocess.run(['firewall-cmd'] + args, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['firewall-cmd'] + args, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) def _setup_local_service_protection(): @@ -160,9 +160,9 @@ def _setup_inter_zone_forwarding(): def setup(): """Perform basic firewalld setup.""" action_utils.service_enable('firewalld') - subprocess.run(['firewall-cmd', '--set-default-zone=external'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=True) + action_utils.run(['firewall-cmd', '--set-default-zone=external'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=True) set_firewall_backend('nftables') _setup_local_service_protection() diff --git a/plinth/modules/ikiwiki/privileged.py b/plinth/modules/ikiwiki/privileged.py index fda1eca9b..798c8581b 100644 --- a/plinth/modules/ikiwiki/privileged.py +++ b/plinth/modules/ikiwiki/privileged.py @@ -7,6 +7,7 @@ import re import shutil import subprocess +from plinth import action_utils from plinth.actions import privileged, secret_str SETUP_WIKI = '/etc/ikiwiki/plinth-wiki.setup' @@ -61,10 +62,10 @@ def create_wiki(wiki_name: str, admin_name: str, admin_password: secret_str): """Create a wiki.""" pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes - subprocess.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, - env=dict(os.environ, PERL_UNICODE='AS'), check=True) + action_utils.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], + stdout=subprocess.PIPE, input=input_, + stderr=subprocess.PIPE, + env=dict(os.environ, PERL_UNICODE='AS'), check=True) @privileged @@ -72,17 +73,17 @@ def create_blog(blog_name: str, admin_name: str, admin_password: secret_str): """Create a blog.""" pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes - subprocess.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, env=dict(os.environ, - PERL_UNICODE='AS')) + action_utils.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], + stdout=subprocess.PIPE, input=input_, + stderr=subprocess.PIPE, env=dict(os.environ, + PERL_UNICODE='AS')) @privileged def setup_site(site_name: str): """Run setup for a site.""" setup_path = os.path.join(WIKI_PATH, site_name + '.setup') - subprocess.run(['ikiwiki', '-setup', setup_path], check=True) + action_utils.run(['ikiwiki', '-setup', setup_path], check=True) @privileged diff --git a/plinth/modules/infinoted/privileged.py b/plinth/modules/infinoted/privileged.py index c37f4680e..e79c3cef7 100644 --- a/plinth/modules/infinoted/privileged.py +++ b/plinth/modules/infinoted/privileged.py @@ -9,6 +9,7 @@ import shutil import subprocess import time +from plinth import action_utils from plinth.actions import privileged DATA_DIR = '/var/lib/infinoted' @@ -105,7 +106,7 @@ def _kill_daemon(): end_time = time.time() + 300 while time.time() < end_time: try: - subprocess.run(['infinoted', '--kill-daemon'], check=True) + action_utils.run(['infinoted', '--kill-daemon'], check=True) break except subprocess.CalledProcessError: pass @@ -129,13 +130,13 @@ def setup(): try: grp.getgrnam('infinoted') except KeyError: - subprocess.run(['addgroup', '--system', 'infinoted'], check=True) + action_utils.run(['addgroup', '--system', 'infinoted'], check=True) # Create infinoted user if needed. try: pwd.getpwnam('infinoted') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'infinoted', '--home', DATA_DIR, '--gecos', 'Infinoted collaborative editing server', 'infinoted' @@ -151,7 +152,7 @@ def setup(): try: # infinoted doesn't have a "create key and exit" mode. Run as # daemon so we can stop after. - subprocess.run([ + action_utils.run([ 'infinoted', '--create-key', '--create-certificate', '--daemonize' ], check=True) diff --git a/plinth/modules/letsencrypt/privileged.py b/plinth/modules/letsencrypt/privileged.py index 0993c02d4..3002a0ef8 100644 --- a/plinth/modules/letsencrypt/privileged.py +++ b/plinth/modules/letsencrypt/privileged.py @@ -115,7 +115,7 @@ def revoke(domain: str): if TEST_MODE: command.append('--staging') - subprocess.run(command, check=True) + action_utils.run(command, check=True) action_utils.webserver_disable(domain, kind='site') @@ -132,7 +132,7 @@ def obtain(domain: str): if TEST_MODE: command.append('--staging') - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged @@ -249,5 +249,5 @@ def _assert_managed_path(module, path): def delete(domain: str): """Disable a domain and delete the certificate.""" command = ['certbot', 'delete', '--non-interactive', '--cert-name', domain] - subprocess.run(command, check=True) + action_utils.run(command, check=True) action_utils.webserver_disable(domain, kind='site') diff --git a/plinth/modules/mediawiki/privileged.py b/plinth/modules/mediawiki/privileged.py index fa01c2b58..1035e6328 100644 --- a/plinth/modules/mediawiki/privileged.py +++ b/plinth/modules/mediawiki/privileged.py @@ -7,6 +7,7 @@ import shutil import subprocess import tempfile +from plinth import action_utils from plinth.actions import privileged, secret_str from plinth.utils import generate_password @@ -26,8 +27,8 @@ def get_php_command(): version = '' try: - process = subprocess.run(['dpkg', '-s', 'php'], stdout=subprocess.PIPE, - check=True) + process = action_utils.run(['dpkg', '-s', 'php'], + stdout=subprocess.PIPE, check=True) for line in process.stdout.decode().splitlines(): if line.startswith('Version:'): version = line.split(':')[-1].split('+')[0].strip() @@ -57,8 +58,9 @@ def setup(): '--scriptpath=/mediawiki', '--passfile', password_file_handle.name, 'Wiki', 'admin' ]) - subprocess.run(['chmod', '-R', 'o-rwx', data_dir], check=True) - subprocess.run(['chown', '-R', 'www-data:www-data', data_dir], check=True) + action_utils.run(['chmod', '-R', 'o-rwx', data_dir], check=True) + action_utils.run(['chown', '-R', 'www-data:www-data', data_dir], + check=True) conf_file = pathlib.Path(CONF_FILE) if not conf_file.exists(): diff --git a/plinth/modules/minidlna/privileged.py b/plinth/modules/minidlna/privileged.py index 2fba83493..e57058770 100644 --- a/plinth/modules/minidlna/privileged.py +++ b/plinth/modules/minidlna/privileged.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Configure minidlna server.""" -import subprocess from os import chmod, fdopen, remove, stat from shutil import move from tempfile import mkstemp @@ -51,7 +50,7 @@ def setup(): encoding='utf-8') as conf: conf.write(SYSCTL_CONF) - subprocess.run(['systemctl', 'restart', 'systemd-sysctl'], check=True) + action_utils.run(['systemctl', 'restart', 'systemd-sysctl'], check=True) @privileged diff --git a/plinth/modules/mumble/privileged.py b/plinth/modules/mumble/privileged.py index 759c0eab3..1469f6d55 100644 --- a/plinth/modules/mumble/privileged.py +++ b/plinth/modules/mumble/privileged.py @@ -37,8 +37,8 @@ def check_setup() -> bool: @privileged def set_super_user_password(password: secret_str): """Set the superuser password with murmurd command.""" - subprocess.run(['murmurd', '-readsupw'], input=password.encode(), - stdout=subprocess.DEVNULL, check=False) + action_utils.run(['murmurd', '-readsupw'], input=password.encode(), + stdout=subprocess.DEVNULL, check=False) @privileged diff --git a/plinth/modules/names/privileged.py b/plinth/modules/names/privileged.py index 37804eebc..f9f7fa248 100644 --- a/plinth/modules/names/privileged.py +++ b/plinth/modules/names/privileged.py @@ -2,7 +2,6 @@ """Configure Names App.""" import pathlib -import subprocess import augeas @@ -22,7 +21,7 @@ HOSTS_LOCAL_IP = '127.0.1.1' @privileged def set_hostname(hostname: str): """Set system hostname using hostnamectl.""" - subprocess.run( + action_utils.run( ['hostnamectl', 'set-hostname', '--transient', '--static', hostname], check=True) action_utils.service_restart('avahi-daemon') @@ -83,7 +82,7 @@ def domain_delete_all(): def install_resolved(): """Install systemd-resolved related packages.""" packages = ['systemd-resolved', 'libnss-resolve'] - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): action_utils.run_apt_command(['--fix-broken', 'install']) returncode = action_utils.run_apt_command(['install'] + packages) diff --git a/plinth/modules/networks/privileged.py b/plinth/modules/networks/privileged.py index 2ea77518e..4f671a97b 100644 --- a/plinth/modules/networks/privileged.py +++ b/plinth/modules/networks/privileged.py @@ -52,7 +52,7 @@ def _add_connection(connection_name: str, interface: str, logging.info('Connection %s already exists for device %s, not adding.', connection_name, interface) else: - subprocess.run([ + action_utils.run([ 'nmcli', 'con', 'add', 'con-name', connection_name, 'ifname', interface ] + remaining_arguments, check=True) @@ -108,8 +108,9 @@ def _set_connection_properties(connection_name: str, properties: dict[str, str]): """Configure property key/values on a connection.""" for key, value in properties.items(): - subprocess.run(['nmcli', 'con', 'modify', connection_name, key, value], - check=True) + action_utils.run( + ['nmcli', 'con', 'modify', connection_name, key, value], + check=True) def _configure_wireless_interface(interface: str): diff --git a/plinth/modules/nextcloud/privileged.py b/plinth/modules/nextcloud/privileged.py index f7da36d57..51813bf43 100644 --- a/plinth/modules/nextcloud/privileged.py +++ b/plinth/modules/nextcloud/privileged.py @@ -79,7 +79,8 @@ def _run_in_container( env_args = [f'--env={key}={value}' for key, value in (env or {}).items()] command = ['podman', 'exec', '--user', WWW_DATA_UID ] + env_args + [CONTAINER_NAME] + list(args) - return subprocess.run(command, capture_output=capture_output, check=check) + return action_utils.run(command, capture_output=capture_output, + check=check) def _run_occ(*args, **kwargs) -> subprocess.CompletedProcess: @@ -174,7 +175,7 @@ def set_default_phone_region(region: str): def _database_query(query: str): """Run a database query.""" - subprocess.run(['mysql'], input=query.encode(), check=True) + action_utils.run(['mysql'], input=query.encode(), check=True) def _create_database(): @@ -239,7 +240,7 @@ def _nextcloud_wait_until_ready(): # obtaining. We are unable to obtain the lock for 5 minutes, fail and stop # the setup process. lock_file = _data_path / 'nextcloud-init-sync.lock' - subprocess.run( + action_utils.run( ['flock', '--exclusive', '--wait', '300', lock_file, 'echo'], check=True) @@ -362,7 +363,7 @@ def dump_database(): with _maintenance_mode(): with DB_BACKUP_FILE.open('w', encoding='utf-8') as file_handle: - subprocess.run([ + action_utils.run([ 'mysqldump', '--add-drop-database', '--add-drop-table', '--add-drop-trigger', '--single-transaction', '--default-character-set=utf8mb4', '--user', 'root', @@ -374,11 +375,11 @@ def dump_database(): def restore_database(): """Restore database from file.""" with DB_BACKUP_FILE.open('r', encoding='utf-8') as file_handle: - subprocess.run(['mysql', '--user', 'root'], stdin=file_handle, - check=True) + action_utils.run(['mysql', '--user', 'root'], stdin=file_handle, + check=True) - subprocess.run(['redis-cli', '-n', - str(REDIS_DB), 'FLUSHDB', 'SYNC'], check=False) + action_utils.run(['redis-cli', '-n', + str(REDIS_DB), 'FLUSHDB', 'SYNC'], check=False) _set_database_privileges(_get_database_password()) diff --git a/plinth/modules/openvpn/privileged.py b/plinth/modules/openvpn/privileged.py index 7ec0d98b1..50f64c722 100644 --- a/plinth/modules/openvpn/privileged.py +++ b/plinth/modules/openvpn/privileged.py @@ -110,7 +110,7 @@ def _setup_firewall(): def _is_tunplus_enabled(): """Return whether tun+ interface is already added.""" try: - process = subprocess.run( + process = action_utils.run( ['firewall-cmd', '--zone', 'internal', '--list-interfaces'], stdout=subprocess.PIPE, check=True) return 'tun+' in process.stdout.decode().strip().split() @@ -135,8 +135,8 @@ def _setup_firewall(): def _run_easy_rsa(args): """Execute easy-rsa command with some default arguments.""" - return subprocess.run(['/usr/share/easy-rsa/easyrsa'] + args, - cwd=KEYS_DIRECTORY, check=True) + return action_utils.run(['/usr/share/easy-rsa/easyrsa'] + args, + cwd=KEYS_DIRECTORY, check=True) def _write_easy_rsa_config(): @@ -162,7 +162,7 @@ def _is_renewable(cert_name): if not cert_path.exists(): return False - process = subprocess.run( + process = action_utils.run( ['openssl', 'x509', '-noout', '-enddate', '-in', str(cert_path)], check=True, stdout=subprocess.PIPE) date_string = process.stdout.decode().strip().partition('=')[2] diff --git a/plinth/modules/snapshot/privileged.py b/plinth/modules/snapshot/privileged.py index 4691003c4..ec3385b09 100644 --- a/plinth/modules/snapshot/privileged.py +++ b/plinth/modules/snapshot/privileged.py @@ -21,13 +21,13 @@ def setup(old_version: int): """Configure snapper.""" # Check if root config exists. command = ['snapper', 'list-configs'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, stdout=subprocess.PIPE, check=True) output = process.stdout.decode() # Create root config if needed. if 'root' not in output: command = ['snapper', 'create-config', '/'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) if old_version and old_version <= 4: _remove_fstab_entry('/') @@ -76,7 +76,7 @@ def _migrate_config_from_version_3(): 'EMPTY_PRE_POST_MIN_AGE=0', 'FREE_LIMIT=0.3', ] - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _set_default_config(): @@ -98,7 +98,7 @@ def _set_default_config(): 'EMPTY_PRE_POST_MIN_AGE=0', 'FREE_LIMIT=0.3', ] - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _remove_fstab_entry(mount_point): @@ -137,16 +137,17 @@ def _remove_fstab_entry(mount_point): def _systemd_path_escape(path): """Escape a string using systemd path rules.""" - process = subprocess.run(['systemd-escape', '--path', path], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['systemd-escape', '--path', path], + stdout=subprocess.PIPE, check=True) return process.stdout.decode().strip() def _get_subvolume_path(mount_point): """Return the subvolume path for .snapshots in a filesystem.""" # -o causes the list of subvolumes directly under the given mount point - process = subprocess.run(['btrfs', 'subvolume', 'list', '-o', mount_point], - stdout=subprocess.PIPE, check=True) + process = action_utils.run( + ['btrfs', 'subvolume', 'list', '-o', mount_point], + stdout=subprocess.PIPE, check=True) for line in process.stdout.decode().splitlines(): entry = line.split() @@ -223,8 +224,8 @@ def _parse_number(number): @privileged def list_() -> list[dict[str, str]]: """List snapshots.""" - process = subprocess.run(['snapper', 'list'], stdout=subprocess.PIPE, - check=True) + process = action_utils.run(['snapper', 'list'], stdout=subprocess.PIPE, + check=True) lines = process.stdout.decode().splitlines() keys = ('number', 'is_default', 'is_active', 'type', 'pre_number', 'date', @@ -246,7 +247,7 @@ def list_() -> list[dict[str, str]]: def _get_default_snapshot(): """Return the default snapshot by looking at default subvolume.""" command = ['btrfs', 'subvolume', 'get-default', '/'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, stdout=subprocess.PIPE, check=True) output = process.stdout.decode() output_parts = output.split() @@ -277,26 +278,26 @@ def disable_apt_snapshot(state: str): def create(): """Create snapshot.""" command = ['snapper', 'create', '--description', 'manually created'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged def delete(number: str): """Delete a snapshot by number.""" command = ['snapper', 'delete', number] - subprocess.run(command, check=True) + action_utils.run(command, check=True) @privileged def set_config(config: list[str]): """Set snapper configuration.""" command = ['snapper', 'set-config'] + config - subprocess.run(command, check=True) + action_utils.run(command, check=True) def _get_config(): command = ['snapper', 'get-config'] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, stdout=subprocess.PIPE, check=True) lines = process.stdout.decode().splitlines() config = {} for line in lines[2:]: @@ -345,4 +346,4 @@ def rollback(number: str): # behavior when a snapshot number to rollback to is provided is the # behavior that we desire. command = ['snapper', '--ambit', 'classic', 'rollback', number] - subprocess.run(command, check=True) + action_utils.run(command, check=True) diff --git a/plinth/modules/sogo/privileged.py b/plinth/modules/sogo/privileged.py index 39b6a198c..1767ee525 100644 --- a/plinth/modules/sogo/privileged.py +++ b/plinth/modules/sogo/privileged.py @@ -7,7 +7,7 @@ import shutil import subprocess import tempfile -from plinth import utils +from plinth import action_utils, utils from plinth.actions import privileged from plinth.db import postgres from plinth.modules.email.privileged.domain import \ @@ -144,8 +144,8 @@ def set_domain(domain: str): def _get_config_value(key: str) -> str: """Return the value of a property from the configuration file.""" - process = subprocess.run(['plget', key], input=CONFIG_FILE.read_bytes(), - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['plget', key], input=CONFIG_FILE.read_bytes(), + stdout=subprocess.PIPE, check=True) return process.stdout.decode().strip() @@ -154,7 +154,7 @@ def _set_config_value(key: str, value: str): with tempfile.NamedTemporaryFile(delete=False) as temp_file: temp_file.write(f'{{\n{key} = "{value}";\n}}'.encode('utf-8')) temp_file.close() - subprocess.run(['plmerge', CONFIG_FILE, temp_file.name], check=True) + action_utils.run(['plmerge', CONFIG_FILE, temp_file.name], check=True) pathlib.Path(temp_file.name).unlink() diff --git a/plinth/modules/storage/privileged.py b/plinth/modules/storage/privileged.py index dc156fa3a..234739ba5 100644 --- a/plinth/modules/storage/privileged.py +++ b/plinth/modules/storage/privileged.py @@ -46,7 +46,7 @@ def _move_gpt_second_header(device): """ command = ['sgdisk', '--move-second-header', device] try: - subprocess.run(command, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError: raise RuntimeError('Error moving GPT second header to the end') @@ -65,12 +65,12 @@ def _resize_partition(device, requested_partition, free_space): 'B', 'resizepart', requested_partition['number'] ] try: - subprocess.run(command, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError: try: input_text = 'yes\n' + str(free_space['end']) - subprocess.run(fallback_command, check=True, - input=input_text.encode()) + action_utils.run(fallback_command, check=True, + input=input_text.encode()) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding partition: {exception}') @@ -90,8 +90,8 @@ def _resize_ext4(device, requested_partition, _free_space, _mount_point): requested_partition['number']) try: command = ['resize2fs', partition_device] - subprocess.run(command, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(command, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -100,7 +100,7 @@ def _resize_btrfs(_device, _requested_partition, _free_space, mount_point='/'): """Resize a btrfs file system inside a partition.""" try: command = ['btrfs', 'filesystem', 'resize', 'max', mount_point] - subprocess.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, stdout=subprocess.DEVNULL, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -167,7 +167,7 @@ def _get_partitions_and_free_spaces(device, partition_number): command = [ 'parted', '--machine', '--script', device, 'unit', 'B', 'print', 'free' ] - process = subprocess.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, stdout=subprocess.PIPE, check=True) requested_partition = None free_spaces = [] @@ -215,7 +215,7 @@ def mount(block_device: str): UDISKS_FILESYSTEM_SHARED=1 by writing a udev rule. """ - subprocess.run([ + action_utils.run([ 'udisksctl', 'mount', '--block-device', block_device, '--no-user-interaction' ], check=True) diff --git a/plinth/modules/syncthing/privileged.py b/plinth/modules/syncthing/privileged.py index 6180c353d..5359a09d6 100644 --- a/plinth/modules/syncthing/privileged.py +++ b/plinth/modules/syncthing/privileged.py @@ -5,7 +5,6 @@ import grp import os import pwd import shutil -import subprocess import time import augeas @@ -37,13 +36,13 @@ def setup(): try: grp.getgrnam('syncthing') except KeyError: - subprocess.run(['addgroup', '--system', 'syncthing'], check=True) + action_utils.run(['addgroup', '--system', 'syncthing'], check=True) # Create syncthing user if needed. try: pwd.getpwnam('syncthing') except KeyError: - subprocess.run([ + action_utils.run([ 'adduser', '--system', '--ingroup', 'syncthing', '--home', DATA_DIR, '--gecos', 'Syncthing file synchronization server', 'syncthing' diff --git a/plinth/modules/tor/privileged.py b/plinth/modules/tor/privileged.py index b2b4847f2..17ca0bebe 100644 --- a/plinth/modules/tor/privileged.py +++ b/plinth/modules/tor/privileged.py @@ -8,7 +8,6 @@ import pathlib import re import shutil import socket -import subprocess import time from typing import Any @@ -54,7 +53,7 @@ def _first_time_setup(): """Setup Tor configuration for the first time setting defaults.""" logger.info('Performing first time setup for Tor') - subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True) + action_utils.run(['tor-instance-create', INSTANCE_NAME], check=True) # Remove line starting with +SocksPort, since our augeas lens # doesn't handle it correctly. diff --git a/plinth/modules/torproxy/privileged.py b/plinth/modules/torproxy/privileged.py index 8ce68cc2a..883df4a79 100644 --- a/plinth/modules/torproxy/privileged.py +++ b/plinth/modules/torproxy/privileged.py @@ -4,7 +4,6 @@ import logging import os import shutil -import subprocess from typing import Any import augeas @@ -31,7 +30,7 @@ def setup(): # Mask the service to prevent re-enabling it by the Tor master service. action_utils.service_mask('tor@default') - subprocess.run(['tor-instance-create', INSTANCE_NAME], check=True) + action_utils.run(['tor-instance-create', INSTANCE_NAME], check=True) # Remove line starting with +SocksPort, since our augeas lens # doesn't handle it correctly. diff --git a/plinth/modules/upgrades/distupgrade.py b/plinth/modules/upgrades/distupgrade.py index ce95eff6f..0e978c6d6 100644 --- a/plinth/modules/upgrades/distupgrade.py +++ b/plinth/modules/upgrades/distupgrade.py @@ -5,7 +5,6 @@ import contextlib import datetime import logging import pathlib -import subprocess from datetime import timezone from typing import Generator @@ -218,11 +217,11 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]: try: logger.info('Taking a snapshot before dist upgrade...') command = ['snapper', 'create', '--description', 'before dist-upgrade'] - subprocess.run(command, check=True) + action_utils.run(command, check=True) aug = snapshot_module.load_augeas() if snapshot_module.is_apt_snapshots_enabled(aug): logger.info('Disabling apt snapshots during dist upgrade...') - subprocess.run([ + action_utils.run([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot', @@ -235,7 +234,7 @@ def _snapshot_run_and_disable() -> Generator[None, None, None]: finally: if reenable: logger.info('Re-enabling apt snapshots...') - subprocess.run([ + action_utils.run([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot' ], input='{"args": ["no"], "kwargs": {}}'.encode(), check=True) else: @@ -303,7 +302,7 @@ def _apt_update(): def _apt_fix(): """Try to fix any problems with apt/dpkg before the upgrade.""" logger.info('Fixing any broken apt/dpkg states...') - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) _apt_run(['--fix-broken', 'install']) @@ -341,7 +340,7 @@ def _unattended_upgrades_run(): To handle upgrading the freedombox package. """ logger.info('Running unattended-upgrade...') - subprocess.run(['unattended-upgrade', '--verbose'], check=False) + action_utils.run(['unattended-upgrade', '--verbose'], check=False) def _freedombox_restart(): @@ -360,7 +359,7 @@ def _trigger_on_complete(): # file will not be possible. For that, we need to launch a new process with # a different systemd service (which does not have the bind mounts). logger.info('Triggering on-complete to commit sources.lists') - subprocess.run([ + action_utils.run([ 'systemd-run', '--unit=freedombox-dist-upgrade-on-complete', '--description=Finish up upgrade to new stable Debian release', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete', @@ -417,7 +416,7 @@ def start_service(): '--property=KillMode=process', '--property=TimeoutSec=72hr', f'--property=BindPaths={temp_sources_list}:{sources_list}' ] - subprocess.run(['systemd-run'] + args + [ + action_utils.run(['systemd-run'] + args + [ 'systemd-inhibit', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade', '--no-args' ], check=True) diff --git a/plinth/modules/upgrades/privileged.py b/plinth/modules/upgrades/privileged.py index ad107cdfa..8629f5345 100644 --- a/plinth/modules/upgrades/privileged.py +++ b/plinth/modules/upgrades/privileged.py @@ -7,6 +7,7 @@ import pathlib import re import subprocess +from plinth import action_utils from plinth.action_utils import (apt_hold_flag, apt_unhold_freedombox, is_package_manager_busy, run_apt_command, service_is_running) @@ -130,14 +131,14 @@ def release_held_packages(): output = subprocess.check_output(['apt-mark', 'showhold']).decode().strip() holds = output.split('\n') logger.info('Releasing package holds: %s', holds) - subprocess.run(['apt-mark', 'unhold', *holds], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['apt-mark', 'unhold', *holds], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) @privileged def run(): """Run unattended-upgrades.""" - subprocess.run(['dpkg', '--configure', '-a'], check=False) + action_utils.run(['dpkg', '--configure', '-a'], check=False) run_apt_command(['--fix-broken', 'install']) _release_held_freedombox() diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index 8852de9f8..1d3fc1e52 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -219,7 +219,7 @@ def test_snapshot_run_and_disable(is_supported, is_apt_snapshots_enabled, run): with distupgrade._snapshot_run_and_disable(): assert run.call_args_list == [ call(['snapper', 'create', '--description', 'before dist-upgrade'], - check=True) + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) ] run.reset_mock() @@ -230,16 +230,18 @@ def test_snapshot_run_and_disable(is_supported, is_apt_snapshots_enabled, run): with distupgrade._snapshot_run_and_disable(): assert run.call_args_list == [ call(['snapper', 'create', '--description', 'before dist-upgrade'], - check=True), + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True), call([ '/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot' - ], input=b'{"args": ["yes"], "kwargs": {}}', check=True) + ], input=b'{"args": ["yes"], "kwargs": {}}', + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) ] run.reset_mock() assert run.call_args_list == [ call(['/usr/bin/freedombox-cmd', 'snapshot', 'disable_apt_snapshot'], - input=b'{"args": ["no"], "kwargs": {}}', check=True) + input=b'{"args": ["no"], "kwargs": {}}', stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True) ] @@ -278,8 +280,10 @@ def test_apt_hold_packages(check_output, check_call, run, tmp_path): expected_call = [call(['apt-mark', 'hold', 'freedombox'])] assert check_call.call_args_list == expected_call expected_calls = [ - call(['apt-mark', 'hold', 'package1'], check=False), - call(['apt-mark', 'hold', 'package2'], check=False) + call(['apt-mark', 'hold', 'package1'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False), + call(['apt-mark', 'hold', 'package2'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) ] assert run.call_args_list == expected_calls check_call.reset_mock() @@ -340,7 +344,8 @@ def test_apt_fix(run, apt_run): """Test that apt fixes work.""" distupgrade._apt_fix() assert run.call_args_list == [ - call(['dpkg', '--configure', '-a'], check=False) + call(['dpkg', '--configure', '-a'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False) ] assert apt_run.call_args_list == [call(['--fix-broken', 'install'])] @@ -365,7 +370,9 @@ def test_apt_full_upgrade(apt_run): def test_unatteneded_upgrades_run(run): """Test that running unattended upgrades works.""" distupgrade._unattended_upgrades_run() - run.assert_called_with(['unattended-upgrade', '--verbose'], check=False) + run.assert_called_with(['unattended-upgrade', '--verbose'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) @patch('plinth.action_utils.service_restart') @@ -384,7 +391,7 @@ def test_trigger_on_complete(run): '--description=Finish up upgrade to new stable Debian release', '/usr/bin/freedombox-cmd', 'upgrades', 'dist_upgrade_on_complete', '--no-args' - ], check=True) + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) def test_on_complete(tmp_path): diff --git a/plinth/modules/users/privileged.py b/plinth/modules/users/privileged.py index 3d75043ca..3bb2670fc 100644 --- a/plinth/modules/users/privileged.py +++ b/plinth/modules/users/privileged.py @@ -68,7 +68,7 @@ def first_setup(): def setup(): """Setup LDAP.""" # Update pam config for mkhomedir. - subprocess.run(['pam-auth-update', '--package'], check=True) + action_utils.run(['pam-auth-update', '--package'], check=True) _configure_ldapscripts() @@ -145,7 +145,7 @@ def _create_organizational_unit(unit): """Create an organizational unit in LDAP.""" distinguished_name = 'ou={unit},dc=thisbox'.format(unit=unit) try: - subprocess.run([ + action_utils.run([ 'ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', distinguished_name, '(objectclass=*)' ], stdout=subprocess.DEVNULL, check=True) @@ -156,14 +156,14 @@ dn: ou={unit},dc=thisbox objectClass: top objectClass: organizationalUnit ou: {unit}'''.format(unit=unit) - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - input=input.encode(), stdout=subprocess.DEVNULL, - check=True) + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], + input=input.encode(), stdout=subprocess.DEVNULL, check=True) def _setup_admin(): """Remove LDAP admin password and Allow root to modify the users.""" - process = subprocess.run([ + process = action_utils.run([ 'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config', '(objectclass=*)', 'olcRootDN', 'olcRootPW' @@ -175,7 +175,7 @@ def _setup_admin(): ldap_object[line[0]] = line[1] if 'olcRootPW' in ldap_object: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, stdout=subprocess.DEVNULL, input=b''' dn: olcDatabase={1}mdb,cn=config @@ -184,7 +184,7 @@ delete: olcRootPW''') root_dn = 'gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth' if ldap_object['olcRootDN'] != root_dn: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, stdout=subprocess.DEVNULL, input=b''' dn: olcDatabase={1}mdb,cn=config @@ -205,7 +205,7 @@ def _setup_ldap_ppolicy() -> bool: """ # Load ppolicy module try: - subprocess.run( + action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, stdout=subprocess.DEVNULL, input=b''' dn: cn=module{0},cn=config @@ -218,7 +218,7 @@ olcModuleLoad: ppolicy''') # Add namedobject schema needed for 'objectClass: namedPolicy'. try: - subprocess.run([ + action_utils.run([ 'ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-f', '/etc/ldap/schema/namedobject.ldif' ], check=True, stdout=subprocess.DEVNULL) @@ -228,8 +228,9 @@ olcModuleLoad: ppolicy''') # Set up default password policy try: - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, + stdout=subprocess.DEVNULL, input=b''' dn: cn=DefaultPPolicy,ou=policies,dc=thisbox cn: DefaultPPolicy objectClass: pwdPolicy @@ -243,8 +244,9 @@ pwdLockout: TRUE''') # Make DefaultPPolicy as a default ppolicy overlay try: - subprocess.run(['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + action_utils.run( + ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, + stdout=subprocess.DEVNULL, input=b''' dn: olcOverlay={0}ppolicy,olcDatabase={1}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig @@ -463,9 +465,9 @@ def _set_samba_user(username, password): If a user already exists, update password. """ - proc = subprocess.run(['smbpasswd', '-a', '-s', username], - input='{0}\n{0}\n'.format(password).encode(), - stderr=subprocess.PIPE, check=False) + proc = action_utils.run(['smbpasswd', '-a', '-s', username], + input='{0}\n{0}\n'.format(password).encode(), + stderr=subprocess.PIPE, check=False) if proc.returncode != 0: raise RuntimeError('Unable to add Samba user: ', proc.stderr) @@ -684,7 +686,7 @@ def set_user_status(username: str, status: str, auth_user: str, if status == 'inactive': # Kill all user processes. This includes disconnectiong ssh, samba and # cockpit sessions. - subprocess.run(['pkill', '--signal', 'KILL', '--uid', username]) + action_utils.run(['pkill', '--signal', 'KILL', '--uid', username]) def _upgrade_inactivate_users(usernames: list[str]): @@ -695,7 +697,7 @@ def _upgrade_inactivate_users(usernames: list[str]): _flush_cache() for username in usernames: - subprocess.run(['pkill', '--signal', 'KILL', '--uid', username]) + action_utils.run(['pkill', '--signal', 'KILL', '--uid', username]) def _flush_cache(): @@ -708,4 +710,4 @@ def _run(arguments, check=True, **kwargs): env = dict(os.environ, LDAPSCRIPTS_CONF=LDAPSCRIPTS_CONF) kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL) kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL) - return subprocess.run(arguments, env=env, check=check, **kwargs) + return action_utils.run(arguments, env=env, check=check, **kwargs) diff --git a/plinth/modules/wordpress/privileged.py b/plinth/modules/wordpress/privileged.py index 0b5c92f72..23fd7b994 100644 --- a/plinth/modules/wordpress/privileged.py +++ b/plinth/modules/wordpress/privileged.py @@ -6,7 +6,6 @@ import pathlib import random import shutil import string -import subprocess import augeas @@ -90,8 +89,8 @@ def _create_database(db_name): # Wordpress' install.php creates the tables. # SQL injection is avoided due to known input. query = f'''CREATE DATABASE {db_name};''' - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=True) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=True) def _set_privileges(db_host, db_name, db_user, db_password): @@ -103,8 +102,8 @@ def _set_privileges(db_host, db_name, db_user, db_password): IDENTIFIED BY '{db_password}'; FLUSH PRIVILEGES; ''' - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=True) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=True) def _generate_secret_key(length=64, chars=None): @@ -146,7 +145,7 @@ def dump_database(): _db_backup_file.parent.mkdir(parents=True, exist_ok=True) with action_utils.service_ensure_running('mysql'): with _db_backup_file.open('w', encoding='utf-8') as file_handle: - subprocess.run([ + action_utils.run([ 'mysqldump', '--add-drop-database', '--add-drop-table', '--add-drop-trigger', '--user', 'root', '--databases', DB_NAME ], stdout=file_handle, check=True) @@ -157,8 +156,8 @@ def restore_database(): """Restore database from file.""" with action_utils.service_ensure_running('mysql'): with _db_backup_file.open('r', encoding='utf-8') as file_handle: - subprocess.run(['mysql', '--user', 'root'], stdin=file_handle, - check=True) + action_utils.run(['mysql', '--user', 'root'], stdin=file_handle, + check=True) _set_privileges(DB_HOST, DB_NAME, DB_USER, _read_db_password()) @@ -192,9 +191,9 @@ def _drop_database(db_host, db_name, db_user): """Drop the mysql database that was created during install.""" with action_utils.service_ensure_running('mysql'): query = f"DROP DATABASE {db_name};" - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=False) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=False) query = f"DROP USER IF EXISTS {db_user}@{db_host};" - subprocess.run(['mysql', '--user', 'root'], input=query.encode(), - check=False) + action_utils.run(['mysql', '--user', 'root'], input=query.encode(), + check=False) diff --git a/plinth/modules/zoph/privileged.py b/plinth/modules/zoph/privileged.py index 6ed2eefe8..a65974402 100644 --- a/plinth/modules/zoph/privileged.py +++ b/plinth/modules/zoph/privileged.py @@ -33,15 +33,15 @@ def get_configuration() -> dict[str, str]: """Return the current configuration.""" configuration = {} try: - process = subprocess.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], + stdout=subprocess.PIPE, check=True) except subprocess.CalledProcessError as exception: if exception.returncode != 96: raise _zoph_setup_cli_user() - process = subprocess.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], + stdout=subprocess.PIPE, check=True) for line in process.stdout.decode().splitlines(): name, value = line.partition(':')[::2] @@ -75,13 +75,13 @@ WHERE def _zoph_configure(key, value): """Set a configure value in Zoph.""" try: - subprocess.run(['zoph', '--config', key, value], check=True) + action_utils.run(['zoph', '--config', key, value], check=True) except subprocess.CalledProcessError as exception: if exception.returncode != 96: raise _zoph_setup_cli_user() - subprocess.run(['zoph', '--config', key, value], check=True) + action_utils.run(['zoph', '--config', key, value], check=True) @privileged @@ -137,15 +137,15 @@ def set_configuration(enable_osm: bool | None = None, query = f"UPDATE zoph_users SET user_name='{admin_user}' \ WHERE user_name='admin';" - subprocess.run(['mysql', _get_db_config()['db_name']], - input=query.encode(), check=True) + action_utils.run(['mysql', _get_db_config()['db_name']], + input=query.encode(), check=True) @privileged def is_configured() -> bool | None: """Return whether zoph app is configured.""" try: - process = subprocess.run( + process = action_utils.run( ['zoph', '--get-config', 'interface.user.remote'], stdout=subprocess.PIPE, check=True) return process.stdout.decode().strip() == 'true' @@ -163,8 +163,8 @@ def dump_database(): db_name = _get_db_config()['db_name'] os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True) with open(DB_BACKUP_FILE, 'w', encoding='utf-8') as db_backup_file: - subprocess.run(['mysqldump', db_name], stdout=db_backup_file, - check=True) + action_utils.run(['mysqldump', db_name], stdout=db_backup_file, + check=True) @privileged @@ -178,15 +178,16 @@ def restore_database(): db_user = _get_db_config()['db_user'] db_host = _get_db_config()['db_host'] db_pass = _get_db_config()['db_pass'] - subprocess.run(['mysqladmin', '--force', 'drop', db_name], check=False) - subprocess.run(['mysqladmin', 'create', db_name], check=True) + action_utils.run(['mysqladmin', '--force', 'drop', db_name], + check=False) + action_utils.run(['mysqladmin', 'create', db_name], check=True) with open(DB_BACKUP_FILE, 'r', encoding='utf-8') as db_restore_file: - subprocess.run(['mysql', db_name], stdin=db_restore_file, - check=True) + action_utils.run(['mysql', db_name], stdin=db_restore_file, + check=True) # Set the password for user from restored configuration query = f'ALTER USER {db_user}@{db_host} IDENTIFIED BY "{db_pass}";' - subprocess.run(['mysql'], input=query.encode(), check=True) + action_utils.run(['mysql'], input=query.encode(), check=True) @privileged @@ -198,12 +199,12 @@ def uninstall(): with action_utils.service_ensure_running('mysql'): try: config = _get_db_config() - subprocess.run( + action_utils.run( ['mysqladmin', '--force', 'drop', config['db_name']], check=False) query = f'DROP USER IF EXISTS {config["db_user"]}@localhost;' - subprocess.run(['mysql'], input=query.encode(), check=False) + action_utils.run(['mysql'], input=query.encode(), check=False) except FileNotFoundError: # Database configuration not found pass diff --git a/plinth/privileged/packages.py b/plinth/privileged/packages.py index 90d40ccd1..68928fd04 100644 --- a/plinth/privileged/packages.py +++ b/plinth/privileged/packages.py @@ -3,7 +3,6 @@ import logging import os -import subprocess from collections import defaultdict from typing import Any @@ -14,7 +13,7 @@ import apt_pkg from plinth import action_utils from plinth import app as app_module from plinth import module_loader -from plinth.action_utils import run_apt_command +from plinth.action_utils import run, run_apt_command from plinth.actions import privileged logger = logging.getLogger(__name__) @@ -61,7 +60,7 @@ def install(app_id: str, packages: list[str], skip_recommends: bool = False, if force_missing_configuration: extra_arguments += ['-o', 'Dpkg::Options::=--force-confmiss'] - subprocess.run(['dpkg', '--configure', '-a'], check=False) + run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): run_apt_command(['--fix-broken', 'install']) returncode = run_apt_command(['install'] + extra_arguments + packages) @@ -79,7 +78,7 @@ def remove(app_id: str, packages: list[str], purge: bool): except Exception: raise PermissionError(f'Packages are not managed: {packages}') - subprocess.run(['dpkg', '--configure', '-a'], check=False) + run(['dpkg', '--configure', '-a'], check=False) with action_utils.apt_hold_freedombox(): run_apt_command(['--fix-broken', 'install']) options = [] if not purge else ['--purge'] diff --git a/plinth/tests/test_daemon.py b/plinth/tests/test_daemon.py index 55a58d60f..bcc43c513 100644 --- a/plinth/tests/test_daemon.py +++ b/plinth/tests/test_daemon.py @@ -81,54 +81,59 @@ def test_is_enabled(service_is_enabled, daemon): @patch('subprocess.run') def test_enable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that enabling the daemon works.""" + common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) + common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + check=False) daemon.enable() subprocess_run.assert_has_calls( - [call(['systemctl', 'enable', 'test-unit'], check=False)]) + [call(['systemctl', 'enable', 'test-unit'], **common_args1)]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.enable() + subprocess_run.assert_has_calls([ - call(['systemctl', 'enable', 'test-unit'], check=False), - call(['systemctl', 'start', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), - call(['systemctl', 'enable', 'test-unit-2'], check=False), - call(['systemctl', 'start', 'test-unit-2'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'enable', 'test-unit'], **common_args1), + call(['systemctl', 'start', 'test-unit'], **common_args2), + call(['systemctl', 'enable', 'test-unit-2'], **common_args1), + call(['systemctl', 'start', 'test-unit-2'], **common_args2), ]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit-2'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) @patch('plinth.app.apps_init') @patch('subprocess.run') def test_disable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that disabling the daemon works.""" + common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) + common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + check=False) daemon.disable() subprocess_run.assert_has_calls( - [call(['systemctl', 'disable', 'test-unit'], check=False)]) + [call(['systemctl', 'disable', 'test-unit'], **common_args1)]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.disable() subprocess_run.assert_has_calls([ - call(['systemctl', 'disable', 'test-unit'], check=False), - call(['systemctl', 'stop', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), - call(['systemctl', 'disable', 'test-unit-2'], check=False), - call(['systemctl', 'stop', 'test-unit-2'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'disable', 'test-unit'], **common_args1), + call(['systemctl', 'stop', 'test-unit'], **common_args2), + call(['systemctl', 'disable', 'test-unit-2'], **common_args1), + call(['systemctl', 'stop', 'test-unit-2'], **common_args2), ]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit-2'], - stdout=subprocess.DEVNULL, check=False) + **common_args2) @patch('plinth.action_utils.service_is_running') @@ -148,6 +153,10 @@ def test_is_running(service_is_running, daemon): def test_ensure_running(subprocess_run, service_is_running, apps_init, app_list, mock_privileged, daemon): """Test that checking that the daemon is running works.""" + common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) + common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, + check=False) service_is_running.return_value = True with daemon.ensure_running() as starting_state: assert starting_state @@ -159,16 +168,14 @@ def test_ensure_running(subprocess_run, service_is_running, apps_init, with daemon.ensure_running() as starting_state: assert not starting_state assert subprocess_run.mock_calls == [ - call(['systemctl', 'enable', 'test-unit'], check=False), - call(['systemctl', 'start', 'test-unit'], - stdout=subprocess.DEVNULL, check=False), + call(['systemctl', 'enable', 'test-unit'], **common_args1), + call(['systemctl', 'start', 'test-unit'], **common_args2), ] subprocess_run.reset_mock() assert subprocess_run.mock_calls == [ - call(['systemctl', 'disable', 'test-unit'], check=False), - call(['systemctl', 'stop', 'test-unit'], stdout=subprocess.DEVNULL, - check=False), + call(['systemctl', 'disable', 'test-unit'], **common_args1), + call(['systemctl', 'stop', 'test-unit'], **common_args2), ] From 80e6d940a45c9fcff11355c80ce511de3ece12b7 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 14 Aug 2025 21:43:50 -0700 Subject: [PATCH 25/44] *: Use action_utils.run instead of subprocess.check_call - This is to capture stdout and stderr and transmit that from privileged daemon back to the service to be displayed in HTML. Tests: - Unit tests and code checks pass. - Some of the modified actions work as expected. - systemd daemon-reload is performed during infinoted setup. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 4 ++-- plinth/modules/gitweb/privileged.py | 20 +++++++++---------- plinth/modules/infinoted/privileged.py | 2 +- plinth/modules/kiwix/privileged.py | 7 ++++--- plinth/modules/mediawiki/privileged.py | 12 +++++------ plinth/modules/samba/privileged.py | 14 +++++++------ plinth/modules/ssh/privileged.py | 3 +-- .../upgrades/tests/test_distupgrade.py | 20 +++++++++---------- plinth/modules/users/privileged.py | 6 +++--- 9 files changed, 44 insertions(+), 44 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 119c842de..dfe012748 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -511,7 +511,7 @@ def apt_hold(packages): yield held_packages finally: for package in held_packages: - subprocess.check_call(['apt-mark', 'unhold', package]) + run(['apt-mark', 'unhold', package], check=True) @contextmanager @@ -527,7 +527,7 @@ def apt_hold_freedombox(): # Set the flag. apt_hold_flag.parent.mkdir(mode=0o755, parents=True, exist_ok=True) apt_hold_flag.touch(mode=0o660) - yield subprocess.check_call(['apt-mark', 'hold', 'freedombox']) + yield run(['apt-mark', 'hold', 'freedombox'], check=True) finally: # Was the package held, either in this process or a previous one? if not current_hold or apt_hold_flag.exists(): diff --git a/plinth/modules/gitweb/privileged.py b/plinth/modules/gitweb/privileged.py index 25e50d73b..f64af5c65 100644 --- a/plinth/modules/gitweb/privileged.py +++ b/plinth/modules/gitweb/privileged.py @@ -57,8 +57,8 @@ def _get_global_default_branch(): def _set_global_default_branch(name): """Configure default branch name globally.""" - subprocess.check_call( - ['git', 'config', '--global', 'init.defaultBranch', name]) + action_utils.run(['git', 'config', '--global', 'init.defaultBranch', name], + check=True) def _clone_with_progress_report(url, repo_dir): @@ -166,9 +166,9 @@ def _clone_repo(url: str, description: str, owner: str, keep_ownership: bool): shutil.rmtree(repo_temp_path) if not keep_ownership: - subprocess.check_call( + action_utils.run( ['chown', '-R', f'{REPO_DIR_OWNER}:{REPO_DIR_OWNER}', repo], - cwd=GIT_REPO_PATH) + cwd=GIT_REPO_PATH, check=True) _set_repo_description(repo, description) _set_repo_owner(repo, owner) @@ -178,12 +178,12 @@ def _create_repo(repo: str, description: str, owner: str, is_private: bool, keep_ownership: bool): """Create an empty repository.""" try: - subprocess.check_call(['git', 'init', '-q', '--bare', repo], - cwd=GIT_REPO_PATH) + action_utils.run(['git', 'init', '-q', '--bare', repo], + cwd=GIT_REPO_PATH, check=True) if not keep_ownership: - subprocess.check_call( + action_utils.run( ['chown', '-R', f'{REPO_DIR_OWNER}:{REPO_DIR_OWNER}', repo], - cwd=GIT_REPO_PATH) + cwd=GIT_REPO_PATH, check=True) _set_repo_description(repo, description) _set_repo_owner(repo, owner) if is_private: @@ -372,8 +372,8 @@ def repo_exists(url: str) -> bool: url = validate_repo_url(url) env = dict(os.environ, GIT_TERMINAL_PROMPT='0') try: - subprocess.check_call(['git', 'ls-remote', url, 'HEAD'], timeout=10, - env=env) + action_utils.run(['git', 'ls-remote', url, 'HEAD'], timeout=10, + env=env, check=True) return True except subprocess.CalledProcessError: return False diff --git a/plinth/modules/infinoted/privileged.py b/plinth/modules/infinoted/privileged.py index e79c3cef7..80bf1e275 100644 --- a/plinth/modules/infinoted/privileged.py +++ b/plinth/modules/infinoted/privileged.py @@ -124,7 +124,7 @@ def setup(): with open(SYSTEMD_SERVICE_PATH, 'w', encoding='utf-8') as file_handle: file_handle.write(SYSTEMD_SERVICE) - subprocess.check_call(['systemctl', 'daemon-reload']) + action_utils.service_daemon_reload() # Create infinoted group if needed. try: diff --git a/plinth/modules/kiwix/privileged.py b/plinth/modules/kiwix/privileged.py index bcaad19e1..8dd5ae750 100644 --- a/plinth/modules/kiwix/privileged.py +++ b/plinth/modules/kiwix/privileged.py @@ -53,7 +53,8 @@ def add_package(file_name: str, temporary_file_path: str): def _kiwix_manage_add(zim_file: str): - subprocess.check_call(['kiwix-manage', LIBRARY_FILE, 'add', zim_file]) + action_utils.run(['kiwix-manage', LIBRARY_FILE, 'add', zim_file], + check=True) # kiwix-serve doesn't read the library file unless it is restarted. action_utils.service_try_restart('kiwix-server-freedombox') @@ -97,8 +98,8 @@ def delete_package(zim_id: str): if book.attrib['id'] != zim_id: continue - subprocess.check_call( - ['kiwix-manage', LIBRARY_FILE, 'remove', zim_id]) + action_utils.run(['kiwix-manage', LIBRARY_FILE, 'remove', zim_id], + check=True) (KIWIX_HOME / book.attrib['path']).unlink() action_utils.service_try_restart('kiwix-server-freedombox') return diff --git a/plinth/modules/mediawiki/privileged.py b/plinth/modules/mediawiki/privileged.py index 1035e6328..cfdcc6f7d 100644 --- a/plinth/modules/mediawiki/privileged.py +++ b/plinth/modules/mediawiki/privileged.py @@ -52,12 +52,12 @@ def setup(): with tempfile.NamedTemporaryFile() as password_file_handle: password_file_handle.write(password.encode()) password_file_handle.flush() - subprocess.check_call([ + action_utils.run([ get_php_command(), install_script, '--confpath=/etc/mediawiki', '--dbtype=sqlite', '--dbpath=' + data_dir, '--scriptpath=/mediawiki', '--passfile', password_file_handle.name, 'Wiki', 'admin' - ]) + ], check=True) action_utils.run(['chmod', '-R', 'o-rwx', data_dir], check=True) action_utils.run(['chown', '-R', 'www-data:www-data', data_dir], check=True) @@ -102,17 +102,17 @@ def change_password(username: str, password: secret_str): change_password_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'changePassword.php') - subprocess.check_call([ + action_utils.run([ get_php_command(), change_password_script, '--user', username, '--password', password - ]) + ], check=True) @privileged def update(): """Run update.php maintenance script when version upgrades happen.""" update_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'update.php') - subprocess.check_call([get_php_command(), update_script, '--quick']) + action_utils.run([get_php_command(), update_script, '--quick'], check=True) def _update_setting(setting_name, setting_line): @@ -180,7 +180,7 @@ def set_default_language(language: str): # languages. rebuild_messages_script = os.path.join(MAINTENANCE_SCRIPTS_DIR, 'rebuildmessages.php') - subprocess.check_call([get_php_command(), rebuild_messages_script]) + action_utils.run([get_php_command(), rebuild_messages_script], check=True) @privileged diff --git a/plinth/modules/samba/privileged.py b/plinth/modules/samba/privileged.py index c8f0078f1..d03b39f67 100644 --- a/plinth/modules/samba/privileged.py +++ b/plinth/modules/samba/privileged.py @@ -7,6 +7,7 @@ import pathlib import shutil import subprocess +from plinth import action_utils from plinth.actions import privileged DEFAULT_FILE = '/etc/default/samba' @@ -51,12 +52,13 @@ CONF = r''' def _close_share(share_name): """Disconnect all samba users who are connected to the share.""" - subprocess.check_call(['smbcontrol', 'smbd', 'close-share', share_name]) + action_utils.run(['smbcontrol', 'smbd', 'close-share', share_name], + check=True) def _conf_command(parameters, **kwargs): """Run samba configuration registry command.""" - subprocess.check_call(['net', 'conf'] + parameters, **kwargs) + action_utils.run(['net', 'conf'] + parameters, check=True, **kwargs) def _create_share(mount_point, share_type, windows_filesystem=False): @@ -198,8 +200,8 @@ def _set_open_share_permissions(directory): file_path = os.path.join(root, file) shutil.chown(file_path, group='freedombox-share') os.chmod(file_path, 0o0664) - subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory]) - subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory]) + action_utils.run(['setfacl', '-Rm', 'g::rwX', directory], check=True) + action_utils.run(['setfacl', '-Rdm', 'g::rwX', directory], check=True) def _use_config_file(conf_file): @@ -229,8 +231,8 @@ def _set_share_permissions(directory): file_path = os.path.join(root, file) shutil.chown(file_path, group='freedombox-share') os.chmod(file_path, 0o0664) - subprocess.check_call(['setfacl', '-Rm', 'g::rwX', directory]) - subprocess.check_call(['setfacl', '-Rdm', 'g::rwX', directory]) + action_utils.run(['setfacl', '-Rm', 'g::rwX', directory], check=True) + action_utils.run(['setfacl', '-Rdm', 'g::rwX', directory], check=True) @privileged diff --git a/plinth/modules/ssh/privileged.py b/plinth/modules/ssh/privileged.py index f1a744fe5..7cb0a07f5 100644 --- a/plinth/modules/ssh/privileged.py +++ b/plinth/modules/ssh/privileged.py @@ -7,7 +7,6 @@ import pathlib import pwd import shutil import stat -import subprocess import augeas @@ -101,7 +100,7 @@ def set_keys(user: str, keys: str, auth_user: str, auth_password: secret_str): ssh_folder = os.path.join(get_user_homedir(user), '.ssh') key_file_path = os.path.join(ssh_folder, 'authorized_keys') - subprocess.check_call(['mkhomedir_helper', user]) + action_utils.run(['mkhomedir_helper', user], check=True) if not os.path.exists(ssh_folder): os.makedirs(ssh_folder) diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index 1d3fc1e52..e1cd07e39 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -264,9 +264,8 @@ def test_services_disable(service_is_running, service_disable, service_enable): @patch('subprocess.run') -@patch('subprocess.check_call') @patch('subprocess.check_output') -def test_apt_hold_packages(check_output, check_call, run, tmp_path): +def test_apt_hold_packages(check_output, run, tmp_path): """Test that holding apt packages works.""" hold_flag = tmp_path / 'flag' run.return_value.returncode = 0 @@ -277,29 +276,28 @@ def test_apt_hold_packages(check_output, check_call, run, tmp_path): with distupgrade._apt_hold_packages(): assert hold_flag.exists() assert hold_flag.stat().st_mode & 0o117 == 0 - expected_call = [call(['apt-mark', 'hold', 'freedombox'])] - assert check_call.call_args_list == expected_call expected_calls = [ + call(['apt-mark', 'hold', 'freedombox'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True), call(['apt-mark', 'hold', 'package1'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False), call(['apt-mark', 'hold', 'package2'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) ] assert run.call_args_list == expected_calls - check_call.reset_mock() run.reset_mock() expected_call = [ + call(['apt-mark', 'unhold', 'package1'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True), + call(['apt-mark', 'unhold', 'package2'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=True), call(['apt-mark', 'unhold', 'freedombox'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False) + check=False), ] assert run.call_args_list == expected_call - expected_calls = [ - call(['apt-mark', 'unhold', 'package1']), - call(['apt-mark', 'unhold', 'package2']) - ] - assert check_call.call_args_list == expected_calls @patch('plinth.action_utils.debconf_set_selections') diff --git a/plinth/modules/users/privileged.py b/plinth/modules/users/privileged.py index 3bb2670fc..0ca7c291e 100644 --- a/plinth/modules/users/privileged.py +++ b/plinth/modules/users/privileged.py @@ -339,14 +339,14 @@ def _get_samba_users(): def _delete_samba_user(username): """Delete a Samba user.""" if username in _get_samba_users(): - subprocess.check_call(['smbpasswd', '-x', username]) + action_utils.run(['smbpasswd', '-x', username], check=True) _disconnect_samba_user(username) def _disconnect_samba_user(username): """Disconnect a Samba user.""" try: - subprocess.check_call(['pkill', '-U', username, 'smbd']) + action_utils.run(['pkill', '-U', username, 'smbd'], check=True) except subprocess.CalledProcessError as error: if error.returncode != 1: raise @@ -679,7 +679,7 @@ def set_user_status(username: str, status: str, auth_user: str, # Set user status in Samba password database if username in _get_samba_users(): - subprocess.check_call(['smbpasswd', smbpasswd_flag, username]) + action_utils.run(['smbpasswd', smbpasswd_flag, username], check=True) _flush_cache() From bf9005ac482d86fa4ff2e5bf8f827f6b315b39b3 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Thu, 14 Aug 2025 21:47:47 -0700 Subject: [PATCH 26/44] *: Use action_utils.run instead of subprocess.call - This is to capture stdout and stderr and transmit that from privileged daemon back to the service to be displayed in HTML. Tests: - Unit tests and code checks pass. - Some of the modified actions work as expected. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/calibre/privileged.py | 4 ++-- plinth/modules/calibre/tests/test_privileged.py | 5 ++--- plinth/modules/ejabberd/privileged.py | 12 ++++++------ plinth/modules/openvpn/privileged.py | 4 ++-- plinth/modules/power/privileged.py | 7 +++---- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/plinth/modules/calibre/privileged.py b/plinth/modules/calibre/privileged.py index 962f2f575..86d320aa1 100644 --- a/plinth/modules/calibre/privileged.py +++ b/plinth/modules/calibre/privileged.py @@ -28,9 +28,9 @@ def create_library(name: str): calibre.validate_library_name(name) library = LIBRARIES_PATH / name library.mkdir(mode=0o755) # Raise exception if already exists - subprocess.call( + action_utils.run( ['calibredb', '--with-library', library, 'list_categories'], - stdout=subprocess.DEVNULL) + stdout=subprocess.DEVNULL, check=False) # Force systemd StateDirectory= logic to assign proper ownership to the # DynamicUser= diff --git a/plinth/modules/calibre/tests/test_privileged.py b/plinth/modules/calibre/tests/test_privileged.py index 6b41173f8..346897e25 100644 --- a/plinth/modules/calibre/tests/test_privileged.py +++ b/plinth/modules/calibre/tests/test_privileged.py @@ -29,9 +29,8 @@ def fixture_patch(): path = pathlib.Path(args[0][2]) / 'metadata.db' path.touch() - with patch('subprocess.call') as subprocess_call, \ - patch('subprocess.run'), patch('shutil.chown'): - subprocess_call.side_effect = side_effect + with patch('subprocess.run') as subprocess_run, patch('shutil.chown'): + subprocess_run.side_effect = side_effect yield diff --git a/plinth/modules/ejabberd/privileged.py b/plinth/modules/ejabberd/privileged.py index a682a073e..49d7a68a8 100644 --- a/plinth/modules/ejabberd/privileged.py +++ b/plinth/modules/ejabberd/privileged.py @@ -145,7 +145,7 @@ def pre_change_hostname(old_hostname: str, new_hostname: str): logger.info('ejabberdctl not found') return - subprocess.call(['ejabberdctl', 'backup', EJABBERD_BACKUP]) + action_utils.run(['ejabberdctl', 'backup', EJABBERD_BACKUP], check=False) subprocess.check_output([ 'ejabberdctl', 'mnesia-change-nodename', 'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW @@ -160,12 +160,12 @@ def change_hostname(): return action_utils.service_stop('ejabberd') - subprocess.call(['pkill', '-u', 'ejabberd']) + action_utils.run(['pkill', '-u', 'ejabberd'], check=False) # Make sure there aren't files in the Mnesia spool dir os.makedirs('/var/lib/ejabberd/oldfiles', exist_ok=True) - subprocess.call('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/', - shell=True) + action_utils.run('mv /var/lib/ejabberd/*.* /var/lib/ejabberd/oldfiles/', + shell=True, check=False) action_utils.service_start('ejabberd') @@ -278,7 +278,7 @@ def mam(command: str) -> bool | None: yaml.dump(conf, file_handle) if action_utils.service_is_running('ejabberd'): - subprocess.call(['ejabberdctl', 'reload_config']) + action_utils.run(['ejabberdctl', 'reload_config'], check=False) return None @@ -359,7 +359,7 @@ def configure_turn(turn_server_config: dict[str, Any], managed: bool): Path(EJABBERD_MANAGED_COTURN).unlink(missing_ok=True) if action_utils.service_is_running('ejabberd'): - subprocess.call(['ejabberdctl', 'reload_config']) + action_utils.run(['ejabberdctl', 'reload_config'], check=False) def _get_version(): diff --git a/plinth/modules/openvpn/privileged.py b/plinth/modules/openvpn/privileged.py index 50f64c722..5679a5660 100644 --- a/plinth/modules/openvpn/privileged.py +++ b/plinth/modules/openvpn/privileged.py @@ -104,8 +104,8 @@ def _setup_firewall(): 'firewall-cmd', '--zone', 'internal', '--{}-interface'.format(operation), interface ] - subprocess.call(command) - subprocess.call(command + ['--permanent']) + action_utils.run(command, check=False) + action_utils.run(command + ['--permanent'], check=False) def _is_tunplus_enabled(): """Return whether tun+ interface is already added.""" diff --git a/plinth/modules/power/privileged.py b/plinth/modules/power/privileged.py index 06e22967c..4f468d9c9 100644 --- a/plinth/modules/power/privileged.py +++ b/plinth/modules/power/privileged.py @@ -1,18 +1,17 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Shutdown/restart the system.""" -import subprocess - +from plinth import action_utils from plinth.actions import privileged @privileged def restart(): """Restart the system.""" - subprocess.call('reboot') + action_utils.run('reboot', check=False) @privileged def shutdown(): """Shut down the system.""" - subprocess.call(['shutdown', 'now']) + action_utils.run(['shutdown', 'now'], check=False) From b253166f6dd7c852fa94beaf99e478d1d43bdb6e Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 17 Aug 2025 17:02:48 -0700 Subject: [PATCH 27/44] *: Use action_utils.run instead of subprocess.check_output - This is to capture stdout and stderr and transmit that from privileged daemon back to the service to be displayed in HTML. Tests: - Unit tests and code checks pass. - Some of the modified actions work as expected. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 23 ++++++------- plinth/modules/ejabberd/privileged.py | 14 ++++---- plinth/modules/email/privileged/dkim.py | 5 +-- plinth/modules/firewall/privileged.py | 8 +++-- plinth/modules/gitweb/privileged.py | 6 ++-- plinth/modules/letsencrypt/privileged.py | 9 ++--- plinth/modules/networks/privileged.py | 10 +++--- .../modules/networks/tests/test_privileged.py | 6 ++-- plinth/modules/samba/privileged.py | 4 +-- plinth/modules/upgrades/__init__.py | 9 +++++ plinth/modules/upgrades/privileged.py | 3 +- .../upgrades/tests/test_distupgrade.py | 34 +++++++++++++------ plinth/modules/upgrades/tests/test_utils.py | 6 ++-- plinth/modules/upgrades/utils.py | 12 +++---- plinth/modules/users/__init__.py | 6 ++-- plinth/modules/users/privileged.py | 11 +++--- plinth/modules/wireguard/privileged.py | 7 ++-- 17 files changed, 101 insertions(+), 72 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index dfe012748..4ff7c1654 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -209,8 +209,7 @@ def webserver_is_enabled(name, kind='config'): option_map = {'config': '-c', 'site': '-s', 'module': '-m'} try: # Don't print anything on the terminal - subprocess.check_output(['a2query', option_map[kind], name], - stderr=subprocess.STDOUT) + run(['a2query', option_map[kind], name], check=True) return True except subprocess.CalledProcessError: return False @@ -232,7 +231,7 @@ def webserver_enable(name, kind='config', apply_changes=True): 'site': 'a2ensite', 'module': 'a2enmod' } - subprocess.check_output([command_map[kind], name]) + run([command_map[kind], name], check=True) action_required = 'restart' if kind == 'module' else 'reload' @@ -261,7 +260,7 @@ def webserver_disable(name, kind='config', apply_changes=True): 'site': 'a2dissite', 'module': 'a2dismod' } - subprocess.check_output([command_map[kind], name]) + run([command_map[kind], name], check=True) action_required = 'restart' if kind == 'module' else 'reload' @@ -389,7 +388,7 @@ def get_ip_addresses() -> list[dict[str, str | bool]]: """Return a list of IP addresses assigned to the system.""" addresses = [] - output = subprocess.check_output(['ip', '-o', 'addr']) + output = run(['ip', '-o', 'addr'], check=True).stdout for line in output.decode().splitlines(): parts = line.split() address: dict[str, str | bool] = { @@ -415,7 +414,7 @@ def get_ip_addresses() -> list[dict[str, str | bool]]: def get_hostname(): """Return the current hostname.""" - return subprocess.check_output(['hostname']).decode().strip() + return run(['hostname'], check=True).stdout.decode().strip() def dpkg_reconfigure(package, config): @@ -457,7 +456,7 @@ def debconf_set_selections(presets): pass presets = '\n'.join(presets) - subprocess.check_output(['debconf-set-selections'], input=presets.encode()) + run(['debconf-set-selections'], input=presets.encode(), check=True) def is_disk_image(): @@ -501,8 +500,8 @@ def apt_hold(packages): held_packages = [] try: for package in packages: - current_hold = subprocess.check_output( - ['apt-mark', 'showhold', package]) + current_hold = run(['apt-mark', 'showhold', package], + check=True).stdout if not current_hold: process = run(['apt-mark', 'hold', package], check=False) if process.returncode == 0: # success @@ -517,8 +516,8 @@ def apt_hold(packages): @contextmanager def apt_hold_freedombox(): """Prevent freedombox package from being removed during apt operations.""" - current_hold = subprocess.check_output( - ['apt-mark', 'showhold', 'freedombox']) + current_hold = run(['apt-mark', 'showhold', 'freedombox'], + check=True).stdout try: if current_hold: # Package is already held, possibly by administrator. @@ -548,7 +547,7 @@ def is_package_manager_busy(): is open which indicates that the package manager is busy""" LOCK_FILE = '/var/lib/dpkg/lock' try: - subprocess.check_output(['lsof', LOCK_FILE]) + run(['lsof', LOCK_FILE], check=True) return True except subprocess.CalledProcessError: return False diff --git a/plinth/modules/ejabberd/privileged.py b/plinth/modules/ejabberd/privileged.py index 49d7a68a8..85810d4cf 100644 --- a/plinth/modules/ejabberd/privileged.py +++ b/plinth/modules/ejabberd/privileged.py @@ -89,7 +89,7 @@ def setup(domain_name: str): _upgrade_config(domain_name) try: - subprocess.check_output(['ejabberdctl', 'restart']) + action_utils.run(['ejabberdctl', 'restart'], check=True) except subprocess.CalledProcessError as err: logger.warn('Failed to restart ejabberd with new configuration: %s', err) @@ -146,10 +146,10 @@ def pre_change_hostname(old_hostname: str, new_hostname: str): return action_utils.run(['ejabberdctl', 'backup', EJABBERD_BACKUP], check=False) - subprocess.check_output([ + action_utils.run([ 'ejabberdctl', 'mnesia-change-nodename', 'ejabberd@' + old_hostname, 'ejabberd@' + new_hostname, EJABBERD_BACKUP, EJABBERD_BACKUP_NEW - ]) + ], check=True) os.remove(EJABBERD_BACKUP) @@ -172,8 +172,8 @@ def change_hostname(): # restore backup database if os.path.exists(EJABBERD_BACKUP_NEW): try: - subprocess.check_output( - ['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW]) + action_utils.run(['ejabberdctl', 'restore', EJABBERD_BACKUP_NEW], + check=True) os.remove(EJABBERD_BACKUP_NEW) except subprocess.CalledProcessError as err: logger.error('Failed to restore ejabberd backup database: %s', err) @@ -365,8 +365,8 @@ def configure_turn(turn_server_config: dict[str, Any], managed: bool): def _get_version(): """Get the current ejabberd version.""" try: - output = subprocess.check_output(['ejabberdctl', - 'status']).decode('utf-8') + output = action_utils.run(['ejabberdctl', 'status'], + check=True).stdout.decode('utf-8') except subprocess.CalledProcessError: return None diff --git a/plinth/modules/email/privileged/dkim.py b/plinth/modules/email/privileged/dkim.py index 62195a5d9..4fa401efc 100644 --- a/plinth/modules/email/privileged/dkim.py +++ b/plinth/modules/email/privileged/dkim.py @@ -31,9 +31,10 @@ def get_dkim_public_key(domain: str) -> str: """Privileged action to get the public key from DKIM key.""" _validate_domain_name(domain) key_file = _keys_dir / f'{domain}.dkim.key' - output = subprocess.check_output( + output = action_utils.run( ['openssl', 'rsa', '-in', - str(key_file), '-pubout'], stderr=subprocess.DEVNULL) + str(key_file), '-pubout'], stderr=subprocess.DEVNULL, + check=True).stdout return ''.join(output.decode().splitlines()[1:-1]) diff --git a/plinth/modules/firewall/privileged.py b/plinth/modules/firewall/privileged.py index 33f0dd7bf..9c326d1ba 100644 --- a/plinth/modules/firewall/privileged.py +++ b/plinth/modules/firewall/privileged.py @@ -176,7 +176,8 @@ def get_config() -> FirewallConfig: config: FirewallConfig = {} # Get the default zone. - output = subprocess.check_output(['firewall-cmd', '--get-default-zone']) + output = action_utils.run(['firewall-cmd', '--get-default-zone'], + check=True).stdout config['default_zone'] = output.decode().strip() # Load Augeas lens. @@ -191,8 +192,9 @@ def get_config() -> FirewallConfig: config['backend'] = aug.get('FirewallBackend') # Get the list of direct passthroughs. - output = subprocess.check_output( - ['firewall-cmd', '--direct', '--get-all-passthroughs']) + output = action_utils.run( + ['firewall-cmd', '--direct', '--get-all-passthroughs'], + check=True).stdout config['passthroughs'] = output.decode().strip().split('\n') return config diff --git a/plinth/modules/gitweb/privileged.py b/plinth/modules/gitweb/privileged.py index f64af5c65..70e4b765f 100644 --- a/plinth/modules/gitweb/privileged.py +++ b/plinth/modules/gitweb/privileged.py @@ -44,9 +44,9 @@ def setup(): def _get_global_default_branch(): """Get globally configured default branch name.""" try: - default_branch = subprocess.check_output( - ['git', 'config', '--global', '--get', - 'init.defaultBranch']).decode().strip() + default_branch = action_utils.run( + ['git', 'config', '--global', '--get', 'init.defaultBranch'], + check=True).stdout.decode().strip() except subprocess.CalledProcessError as exception: if exception.returncode == 1: # Default branch not configured return None diff --git a/plinth/modules/letsencrypt/privileged.py b/plinth/modules/letsencrypt/privileged.py index 3002a0ef8..f96184cf2 100644 --- a/plinth/modules/letsencrypt/privileged.py +++ b/plinth/modules/letsencrypt/privileged.py @@ -8,7 +8,6 @@ import os import pathlib import re import shutil -import subprocess import sys from typing import Any @@ -28,8 +27,9 @@ WEB_ROOT_PATH = '/var/www/html' def _get_certificate_expiry(domain: str) -> str: """Return the expiry date of a certificate.""" certificate_file = os.path.join(le.LIVE_DIRECTORY, domain, 'cert.pem') - output = subprocess.check_output( - ['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file]) + output = action_utils.run( + ['openssl', 'x509', '-enddate', '-noout', '-in', certificate_file], + check=True).stdout return output.decode().strip().split('=')[1] @@ -41,7 +41,8 @@ def _get_modified_time(domain: str) -> int: def _get_validity_status(domain: str) -> str: """Return validity status of a certificate; valid, revoked, expired.""" - output = subprocess.check_output(['certbot', 'certificates', '-d', domain]) + output = action_utils.run(['certbot', 'certificates', '-d', domain], + check=True).stdout line = output.decode(sys.stdout.encoding) match = re.search(r'INVALID: (.*)\)', line) diff --git a/plinth/modules/networks/privileged.py b/plinth/modules/networks/privileged.py index 4f671a97b..05eaf2be6 100644 --- a/plinth/modules/networks/privileged.py +++ b/plinth/modules/networks/privileged.py @@ -29,8 +29,9 @@ def _sort_interfaces(interfaces: list[str]) -> list[str]: def _get_interfaces() -> dict[str, list[str]]: """Return all network interfaces by their type.""" - output = subprocess.check_output( - ['nmcli', '--terse', '--fields', 'type,device', 'device']) + output = action_utils.run( + ['nmcli', '--terse', '--fields', 'type,device', 'device'], + check=True).stdout interfaces = collections.defaultdict(list) for line in output.decode().splitlines(): type_, _, interface = line.partition(':') @@ -45,8 +46,9 @@ def _get_interfaces() -> dict[str, list[str]]: def _add_connection(connection_name: str, interface: str, remaining_arguments: list[str]): """Add an Ethernet/Wi-Fi connection of type regular or shared.""" - output = subprocess.check_output( - ['nmcli', '--terse', '--fields', 'name,device', 'con', 'show']) + output = action_utils.run( + ['nmcli', '--terse', '--fields', 'name,device', 'con', 'show'], + check=True).stdout lines = output.decode().splitlines() if f'{connection_name}:{interface}' in lines: logging.info('Connection %s already exists for device %s, not adding.', diff --git a/plinth/modules/networks/tests/test_privileged.py b/plinth/modules/networks/tests/test_privileged.py index b18de501e..9bcefa2bf 100644 --- a/plinth/modules/networks/tests/test_privileged.py +++ b/plinth/modules/networks/tests/test_privileged.py @@ -6,10 +6,10 @@ from unittest.mock import patch from .. import privileged -@patch('subprocess.check_output') -def test_get_interfaces(check_output): +@patch('subprocess.run') +def test_get_interfaces(run): """Test returning list of network interfaces in sorted order.""" - check_output.return_value = '\n'.join([ + run.return_value.stdout = '\n'.join([ 'ethernet:ve-fbx-testing', 'ethernet:enp39s0', 'ethernet:enp32s1', diff --git a/plinth/modules/samba/privileged.py b/plinth/modules/samba/privileged.py index d03b39f67..db407a596 100644 --- a/plinth/modules/samba/privileged.py +++ b/plinth/modules/samba/privileged.py @@ -155,7 +155,7 @@ def _get_mount_point(path): def _get_shares() -> list[dict[str, str]]: """Get shares.""" shares = [] - output = subprocess.check_output(['net', 'conf', 'list']) + output = action_utils.run(['net', 'conf', 'list'], check=True).stdout config = configparser.RawConfigParser() config.read_string(output.decode()) for name in config.sections(): @@ -272,7 +272,7 @@ def get_shares() -> list[dict[str, str]]: @privileged def get_users() -> list[str]: """Get users from Samba database.""" - output = subprocess.check_output(['pdbedit', '-L']).decode() + output = action_utils.run(['pdbedit', '-L'], check=True).stdout.decode() samba_users = [line.split(':')[0] for line in output.split()] return samba_users diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index f573ecdcd..a412e022e 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -351,6 +351,15 @@ def is_backports_enabled(): return os.path.exists(privileged.BACKPORTS_SOURCES_LIST) +def get_current_release(): + """Return current release and codename as a tuple.""" + output = action_utils.run( + ['lsb_release', '--release', '--codename', '--short'], + check=True).stdout.decode().strip() + lines = output.split('\n') + return lines[0], lines[1] + + def is_backports_current(): """Return whether backports are enabled for the current release.""" if not is_backports_enabled(): diff --git a/plinth/modules/upgrades/privileged.py b/plinth/modules/upgrades/privileged.py index 8629f5345..8b82976ae 100644 --- a/plinth/modules/upgrades/privileged.py +++ b/plinth/modules/upgrades/privileged.py @@ -128,7 +128,8 @@ def release_held_packages(): 'holds.') return - output = subprocess.check_output(['apt-mark', 'showhold']).decode().strip() + output = action_utils.run(['apt-mark', 'showhold'], + check=True).stdout.decode().strip() holds = output.split('\n') logger.info('Releasing package holds: %s', holds) action_utils.run(['apt-mark', 'unhold', *holds], stdout=subprocess.DEVNULL, diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index e1cd07e39..89bc0ca1b 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -7,7 +7,7 @@ import re import subprocess from datetime import datetime as datetime_original from datetime import timezone -from unittest.mock import call, patch +from unittest.mock import Mock, call, patch import pytest @@ -264,28 +264,40 @@ def test_services_disable(service_is_running, service_disable, service_enable): @patch('subprocess.run') -@patch('subprocess.check_output') -def test_apt_hold_packages(check_output, run, tmp_path): +def test_apt_hold_packages(run, tmp_path): """Test that holding apt packages works.""" + + def _run(command, **kwargs): + if 'showhold' in command: + return Mock(stdout=False) + + return Mock(returncode=0) + hold_flag = tmp_path / 'flag' - run.return_value.returncode = 0 + run.side_effect = _run with patch('plinth.action_utils.apt_hold_flag', hold_flag), \ patch('plinth.modules.upgrades.distupgrade.PACKAGES_WITH_PROMPTS', ['package1', 'package2']): - check_output.return_value = False with distupgrade._apt_hold_packages(): assert hold_flag.exists() assert hold_flag.stat().st_mode & 0o117 == 0 expected_calls = [ - call(['apt-mark', 'hold', 'freedombox'], + call(['apt-mark', 'showhold', 'freedombox'], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'hold', 'freedombox'], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'showhold', 'package1'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True), - call(['apt-mark', 'hold', 'package1'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False), - call(['apt-mark', 'hold', 'package2'], stdout=subprocess.PIPE, - stderr=subprocess.PIPE, check=False) + call(['apt-mark', 'hold', 'package1'], check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE), + call(['apt-mark', 'showhold', 'package2'], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=True), + call(['apt-mark', 'hold', 'package2'], check=False, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) ] - assert run.call_args_list == expected_calls + assert run.mock_calls == expected_calls run.reset_mock() expected_call = [ diff --git a/plinth/modules/upgrades/tests/test_utils.py b/plinth/modules/upgrades/tests/test_utils.py index 8336c9731..abd98bc84 100644 --- a/plinth/modules/upgrades/tests/test_utils.py +++ b/plinth/modules/upgrades/tests/test_utils.py @@ -64,10 +64,10 @@ deb http://deb.debian.org/debian trixie main assert utils.get_sources_list_codename() == 'testing' -@patch('subprocess.check_output') -def test_get_current_release(check_output): +@patch('subprocess.run') +def test_get_current_release(run): """Test that getting current release works.""" - check_output.return_value = b'test-release\ntest-codename\n\n' + run.return_value.stdout = b'test-release\ntest-codename\n\n' assert utils.get_current_release() == ('test-release', 'test-codename') diff --git a/plinth/modules/upgrades/utils.py b/plinth/modules/upgrades/utils.py index 5904554fc..bc48cfeab 100644 --- a/plinth/modules/upgrades/utils.py +++ b/plinth/modules/upgrades/utils.py @@ -3,10 +3,10 @@ import pathlib import re -import subprocess import augeas +from plinth import action_utils from plinth.modules.apache.components import check_url RELEASE_FILE_URL = \ @@ -23,7 +23,7 @@ def check_auto() -> bool: 'apt-config', 'shell', 'UpdateInterval', 'APT::Periodic::Update-Package-Lists' ] - output = subprocess.check_output(arguments).decode() + output = action_utils.run(arguments, check=True).stdout.decode() update_interval = 0 match = re.match(r"UpdateInterval='(.*)'", output) if match: @@ -62,7 +62,7 @@ def is_release_file_available(protocol: str, dist: str, def is_sufficient_free_space() -> bool: """Return whether there is sufficient free space for dist upgrade.""" - output = subprocess.check_output(['df', '--output=avail', '/']) + output = action_utils.run(['df', '--output=avail', '/'], check=True).stdout free_space = int(output.decode().split('\n')[1]) return free_space >= DIST_UPGRADE_REQUIRED_FREE_SPACE @@ -100,9 +100,9 @@ def get_sources_list_codename() -> str | None: def get_current_release(): """Return current release and codename as a tuple.""" - output = subprocess.check_output( - ['lsb_release', '--release', '--codename', - '--short']).decode().strip() + output = action_utils.run( + ['lsb_release', '--release', '--codename', '--short'], + check=True).stdout.decode().strip() lines = output.split('\n') return lines[0], lines[1] diff --git a/plinth/modules/users/__init__.py b/plinth/modules/users/__init__.py index 71e6418cb..67543f3cd 100644 --- a/plinth/modules/users/__init__.py +++ b/plinth/modules/users/__init__.py @@ -9,6 +9,7 @@ from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_noop +from plinth import action_utils from plinth import app as app_module from plinth import cfg, menu from plinth.config import DropinConfigs @@ -127,8 +128,9 @@ def _diagnose_ldap_entry(search_item: str) -> DiagnosticCheck: result = Result.FAILED try: - output = subprocess.check_output( - ['ldapsearch', '-LLL', '-x', '-b', 'dc=thisbox', search_item]) + output = action_utils.run( + ['ldapsearch', '-LLL', '-x', '-b', 'dc=thisbox', search_item], + check=True).stdout if search_item in output.decode(): result = Result.PASSED except subprocess.CalledProcessError: diff --git a/plinth/modules/users/privileged.py b/plinth/modules/users/privileged.py index 0ca7c291e..0d7ad0bd3 100644 --- a/plinth/modules/users/privileged.py +++ b/plinth/modules/users/privileged.py @@ -331,8 +331,8 @@ def get_nslcd_config() -> dict[str, str]: def _get_samba_users(): """Get users from the Samba user database.""" # 'pdbedit -L' is better for listing users but is installed only with samba - stdout = subprocess.check_output( - ['tdbdump', '/var/lib/samba/private/passdb.tdb']).decode() + stdout = action_utils.run(['tdbdump', '/var/lib/samba/private/passdb.tdb'], + check=True).stdout.decode() return re.findall(r'USER_(.*)\\0', stdout) @@ -354,7 +354,8 @@ def _disconnect_samba_user(username): def _get_user_home(username): """Return the user home directory.""" - output = subprocess.check_output(['getent', 'passwd', username], text=True) + output = action_utils.run(['getent', 'passwd', username], + check=True).stdout.decode() return pathlib.Path(output.split(':')[5]) @@ -491,11 +492,11 @@ def _get_admin_users(): admin_users = [] try: - output = subprocess.check_output([ + output = action_utils.run([ 'ldapsearch', '-LLL', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-o', 'ldif-wrap=no', '-s', 'base', '-b', 'cn=admin,ou=groups,dc=thisbox', 'memberUid' - ]).decode() + ], check=True).stdout.decode() except subprocess.CalledProcessError as error: if error.returncode == 32: # no entries found diff --git a/plinth/modules/wireguard/privileged.py b/plinth/modules/wireguard/privileged.py index 50dd0cb26..fc37ca833 100644 --- a/plinth/modules/wireguard/privileged.py +++ b/plinth/modules/wireguard/privileged.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Configuration helper for WireGuard.""" -import subprocess - +from plinth import action_utils from plinth.actions import privileged SERVER_INTERFACE = 'wg0' @@ -11,8 +10,8 @@ SERVER_INTERFACE = 'wg0' @privileged def get_info() -> dict[str, dict]: """Return info for each configured interface.""" - output = subprocess.check_output(['wg', 'show', 'all', - 'dump']).decode().strip() + output = action_utils.run(['wg', 'show', 'all', 'dump'], + check=True).stdout.decode().strip() lines = output.split('\n') interfaces: dict[str, dict] = {} for line in lines: From 7f608cd570085555cf632fdfaf8289678e4377b6 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Sun, 17 Aug 2025 23:38:04 -0700 Subject: [PATCH 28/44] *: Collect output for all privileged sub-processes - Now that we have a mechanism for properly collecting, transmitting, and display the stdout and stderr. There is no reason not to collect all of the stdin and stderr. - Also, the stdin/stderr=subprocess.PIPE is redundant and prevents the output from getting collected for debugging. So, remove it. Tests: - Ran functional tests on backups, calibre, ejabberd, email, gitweb, ikiwiki, infinoted, kiwix, mediawiki, mumble, nextcloud,, openvpn, samba, wireguard, zoph. 2-3 issues were found but did not seem like new errors. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/action_utils.py | 27 ++++----- plinth/conftest.py | 4 +- plinth/modules/backups/privileged.py | 18 ++---- plinth/modules/calibre/privileged.py | 3 +- plinth/modules/datetime/privileged.py | 4 +- plinth/modules/email/postfix.py | 2 +- plinth/modules/email/privileged/dkim.py | 4 +- plinth/modules/firewall/privileged.py | 4 +- plinth/modules/gitweb/privileged.py | 6 +- plinth/modules/ikiwiki/privileged.py | 10 +--- plinth/modules/mediawiki/privileged.py | 3 +- plinth/modules/mumble/privileged.py | 3 +- plinth/modules/nextcloud/privileged.py | 19 +++--- plinth/modules/openvpn/privileged.py | 4 +- plinth/modules/samba/privileged.py | 6 +- plinth/modules/snapshot/privileged.py | 16 ++--- plinth/modules/sogo/privileged.py | 3 +- plinth/modules/storage/privileged.py | 7 +-- plinth/modules/upgrades/distupgrade.py | 2 +- plinth/modules/upgrades/privileged.py | 3 +- .../upgrades/tests/test_distupgrade.py | 7 +-- plinth/modules/users/privileged.py | 28 +++++---- plinth/modules/zoph/privileged.py | 9 +-- plinth/tests/test_daemon.py | 58 +++++++++---------- 24 files changed, 99 insertions(+), 151 deletions(-) diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 4ff7c1654..b6805d6fa 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -33,8 +33,7 @@ def is_systemd_running(): def systemd_get_default() -> str: """Return the default target that systemd will boot into.""" - process = run(['systemctl', 'get-default'], stdout=subprocess.PIPE, - check=True) + process = run(['systemctl', 'get-default'], check=True) return process.stdout.decode().strip() @@ -45,7 +44,7 @@ def systemd_set_default(target: str): def service_daemon_reload(): """Reload systemd to ensure that newer unit files are read.""" - run(['systemctl', 'daemon-reload'], check=True, stdout=subprocess.DEVNULL) + run(['systemctl', 'daemon-reload'], check=True) def service_is_running(servicename): @@ -54,8 +53,7 @@ def service_is_running(servicename): Does not need to run as root. """ try: - run(['systemctl', 'status', servicename], check=True, - stdout=subprocess.DEVNULL) + run(['systemctl', 'status', servicename], check=True) return True except subprocess.CalledProcessError: # If a service is not running we get a status code != 0 and @@ -101,8 +99,7 @@ def service_is_enabled(service_name, strict_check=False): """ try: - process = run(['systemctl', 'is-enabled', service_name], check=True, - stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + process = run(['systemctl', 'is-enabled', service_name], check=True) if not strict_check: return True @@ -179,14 +176,14 @@ def service_get_logs(service_name: str) -> str: command = [ 'journalctl', '--no-pager', '--lines=200', '--unit', service_name ] - process = run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False) return process.stdout.decode() def service_show(service_name: str) -> dict[str, str]: """Return the status of the service in dictionary format.""" command = ['systemctl', 'show', service_name] - process = run(command, check=False, stdout=subprocess.PIPE) + process = run(command, check=False) status = {} for line in process.stdout.decode().splitlines(): parts = line.partition('=') @@ -197,8 +194,7 @@ def service_show(service_name: str) -> dict[str, str]: def service_action(service_name: str, action: str, check: bool = False): """Perform the given action on the service_name.""" - run(['systemctl', action, service_name], stdout=subprocess.DEVNULL, - check=check) + run(['systemctl', action, service_name], check=check) def webserver_is_enabled(name, kind='config'): @@ -469,8 +465,7 @@ def is_disk_image(): return os.path.exists('/var/lib/freedombox/is-freedombox-disk-image') -def run_apt_command(arguments, stdout=subprocess.DEVNULL, - enable_triggers: bool = False): +def run_apt_command(arguments, enable_triggers: bool = False): """Run apt-get with provided arguments.""" command = ['apt-get', '--assume-yes', '--quiet=2'] + arguments @@ -478,8 +473,7 @@ def run_apt_command(arguments, stdout=subprocess.DEVNULL, env['DEBIAN_FRONTEND'] = 'noninteractive' if not enable_triggers: env['FREEDOMBOX_INVOKED'] = 'true' - process = run(command, stdin=subprocess.DEVNULL, stdout=stdout, env=env, - check=False) + process = run(command, stdin=subprocess.DEVNULL, env=env, check=False) return process.returncode @@ -535,8 +529,7 @@ def apt_hold_freedombox(): def apt_unhold_freedombox(): """Remove any hold on freedombox package, and clear flag.""" - run(['apt-mark', 'unhold', 'freedombox'], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=False) + run(['apt-mark', 'unhold', 'freedombox'], check=False) if apt_hold_flag.exists(): apt_hold_flag.unlink() diff --git a/plinth/conftest.py b/plinth/conftest.py index dee221380..a67589b22 100644 --- a/plinth/conftest.py +++ b/plinth/conftest.py @@ -6,7 +6,6 @@ pytest configuration for all tests. import importlib import os import pathlib -import subprocess from unittest.mock import patch import pytest @@ -188,7 +187,8 @@ def fixture_mock_run_as_user(): """A fixture to override action_utils.run_as_user.""" def _bypass_runuser(*args, username, **kwargs): - return subprocess.run(*args, **kwargs) + from plinth import action_utils + return action_utils.run(*args, **kwargs) with patch('plinth.action_utils.run_as_user') as mock: mock.side_effect = _bypass_runuser diff --git a/plinth/modules/backups/privileged.py b/plinth/modules/backups/privileged.py index 132cd4f27..a2276ead4 100644 --- a/plinth/modules/backups/privileged.py +++ b/plinth/modules/backups/privileged.py @@ -244,8 +244,7 @@ def init(path: str, encryption: str, @privileged def info(path: str, encryption_passphrase: secret_str | None = None) -> dict: """Show repository information.""" - process = _run(['borg', 'info', '--json', path], encryption_passphrase, - stdout=subprocess.PIPE) + process = _run(['borg', 'info', '--json', path], encryption_passphrase) return json.loads(process.stdout.decode()) @@ -255,7 +254,7 @@ def list_repo(path: str, encryption_passphrase: secret_str | None = None) -> dict: """List repository contents.""" process = _run(['borg', 'list', '--json', '--format="{comment}"', path], - encryption_passphrase, stdout=subprocess.PIPE) + encryption_passphrase) return json.loads(process.stdout.decode()) @@ -281,7 +280,7 @@ def remove_uploaded_archive(file_path: str): def _get_borg_version(): """Return the version of borgbackup.""" - process = _run(['borg', '--version'], stdout=subprocess.PIPE) + process = _run(['borg', '--version']) return process.stdout.decode().split()[1] # Example: "borg 1.1.9" @@ -322,10 +321,7 @@ def _extract(archive_path, destination, encryption_passphrase, locations=None): try: os.chdir(os.path.expanduser(destination)) - # TODO: with python 3.7 use subprocess.run with the 'capture_output' - # argument - process = _run(borg_call, encryption_passphrase, check=False, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = _run(borg_call, encryption_passphrase, check=False) if process.returncode != 0: error = process.stderr.decode() # Don't fail on the borg error when no files were matched @@ -350,8 +346,7 @@ def export_tar(path: str, encryption_passphrase: secret_str | None = None): def _read_archive_file(archive, filepath, encryption_passphrase): """Read the content of a file inside an archive.""" borg_call = ['borg', 'extract', archive, filepath, '--stdout'] - return _run(borg_call, encryption_passphrase, - stdout=subprocess.PIPE).stdout.decode() + return _run(borg_call, encryption_passphrase).stdout.decode() @reraise_known_errors @@ -365,8 +360,7 @@ def get_archive_apps( 'borg', 'list', path, manifest_folder, '--format', '{path}{NEWLINE}' ] try: - borg_process = _run(borg_call, encryption_passphrase, - stdout=subprocess.PIPE) + borg_process = _run(borg_call, encryption_passphrase) manifest_path = borg_process.stdout.decode().strip() except subprocess.CalledProcessError: raise RuntimeError('Borg exited unsuccessfully') diff --git a/plinth/modules/calibre/privileged.py b/plinth/modules/calibre/privileged.py index 86d320aa1..75829b569 100644 --- a/plinth/modules/calibre/privileged.py +++ b/plinth/modules/calibre/privileged.py @@ -3,7 +3,6 @@ import pathlib import shutil -import subprocess from plinth import action_utils from plinth.actions import privileged @@ -30,7 +29,7 @@ def create_library(name: str): library.mkdir(mode=0o755) # Raise exception if already exists action_utils.run( ['calibredb', '--with-library', library, 'list_categories'], - stdout=subprocess.DEVNULL, check=False) + check=False) # Force systemd StateDirectory= logic to assign proper ownership to the # DynamicUser= diff --git a/plinth/modules/datetime/privileged.py b/plinth/modules/datetime/privileged.py index 8e435bfcd..666f79245 100644 --- a/plinth/modules/datetime/privileged.py +++ b/plinth/modules/datetime/privileged.py @@ -1,8 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """Set time zone with timedatectl.""" -import subprocess - from plinth import action_utils from plinth.actions import privileged @@ -11,4 +9,4 @@ from plinth.actions import privileged def set_timezone(timezone: str): """Set time zone with timedatectl.""" command = ['timedatectl', 'set-timezone', timezone] - action_utils.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) diff --git a/plinth/modules/email/postfix.py b/plinth/modules/email/postfix.py index 2f6b4a57c..f0700546c 100644 --- a/plinth/modules/email/postfix.py +++ b/plinth/modules/email/postfix.py @@ -111,7 +111,7 @@ def _run(args): Raise a RuntimeError on non-zero exit codes. """ try: - result = action_utils.run(args, check=True, capture_output=True) + result = action_utils.run(args, check=True) return result.stdout.decode() except subprocess.SubprocessError as subprocess_error: raise RuntimeError('Subprocess failed') from subprocess_error diff --git a/plinth/modules/email/privileged/dkim.py b/plinth/modules/email/privileged/dkim.py index 4fa401efc..3ffec7aba 100644 --- a/plinth/modules/email/privileged/dkim.py +++ b/plinth/modules/email/privileged/dkim.py @@ -7,7 +7,6 @@ See: https://rspamd.com/doc/modules/dkim_signing.html import pathlib import re import shutil -import subprocess from plinth import action_utils from plinth.actions import privileged @@ -33,8 +32,7 @@ def get_dkim_public_key(domain: str) -> str: key_file = _keys_dir / f'{domain}.dkim.key' output = action_utils.run( ['openssl', 'rsa', '-in', - str(key_file), '-pubout'], stderr=subprocess.DEVNULL, - check=True).stdout + str(key_file), '-pubout'], check=True).stdout return ''.join(output.decode().splitlines()[1:-1]) diff --git a/plinth/modules/firewall/privileged.py b/plinth/modules/firewall/privileged.py index 9c326d1ba..54ccb5e93 100644 --- a/plinth/modules/firewall/privileged.py +++ b/plinth/modules/firewall/privileged.py @@ -67,8 +67,7 @@ def set_firewall_backend(backend): def _run_firewall_cmd(args): """Run firewall-cmd command, discard output and check return value.""" - action_utils.run(['firewall-cmd'] + args, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['firewall-cmd'] + args, check=True) def _setup_local_service_protection(): @@ -161,7 +160,6 @@ def setup(): """Perform basic firewalld setup.""" action_utils.service_enable('firewalld') action_utils.run(['firewall-cmd', '--set-default-zone=external'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) set_firewall_backend('nftables') diff --git a/plinth/modules/gitweb/privileged.py b/plinth/modules/gitweb/privileged.py index 70e4b765f..bf69f0cb4 100644 --- a/plinth/modules/gitweb/privileged.py +++ b/plinth/modules/gitweb/privileged.py @@ -202,8 +202,7 @@ def _get_default_branch(repo): return action_utils.run_as_user( ['git', '-C', str(repo_path), 'symbolic-ref', '--short', 'HEAD'], - username=REPO_DIR_OWNER, check=True, - stdout=subprocess.PIPE).stdout.decode().strip() + username=REPO_DIR_OWNER, check=True).stdout.decode().strip() def _get_repo_description(repo): @@ -271,8 +270,7 @@ def _get_branches(repo): """Return list of the branches in the repository.""" process = action_utils.run_as_user( ['git', '-C', repo, 'branch', '--format=%(refname:short)'], - cwd=GIT_REPO_PATH, username=REPO_DIR_OWNER, check=True, - stdout=subprocess.PIPE) + cwd=GIT_REPO_PATH, username=REPO_DIR_OWNER, check=True) return process.stdout.decode().strip().split() diff --git a/plinth/modules/ikiwiki/privileged.py b/plinth/modules/ikiwiki/privileged.py index 798c8581b..92b3bb086 100644 --- a/plinth/modules/ikiwiki/privileged.py +++ b/plinth/modules/ikiwiki/privileged.py @@ -5,7 +5,6 @@ import os import pathlib import re import shutil -import subprocess from plinth import action_utils from plinth.actions import privileged, secret_str @@ -63,9 +62,8 @@ def create_wiki(wiki_name: str, admin_name: str, admin_password: secret_str): pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes action_utils.run(['ikiwiki', '-setup', SETUP_WIKI, wiki_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, - env=dict(os.environ, PERL_UNICODE='AS'), check=True) + input=input_, env=dict(os.environ, + PERL_UNICODE='AS'), check=True) @privileged @@ -74,9 +72,7 @@ def create_blog(blog_name: str, admin_name: str, admin_password: secret_str): pw_bytes = admin_password.encode() input_ = pw_bytes + b'\n' + pw_bytes action_utils.run(['ikiwiki', '-setup', SETUP_BLOG, blog_name, admin_name], - stdout=subprocess.PIPE, input=input_, - stderr=subprocess.PIPE, env=dict(os.environ, - PERL_UNICODE='AS')) + input=input_, env=dict(os.environ, PERL_UNICODE='AS')) @privileged diff --git a/plinth/modules/mediawiki/privileged.py b/plinth/modules/mediawiki/privileged.py index cfdcc6f7d..06004922a 100644 --- a/plinth/modules/mediawiki/privileged.py +++ b/plinth/modules/mediawiki/privileged.py @@ -27,8 +27,7 @@ def get_php_command(): version = '' try: - process = action_utils.run(['dpkg', '-s', 'php'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['dpkg', '-s', 'php'], check=True) for line in process.stdout.decode().splitlines(): if line.startswith('Version:'): version = line.split(':')[-1].split('+')[0].strip() diff --git a/plinth/modules/mumble/privileged.py b/plinth/modules/mumble/privileged.py index 1469f6d55..f602087e1 100644 --- a/plinth/modules/mumble/privileged.py +++ b/plinth/modules/mumble/privileged.py @@ -4,7 +4,6 @@ Configure Mumble server. """ import pathlib -import subprocess import augeas @@ -38,7 +37,7 @@ def check_setup() -> bool: def set_super_user_password(password: secret_str): """Set the superuser password with murmurd command.""" action_utils.run(['murmurd', '-readsupw'], input=password.encode(), - stdout=subprocess.DEVNULL, check=False) + check=False) @privileged diff --git a/plinth/modules/nextcloud/privileged.py b/plinth/modules/nextcloud/privileged.py index 51813bf43..4a373f203 100644 --- a/plinth/modules/nextcloud/privileged.py +++ b/plinth/modules/nextcloud/privileged.py @@ -73,14 +73,13 @@ def setup(): def _run_in_container( - *args, capture_output: bool = False, check: bool = True, + *args, check: bool = True, env: dict[str, str] | None = None) -> subprocess.CompletedProcess: """Run a command inside the container.""" env_args = [f'--env={key}={value}' for key, value in (env or {}).items()] command = ['podman', 'exec', '--user', WWW_DATA_UID ] + env_args + [CONTAINER_NAME] + list(args) - return action_utils.run(command, capture_output=capture_output, - check=check) + return action_utils.run(command, check=check) def _run_occ(*args, **kwargs) -> subprocess.CompletedProcess: @@ -110,8 +109,7 @@ def disable(): def get_override_domain(): """Return the domain name that Nextcloud is configured to override with.""" try: - domain = _run_occ('config:system:get', 'overwritehost', - capture_output=True) + domain = _run_occ('config:system:get', 'overwritehost') return domain.stdout.decode().strip() except subprocess.CalledProcessError: return None @@ -160,8 +158,7 @@ def get_default_phone_region(): """"Get the value of default_phone_region.""" try: default_phone_region = _run_occ('config:system:get', - 'default_phone_region', - capture_output=True) + 'default_phone_region') return default_phone_region.stdout.decode().strip() except subprocess.CalledProcessError: return None @@ -247,7 +244,7 @@ def _nextcloud_wait_until_ready(): def _nextcloud_get_status(): """Return Nextcloud status such installed, in maintenance, etc.""" - output = _run_occ('status', '--output=json', capture_output=True) + output = _run_occ('status', '--output=json') return json.loads(output.stdout) @@ -282,8 +279,7 @@ def _configure_ldap(): # Check if LDAP has already been configured. This is necessary because # if the setup proccess is rerun when updating the FredomBox app another # redundant LDAP config would be created. - output = _run_occ('ldap:test-config', 's01', capture_output=True, - check=False) + output = _run_occ('ldap:test-config', 's01', check=False) if 'Invalid configID' in output.stdout.decode(): _run_occ('ldap:create-empty-config') @@ -406,8 +402,7 @@ def _get_database_password(): code = 'if (file_exists("/var/www/html/config/config.php")) {' \ 'include_once("/var/www/html/config/config.php");' \ 'print($CONFIG["dbpassword"] ?? ""); }' - return _run_in_container('php', '-r', code, - capture_output=True).stdout.decode().strip() + return _run_in_container('php', '-r', code).stdout.decode().strip() def _create_redis_config(): diff --git a/plinth/modules/openvpn/privileged.py b/plinth/modules/openvpn/privileged.py index 5679a5660..9fb620a85 100644 --- a/plinth/modules/openvpn/privileged.py +++ b/plinth/modules/openvpn/privileged.py @@ -112,7 +112,7 @@ def _setup_firewall(): try: process = action_utils.run( ['firewall-cmd', '--zone', 'internal', '--list-interfaces'], - stdout=subprocess.PIPE, check=True) + check=True) return 'tun+' in process.stdout.decode().strip().split() except subprocess.CalledProcessError: return True # Safer @@ -164,7 +164,7 @@ def _is_renewable(cert_name): process = action_utils.run( ['openssl', 'x509', '-noout', '-enddate', '-in', - str(cert_path)], check=True, stdout=subprocess.PIPE) + str(cert_path)], check=True) date_string = process.stdout.decode().strip().partition('=')[2] cert_expiry_time = datetime.datetime.strptime(date_string, '%b %d %H:%M:%S %Y GMT') diff --git a/plinth/modules/samba/privileged.py b/plinth/modules/samba/privileged.py index db407a596..3e2e1d6e1 100644 --- a/plinth/modules/samba/privileged.py +++ b/plinth/modules/samba/privileged.py @@ -105,7 +105,7 @@ def _create_share_name(mount_point): def _define_open_share(name, path, windows_filesystem=False): """Define an open samba share.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=y']) @@ -117,7 +117,7 @@ def _define_open_share(name, path, windows_filesystem=False): def _define_group_share(name, path, windows_filesystem=False): """Define a group samba share.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass _conf_command(['addshare', name, path, 'writeable=y', 'guest_ok=n']) @@ -130,7 +130,7 @@ def _define_group_share(name, path, windows_filesystem=False): def _define_homes_share(name, path): """Define a samba share for private homes.""" try: - _conf_command(['delshare', name], stderr=subprocess.DEVNULL) + _conf_command(['delshare', name]) except subprocess.CalledProcessError: pass userpath = os.path.join(path, '%u') diff --git a/plinth/modules/snapshot/privileged.py b/plinth/modules/snapshot/privileged.py index ec3385b09..f899b86ee 100644 --- a/plinth/modules/snapshot/privileged.py +++ b/plinth/modules/snapshot/privileged.py @@ -4,7 +4,6 @@ import os import pathlib import signal -import subprocess import augeas import dbus @@ -21,7 +20,7 @@ def setup(old_version: int): """Configure snapper.""" # Check if root config exists. command = ['snapper', 'list-configs'] - process = action_utils.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) output = process.stdout.decode() # Create root config if needed. @@ -137,8 +136,7 @@ def _remove_fstab_entry(mount_point): def _systemd_path_escape(path): """Escape a string using systemd path rules.""" - process = action_utils.run(['systemd-escape', '--path', path], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['systemd-escape', '--path', path], check=True) return process.stdout.decode().strip() @@ -146,8 +144,7 @@ def _get_subvolume_path(mount_point): """Return the subvolume path for .snapshots in a filesystem.""" # -o causes the list of subvolumes directly under the given mount point process = action_utils.run( - ['btrfs', 'subvolume', 'list', '-o', mount_point], - stdout=subprocess.PIPE, check=True) + ['btrfs', 'subvolume', 'list', '-o', mount_point], check=True) for line in process.stdout.decode().splitlines(): entry = line.split() @@ -224,8 +221,7 @@ def _parse_number(number): @privileged def list_() -> list[dict[str, str]]: """List snapshots.""" - process = action_utils.run(['snapper', 'list'], stdout=subprocess.PIPE, - check=True) + process = action_utils.run(['snapper', 'list'], check=True) lines = process.stdout.decode().splitlines() keys = ('number', 'is_default', 'is_active', 'type', 'pre_number', 'date', @@ -247,7 +243,7 @@ def list_() -> list[dict[str, str]]: def _get_default_snapshot(): """Return the default snapshot by looking at default subvolume.""" command = ['btrfs', 'subvolume', 'get-default', '/'] - process = action_utils.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) output = process.stdout.decode() output_parts = output.split() @@ -297,7 +293,7 @@ def set_config(config: list[str]): def _get_config(): command = ['snapper', 'get-config'] - process = action_utils.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) lines = process.stdout.decode().splitlines() config = {} for line in lines[2:]: diff --git a/plinth/modules/sogo/privileged.py b/plinth/modules/sogo/privileged.py index 1767ee525..5e22cee61 100644 --- a/plinth/modules/sogo/privileged.py +++ b/plinth/modules/sogo/privileged.py @@ -4,7 +4,6 @@ import pathlib import re import shutil -import subprocess import tempfile from plinth import action_utils, utils @@ -145,7 +144,7 @@ def set_domain(domain: str): def _get_config_value(key: str) -> str: """Return the value of a property from the configuration file.""" process = action_utils.run(['plget', key], input=CONFIG_FILE.read_bytes(), - stdout=subprocess.PIPE, check=True) + check=True) return process.stdout.decode().strip() diff --git a/plinth/modules/storage/privileged.py b/plinth/modules/storage/privileged.py index 234739ba5..04f4e10d1 100644 --- a/plinth/modules/storage/privileged.py +++ b/plinth/modules/storage/privileged.py @@ -90,8 +90,7 @@ def _resize_ext4(device, requested_partition, _free_space, _mount_point): requested_partition['number']) try: command = ['resize2fs', partition_device] - action_utils.run(command, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -100,7 +99,7 @@ def _resize_btrfs(_device, _requested_partition, _free_space, mount_point='/'): """Resize a btrfs file system inside a partition.""" try: command = ['btrfs', 'filesystem', 'resize', 'max', mount_point] - action_utils.run(command, stdout=subprocess.DEVNULL, check=True) + action_utils.run(command, check=True) except subprocess.CalledProcessError as exception: raise RuntimeError(f'Error expanding filesystem: {exception}') @@ -167,7 +166,7 @@ def _get_partitions_and_free_spaces(device, partition_number): command = [ 'parted', '--machine', '--script', device, 'unit', 'B', 'print', 'free' ] - process = action_utils.run(command, stdout=subprocess.PIPE, check=True) + process = action_utils.run(command, check=True) requested_partition = None free_spaces = [] diff --git a/plinth/modules/upgrades/distupgrade.py b/plinth/modules/upgrades/distupgrade.py index 0e978c6d6..e9024727e 100644 --- a/plinth/modules/upgrades/distupgrade.py +++ b/plinth/modules/upgrades/distupgrade.py @@ -72,7 +72,7 @@ distribution_info: dict = { def _apt_run(arguments: list[str]): """Run an apt command and ensure that output is written to stdout.""" - returncode = action_utils.run_apt_command(arguments, stdout=None) + returncode = action_utils.run_apt_command(arguments) if returncode: raise RuntimeError( f'Apt command failed with return code: {returncode}') diff --git a/plinth/modules/upgrades/privileged.py b/plinth/modules/upgrades/privileged.py index 8b82976ae..bec033389 100644 --- a/plinth/modules/upgrades/privileged.py +++ b/plinth/modules/upgrades/privileged.py @@ -132,8 +132,7 @@ def release_held_packages(): check=True).stdout.decode().strip() holds = output.split('\n') logger.info('Releasing package holds: %s', holds) - action_utils.run(['apt-mark', 'unhold', *holds], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=True) + action_utils.run(['apt-mark', 'unhold', *holds], check=True) @privileged diff --git a/plinth/modules/upgrades/tests/test_distupgrade.py b/plinth/modules/upgrades/tests/test_distupgrade.py index 89bc0ca1b..84efd6a8c 100644 --- a/plinth/modules/upgrades/tests/test_distupgrade.py +++ b/plinth/modules/upgrades/tests/test_distupgrade.py @@ -24,7 +24,7 @@ def test_apt_run(run): distupgrade._apt_run(args) assert run.call_args.args == \ (['apt-get', '--assume-yes', '--quiet=2'] + args,) - assert not run.call_args.kwargs['stdout'] + assert run.call_args.kwargs['stdout'] == subprocess.PIPE run.return_value.returncode = 10 with pytest.raises(RuntimeError): @@ -305,9 +305,8 @@ def test_apt_hold_packages(run, tmp_path): stderr=subprocess.PIPE, check=True), call(['apt-mark', 'unhold', 'package2'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True), - call(['apt-mark', 'unhold', 'freedombox'], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=False), + call(['apt-mark', 'unhold', 'freedombox'], stdout=subprocess.PIPE, + stderr=subprocess.PIPE, check=False), ] assert run.call_args_list == expected_call diff --git a/plinth/modules/users/privileged.py b/plinth/modules/users/privileged.py index 0d7ad0bd3..a676f2c65 100644 --- a/plinth/modules/users/privileged.py +++ b/plinth/modules/users/privileged.py @@ -148,7 +148,7 @@ def _create_organizational_unit(unit): action_utils.run([ 'ldapsearch', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', distinguished_name, '(objectclass=*)' - ], stdout=subprocess.DEVNULL, check=True) + ], check=True) return # Already exists except subprocess.CalledProcessError: input = ''' @@ -158,7 +158,7 @@ objectClass: organizationalUnit ou: {unit}'''.format(unit=unit) action_utils.run( ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - input=input.encode(), stdout=subprocess.DEVNULL, check=True) + input=input.encode(), check=True) def _setup_admin(): @@ -167,7 +167,7 @@ def _setup_admin(): 'ldapsearch', '-Q', '-L', '-L', '-L', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-s', 'base', '-b', 'olcDatabase={1}mdb,cn=config', '(objectclass=*)', 'olcRootDN', 'olcRootPW' - ], check=True, stdout=subprocess.PIPE) + ], check=True) ldap_object = {} for line in process.stdout.decode().splitlines(): if line: @@ -177,7 +177,7 @@ def _setup_admin(): if 'olcRootPW' in ldap_object: action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify delete: olcRootPW''') @@ -186,7 +186,7 @@ delete: olcRootPW''') if ldap_object['olcRootDN'] != root_dn: action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: olcDatabase={1}mdb,cn=config changetype: modify replace: olcRootDN @@ -207,7 +207,7 @@ def _setup_ldap_ppolicy() -> bool: try: action_utils.run( ['ldapmodify', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], - check=True, stdout=subprocess.DEVNULL, input=b''' + check=True, input=b''' dn: cn=module{0},cn=config changetype: modify add: olcModuleLoad @@ -221,7 +221,7 @@ olcModuleLoad: ppolicy''') action_utils.run([ 'ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///', '-f', '/etc/ldap/schema/namedobject.ldif' - ], check=True, stdout=subprocess.DEVNULL) + ], check=True) except subprocess.CalledProcessError as error: if error.returncode != 80: # Schema already added raise @@ -230,7 +230,7 @@ olcModuleLoad: ppolicy''') try: action_utils.run( ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, - stdout=subprocess.DEVNULL, input=b''' + input=b''' dn: cn=DefaultPPolicy,ou=policies,dc=thisbox cn: DefaultPPolicy objectClass: pwdPolicy @@ -246,7 +246,7 @@ pwdLockout: TRUE''') try: action_utils.run( ['ldapadd', '-Q', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], check=True, - stdout=subprocess.DEVNULL, input=b''' + input=b''' dn: olcOverlay={0}ppolicy,olcDatabase={1}mdb,cn=config objectClass: olcOverlayConfig objectClass: olcPPolicyConfig @@ -456,7 +456,7 @@ def rename_user(old_username: str, new_username: str): def _set_user_password(username, password): """Set a user's password.""" - process = _run(['slappasswd', '-s', password], stdout=subprocess.PIPE) + process = _run(['slappasswd', '-s', password]) password = process.stdout.decode().strip() _run(['ldapsetpasswd', username, password]) @@ -468,7 +468,7 @@ def _set_samba_user(username, password): """ proc = action_utils.run(['smbpasswd', '-a', '-s', username], input='{0}\n{0}\n'.format(password).encode(), - stderr=subprocess.PIPE, check=False) + check=False) if proc.returncode != 0: raise RuntimeError('Unable to add Samba user: ', proc.stderr) @@ -514,7 +514,7 @@ def _get_admin_users(): def _get_user_ids(username: str) -> str | None: """Get user information in format like `id` command.""" try: - process = _run(['ldapid', username], stdout=subprocess.PIPE) + process = _run(['ldapid', username]) except subprocess.CalledProcessError as error: if error.returncode == 1: # User doesn't exist @@ -533,7 +533,7 @@ def _user_exists(username): def _get_group_users(groupname): """Return list of members in the group.""" try: - process = _run(['ldapgid', '-P', groupname], stdout=subprocess.PIPE) + process = _run(['ldapgid', '-P', groupname]) except subprocess.CalledProcessError: return [] # Group does not exist @@ -709,6 +709,4 @@ def _flush_cache(): def _run(arguments, check=True, **kwargs): """Run a command. Check return code and suppress output by default.""" env = dict(os.environ, LDAPSCRIPTS_CONF=LDAPSCRIPTS_CONF) - kwargs['stdout'] = kwargs.get('stdout', subprocess.DEVNULL) - kwargs['stderr'] = kwargs.get('stderr', subprocess.DEVNULL) return action_utils.run(arguments, env=env, check=check, **kwargs) diff --git a/plinth/modules/zoph/privileged.py b/plinth/modules/zoph/privileged.py index a65974402..00c136dbb 100644 --- a/plinth/modules/zoph/privileged.py +++ b/plinth/modules/zoph/privileged.py @@ -33,15 +33,13 @@ def get_configuration() -> dict[str, str]: """Return the current configuration.""" configuration = {} try: - process = action_utils.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], check=True) except subprocess.CalledProcessError as exception: if exception.returncode != 96: raise _zoph_setup_cli_user() - process = action_utils.run(['zoph', '--dump-config'], - stdout=subprocess.PIPE, check=True) + process = action_utils.run(['zoph', '--dump-config'], check=True) for line in process.stdout.decode().splitlines(): name, value = line.partition(':')[::2] @@ -146,8 +144,7 @@ def is_configured() -> bool | None: """Return whether zoph app is configured.""" try: process = action_utils.run( - ['zoph', '--get-config', 'interface.user.remote'], - stdout=subprocess.PIPE, check=True) + ['zoph', '--get-config', 'interface.user.remote'], check=True) return process.stdout.decode().strip() == 'true' except (FileNotFoundError, subprocess.CalledProcessError): return None diff --git a/plinth/tests/test_daemon.py b/plinth/tests/test_daemon.py index bcc43c513..2fe05179c 100644 --- a/plinth/tests/test_daemon.py +++ b/plinth/tests/test_daemon.py @@ -81,59 +81,55 @@ def test_is_enabled(service_is_enabled, daemon): @patch('subprocess.run') def test_enable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that enabling the daemon works.""" - common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, - check=False) - common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, - check=False) + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) daemon.enable() subprocess_run.assert_has_calls( - [call(['systemctl', 'enable', 'test-unit'], **common_args1)]) + [call(['systemctl', 'enable', 'test-unit'], **common_args)]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - **common_args2) + **common_args) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.enable() subprocess_run.assert_has_calls([ - call(['systemctl', 'enable', 'test-unit'], **common_args1), - call(['systemctl', 'start', 'test-unit'], **common_args2), - call(['systemctl', 'enable', 'test-unit-2'], **common_args1), - call(['systemctl', 'start', 'test-unit-2'], **common_args2), + call(['systemctl', 'enable', 'test-unit'], **common_args), + call(['systemctl', 'start', 'test-unit'], **common_args), + call(['systemctl', 'enable', 'test-unit-2'], **common_args), + call(['systemctl', 'start', 'test-unit-2'], **common_args), ]) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit'], - **common_args2) + **common_args) subprocess_run.assert_any_call(['systemctl', 'start', 'test-unit-2'], - **common_args2) + **common_args) @patch('plinth.app.apps_init') @patch('subprocess.run') def test_disable(subprocess_run, apps_init, app_list, mock_privileged, daemon): """Test that disabling the daemon works.""" - common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, - check=False) - common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, - check=False) + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) daemon.disable() subprocess_run.assert_has_calls( - [call(['systemctl', 'disable', 'test-unit'], **common_args1)]) + [call(['systemctl', 'disable', 'test-unit'], **common_args)]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - **common_args2) + **common_args) subprocess_run.reset_mock() daemon.alias = 'test-unit-2' daemon.disable() subprocess_run.assert_has_calls([ - call(['systemctl', 'disable', 'test-unit'], **common_args1), - call(['systemctl', 'stop', 'test-unit'], **common_args2), - call(['systemctl', 'disable', 'test-unit-2'], **common_args1), - call(['systemctl', 'stop', 'test-unit-2'], **common_args2), + call(['systemctl', 'disable', 'test-unit'], **common_args), + call(['systemctl', 'stop', 'test-unit'], **common_args), + call(['systemctl', 'disable', 'test-unit-2'], **common_args), + call(['systemctl', 'stop', 'test-unit-2'], **common_args), ]) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit'], - **common_args2) + **common_args) subprocess_run.assert_any_call(['systemctl', 'stop', 'test-unit-2'], - **common_args2) + **common_args) @patch('plinth.action_utils.service_is_running') @@ -153,10 +149,8 @@ def test_is_running(service_is_running, daemon): def test_ensure_running(subprocess_run, service_is_running, apps_init, app_list, mock_privileged, daemon): """Test that checking that the daemon is running works.""" - common_args1 = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, - check=False) - common_args2 = dict(stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, - check=False) + common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, + check=False) service_is_running.return_value = True with daemon.ensure_running() as starting_state: assert starting_state @@ -168,14 +162,14 @@ def test_ensure_running(subprocess_run, service_is_running, apps_init, with daemon.ensure_running() as starting_state: assert not starting_state assert subprocess_run.mock_calls == [ - call(['systemctl', 'enable', 'test-unit'], **common_args1), - call(['systemctl', 'start', 'test-unit'], **common_args2), + call(['systemctl', 'enable', 'test-unit'], **common_args), + call(['systemctl', 'start', 'test-unit'], **common_args), ] subprocess_run.reset_mock() assert subprocess_run.mock_calls == [ - call(['systemctl', 'disable', 'test-unit'], **common_args1), - call(['systemctl', 'stop', 'test-unit'], **common_args2), + call(['systemctl', 'disable', 'test-unit'], **common_args), + call(['systemctl', 'stop', 'test-unit'], **common_args), ] From f19ab6855334694c32de6324f3cee42b863ae93d Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Sep 2025 08:47:59 -0700 Subject: [PATCH 29/44] ci: Switch backports test to trixie-backports Tests: - None. Failing CI pipeline show be fixed. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5e07e227..832400875 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -56,7 +56,7 @@ build: build-backports: extends: .build-package variables: - RELEASE: bookworm-backports + RELEASE: trixie-backports build i386: extends: .build-package-i386 From 279738c305d5281ac90bbaa114851b50d7665426 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Sep 2025 13:28:47 -0700 Subject: [PATCH 30/44] actions: Raise an exception if privileged server response is empty - These situation occur when server encounters an error when trying to formulate a response. All exceptions during execution of actions are caught and reported properly. However, server may encounter errors during processing of exception raised in an action. Or may die abruptly. This special error will make identifying such situations easier. Tests: - Add a 'return' after _read_request() in privileged_daemon.py:RequestHandler:handle(). This will trigger this error. Starting FreedomBox service will show these errors as 'ConnectionError: Server returned empty response'. Similarly running 'freedombox-cmd --no-args plinth is_package_manager_busy' will show the same error. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plinth/actions.py b/plinth/actions.py index d045c4000..0e75fc0f4 100644 --- a/plinth/actions.py +++ b/plinth/actions.py @@ -88,6 +88,9 @@ def _read_from_server(client_socket: socket.socket) -> bytes: response += chunk + if not response: + raise ConnectionError('Server returned empty response') + return json.loads(response) From 397a67329b23b7ac85a4548782c95a71fcc81aa0 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Sep 2025 15:46:55 -0700 Subject: [PATCH 31/44] debian: Stop privileged service during upgrade or removal - The privileged service will stop by itself if left idle for 5 minutes. However, if someone is viewing a reloading page such as during manual software update, the privileged service is never idle. - When freedombox package is updated to a newer version, the old version of privileged daemon could run for a long time but newer version of freedombox service might be running by then. This would cause protocol mismatch problems (unless backwards compatibility is provided which is unnecessarily hard). - Adding PartOf=.socket in .service file means that if .socket unit is stopped or restarted, the .service unit will be stopped or restarted too. We still don't want the dh_installsystemd script to be starting the .service unit, so this is ideal. Tests: - During fresh install of freedombox package, freedombox-privilged.socket is started but freedombox-privileged.service is not. It is started due to socket activation (as seen in journal logs of privileged daemon). - During removal of freedombox package, .service is stopped when .socket unit is stopped. - During reinstall of freedombox package, .service is restarted when .socket unit is restarted. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- data/usr/lib/systemd/system/freedombox-privileged.service | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/usr/lib/systemd/system/freedombox-privileged.service b/data/usr/lib/systemd/system/freedombox-privileged.service index fe255c94c..68aaebda8 100644 --- a/data/usr/lib/systemd/system/freedombox-privileged.service +++ b/data/usr/lib/systemd/system/freedombox-privileged.service @@ -5,6 +5,8 @@ Description=FreedomBox Privileged Service Documentation=https://wiki.debian.org/FreedomBox/ # Don't hit the start rate limiting. StartLimitIntervalSec=0 +# Stop/restart along with .socket unit (invoked from dpkg scripts). +PartOf=freedombox-privileged.socket [Service] Type=notify From f2bceb48cf41c8c44fbc66114fd525d11535c72c Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 26 Sep 2025 16:55:34 -0700 Subject: [PATCH 32/44] backups: Don't show enable/disable button as app can't be disabled Fixes: #2472. Tests: - On backups page, the enable/disable toggle button is not visible anymore. Signed-off-by: Sunil Mohan Adapa Reviewed-by: Veiko Aasa --- plinth/modules/backups/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plinth/modules/backups/__init__.py b/plinth/modules/backups/__init__.py index 6b1542055..66a231110 100644 --- a/plinth/modules/backups/__init__.py +++ b/plinth/modules/backups/__init__.py @@ -35,6 +35,8 @@ class BackupsApp(app_module.App): _version = 3 + can_be_disabled = False + def __init__(self) -> None: """Create components for the app.""" super().__init__() From b559e1998a05ead83731732dbec922dcca4a45fc Mon Sep 17 00:00:00 2001 From: Dietmar Date: Sun, 28 Sep 2025 20:10:11 +0200 Subject: [PATCH 33/44] Translated using Weblate (German) Currently translated at 98.4% (1849 of 1879 strings) --- plinth/locale/de/LC_MESSAGES/django.po | 36 ++++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/plinth/locale/de/LC_MESSAGES/django.po b/plinth/locale/de/LC_MESSAGES/django.po index bec2e9ef8..eda0b7877 100644 --- a/plinth/locale/de/LC_MESSAGES/django.po +++ b/plinth/locale/de/LC_MESSAGES/django.po @@ -10,7 +10,7 @@ msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-11 18:01+0000\n" +"PO-Revision-Date: 2025-09-29 19:01+0000\n" "Last-Translator: Dietmar \n" "Language-Team: German \n" @@ -1680,6 +1680,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Diese App zeigt auch die Protokolle für {box_name}" +" Dienste an." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -10687,46 +10689,34 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"Dies ist ein interner Fehler und nicht etwas, das Sie verursacht haben oder " -"beheben können. Bitte melden Sie den Fehler im online Fehlermelder, so dass wir ihn beheben können. Fügen Sie auch das Statusprotokoll dem Fehlerbericht bei." +"Dies ist ein interner Fehler, den Sie weder verursacht haben noch beheben " +"können. Bitte melden Sie den Fehler im Bug-Tracker, damit wir ihn beheben können. Fügen Sie dem Fehlerbericht bitte auch die " +"Protokolle bei." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Installation" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Dies sind die letzten %(num_lines)s Zeilen des Statusprotokolls der " -"Weboberfläche. Bitte melden Sie den Fehler im online Fehlerverfolger und fügen diese Ausgabe hinzu." +"Dies sind die letzten Zeilen der Protokolle für Dienste, die an dieser App " +"beteiligt sind. Wenn Sie einen Fehler melden möchten, verwenden Sie bitte " +"den Bug-Tracker und fügen Sie dieses Protokoll dem Fehlerbericht bei." #: plinth/templates/app-logs.html:26 msgid "" From 1c0f25c134347548d75db20010d4abeac6eeb918 Mon Sep 17 00:00:00 2001 From: Dietmar Date: Sun, 28 Sep 2025 20:13:51 +0200 Subject: [PATCH 34/44] Translated using Weblate (Italian) Currently translated at 47.2% (887 of 1879 strings) --- plinth/locale/it/LC_MESSAGES/django.po | 34 +++++++++----------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/plinth/locale/it/LC_MESSAGES/django.po b/plinth/locale/it/LC_MESSAGES/django.po index eb2d8b562..7da6b163a 100644 --- a/plinth/locale/it/LC_MESSAGES/django.po +++ b/plinth/locale/it/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-11 18:02+0000\n" +"PO-Revision-Date: 2025-09-29 19:02+0000\n" "Last-Translator: Dietmar \n" "Language-Team: Italian \n" @@ -1617,6 +1617,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Questa app mostra anche i log per i servizi " +"{box_name}." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -9739,13 +9741,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the logs to the bug report." msgstr "" "Si tratta di un errore interno e non di qualcosa che hai causato o che puoi " -"correggere. Si prega di segnalare l'errore sul bug tracker in modo " -"da poterlo correggere. Inoltre, si prega di allegare il status log alla segnalazione del bug." +"correggere. Si prega di segnalare l'errore sul bug tracker in modo da poterlo correggere. Inoltre, si prega di allegare i status log alla segnalazione del bug." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Queste sono le ultime %(num_lines)s del status log di questa interfaccia " -"web. Se vuoi riportare un bug, prego usa il bug tracker e " -"allega questo status log report del bug." +"Queste sono le ultime righe dei status log di questa app. Se vuoi riportare " +"un bug, prego usa il bug tracker e allega questo status log report del bug." #: plinth/templates/app-logs.html:26 msgid "" From e38ac648fed2a795dc53278596e326df0fc1202c Mon Sep 17 00:00:00 2001 From: Roman Akimov Date: Mon, 29 Sep 2025 09:53:01 +0200 Subject: [PATCH 35/44] Translated using Weblate (Russian) Currently translated at 98.6% (1854 of 1879 strings) --- plinth/locale/ru/LC_MESSAGES/django.po | 125 +++++++++---------------- 1 file changed, 44 insertions(+), 81 deletions(-) diff --git a/plinth/locale/ru/LC_MESSAGES/django.po b/plinth/locale/ru/LC_MESSAGES/django.po index d202f1f77..9f53f254f 100644 --- a/plinth/locale/ru/LC_MESSAGES/django.po +++ b/plinth/locale/ru/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-09-18 10:01+0000\n" +"PO-Revision-Date: 2025-09-29 19:02+0000\n" "Last-Translator: Roman Akimov \n" "Language-Team: Russian \n" @@ -213,7 +213,7 @@ msgstr "Местный" #: plinth/modules/avahi/manifest.py:14 msgid "mDNS" -msgstr "mDNS" +msgstr "" #: plinth/modules/backups/__init__.py:24 msgid "Backups allows creating and managing backup archives." @@ -503,7 +503,7 @@ msgstr "Конфигурация" #: plinth/modules/backups/manifest.py:21 msgid "Borg" -msgstr "Borg" +msgstr "" #: plinth/modules/backups/privileged.py:34 msgid "" @@ -935,7 +935,7 @@ msgstr "Список и чтение всех файлов" #: plinth/modules/bepasty/__init__.py:57 plinth/modules/bepasty/manifest.py:6 msgid "bepasty" -msgstr "bepasty" +msgstr "" #: plinth/modules/bepasty/forms.py:17 msgid "Public Access (default permissions)" @@ -977,7 +977,7 @@ msgstr "Обмен файлами" #: plinth/modules/bepasty/manifest.py:23 msgid "Pastebin" -msgstr "Pastebin" +msgstr "" #: plinth/modules/bepasty/templates/bepasty.html:12 msgid "Manage Passwords" @@ -1658,6 +1658,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Это приложение также показывает журналы для " +"{box_name} ." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -2455,7 +2457,7 @@ msgstr "Thunderbird" #: plinth/modules/email/manifest.py:37 msgid "Thunderbird Mobile" -msgstr "Thunderbird Mobile" +msgstr "" #: plinth/modules/email/manifest.py:52 msgid "FairEmail" @@ -2531,8 +2533,6 @@ msgid "Host/Target/Value" msgstr "Хост/Цель/Значение" #: plinth/modules/email/templates/email-dns.html:50 -#, fuzzy -#| msgid "Server hostname or IP address" msgid "Reverse DNS Records for IP Addresses" msgstr "Обратные записи DNS для IP-адресов" @@ -2566,8 +2566,6 @@ msgstr "" "разделе. Это можно настроить в приложении конфиденциальности." #: plinth/modules/email/templates/email-dns.html:76 -#, fuzzy -#| msgid "Hostname" msgid "Host" msgstr "Хост" @@ -2584,8 +2582,7 @@ msgstr "" "записей DNS домена." #: plinth/modules/email/templates/email.html:35 -#, fuzzy, python-format -#| msgid "Resolve domain name: {domain}" +#, python-format msgid "View domain: %(domain)s" msgstr "Просмотр домена: %(domain)s" @@ -2705,9 +2702,8 @@ msgstr "Веб-сайт" #: plinth/modules/featherwiki/manifest.py:18 #: plinth/modules/tiddlywiki/manifest.py:25 -#, fuzzy msgid "Quine" -msgstr "Quine" +msgstr "" #: plinth/modules/featherwiki/manifest.py:18 #: plinth/modules/nextcloud/manifest.py:56 @@ -3247,8 +3243,6 @@ msgstr "" "силу." #: plinth/modules/gnome/__init__.py:48 -#, fuzzy -#| msgid "GNOME Files" msgid "GNOME" msgstr "GNOME" @@ -3257,8 +3251,6 @@ msgid "Desktop" msgstr "Десктоп" #: plinth/modules/gnome/manifest.py:10 -#, fuzzy -#| msgid "Tor Browser" msgid "Browser" msgstr "Браузер" @@ -3267,8 +3259,6 @@ msgid "Office suite" msgstr "Офисный пакет" #: plinth/modules/gnome/manifest.py:12 -#, fuzzy -#| msgid "Software Update" msgid "Software store" msgstr "Магазин программного обеспечения" @@ -3658,12 +3648,6 @@ msgid "{box_name} Manual" msgstr "Руководство {box_name}" #: plinth/modules/homeassistant/__init__.py:31 -#, fuzzy -#| msgid "" -#| "Home Assistant is a home automation hub with emphasis on local control " -#| "and privacy. It integrates with thousands of devices including smart " -#| "bulbs, alarms, presense sensors, door bells, thermostats, irrigation " -#| "timers, energy monitors, etc." msgid "" "Home Assistant is a home automation hub with emphasis on local control and " "privacy. It integrates with thousands of devices including smart bulbs, " @@ -3727,9 +3711,8 @@ msgid "Home Automation" msgstr "Домашняя автоматизация" #: plinth/modules/homeassistant/manifest.py:63 -#, fuzzy msgid "IoT" -msgstr "IoT" +msgstr "" #: plinth/modules/homeassistant/manifest.py:64 #: plinth/modules/networks/manifest.py:8 @@ -3742,11 +3725,11 @@ msgstr "Wi-Fi" #: plinth/modules/homeassistant/manifest.py:65 msgid "ZigBee" -msgstr "ZigBee" +msgstr "" #: plinth/modules/homeassistant/manifest.py:66 msgid "Z-Wave" -msgstr "Z-Wave" +msgstr "" #: plinth/modules/homeassistant/manifest.py:67 msgid "Thread" @@ -3955,7 +3938,7 @@ msgstr "Видеокомната Janus" #: plinth/modules/janus/manifest.py:16 msgid "WebRTC" -msgstr "WebRTC" +msgstr "" #: plinth/modules/janus/manifest.py:16 msgid "Web conference" @@ -4038,7 +4021,7 @@ msgstr "Управление сервером контента Kiwix" #: plinth/modules/kiwix/__init__.py:56 plinth/modules/kiwix/manifest.py:8 msgid "Kiwix" -msgstr "Kiwix" +msgstr "" #: plinth/modules/kiwix/forms.py:23 msgid "Content packages have to be in .zim format" @@ -4302,6 +4285,10 @@ msgid "" "only be installed if frequent feature updates is enabled in the Software Update app." msgstr "" +"Примечание: Это приложение часто получает обновления. Оно " +"может быть установлено только в том случае, если в системе включено частое " +"обновление функций. Обновление программного " +"обеспечения ." #: plinth/modules/matrixsynapse/__init__.py:59 msgid "Matrix Synapse" @@ -4775,8 +4762,6 @@ msgid "UPnP" msgstr "UPnP" #: plinth/modules/minidlna/manifest.py:116 -#, fuzzy -#| msgid "MiniDLNA" msgid "DLNA" msgstr "DLNA" @@ -4814,9 +4799,8 @@ msgstr "" #: plinth/modules/miniflux/__init__.py:42 #: plinth/modules/miniflux/manifest.py:10 -#, fuzzy msgid "Miniflux" -msgstr "Miniflux" +msgstr "" #: plinth/modules/miniflux/forms.py:12 msgid "Enter a username for the user." @@ -4839,28 +4823,24 @@ msgid "Passwords do not match." msgstr "Пароли не совпадают." #: plinth/modules/miniflux/manifest.py:18 -#, fuzzy msgid "Fluent Reader Lite" -msgstr "Fluent Reader Lite" +msgstr "" #: plinth/modules/miniflux/manifest.py:33 msgid "Fluent Reader" msgstr "Беглое чтение" #: plinth/modules/miniflux/manifest.py:46 -#, fuzzy msgid "FluxNews" -msgstr "FluxNews" +msgstr "" #: plinth/modules/miniflux/manifest.py:61 -#, fuzzy msgid "MiniFlutt" -msgstr "MiniFlutt" +msgstr "" #: plinth/modules/miniflux/manifest.py:71 -#, fuzzy msgid "NetNewsWire" -msgstr "NetNewsWire" +msgstr "" #: plinth/modules/miniflux/manifest.py:86 msgid "Newsflash" @@ -4884,14 +4864,11 @@ msgstr "Агрегация новостей" #: plinth/modules/miniflux/manifest.py:138 #: plinth/modules/rssbridge/manifest.py:16 plinth/modules/ttrss/manifest.py:55 -#, fuzzy -#| msgid "SSH" msgid "RSS" msgstr "RSS" #: plinth/modules/miniflux/manifest.py:138 #: plinth/modules/rssbridge/manifest.py:16 plinth/modules/ttrss/manifest.py:55 -#, fuzzy msgid "ATOM" msgstr "ATOM" @@ -5201,7 +5178,7 @@ msgstr "Ссылка" #: plinth/modules/names/templates/names.html:104 #: plinth/modules/networks/templates/connection_show.html:268 msgid "DNS-over-TLS" -msgstr "DNS-over-TLS" +msgstr "" #: plinth/modules/names/templates/names.html:108 msgid "DNSSEC" @@ -6249,7 +6226,7 @@ msgstr "локальная ссылка" #: plinth/modules/networks/views.py:32 msgid "dhcp" -msgstr "dhcp" +msgstr "" #: plinth/modules/networks/views.py:33 msgid "ignore" @@ -6509,7 +6486,7 @@ msgstr "" #: plinth/modules/nextcloud/manifest.py:11 #: plinth/modules/nextcloud/manifest.py:18 msgid "Nextcloud" -msgstr "Nextcloud" +msgstr "" #: plinth/modules/nextcloud/forms.py:19 msgid "Not set" @@ -7233,11 +7210,11 @@ msgstr "Контакты" #: plinth/modules/radicale/manifest.py:91 plinth/modules/sogo/manifest.py:75 msgid "CalDAV" -msgstr "CalDAV" +msgstr "" #: plinth/modules/radicale/manifest.py:91 plinth/modules/sogo/manifest.py:76 msgid "CardDAV" -msgstr "CardDAV" +msgstr "" #: plinth/modules/radicale/views.py:32 msgid "Access rights configuration updated" @@ -7833,7 +7810,7 @@ msgstr "Точка входа" #: plinth/modules/shadowsocks/manifest.py:22 #: plinth/modules/shadowsocksserver/manifest.py:21 msgid "Shadowsocks" -msgstr "Shadowsocks" +msgstr "" #: plinth/modules/shadowsocksserver/__init__.py:26 #, python-brace-format @@ -8102,7 +8079,7 @@ msgstr "Заведомо исправное состояние" #: plinth/modules/snapshot/manifest.py:14 msgid "Btrfs" -msgstr "Btrfs" +msgstr "" #: plinth/modules/snapshot/templates/snapshot_delete_selected.html:12 msgid "Delete the following snapshots permanently?" @@ -8730,7 +8707,7 @@ msgstr "" #: plinth/modules/tiddlywiki/__init__.py:64 #: plinth/modules/tiddlywiki/manifest.py:9 msgid "TiddlyWiki" -msgstr "TiddlyWiki" +msgstr "" #: plinth/modules/tiddlywiki/forms.py:39 msgid "A TiddlyWiki file with .html file extension" @@ -9103,7 +9080,7 @@ msgstr "TTRSS-читатель" #: plinth/modules/ttrss/manifest.py:25 msgid "Geekttrss" -msgstr "Geekttrss" +msgstr "" #: plinth/modules/upgrades/__init__.py:34 msgid "Check for and apply the latest software and security updates." @@ -9367,7 +9344,7 @@ msgstr "Автоматическое обновление отключено." #: plinth/modules/upgrades/templates/upgrades-dist-upgrade.html:54 msgid "Distribution upgrades are disabled." -msgstr "Обновления дистрибутива отключены" +msgstr "Обновления дистрибутива отключены." #: plinth/modules/upgrades/templates/upgrades-dist-upgrade.html:58 msgid "" @@ -10543,13 +10520,7 @@ msgid "500" msgstr "500" #: plinth/templates/500.html:14 -#, fuzzy, python-format -#| msgid "" -#| "This is an internal error and not something you caused or can fix. Please " -#| "report the error on the bug tracker so we can fix it. Also, please " -#| "attach the status log to the bug " -#| "report." +#, python-format msgid "" "This is an internal error and not something you caused or can fix. Please " "report the error on the logs to the bug report." msgstr "" "Это внутренняя ошибка, а не то, что вы вызвали или можете исправить. " -"Сообщите об ошибке в трекере ошибок, чтобы мы могли ее исправить. Также " -"приложите журнал состояния к отчету об " -"ошибке." +"Пожалуйста, сообщите об ошибке на трекере " +"ошибок чтобы мы могли это исправить. Кроме того, пожалуйста, приложите " +"файлы журналов к сообщению об ошибке." #: plinth/templates/app-header.html:26 msgid "Installation" msgstr "Установка" #: plinth/templates/app-logs.html:12 -#, fuzzy -#| msgid "" -#| "These are the last %(num_lines)s lines of the status log for this web " -#| "interface. If you want to report a bug, please use the bug tracker and " -#| "attach this status log to the bug report." msgid "" "These are the last lines of the logs for services involved in this app. If " "you want to report a bug, please use the bug tracker and attach this log to " "the bug report." msgstr "" -"Это последние %(num_lines)s строк журнала состояния для этого веб-" -"интерфейса. Если вы хотите сообщить об ошибке, используйте трекер ошибок и " -"прикрепите этот журнал состояния к отчету об ошибке." +"Это последние строки журналов для служб, задействованных в этом приложении. " +"Если вы хотите сообщить об ошибке, пожалуйста, воспользуйтесь отслеживаним " +"ошибок и приложите этот журнал к отчету об ошибке." #: plinth/templates/app-logs.html:26 msgid "" @@ -10844,10 +10809,8 @@ msgid "Clear all tags" msgstr "Очистить все теги" #: plinth/templates/toolbar.html:39 plinth/templates/toolbar.html:40 -#, fuzzy -#| msgid "Logs" msgid "View Logs" -msgstr "Журналы" +msgstr "Просмотр журналов" #: plinth/templates/toolbar.html:46 plinth/templates/toolbar.html:47 msgid "Backup" From dc837bd6b8189ffe8838b6a7291a0c78871d3a3d Mon Sep 17 00:00:00 2001 From: Veiko Aasa Date: Tue, 23 Sep 2025 12:25:43 +0300 Subject: [PATCH 36/44] gitweb: Use Git credential helper when cloning URLs with credentials This prevents logging usernames and passwords to the journal logs and to the Git repo configuration. Also, avoids usernames and passwords appear in the process list when cloning a repository. Tests performed: - Create a new repository by cloning an existing repository URL with basic auth credentials. Check that: - Cloning succeeds. - Journal logs don't contain URLs with credential info. - The configuration of the cloned repository doesn't contain credential info. - Try to clone a non-existing repository URL that contains credential info. Cloning fails and there are no credential info in the journal logs. - Cloning a public git repository without credential info succeeds. - All the gitweb module tests pass. Signed-off-by: Veiko Aasa [sunil: Add/fix some more type hints] [sunil: Add tests for URL parsing] Signed-off-by: Sunil Mohan Adapa Reviewed-by: Sunil Mohan Adapa --- plinth/modules/gitweb/__init__.py | 2 +- plinth/modules/gitweb/privileged.py | 78 +++++++++++++------ .../modules/gitweb/tests/test_privileged.py | 25 ++++++ 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/plinth/modules/gitweb/__init__.py b/plinth/modules/gitweb/__init__.py index 3dcd79829..00c751e7a 100644 --- a/plinth/modules/gitweb/__init__.py +++ b/plinth/modules/gitweb/__init__.py @@ -34,7 +34,7 @@ class GitwebApp(app_module.App): app_id = 'gitweb' - _version = 3 + _version = 4 def __init__(self) -> None: """Create components for the app.""" diff --git a/plinth/modules/gitweb/privileged.py b/plinth/modules/gitweb/privileged.py index bf69f0cb4..332d42af5 100644 --- a/plinth/modules/gitweb/privileged.py +++ b/plinth/modules/gitweb/privileged.py @@ -4,14 +4,16 @@ import configparser import logging import os +import pathlib import re import shutil import subprocess import time from typing import Any +from urllib import parse from plinth import action_utils -from plinth.actions import privileged +from plinth.actions import privileged, secret_str from plinth.modules.gitweb.forms import RepositoryValidator, get_name_from_url from plinth.modules.gitweb.manifest import GIT_REPO_PATH, REPO_DIR_OWNER @@ -27,7 +29,7 @@ def validate_repo_name(name: str) -> str: return name -def validate_repo_url(url: str) -> str: +def validate_repo_url(url: secret_str) -> secret_str: """Validate a repository URL.""" RepositoryValidator(input_should_be='url')(url) return url @@ -35,33 +37,58 @@ def validate_repo_url(url: str) -> str: @privileged def setup(): - """Disable default Apache2 Gitweb configuration.""" + """Configure Gitweb module.""" + # Disable default Apache2 Gitweb configuration. action_utils.webserver_disable('gitweb') - if not _get_global_default_branch(): - _set_global_default_branch('main') + + # Configure Git client. + if not _get_git_global_config('init.defaultBranch'): + _set_git_global_config('init.defaultBranch', 'main') + if not _get_git_global_config('credential.helper'): + _set_git_global_config('credential.helper', 'cache') -def _get_global_default_branch(): - """Get globally configured default branch name.""" +def _get_git_global_config(key: str) -> str | None: + """Return a value from Git global configuration.""" try: - default_branch = action_utils.run( - ['git', 'config', '--global', '--get', 'init.defaultBranch'], - check=True).stdout.decode().strip() + value = action_utils.run(['git', 'config', '--global', '--get', key], + check=True).stdout.decode().strip() except subprocess.CalledProcessError as exception: - if exception.returncode == 1: # Default branch not configured + if exception.returncode == 1: # Configuration option doesn't exist return None raise - return default_branch + return value -def _set_global_default_branch(name): - """Configure default branch name globally.""" - action_utils.run(['git', 'config', '--global', 'init.defaultBranch', name], - check=True) +def _set_git_global_config(key: str, value: str) -> None: + """Set a Git global configuration value.""" + action_utils.run(['git', 'config', '--global', key, value], check=True) -def _clone_with_progress_report(url, repo_dir): +def _setup_git_credentials(url: secret_str) -> str: + """Set up git credential helper and return URL without credentials.""" + url_parts = parse.urlsplit(url) + safe_netloc = url_parts.netloc.split('@')[-1] + safe_url = url_parts._replace(netloc=safe_netloc).geturl() + username = url_parts.username or '' + password = url_parts.password or '' + + if username or password: + # Feed credentials to Git credential helper + input = (f'protocol={url_parts.scheme}\n' + f'host={safe_netloc}\n' + f'username={username}\n' + f'password={password}\n\n') + env = dict(os.environ, GIT_TERMINAL_PROMPT='0') + action_utils.run(['git', 'credential', 'approve'], + input=input.encode(), stdout=subprocess.DEVNULL, + check=True, env=env) + + return safe_url + + +def _clone_with_progress_report(url: secret_str, repo_dir: pathlib.Path): """Clone a repository and write progress info to the file.""" starttime = time.time() status_file = repo_dir / 'clone_progress' @@ -70,9 +97,12 @@ def _clone_with_progress_report(url, repo_dir): env = dict(os.environ, GIT_TERMINAL_PROMPT='0', LC_ALL='C', GIT_HTTP_LOW_SPEED_LIMIT='100', GIT_HTTP_LOW_SPEED_TIME='60') + safe_url = _setup_git_credentials(url) + logger.info(f'Cloning Git repository {safe_url} ...') proc = subprocess.Popen( - ['git', 'clone', '--bare', '--progress', url, + ['git', 'clone', '--bare', '--progress', safe_url, str(repo_temp_dir)], stderr=subprocess.PIPE, text=True, env=env) + assert proc.stderr is not None # write clone progress to the file errors = [] @@ -108,7 +138,7 @@ def _clone_with_progress_report(url, repo_dir): raise RuntimeError('Git repository cloning failed.', errors) -def _prepare_clone_repo(url: str, is_private: bool): +def _prepare_clone_repo(url: secret_str, is_private: bool): """Prepare cloning a repository.""" repo_name = get_name_from_url(url) if not repo_name.endswith('.git'): @@ -150,7 +180,8 @@ def _clone_status_line_to_percent(line): return None -def _clone_repo(url: str, description: str, owner: str, keep_ownership: bool): +def _clone_repo(url: secret_str, description: str, owner: str, + keep_ownership: bool): """Clone a repository.""" repo = get_name_from_url(url) if not repo.endswith('.git'): @@ -343,7 +374,7 @@ def repo_info(name: str) -> dict[str, str]: @privileged -def create_repo(url: str | None = None, name: str | None = None, +def create_repo(url: secret_str | None = None, name: str | None = None, description: str = '', owner: str = '', keep_ownership: bool = False, is_private: bool = False, skip_prepare: bool = False, prepare_only: bool = False): @@ -365,12 +396,13 @@ def create_repo(url: str | None = None, name: str | None = None, @privileged -def repo_exists(url: str) -> bool: +def repo_exists(url: secret_str) -> bool: """Return whether remote repository exists.""" url = validate_repo_url(url) + safe_url = _setup_git_credentials(url) env = dict(os.environ, GIT_TERMINAL_PROMPT='0') try: - action_utils.run(['git', 'ls-remote', url, 'HEAD'], timeout=10, + action_utils.run(['git', 'ls-remote', safe_url, 'HEAD'], timeout=10, env=env, check=True) return True except subprocess.CalledProcessError: diff --git a/plinth/modules/gitweb/tests/test_privileged.py b/plinth/modules/gitweb/tests/test_privileged.py index f9ddf6d12..cc884c501 100644 --- a/plinth/modules/gitweb/tests/test_privileged.py +++ b/plinth/modules/gitweb/tests/test_privileged.py @@ -2,6 +2,8 @@ """Test module for gitweb module operations.""" import pathlib +import subprocess +from unittest.mock import call, patch import pytest from django.forms import ValidationError @@ -121,3 +123,26 @@ def test_action_create_repo_with_invalid_urls(url): with pytest.raises(ValidationError): privileged.create_repo(url=url, description='', owner='', keep_ownership=True) + + +@patch('plinth.action_utils.run') +def test_setup_git_creentials(run): + """Test that setting up git credentials works.""" + url = 'https://user:pass@host.example/path?key=value' + safe_url = privileged._setup_git_credentials(url) + assert safe_url == 'https://host.example/path?key=value' + + input_ = b'protocol=https\nhost=host.example\nusername=user\n' \ + b'password=pass\n\n' + env = run.mock_calls[0].kwargs.pop('env') + assert env['GIT_TERMINAL_PROMPT'] == '0' + assert run.mock_calls == [ + call(['git', 'credential', 'approve'], input=input_, + stdout=subprocess.DEVNULL, check=True) + ] + + run.reset_mock() + url = 'https://host2.example/path?key=value' + safe_url = privileged._setup_git_credentials(url) + assert safe_url == 'https://host2.example/path?key=value' + run.assert_not_called() From 4ed2a25a8baf768c0e430f7f2be3a2d37cb59912 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 29 Sep 2025 16:43:30 -0700 Subject: [PATCH 37/44] locale: Fix a string formatting issue in Italian translation Signed-off-by: Sunil Mohan Adapa --- plinth/locale/it/LC_MESSAGES/django.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plinth/locale/it/LC_MESSAGES/django.po b/plinth/locale/it/LC_MESSAGES/django.po index 7da6b163a..6410ab0a3 100644 --- a/plinth/locale/it/LC_MESSAGES/django.po +++ b/plinth/locale/it/LC_MESSAGES/django.po @@ -9752,7 +9752,7 @@ msgstr "" "correggere. Si prega di segnalare l'errore sul bug tracker in modo da poterlo correggere. Inoltre, si prega di allegare i status log alla segnalazione del bug." +"\"%(logs_url)s\">status log alla segnalazione del bug." #: plinth/templates/app-header.html:26 msgid "Installation" From f9ca06dc5f66ae246df561435fd4e2c12d923ee9 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Sep 2025 16:25:28 -0700 Subject: [PATCH 38/44] daemon: When ensuring running state handle not-installed state Tests: - Uninstall miniflux and postgresql. Install freshly with all the patches in this series. When installing miniflux freshly, postgresql is not disabled soon after miniflux package is installed. Without this patch, postgresql is disabled after packages are installed leading to a setup failure. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/daemon.py | 7 +++++++ plinth/tests/test_daemon.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/plinth/daemon.py b/plinth/daemon.py index 9c8fba8d9..8b91a8148 100644 --- a/plinth/daemon.py +++ b/plinth/daemon.py @@ -91,6 +91,13 @@ class Daemon(app.LeaderComponent, log.LogEmitter): def ensure_running(self): """Ensure a service is running and return to previous state.""" from plinth.privileged import service as service_privileged + + if action_utils.service_show(self.unit)['LoadState'] == 'not-found': + # The service's package not installed yet, don't try to start it + # and later stop it after it is installed. + yield False # Not running + return + starting_state = self.is_running() if not starting_state: service_privileged.enable(self.unit) diff --git a/plinth/tests/test_daemon.py b/plinth/tests/test_daemon.py index 2fe05179c..879774db2 100644 --- a/plinth/tests/test_daemon.py +++ b/plinth/tests/test_daemon.py @@ -145,12 +145,21 @@ def test_is_running(service_is_running, daemon): @patch('plinth.app.apps_init') @patch('plinth.action_utils.service_is_running') +@patch('plinth.action_utils.service_show') @patch('subprocess.run') -def test_ensure_running(subprocess_run, service_is_running, apps_init, - app_list, mock_privileged, daemon): +def test_ensure_running(subprocess_run, service_show, service_is_running, + apps_init, app_list, mock_privileged, daemon): """Test that checking that the daemon is running works.""" common_args = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False) + + service_show.return_value = {'LoadState': 'not-found'} + with daemon.ensure_running() as starting_state: + assert not starting_state + assert subprocess_run.mock_calls == [] + + service_show.return_value = {'LoadState': 'loaded'} + service_is_running.return_value = True with daemon.ensure_running() as starting_state: assert starting_state From 60c57b67073ca6a230d8f7a2da805ad03f181884 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Sep 2025 16:39:00 -0700 Subject: [PATCH 39/44] miniflux: Fix DB connection issues during install/uninstall Fixes: #2536. Multiple fixes: - When miniflux and postgresql are install simultaneously, miniflux setup may be installed before postgresql is started. - When postgresql is already installed and disabled (due to a previous uninstall), then postgresql may not be running during miniflux package installation (and fail initial DB setup). - When app is being installed while it is disabled, the database may not running and may lead to failure in removing the app database. Tests: - Run functional tests on stable/testing twice in a row. - Install the app without postgresql or miniflux installed. - Disable the app and uninstall it. DB is purged. - Uninstall and re-install (with postgresql is disabled during installed). Reviewed-by: James Valleroy --- plinth/modules/miniflux/__init__.py | 27 +++++++++++++++++++++++---- plinth/modules/miniflux/privileged.py | 9 +++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/plinth/modules/miniflux/__init__.py b/plinth/modules/miniflux/__init__.py index ab1461d53..1a4083379 100644 --- a/plinth/modules/miniflux/__init__.py +++ b/plinth/modules/miniflux/__init__.py @@ -58,7 +58,12 @@ class MinifluxApp(app_module.App): login_required=True) self.add(shortcut) - packages = Packages('packages-miniflux', ['miniflux', 'postgresql']) + # miniflux package does not depend on postgres. Install postgresql, + # start it and only then install miniflux. + packages = Packages('packages-miniflux-postgresql', ['postgresql']) + self.add(packages) + + packages = Packages('packages-miniflux', ['miniflux']) self.add(packages) drop_in_configs = DropinConfigs( @@ -74,7 +79,7 @@ class MinifluxApp(app_module.App): urls=['https://{host}/miniflux/']) self.add(webserver) - daemon = SharedDaemon('shared-daemon-miniflus-postgresql', + daemon = SharedDaemon('shared-daemon-miniflux-postgresql', 'postgresql') self.add(daemon) @@ -89,7 +94,17 @@ class MinifluxApp(app_module.App): def setup(self, old_version=None): """Install and configure the app.""" privileged.pre_setup() - super().setup(old_version) + + # Database needs to be running for successful initialization or upgrade + # of miniflux database. 1. The app and database are being freshly + # installed. 2. The app is being freshly installed, but database server + # is already installed but disabled. 3. The app is being updated to new + # version but is currently disabled (DB is installed but disabled or + # enabled). + with self.get_component( + 'shared-daemon-miniflux-postgresql').ensure_running(): + super().setup(old_version) + privileged.setup(old_version) if not old_version: self.enable() @@ -97,7 +112,11 @@ class MinifluxApp(app_module.App): def uninstall(self): """De-configure and uninstall the app.""" privileged.uninstall() - super().uninstall() + with self.get_component( + 'shared-daemon-miniflux-postgresql').ensure_running(): + # Database needs to be running for successful removal miniflux + # database. + super().uninstall() class MinifluxBackupRestore(BackupRestore): diff --git a/plinth/modules/miniflux/privileged.py b/plinth/modules/miniflux/privileged.py index 1018e53f4..e30b09170 100644 --- a/plinth/modules/miniflux/privileged.py +++ b/plinth/modules/miniflux/privileged.py @@ -123,8 +123,13 @@ def reset_user_password(username: str, password: secret_str): @privileged def uninstall(): """Ensure that the database is removed.""" - action_utils.debconf_set_selections( - ['miniflux miniflux/purge boolean true']) + action_utils.debconf_set_selections([ + 'miniflux miniflux/purge boolean true', + 'miniflux miniflux/dbconfig-install boolean true', + 'miniflux miniflux/dbconfig-reinstall boolean true' + 'miniflux miniflux/dbconfig-upgrade boolean true', + 'miniflux miniflux/dbconfig-remove boolean true', + ]) def _get_database_config(): From 43ff0b57ce2c739f816b45586505f39fdc53273f Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 23 Sep 2025 20:03:00 -0700 Subject: [PATCH 40/44] zoph: Additional dbconfig configuration keys Tests: - Install, uninstall and re-run setup work. - Functional tests work. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/zoph/privileged.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plinth/modules/zoph/privileged.py b/plinth/modules/zoph/privileged.py index 00c136dbb..b63e7f960 100644 --- a/plinth/modules/zoph/privileged.py +++ b/plinth/modules/zoph/privileged.py @@ -23,6 +23,7 @@ def pre_install(): 'zoph zoph/dbconfig-install boolean true', 'zoph zoph/dbconfig-upgrade boolean true', 'zoph zoph/dbconfig-remove boolean true', + 'zoph zoph/dbconfig-reinstall boolean true' 'zoph zoph/mysql/admin-user string root', 'zoph zoph/rm_images select yes', ]) From cdec8a4af9ab830fc07374a16fbc558a172a77f4 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Sat, 4 Oct 2025 20:07:33 +0200 Subject: [PATCH 41/44] Translated using Weblate (Albanian) Currently translated at 98.3% (1848 of 1879 strings) --- plinth/locale/sq/LC_MESSAGES/django.po | 82 +++++++++----------------- 1 file changed, 27 insertions(+), 55 deletions(-) diff --git a/plinth/locale/sq/LC_MESSAGES/django.po b/plinth/locale/sq/LC_MESSAGES/django.po index 379f32167..bcbab0cd7 100644 --- a/plinth/locale/sq/LC_MESSAGES/django.po +++ b/plinth/locale/sq/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2025-09-23 00:05+0000\n" -"PO-Revision-Date: 2025-06-25 22:04+0000\n" +"PO-Revision-Date: 2025-10-05 18:02+0000\n" "Last-Translator: Besnik Bleta \n" "Language-Team: Albanian \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.13-dev\n" +"X-Generator: Weblate 5.14-dev\n" #: plinth/config.py:103 #, python-brace-format @@ -25,10 +25,9 @@ msgid "Static configuration {etc_path} is setup properly" msgstr "Formësimi statik {etc_path} është ujdisur si duhet" #: plinth/container.py:140 -#, fuzzy, python-brace-format -#| msgid "Service {service_name} is running" +#, python-brace-format msgid "Container {container_name} is running" -msgstr "Shërbimi {service_name} po xhiron" +msgstr "Kontejneri {container_name} po xhiron" #: plinth/context_processors.py:21 plinth/views.py:175 msgid "FreedomBox" @@ -72,10 +71,8 @@ msgid "Repository to backup to" msgstr "Depo për kopjeruajtje" #: plinth/forms.py:62 -#, fuzzy -#| msgid "None" msgid "(None)" -msgstr "Asnjë" +msgstr "(Asnjë)" #: plinth/forms.py:68 msgid "Select a domain name to be used with this application" @@ -707,27 +704,15 @@ msgid "Restore data from" msgstr "Riktheje të dhëna prej" #: plinth/modules/backups/templates/backups_upload.html:17 -#, fuzzy, python-format -#| msgid "" -#| "\n" -#| " Upload a backup file downloaded from another %(box_name)s to " -#| "restore its\n" -#| " contents. You can choose the apps you wish to restore after " -#| "uploading a\n" -#| " backup file.\n" -#| " " +#, python-format msgid "" "Upload a backup file downloaded from another %(box_name)s to restore its " "contents. You can choose the apps you wish to restore after uploading a " "backup file." msgstr "" -"\n" -" Ngarkoni një kartel kopjeruajtje të shkarkuar prej një tjetër " -"%(box_name)s për\n" -" të rikthyer lëndën e tij. Pas ngarkimit të kartelës kopjeruajtje, mund " -"të zgjidhni\n" -" aplikacionet që doni të rikthehen.\n" -" " +"Ngarkoni një kartelë kopjeruajtje të shkarkuar prej një tjetër %(box_name)s " +"për të rikthyer lëndën e tij. Pas ngarkimit të kartelës kopjeruajtje, mund " +"të zgjidhni aplikacionet që doni të rikthehen." #: plinth/modules/backups/templates/backups_upload.html:31 #, python-format @@ -1673,6 +1658,8 @@ msgid "" "This app also shows the logs for {box_name} " "services." msgstr "" +"Ky aplikacion shfaq gjithashtu regjistrat për " +"shërbimet {box_name}." #: plinth/modules/diagnostics/__init__.py:60 #: plinth/modules/diagnostics/__init__.py:254 @@ -1828,31 +1815,31 @@ msgstr "Përfundime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:53 #, python-format msgid "%(number)s passed" -msgstr "" +msgstr "%(number)s të kaluar" #: plinth/modules/diagnostics/templates/diagnostics_full.html:57 #, python-format msgid "%(number)s failed" -msgstr "" +msgstr "%(number)s me dështim" #: plinth/modules/diagnostics/templates/diagnostics_full.html:61 #, python-format msgid "%(number)s warnings" -msgstr "" +msgstr "%(number)s sinjalizime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:65 #, python-format msgid "%(number)s errors" -msgstr "" +msgstr "%(number)s gabime" #: plinth/modules/diagnostics/templates/diagnostics_full.html:69 #, python-format msgid "%(number)s skipped" -msgstr "" +msgstr "%(number)s të anashkaluar" #: plinth/modules/diagnostics/templates/diagnostics_full.html:111 msgid "Running..." -msgstr "" +msgstr "Po xhirohet…" #: plinth/modules/diagnostics/templates/diagnostics_results.html:11 msgid "Test" @@ -1903,13 +1890,6 @@ msgstr "" "tuaj DNS, do të marrë një përgjigje me adresën tuaj aktuale IP." #: plinth/modules/dynamicdns/__init__.py:41 -#, fuzzy -#| msgid "" -#| "If you are looking for a free dynamic DNS account, you may find a free " -#| "GnuDIP service at ddns.freedombox.org or you may find free update URL " -#| "based services at freedns.afraid.org." msgid "" "If you are looking for a free dynamic DNS account, you may find a free " "GnuDIP service at ddns.freedombox.org, ose mund të gjeni shërbime të " -"lira me bazë përditësim URL-je, te freedns.afraid.org." +"shërbim GnuDIP të lirë, te ddns.freedombox.org. Me këtë shërbim, përfitoni nënpërkatësi " +"të pakufizuara (me mundësinë e shenjës së gjithëpushtetshme të aktivizuar te " +"rregullimet e llogarisë). Që të përdorni një nënpërkatësi, shtojeni si një " +"përkatësi statike te aplikacioni Emra." #: plinth/modules/dynamicdns/__init__.py:47 -#, fuzzy -#| msgid "" -#| "If you are looking for a free dynamic DNS account, you may find a free " -#| "GnuDIP service at ddns.freedombox.org or you may find free update URL " -#| "based services at freedns.afraid.org." msgid "" "Alternatively, you may find a free update URL based service at freedns.afraid.org." msgstr "" -"Nëse po kërkoni për një llogari falas DNS-je dinamike, mund të gjeni një " -"shërbim GnuDIP të lirë, te ddns.freedombox.org, ose mund të gjeni shërbime të " -"lira me bazë përditësim URL-je, te freedns.afraid.org." +"Ndryshe, mund të gjeni një shërbim falas të bazuar në përditësim URL-sh, te " +"freedns.afraid.org." #: plinth/modules/dynamicdns/__init__.py:50 msgid "" @@ -2088,7 +2060,7 @@ msgstr "Përkatësi" #: plinth/modules/dynamicdns/manifest.py:17 msgid "Free" -msgstr "" +msgstr "I lirë" #: plinth/modules/dynamicdns/manifest.py:17 msgid "Needs public IP" @@ -2680,7 +2652,7 @@ msgstr "Feather Wiki" #: plinth/modules/featherwiki/forms.py:13 plinth/modules/tiddlywiki/forms.py:13 msgid "Wiki files cannot be named \"index.html\"." -msgstr "" +msgstr "Kartelat Wiki s’mund të emërtohen “index.html”." #: plinth/modules/featherwiki/forms.py:20 plinth/modules/tiddlywiki/forms.py:20 msgid "Name of the wiki file, with file extension \".html\"" From 3c7393cd0715d0aaa4673d3d1abc3323ac48b2c9 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 6 Oct 2025 20:04:53 -0400 Subject: [PATCH 42/44] locale: Update translation strings --- plinth/locale/ar/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/ar_SA/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/be/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/bg/LC_MESSAGES/django.po | 88 +++++++++---------- plinth/locale/bn/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/ca/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/cs/LC_MESSAGES/django.po | 89 ++++++++++---------- plinth/locale/da/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/de/LC_MESSAGES/django.po | 93 +++++++++++---------- plinth/locale/django.pot | 76 ++++++++--------- plinth/locale/el/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/es/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/et/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/fa/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/fake/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/fr/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/gl/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/gu/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/hi/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/hu/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/id/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/it/LC_MESSAGES/django.po | 90 ++++++++++---------- plinth/locale/ja/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/kn/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/lt/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/lv/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/nb/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/nl/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/pl/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/pt/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/ru/LC_MESSAGES/django.po | 90 ++++++++++---------- plinth/locale/si/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/sl/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/sq/LC_MESSAGES/django.po | 90 ++++++++++---------- plinth/locale/sr/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/sv/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/ta/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/te/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/tr/LC_MESSAGES/django.po | 93 ++++++++++----------- plinth/locale/uk/LC_MESSAGES/django.po | 90 ++++++++++---------- plinth/locale/vi/LC_MESSAGES/django.po | 76 ++++++++--------- plinth/locale/zh_Hans/LC_MESSAGES/django.po | 87 ++++++++++--------- plinth/locale/zh_Hant/LC_MESSAGES/django.po | 76 ++++++++--------- 43 files changed, 1696 insertions(+), 1698 deletions(-) diff --git a/plinth/locale/ar/LC_MESSAGES/django.po b/plinth/locale/ar/LC_MESSAGES/django.po index df54c1b5f..6200344c5 100644 --- a/plinth/locale/ar/LC_MESSAGES/django.po +++ b/plinth/locale/ar/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-04-16 02:28+0000\n" "Last-Translator: MohammedSaalif <2300031323@kluniversity.in>\n" "Language-Team: Arabic \n" "Language-Team: Arabic (Saudi Arabia) " -"\n" +"Last-Translator: 109247019824 " +"<109247019824@users.noreply.hosted.weblate.org>\n" "Language-Team: Bulgarian \n" "Language: bg\n" @@ -34,27 +34,27 @@ msgstr "Контейнерът {container_name} работи" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "Услугата {service_name} работи" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Слушане на {kind} порт {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Слушане на {kind} порт {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Свързване с {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Не може да се свърже с {host}:{port}" @@ -218,12 +218,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Създаване и управление на архиви с резервни копия." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Резервни копия" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -232,19 +232,19 @@ msgstr "" "Предпочетете шифровано местоположение за отдалечено архивиране или " "допълнително свързан диск." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Включване на резервни копия по график" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Към {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -254,7 +254,7 @@ msgstr "" "опита за създаване на резервно копие са безуспешни. Последната грешка е: " "{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Грешка при създаване на резервно копие" @@ -1610,53 +1610,53 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Диагностика" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "пропусната" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "преминала" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "неуспех" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "грешка" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "предупреждение" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "МБ" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "ГБ" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "За да намалее използваната памет, трябва да изключите някои приложения." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Не трябва да инсталирате нови приложения." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1665,25 +1665,25 @@ msgstr "" "Системата разполага с малко памет: използвана {percent_used} %, свободна " "{memory_available} {memory_available_unit}. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Паметта е малко" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Диагностициране" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" "По време на обичайната проверка са намерени {issue_count} неизправности." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Резултати от диагностиката" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Резултати от диагностиката" @@ -8407,7 +8407,7 @@ msgstr "" msgid "Distribution Update" msgstr "Обновяване на дистрибуцията" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Проверка за задържани от обновяване пакети" @@ -8828,7 +8828,7 @@ msgstr "Грешка при започване на обновяване." msgid "Frequent feature updates activated." msgstr "Честото обновяване на пакети е включено." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -8839,7 +8839,7 @@ msgstr "" "получи достъп, някои приложения още имат изискване потребителският профил да " "бъде част от определена група." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -8851,25 +8851,25 @@ msgstr "" "администратори могат да променят приложенията и настройките на " "системата." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Потребители и групи" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Достъп до всички услуги и системни настройки" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Проверете записа на LDAP „{search_item}“" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Проверете настройката на nslcd „{key} {value}“" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Проверете настройката на nsswitch „{database}“" @@ -9060,10 +9060,10 @@ msgid "" msgstr "" "За да създадете профил, който може да бъде използван с %(box_name)s, " "премахнете тези профили от командния ред и презаредете страницата. От " -"команднен ред изпълнете командата „echo '{\"args\": " -"[\"USERNAME\", \"AUTH_USER\", \"AUTH_PASSWORD\"], \"kwargs\": {}}' | sudo " -"freedombox-cmd users remove_user“. Ако профилът вече може да се използва с " -"%(box_name)s, прескочете тази стъпка." +"команднен ред изпълнете командата „echo '{\"args\": [\"USERNAME\", " +"\"AUTH_USER\", \"AUTH_PASSWORD\"], \"kwargs\": {}}' | sudo freedombox-cmd " +"users remove_user“. Ако профилът вече може да се използва с %(box_name)s, " +"прескочете тази стъпка." #: plinth/modules/users/templates/users_firstboot.html:69 msgid "Skip this step" diff --git a/plinth/locale/bn/LC_MESSAGES/django.po b/plinth/locale/bn/LC_MESSAGES/django.po index e6d4b1788..74438b562 100644 --- a/plinth/locale/bn/LC_MESSAGES/django.po +++ b/plinth/locale/bn/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-04-01 03:02+0000\n" "Last-Translator: MURALA SAI GANESH \n" "Language-Team: Bengali \n" "Language-Team: Catalan \n" "Language-Team: Czech admin." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Uživatelé a skupiny" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Přístup ke všem službám a nastavení systému" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Zkontrolujte LDAP položku „{search_item}“" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Zkontrolujte konfiguraci nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Zkontrolujte konfiguraci nsswitch \" {database}\"" @@ -10447,10 +10447,9 @@ msgid "" "the logs to the bug report." msgstr "" "Jedná se o interní chybu, kterou jste nezpůsobili ani ji nemůžete opravit. " -"Nahlaste prosím chybu na sledovači " -"chyb, abychom ji mohli opravit. K hlášení chyby prosím připojte také protokoly." +"Nahlaste prosím chybu na sledovači chyb, abychom ji mohli opravit. K hlášení " +"chyby prosím připojte také protokoly." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -10464,9 +10463,9 @@ msgid "" "the bug report." msgstr "" "Toto jsou poslední řádky protokolů služeb souvisejících s touto aplikací. " -"Pokud chcete nahlásit chybu, použijte prosím bug tracker a připojte tento protokol k hlášení o chybě." +"Pokud chcete nahlásit chybu, použijte prosím bug tracker a " +"připojte tento protokol k hlášení o chybě." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/da/LC_MESSAGES/django.po b/plinth/locale/da/LC_MESSAGES/django.po index e8aa26647..4474fff90 100644 --- a/plinth/locale/da/LC_MESSAGES/django.po +++ b/plinth/locale/da/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:19+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Danish \n" "Language-Team: German logs for {box_name} " "services." msgstr "" -"Diese App zeigt auch die Protokolle für {box_name}" -" Dienste an." +"Diese App zeigt auch die Protokolle für " +"{box_name} Dienste an." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Diagnose" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "übersprungen" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "bestanden" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "gescheitert" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "Fehler" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "Warnung" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Sie sollten einige Anwendungen deaktivieren, um den Speicherverbrauch zu " "reduzieren." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Sie sollten auf diesem System keine neuen Anwendungen installieren." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1740,24 +1740,24 @@ msgstr "" "Das System hat wenig Speicherplatz: {percent_used}% verwendet, " "{memory_available}·{memory_available_unit}frei. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Wenig Speicher" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Laufende Diagnosen" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Bei Routinetests wurden {issue_count} Probleme gefunden." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Diagnose-Ergebnisse" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Zu Diagnose-Ergebnisse gehen" @@ -9265,7 +9265,7 @@ msgstr "" msgid "Distribution Update" msgstr "Distributionsaktualisierung" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Auf Paket-Sperren überprüfen" @@ -9695,7 +9695,7 @@ msgstr "Starten der Aktualisierung fehlgeschlagen." msgid "Frequent feature updates activated." msgstr "Häufige Funktions-Updates aktiviert." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9706,7 +9706,7 @@ msgstr "" "muss ein Benutzerkonto Teil einer Gruppe sein, damit ein Benutzer auf die " "App zugreifen kann." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9718,25 +9718,25 @@ msgstr "" "dürfen nur Mitglieder der Gruppe admin Apps oder " "Systemeinstellungen ändern." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Benutzer und Gruppen" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Zugriff auf alle Anwendungen und Systemeinstellungen" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP-Eintrag „{search_item}“ prüfen" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Prüfen Sie die nslcd-Konfiguration \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Prüfen Sie die nsswitch-Konfiguration \"{database}\"" @@ -10697,10 +10697,10 @@ msgid "" "the logs to the bug report." msgstr "" "Dies ist ein interner Fehler, den Sie weder verursacht haben noch beheben " -"können. Bitte melden Sie den Fehler im Bug-Tracker, damit wir ihn beheben können. Fügen Sie dem Fehlerbericht bitte auch die " -"Protokolle bei." +"können. Bitte melden Sie den Fehler im Bug-Tracker, damit wir ihn beheben " +"können. Fügen Sie dem Fehlerbericht bitte auch die Protokolle bei." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -10715,8 +10715,9 @@ msgid "" msgstr "" "Dies sind die letzten Zeilen der Protokolle für Dienste, die an dieser App " "beteiligt sind. Wenn Sie einen Fehler melden möchten, verwenden Sie bitte " -"den Bug-Tracker und fügen Sie dieses Protokoll dem Fehlerbericht bei." +"den Bug-Tracker und fügen Sie dieses Protokoll dem Fehlerbericht " +"bei." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/django.pot b/plinth/locale/django.pot index d9aa4c44a..b48e31f3f 100644 --- a/plinth/locale/django.pot +++ b/plinth/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -31,27 +31,27 @@ msgstr "" msgid "FreedomBox" msgstr "" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "" @@ -206,37 +206,37 @@ msgstr "" msgid "Backups allows creating and managing backup archives." msgstr "" -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." msgstr "" -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " "succeed. The latest error is: {error_message}" msgstr "" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "" @@ -1519,76 +1519,76 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "" -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " "{memory_available_unit} free. {advice_message}" msgstr "" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "" @@ -7987,7 +7987,7 @@ msgstr "" msgid "Distribution Update" msgstr "" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -8330,14 +8330,14 @@ msgstr "" msgid "Frequent feature updates activated." msgstr "" -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " "account to be part of a group to authorize the user to access the app." msgstr "" -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -8345,25 +8345,25 @@ msgid "" "group may alter apps or system settings." msgstr "" -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/el/LC_MESSAGES/django.po b/plinth/locale/el/LC_MESSAGES/django.po index 2671518ea..e13d077d8 100644 --- a/plinth/locale/el/LC_MESSAGES/django.po +++ b/plinth/locale/el/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:20+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Greek admin μπορούν να " "τροποποιήσουν τις εφαρμογές ή τις ρυθμίσεις του συστήματος." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Χρήστες και ομάδες" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Πρόσβαση σε όλες τις υπηρεσίες και τις ρυθμίσεις συστήματος" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Ελέγξτε την καταχώρηση LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/es/LC_MESSAGES/django.po b/plinth/locale/es/LC_MESSAGES/django.po index c4cdf994d..b881c1d50 100644 --- a/plinth/locale/es/LC_MESSAGES/django.po +++ b/plinth/locale/es/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2024-11-01 17:00+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Spanish admin pueden cambiar configuraciones de " "apps o del sistema." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Usuarias/os y grupos" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Acceso a todos los servicios y configuraciones del sistema" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Comprobar la entrada LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Comprobar la configuración de nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Comprueba la configuración del nsswitch \"{database}\"" diff --git a/plinth/locale/et/LC_MESSAGES/django.po b/plinth/locale/et/LC_MESSAGES/django.po index 21fd15e98..dfcee8482 100644 --- a/plinth/locale/et/LC_MESSAGES/django.po +++ b/plinth/locale/et/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-07-20 18:01+0000\n" "Last-Translator: Priit Jõerüüt \n" "Language-Team: Estonian \n" "Language-Team: Persian \n" "Language-Team: Plinth Developers \n" "Language-Team: French admin peuvent modifier les applications ou changer les paramètres système." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Utilisateurs et groupes" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Accès à tous les services et à la configuration du système" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Vérification de l’entrée LDAP « {search_item} »" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Vérifier la configuration nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Vérifier la configuration nsswitch \"{database}\"" diff --git a/plinth/locale/gl/LC_MESSAGES/django.po b/plinth/locale/gl/LC_MESSAGES/django.po index 2d97c0568..29697a651 100644 --- a/plinth/locale/gl/LC_MESSAGES/django.po +++ b/plinth/locale/gl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-12-30 10:51+0000\n" "Last-Translator: gallegonovato \n" "Language-Team: Galician \n" "Language-Team: Gujarati \n" "Language-Team: Hindi \n" @@ -35,27 +35,27 @@ msgstr "A szolgáltatás fut: {service_name}" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "A szolgáltatás fut: {service_name}" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "Figyelés a {kind} porton: {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "Figyelés {kind} porton: {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "Csatlakozás ide: {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "Nem lehet ide csatlakozni: {host}:{port}" @@ -225,12 +225,12 @@ msgstr "mDNS" msgid "Backups allows creating and managing backup archives." msgstr "Lehetővé teszi a biztonsági mentés létrehozását és kezelését." -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "Biztonsági mentések" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." @@ -238,19 +238,19 @@ msgstr "" "Automatikus biztonsági mentések ütemezésének engedélyezése. Ha lehet, " "használj egy titkosított távoli helyet, vagy egy extra külső lemezt." -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "Ütemezett biztonsági mentés engedélyezése" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "Ugrás ide: {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -260,7 +260,7 @@ msgstr "" "mentésre tett próbálkozás nem sikerült. A legutóbbi hibaüzenet: " "{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "Hiba történt a biztonsági mentés közben" @@ -1724,52 +1724,52 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Hibaellenőrzés" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "sikerült" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "sikertelen" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "hiba" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "figyelmeztetés" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "A memóriahasználat csökkentése érdekében tilts le néhány alkalmazást." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Ne telepíts további alkalmazásokat erre a rendszerre." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1778,28 +1778,28 @@ msgstr "" "A rendszerben kevés a memória: {percent_used}% használt, " "{memory_available} {memory_available_unit} szabad. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Kevés a memória" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 #, fuzzy #| msgid "Run Diagnostics" msgid "Running diagnostics" msgstr "Ellenőrzés indítása" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 #, fuzzy #| msgid "Diagnostic Results" msgid "Diagnostics results" msgstr "Az ellenőrzés eredménye" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 #, fuzzy #| msgid "Diagnostic Results" msgid "Go to diagnostics results" @@ -9442,7 +9442,7 @@ msgstr "" msgid "Distribution Update" msgstr "A disztribúció frissítése elindult" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -9851,7 +9851,7 @@ msgstr "A frissítést nem sikerült elindítani." msgid "Frequent feature updates activated." msgstr "Gyakori funkciófrissítések aktiválva." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9862,7 +9862,7 @@ msgstr "" "alkalmazások megkövetelik továbbá, hogy a felhasználói fiók egy csoport " "tagja legyen, hogy a felhasználó hozzáférhessen az alkalmazáshoz." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9874,25 +9874,25 @@ msgstr "" "az admin csoport felhasználói módosíthatják az alkalmazásokat vagy " "a rendszerbeállításokat." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Felhasználók és csoportok" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Hozzáférés az összes szolgáltatáshoz és rendszerbeállításhoz" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP-bejegyzés ellenőrzése: \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/id/LC_MESSAGES/django.po b/plinth/locale/id/LC_MESSAGES/django.po index eee9211b5..6c8c8f3ca 100644 --- a/plinth/locale/id/LC_MESSAGES/django.po +++ b/plinth/locale/id/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Indonesian (FreedomBox)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:19+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Indonesian \n" "Language-Team: Italian logs to the bug report." msgstr "" "Si tratta di un errore interno e non di qualcosa che hai causato o che puoi " -"correggere. Si prega di segnalare l'errore sul bug tracker in modo da poterlo correggere. Inoltre, si prega di allegare i status log alla segnalazione del bug." +"correggere. Si prega di segnalare l'errore sul bug tracker in modo " +"da poterlo correggere. Inoltre, si prega di allegare i status log alla segnalazione del bug." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -9766,9 +9766,9 @@ msgid "" "the bug report." msgstr "" "Queste sono le ultime righe dei status log di questa app. Se vuoi riportare " -"un bug, prego usa il bug tracker e allega questo status log report del bug." +"un bug, prego usa il bug tracker e allega questo status log report del " +"bug." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/ja/LC_MESSAGES/django.po b/plinth/locale/ja/LC_MESSAGES/django.po index 2d5e3c34c..864118bed 100644 --- a/plinth/locale/ja/LC_MESSAGES/django.po +++ b/plinth/locale/ja/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2023-05-07 23:50+0000\n" "Last-Translator: Nobuhiro Iwamatsu \n" "Language-Team: Japanese \n" "Language-Team: Kannada \n" "Language-Team: Lithuanian \n" "Language-Team: Latvian \n" "Language-Team: Norwegian Bokmål admin-gruppen endre programmer eller systeminnstillinger." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Brukere og grupper" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tilgang til alle tjenester og systeminnstillinger" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Sjekk LDAP-oppføring «{search_item}»" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" diff --git a/plinth/locale/nl/LC_MESSAGES/django.po b/plinth/locale/nl/LC_MESSAGES/django.po index 67b282a75..e84914cba 100644 --- a/plinth/locale/nl/LC_MESSAGES/django.po +++ b/plinth/locale/nl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-09-17 09:01+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Dutch admin -groep mogen toepassings- of " "systeeminstellingen wijzigen." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Gebruikers en Groepen" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Toegang tot alle diensten en systeeminstellingen" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Zoek LDAP item \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Controleer nslcd configuratie \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Controleer nsswitch config \"{database}\"" diff --git a/plinth/locale/pl/LC_MESSAGES/django.po b/plinth/locale/pl/LC_MESSAGES/django.po index f937557df..58dc870cb 100644 --- a/plinth/locale/pl/LC_MESSAGES/django.po +++ b/plinth/locale/pl/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2024-07-13 12:09+0000\n" "Last-Translator: Monika \n" "Language-Team: Polish \n" "Language-Team: Portuguese \n" "Language-Team: Russian admin могут изменять приложения или системные " "настройки." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Пользователи и группы" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Доступ ко всем сервисам и настройкам системы" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Проверьте запись LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Проверьте конфигурацию nslcd \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Проверьте конфигурацию nsswitch \"{database}\"" @@ -10528,10 +10528,10 @@ msgid "" "the logs to the bug report." msgstr "" "Это внутренняя ошибка, а не то, что вы вызвали или можете исправить. " -"Пожалуйста, сообщите об ошибке на трекере " -"ошибок чтобы мы могли это исправить. Кроме того, пожалуйста, приложите " -"файлы журналов к сообщению об ошибке." +"Пожалуйста, сообщите об ошибке на трекере ошибок чтобы мы могли это " +"исправить. Кроме того, пожалуйста, приложите файлы " +"журналов к сообщению об ошибке." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -10545,9 +10545,9 @@ msgid "" "the bug report." msgstr "" "Это последние строки журналов для служб, задействованных в этом приложении. " -"Если вы хотите сообщить об ошибке, пожалуйста, воспользуйтесь отслеживаним " -"ошибок и приложите этот журнал к отчету об ошибке." +"Если вы хотите сообщить об ошибке, пожалуйста, воспользуйтесь отслеживаним ошибок и приложите этот журнал к отчету об ошибке." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/si/LC_MESSAGES/django.po b/plinth/locale/si/LC_MESSAGES/django.po index bdc6deaef..5ad95f989 100644 --- a/plinth/locale/si/LC_MESSAGES/django.po +++ b/plinth/locale/si/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2021-04-27 13:32+0000\n" "Last-Translator: HelaBasa \n" "Language-Team: Sinhala \n" "Language-Team: Slovenian \n" "Language-Team: Albanian ddns.freedombox.org. Me këtë shërbim, përfitoni nënpërkatësi " -"të pakufizuara (me mundësinë e shenjës së gjithëpushtetshme të aktivizuar te " -"rregullimet e llogarisë). Që të përdorni një nënpërkatësi, shtojeni si një " -"përkatësi statike te aplikacioni Emra." +"shërbim GnuDIP të lirë, te ddns.freedombox.org. Me këtë shërbim, përfitoni " +"nënpërkatësi të pakufizuara (me mundësinë e shenjës së gjithëpushtetshme të " +"aktivizuar te rregullimet e llogarisë). Që të përdorni një nënpërkatësi, " +"shtojeni si një përkatësi statike te aplikacioni Emra." #: plinth/modules/dynamicdns/__init__.py:47 msgid "" @@ -1910,8 +1910,8 @@ msgid "" "href='http://freedns.afraid.org/' target='_blank'>freedns.afraid.org." msgstr "" "Ndryshe, mund të gjeni një shërbim falas të bazuar në përditësim URL-sh, te " -"freedns.afraid.org." +"freedns.afraid.org." #: plinth/modules/dynamicdns/__init__.py:50 msgid "" @@ -9144,7 +9144,7 @@ msgstr "" msgid "Distribution Update" msgstr "Përditësim Shpërndarjeje" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Kontrolloni për mbajtje paketash" @@ -9575,7 +9575,7 @@ msgstr "Nisja e përmirësimi dështoi." msgid "Frequent feature updates activated." msgstr "Përditësime të shpeshta veçorish të aktivizuara." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9586,7 +9586,7 @@ msgstr "" "aplikacione kërkojnë doemos një llogari përdoruesi, për të qenë pjesë e një " "grupi që autorizon përdoruesin të përdorë aplikacionin." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9598,25 +9598,25 @@ msgstr "" "vetëm përdoruesit e grupit përgjegjës mund të ndryshojnë " "aplikacionet, apo rregullimet e sistemit." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Përdorues dhe Grupe" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Hyrje te krejt shërbimet dhe rregullime të sistemit" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Kontrolloni zërin LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Kontrolloni “{key} {value}” formësimi nslcd-je" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Kontrolloni “{database}” formësimi nsswitch" diff --git a/plinth/locale/sr/LC_MESSAGES/django.po b/plinth/locale/sr/LC_MESSAGES/django.po index 2e71ad406..f53580344 100644 --- a/plinth/locale/sr/LC_MESSAGES/django.po +++ b/plinth/locale/sr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2022-09-14 17:20+0000\n" "Last-Translator: ikmaak \n" "Language-Team: Serbian \n" "Language-Team: Swedish admin kan dock ändra appar eller Systeminställningar." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Användare och grupper" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tillgång till alla tjänster och systeminställningar" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Kontrollera LDAP-posten \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Kontrollera nslcd-konfigurationen \"{key}{value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Kontrollera nsswitch-konfigurationen \"{database}\"" diff --git a/plinth/locale/ta/LC_MESSAGES/django.po b/plinth/locale/ta/LC_MESSAGES/django.po index 28942989c..6a135084f 100644 --- a/plinth/locale/ta/LC_MESSAGES/django.po +++ b/plinth/locale/ta/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-08-17 08:02+0000\n" "Last-Translator: தமிழ்நேரம் \n" "Language-Team: Tamil நிர்வாகம் குழுவின் பயனர்கள் " "மட்டுமே பயன்பாடுகள் அல்லது கணினி அமைப்புகளை மாற்றலாம்." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "பயனர்கள் மற்றும் குழுக்கள்" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "அனைத்து சேவைகள் மற்றும் கணினி அமைப்புகளுக்கான அணுகல்" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP உள்ளீட்டை சரிபார்க்கவும் \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "NSLCD கட்டமைப்பைச் சரிபார்க்கவும் \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "NSSWITCH கட்டமைப்பு \"{database}\"" diff --git a/plinth/locale/te/LC_MESSAGES/django.po b/plinth/locale/te/LC_MESSAGES/django.po index e5e0fcf9f..ea7ded507 100644 --- a/plinth/locale/te/LC_MESSAGES/django.po +++ b/plinth/locale/te/LC_MESSAGES/django.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: FreedomBox UI\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-05-14 17:03+0000\n" "Last-Translator: Sripath Roy Koganti \n" "Language-Team: Telugu అడ్మిన్ సమూహం యొక్క వినియోగదారులు మాత్రమే యాప్‌లు లేదా సిస్టమ్ " "సెట్టింగ్‌లను మార్చవచ్చు." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "వినియోగదారులు మరియు సమూహాలు" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "అన్ని సేవలకు మరియు సిస్టమ్ అమరికలకు ప్రాప్యత" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP నమోదు \"{search_item}\" తనిఖీ" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "nslcd config \"{key} {value}\"ని తనిఖీ చేయండి" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "nsswitch config \"{database}\"ని తనిఖీ చేయండి" diff --git a/plinth/locale/tr/LC_MESSAGES/django.po b/plinth/locale/tr/LC_MESSAGES/django.po index 014df9156..2d2f636e2 100644 --- a/plinth/locale/tr/LC_MESSAGES/django.po +++ b/plinth/locale/tr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-09-24 03:01+0000\n" "Last-Translator: Burak Yavuz \n" "Language-Team: Turkish logs for {box_name} " "services." msgstr "" -"Bu uygulama ayrıca {box_name} hizmetleri için günlükleri gösterir." +"Bu uygulama ayrıca {box_name} hizmetleri için günlükleri gösterir." #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "Tanılama" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "atlandı" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "geçti" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "başarısız" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "hata" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "uyarı" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "" "Bellek kullanımını azaltmak için bazı uygulamaları etkisizleştirmelisiniz." -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "Bu sisteme herhangi bir yeni uygulama yüklememelisiniz." -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1701,24 +1701,24 @@ msgstr "" "Sistem belleği düşük: %{percent_used} kullanılıyor, {memory_available}." "{memory_available_unit} boş. {advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "Düşük Bellek" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "Tanılama çalıştırılıyor" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "Sıradan denemeler sırasında {issue_count} sorun bulundu." -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "Tanı sonuçları" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "Tanı sonuçlarına git" @@ -9114,7 +9114,7 @@ msgstr "" msgid "Distribution Update" msgstr "Dağıtım Güncellemesi" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "Paket bekletmelerini denetle" @@ -9529,7 +9529,7 @@ msgstr "Yükseltmeyi başlatma başarısız oldu." msgid "Frequent feature updates activated." msgstr "Sık yapılan özellik güncellemeleri etkinleştirildi." -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " @@ -9540,7 +9540,7 @@ msgstr "" "kullanıcıya, uygulamaya erişme yetkisi vermek için bir kullanıcı hesabının " "bir grubun parçası olmasını gerektirir." -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -9552,25 +9552,25 @@ msgstr "" "sadece admin grubunun kullanıcıları uygulamaları veya sistem " "ayarlarını değiştirebilir." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Kullanıcılar ve Gruplar" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Tüm hizmetlere ve sistem ayarlarına erişim" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "LDAP \"{search_item}\" girişini denetleme" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "\"{key} {value}\" nslcd yapılandırmasını denetleme" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "\"{database}\" nsswitch yapılandırmasını denetleme" @@ -10502,10 +10502,9 @@ msgid "" "the logs to the bug report." msgstr "" "Bu bir iç hatadır ve sizin neden olduğunuz veya düzeltebileceğiniz bir şey " -"değildir. Lütfen düzeltebilmemiz için hata " -"izleyicide hatayı bildirin. Ayrıca, lütfen hata raporuna günlükleri ekleyin." +"değildir. Lütfen düzeltebilmemiz için hata izleyicide hatayı bildirin. " +"Ayrıca, lütfen hata raporuna günlükleri ekleyin." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -10519,9 +10518,9 @@ msgid "" "the bug report." msgstr "" "Bunlar, bu uygulamada yer alan hizmetler için günlüklerin son satırlarıdır. " -"Eğer bir hata bildirmek istiyorsanız, lütfen hata " -"izleyiciyi kullanın ve bu günlüğü hata raporuna ekleyin." +"Eğer bir hata bildirmek istiyorsanız, lütfen hata izleyiciyi " +"kullanın ve bu günlüğü hata raporuna ekleyin." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/uk/LC_MESSAGES/django.po b/plinth/locale/uk/LC_MESSAGES/django.po index f757cc802..5bd154c7f 100644 --- a/plinth/locale/uk/LC_MESSAGES/django.po +++ b/plinth/locale/uk/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-09-24 03:02+0000\n" "Last-Translator: Максим Горпиніч \n" "Language-Team: Ukrainian admin можуть змінювати застосунки або системні " "налаштування." -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "Користувачі і групи" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "Доступ до всіх сервісів і налаштувань системи" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "Перевірка запису LDAP \"{search_item}\"" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "Перевірте nslcd конфігурацію \"{key} {value}\"" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "Перевірте nsswitch конфігурацію \"{database}\"" @@ -10493,10 +10493,10 @@ msgid "" "the logs to the bug report." msgstr "" "Це внутрішня помилка, яку ви не спричинили і не можете виправити. Будь " -"ласка, повідомте про помилку на bug tracker, щоб ми могли її виправити. Також, будь ласка, додайте журнали до звіту про помилку." +"ласка, повідомте про помилку на bug tracker, щоб ми могли її " +"виправити. Також, будь ласка, додайте журнали " +"до звіту про помилку." #: plinth/templates/app-header.html:26 msgid "Installation" @@ -10510,9 +10510,9 @@ msgid "" "the bug report." msgstr "" "Це останні рядки журналів для служб, пов'язаних із цією програмою. Якщо ви " -"хочете повідомити про помилку, скористайтеся системою " -"відстеження помилок і додайте цей журнал до звіту про помилку." +"хочете повідомити про помилку, скористайтеся системою відстеження " +"помилок і додайте цей журнал до звіту про помилку." #: plinth/templates/app-logs.html:26 msgid "" diff --git a/plinth/locale/vi/LC_MESSAGES/django.po b/plinth/locale/vi/LC_MESSAGES/django.po index 77014a3af..d26da84c7 100644 --- a/plinth/locale/vi/LC_MESSAGES/django.po +++ b/plinth/locale/vi/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2021-07-28 08:34+0000\n" "Last-Translator: bruh \n" "Language-Team: Vietnamese \n" @@ -34,27 +34,27 @@ msgstr "{container_name} 容器正在运行" msgid "FreedomBox" msgstr "FreedomBox" -#: plinth/daemon.py:124 +#: plinth/daemon.py:131 #, python-brace-format msgid "Service {service_name} is running" msgstr "服务{service_name}正在运行" -#: plinth/daemon.py:221 +#: plinth/daemon.py:228 #, python-brace-format msgid "Listening on {kind} port {listen_address}:{port}" msgstr "正在侦听 {kind} 端口 {listen_address}:{port}" -#: plinth/daemon.py:224 +#: plinth/daemon.py:231 #, python-brace-format msgid "Listening on {kind} port {port}" msgstr "正在侦听 {kind} 端口 {port}" -#: plinth/daemon.py:295 +#: plinth/daemon.py:302 #, python-brace-format msgid "Connect to {host}:{port}" msgstr "连接到主机 {host}:{port}" -#: plinth/daemon.py:303 +#: plinth/daemon.py:310 #, python-brace-format msgid "Cannot connect to {host}:{port}" msgstr "不能连接到主机 {host}:{port}" @@ -213,30 +213,30 @@ msgstr "" msgid "Backups allows creating and managing backup archives." msgstr "备份允许创建和管理备份存档。" -#: plinth/modules/backups/__init__.py:44 plinth/modules/backups/__init__.py:174 -#: plinth/modules/backups/__init__.py:219 +#: plinth/modules/backups/__init__.py:46 plinth/modules/backups/__init__.py:176 +#: plinth/modules/backups/__init__.py:221 msgid "Backups" msgstr "备份" -#: plinth/modules/backups/__init__.py:171 +#: plinth/modules/backups/__init__.py:173 msgid "" "Enable an automatic backup schedule for data safety. Prefer an encrypted " "remote backup location or an extra attached disk." msgstr "为数据安全启用自动备份计划,最好使用加密的远程备份位置或附加的磁盘。" -#: plinth/modules/backups/__init__.py:177 +#: plinth/modules/backups/__init__.py:179 msgid "Enable a Backup Schedule" msgstr "启用备份计划" -#: plinth/modules/backups/__init__.py:181 -#: plinth/modules/backups/__init__.py:228 plinth/modules/privacy/__init__.py:84 +#: plinth/modules/backups/__init__.py:183 +#: plinth/modules/backups/__init__.py:230 plinth/modules/privacy/__init__.py:84 #: plinth/modules/storage/__init__.py:326 #: plinth/modules/upgrades/__init__.py:152 #, python-brace-format msgid "Go to {app_name}" msgstr "转到 {app_name}" -#: plinth/modules/backups/__init__.py:216 +#: plinth/modules/backups/__init__.py:218 #, python-brace-format msgid "" "A scheduled backup failed. Past {error_count} attempts for backup did not " @@ -244,7 +244,7 @@ msgid "" msgstr "" "定时备份失败,尝试{error_count}次备份未成功。最后的错误是:{error_message}" -#: plinth/modules/backups/__init__.py:224 +#: plinth/modules/backups/__init__.py:226 msgid "Error During Backup" msgstr "备份时出错" @@ -1579,52 +1579,52 @@ msgid "" msgstr "" #: plinth/modules/diagnostics/__init__.py:60 -#: plinth/modules/diagnostics/__init__.py:254 +#: plinth/modules/diagnostics/__init__.py:255 msgid "Diagnostics" msgstr "诊断程序" -#: plinth/modules/diagnostics/__init__.py:114 +#: plinth/modules/diagnostics/__init__.py:115 msgid "skipped" msgstr "" -#: plinth/modules/diagnostics/__init__.py:115 +#: plinth/modules/diagnostics/__init__.py:116 msgid "passed" msgstr "通过了" -#: plinth/modules/diagnostics/__init__.py:116 +#: plinth/modules/diagnostics/__init__.py:117 #: plinth/modules/networks/views.py:51 msgid "failed" msgstr "失败" -#: plinth/modules/diagnostics/__init__.py:117 +#: plinth/modules/diagnostics/__init__.py:118 msgid "error" msgstr "错误" -#: plinth/modules/diagnostics/__init__.py:118 +#: plinth/modules/diagnostics/__init__.py:119 msgid "warning" msgstr "警告" #. Translators: This is the unit of computer storage Mebibyte similar to #. Megabyte. -#: plinth/modules/diagnostics/__init__.py:220 +#: plinth/modules/diagnostics/__init__.py:221 msgid "MiB" msgstr "MiB" #. Translators: This is the unit of computer storage Gibibyte similar to #. Gigabyte. -#: plinth/modules/diagnostics/__init__.py:225 +#: plinth/modules/diagnostics/__init__.py:226 msgid "GiB" msgstr "GiB" -#: plinth/modules/diagnostics/__init__.py:232 +#: plinth/modules/diagnostics/__init__.py:233 msgid "You should disable some apps to reduce memory usage." msgstr "你应该禁用一些应用程序以减少内存的使用。" -#: plinth/modules/diagnostics/__init__.py:237 +#: plinth/modules/diagnostics/__init__.py:238 msgid "You should not install any new apps on this system." msgstr "你不应该在这个系统上安装任何新的应用程序。" -#: plinth/modules/diagnostics/__init__.py:249 +#: plinth/modules/diagnostics/__init__.py:250 #, no-python-format, python-brace-format msgid "" "System is low on memory: {percent_used}% used, {memory_available} " @@ -1633,24 +1633,24 @@ msgstr "" "系统内存不足:已使用 {percent_used},{memory_available} " "{memory_available_unit}可用。{advice_message}" -#: plinth/modules/diagnostics/__init__.py:251 +#: plinth/modules/diagnostics/__init__.py:252 msgid "Low Memory" msgstr "低内存" -#: plinth/modules/diagnostics/__init__.py:282 +#: plinth/modules/diagnostics/__init__.py:283 msgid "Running diagnostics" msgstr "运行诊断程序" -#: plinth/modules/diagnostics/__init__.py:328 +#: plinth/modules/diagnostics/__init__.py:329 #, no-python-format, python-brace-format msgid "Found {issue_count} issues during routine tests." msgstr "" -#: plinth/modules/diagnostics/__init__.py:329 +#: plinth/modules/diagnostics/__init__.py:330 msgid "Diagnostics results" msgstr "诊断结果" -#: plinth/modules/diagnostics/__init__.py:334 +#: plinth/modules/diagnostics/__init__.py:335 msgid "Go to diagnostics results" msgstr "转到诊断结果" @@ -8232,7 +8232,7 @@ msgstr "" msgid "Distribution Update" msgstr "更新分发" -#: plinth/modules/upgrades/__init__.py:385 +#: plinth/modules/upgrades/__init__.py:394 msgid "Check for package holds" msgstr "" @@ -8577,14 +8577,14 @@ msgstr "开始升级失败。" msgid "Frequent feature updates activated." msgstr "" -#: plinth/modules/users/__init__.py:33 +#: plinth/modules/users/__init__.py:34 msgid "" "Create and manage user accounts. These accounts serve as centralized " "authentication mechanism for most apps. Some apps further require a user " "account to be part of a group to authorize the user to access the app." msgstr "" -#: plinth/modules/users/__init__.py:38 +#: plinth/modules/users/__init__.py:39 #, python-brace-format msgid "" "Any user may login to {box_name} web interface to see a list of apps " @@ -8592,25 +8592,25 @@ msgid "" "group may alter apps or system settings." msgstr "" -#: plinth/modules/users/__init__.py:59 +#: plinth/modules/users/__init__.py:60 msgid "Users and Groups" msgstr "用户和组" -#: plinth/modules/users/__init__.py:85 +#: plinth/modules/users/__init__.py:86 msgid "Access to all services and system settings" msgstr "" -#: plinth/modules/users/__init__.py:137 +#: plinth/modules/users/__init__.py:139 #, python-brace-format msgid "Check LDAP entry \"{search_item}\"" msgstr "请检查 LDAP 条目“{search_item}”" -#: plinth/modules/users/__init__.py:152 +#: plinth/modules/users/__init__.py:154 #, python-brace-format msgid "Check nslcd config \"{key} {value}\"" msgstr "" -#: plinth/modules/users/__init__.py:182 +#: plinth/modules/users/__init__.py:184 #, python-brace-format msgid "Check nsswitch config \"{database}\"" msgstr "" @@ -9454,10 +9454,9 @@ msgid "" "freedombox/issues\">bug tracker so we can fix it. Also, please attach " "the logs to the bug report." msgstr "" -"这是一个内部错误,不是你造成的或可以修复的。 请报告到 bug 追踪器 上,让我们可以修复该错误。同时请附加 日志到 " -"Bug 报告里。" +"这是一个内部错误,不是你造成的或可以修复的。 请报告到 bug 追踪器 上,让我" +"们可以修复该错误。同时请附加 日志到 Bug 报告里。" #: plinth/templates/app-header.html:26 msgid "Installation" @@ -9470,8 +9469,8 @@ msgid "" "freedombox-team/freedombox/issues\">bug tracker and attach this log to " "the bug report." msgstr "" -"这些是本应用涉及到的服务的日志的最后几行。如果想报 Bug,请通过 bug 追踪器 " +"这些是本应用涉及到的服务的日志的最后几行。如果想报 Bug,请通过 bug 追踪器 " "并附上此日志。" #: plinth/templates/app-logs.html:26 diff --git a/plinth/locale/zh_Hant/LC_MESSAGES/django.po b/plinth/locale/zh_Hant/LC_MESSAGES/django.po index c962ac322..0f7bfd144 100644 --- a/plinth/locale/zh_Hant/LC_MESSAGES/django.po +++ b/plinth/locale/zh_Hant/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-23 00:05+0000\n" +"POT-Creation-Date: 2025-10-07 00:04+0000\n" "PO-Revision-Date: 2025-02-07 12:01+0000\n" "Last-Translator: pesder \n" "Language-Team: Chinese (Traditional Han script) Date: Mon, 6 Oct 2025 20:30:03 -0400 Subject: [PATCH 43/44] doc: Fetch latest manual --- doc/manual/en/ReleaseNotes.raw.wiki | 41 +++++++++++++++++++++++++++++ doc/manual/es/ReleaseNotes.raw.wiki | 41 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/doc/manual/en/ReleaseNotes.raw.wiki b/doc/manual/en/ReleaseNotes.raw.wiki index 6a75d982b..ebacfd9a8 100644 --- a/doc/manual/en/ReleaseNotes.raw.wiki +++ b/doc/manual/en/ReleaseNotes.raw.wiki @@ -8,6 +8,47 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f The following are the release notes for each !FreedomBox version. +== FreedomBox 25.13 (2025-10-06) == + +=== Highlights === + + * backups: Fix robust handling of known errors + * debian: Stop privileged service during upgrade or removal + * miniflux: Fix DB connection issues during install/uninstall + +=== Other Changes === + + * *: Collect output for all privileged sub-processes + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.check_output + * *: Use action_utils.run instead of subprocess.run + * action_utils: Handle capture_output argument in run wrapper + * actions: Fix lifetime of thread local storage + * actions: Log full exception from privileged daemon on error + * actions: Log method arguments in privileged daemon + * actions: Raise an exception if privileged server response is empty + * actions: Work with older privileged daemon + * actions_utils: Fix issue with collecting stdout/stderr + * backups: Don't show enable/disable button as app can't be disabled + * backups: Ignore a typing error with mypy + * ci: Switch backports test to trixie-backports + * config: Set home page to !FreedomBox for invalid values + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * daemon: When ensuring running state handle not-installed state + * diagnostics: In development mode, run diagnostics more rarely + * doc: Add manual page for freedombox-cmd + * gitweb: Use Git credential helper when cloning URLs with credentials + * glib: Add schedule parameter for setting interval in develop mode + * locale: Fix a string formatting issue in Italian translation + * locale: Update translations for Albanian, Bulgarian, Chinese (Simplified Han script), Czech, German, Italian, Russian, Turkish, Ukrainian + * Makefile: Move privileged daemon to /usr/lib/freedombox + * privileged_daemon: Fix showing errors for freedombox-cmd command + * privileged_daemon: Implement handling termination signal + * setup: Log full exception traceback when setup fails + * storage: Fix disk usage checking with disconnected SSH mounts + * zoph: Additional dbconfig configuration keys + == FreedomBox 25.12 (2025-09-22) == === Highlights === diff --git a/doc/manual/es/ReleaseNotes.raw.wiki b/doc/manual/es/ReleaseNotes.raw.wiki index 6a75d982b..ebacfd9a8 100644 --- a/doc/manual/es/ReleaseNotes.raw.wiki +++ b/doc/manual/es/ReleaseNotes.raw.wiki @@ -8,6 +8,47 @@ For more technical details, see the [[https://salsa.debian.org/freedombox-team/f The following are the release notes for each !FreedomBox version. +== FreedomBox 25.13 (2025-10-06) == + +=== Highlights === + + * backups: Fix robust handling of known errors + * debian: Stop privileged service during upgrade or removal + * miniflux: Fix DB connection issues during install/uninstall + +=== Other Changes === + + * *: Collect output for all privileged sub-processes + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.check_output + * *: Use action_utils.run instead of subprocess.run + * action_utils: Handle capture_output argument in run wrapper + * actions: Fix lifetime of thread local storage + * actions: Log full exception from privileged daemon on error + * actions: Log method arguments in privileged daemon + * actions: Raise an exception if privileged server response is empty + * actions: Work with older privileged daemon + * actions_utils: Fix issue with collecting stdout/stderr + * backups: Don't show enable/disable button as app can't be disabled + * backups: Ignore a typing error with mypy + * ci: Switch backports test to trixie-backports + * config: Set home page to !FreedomBox for invalid values + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * daemon: When ensuring running state handle not-installed state + * diagnostics: In development mode, run diagnostics more rarely + * doc: Add manual page for freedombox-cmd + * gitweb: Use Git credential helper when cloning URLs with credentials + * glib: Add schedule parameter for setting interval in develop mode + * locale: Fix a string formatting issue in Italian translation + * locale: Update translations for Albanian, Bulgarian, Chinese (Simplified Han script), Czech, German, Italian, Russian, Turkish, Ukrainian + * Makefile: Move privileged daemon to /usr/lib/freedombox + * privileged_daemon: Fix showing errors for freedombox-cmd command + * privileged_daemon: Implement handling termination signal + * setup: Log full exception traceback when setup fails + * storage: Fix disk usage checking with disconnected SSH mounts + * zoph: Additional dbconfig configuration keys + == FreedomBox 25.12 (2025-09-22) == === Highlights === From d1be37d1df77c3438f6e3143cec12870faae40a3 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Mon, 6 Oct 2025 20:30:32 -0400 Subject: [PATCH 44/44] Release v25.13 to unstable --- debian/changelog | 69 ++++++++++++++++++++++++++++++++++++++++++++++ plinth/__init__.py | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index 96d7221fa..7cfaf5220 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,72 @@ +freedombox (25.13) unstable; urgency=medium + + [ Burak Yavuz ] + * Translated using Weblate (Turkish) + + [ 大王叫我来巡山 ] + * Translated using Weblate (Chinese (Simplified Han script)) + + [ Максим Горпиніч ] + * Translated using Weblate (Ukrainian) + + [ 109247019824 ] + * Translated using Weblate (Bulgarian) + + [ Jiří Podhorecký ] + * Translated using Weblate (Czech) + + [ Sunil Mohan Adapa ] + * glib: Add schedule parameter for setting interval in develop mode + * diagnostics: In development mode, run diagnostics more rarely + * backups: Ignore a typing error with mypy + * Makefile: Move privileged daemon to /usr/lib/freedombox + * action_utils: Handle capture_output argument in run wrapper + * privileged_daemon: Fix showing errors for freedombox-cmd command + * doc: Add manual page for freedombox-cmd + * storage: Fix disk usage checking with disconnected SSH mounts + * actions: Work with older privileged daemon + * privileged_daemon: Implement handling termination signal + * d/rules: Drop a workaround for dh_installsytemd needed for /usr/lib + * actions: Log method arguments in privileged daemon + * backups: Fix robust handling of known errors + * config: Set home page to FreedomBox for invalid values + * actions: Log full exception from privileged daemon on error + * setup: Log full exception traceback when setup fails + * actions: Fix lifetime of thread local storage + * actions_utils: Fix issue with collecting stdout/stderr + * *: Use action_utils.run instead of subprocess.run + * *: Use action_utils.run instead of subprocess.check_call + * *: Use action_utils.run instead of subprocess.call + * *: Use action_utils.run instead of subprocess.check_output + * *: Collect output for all privileged sub-processes + * ci: Switch backports test to trixie-backports + * actions: Raise an exception if privileged server response is empty + * debian: Stop privileged service during upgrade or removal + * backups: Don't show enable/disable button as app can't be disabled + * locale: Fix a string formatting issue in Italian translation + * daemon: When ensuring running state handle not-installed state + * miniflux: Fix DB connection issues during install/uninstall + * zoph: Additional dbconfig configuration keys + + [ Dietmar ] + * Translated using Weblate (German) + * Translated using Weblate (Italian) + + [ Roman Akimov ] + * Translated using Weblate (Russian) + + [ Veiko Aasa ] + * gitweb: Use Git credential helper when cloning URLs with credentials + + [ Besnik Bleta ] + * Translated using Weblate (Albanian) + + [ James Valleroy ] + * locale: Update translation strings + * doc: Fetch latest manual + + -- James Valleroy Mon, 06 Oct 2025 20:30:14 -0400 + freedombox (25.12) unstable; urgency=medium [ Burak Yavuz ] diff --git a/plinth/__init__.py b/plinth/__init__.py index 665ddf78b..00a1c2fc9 100644 --- a/plinth/__init__.py +++ b/plinth/__init__.py @@ -3,4 +3,4 @@ Package init file. """ -__version__ = '25.12' +__version__ = '25.13'