Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ jobs:
NIX_BINARY_CACHE_PRIVATE_KEY: ${{ secrets.NIX_BINARY_CACHE_PRIVATE_KEY }}
- run: NIXPKGS_ALLOW_UNFREE=1 ./build vm

build-vm-portable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: ./.github/actions/prep-build-env
with:
NIX_CACHE_ACCOUNT_ID: ${{ secrets.NIX_CACHE_ACCOUNT_ID }}
NIX_CACHE_ACCESS_KEY_ID: ${{ secrets.NIX_CACHE_ACCESS_KEY_ID }}
NIX_CACHE_SECRET_ACCESS_KEY: ${{ secrets.NIX_CACHE_SECRET_ACCESS_KEY }}
NIX_BINARY_CACHE_PRIVATE_KEY: ${{ secrets.NIX_BINARY_CACHE_PRIVATE_KEY }}
- run: NIXPKGS_ALLOW_UNFREE=1 ./build vm-portable

e2e-tests:
runs-on: ubuntu-latest
steps:
Expand Down
8 changes: 8 additions & 0 deletions base/controller-service.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ with lib;
description = "DNS-SD service types to browse for and annotate networks with in controller UI";
type = types.listOf types.str;
};
downloadLimit = mkOption {
default = "10M";
example = "500K";
description = "curl --limit-rate value for update bundle downloads. If unset, no limit is applied.";
type = types.nullOr types.str;
};
};
};

Expand Down Expand Up @@ -58,6 +64,8 @@ with lib;
environment = {
PLAYOS_ANNOTATE_DISCOVERED_SERVICES =
mkIf hasAnnotatedServices (concatStringsSep ";" cfg.annotateDiscoveredServices);
PLAYOS_UPDATE_DL_LIMIT =
mkIf (cfg.downloadLimit != null) cfg.downloadLimit;
};
};
};
Expand Down
40 changes: 33 additions & 7 deletions build
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ set -euo pipefail
PRODUCTION_TARGETS="develop|validation|master|release-disk"

# other targets used for development/testing/etc
OTHER_TARGETS="vm|stuck|lab-key|shed-key|test-e2e|all"
OTHER_TARGETS="vm|stuck|lab-key|shed-key|test-e2e|portable|vm-portable|all"

ALL_TARGETS="$PRODUCTION_TARGETS|$OTHER_TARGETS"

Expand Down Expand Up @@ -44,13 +44,22 @@ fi
# See https://nixos.wiki/wiki/FAQ/How_can_I_install_a_proprietary_or_unfree_package%3F
export NIXPKGS_ALLOW_UNFREE=1

if [ "$TARGET" == "vm" ]; then
if [[ "$TARGET" == "vm" ]] || [[ "$TARGET" == "vm-portable" ]] ; then

(set -x; nix-build \
--arg buildInstaller false \
--arg buildBundle false \
--arg buildLive false \
--arg buildDisk false)
build_args=(
--arg buildInstaller false
--arg buildBundle false
--arg buildLive false
--arg buildDisk false
)

if [ "$TARGET" == "vm-portable" ]; then
build_args+=(
--arg applicationPath ./flavors/portable.nix
)
fi

(set -x; nix-build "${build_args[@]}")

echo -e "
Run ./result/bin/run-in-vm to start a VM.
Expand Down Expand Up @@ -115,6 +124,23 @@ elif [ "$TARGET" == "master" ]; then
echo
echo "Run ./result/bin/deploy-update to deploy."

elif [ "$TARGET" == "portable" ]; then

scripts/info-branch-commit
scripts/confirm-or-abort

(set -x; nix-build \
--arg applicationPath ./flavors/portable.nix \
--arg updateCert ./pki/master/cert.pem \
--arg updateUrl https://dist.dividat.com/releases/playos/portable/ \
--arg deployUrl s3://dist.dividat.ch/releases/playos/portable/ \
--arg kioskUrl https://play.dividat.com/ \
--arg watchdogUrls '["https://play.dividat.com/" "https://api.dividat.com"]' \
--arg buildDisk false)

echo
echo "Run ./result/bin/deploy-update to deploy."

elif [ "$TARGET" == "stuck" ]; then

echo "Creating a stuck system that will not self-update."
Expand Down
14 changes: 13 additions & 1 deletion controller/bindings/network-watchdog/network_watchdog.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,19 @@ let watchdog_service_name = "playos-network-watchdog.service"
let watchdog_disable_filepath =
"/home/play/.config/playos-network-watchdog/disabled"

let is_disabled () = Lwt_unix.file_exists watchdog_disable_filepath
let is_disabled systemd =
let%lwt is_installed =
Lwt.catch
(fun () ->
let%lwt _ = Systemd.Manager.get_unit systemd watchdog_service_name in
Lwt.return true
)
(fun _ -> Lwt.return false)
in
let%lwt is_manually_disabled =
Lwt_unix.file_exists watchdog_disable_filepath
in
Lwt.return (is_manually_disabled || not is_installed)

