From 6ef8bb8b62f2a123ab9d5e5ecf89c33644fb840e Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Thu, 17 Oct 2024 06:12:01 +0000 Subject: [PATCH 1/7] Revert "Merge pull request #169 from knuton/revert-qt6" This reverts commit a524d7a768b30b974e7d08fe3b6dd4ee3e5119ec, reversing changes made to 762787df703d8c86445e41011b2d8ec436e6371d. --- LICENSE | 8 +- Readme.md | 60 ++------ application.nix | 2 +- controller/Changelog.md | 2 + controller/bindings/util/dune | 2 +- controller/bindings/util/util.ml | 7 + controller/dune | 4 + controller/gui/style.css | 10 ++ controller/licenses/FEATHER | 21 +++ controller/licenses/NIXPKGS | 20 +++ controller/licenses/PLAYOS | 19 +++ controller/licenses/QT6 | 167 ++++++++++++++++++++++ controller/server/dune | 2 +- controller/server/gui.ml | 29 ++-- controller/server/view/common/icon.ml | 73 ++++++---- controller/server/view/common/icon.mli | 1 + controller/server/view/common/page.ml | 14 +- controller/server/view/common/page.mli | 1 + controller/server/view/licensing_page.ml | 58 ++++++++ controller/server/view/licensing_page.mli | 2 + docs/arch/Readme.org | 4 +- kiosk/Readme.md | 4 +- kiosk/bin/kiosk-browser | 2 +- kiosk/default.nix | 27 ++-- kiosk/kiosk_browser/__init__.py | 10 +- kiosk/kiosk_browser/browser_widget.py | 25 ++-- kiosk/kiosk_browser/captive_portal.py | 2 +- kiosk/kiosk_browser/dialogable_widget.py | 10 +- kiosk/kiosk_browser/main_widget.py | 6 +- kiosk/kiosk_browser/proxy.py | 8 +- kiosk/mypy.ini | 10 +- kiosk/shell.nix | 3 +- testing/integration/kiosk-proxy.nix | 72 ++++++++++ 33 files changed, 523 insertions(+), 162 deletions(-) mode change 100644 => 120000 LICENSE create mode 100644 controller/licenses/FEATHER create mode 100644 controller/licenses/NIXPKGS create mode 100644 controller/licenses/PLAYOS create mode 100644 controller/licenses/QT6 create mode 100644 controller/server/view/licensing_page.ml create mode 100644 controller/server/view/licensing_page.mli create mode 100644 testing/integration/kiosk-proxy.nix diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 514e68a3..00000000 --- a/LICENSE +++ /dev/null @@ -1,7 +0,0 @@ -Copyright 2019 Dividat AG - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 120000 index 00000000..e7aa3976 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +controller/licenses/PLAYOS \ No newline at end of file diff --git a/Readme.md b/Readme.md index 845c14f3..17241d8d 100644 --- a/Readme.md +++ b/Readme.md @@ -197,57 +197,13 @@ The following [dev tools](dev-tools/Readme.md) are available: ## Attribution and Licensing -Most code in this repository is authored by the Dividat AG and the project contributors. This code is licensed under an MIT license. +Most code in this repository is authored by the Dividat AG and the project +contributors. This code is licensed under [the MIT license](./LICENSE). -Some source files in this project are portions of other open-source project and may be released under different licenses. The applicable licenses are stated here as well as in the relevant subdirectories. +Some source files in this project are portions of other open-source project and +may be released under different licenses. The applicable licenses are stated +here as well as in the relevant subdirectories. -### [nixpkgs](https://github.com/NixOS/nixpkgs) - -``` -Copyright (c) 2003-2018 Eelco Dolstra and the Nixpkgs/NixOS contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` - -### [Feather](https://feathericons.com/) - -``` -The MIT License (MIT) - -Copyright (c) 2013-2017 Cole Bemis - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` +- [nixpkgs](https://github.com/NixOS/nixpkgs) ([MIT License](./controller/licenses/NIXPKGS)) +- [Feather](https://feathericons.com/) ([MIT License](./controller/licenses/FEATHER)) – used in the [controller](./controller/Readme.md) +- [Qt6](https://www.qt.io/product/qt6) ([LGPLv3 License](./controller/licenses/QT6)) – used in the [kiosk](./kiosk/Readme.md) diff --git a/application.nix b/application.nix index b5e837c2..cfeb72e6 100644 --- a/application.nix +++ b/application.nix @@ -88,7 +88,7 @@ rec { ;; esac - # Enable Qt WebEngine Developer Tools (https://doc.qt.io/qt-5/qtwebengine-debugging.html) + # Enable Qt WebEngine Developer Tools (https://doc.qt.io/qt-6/qtwebengine-debugging.html) export QTWEBENGINE_REMOTE_DEBUGGING="127.0.0.1:3355" ${pkgs.playos-kiosk-browser}/bin/kiosk-browser \ diff --git a/controller/Changelog.md b/controller/Changelog.md index 566ed455..a2fe3083 100644 --- a/controller/Changelog.md +++ b/controller/Changelog.md @@ -23,6 +23,7 @@ - kiosk: Add a key combination to perform hard refresh (Ctrl-Shift-R) - os: Added localization options for Polish and Czech +- controller: Add licensing page in System Settings - status screen: Display MAC addresses in text and QR code ## Changed @@ -33,6 +34,7 @@ - os: Limit permitted SSH modes and forwarding options - os: Ignore suspend and hibernation key presses, but interpret as poweroff when long-pressed - driver: Upgrade to add support for Senso firmware updates via app +- kiosk: Migrate to Qt6 ## Removed diff --git a/controller/bindings/util/dune b/controller/bindings/util/dune index c56da1f2..48205593 100644 --- a/controller/bindings/util/dune +++ b/controller/bindings/util/dune @@ -1,6 +1,6 @@ (library (name util) (modules util) - (libraries logs logs.lwt cohttp-lwt-unix sexplib) + (libraries logs logs.lwt cohttp-lwt-unix sexplib fpath) (preprocess (pps lwt_ppx ppx_sexp_conv))) diff --git a/controller/bindings/util/util.ml b/controller/bindings/util/util.ml index 6db70a59..de70df0c 100644 --- a/controller/bindings/util/util.ml +++ b/controller/bindings/util/util.ml @@ -1,5 +1,12 @@ open Lwt +(* Require the resource directory to be at a directory fixed to the binary + * location. This is not optimal, but works for the moment. *) +let resource_path end_path = + let open Fpath in + (Sys.argv.(0) |> v |> parent) / ".." / "share" // end_path + |> to_string + let read_from_file log_src path = let%lwt exists = Lwt_unix.file_exists path in if exists then diff --git a/controller/dune b/controller/dune index 072c8266..d5b1e30d 100644 --- a/controller/dune +++ b/controller/dune @@ -2,6 +2,10 @@ (section share_root) (files (Changelog.html as Changelog.html) + (licenses/PLAYOS as licenses/PLAYOS) + (licenses/NIXPKGS as licenses/NIXPKGS) + (licenses/FEATHER as licenses/FEATHER) + (licenses/QT6 as licenses/QT6) (gui/reset.css as static/reset.css) (gui/style.css as static/style.css) (gui/vendor/focus-shift-1.0.0.js as static/vendor/focus-shift.js) diff --git a/controller/gui/style.css b/controller/gui/style.css index 6d959e9b..2c8fb277 100644 --- a/controller/gui/style.css +++ b/controller/gui/style.css @@ -318,6 +318,16 @@ html { margin-top: 2rem; } +/* Licensing */ + +.d-Licensing__Link { + text-decoration: underline; +} + +.d-Licensing__Details { + margin-bottom: var(--spacing-dog); +} + /* Form */ :root { diff --git a/controller/licenses/FEATHER b/controller/licenses/FEATHER new file mode 100644 index 00000000..1f4f4336 --- /dev/null +++ b/controller/licenses/FEATHER @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-2023 Cole Bemis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/controller/licenses/NIXPKGS b/controller/licenses/NIXPKGS new file mode 100644 index 00000000..40eae096 --- /dev/null +++ b/controller/licenses/NIXPKGS @@ -0,0 +1,20 @@ +Copyright (c) 2003-2024 Eelco Dolstra and the Nixpkgs/NixOS contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/controller/licenses/PLAYOS b/controller/licenses/PLAYOS new file mode 100644 index 00000000..d35cf97c --- /dev/null +++ b/controller/licenses/PLAYOS @@ -0,0 +1,19 @@ +Copyright © Dividat AG + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/controller/licenses/QT6 b/controller/licenses/QT6 new file mode 100644 index 00000000..22f0f6fc --- /dev/null +++ b/controller/licenses/QT6 @@ -0,0 +1,167 @@ +The Qt Toolkit is Copyright (C) 2018 The Qt Company Ltd. and other contributors. +Contact: https://www.qt.io/licensing/ + +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/controller/server/dune b/controller/server/dune index 3c5a757f..beef2d98 100644 --- a/controller/server/dune +++ b/controller/server/dune @@ -6,7 +6,7 @@ (modules server update info gui health info_page localization_page status_page error_page network_list_page network_details_page changelog_page - page definition icon) + licensing_page page definition icon) (libraries lwt logs logs.fmt logs.lwt fpath cohttp-lwt-unix logging opium tyxml rauc zerotier connman locale network timedate systemd semver2 fieldslib screen_settings) diff --git a/controller/server/gui.ml b/controller/server/gui.ml index d5ba55a4..f7a70b23 100644 --- a/controller/server/gui.ml +++ b/controller/server/gui.ml @@ -5,17 +5,9 @@ open Opium.App let log_src = Logs.Src.create "gui" -(* Require the resource directory to be at a directory fixed to the binary - * location. This is not optimal, but works for the moment. *) -let resource_path end_path = - let open Fpath in - (Sys.argv.(0) |> v |> parent) / ".." / "share" // end_path - |> to_string - - (* Middleware that makes static content available *) let static () = - let static_dir = resource_path (Fpath.v "static") in + let static_dir = Util.resource_path (Fpath.v "static") in Logs.debug (fun m -> m "static content dir: %s" static_dir); Opium.Middleware.static ~local_path:static_dir ~uri_prefix:"/static" () @@ -218,7 +210,7 @@ module NetworkGui = struct let pp_proxy p = let uri = p |> Service.Proxy.to_uri ~include_userinfo:false |> Uri.to_string in match p.credentials with - | Some({ user; password }) -> + | Some({ user; password }) -> let password_indication = if password = "" then "" else ", password: *****" in uri ^ " (user: " ^ user ^ password_indication ^ ")" | None -> uri @@ -233,7 +225,7 @@ module NetworkGui = struct })) (** Internet status **) - let internet_status ~connman _ = + let internet_status ~connman _ = let%lwt proxy = Manager.get_default_proxy connman in match%lwt Curl.request ?proxy:(Option.map (Service.Proxy.to_uri ~include_userinfo:true) proxy) (Uri.of_string "http://captive.dividat.com/") with | RequestSuccess (code, response) -> @@ -279,10 +271,10 @@ module NetworkGui = struct let password_input = form_data |> List.assoc "proxy_password" |> List.hd |> non_empty in - let keep_password = + let keep_password = form_data |> List.assoc_opt "keep_password" |> Option.is_some in - let password = + let password = match (keep_password, current_proxy_opt) with | (true, Some ({ host; port; credentials = Some { user; password } })) -> if host_input = Some host && port_input = Some port && user_input = Some user then @@ -432,10 +424,18 @@ module ChangelogGui = struct let build app = app |> get "/changelog" (fun _ -> - let%lwt changelog = Util.read_from_file log_src (resource_path (Fpath.v "Changelog.html")) in + let%lwt changelog = Util.read_from_file log_src (Util.resource_path (Fpath.v "Changelog.html")) in Lwt.return (page (Changelog_page.html changelog))) end +module LicensingGui = struct + let build app = + app + |> get "/licensing" (fun _ -> + let%lwt p = Licensing_page.html in + Lwt.return (page p)) +end + module RemoteMaintenanceGui = struct let rec wait_until_zerotier_is_on () = @@ -483,6 +483,7 @@ let routes ~systemd ~shutdown ~health_s ~update_s ~rauc ~connman app = |> LocalizationGui.build |> StatusGui.build ~health_s ~update_s ~rauc |> ChangelogGui.build + |> LicensingGui.build |> RemoteMaintenanceGui.build ~systemd (* NOTE: probably easier to create a record with all the inputs instead of passing in x arguments. *) diff --git a/controller/server/view/common/icon.ml b/controller/server/view/common/icon.ml index c231a75b..ac39d98b 100644 --- a/controller/server/view/common/icon.ml +++ b/controller/server/view/common/icon.ml @@ -1,10 +1,10 @@ open Tyxml.Svg -(* Helpers *) +(* Helpers *) let svg ?a ?stroke_width content = - Tyxml.Html.svg - ~a:([ a_viewBox (0., 0., 24., 24.) + Tyxml.Html.svg + ~a:([ a_viewBox (0., 0., 24., 24.) ; a_width (24., None) ; a_height (24., None) ; a_fill `None @@ -16,31 +16,31 @@ let svg ?a ?stroke_width content = content let line (x1, y1) (x2, y2) = - Tyxml.Svg.line - ~a:[ a_x1 (x1, None) - ; a_y1 (y1, None) - ; a_x2 (x2, None) - ; a_y2 (y2, None) - ] + Tyxml.Svg.line + ~a:[ a_x1 (x1, None) + ; a_y1 (y1, None) + ; a_x2 (x2, None) + ; a_y2 (y2, None) + ] [] let circle (x, y) r = - Tyxml.Svg.circle - ~a:[ a_cx (x, None) - ; a_cy (y, None) - ; a_r (r, None) + Tyxml.Svg.circle + ~a:[ a_cx (x, None) + ; a_cy (y, None) + ; a_r (r, None) ] [] let rect ?rx ?fill (x1, y1) (x2, y2) = - Tyxml.Svg.rect - ~a:[ a_x (x1, None) - ; a_y (y1, None) - ; a_width (x2 -. x1, None) - ; a_height (y2 -. y1, None) - ; a_rx (Option.value ~default:0. rx, None) + Tyxml.Svg.rect + ~a:[ a_x (x1, None) + ; a_y (y1, None) + ; a_width (x2 -. x1, None) + ; a_height (y2 -. y1, None) + ; a_rx (Option.value ~default:0. rx, None) ; a_fill (`Color (Option.value ~default:"transparent" fill, None)) - ] + ] [] (* Icons *) @@ -60,7 +60,7 @@ let wifi ?strength () = else if strength < 75 then "Medium" else "Strong" in - svg + svg ~a:[ a_class [ "d-WifiSignal--" ^ modifier ] ] [ path ~a:[ a_class [ "d-WifiSignal__Wave--Outer" ] ; a_d "M1.42 9a16 16 0 0 1 21.16 0" ] [] ; path ~a:[ a_class [ "d-WifiSignal__Wave--Middle" ] ; a_d "M5 12.55a11 11 0 0 1 14.08 0" ] [] @@ -68,7 +68,7 @@ let wifi ?strength () = ; line (12., 20.) (12., 20.) ] -let ethernet = +let ethernet = svg [ path ~a: [ a_d "M2 2 H22 V18 H18 V22 H6 V18 H2 Z" ] [] ; line (6., 6.) (6., 10.) @@ -90,14 +90,14 @@ let power = ; line (12., 2.) (12., 12.) ] -let screen = +let screen = svg [ rect ~rx:2. (2.5, 2.) (21.5, 16.) ; line (12., 16.) (12., 22.) ; line (8., 22.) (16., 22.) ] -let document = +let document = svg [ rect ~rx:1. (4., 2.) (20., 22.) ; line (8., 8.) (16., 8.) @@ -112,17 +112,30 @@ let arrow_left = ; line (2., 12.) (12., 2.) ] -let letter = +let letter = svg ~stroke_width:1. [ rect ~rx:1. ~fill:"black" (2., 2.) (22., 22.) - ; text - ~a:[ a_fill (`Color ("white", None)) - ; a_stroke (`Color ("white", None)) + ; text + ~a:[ a_fill (`Color ("white", None)) + ; a_stroke (`Color ("white", None)) ; a_font_size "16" ; Unsafe.string_attrib "x" "50%" ; Unsafe.string_attrib "y" "55%" ; a_dominant_baseline `Middle ; a_text_anchor `Middle - ] - [ txt "A" ] + ] + [ txt "A" ] + ] + +let copyright = + svg + [ circle (12., 12.) 10. + ; text + ~a:[ a_font_size "10" + ; Unsafe.string_attrib "x" "50%" + ; Unsafe.string_attrib "y" "55%" + ; a_dominant_baseline `Middle + ; a_text_anchor `Middle + ] + [ txt "C" ] ] diff --git a/controller/server/view/common/icon.mli b/controller/server/view/common/icon.mli index d039a04e..8099ff09 100644 --- a/controller/server/view/common/icon.mli +++ b/controller/server/view/common/icon.mli @@ -7,3 +7,4 @@ val screen : [> Html_types.svg ] Tyxml.Html.elt val document : [> Html_types.svg ] Tyxml.Html.elt val arrow_left : [> Html_types.svg ] Tyxml.Html.elt val letter : [> Html_types.svg ] Tyxml.Html.elt +val copyright : [> Html_types.svg ] Tyxml.Html.elt diff --git a/controller/server/view/common/page.ml b/controller/server/view/common/page.ml index 3c38108b..9eda7f7a 100644 --- a/controller/server/view/common/page.ml +++ b/controller/server/view/common/page.ml @@ -6,6 +6,7 @@ type page = | Localization | SystemStatus | Changelog + | Licensing | Shutdown let menu_link page = @@ -15,6 +16,7 @@ let menu_link page = | Localization -> "/localization" | SystemStatus -> "/status" | Changelog -> "/changelog" + | Licensing -> "/licensing" | Shutdown -> "/shutdown" let menu_icon page = @@ -24,6 +26,7 @@ let menu_icon page = | Localization -> Icon.letter | SystemStatus -> Icon.screen | Changelog -> Icon.document + | Licensing -> Icon.copyright | Shutdown -> Icon.power let menu_label page = @@ -33,6 +36,7 @@ let menu_label page = | Localization -> "Localization & Display" | SystemStatus -> "System Status" | Changelog -> "Changelog" + | Licensing -> "Licensing" | Shutdown -> "Shutdown" let menu_item current_page page = @@ -50,7 +54,7 @@ let menu_item current_page page = ] let html ?current_page ?header content = - let header = + let header = match header with | Some header -> [ Tyxml.Html.header ~a:[ a_class [ "d-Layout__Header" ] ] [ header ] ] | None -> [] @@ -71,13 +75,13 @@ let html ?current_page ?header content = ; a_user_data "focus-group" "active" ] [ nav - ([ Info; Network; Localization; SystemStatus; Changelog ] + ([ Info; Network; Localization; SystemStatus; Changelog; Licensing ] |> List.concat_map (fun page -> [ menu_item current_page page; txt " " ])) ; div ~a: [ a_class [ "d-Layout__Shutdown" ] ] [ menu_item current_page Shutdown ] - ]) - :: header + ]) + :: header @ [ main ~a:[ a_class [ "d-Layout__Main" ] ] [ content ] @@ -97,7 +101,7 @@ let header_title ?back_url ?icon ?right_action content = | Some icon -> [ span ~a: [ a_class [ "d-Header__Icon" ] ] [ icon ] ] | None -> [] in - let right_action = + let right_action = match right_action with | Some right_action -> [ right_action ] | None -> [] diff --git a/controller/server/view/common/page.mli b/controller/server/view/common/page.mli index d57fe75f..3403b8da 100644 --- a/controller/server/view/common/page.mli +++ b/controller/server/view/common/page.mli @@ -4,6 +4,7 @@ type page = | Localization | SystemStatus | Changelog + | Licensing | Shutdown val html : diff --git a/controller/server/view/licensing_page.ml b/controller/server/view/licensing_page.ml new file mode 100644 index 00000000..1b08a190 --- /dev/null +++ b/controller/server/view/licensing_page.ml @@ -0,0 +1,58 @@ +open Tyxml.Html +open Lwt + +let log_src = Logs.Src.create "licensing_page" + +let tool ~name ~license_name ~license_content content = + div + [ h2 ~a:[ a_class [ "d-Title" ] ] [ txt name ] + ; div content + ; details + ~a:[ a_class [ "d-Licensing__Details" ] ] + (summary [ txt license_name ]) + [ pre ~a: [ a_class [ "d-Preformatted" ] ] [ txt license_content ] ] + ] + +let read_license key = + Util.read_from_file log_src (Util.resource_path (Fpath.v ("licenses/" ^ key))) + +let html = + let%lwt playos_license = read_license "PLAYOS" in + let%lwt nixpkgs_license = read_license "NIXPKGS" in + let%lwt feather_license = read_license "FEATHER" in + let%lwt qt6_license = read_license "QT6" in + Lwt.return (Page.html + ~current_page:Page.Licensing + ~header:(Page.header_title + ~icon:Icon.copyright + [ txt "Licensing" ]) + (div + [ tool + ~name:"PlayOS" + ~license_name:"MIT License" + ~license_content:playos_license + [ p + ~a:[ a_class [ "d-Paragraph" ] ] + [ txt "Source code is available at " + ; span (* Using span as we don’t intend the user to leave the current page *) + ~a:[ a_class [ "d-Licensing__Link" ] ] + [ txt "https://github.com/dividat/playos" ] + ; txt ", with instructions to build and modify the software." + ] + ] + ; tool + ~name:"Nixpkgs" + ~license_name:"MIT License" + ~license_content:nixpkgs_license + [] + ; tool + ~name:"Feather" + ~license_name:"MIT License" + ~license_content:feather_license + [] + ; tool + ~name:"Qt6" + ~license_name:"GNU Lesser General Public License v3.0" + ~license_content:qt6_license + [] + ])) diff --git a/controller/server/view/licensing_page.mli b/controller/server/view/licensing_page.mli new file mode 100644 index 00000000..8044a962 --- /dev/null +++ b/controller/server/view/licensing_page.mli @@ -0,0 +1,2 @@ +val html : + [> Html_types.html ] Tyxml.Html.elt Lwt.t diff --git a/docs/arch/Readme.org b/docs/arch/Readme.org index a5bfb7f1..8b34117c 100644 --- a/docs/arch/Readme.org +++ b/docs/arch/Readme.org @@ -210,13 +210,13 @@ The [[https://github.com/dividat/driver][Dividat Driver]], which handles connect ** Kiosk -The system automatically logs in the user ~play~, starts an X session and launches a custom Kiosk Application based on [[http://doc.qt.io/qt-5/qtwebengine-index.html][QtWebEngine]]. The Kiosk Application loads Dividat Play in a restricted environment. +The system automatically logs in the user ~play~, starts an X session and launches a custom Kiosk Application based on [[https://doc.qt.io/qt-6/qtwebengine-index.html][QtWebEngine]]. The Kiosk Application loads Dividat Play in a restricted environment. The [[*User interface][user interface for system configuration]] can be accessed with the key-combination ~Ctrl-Shift-F12~. If a captive portal is detected, which requires user interaction before granting Internet access, a prompt appears to open it. -For debugging the [[https://doc.qt.io/qt-5/qtwebengine-debugging.html][Qt WebEngine Developer Tools]] are enabled and accessible at http://localhost:3355 and chrome://inspect/#devices. The Dev Tools can be used to inspect and interact with the running page (e.g. load a new page with ~location.replace("https://nixos.org")~). +For debugging the [[https://doc.qt.io/qt-6/qtwebengine-debugging.html][Qt WebEngine Developer Tools]] are enabled and accessible at http://localhost:3355 and chrome://inspect/#devices. The Dev Tools can be used to inspect and interact with the running page (e.g. load a new page with ~location.replace("https://nixos.org")~). ** Audio diff --git a/kiosk/Readme.md b/kiosk/Readme.md index f3de69af..b02467e4 100644 --- a/kiosk/Readme.md +++ b/kiosk/Readme.md @@ -1,6 +1,6 @@ # PlayOS Kiosk Browser -Cycle between two urls in a full screen, locked down browser based on [QtWebEngine](http://doc.qt.io/qt-5/qtwebengine-index.html). Allow login to captive portals. +Cycle between two urls in a full screen, locked down browser based on [QtWebEngine](https://doc.qt.io/qt-6/qtwebengine-index.html). Allow login to captive portals. ## Development @@ -27,4 +27,4 @@ QTWEBENGINE_REMOTE_DEBUGGING=3355 bin/kiosk-browser … Then, point a Chromium-based browser to `http://127.0.0.1:3355`. Additional documentation is available at: -https://doc.qt.io/qt-5/qtwebengine-debugging.html +https://doc.qt.io/qt-6/qtwebengine-debugging.html diff --git a/kiosk/bin/kiosk-browser b/kiosk/bin/kiosk-browser index fa3fdbc0..8cb828c9 100755 --- a/kiosk/bin/kiosk-browser +++ b/kiosk/bin/kiosk-browser @@ -10,7 +10,7 @@ default_toggle_settings_key = 'CTRL+SHIFT+F12' parser = argparse.ArgumentParser( prog='kiosk-browser', description='Cycle through two urls in kiosk mode. Allow login to captive portals.', - epilog='Additional browser debugging environment variables can be found under https://doc.qt.io/qt-5/qtwebengine-debugging.html' + epilog='Additional browser debugging environment variables can be found under https://doc.qt.io/qt-6/qtwebengine-debugging.html' ) parser.add_argument( diff --git a/kiosk/default.nix b/kiosk/default.nix index 837d0956..75026daf 100644 --- a/kiosk/default.nix +++ b/kiosk/default.nix @@ -1,4 +1,4 @@ -{ pkgs, system_name, system_version }: +{ pkgs, system_name, system_version, additional_inputs ? [] }: with pkgs; @@ -15,32 +15,37 @@ python3Packages.buildPythonApplication rec { --replace "@system_version@" "${system_version}" ''; - doCheck = false; + buildInputs = [ + bashInteractive + makeWrapper + ]; - nativeBuildInputs = [ qt5.wrapQtAppsHook mypy ]; + nativeBuildInputs = [ + mypy + qt6.wrapQtAppsHook + ]; propagatedBuildInputs = with python3Packages; [ dbus-python pygobject3 - pyqtwebengine + pyqt6-webengine pytest + qt6.qtbase requests types-requests - ]; + ] ++ additional_inputs; postInstall = '' cp -r images/ $out/images ''; - dontWrapQtApps = true; - makeWrapperArgs = [ "\${qtWrapperArgs[@]}" ]; - shellHook = '' # Give access to kiosk_browser module export PYTHONPATH=./:$PYTHONPATH - # Give access to Qt platform plugin "xcb" in nix-shell - export QT_QPA_PLATFORM_PLUGIN_PATH="${qt5.qtbase.bin}/lib/qt-${qt5.qtbase.version}/plugins"; + # Setup Qt environment + bashdir=$(mktemp -d) + makeWrapper "$(type -p bash)" "$bashdir/bash" "''${qtWrapperArgs[@]}" + exec "$bashdir/bash" ''; - } diff --git a/kiosk/kiosk_browser/__init__.py b/kiosk/kiosk_browser/__init__.py index 601b28a0..a8bb5f6f 100644 --- a/kiosk/kiosk_browser/__init__.py +++ b/kiosk/kiosk_browser/__init__.py @@ -1,9 +1,9 @@ import sys import logging import signal -from PyQt5.QtCore import Qt, QUrl, QSize -from PyQt5.QtGui import QKeySequence -from PyQt5.QtWidgets import QApplication +from PyQt6.QtCore import Qt, QUrl +from PyQt6.QtGui import QKeySequence +from PyQt6.QtWidgets import QApplication from kiosk_browser import main_widget @@ -19,7 +19,7 @@ def start(kiosk_url, settings_url, toggle_settings_key, fullscreen = True): toggle_settings_key = QKeySequence(toggle_settings_key) ) - mainWidget.setContextMenuPolicy(Qt.NoContextMenu) + mainWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) screen_size = app.primaryScreen().size() @@ -43,7 +43,7 @@ def quit_on_signal(signum, _frame): signal.signal(signal.SIGTERM, quit_on_signal) # Start application - app.exec_() + app.exec() def parseUrl(url): parsed_url = QUrl(url) diff --git a/kiosk/kiosk_browser/browser_widget.py b/kiosk/kiosk_browser/browser_widget.py index 22f13d39..bf2a3135 100644 --- a/kiosk/kiosk_browser/browser_widget.py +++ b/kiosk/kiosk_browser/browser_widget.py @@ -1,4 +1,4 @@ -from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, QtGui, QtSvg +from PyQt6 import QtCore, QtWidgets, QtWebEngineWidgets, QtWebEngineCore, QtGui, QtSvgWidgets from enum import Enum, auto import logging import re @@ -52,7 +52,7 @@ def __init__(self, url, get_current_proxy, parent): )) # Allow sound playback without user gesture - self._webview.page().settings().setAttribute(QtWebEngineWidgets.QWebEngineSettings.PlaybackRequiresUserGesture, False) + self._webview.page().settings().setAttribute(QtWebEngineCore.QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, False) # Prevent opening context menu on right click or pressing menu self._webview.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) @@ -63,9 +63,9 @@ def __init__(self, url, get_current_proxy, parent): self._webview.loadFinished.connect(self._load_finished) # Shortcut to manually reload - QtWidgets.QShortcut('CTRL+R', self).activated.connect(self.reload) + QtGui.QShortcut('CTRL+R', self).activated.connect(self.reload) # Shortcut to perform a hard refresh - QtWidgets.QShortcut('CTRL+SHIFT+R', self).activated.connect(self._hard_refresh) + QtGui.QShortcut('CTRL+SHIFT+R', self).activated.connect(self._hard_refresh) # Prepare reload timer self._reload_timer = QtCore.QTimer(self) @@ -79,6 +79,7 @@ def reload(self): self._webview.setUrl(self._url) self._view(Status.LOADING) + # If reload_timer is ongoing, stop it, as we’re already reloading if self._reload_timer.isActive(): self._reload_timer.stop() @@ -108,12 +109,12 @@ def _hard_refresh(self): logging.info(f"Clearing HTTP cache (hard refresh)") self._webview.page().profile().clearHttpCache() - # Sleep before triggering reload to avoid a possible race condition. - # Future Qt versions may provide a signal on `QWebEngineProfile` to - # allow to queue the reload on clear-cache completion instead. + # Wait before triggering reload to avoid a possible race condition: # https://bugreports.qt.io/browse/QTBUG-111541 - time.sleep(0.25) - self.reload() + # Version 6.7 of Qt will provide a signal once the cache has been cleared: + # https://doc.qt.io/qt-6/qwebengineprofile.html#clearHttpCacheCompleted + self._view(Status.LOADING) + self._reload_timer.start(250) def _proxy_auth(self, get_current_proxy, url, auth, proxyHost): proxy = get_current_proxy() @@ -137,6 +138,8 @@ def _view(self, status): self._loading_page.hide() self._network_error_page.hide() self._webview.show() + # Set focus by clearing first, otherwise focus is lost after using CTRL+R + self._webview.clearFocus() self._webview.setFocus() def user_agent_with_system(user_agent, system_name, system_version): @@ -182,8 +185,8 @@ def network_error_page(parent): paragraph_1 = paragraph("Please ensure the Internet connection to this device is active.", parent) paragraph_2 = paragraph("If the problem persists, contact Senso Service.", parent) - logo = QtSvg.QSvgWidget("images/dividat-logo.svg", parent) - logo.renderer().setAspectRatioMode(QtCore.Qt.KeepAspectRatio) + logo = QtSvgWidgets.QSvgWidget("images/dividat-logo.svg", parent) + logo.renderer().setAspectRatioMode(QtCore.Qt.AspectRatioMode.KeepAspectRatio) logo.setFixedHeight(30) layout = QtWidgets.QVBoxLayout() diff --git a/kiosk/kiosk_browser/captive_portal.py b/kiosk/kiosk_browser/captive_portal.py index 5f78fcc0..8e2eec7a 100644 --- a/kiosk/kiosk_browser/captive_portal.py +++ b/kiosk/kiosk_browser/captive_portal.py @@ -10,7 +10,7 @@ import logging from enum import Enum, auto from http import HTTPStatus -from PyQt5 import QtWidgets +from PyQt6 import QtWidgets from typing import Callable check_connection_url = 'http://captive.dividat.com/' diff --git a/kiosk/kiosk_browser/dialogable_widget.py b/kiosk/kiosk_browser/dialogable_widget.py index 227b907b..14e6200d 100644 --- a/kiosk/kiosk_browser/dialogable_widget.py +++ b/kiosk/kiosk_browser/dialogable_widget.py @@ -1,4 +1,4 @@ -from PyQt5 import QtWidgets, QtCore, QtGui +from PyQt6 import QtWidgets, QtCore, QtGui from typing import Callable overlay_color: str = '#888888' @@ -30,8 +30,8 @@ def __init__( policy = QtWidgets.QSizePolicy() policy.setVerticalStretch(1) policy.setHorizontalStretch(1) - policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred) - policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Preferred) + policy.setVerticalPolicy(QtWidgets.QSizePolicy.Policy.Preferred) + policy.setHorizontalPolicy(QtWidgets.QSizePolicy.Policy.Preferred) self.setSizePolicy(policy) # Layout @@ -41,7 +41,7 @@ def __init__( self.setLayout(self._layout) # Shortcuts - QtWidgets.QShortcut('ESC', self).activated.connect(self._on_escape) + QtGui.QShortcut('ESC', self).activated.connect(self._on_escape) def inner_widget(self): return self._inner_widget @@ -118,7 +118,7 @@ def title_line( """) button = QtWidgets.QPushButton("❌", dialog) - button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) + button.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) button.setStyleSheet(""" QPushButton { background-color: rgba(255, 255, 255, 0.2); diff --git a/kiosk/kiosk_browser/main_widget.py b/kiosk/kiosk_browser/main_widget.py index 1d7e4af9..cb6c32dd 100644 --- a/kiosk/kiosk_browser/main_widget.py +++ b/kiosk/kiosk_browser/main_widget.py @@ -1,4 +1,4 @@ -from PyQt5 import QtWidgets, QtCore +from PyQt6 import QtWidgets, QtCore, QtGui import time from kiosk_browser import browser_widget, captive_portal, dialogable_widget, proxy as proxy_module @@ -48,8 +48,8 @@ def __init__(self, kiosk_url: str, settings_url: str, toggle_settings_key: str): self._layout.addWidget(self._dialogable_browser) self.setLayout(self._layout) - # Shortcut to toggle settings - QtWidgets.QShortcut(toggle_settings_key, self).activated.connect(self._toggle_settings) + # Shortcuts + QtGui.QShortcut(toggle_settings_key, self).activated.connect(self._toggle_settings) # Look at events with the eventFilter function self.installEventFilter(self) diff --git a/kiosk/kiosk_browser/proxy.py b/kiosk/kiosk_browser/proxy.py index d5c26900..1fa64cb6 100644 --- a/kiosk/kiosk_browser/proxy.py +++ b/kiosk/kiosk_browser/proxy.py @@ -6,7 +6,7 @@ import logging import threading import urllib -from PyQt5.QtNetwork import QNetworkProxy +from PyQt6.QtNetwork import QNetworkProxy from dataclasses import dataclass from dbus.mainloop.glib import DBusGMainLoop from gi.repository import GLib @@ -104,16 +104,16 @@ def find(f, xs): return next((x for x in xs if f(x)), None) def set_proxy_in_qt_app(hostname, port): - logging.info(f"Set proxy to {hostname}:{port} in Qt application") network_proxy = QNetworkProxy() - network_proxy.setType(QNetworkProxy.HttpProxy) + network_proxy.setType(QNetworkProxy.ProxyType.HttpProxy) network_proxy.setHostName(hostname) network_proxy.setPort(port) QNetworkProxy.setApplicationProxy(network_proxy) + logging.info(f"Set proxy to {hostname}:{port} in Qt application") def set_no_proxy_in_qt_app(): - logging.info(f"Set no proxy in Qt application") QNetworkProxy.setApplicationProxy(QNetworkProxy()) + logging.info(f"Set no proxy in Qt application") class Proxy(): diff --git a/kiosk/mypy.ini b/kiosk/mypy.ini index f7e489a3..ff82608e 100644 --- a/kiosk/mypy.ini +++ b/kiosk/mypy.ini @@ -1,18 +1,18 @@ [mypy] -[mypy-PyQt5] +[mypy-PyQt6] ignore_missing_imports = True -[mypy-PyQt5.QtNetwork] +[mypy-PyQt6.QtNetwork] ignore_missing_imports = True -[mypy-PyQt5.QtCore] +[mypy-PyQt6.QtCore] ignore_missing_imports = True -[mypy-PyQt5.QtGui] +[mypy-PyQt6.QtGui] ignore_missing_imports = True -[mypy-PyQt5.QtWidgets] +[mypy-PyQt6.QtWidgets] ignore_missing_imports = True [mypy-dbus] diff --git a/kiosk/shell.nix b/kiosk/shell.nix index a93c36c2..b19ed44f 100644 --- a/kiosk/shell.nix +++ b/kiosk/shell.nix @@ -5,5 +5,6 @@ in pkgs = pkgs; system_name = "PlayOS"; system_version = "1.0.0-dev"; + # Provides qtwayland only for testing + additional_inputs = [ pkgs.qt6.qtwayland ]; } - diff --git a/testing/integration/kiosk-proxy.nix b/testing/integration/kiosk-proxy.nix new file mode 100644 index 00000000..71d91863 --- /dev/null +++ b/testing/integration/kiosk-proxy.nix @@ -0,0 +1,72 @@ +let + pkgs = import ../../pkgs { }; + serverPort = 8080; + proxyPort = 8888; + kiosk = import ../../kiosk { + pkgs = pkgs; + system_name = "PlayOS"; + system_version = "1.0.0"; + }; + toString = builtins.toString; +in +pkgs.nixosTest { + name = "proxy-test"; + + nodes = { + client = { config, ... }: { + imports = [ + (pkgs.importFromNixos "tests/common/user-account.nix") + (pkgs.importFromNixos "tests/common/x11.nix") + ]; + + # Override is needed to enable in test VM, see connman tests: + # https://github.com/NixOS/nixpkgs/blob/1772251828be641110eb9a47ef530a1252ba211e/nixos/tests/connman.nix#L47-L52 + services.connman.enable = pkgs.lib.mkOverride 0 true; + + # We need a graphical environment and regular user for the kiosk browser + services.xserver = { + enable = true; + }; + test-support.displayManager.auto.user = "alice"; + + environment.systemPackages = [ + pkgs.connman + kiosk + ]; + }; + }; + + testScript = '' + start_all() + + # Wait for X11 and connman, required by kiosk + client.wait_for_x() + client.wait_for_unit("connman.service") + + with subtest('kiosk-browser uses configured proxy'): + service_name = client.succeed("connmanctl services | head -1 | awk '{print $3}'").strip(' \t\n\r') + client.succeed(f"connmanctl config {service_name} proxy manual http://user:p4ssw0rd@theproxy:${toString proxyPort}") + + kiosk_result = client.execute( + 'su - alice -c "kiosk-browser http://thecloud:${toString serverPort} http://foo.xyz" 2>&1', + check_return=False, + check_output=True, + timeout=10 + ) + + # Ideally here we would check if starting the kiosk resulted in a request + # to a proxy and HTTP server running in separate VMs. Unfortunately it + # has proven difficult to set up such a test with connman on the client, + # and there are very few NixOS tests using connman to take inspiration + # from. + # + # So for now we simply test whether the proxy has been picked up and + # configured in the Qt framework. + + if "Set proxy to theproxy:${toString proxyPort}" not in kiosk_result[1]: + print(kiosk_result[1]) + raise AssertionError("Expected kiosk logs to contain info about configured proxy.") + ''; + +} + From 59e295c1e6a3a1a0bd4fa3ce545dd0ba03248f09 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Thu, 17 Oct 2024 06:19:47 +0000 Subject: [PATCH 2/7] Adapt eventFilter implementation for Qt6 In Qt6, enum values have been namespaced with the enum type. This handler has been added after the previous attempt at migrating to Qt6. --- kiosk/kiosk_browser/main_widget.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kiosk/kiosk_browser/main_widget.py b/kiosk/kiosk_browser/main_widget.py index cb6c32dd..1be8a9e1 100644 --- a/kiosk/kiosk_browser/main_widget.py +++ b/kiosk/kiosk_browser/main_widget.py @@ -84,15 +84,15 @@ def _close_dialog(self): def eventFilter(self, source, event): # Toggle settings with a long press on the Menu key - if event.type() == QtCore.QEvent.ShortcutOverride: - if event.key() == QtCore.Qt.Key_Menu: + if event.type() == QtCore.QEvent.Type.ShortcutOverride: + if event.key() == QtCore.Qt.Key.Key_Menu: if not event.isAutoRepeat(): self._menu_press_since = time.time() elif self._menu_press_since is not None and time.time() - self._menu_press_since > self._menu_press_delay_seconds: self._menu_press_since = None self._toggle_settings() - elif event.type() == QtCore.QEvent.KeyRelease: - if event.key() == QtCore.Qt.Key_Menu and not event.isAutoRepeat(): + elif event.type() == QtCore.QEvent.Type.KeyRelease: + if event.key() == QtCore.Qt.Key.Key_Menu and not event.isAutoRepeat(): self._menu_press_since = None return super(MainWidget, self).eventFilter(source, event) From 1753ea238de36ad968a44e1b50f6f44050ab0854 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Thu, 17 Oct 2024 06:29:25 +0000 Subject: [PATCH 3/7] Use an on-the-record profile named 'Default' In Qt 6 the default behaviour was changed to create off-the-record profiles, which are not written to disk. In order to obtain persistent profiles again, we explicitly create a profile named "Default", which matches the default naming in Qt 5. We unset the page when the app is closed, to avoid the following warning due to page and profile being torn down in the wrong order: Release of profile requested but WebEnginePage still not deleted. Expect troubles ! --- kiosk/kiosk_browser/browser_widget.py | 3 ++- kiosk/kiosk_browser/main_widget.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/kiosk/kiosk_browser/browser_widget.py b/kiosk/kiosk_browser/browser_widget.py index bf2a3135..ae410663 100644 --- a/kiosk/kiosk_browser/browser_widget.py +++ b/kiosk/kiosk_browser/browser_widget.py @@ -32,7 +32,8 @@ def __init__(self, url, get_current_proxy, parent): # Init views self._loading_page = loading_page(self) self._network_error_page = network_error_page(self) - self._webview = QtWebEngineWidgets.QWebEngineView(self) + self._profile = QtWebEngineCore.QWebEngineProfile("Default") + self._webview = QtWebEngineWidgets.QWebEngineView(self._profile, self) # Add views to layout self._layout.addWidget(self._loading_page) diff --git a/kiosk/kiosk_browser/main_widget.py b/kiosk/kiosk_browser/main_widget.py index 1be8a9e1..70d3136b 100644 --- a/kiosk/kiosk_browser/main_widget.py +++ b/kiosk/kiosk_browser/main_widget.py @@ -54,6 +54,12 @@ def __init__(self, kiosk_url: str, settings_url: str, toggle_settings_key: str): # Look at events with the eventFilter function self.installEventFilter(self) + def closeEvent(self, event): + event.accept() + + # Unset page in web view to avoid it outliving the browser profile + self._dialogable_browser.inner_widget()._webview.setPage(None) + # Private def _toggle_settings(self): From fda922bc041ef2db6f6c37df1a0336b556eab56d Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Thu, 17 Oct 2024 07:07:34 +0000 Subject: [PATCH 4/7] Set an explicit app name for kiosk-browser We previously let the app name be inferred from the name of the executable, but this is brittle and caused a rename when the Nix Qt wrapper configuration was changed. We explicitly set the app name to "kiosk-browser", which is what was the previously inferred name with the wrapped executable. This is important to preserve earlier cache and storage locations which are derived from the app name, see https://doc.qt.io/qt-6/qstandardpaths.html --- kiosk/kiosk_browser/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kiosk/kiosk_browser/__init__.py b/kiosk/kiosk_browser/__init__.py index a8bb5f6f..f9b576c5 100644 --- a/kiosk/kiosk_browser/__init__.py +++ b/kiosk/kiosk_browser/__init__.py @@ -12,6 +12,7 @@ def start(kiosk_url, settings_url, toggle_settings_key, fullscreen = True): logging.basicConfig(level=logging.INFO) app = QApplication(sys.argv) + app.setApplicationName("kiosk-browser") mainWidget = main_widget.MainWidget( kiosk_url = parseUrl(kiosk_url), From cf961669c5c3719ea616144b04953ddf5596ad98 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Thu, 17 Oct 2024 11:30:30 +0200 Subject: [PATCH 5/7] Fix position of changelog entry --- controller/Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/Changelog.md b/controller/Changelog.md index a2fe3083..5468cb16 100644 --- a/controller/Changelog.md +++ b/controller/Changelog.md @@ -2,6 +2,7 @@ ## Added +- kiosk: Migrate to Qt6 - kiosk: Open settings with a long press on the Menu key - controller: Enable spatial navigation using the arrow keys - os: Improve installation device selection @@ -34,7 +35,6 @@ - os: Limit permitted SSH modes and forwarding options - os: Ignore suspend and hibernation key presses, but interpret as poweroff when long-pressed - driver: Upgrade to add support for Senso firmware updates via app -- kiosk: Migrate to Qt6 ## Removed From 4c4decbc3ae6845c70f7b133c09bf5715f5d6914 Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Fri, 18 Oct 2024 11:43:07 +0200 Subject: [PATCH 6/7] Revert "Remove ineffective multi-VM test for kiosk proxy use" This reverts commit d20087788e6311292f1ba25bbd78e7184cce06cb. --- testing/integration/kiosk-proxy.nix | 91 ++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 8 deletions(-) diff --git a/testing/integration/kiosk-proxy.nix b/testing/integration/kiosk-proxy.nix index 71d91863..41a10947 100644 --- a/testing/integration/kiosk-proxy.nix +++ b/testing/integration/kiosk-proxy.nix @@ -13,15 +13,63 @@ pkgs.nixosTest { name = "proxy-test"; nodes = { + thecloud = { + virtualisation.vlans = [ 1 ]; + + networking = { + firewall.allowedTCPPorts = [ serverPort ]; + }; + + systemd.services.http-server = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = + let + counter = pkgs.writeScript "request-counter.sh" '' + #!${pkgs.bash}/bin/bash + COUNTER_FILE="/var/log/request-counter" + + COUNT=$(cat $COUNTER_FILE || echo 0) + ((COUNT++)) + echo $COUNT > $COUNTER_FILE + echo "HTTP/1.1 200 OK" + ''; + in + "${pkgs.nmap}/bin/ncat -lk -p ${toString serverPort} -c ${counter}"; + Restart = "always"; + }; + }; + }; + + theproxy = { config, ... }: { + virtualisation.vlans = [ 2 1 ]; + + networking.nat.enable = true; + networking.firewall.allowedTCPPorts = [ proxyPort ]; + + services.tinyproxy = { + enable = true; + settings = { + Listen = "0.0.0.0"; + Port = proxyPort; + BasicAuth = "user p4ssw0rd"; + }; + }; + }; + client = { config, ... }: { imports = [ (pkgs.importFromNixos "tests/common/user-account.nix") (pkgs.importFromNixos "tests/common/x11.nix") ]; + virtualisation.vlans = [ 2 ]; + # Override is needed to enable in test VM, see connman tests: # https://github.com/NixOS/nixpkgs/blob/1772251828be641110eb9a47ef530a1252ba211e/nixos/tests/connman.nix#L47-L52 services.connman.enable = pkgs.lib.mkOverride 0 true; + services.connman.networkInterfaceBlacklist = [ "eth0" ]; # We need a graphical environment and regular user for the kiosk browser services.xserver = { @@ -30,6 +78,7 @@ pkgs.nixosTest { test-support.displayManager.auto.user = "alice"; environment.systemPackages = [ + pkgs.curl pkgs.connman kiosk ]; @@ -37,13 +86,40 @@ pkgs.nixosTest { }; testScript = '' + def reset(): + thecloud.succeed('rm -f /var/log/request-counter') + + def expect_requests(n): + if (n == 0): + thecloud.succeed('test ! -f /var/log/request-counter') + else: + thecloud.succeed(f'diff <(echo {n}) /var/log/request-counter') + start_all() + # Wait for the HTTP server and proxy to start + thecloud.wait_for_unit('http-server.service') + theproxy.wait_for_unit('tinyproxy.service') + + with subtest('Sanity check: Direct curl request fails'): + client.fail('curl http://thecloud:${toString serverPort}') + expect_requests(0) + + reset() + + with subtest('Sanity check: Proxied curl request arrives'): + client.succeed( + 'curl --proxy http://user:p4ssw0rd@theproxy:${toString proxyPort} http://thecloud:${toString serverPort}' + ) + expect_requests(1) + + reset() + # Wait for X11 and connman, required by kiosk client.wait_for_x() client.wait_for_unit("connman.service") - with subtest('kiosk-browser uses configured proxy'): + with subtest('kiosk-browser can run with configured proxy'): service_name = client.succeed("connmanctl services | head -1 | awk '{print $3}'").strip(' \t\n\r') client.succeed(f"connmanctl config {service_name} proxy manual http://user:p4ssw0rd@theproxy:${toString proxyPort}") @@ -54,14 +130,13 @@ pkgs.nixosTest { timeout=10 ) - # Ideally here we would check if starting the kiosk resulted in a request - # to a proxy and HTTP server running in separate VMs. Unfortunately it - # has proven difficult to set up such a test with connman on the client, - # and there are very few NixOS tests using connman to take inspiration - # from. - # + # Ideally here we would check if the request actually arrived at thecloud. + # Unfortunately the proxy is currently not contacted by the kiosk in this + # test setup, even though if we look we see that the proxy is set as + # application proxy. This issue seems to be specific to the test setup, if + # we test in a real installation, the configured proxy is actually used. # So for now we simply test whether the proxy has been picked up and - # configured in the Qt framework. + # configured. if "Set proxy to theproxy:${toString proxyPort}" not in kiosk_result[1]: print(kiosk_result[1]) From ca3cfe95dd8ab3e76e8b1a1f7f243dca1e71ad0b Mon Sep 17 00:00:00 2001 From: Johannes Emerich Date: Fri, 18 Oct 2024 12:47:52 +0200 Subject: [PATCH 7/7] Add test for actual proxy usage by kiosk --- testing/integration/kiosk-proxy.nix | 72 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/testing/integration/kiosk-proxy.nix b/testing/integration/kiosk-proxy.nix index 41a10947..6a084d69 100644 --- a/testing/integration/kiosk-proxy.nix +++ b/testing/integration/kiosk-proxy.nix @@ -2,6 +2,7 @@ let pkgs = import ../../pkgs { }; serverPort = 8080; proxyPort = 8888; + kioskUrl = "http://kiosk.local/"; kiosk = import ../../kiosk { pkgs = pkgs; system_name = "PlayOS"; @@ -13,11 +14,12 @@ pkgs.nixosTest { name = "proxy-test"; nodes = { - thecloud = { + theproxy = { virtualisation.vlans = [ 1 ]; - networking = { - firewall.allowedTCPPorts = [ serverPort ]; + networking.firewall = { + enable = false; + allowedTCPPorts = [ proxyPort ]; }; systemd.services.http-server = { @@ -40,13 +42,6 @@ pkgs.nixosTest { Restart = "always"; }; }; - }; - - theproxy = { config, ... }: { - virtualisation.vlans = [ 2 1 ]; - - networking.nat.enable = true; - networking.firewall.allowedTCPPorts = [ proxyPort ]; services.tinyproxy = { enable = true; @@ -54,6 +49,9 @@ pkgs.nixosTest { Listen = "0.0.0.0"; Port = proxyPort; BasicAuth = "user p4ssw0rd"; + Upstream = [ + ''http 127.0.0.1:8080 "${builtins.head (builtins.match "https?://([^/]+)/?" kioskUrl)}"'' + ]; }; }; }; @@ -64,12 +62,11 @@ pkgs.nixosTest { (pkgs.importFromNixos "tests/common/x11.nix") ]; - virtualisation.vlans = [ 2 ]; + virtualisation.vlans = [ 1 ]; # Override is needed to enable in test VM, see connman tests: # https://github.com/NixOS/nixpkgs/blob/1772251828be641110eb9a47ef530a1252ba211e/nixos/tests/connman.nix#L47-L52 services.connman.enable = pkgs.lib.mkOverride 0 true; - services.connman.networkInterfaceBlacklist = [ "eth0" ]; # We need a graphical environment and regular user for the kiosk browser services.xserver = { @@ -85,31 +82,41 @@ pkgs.nixosTest { }; }; + extraPythonPackages = ps: [ + ps.colorama + ps.types-colorama + ]; + + testScript = '' + ${builtins.readFile ../helpers/nixos-test-script-helpers.py} + def reset(): - thecloud.succeed('rm -f /var/log/request-counter') + theproxy.succeed('rm -f /var/log/request-counter') - def expect_requests(n): - if (n == 0): - thecloud.succeed('test ! -f /var/log/request-counter') + def expect_requests(n = None): + if n is None: + theproxy.succeed('test -f /var/log/request-counter') + elif n == 0: + theproxy.succeed('test ! -f /var/log/request-counter') else: - thecloud.succeed(f'diff <(echo {n}) /var/log/request-counter') + theproxy.succeed(f'diff <(echo {n}) /var/log/request-counter') start_all() # Wait for the HTTP server and proxy to start - thecloud.wait_for_unit('http-server.service') + theproxy.wait_for_unit('http-server.service') theproxy.wait_for_unit('tinyproxy.service') - with subtest('Sanity check: Direct curl request fails'): - client.fail('curl http://thecloud:${toString serverPort}') + with TestPrecondition('Direct curl request to kiosk URL fails'): + client.fail('curl ${kioskUrl}') expect_requests(0) reset() - with subtest('Sanity check: Proxied curl request arrives'): + with TestPrecondition('Proxied curl request to kiosk URL arrives'): client.succeed( - 'curl --proxy http://user:p4ssw0rd@theproxy:${toString proxyPort} http://thecloud:${toString serverPort}' + 'curl --proxy http://user:p4ssw0rd@theproxy:${toString proxyPort} ${kioskUrl}' ) expect_requests(1) @@ -119,29 +126,28 @@ pkgs.nixosTest { client.wait_for_x() client.wait_for_unit("connman.service") - with subtest('kiosk-browser can run with configured proxy'): + with TestCase('kiosk-browser uses configured proxy'): service_name = client.succeed("connmanctl services | head -1 | awk '{print $3}'").strip(' \t\n\r') client.succeed(f"connmanctl config {service_name} proxy manual http://user:p4ssw0rd@theproxy:${toString proxyPort}") kiosk_result = client.execute( - 'su - alice -c "kiosk-browser http://thecloud:${toString serverPort} http://foo.xyz" 2>&1', + 'su - alice -c "kiosk-browser ${kioskUrl} http://foo.xyz" 2>&1', check_return=False, check_output=True, timeout=10 ) - # Ideally here we would check if the request actually arrived at thecloud. - # Unfortunately the proxy is currently not contacted by the kiosk in this - # test setup, even though if we look we see that the proxy is set as - # application proxy. This issue seems to be specific to the test setup, if - # we test in a real installation, the configured proxy is actually used. - # So for now we simply test whether the proxy has been picked up and - # configured. - + # Expect proxy takeup in kiosk logs if "Set proxy to theproxy:${toString proxyPort}" not in kiosk_result[1]: print(kiosk_result[1]) raise AssertionError("Expected kiosk logs to contain info about configured proxy.") - ''; + + # Expect kiosk request in proxy logs + wait_for_logs(theproxy, "GET http://kiosk.local/", unit="tinyproxy.service") + + # Expect arrival of one or more requests at the mock server + expect_requests() +''; }