diff --git a/contrib/ci/fedora.sh b/contrib/ci/fedora.sh index b2a51a0ba..37a515f27 100755 --- a/contrib/ci/fedora.sh +++ b/contrib/ci/fedora.sh @@ -65,6 +65,7 @@ cp $HOME/rpmbuild/RPMS/*/*.rpm dist if [ "$CI" = "true" ]; then sed "s,^DisabledPlugins=.*,DisabledPlugins=," -i /etc/fwupd/daemon.conf + sed "s,^AllowEmulation=false,AllowEmulation=true," -i /etc/fwupd/daemon.conf # set up enough PolicyKit and D-Bus to run the daemon mkdir -p /run/dbus diff --git a/data/bash-completion/fwupdmgr b/data/bash-completion/fwupdmgr index 34001e0dd..572408e7e 100644 --- a/data/bash-completion/fwupdmgr +++ b/data/bash-completion/fwupdmgr @@ -4,9 +4,14 @@ _fwupdmgr_cmd_list=( 'clear-results' 'disable-remote' 'device-test' + 'device-emulate' 'downgrade' 'download' 'enable-remote' + 'emulation-tag' + 'emulation-untag' + 'emulation-load' + 'emulation-save' 'get-approved-firmware' 'get-bios-setting' 'get-blocked-firmware' diff --git a/data/daemon.conf b/data/daemon.conf index 4dbb02077..92a9fb459 100644 --- a/data/daemon.conf +++ b/data/daemon.conf @@ -52,6 +52,9 @@ OnlyTrusted=true # Show private data like device serial numbers and instance IDs to clients ShowDevicePrivate=true +# Allow capturing and loading device emulation +AllowEmulation=false + # UIDs that should marked as trusted TrustedUids= diff --git a/data/device-tests/analogix-anx7518.json b/data/device-tests/analogix-anx7518.json index 296c6da3a..4cdb29f5a 100644 --- a/data/device-tests/analogix-anx7518.json +++ b/data/device-tests/analogix-anx7518.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/5d860747c1378ef8921f95e41bbb7055dc9b470043655a65b00157ab74dd12e8-Analogix-ANX7518-fw-1.5.01-rx-1206.cab", + "emulation-url": "https://fwupd.org/downloads/63698f1d36491d795f36335a28e301e7484b1735c63aa763c007c149cc74569c-Analogix-ANX7518-fw-1.5.01-rx-1206.zip", "components": [ { "version": "0001.1501", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/2e4b5747ea2659ddae893a7b8a238d6d9c3166b426c0d0e5feb308a443662c38-Analogix-ANX7518-fw-1.5.08.cab", + "emulation-url": "https://fwupd.org/downloads/ff75670536d3de6def5cb4e6e1377dc1ef132882288dd6ceee37c0aecc3fbda3-Analogix-ANX7518-fw-1.5.08.zip", "components": [ { "version": "0001.1508", diff --git a/data/device-tests/caldigit-ts4.json b/data/device-tests/caldigit-ts4.json index 1d227eeb0..f31f62cde 100644 --- a/data/device-tests/caldigit-ts4.json +++ b/data/device-tests/caldigit-ts4.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/dbeeba12332296ae68eaa104ed3b4b437659bfe027e347010a87c75bcea87292-CalDigit-TS4-combind-050322-secure-Cus-22.cab", + "emulation-url": "https://fwupd.org/downloads/e755191ff3c7d26777273262eff0be266758ebe5b79685cd44d14d491ab6107a-CalDigit-TS4-combind-050322-secure-Cus-22.zip", "components": [ { "version": "F907.14.10", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/161f0243e4f1222114b941b7dce057b962e38f9f9a1e799437531afcd5a68677-CalDigit-TS4-combind-121022-secure-Cus-37.cab", + "emulation-url": "https://fwupd.org/downloads/21c81548a6126f806966497613bc6e33ddd5b3dc045e7cc83b27abd64ba6afd8-CalDigit-TS4-combind-121022-secure-Cus-37.zip", "components": [ { "version": "F907.14.13", diff --git a/data/device-tests/fwupd-a3bu-xplained.json b/data/device-tests/fwupd-a3bu-xplained.json index 9ffb0cae1..00cd1ab3b 100644 --- a/data/device-tests/fwupd-a3bu-xplained.json +++ b/data/device-tests/fwupd-a3bu-xplained.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/f5bbeaba1037dce31dd12f349e8148ae35f98b61-a3bu-xplained123.cab", + "emulation-url": "https://fwupd.org/downloads/01b95b0206f1a42a2bf95a432d162ef1f9f1f71edb5696127c923ceffadfdf68-a3bu-xplained123.zip", "components": [ { "version": "1.23", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/24d838541efe0340bf67e1cc5a9b95526e4d3702-a3bu-xplained124.cab", + "emulation-url": "https://fwupd.org/downloads/357483755bbc4f3a33c0b5bc05a0ef49654be0a15c14cbde2bdf0cd9c203d632-a3bu-xplained124.zip", "components": [ { "version": "1.24", diff --git a/data/device-tests/fwupd-at90usbkey.json b/data/device-tests/fwupd-at90usbkey.json index f0363b5d2..69635eadf 100644 --- a/data/device-tests/fwupd-at90usbkey.json +++ b/data/device-tests/fwupd-at90usbkey.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/b6bef375597e848971f230cf992c9740f7bf5b92-at90usbkey123.cab", + "emulation-url": "https://fwupd.org/downloads/57c8acd0de45ff01d91da5ecec1f826fbb438cc3f7b11ca732a8fbfdc2ce24e1-at90usbkey123.zip", "components": [ { "version": "1.23", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/47807fd4a94a4d5514ac6bf7a73038e00ed63225-at90usbkey124.cab", + "emulation-url": "https://fwupd.org/downloads/e65432a2dd63c3ce666cc13a0f5ae13eb6d15cebbc1d022cafcff46b892cdcc4-at90usbkey124.zip", "components": [ { "version": "1.24", diff --git a/data/device-tests/google-servo-micro.json b/data/device-tests/google-servo-micro.json index adc77aafd..f629b542d 100644 --- a/data/device-tests/google-servo-micro.json +++ b/data/device-tests/google-servo-micro.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/3c3123b6eaa89d5469553b301210a9e4cb06efa98a1cb7c6847a1d10bb0a0c4b-servo_micro_v2.4.0.cab", + "emulation-url": "https://fwupd.org/downloads/72f84e212c5c5976bd46ea0fa4f1dda26b4c41f35ac01aaa212867e672690726-servo_micro_v2.4.0.zip", "components": [ { "version": "2.4.0", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/1dc362734f138e71fa838ac5503491d297715d7e9bccc0424a62c4a5c68526cc-servo_micro_v2.4.17.cab", + "emulation-url": "https://fwupd.org/downloads/b5f152b8e1f814375ae586bdac9536b70b3a9cd1a2a12bf685e8da50eef78f2a-servo_micro_v2.4.17.zip", "components": [ { "version": "2.4.17", diff --git a/data/device-tests/hughski-colorhug-plus.json b/data/device-tests/hughski-colorhug-plus.json index e5313a398..6249442ed 100644 --- a/data/device-tests/hughski-colorhug-plus.json +++ b/data/device-tests/hughski-colorhug-plus.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/5cbff92158331aeb10008ca36fa918a9637dde7bfe31de3e0523d14090be8977-fakedevice01_dfu.cab", + "emulation-url": "https://fwupd.org/downloads/8be5c7f3fe5c399a8699768da3f4ddd2582df19c70e197b852efaa027f42688b-fakedevice01_dfu.zip", "components": [ { "version": "0.1", @@ -16,6 +17,7 @@ }, { "url": "https://fwupd.org/downloads/8bc3afd07a0af3baaab8b19893791dd3972e8305-fakedevice02_dfu.cab", + "emulation-url": "https://fwupd.org/downloads/72c580400020f22929510e9fec43d6781d56af697e72e8d560b45daa57e1817c-fakedevice02_dfu.zip", "components": [ { "version": "0.2", diff --git a/data/device-tests/hughski-colorhug.json b/data/device-tests/hughski-colorhug.json index 96fef5e0e..0faba9ca0 100644 --- a/data/device-tests/hughski-colorhug.json +++ b/data/device-tests/hughski-colorhug.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/9a4e77009da7d3b5f15a1388afeb9e5d41a5a8ae-hughski-colorhug2-1.2.5.cab", + "emulation-url": "https://fwupd.org/downloads/08ddc01a50fb866398d01cc4829531640e651f0233306e10a54117f88474b153-hughski-colorhug-1.2.5.zip", "components": [ { "version": "1.2.5", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/2a066c8a1bfbd99f161c867b4dbe7e51ac36fc2b16ef37b11d18419874fbcb6c-hughski-colorhug-1.2.6.cab", + "emulation-url": "https://fwupd.org/downloads/b25122f17912467c1488aaadb59483efc3fe773cb7d002f64289177ca2f3285c-hughski-colorhug-1.2.6.zip", "components": [ { "version": "1.2.6", diff --git a/data/device-tests/hughski-colorhug2.json b/data/device-tests/hughski-colorhug2.json index 7c0e1db06..19d0a6921 100644 --- a/data/device-tests/hughski-colorhug2.json +++ b/data/device-tests/hughski-colorhug2.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab", + "emulation-url": "https://fwupd.org/downloads/5ddb5d8e9ac85ea05b8dafcad638befdf3e70e5e1db0ca47489fa43d93f33a57-hughski-colorhug2-2.0.6.zip", "components": [ { "version": "2.0.6", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/e5ad222bdbd3d3d48d8613e67c7e0a0e194f8cd828e33c554d9f05d933e482c7-hughski-colorhug2-2.0.7.cab", + "emulation-url": "https://fwupd.org/downloads/29ee025e65b6a469556bbdd819cba665b043244c90c65b9f659933a347f8cf2e-hughski-colorhug2-2.0.7.zip", "components": [ { "version": "2.0.7", diff --git a/data/device-tests/lenovo-03x7609-cxaudio.json b/data/device-tests/lenovo-03x7609-cxaudio.json index 6d6828fc0..5217dfe6a 100644 --- a/data/device-tests/lenovo-03x7609-cxaudio.json +++ b/data/device-tests/lenovo-03x7609-cxaudio.json @@ -5,6 +5,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/2e0bf8aaf9c63ca11cfe3444d032277c21ec0d678e5963123a8b33e5dcd37d99-Lenovo-ThinkPad-USBCGen2Dock-Firmware-49-0E-14.cab", + "emulation-url": "https://fwupd.org/downloads/9b6a1401bbd5ab3304a50a00dfc6d17d853bfbfd63f80c0360774d0e9e46e772-Lenovo-ThinkPad-USBCGen2Dock-Firmware-49-0E-14.zip", "components": [ { "name": "cxaudio", diff --git a/data/device-tests/lenovo-GX90T33021-vli.json b/data/device-tests/lenovo-GX90T33021-vli.json index 7e7f43600..eeb2db3ac 100644 --- a/data/device-tests/lenovo-GX90T33021-vli.json +++ b/data/device-tests/lenovo-GX90T33021-vli.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/2004603b40bd529d85c2fcfcf9d50d77b47229e6564ef0ea9c03633bdccff94d-Lenovo-Travel_Hub_1in3_New.cab", + "emulation-url": "https://fwupd.org/downloads/de48f2281354c4479b8b2302cf85b9643b0ca248fcf4beb19a71f5aba7927278-Lenovo-Travel_Hub_1in3_New.zip", "components": [ { "name": "tier1", @@ -16,10 +17,11 @@ }, { "url": "https://fwupd.org/downloads/5fb4f4dce233626558806c2b7474d3b5ed4f500f6013aa3b376a54322642d449-Lenovo-Travel_Hub_1in3_Old.cab", + "emulation-url": "https://fwupd.org/downloads/58221c63de8cdec9b4db3cc72b1e0303b6cfee80b7e7965edad9816c124b0cd7-Lenovo-Travel_Hub_1in3_Old.zip", "components": [ { "name": "tier1", - "version": "04.33", + "version": "4.33", "guids": [ "7636b85e-d79f-5d30-a329-458957958b88" ] diff --git a/data/device-tests/realtek-rts5423.json b/data/device-tests/realtek-rts5423.json index 8a753d6dc..e45a12582 100644 --- a/data/device-tests/realtek-rts5423.json +++ b/data/device-tests/realtek-rts5423.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/460a18d93f5d6118908d8437009ebbd6eceb3c6a0cdfbaf31f8a96df008564da-Realtek-RTS5423-1.56.cab", + "emulation-url": "https://fwupd.org/downloads/eaa82f84d31f524f48369c9b46698ff00bf6e398dbb66fb7fed3e6808c69789c-Realtek-RTS5423-1.56.zip", "components": [ { "version": "1.56", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/659e721b0efbe2dfc003d3d30ea5b771c00113cc9a162333ee5a8918c4515f69-Realtek-RTS5423-1.57.cab", + "emulation-url": "https://fwupd.org/downloads/962e175bf7809183620ecfad90583e912fb0e16a98c44b0ab539468fd340fc8a-Realtek-RTS5423-1.57.zip", "components": [ { "version": "1.57", diff --git a/data/device-tests/realtek-rts5855.json b/data/device-tests/realtek-rts5855.json index c19ff22b1..77e4e4405 100644 --- a/data/device-tests/realtek-rts5855.json +++ b/data/device-tests/realtek-rts5855.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/ff989c4b71c92a4a217dfb2f82c1c87691b8eb31-rts5855_v0.4.cab", + "emulation-url": "https://fwupd.org/downloads/75d6c26e3842bbd00991a585078e872cae66c7a81e15c5736fc478ba5ec0f77d-rts5855_v0.4.zip", "components": [ { "version": "0.4", diff --git a/data/device-tests/synaptics-prometheus.json b/data/device-tests/synaptics-prometheus.json index d0573d151..d60181da3 100644 --- a/data/device-tests/synaptics-prometheus.json +++ b/data/device-tests/synaptics-prometheus.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/7c260a13ea6df444f7a1fa8fa2bf431876a8c7203b6aeac371564aad30756ff1-Synaptics-Prometheus-10.01.3121519.cab", + "emulation-url": "https://fwupd.org/downloads/73877ed7c8c15dac6adf04db32bd5e9f1e702229cfdb266fe98855ee53929cd1-Synaptics-Prometheus-10.01.3121519.zip", "components": [ { "version": "10.01.3121519", diff --git a/data/device-tests/ugreen-cm260.json b/data/device-tests/ugreen-cm260.json index 28e35ad9e..ed135dfaf 100644 --- a/data/device-tests/ugreen-cm260.json +++ b/data/device-tests/ugreen-cm260.json @@ -3,7 +3,7 @@ "interactive": false, "steps": [ { - "url": "https://fwupd.org/downloads/53cd390673287a99e5d4cc4cf30b5fade0f5e708fe0b1bd424bf36467642d76c-Ugreen-CM260-7.2.1.0.cab", + "url": "https://fwupd.org/downloads/de152591660080ed5de8550b1b2dfcd738d1f5a68bd30e5f4c684b39b9ebeeb2-Ugreen-CM260-7.2.1.0.cab", "components": [ { "version": "7.2.1.0", @@ -14,10 +14,10 @@ ] }, { - "url": "https://fwupd.org/downloads/eec2c6216fa86077504587633fc8d018a366a1cfe3df8dd9c575c2e8ef0ef423-Ugreen-CM260-7.2.2.0.cab", + "url": "https://fwupd.org/downloads/9480d06d1a3e524609660653fadc337d14d72f362bd2e80b15c5c3c1733f629b-Ugreen-CM260-7.2.2.0.cab", "components": [ { - "version": "7.2.1.0", + "version": "7.2.2.0", "guids": [ "7afc5bff-be55-5e95-81ca-584f13207b1d" ] diff --git a/data/device-tests/wacom-intuos-bt-m.json b/data/device-tests/wacom-intuos-bt-m.json index bcf88e653..febdd4707 100644 --- a/data/device-tests/wacom-intuos-bt-m.json +++ b/data/device-tests/wacom-intuos-bt-m.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/d16b682de56c42b134f1e5af7f9926c62dc246df851648e49aa1d1d0e5b38532-Wacom-Intuos_BT-M_MainFW-1.66.cab", + "emulation-url": "https://fwupd.org/downloads/3cfdf76725fe4f58a2f0b2e41e402e6e76af37cde526f8be72b3382b230da9e1-Wacom-Intuos_BT-M_MainFW-1.66.zip", "components": [ { "name": "main", @@ -16,6 +17,7 @@ }, { "url": "https://fwupd.org/downloads/1ee4f3dc9fd08acd4c6bc833b25e7061f85ddd40122148d66940cd3ddd748920-Wacom-Intuos_BT-M_BluetoothFW-1.12.cab", + "emulation-url": "https://fwupd.org/downloads/2579550908be1d3e81f0776f2b26e68f6b95e485e58c35f8b02ffdb5dec1ee13-Wacom-Intuos_BT-M_BluetoothFW-1.12.zip", "components": [ { "name": "bluetooth", diff --git a/data/device-tests/wistron-dock-40b7.json b/data/device-tests/wistron-dock-40b7.json index 48dc52c37..3b687ddf7 100644 --- a/data/device-tests/wistron-dock-40b7.json +++ b/data/device-tests/wistron-dock-40b7.json @@ -4,6 +4,7 @@ "steps": [ { "url": "https://fwupd.org/downloads/f00838f10d6faf58ff57cbfcfca390ed63bd804e4c654f19ff89423b286dc5d2-lda_viking_r_composite.cab", + "emulation-url": "https://fwupd.org/downloads/08c477c340e51990fc5df84907149f513aef65ae7f828b469e0a14ac01579a34-lda_viking_r_composite.zip", "components": [ { "version": "1.0.1.4", @@ -15,6 +16,7 @@ }, { "url": "https://fwupd.org/downloads/43bcfb3278ad2baf918af6abc972e0de9a0de66a25b012177b00d96664819010-lda_viking_r_composite.cab", + "emulation-url": "https://fwupd.org/downloads/330411c3eaaba8b7e8b51108fddc614963af5b5fcc0fd328683197491584f387-lda_viking_r_composite2.zip", "components": [ { "version": "1.0.1.6", diff --git a/data/fish-completion/fwupdmgr.fish b/data/fish-completion/fwupdmgr.fish index 85fe77e25..8bd9b3ca0 100644 --- a/data/fish-completion/fwupdmgr.fish +++ b/data/fish-completion/fwupdmgr.fish @@ -67,6 +67,10 @@ complete -c fwupdmgr -n '__fish_use_subcommand' -x -a verify-update -d 'Update t complete -c fwupdmgr -n '__fish_use_subcommand' -x -a inhibit -d 'Inhibit the system to prevent upgrades' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a uninhibit -d 'Uninhibit the system to allow upgrades' complete -c fwupdmgr -n '__fish_use_subcommand' -x -a quit -d 'Asks the daemon to quit' +complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-load -d 'Load device emulation data' +complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-save -d 'Save device emulation data' +complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-tag -d 'Adds devices to watch for future emulation' +complete -c fwupdmgr -n '__fish_use_subcommand' -x -a emulation-untag -d 'Removes devices to watch for future emulation' # commands exclusively consuming device IDs set -l deviceid_consumers activate clear-results downgrade get-releases get-results get-updates reinstall switch-branch unlock update verify verify-update diff --git a/data/installed-tests/fwupd.sh b/data/installed-tests/fwupd.sh index 320ded0d5..ae553c5c0 100755 --- a/data/installed-tests/fwupd.sh +++ b/data/installed-tests/fwupd.sh @@ -1,12 +1,11 @@ #!/bin/sh exec 2>&1 -dirname=`dirname $0` run_test() { - if [ -f $dirname/$1 ]; then - $dirname/$1 + if [ -f @installedtestsbindir@/$1 ]; then + @installedtestsbindir@/$1 rc=$?; if [ $rc != 0 ]; then exit $rc; fi fi } @@ -27,5 +26,13 @@ run_test synaptics-prometheus-self-test run_test dfu-self-test run_test mtd-self-test +# grab device tests from the CDN to avoid incrementing the download counter +export FWUPD_DEVICE_TESTS_BASE_URI=http://cdn.fwupd.org/downloads +for f in `grep --files-with-matches -r emulation-url @devicetestdir@`; do + echo "Emulating for $f" + fwupdmgr device-emulate --no-unreported-check --no-remote-check --no-metadata-check "$f" + rc=$?; if [ $rc != 0 ]; then exit $rc; fi +done + # success! exit 0 diff --git a/data/installed-tests/meson.build b/data/installed-tests/meson.build index be3d5c6d9..dfce86b1c 100644 --- a/data/installed-tests/meson.build +++ b/data/installed-tests/meson.build @@ -2,6 +2,7 @@ con2 = configuration_data() con2.set('installedtestsdir', installed_test_datadir) con2.set('installedtestsbindir', installed_test_bindir) con2.set('installedtestsdatadir', installed_test_datadir) +con2.set('devicetestdir', join_paths(datadir, 'fwupd', 'device-tests')) con2.set('bindir', bindir) con2.set('libexecdir', libexecdir) @@ -44,9 +45,11 @@ install_data([ install_dir: installed_test_datadir, ) -install_data([ - 'fwupd.sh', - ], +configure_file( + input: 'fwupd.sh', + output: 'fwupd.sh', + configuration: con2, + install: true, install_dir: installed_test_bindir, ) diff --git a/debian/fwupd-tests.postinst b/debian/fwupd-tests.postinst index 81a462c45..d8e1f3801 100644 --- a/debian/fwupd-tests.postinst +++ b/debian/fwupd-tests.postinst @@ -8,6 +8,7 @@ if [ "$1" = configure ] && [ -z "$2" ]; then if [ -f /etc/fwupd/daemon.conf ]; then if [ "$CI" = "true" ]; then sed "s,^DisabledPlugins=.*,DisabledPlugins=," -i /etc/fwupd/daemon.conf + sed "s,^AllowEmulation=false,AllowEmulation=true," -i /etc/fwupd/daemon.conf else echo "To enable test suite, modify /etc/fwupd/daemon.conf" fi diff --git a/debian/fwupd-tests.postrm b/debian/fwupd-tests.postrm index c43835526..d5bae7d3e 100644 --- a/debian/fwupd-tests.postrm +++ b/debian/fwupd-tests.postrm @@ -7,6 +7,7 @@ if [ "$1" = remove -o "$1" = purge ]; then if [ -f /etc/fwupd/daemon.conf ]; then if [ "$CI" = "true" ]; then sed "s,^DisabledPlugins=,DisabledPlugins=test," -i /etc/fwupd/daemon.conf + sed "s,^AllowEmulation=true,AllowEmulation=false," -i /etc/fwupd/daemon.conf else echo "To disable test suite, modify /etc/fwupd/daemon.conf" fi diff --git a/debian/tests/ci b/debian/tests/ci index 2ecab73f2..4b71089d7 100755 --- a/debian/tests/ci +++ b/debian/tests/ci @@ -4,6 +4,7 @@ set -e modprobe mtdram 2>&1 || true sed "s,^DisabledPlugins=.*,DisabledPlugins=," -i /etc/fwupd/daemon.conf sed "s,^VerboseDomains=.*,VerboseDomains=*," -i /etc/fwupd/daemon.conf +sed "s,^AllowEmulation=false,AllowEmulation=true," -i /etc/fwupd/daemon.conf sed "s,ConditionVirtualization=.*,," \ /lib/systemd/system/fwupd.service > \ /etc/systemd/system/fwupd.service diff --git a/docs/device-emulation.md b/docs/device-emulation.md new file mode 100644 index 000000000..d3061c5ee --- /dev/null +++ b/docs/device-emulation.md @@ -0,0 +1,71 @@ +--- +title: Device Emulation +--- + +## Introduction + +Using device-tests, fwupd can prevent regressions by updating and downgrading firmware on real +hardware. However, much past a few dozen devices this does not scale, either by time, or because +real devices need plugging in and out. We can unit test the plugin internals, but this does not +actually test devices being attached, removed, and being updated. + +By recording the backend devices we can build a "history" in GUsb of what control, interrupt and +bulk transfers were sent to, and received from the device. By dumping these we can "replay" the +update without the physical hardware connected. + +There are some problems that make this emulation slightly harder than the naive implementation: + +* Devices are sometimes detached into a different "bootloader" device with a new VID:PID +* Devices might be "composite" and actually be multiple logical devices in one physical device +* Devices might not be 100% deterimistic, e.g. queries might be processed out-of-order + +For people to generate and consume emulated devices, we do need to make the process easy to +understand, and also easy to use. Some key points that we think are important, is the ability to: + +* Dump the device of an unmodified running daemon. +* Filter to multiple or single devices, to avoid storing data for unrelated parts of the system. +* Load an emulated device into an unmodified running daemon. + +Because we do not want to modify the daemon, we think it makes sense to *load* and *save* emulation +state over D-Bus. Each phase can be controlled, which makes it easy to view, and edit, the recorded +emulation data. + +For instance, calling `fwupdmgr emulation-tag` would ask the end user to choose a device to start +*recording* so that subsequent re-plugs are available to save. +As the device state may not be persistent we save the device-should-be-recorded metadata in the +pending database like we would do for a successful firmware update. + +To demo this, something like this could be done: + + # connect ColorHug2 + fwupdmgr modify-config AllowEmulation true + fwupdmgr emulation-tag b0a78eb71f4eeea7df8fb114522556ba8ce22074 + # or, using the GUID + # fwupdmgr emulation-tag 2082b5e0-7a64-478a-b1b2-e3404fab6dad + # remove and re-insert ColorHug2 + fwupdmgr get-devices --filter emulation-tag + fwupdmgr download https://fwupd.org/downloads/170f2c19f17b7819644d3fcc7617621cc3350a04-hughski-colorhug2-2.0.6.cab + fwupdmgr install e5* --allow-reinstall + fwupdmgr emulation-save colorhug.zip + # remove ColorHug2 + fwupdmgr emulation-load colorhug.zip + fwupdmgr get-devices --filter emulated + fwupdmgr install e5* --allow-reinstall + fwupdmgr modify-config AllowEmulation false + +## Device Tests + +The `emulation-url` string parameter can be specified in the `steps` section of a specific device +test. This causes the front end to load the emulation data before running the specific step. + +Device tests without emulation data will be skipped. + +For example: + + fwupdmgr device-emulate ../data/device-tests/hughski-colorhug2.json + Decompressing… [***************************************] + Waiting… [***************************************] + Hughski ColorHug2: OK! + Decompressing… [***************************************] + Waiting… [***************************************] + Hughski ColorHug2: OK! diff --git a/docs/index.html b/docs/index.html index 13086a6b4..2de205113 100644 --- a/docs/index.html +++ b/docs/index.html @@ -48,6 +48,9 @@ + diff --git a/libfwupd/fwupd-client-sync.c b/libfwupd/fwupd-client-sync.c index a52d89909..c2383b33d 100644 --- a/libfwupd/fwupd-client-sync.c +++ b/libfwupd/fwupd-client-sync.c @@ -2566,3 +2566,113 @@ fwupd_client_upload_bytes(FwupdClient *self, } return g_steal_pointer(&helper->bytes); } + +static void +fwupd_client_emulation_load_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + FwupdClientHelper *helper = (FwupdClientHelper *)user_data; + helper->ret = fwupd_client_emulation_load_finish(FWUPD_CLIENT(source), res, &helper->error); + g_main_loop_quit(helper->loop); +} + +/** + * fwupd_client_emulation_load + * @self: a #FwupdClient + * @data: archive data of JSON files + * @cancellable: (nullable): optional #GCancellable + * @error: (nullable): optional return location for an error + * + * Loads an emulated device into the daemon backend that has the phases set by the JSON data, + * for instance, having one USB device emulated for the bootloader and another emulated for the + * runtime interface. + * + * Returns: %TRUE for success + * + * Since: 1.8.11 + **/ +gboolean +fwupd_client_emulation_load(FwupdClient *self, + GBytes *data, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(FwupdClientHelper) helper = NULL; + + g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); + g_return_val_if_fail(data != NULL, FALSE); + g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* connect */ + if (!fwupd_client_connect(self, cancellable, error)) + return FALSE; + + /* call async version and run loop until complete */ + helper = fwupd_client_helper_new(self); + fwupd_client_emulation_load_async(self, + data, + cancellable, + fwupd_client_emulation_load_cb, + helper); + g_main_loop_run(helper->loop); + if (!helper->ret) { + g_propagate_error(error, g_steal_pointer(&helper->error)); + return FALSE; + } + return TRUE; +} + +static void +fwupd_client_emulation_save_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + FwupdClientHelper *helper = (FwupdClientHelper *)user_data; + helper->bytes = + fwupd_client_emulation_save_finish(FWUPD_CLIENT(source), res, &helper->error); + g_main_loop_quit(helper->loop); +} + +/** + * fwupd_client_emulation_save: + * @self: a #FwupdClient + * @cancellable: (nullable): optional #GCancellable + * @error: (nullable): optional return location for an error + * + * Gets the captured data from all filtered devices for all recorded phases. The data is returned + * in a ZIP archive of JSON output. + * + * NOTE: Device events are not automatically recorded for all devices. You must call something + * like `ModifyDevice(device_id, 'flags','emulation-tag')` to start the recording the backend. + * + * Once the device has been re-inserted then the emulation data will be available using + * this API call. + * + * Returns: (transfer full): archive data + * + * Since: 1.8.11 + **/ +GBytes * +fwupd_client_emulation_save(FwupdClient *self, GCancellable *cancellable, GError **error) +{ + g_autoptr(FwupdClientHelper) helper = NULL; + + g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); + g_return_val_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable), NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + + /* connect */ + if (!fwupd_client_connect(self, cancellable, error)) + return NULL; + + /* call async version and run loop until complete */ + helper = fwupd_client_helper_new(self); + fwupd_client_emulation_save_async(self, + cancellable, + fwupd_client_emulation_save_cb, + helper); + g_main_loop_run(helper->loop); + if (helper->bytes == NULL) { + g_propagate_error(error, g_steal_pointer(&helper->error)); + return NULL; + } + return g_steal_pointer(&helper->bytes); +} diff --git a/libfwupd/fwupd-client-sync.h b/libfwupd/fwupd-client-sync.h index d1ad3ba35..8dd82f331 100644 --- a/libfwupd/fwupd-client-sync.h +++ b/libfwupd/fwupd-client-sync.h @@ -254,5 +254,14 @@ fwupd_client_upload_bytes(FwupdClient *self, FwupdClientUploadFlags flags, GCancellable *cancellable, GError **error) G_GNUC_WARN_UNUSED_RESULT; +gboolean +fwupd_client_emulation_load(FwupdClient *self, + GBytes *data, + GCancellable *cancellable, + GError **error); +GBytes * +fwupd_client_emulation_save(FwupdClient *self, + GCancellable *cancellable, + GError **error) G_GNUC_WARN_UNUSED_RESULT; G_END_DECLS diff --git a/libfwupd/fwupd-client.c b/libfwupd/fwupd-client.c index c18b8acf5..20b4de649 100644 --- a/libfwupd/fwupd-client.c +++ b/libfwupd/fwupd-client.c @@ -5650,6 +5650,175 @@ fwupd_client_add_hint(FwupdClient *self, const gchar *key, const gchar *value) g_hash_table_insert(priv->hints, g_strdup(key), g_strdup(value)); } +static void +fwupd_client_emulation_load_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK(user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) val = NULL; + + val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (val == NULL) { + fwupd_client_fixup_dbus_error(error); + g_task_return_error(task, g_steal_pointer(&error)); + return; + } + + /* success */ + g_task_return_boolean(task, TRUE); +} + +/** + * fwupd_client_emulation_load_async: + * @self: a #FwupdClient + * @data: archive data of JSON files + * @cancellable: (nullable): optional #GCancellable + * @callback: the function to run on completion + * @callback_data: the data to pass to @callback + * + * Loads an emulated device into the daemon backend that has the phases set by the JSON data, + * for instance, having one USB device emulated for the bootloader and another emulated for the + * runtime interface. + * + * Since: 1.8.11 + **/ +void +fwupd_client_emulation_load_async(FwupdClient *self, + GBytes *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + FwupdClientPrivate *priv = GET_PRIVATE(self); + g_autoptr(GTask) task = NULL; + GVariant *variant; + + g_return_if_fail(FWUPD_IS_CLIENT(self)); + g_return_if_fail(data != NULL); + g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); + g_return_if_fail(priv->proxy != NULL); + + /* call into daemon */ + task = g_task_new(self, cancellable, callback, callback_data); + variant = g_variant_new_from_bytes(G_VARIANT_TYPE_BYTESTRING, data, FALSE); + g_dbus_proxy_call(priv->proxy, + "EmulationLoad", + g_variant_new_tuple(&variant, 1), + G_DBUS_CALL_FLAGS_NONE, + FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, + cancellable, + fwupd_client_emulation_load_cb, + g_steal_pointer(&task)); +} + +/** + * fwupd_client_emulation_load_finish: + * @self: a #FwupdClient + * @res: (not nullable): the asynchronous result + * @error: (nullable): optional return location for an error + * + * Gets the result of [method@FwupdClient.emulation_load_async]. + * + * Returns: %TRUE for success + * + * Since: 1.8.11 + **/ +gboolean +fwupd_client_emulation_load_finish(FwupdClient *self, GAsyncResult *res, GError **error) +{ + g_return_val_if_fail(FWUPD_IS_CLIENT(self), FALSE); + g_return_val_if_fail(g_task_is_valid(res, self), FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + return g_task_propagate_boolean(G_TASK(res), error); +} + +static void +fwupd_client_emulation_save_cb(GObject *source, GAsyncResult *res, gpointer user_data) +{ + g_autoptr(GTask) task = G_TASK(user_data); + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) val = NULL; + + val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error); + if (val == NULL) { + fwupd_client_fixup_dbus_error(error); + g_task_return_error(task, g_steal_pointer(&error)); + return; + } + + /* success */ + g_task_return_pointer(task, + g_variant_get_data_as_bytes(val), + (GDestroyNotify)g_bytes_unref); +} + +/** + * fwupd_client_emulation_save_async: + * @self: a #FwupdClient + * @cancellable: (nullable): optional #GCancellable + * @callback: the function to run on completion + * @callback_data: the data to pass to @callback + * + * Gets the captured data from all filtered devices for all recorded phases. The data is returned + * in a ZIP archive of JSON output. + * + * NOTE: Device events are not automatically recorded for all devices. You must call something + * like `ModifyDevice(device_id, 'flags','emulation-tag')` to start the recording the backend. + * + * Once the device has been re-inserted then the emulation data will be available using + * this API call. + * + * You must have called [method@Client.connect_async] on @self before using + * this method. + * + * Since: 1.8.11 + **/ +void +fwupd_client_emulation_save_async(FwupdClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + FwupdClientPrivate *priv = GET_PRIVATE(self); + g_autoptr(GTask) task = NULL; + + g_return_if_fail(FWUPD_IS_CLIENT(self)); + g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable)); + g_return_if_fail(priv->proxy != NULL); + + /* call into daemon */ + task = g_task_new(self, cancellable, callback, callback_data); + g_dbus_proxy_call(priv->proxy, + "EmulationSave", + NULL, + G_DBUS_CALL_FLAGS_NONE, + FWUPD_CLIENT_DBUS_PROXY_TIMEOUT, + cancellable, + fwupd_client_emulation_save_cb, + g_steal_pointer(&task)); +} + +/** + * fwupd_client_emulation_save_finish: + * @self: a #FwupdClient + * @res: (not nullable): the asynchronous result + * @error: (nullable): optional return location for an error + * + * Gets the result of [method@FwupdClient.emulation_save_async]. + * + * Returns: (transfer full): archive data + * + * Since: 1.8.11 + **/ +GBytes * +fwupd_client_emulation_save_finish(FwupdClient *self, GAsyncResult *res, GError **error) +{ + g_return_val_if_fail(FWUPD_IS_CLIENT(self), NULL); + g_return_val_if_fail(g_task_is_valid(res, self), NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + return g_task_propagate_pointer(G_TASK(res), error); +} + #ifdef SOUP_SESSION_COMPAT /* this is bad; we dlopen libsoup-2.4.so.1 and get the gtype manually * to avoid deps on both libcurl and libsoup whilst preserving ABI */ diff --git a/libfwupd/fwupd-client.h b/libfwupd/fwupd-client.h index 1cddf1db5..f4952c458 100644 --- a/libfwupd/fwupd-client.h +++ b/libfwupd/fwupd-client.h @@ -405,6 +405,23 @@ gboolean fwupd_client_uninhibit_finish(FwupdClient *self, GAsyncResult *res, GError **error) G_GNUC_WARN_UNUSED_RESULT; +void +fwupd_client_emulation_load_async(FwupdClient *self, + GBytes *data, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data); +gboolean +fwupd_client_emulation_load_finish(FwupdClient *self, GAsyncResult *res, GError **error); +void +fwupd_client_emulation_save_async(FwupdClient *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data); +GBytes * +fwupd_client_emulation_save_finish(FwupdClient *self, + GAsyncResult *res, + GError **error) G_GNUC_WARN_UNUSED_RESULT; FwupdStatus fwupd_client_get_status(FwupdClient *self); diff --git a/libfwupd/fwupd-enums.c b/libfwupd/fwupd-enums.c index d6f871e7d..417c5e330 100644 --- a/libfwupd/fwupd-enums.c +++ b/libfwupd/fwupd-enums.c @@ -211,6 +211,8 @@ fwupd_device_flag_to_string(FwupdDeviceFlags device_flag) return "unsigned-payload"; if (device_flag == FWUPD_DEVICE_FLAG_EMULATED) return "emulated"; + if (device_flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) + return "emulation-tag"; if (device_flag == FWUPD_DEVICE_FLAG_UNKNOWN) return "unknown"; return NULL; @@ -332,6 +334,8 @@ fwupd_device_flag_from_string(const gchar *device_flag) return FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD; if (g_strcmp0(device_flag, "emulated") == 0) return FWUPD_DEVICE_FLAG_EMULATED; + if (g_strcmp0(device_flag, "emulation-tag") == 0) + return FWUPD_DEVICE_FLAG_EMULATION_TAG; return FWUPD_DEVICE_FLAG_UNKNOWN; } diff --git a/libfwupd/fwupd-enums.h b/libfwupd/fwupd-enums.h index dc0165e1b..dea3e8f5f 100644 --- a/libfwupd/fwupd-enums.h +++ b/libfwupd/fwupd-enums.h @@ -535,6 +535,14 @@ typedef enum { * Since: 1.8.11 */ #define FWUPD_DEVICE_FLAG_EMULATED (1llu << 49) +/** + * FWUPD_DEVICE_FLAG_EMULATION_TAG: + * + * The device should be recorded by the backend, allowing emulation. + * + * Since: 1.8.11 + */ +#define FWUPD_DEVICE_FLAG_EMULATION_TAG (1llu << 50) /** * FWUPD_DEVICE_FLAG_UNKNOWN: * diff --git a/libfwupd/fwupd.map b/libfwupd/fwupd.map index 4ece215ee..1e9fb2ff5 100644 --- a/libfwupd/fwupd.map +++ b/libfwupd/fwupd.map @@ -904,6 +904,12 @@ LIBFWUPD_1.8.8 { LIBFWUPD_1.8.11 { global: + fwupd_client_emulation_load; + fwupd_client_emulation_load_async; + fwupd_client_emulation_load_finish; + fwupd_client_emulation_save; + fwupd_client_emulation_save_async; + fwupd_client_emulation_save_finish; fwupd_client_inhibit; fwupd_client_inhibit_async; fwupd_client_inhibit_finish; diff --git a/libfwupdplugin/fu-usb-device-private.h b/libfwupdplugin/fu-usb-device-private.h index 0050c45c9..cd0f7bd9f 100644 --- a/libfwupdplugin/fu-usb-device-private.h +++ b/libfwupdplugin/fu-usb-device-private.h @@ -8,5 +8,7 @@ #include "fu-usb-device.h" +#define FU_USB_DEVICE_EMULATION_TAG "org.freedesktop.fwupd.emulation.v1" + const gchar * fu_usb_device_get_platform_id(FuUsbDevice *self); diff --git a/libfwupdplugin/fu-usb-device.c b/libfwupdplugin/fu-usb-device.c index 9f9de5f3a..e639f0b30 100644 --- a/libfwupdplugin/fu-usb-device.c +++ b/libfwupdplugin/fu-usb-device.c @@ -86,6 +86,18 @@ fu_usb_device_finalize(GObject *object) G_OBJECT_CLASS(fu_usb_device_parent_class)->finalize(object); } +#if G_USB_CHECK_VERSION(0, 4, 5) +static void +fu_usb_device_flags_notify_cb(FuDevice *device, GParamSpec *pspec, gpointer user_data) +{ + GUsbDevice *usb_device = fu_usb_device_get_dev(FU_USB_DEVICE(device)); + if (usb_device == NULL) + return; + if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) + g_usb_device_add_tag(usb_device, FU_USB_DEVICE_EMULATION_TAG); +} +#endif + static void fu_usb_device_init(FuUsbDevice *device) { @@ -104,6 +116,19 @@ fu_usb_device_init(FuUsbDevice *device) #endif } +static void +fu_usb_device_constructed(GObject *obj) +{ + FuUsbDevice *self = FU_USB_DEVICE(obj); +#if G_USB_CHECK_VERSION(0, 4, 5) + /* copy this to the GUsbDevice */ + g_signal_connect(FU_DEVICE(self), + "notify::flags", + G_CALLBACK(fu_usb_device_flags_notify_cb), + NULL); +#endif +} + /** * fu_usb_device_is_open: * @device: a #FuUsbDevice @@ -675,7 +700,7 @@ fu_usb_device_set_dev(FuUsbDevice *device, GUsbDevice *usb_device) } #ifdef HAVE_GUSB -#if G_USB_CHECK_VERSION(0, 4, 4) +#if G_USB_CHECK_VERSION(0, 4, 5) /* propagate emulated flag */ if (usb_device != NULL && g_usb_device_is_emulated(usb_device)) fu_device_add_flag(FU_DEVICE(device), FWUPD_DEVICE_FLAG_EMULATED); @@ -913,6 +938,7 @@ fu_usb_device_class_init(FuUsbDeviceClass *klass) object_class->finalize = fu_usb_device_finalize; object_class->get_property = fu_usb_device_get_property; object_class->set_property = fu_usb_device_set_property; + object_class->constructed = fu_usb_device_constructed; device_class->open = fu_usb_device_open; device_class->setup = fu_usb_device_setup; device_class->ready = fu_usb_device_ready; diff --git a/plugins/corsair/fu-corsair-bp.c b/plugins/corsair/fu-corsair-bp.c index 5b4e91f10..0a6cf10d4 100644 --- a/plugins/corsair/fu-corsair-bp.c +++ b/plugins/corsair/fu-corsair-bp.c @@ -220,6 +220,9 @@ fu_corsair_bp_incorporate(FuDevice *self, FuDevice *donor) FuCorsairBp *bp_self = FU_CORSAIR_BP(self); FuCorsairBp *bp_donor = FU_CORSAIR_BP(donor); + /* FuUsbDevice */ + FU_DEVICE_CLASS(fu_corsair_bp_parent_class)->incorporate(self, donor); + bp_self->epin = bp_donor->epin; bp_self->epout = bp_donor->epout; bp_self->cmd_write_size = bp_donor->cmd_write_size; diff --git a/src/fu-config.c b/src/fu-config.c index 6b93925ea..5b75836e3 100644 --- a/src/fu-config.c +++ b/src/fu-config.c @@ -38,6 +38,7 @@ struct _FuConfig { gboolean ignore_power; gboolean only_trusted; gboolean show_device_private; + gboolean allow_emulation; }; G_DEFINE_TYPE(FuConfig, fu_config, G_TYPE_OBJECT) @@ -70,6 +71,7 @@ fu_config_reload(FuConfig *self, GError **error) g_autoptr(GError) error_ignore_power = NULL; g_autoptr(GError) error_only_trusted = NULL; g_autoptr(GError) error_show_device_private = NULL; + g_autoptr(GError) error_allow_emulation = NULL; g_autoptr(GError) error_enumerate_all = NULL; g_autoptr(GByteArray) buf = g_byte_array_new(); @@ -247,6 +249,14 @@ fu_config_reload(FuConfig *self, GError **error) self->show_device_private = TRUE; } + /* whether to allow emulation to work */ + self->allow_emulation = + g_key_file_get_boolean(keyfile, "fwupd", "AllowEmulation", &error_allow_emulation); + if (!self->allow_emulation && error_allow_emulation != NULL) { + g_debug("failed to read AllowEmulation key: %s", error_allow_emulation->message); + self->allow_emulation = FALSE; + } + /* fetch host best known configuration */ host_bkc = g_key_file_get_string(keyfile, "fwupd", "HostBkc", NULL); if (host_bkc != NULL && host_bkc[0] != '\0') @@ -446,6 +456,13 @@ fu_config_get_show_device_private(FuConfig *self) return self->show_device_private; } +gboolean +fu_config_get_allow_emulation(FuConfig *self) +{ + g_return_val_if_fail(FU_IS_CONFIG(self), FALSE); + return self->allow_emulation; +} + gboolean fu_config_get_enumerate_all_devices(FuConfig *self) { diff --git a/src/fu-config.h b/src/fu-config.h index 1d5fcad2b..6c9abc692 100644 --- a/src/fu-config.h +++ b/src/fu-config.h @@ -44,6 +44,8 @@ gboolean fu_config_get_only_trusted(FuConfig *self); gboolean fu_config_get_show_device_private(FuConfig *self); +gboolean +fu_config_get_allow_emulation(FuConfig *self); const gchar * fu_config_get_host_bkc(FuConfig *self); const gchar * diff --git a/src/fu-daemon.c b/src/fu-daemon.c index c9ff21bdb..39dd83e05 100644 --- a/src/fu-daemon.c +++ b/src/fu-daemon.c @@ -1399,6 +1399,45 @@ fu_daemon_daemon_method_call(GDBusConnection *connection, g_dbus_method_invocation_return_value(invocation, NULL); return; } + if (g_strcmp0(method_name, "EmulationLoad") == 0) { + g_autoptr(GBytes) data = NULL; + + g_debug("Called %s()", method_name); + + /* load data into engine */ + data = g_variant_get_data_as_bytes(g_variant_get_child_value(parameters, 0)); + if (!fu_engine_emulation_load(self->engine, data, &error)) { + g_dbus_method_invocation_return_error(invocation, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "failed to load emulation data: %s", + error->message); + return; + } + + /* success */ + g_dbus_method_invocation_return_value(invocation, NULL); + return; + } + if (g_strcmp0(method_name, "EmulationSave") == 0) { + g_autoptr(GBytes) data = NULL; + + g_debug("Called %s()", method_name); + + /* save data from engine */ + data = fu_engine_emulation_save(self->engine, &error); + if (data == NULL) { + g_dbus_method_invocation_return_error(invocation, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "failed to save emulation data: %s", + error->message); + return; + } + val = g_variant_new_from_bytes(G_VARIANT_TYPE_BYTESTRING, data, FALSE); + g_dbus_method_invocation_return_value(invocation, g_variant_new_tuple(&val, 1)); + return; + } if (g_strcmp0(method_name, "ModifyDevice") == 0) { const gchar *device_id; const gchar *key = NULL; diff --git a/src/fu-device-list.c b/src/fu-device-list.c index adef8900a..eb3d61a36 100644 --- a/src/fu-device-list.c +++ b/src/fu-device-list.c @@ -688,6 +688,8 @@ fu_device_list_replace(FuDeviceList *self, FuDeviceItem *item, FuDevice *device) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_SIGNED_PAYLOAD); if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD)) fu_device_add_flag(device, FWUPD_DEVICE_FLAG_UNSIGNED_PAYLOAD); + if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) + fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); /* device won't come back in right mode */ if (fu_device_has_flag(item->device, FWUPD_DEVICE_FLAG_WILL_DISAPPEAR)) { @@ -866,7 +868,8 @@ fu_device_list_get_wait_for_replug(FuDeviceList *self) GPtrArray *devices = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref); for (guint i = 0; i < self->devices->len; i++) { FuDeviceItem *item_tmp = g_ptr_array_index(self->devices, i); - if (fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG)) + if (fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG) && + !fu_device_has_flag(item_tmp->device, FWUPD_DEVICE_FLAG_EMULATED)) g_ptr_array_add(devices, g_object_ref(item_tmp->device)); } return devices; diff --git a/src/fu-engine.c b/src/fu-engine.c index 441812089..15a7a9d1d 100644 --- a/src/fu-engine.c +++ b/src/fu-engine.c @@ -96,7 +96,7 @@ static void fu_engine_ensure_security_attrs(FuEngine *self); typedef enum { - FU_ENGINE_INSTALL_PHASE_UNKNOWN, + FU_ENGINE_INSTALL_PHASE_SETUP, FU_ENGINE_INSTALL_PHASE_WRITE, FU_ENGINE_INSTALL_PHASE_ATTACH, FU_ENGINE_INSTALL_PHASE_DETACH, @@ -132,6 +132,8 @@ struct _FuEngine { GHashTable *compile_versions; GHashTable *approved_firmware; /* (nullable) */ GHashTable *blocked_firmware; /* (nullable) */ + GHashTable *emulation_phases; /* (element-type int utf8) */ + GHashTable *emulation_backend_ids; /* (element-type str int) */ gchar *host_machine_id; JcatContext *jcat_context; gboolean loaded; @@ -370,6 +372,8 @@ fu_engine_device_request_cb(FuDevice *device, FwupdRequest *request, FuEngine *s static const gchar * fu_engine_install_phase_to_string(FuEngineInstallPhase phase) { + if (phase == FU_ENGINE_INSTALL_PHASE_SETUP) + return "setup"; if (phase == FU_ENGINE_INSTALL_PHASE_WRITE) return "install"; if (phase == FU_ENGINE_INSTALL_PHASE_ATTACH) @@ -763,6 +767,7 @@ gboolean fu_engine_modify_config(FuEngine *self, const gchar *key, const gchar *value, GError **error) { const gchar *keys[] = {"ArchiveSizeMax", + "AllowEmulation", "ApprovedFirmware", "BlockedFirmware", "DisabledDevices", @@ -1056,6 +1061,118 @@ fu_engine_modify_bios_settings(FuEngine *self, return TRUE; } +static void +fu_engine_check_context_flag_save_events(FuEngine *self) +{ + if (g_hash_table_size(self->emulation_backend_ids) > 0 && + fu_config_get_allow_emulation(self->config)) { + fu_context_add_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); + } else { + fu_context_remove_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS); + } +} + +static gboolean +fu_engine_remove_device_flag(FuEngine *self, + const gchar *device_id, + FwupdDeviceFlags flag, + GError **error) +{ + FuDevice *proxy; + g_autoptr(FuDevice) device = NULL; + + if (flag == FWUPD_DEVICE_FLAG_NOTIFIED) { + device = fu_history_get_device_by_id(self->history, device_id, error); + if (device == NULL) + return FALSE; + fu_device_remove_flag(device, flag); + return fu_history_modify_device(self->history, device, error); + } + if (flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { + device = fu_device_list_get_by_id(self->device_list, device_id, error); + if (device == NULL) + return FALSE; + proxy = fu_device_get_proxy(device); + if (proxy != NULL) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device %s uses a proxy, remove the flag on %s instead", + fu_device_get_id(device), + fu_device_get_id(proxy)); + return FALSE; + } + g_hash_table_remove(self->emulation_backend_ids, fu_device_get_backend_id(device)); + return TRUE; + } + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "flag cannot be removed from client"); + return FALSE; +} + +static gboolean +fu_engine_add_device_flag(FuEngine *self, + const gchar *device_id, + FwupdDeviceFlags flag, + GError **error) +{ + FuDevice *proxy; + g_autoptr(FuDevice) device = NULL; + + if (flag == FWUPD_DEVICE_FLAG_REPORTED || flag == FWUPD_DEVICE_FLAG_NOTIFIED) { + device = fu_history_get_device_by_id(self->history, device_id, error); + if (device == NULL) + return FALSE; + fu_device_add_flag(device, flag); + return fu_history_modify_device(self->history, device, error); + } + if (flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { + device = fu_device_list_get_by_id(self->device_list, device_id, error); + if (device == NULL) + return FALSE; + proxy = fu_device_get_proxy(device); + if (proxy != NULL) { + g_set_error(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "device %s uses a proxy, set the flag on %s instead", + fu_device_get_id(device), + fu_device_get_id(proxy)); + return FALSE; + } + g_hash_table_insert(self->emulation_backend_ids, + g_strdup(fu_device_get_backend_id(device)), + GUINT_TO_POINTER(1)); + return TRUE; + } + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "flag cannot be added from client"); + return FALSE; +} + +static gboolean +fu_engine_modify_device_flags(FuEngine *self, + const gchar *device_id, + const gchar *value, + GError **error) +{ + /* add or remove a subset of device flags */ + if (g_str_has_prefix(value, "~")) { + return fu_engine_remove_device_flag(self, + device_id, + fwupd_device_flag_from_string(value + 1), + error); + } + return fu_engine_add_device_flag(self, + device_id, + fwupd_device_flag_from_string(value), + error); +} + /** * fu_engine_modify_device: * @self: a #FuEngine @@ -1076,45 +1193,8 @@ fu_engine_modify_device(FuEngine *self, const gchar *value, GError **error) { - g_autoptr(FuDevice) device = NULL; - - /* find the correct device */ - device = fu_history_get_device_by_id(self->history, device_id, error); - if (device == NULL) - return FALSE; - - /* support adding and removing a subset of device flags */ - if (g_strcmp0(key, "Flags") == 0) { - if (g_str_has_prefix(value, "~")) { - FwupdDeviceFlags flag = fwupd_device_flag_from_string(value + 1); - if (flag == FWUPD_DEVICE_FLAG_NOTIFIED) { - fu_device_remove_flag(device, flag); - } else { - g_set_error(error, - FWUPD_ERROR, - FWUPD_ERROR_NOT_SUPPORTED, - "flag %s cannot be unset from client", - key); - return FALSE; - } - } else { - FwupdDeviceFlags flag = fwupd_device_flag_from_string(value); - if (flag == FWUPD_DEVICE_FLAG_REPORTED || - flag == FWUPD_DEVICE_FLAG_NOTIFIED) { - fu_device_add_flag(device, flag); - } else { - g_set_error(error, - FWUPD_ERROR, - FWUPD_ERROR_NOT_SUPPORTED, - "flag %s cannot be set from client", - key); - return FALSE; - } - } - return fu_history_modify_device(self->history, device, error); - } - - /* others invalid */ + if (g_strcmp0(key, "Flags") == 0) + return fu_engine_modify_device_flags(self, device_id, value, error); g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "key %s not supported", key); return FALSE; } @@ -3032,6 +3112,9 @@ fu_engine_install_release(FuEngine *self, fu_engine_wait_for_acquiesce(self, fu_device_get_acquiesce_delay(device_orig)); } + /* allow capturing setup again */ + fu_engine_set_install_phase(self, FU_ENGINE_INSTALL_PHASE_SETUP); + /* make the UI update */ fu_engine_emit_changed(self); @@ -3055,13 +3138,220 @@ fu_engine_get_plugins(FuEngine *self) return fu_plugin_list_get_all(self->plugin_list); } +static gboolean +fu_engine_emulation_load_json(FuEngine *self, const gchar *json, GError **error) +{ + JsonNode *root; + g_autoptr(JsonParser) parser = json_parser_new(); + + /* parse */ + if (!json_parser_load_from_data(parser, json, -1, error)) + return FALSE; + root = json_parser_get_root(parser); + + /* load into all backends */ + for (guint i = 0; i < self->backends->len; i++) { + FuBackend *backend = g_ptr_array_index(self->backends, i); + if (!fu_backend_load(backend, + json_node_get_object(root), + FU_USB_DEVICE_EMULATION_TAG, + FU_BACKEND_LOAD_FLAG_NONE, + error)) + return FALSE; + } + + /* success */ + return TRUE; +} + +static gboolean +fu_engine_emulation_load_phase(FuEngine *self, FuEngineInstallPhase phase, GError **error) +{ + const gchar *json = g_hash_table_lookup(self->emulation_phases, GINT_TO_POINTER(phase)); + if (json == NULL) + return TRUE; + if (g_getenv("FWUPD_BACKEND_VERBOSE") != NULL) + g_debug("loading phase %s: %s", fu_engine_install_phase_to_string(phase), json); + return fu_engine_emulation_load_json(self, json, error); +} + +gboolean +fu_engine_emulation_load(FuEngine *self, GBytes *data, GError **error) +{ + gboolean got_json = FALSE; + g_autoptr(FuArchive) archive = NULL; + + g_return_val_if_fail(FU_IS_ENGINE(self), FALSE); + g_return_val_if_fail(data != NULL, FALSE); + g_return_val_if_fail(error == NULL || *error == NULL, FALSE); + + /* not supported */ + if (!fu_config_get_allow_emulation(self->config)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "emulation is not allowed from config"); + return FALSE; + } + + /* unload any existing devices */ + if (!fu_engine_emulation_load_json(self, "{\"UsbDevices\":[]}", error)) + return FALSE; + + /* load archive */ + archive = fu_archive_new(data, FU_ARCHIVE_FLAG_NONE, error); + if (archive == NULL) + return FALSE; + + /* load JSON files from archive */ + g_hash_table_remove_all(self->emulation_phases); + for (guint phase = FU_ENGINE_INSTALL_PHASE_SETUP; phase < FU_ENGINE_INSTALL_PHASE_LAST; + phase++) { + g_autofree gchar *fn = + g_strdup_printf("%s.json", fu_engine_install_phase_to_string(phase)); + g_autofree gchar *json_safe = NULL; + GBytes *blob = fu_archive_lookup_by_fn(archive, fn, NULL); + + /* not found */ + if (blob == NULL) + continue; + json_safe = g_strndup(g_bytes_get_data(blob, NULL), g_bytes_get_size(blob)); + got_json = TRUE; + g_debug("got emulation for phase %s", fu_engine_install_phase_to_string(phase)); + if (phase == FU_ENGINE_INSTALL_PHASE_SETUP) { + if (!fu_engine_emulation_load_json(self, json_safe, error)) + return FALSE; + } else { + g_hash_table_insert(self->emulation_phases, + GINT_TO_POINTER(phase), + g_steal_pointer(&json_safe)); + } + } + if (!got_json) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "no emulation data found in archive"); + return FALSE; + } + + /* success */ + return TRUE; +} + +GBytes * +fu_engine_emulation_save(FuEngine *self, GError **error) +{ + gboolean got_json = FALSE; + g_autoptr(GBytes) bytes = NULL; + g_autoptr(FuArchive) archive = fu_archive_new(NULL, FU_ARCHIVE_FLAG_NONE, NULL); + + g_return_val_if_fail(FU_IS_ENGINE(self), NULL); + g_return_val_if_fail(error == NULL || *error == NULL, NULL); + + /* not supported */ + if (!fu_config_get_allow_emulation(self->config)) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "emulation is not allowed from config"); + return NULL; + } + + /* sanity check */ + for (guint phase = FU_ENGINE_INSTALL_PHASE_SETUP; phase < FU_ENGINE_INSTALL_PHASE_LAST; + phase++) { + const gchar *json = + g_hash_table_lookup(self->emulation_phases, GINT_TO_POINTER(phase)); + g_autofree gchar *fn = + g_strdup_printf("%s.json", fu_engine_install_phase_to_string(phase)); + g_autoptr(GBytes) blob = NULL; + + /* nothing set */ + if (json == NULL) + continue; + got_json = TRUE; + blob = g_bytes_new_static(json, strlen(json)); + fu_archive_add_entry(archive, fn, blob); + } + if (!got_json) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_NOT_SUPPORTED, + "no emulation data, perhaps no devices have been added?"); + return NULL; + } + + /* write */ + bytes = + fu_archive_write(archive, FU_ARCHIVE_FORMAT_ZIP, FU_ARCHIVE_COMPRESSION_GZIP, error); + if (bytes == NULL) + return NULL; + + /* success */ + g_hash_table_remove_all(self->emulation_phases); + return g_steal_pointer(&bytes); +} + +static gboolean +fu_engine_backends_save_phase(FuEngine *self, GError **error) +{ + const gchar *data_old; + g_autofree gchar *data_new = NULL; + g_autoptr(JsonBuilder) json_builder = json_builder_new(); + g_autoptr(JsonGenerator) json_generator = NULL; + g_autoptr(JsonNode) json_root = NULL; + + /* all devices in all backends */ + for (guint i = 0; i < self->backends->len; i++) { + FuBackend *backend = g_ptr_array_index(self->backends, i); + if (!fu_backend_save(backend, + json_builder, + FU_USB_DEVICE_EMULATION_TAG, + FU_BACKEND_SAVE_FLAG_NONE, + error)) + return FALSE; + } + json_root = json_builder_get_root(json_builder); + json_generator = json_generator_new(); + json_generator_set_pretty(json_generator, TRUE); + json_generator_set_root(json_generator, json_root); + + data_old = + g_hash_table_lookup(self->emulation_phases, GINT_TO_POINTER(self->install_phase)); + data_new = json_generator_to_data(json_generator, NULL); + if (g_strcmp0(data_new, "") == 0) { + g_debug("no data for phase %s", + fu_engine_install_phase_to_string(self->install_phase)); + return TRUE; + } + if (g_strcmp0(data_old, data_new) == 0) { + g_debug("JSON unchanged for phase %s", + fu_engine_install_phase_to_string(self->install_phase)); + return TRUE; + } + if (g_getenv("FWUPD_BACKEND_VERBOSE") != NULL) { + g_autofree gchar *data_new_safe = data_new_safe = g_strndup(data_new, 8000); + g_debug("JSON %s for phase %s: %s...", + data_old == NULL ? "added" : "changed", + fu_engine_install_phase_to_string(self->install_phase), + data_new_safe); + } + g_hash_table_insert(self->emulation_phases, + GINT_TO_POINTER(self->install_phase), + g_steal_pointer(&data_new)); + + /* success */ + return TRUE; +} + /** * fu_engine_get_device: * @self: a #FuEngine * @device_id: a device ID * @error: (nullable): optional return location for an error * - * Gets a specific device. + * Gets a specific device, optionally loading an emulated phase. * * Returns: (transfer full): a device, or %NULL if not found **/ @@ -3070,6 +3360,17 @@ fu_engine_get_device(FuEngine *self, const gchar *device_id, GError **error) { g_autoptr(FuDevice) device = NULL; + /* we are emulating a device */ + if (self->install_phase != FU_ENGINE_INSTALL_PHASE_SETUP) { + g_autoptr(FuDevice) device_old = NULL; + device_old = fu_device_list_get_by_id(self->device_list, device_id, NULL); + if (device_old != NULL && + fu_device_has_flag(device_old, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_emulation_load_phase(self, self->install_phase, error)) + return NULL; + } + } + /* wait for any device to disconnect and reconnect */ if (!fu_device_list_wait_for_replug(self->device_list, error)) { g_prefix_error(error, "failed to wait for detach replug: "); @@ -3214,6 +3515,12 @@ fu_engine_prepare(FuEngine *self, return FALSE; } + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } return TRUE; } @@ -3245,6 +3552,12 @@ fu_engine_cleanup(FuEngine *self, return FALSE; } + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } return TRUE; } @@ -3301,6 +3614,13 @@ fu_engine_detach(FuEngine *self, return FALSE; } + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } + /* success */ return TRUE; } @@ -3333,6 +3653,14 @@ fu_engine_attach(FuEngine *self, const gchar *device_id, FuProgress *progress, G if (!fu_plugin_runner_attach(plugin, device, progress, error)) return FALSE; + + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } + return TRUE; } @@ -3412,6 +3740,15 @@ fu_engine_reload(FuEngine *self, const gchar *device_id, GError **error) g_prefix_error(error, "failed to reload device: "); return FALSE; } + + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } + + /* success */ return TRUE; } @@ -3457,11 +3794,13 @@ fu_engine_write_firmware(FuEngine *self, g_autoptr(GError) error_cleanup = NULL; /* attack back into runtime then cleanup */ + fu_engine_set_install_phase(self, FU_ENGINE_INSTALL_PHASE_ATTACH); fu_progress_reset(progress); if (!fu_plugin_runner_attach(plugin, device, progress, &error_attach)) { g_warning("failed to attach device after failed update: %s", error_attach->message); } + fu_engine_set_install_phase(self, FU_ENGINE_INSTALL_PHASE_CLEANUP); fu_progress_reset(progress); if (!fu_engine_cleanup(self, device_id, progress, flags, &error_cleanup)) { g_warning("failed to update-cleanup after failed update: %s", @@ -3498,6 +3837,15 @@ fu_engine_write_firmware(FuEngine *self, } } } + + /* save to emulated phase */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + if (!fu_engine_backends_save_phase(self, error)) + return FALSE; + } + + /* success */ return TRUE; } @@ -3659,7 +4007,6 @@ fu_engine_install_blob(FuEngine *self, fu_progress_step_done(progress_local); /* the device and plugin both may have changed */ - fu_engine_set_install_phase(self, FU_ENGINE_INSTALL_PHASE_UNKNOWN); device_tmp = fu_engine_get_device(self, device_id, error); if (device_tmp == NULL) { g_prefix_error(error, "failed to get device after install blob: "); @@ -6353,6 +6700,25 @@ fu_engine_device_inherit_history(FuEngine *self, FuDevice *device) } } +static void +fu_engine_ensure_device_emulation_tag(FuEngine *self, FuDevice *device) +{ + /* already done */ + if (fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG)) + return; + + /* we matched this physical ID */ + if (fu_device_get_backend_id(device) == NULL) + return; + if (!g_hash_table_contains(self->emulation_backend_ids, fu_device_get_backend_id(device))) + return; + + /* success */ + g_debug("adding emulation-tag to %s", fu_device_get_backend_id(device)); + fu_device_add_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG); + fu_engine_check_context_flag_save_events(self); +} + void fu_engine_add_device(FuEngine *self, FuDevice *device) { @@ -6432,6 +6798,9 @@ fu_engine_add_device(FuEngine *self, FuDevice *device) } } + /* check if the device needs emulation-tag */ + fu_engine_ensure_device_emulation_tag(self, device); + /* set or clear the SUPPORTED flag */ fu_engine_ensure_device_supported(self, device); @@ -6482,6 +6851,16 @@ fu_engine_add_device(FuEngine *self, FuDevice *device) if (component != NULL) fu_engine_md_refresh_device_from_component(self, device, component); + /* save to emulated phase, but avoid overwriting reload */ + if (fu_context_has_flag(self->ctx, FU_CONTEXT_FLAG_SAVE_EVENTS) && + self->install_phase == FU_ENGINE_INSTALL_PHASE_SETUP && + fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATION_TAG) && + !fu_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED)) { + g_autoptr(GError) error_local = NULL; + if (!fu_engine_backends_save_phase(self, &error_local)) + g_warning("failed to save phase: %s", error_local->message); + } + fu_engine_emit_changed(self); } @@ -7438,6 +7817,9 @@ fu_engine_backend_device_added(FuEngine *self, FuDevice *device, FuProgress *pro } fu_progress_step_done(progress); + /* check if the device needs emulation-tag */ + fu_engine_ensure_device_emulation_tag(self, device); + /* super useful for plugin development */ if (g_getenv("FWUPD_PROBE_VERBOSE") != NULL) { g_autofree gchar *str = fu_device_to_string(FU_DEVICE(device)); @@ -8438,22 +8820,6 @@ fu_engine_idle_status_notify_cb(FuIdle *idle, GParamSpec *pspec, FuEngine *self) fu_engine_set_status(self, status); } -gboolean -fu_engine_backends_save(FuEngine *self, JsonBuilder *json_builder, GError **error) -{ - json_builder_begin_object(json_builder); - json_builder_set_member_name(json_builder, "Backends"); - json_builder_begin_array(json_builder); - for (guint i = 0; i < self->backends->len; i++) { - FuBackend *backend = g_ptr_array_index(self->backends, i); - if (!fu_backend_save(backend, json_builder, NULL, FU_BACKEND_SAVE_FLAG_NONE, error)) - return FALSE; - } - json_builder_end_array(json_builder); - json_builder_end_object(json_builder); - return TRUE; -} - static void fu_engine_init(FuEngine *self) { @@ -8479,6 +8845,8 @@ fu_engine_init(FuEngine *self) self->runtime_versions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->compile_versions = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); self->acquiesce_loop = g_main_loop_new(NULL, FALSE); + self->emulation_phases = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); + self->emulation_backend_ids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); fu_context_set_runtime_versions(self->ctx, self->runtime_versions); fu_context_set_compile_versions(self->ctx, self->compile_versions); @@ -8639,6 +9007,8 @@ fu_engine_finalize(GObject *obj) g_ptr_array_unref(self->local_monitors); g_hash_table_unref(self->runtime_versions); g_hash_table_unref(self->compile_versions); + g_hash_table_unref(self->emulation_phases); + g_hash_table_unref(self->emulation_backend_ids); g_object_unref(self->plugin_list); G_OBJECT_CLASS(fu_engine_parent_class)->finalize(obj); diff --git a/src/fu-engine.h b/src/fu-engine.h index abd0b979e..e90f0ac2b 100644 --- a/src/fu-engine.h +++ b/src/fu-engine.h @@ -250,4 +250,6 @@ fu_engine_modify_bios_settings(FuEngine *self, gboolean force_ro, GError **error); gboolean -fu_engine_backends_save(FuEngine *self, JsonBuilder *json_builder, GError **error); +fu_engine_emulation_load(FuEngine *self, GBytes *data, GError **error); +GBytes * +fu_engine_emulation_save(FuEngine *self, GError **error); diff --git a/src/fu-self-test.c b/src/fu-self-test.c index ac4400079..94b20051c 100644 --- a/src/fu-self-test.c +++ b/src/fu-self-test.c @@ -3243,7 +3243,7 @@ fu_backend_usb_func(gconstpointer user_data) g_autoptr(GPtrArray) devices = NULL; g_autoptr(GPtrArray) possible_plugins = NULL; -#if !G_USB_CHECK_VERSION(0, 4, 4) +#if !G_USB_CHECK_VERSION(0, 4, 5) g_test_skip("GUsb version too old"); return; #endif @@ -3332,7 +3332,7 @@ fu_backend_usb_invalid_func(gconstpointer user_data) g_autoptr(GPtrArray) devices = NULL; g_autoptr(JsonParser) parser = json_parser_new(); -#if !G_USB_CHECK_VERSION(0, 4, 2) +#if !G_USB_CHECK_VERSION(0, 4, 5) g_test_skip("GUsb version too old"); return; #endif diff --git a/src/fu-tool.c b/src/fu-tool.c index 721201222..da145d5cd 100644 --- a/src/fu-tool.c +++ b/src/fu-tool.c @@ -3441,34 +3441,6 @@ fu_util_setup_interactive(FuUtilPrivate *priv, GError **error) return fu_util_setup_interactive_console(error); } -static gboolean -fu_util_backends_save(FuUtilPrivate *priv, const gchar *fn, GError **error) -{ - g_autofree gchar *data = NULL; - g_autoptr(JsonBuilder) json_builder = json_builder_new(); - g_autoptr(JsonGenerator) json_generator = NULL; - g_autoptr(JsonNode) json_root = NULL; - - /* export as a string */ - if (!fu_engine_backends_save(priv->engine, json_builder, error)) - return FALSE; - json_root = json_builder_get_root(json_builder); - json_generator = json_generator_new(); - json_generator_set_pretty(json_generator, TRUE); - json_generator_set_root(json_generator, json_root); - data = json_generator_to_data(json_generator, NULL); - if (data == NULL) { - g_set_error_literal(error, - G_IO_ERROR, - G_IO_ERROR_INVALID_DATA, - "Failed to convert to JSON string"); - return FALSE; - } - - /* save to file */ - return g_file_set_contents(fn, data, -1, error); -} - static void fu_util_print_error(FuUtilPrivate *priv, const GError *error) { @@ -3498,7 +3470,6 @@ main(int argc, char *argv[]) g_autoptr(GPtrArray) cmd_array = fu_util_cmd_array_new(); g_autofree gchar *cmd_descriptions = NULL; g_autofree gchar *filter = NULL; - g_autofree gchar *save_backends_fn = NULL; const GOptionEntry options[] = { {"version", '\0', @@ -3653,15 +3624,6 @@ main(int argc, char *argv[]) N_("Filter with a set of device flags using a ~ prefix to " "exclude, e.g. 'internal,~needs-reboot'"), NULL}, - {"save-backends", - '\0', - 0, - G_OPTION_ARG_STRING, - &save_backends_fn, - /* TRANSLATORS: command line option */ - N_("Specify a filename to use to save backend events"), - /* TRANSLATORS: filename argument with path */ - N_("FILENAME")}, {"json", '\0', 0, @@ -4103,10 +4065,6 @@ main(int argc, char *argv[]) /* load engine */ priv->engine = fu_engine_new(); - if (save_backends_fn != NULL) { - fu_context_add_flag(fu_engine_get_context(priv->engine), - FU_CONTEXT_FLAG_SAVE_EVENTS); - } g_signal_connect(FU_ENGINE(priv->engine), "device-request", G_CALLBACK(fu_util_update_device_request_cb), @@ -4165,12 +4123,6 @@ main(int argc, char *argv[]) return EXIT_FAILURE; } - /* dump devices */ - if (save_backends_fn != NULL && !fu_util_backends_save(priv, save_backends_fn, &error)) { - g_printerr("%s\n", error->message); - return EXIT_FAILURE; - } - /* a good place to do the traceback */ if (fu_progress_get_profile(priv->progress)) { g_autofree gchar *str = fu_progress_traceback(priv->progress); diff --git a/src/fu-usb-backend.c b/src/fu-usb-backend.c index 2a6a42f29..39bfb1e08 100644 --- a/src/fu-usb-backend.c +++ b/src/fu-usb-backend.c @@ -50,52 +50,18 @@ fu_usb_backend_device_notify_flags_cb(FuDevice *device, GParamSpec *pspec, FuBac static void fu_usb_backend_device_added_cb(GUsbContext *ctx, GUsbDevice *usb_device, FuBackend *backend) { - FuDevice *device_tmp; - g_autoptr(FuUsbDevice) device = NULL; - - /* is emulated? */ - device_tmp = fu_backend_lookup_by_id(backend, g_usb_device_get_platform_id(usb_device)); - if (device_tmp != NULL && fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_EMULATED)) { - GUsbDevice *usb_device_tmp = fu_usb_device_get_dev(FU_USB_DEVICE(device_tmp)); -#if G_USB_CHECK_VERSION(0, 4, 5) - if (g_date_time_equal(g_usb_device_get_created(usb_device), - g_usb_device_get_created(usb_device_tmp))) { -#else - if (g_usb_device_get_vid(usb_device) == g_usb_device_get_vid(usb_device_tmp) && - g_usb_device_get_pid(usb_device) == g_usb_device_get_pid(usb_device_tmp)) { -#endif - g_debug("replacing GUsbDevice of emulated device %s", - fu_usb_device_get_platform_id(FU_USB_DEVICE(device_tmp))); - fu_usb_device_set_dev(FU_USB_DEVICE(device_tmp), usb_device); - fu_backend_device_changed(backend, device_tmp); - return; - } - g_debug("delayed removal as emulated device changed"); - fu_backend_device_removed(backend, device_tmp); - } - - /* success */ - device = fu_usb_device_new(fu_backend_get_context(backend), usb_device); + g_autoptr(FuUsbDevice) device = + fu_usb_device_new(fu_backend_get_context(backend), usb_device); fu_backend_device_added(backend, FU_DEVICE(device)); } static void fu_usb_backend_device_removed_cb(GUsbContext *ctx, GUsbDevice *usb_device, FuBackend *backend) { - FuUsbBackend *self = FU_USB_BACKEND(backend); - FuDevice *device_tmp; - - /* find the device we enumerated */ - device_tmp = - fu_backend_lookup_by_id(FU_BACKEND(self), g_usb_device_get_platform_id(usb_device)); - if (device_tmp != NULL) { - if (fu_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_EMULATED)) { - g_debug("ignoring removal of emulated device %s", - fu_device_get_id(device_tmp)); - return; - } - fu_backend_device_removed(backend, device_tmp); - } + FuDevice *device = + fu_backend_lookup_by_id(backend, g_usb_device_get_platform_id(usb_device)); + if (device != NULL) + fu_backend_device_removed(backend, device); } static void @@ -107,13 +73,11 @@ fu_usb_backend_context_finalized_cb(gpointer data, GObject *where_the_object_was static void fu_usb_backend_context_flags_check(FuUsbBackend *self) { -#if G_USB_CHECK_VERSION(0, 4, 1) +#if G_USB_CHECK_VERSION(0, 4, 5) FuContext *ctx = fu_backend_get_context(FU_BACKEND(self)); GUsbContextFlags usb_flags = G_USB_CONTEXT_FLAGS_NONE; -#if G_USB_CHECK_VERSION(0, 4, 4) if (g_getenv("FWUPD_BACKEND_VERBOSE") != NULL) usb_flags |= G_USB_CONTEXT_FLAGS_DEBUG; -#endif if (fu_context_has_flag(ctx, FU_CONTEXT_FLAG_SAVE_EVENTS)) { g_debug("saving FuUsbBackend events"); usb_flags |= G_USB_CONTEXT_FLAGS_SAVE_EVENTS; @@ -213,7 +177,7 @@ fu_usb_backend_load(FuBackend *backend, FuBackendLoadFlags flags, GError **error) { -#if G_USB_CHECK_VERSION(0, 4, 1) +#if G_USB_CHECK_VERSION(0, 4, 5) FuUsbBackend *self = FU_USB_BACKEND(backend); return g_usb_context_load_with_tag(self->usb_ctx, json_object, tag, error); #else @@ -232,9 +196,30 @@ fu_usb_backend_save(FuBackend *backend, FuBackendSaveFlags flags, GError **error) { -#if G_USB_CHECK_VERSION(0, 4, 1) +#if G_USB_CHECK_VERSION(0, 4, 5) FuUsbBackend *self = FU_USB_BACKEND(backend); - return g_usb_context_save_with_tag(self->usb_ctx, json_builder, tag, error); + guint usb_events_cnt = 0; + g_autoptr(GPtrArray) devices = g_usb_context_get_devices(self->usb_ctx); + + for (guint i = 0; i < devices->len; i++) { + GUsbDevice *usb_device = g_ptr_array_index(devices, i); + g_autoptr(GPtrArray) usb_events = g_usb_device_get_events(usb_device); + if (usb_events->len > 0 || g_usb_device_has_tag(usb_device, tag)) { + g_debug("%u USB events to save for %s", + usb_events->len, + g_usb_device_get_platform_id(usb_device)); + } + usb_events_cnt += usb_events->len; + } + if (usb_events_cnt == 0) + return TRUE; + if (!g_usb_context_save_with_tag(self->usb_ctx, json_builder, tag, error)) + return FALSE; + for (guint i = 0; i < devices->len; i++) { + GUsbDevice *usb_device = g_ptr_array_index(devices, i); + g_usb_device_clear_events(usb_device); + } + return TRUE; #else g_set_error_literal(error, G_IO_ERROR, diff --git a/src/fu-util-common.c b/src/fu-util-common.c index 106bf0ab0..0cdcd68ab 100644 --- a/src/fu-util-common.c +++ b/src/fu-util-common.c @@ -1316,6 +1316,10 @@ fu_util_device_flag_to_string(guint64 device_flag) /* TRANSLATORS: this device is not actually real */ return _("Emulated"); } + if (device_flag == FWUPD_DEVICE_FLAG_EMULATION_TAG) { + /* TRANSLATORS: we're saving all USB events for emulation */ + return _("Tagged for emulation"); + } if (device_flag == FWUPD_DEVICE_FLAG_SKIPS_RESTART) { /* skip */ return NULL; diff --git a/src/fu-util.c b/src/fu-util.c index 65c09bcbe..06d36ce9a 100644 --- a/src/fu-util.c +++ b/src/fu-util.c @@ -689,6 +689,7 @@ typedef struct { guint nr_missing; JsonBuilder *builder; const gchar *name; + gboolean use_emulation; } FuUtilDeviceTestHelper; static gboolean @@ -814,12 +815,45 @@ fu_util_device_test_step(FuUtilPrivate *priv, { JsonArray *json_array; const gchar *url; + const gchar *emulation_url = NULL; const gchar *baseuri = g_getenv("FWUPD_DEVICE_TESTS_BASE_URI"); g_autofree gchar *filename = NULL; g_autofree gchar *url_safe = NULL; g_autoptr(GBytes) fw = NULL; g_autoptr(GError) error_local = NULL; + /* send this data to the daemon */ + if (helper->use_emulation) { + g_autofree gchar *emulation_filename = NULL; + g_autofree gchar *emulation_safe = NULL; + g_autoptr(GBytes) emulation_data = NULL; + + /* just ignore anything without emulation data */ + if (!json_object_has_member(json_obj, "emulation-url")) + return TRUE; + + emulation_url = json_object_get_string_member(json_obj, "emulation-url"); + if (baseuri != NULL) { + g_autofree gchar *basename = g_path_get_basename(emulation_url); + emulation_safe = g_build_filename(baseuri, basename, NULL); + } else { + emulation_safe = g_strdup(emulation_url); + } + emulation_filename = fu_util_download_if_required(priv, emulation_safe, error); + if (emulation_filename == NULL) { + g_prefix_error(error, "failed to download %s: ", emulation_safe); + return FALSE; + } + emulation_data = fu_bytes_get_contents(emulation_filename, error); + if (emulation_data == NULL) + return FALSE; + if (!fwupd_client_emulation_load(priv->client, + emulation_data, + priv->cancellable, + error)) + return FALSE; + } + /* download file if required */ if (!json_object_has_member(json_obj, "url")) { g_set_error_literal(error, @@ -1028,13 +1062,13 @@ fu_util_quit(FuUtilPrivate *priv, gchar **values, GError **error) } static gboolean -fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) +fu_util_device_test_full(FuUtilPrivate *priv, + gchar **values, + FuUtilDeviceTestHelper *helper, + GError **error) { g_autoptr(JsonBuilder) builder = json_builder_new(); - FuUtilDeviceTestHelper helper = {.nr_failed = 0, - .nr_success = 0, - .builder = builder, - .name = "Unknown"}; + helper->builder = builder; /* required for interactive devices */ priv->current_operation = FU_UTIL_OPERATION_UPDATE; @@ -1056,7 +1090,7 @@ fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) json_builder_begin_array(builder); for (guint i = 0; values[i] != NULL; i++) { json_builder_begin_object(builder); - if (!fu_util_device_test_filename(priv, &helper, values[i], error)) + if (!fu_util_device_test_filename(priv, helper, values[i], error)) return FALSE; json_builder_end_object(builder); } @@ -1070,23 +1104,23 @@ fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) } /* we need all to pass for a zero return code */ - if (helper.nr_failed > 0) { + if (helper->nr_failed > 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "Some of the tests failed"); return FALSE; } - if (helper.nr_missing > 0) { + if (helper->nr_missing > 0) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "%u devices required for %u tests were not found", - helper.nr_missing, + helper->nr_missing, g_strv_length(values)); return FALSE; } - if (helper.nr_success == 0) { + if (helper->nr_success == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, @@ -1102,6 +1136,20 @@ fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) return TRUE; } +static gboolean +fu_util_device_emulate(FuUtilPrivate *priv, gchar **values, GError **error) +{ + FuUtilDeviceTestHelper helper = {.use_emulation = TRUE}; + return fu_util_device_test_full(priv, values, &helper, error); +} + +static gboolean +fu_util_device_test(FuUtilPrivate *priv, gchar **values, GError **error) +{ + FuUtilDeviceTestHelper helper = {.use_emulation = FALSE}; + return fu_util_device_test_full(priv, values, &helper, error); +} + static gboolean fu_util_download(FuUtilPrivate *priv, gchar **values, GError **error) { @@ -4025,6 +4073,89 @@ fu_util_get_bios_setting(FuUtilPrivate *priv, gchar **values, GError **error) return TRUE; } +static gboolean +fu_util_emulation_tag(FuUtilPrivate *priv, gchar **values, GError **error) +{ + g_autoptr(FwupdDevice) dev = NULL; + g_autofree gchar *cmd = g_strdup_printf("%s emulation-save", g_get_prgname()); + + /* set the flag */ + dev = fu_util_get_device_or_prompt(priv, values, error); + if (dev == NULL) + return FALSE; + if (!fwupd_client_modify_device(priv->client, + fwupd_device_get_id(dev), + "Flags", + "emulation-tag", + priv->cancellable, + error)) + return FALSE; + + /* TRANSLATORS: these are instructions on how to generate the data, and the %1 is a command + * like 'fwupdmgr emulation-save' */ + g_print(_("Now unplug and replug the device, install the firmware, and then run %s"), cmd); + g_print("\n"); + return TRUE; +} + +static gboolean +fu_util_emulation_untag(FuUtilPrivate *priv, gchar **values, GError **error) +{ + g_autoptr(FwupdDevice) dev = NULL; + + /* set the flag */ + priv->filter_include |= FWUPD_DEVICE_FLAG_EMULATION_TAG; + dev = fu_util_get_device_or_prompt(priv, values, error); + if (dev == NULL) + return FALSE; + return fwupd_client_modify_device(priv->client, + fwupd_device_get_id(dev), + "Flags", + "~emulation-tag", + priv->cancellable, + error); +} + +static gboolean +fu_util_emulation_save(FuUtilPrivate *priv, gchar **values, GError **error) +{ + g_autoptr(GBytes) data = NULL; + + /* check args */ + if (g_strv_length(values) != 1) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_ARGS, + "Invalid arguments, expected FILENAME"); + return FALSE; + } + + /* save */ + data = fwupd_client_emulation_save(priv->client, priv->cancellable, error); + if (data == NULL) + return FALSE; + return fu_bytes_set_contents(values[0], data, error); +} + +static gboolean +fu_util_emulation_load(FuUtilPrivate *priv, gchar **values, GError **error) +{ + g_autoptr(GBytes) data = NULL; + + /* check args */ + if (g_strv_length(values) != 1) { + g_set_error_literal(error, + FWUPD_ERROR, + FWUPD_ERROR_INVALID_ARGS, + "Invalid arguments, expected FILENAME"); + return FALSE; + } + data = fu_bytes_get_contents(values[0], error); + if (data == NULL) + return FALSE; + return fwupd_client_emulation_load(priv->client, data, priv->cancellable, error); +} + static gboolean fu_util_version(FuUtilPrivate *priv, GError **error) { @@ -4555,6 +4686,13 @@ main(int argc, char *argv[]) /* TRANSLATORS: command description */ _("Test a device using a JSON manifest"), fu_util_device_test); + fu_util_cmd_array_add(cmd_array, + "device-emulate", + /* TRANSLATORS: command argument: uppercase, spaces->dashes */ + _("[FILENAME1] [FILENAME2]"), + /* TRANSLATORS: command description */ + _("Emulate a device using a JSON manifest"), + fu_util_device_emulate); fu_util_cmd_array_add(cmd_array, "inhibit", /* TRANSLATORS: command argument: uppercase, spaces->dashes */ @@ -4590,6 +4728,34 @@ main(int argc, char *argv[]) /* TRANSLATORS: command description */ _("Sets one or more BIOS settings"), fu_util_set_bios_setting); + fu_util_cmd_array_add(cmd_array, + "emulation-load", + /* TRANSLATORS: command argument: uppercase, spaces->dashes */ + _("FILENAME"), + /* TRANSLATORS: command description */ + _("Load device emulation data"), + fu_util_emulation_load); + fu_util_cmd_array_add(cmd_array, + "emulation-save", + /* TRANSLATORS: command argument: uppercase, spaces->dashes */ + _("FILENAME"), + /* TRANSLATORS: command description */ + _("Save device emulation data"), + fu_util_emulation_save); + fu_util_cmd_array_add(cmd_array, + "emulation-tag", + /* TRANSLATORS: command argument: uppercase, spaces->dashes */ + _("[DEVICE-ID|GUID]"), + /* TRANSLATORS: command description */ + _("Adds devices to watch for future emulation"), + fu_util_emulation_tag); + fu_util_cmd_array_add(cmd_array, + "emulation-untag", + /* TRANSLATORS: command argument: uppercase, spaces->dashes */ + _("[DEVICE-ID|GUID]"), + /* TRANSLATORS: command description */ + _("Removes devices to watch for future emulation"), + fu_util_emulation_untag); /* do stuff on ctrl+c */ priv->cancellable = g_cancellable_new(); diff --git a/src/org.freedesktop.fwupd.xml b/src/org.freedesktop.fwupd.xml index f9008260e..33aced03a 100644 --- a/src/org.freedesktop.fwupd.xml +++ b/src/org.freedesktop.fwupd.xml @@ -957,6 +957,48 @@ + + + + + + Load emulation data. + + + + + + + + JSON data of each phase packaged as a ZIP archive + (e.g. setup.json, install.json, reload.json). + + + + + + + + + + + + Return emulation data. + + + + + + + + JSON data of each phase packaged as a compressed ZIP archive + (e.g. setup.json, install.json, reload.json). + + + + + + diff --git a/subprojects/gusb.wrap b/subprojects/gusb.wrap index 10558da8c..4b2d38aa7 100644 --- a/subprojects/gusb.wrap +++ b/subprojects/gusb.wrap @@ -1,4 +1,4 @@ [wrap-git] directory = gusb url = https://github.com/hughsie/libgusb.git -revision = 0.4.0 +revision = 0.4.5