let enable systemd =
let%lwt exists = Lwt_unix.file_exists watchdog_disable_filepath in
Expand Down
2 changes: 1 addition & 1 deletion controller/bindings/network-watchdog/network_watchdog.mli
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
(* Tiny interface for enabling/disabling the PlayOS network watchdog *)

val is_disabled : unit -> bool Lwt.t
val is_disabled : Systemd.Manager.t -> bool Lwt.t

val enable : Systemd.Manager.t -> unit Lwt.t

Expand Down
7 changes: 4 additions & 3 deletions controller/server/gui.ml
Original file line number Diff line number Diff line change
Expand Up @@ -484,10 +484,11 @@ module StatusGui = struct
in
reboot ()

let get_status ~health_s ~(update_s : Update.state React.signal) ~rauc =
let get_status ~systemd ~health_s ~(update_s : Update.state React.signal)
~rauc =
let health_state = health_s |> Lwt_react.S.value in
let update_state = update_s |> Lwt_react.S.value in
let%lwt watchdog_disabled = Network_watchdog.is_disabled () in
let%lwt watchdog_disabled = Network_watchdog.is_disabled systemd in
let%lwt booted_slot = Rauc.get_booted_slot rauc in
let%lwt rauc =
match update_state.process_state with
Expand Down Expand Up @@ -532,7 +533,7 @@ module StatusGui = struct
redirect' (Uri.of_string "/status")
)
|> get "/status" (fun _req ->
let%lwt status = get_status ~update_s ~health_s ~rauc in
let%lwt status = get_status ~systemd ~update_s ~health_s ~rauc in
Lwt.return (page (Status_page.html status))
)
end
Expand Down
30 changes: 20 additions & 10 deletions controller/server/update_client.ml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ module type UpdateClientDeps = sig

val download_dir : string

val download_limit : string option

val get_proxy : unit -> Uri.t option Lwt.t
end

let make_deps ?download_dir_override get_proxy base_url :
(module UpdateClientDeps) =
let make_deps ?download_dir_override ?download_limit_override get_proxy base_url
: (module UpdateClientDeps) =
(module struct
let base_url = base_url

Expand All @@ -29,6 +31,13 @@ let make_deps ?download_dir_override get_proxy base_url :
Sys.getenv_opt "STATE_DIRECTORY" |> Option.value ~default:"/tmp"
in
download_dir_override |> Option.value ~default:default_download_dir

let download_limit =
match download_limit_override with
| Some _ ->
download_limit_override
| None ->
Sys.getenv_opt "PLAYOS_UPDATE_DL_LIMIT"
end
)

Expand Down Expand Up @@ -71,15 +80,16 @@ module UpdateClient (DepsI : UpdateClientDeps) = struct
let bundle_path =
Format.sprintf "%s/%s" download_dir (bundle_file_name version)
in
let options =
[ "--continue-at"
; "-" (* resume download *)
; "--limit-rate"
; "10M"
; "--output"
; bundle_path
]
let limit_rate_opts =
match DepsI.download_limit with
| Some limit ->
[ "--limit-rate"; limit ]
| None ->
[]
in
let resume_opts = [ "--continue-at"; "-" ] in
let output_opts = [ "--output"; bundle_path ] in
let options = List.flatten [ limit_rate_opts; resume_opts; output_opts ] in
let%lwt proxy = get_proxy () in
match%lwt Curl.request ?proxy ~options url with
| RequestSuccess _ ->
Expand Down
4 changes: 4 additions & 0 deletions controller/server/update_client.mli
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ module type UpdateClientDeps = sig

val download_dir : string

(** If set, passed to curl as --limit-rate *)
val download_limit : string option

val get_proxy : unit -> Uri.t option Lwt.t
end

val make_deps :
?download_dir_override:string
-> ?download_limit_override:string
-> (unit -> Uri.t option Lwt.t)
-> Uri.t
-> (module UpdateClientDeps)
Expand Down
39 changes: 28 additions & 11 deletions controller/tests/server/update/update_client_tests.ml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ let rec wait_for_mock_server ?(timeout = 0.2) ?(remaining_tries = 3) url =
in
Lwt.fail (Failure err_msg)

let run_test_case ?(proxy = NoProxy) switch f =
let run_test_case ?(proxy = NoProxy) ?download_limit switch f =
let server = mock_server () in
let server_url, server_task = server#run () in
let%lwt () = wait_for_mock_server server_url in
Expand All @@ -78,8 +78,8 @@ let run_test_case ?(proxy = NoProxy) switch f =
in
let () = Sys.mkdir temp_dir 0o777 in
let module DepsI =
( val Update_client.make_deps ~download_dir_override:temp_dir get_proxy
base_url
( val Update_client.make_deps ~download_dir_override:temp_dir
?download_limit_override:download_limit get_proxy base_url
)
in
let module UpdateC = Update_client.Make (DepsI) in
Expand Down Expand Up @@ -153,16 +153,32 @@ let test_invalid_proxy_fail _ (module Client : S) =
^ Printexc.to_string other_exn
)

let test_download_limit_honored server (module Client : S) =
let version = "1.0.0" in
let bundle = String.make (3 * 1024) 'x' in
let () = server#add_bundle version bundle in
let start = Unix.gettimeofday () in
let%lwt _bundle_path = Client.download version in
let elapsed = Unix.gettimeofday () -. start in
Alcotest.(check bool)
"Download took at least 2.0 seconds with 1K/s limit" true (elapsed >= 2.0) ;
Lwt.return ()

let () =
let () = setup_log () in
(* All tests cases are run with proxy setup and without to verify it works
(* Common test cases are run with proxy setup and without to verify it works
always *)
let test_cases =
[ ("Get latest version", test_get_version_ok)
; ("Download bundle", test_download_bundle_ok)
; ("Resume download works", test_resume_bundle_download)
]
in
let download_limit_case =
Alcotest_lwt.test_case "Download rate is limited" `Slow (fun switch () ->
run_test_case ~download_limit:"1K" switch test_download_limit_honored
)
in
(* An extra case to check that proxy settings are honored in general *)
let invalid_proxy_case =
Alcotest_lwt.test_case "Invalid proxy specified" `Quick (fun switch () ->
Expand All @@ -173,13 +189,14 @@ let () =
Lwt_main.run
@@ Alcotest_lwt.run "Basic tests"
[ ( "without-proxy"
, List.map
(fun (name, test_f) ->
Alcotest_lwt.test_case name `Quick (fun switch () ->
run_test_case switch test_f
)
)
test_cases
, download_limit_case
:: List.map
(fun (name, test_f) ->
Alcotest_lwt.test_case name `Quick (fun switch () ->
run_test_case switch test_f
)
)
test_cases
)
; ( "with-proxy"
, invalid_proxy_case
Expand Down
78 changes: 78 additions & 0 deletions flavors/portable.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
let
defaults = import ../application.nix;
in
{
inherit (defaults) safeProductName version overlays greeting;

fullProductName = "${defaults.fullProductName} (portable)";

module = { config, lib, pkgs, ... }: {
imports = [
defaults.module
];

# use a lower download limit to ensure minimal interference
playos.controller.downloadLimit = "2M";

# we do not expect a stable/permanent network connection
playos.networking.watchdog.enable = lib.mkForce false;

# metrics are optimized for specific hardware and stable setup, probably
# not very useful for ad-hoc portable setups
playos.monitoring.enable = lib.mkForce false;

# Do not hard-code HDMI as default
hardware.pulseaudio = {
extraConfig = lib.mkForce ''
# Respond to changes in connected outputs
load-module module-switch-on-port-available
load-module module-switch-on-connect blacklist=""

# Prevent PulseAudio from remembering previous muted states.
# First, we unload the default restore module, then reload it
# explicitly telling it NOT to restore mute states.
unload-module module-device-restore
load-module module-device-restore restore_volume=true restore_muted=false
'';
};

systemd.user.services.force-unmute = {
description = "Force unmute PulseAudio on login";
wantedBy = [ "default.target" ];
after = [ "pulseaudio.service" ];

path = [ pkgs.pulseaudio ];

script = ''
# Brief pause to ensure PulseAudio has fully initialized its sinks
sleep 2
pactl set-sink-mute @DEFAULT_SINK@ 0

# Optional: Force volume to a safe default so it is never at 0%
pactl set-sink-volume @DEFAULT_SINK@ 70%
'';

serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};

# Add bindings for media keys to allow volume control
environment.etc."sxhkd/sxhkdrc".text = ''
XF86AudioLowerVolume
${pkgs.pulseaudio}/bin/pactl set-sink-volume @DEFAULT_SINK@ -5%

XF86AudioRaiseVolume
${pkgs.pulseaudio}/bin/pactl set-sink-volume @DEFAULT_SINK@ +5%

XF86AudioMute
${pkgs.pulseaudio}/bin/pactl set-sink-mute @DEFAULT_SINK@ toggle
'';

services.xserver.displayManager.sessionCommands = ''
# Provides media key bindings for volume control
${pkgs.sxhkd}/bin/sxhkd -c /etc/sxhkd/sxhkdrc &
Comment thread
knuton marked this conversation as resolved.
'';
};
}
Loading
Loading