Automated hardware testing improvements
PATCH 7/8 scripts/contrib/dialog-power-control: add a trivial power prompt script
In reply to Paul EggletonIf you want to do automated hardware testing but don't have a controllable power strip this script can be useful so that you know when you need to cycle the power.
--- scripts/contrib/dialog-power-control | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 scripts/contrib/dialog-power-control
scripts/contrib/dialog-power-control new file +53 -0
@@ -0,0 +1,53 @@ +#!/bin/sh +# +# Simple script to show a manual power prompt for when you want to use +# automated hardware testing with testimage.bbclass but you don't have a +# web-enabled power strip or similar to do the power on/off/cycle. +# +# You can enable it by enabling testimage (see the Yocto Project +# Development manual "Performing Automated Runtime Testing" section) +# and setting the following in your local.conf: +# +# TEST_POWERCONTROL_CMD = "${COREBASE}/scripts/contrib/dialog-power-control" +# + +PROMPT="" +while true; do + case $1 in + on) + PROMPT="Please turn device power on";; + off) + PROMPT="Please turn device power off";; + cycle) + PROMPT="Please click Done, then turn the device power off then on";; + "") + break;; + esac + shift +done + +if [ "$PROMPT" = "" ] ; then + echo "ERROR: no power action specified on command line" + exit 2 +fi + +if [ "`which kdialog 2>/dev/null`" != "" ] ; then + DIALOGUTIL="kdialog" +elif [ "`which zenity 2>/dev/null`" != "" ] ; then + DIALOGUTIL="zenity" +else + echo "ERROR: couldn't find program to display a message, install kdialog or zenity" + exit 3 +fi + +if [ "$DIALOGUTIL" = "kdialog" ] ; then + kdialog --yesno "$PROMPT" --title "TestImage Power Control" --yes-label "Done" --no-label "Cancel test" +elif [ "$DIALOGUTIL" = "zenity" ] ; then + zenity --question --text="$PROMPT" --title="TestImage Power Control" --ok-label="Done" --cancel-label="Cancel test" +fi + +if [ "$?" != "0" ] ; then + echo "User cancelled test at power prompt" + exit 1 +fi +
Signature · 1 line
1.9.0
PATCH 5/8 classes/testimage: if start fails, don't try to stop
In reply to Paul EggletonIf we couldn't start the target, it doesn't make sense to try and stop it here since logically it shouldn't now be in any kind of "started" state. (It's the start function's job to clean up after itself if it fails - to that end, fix up the QemuTarget class so that it does.)
--- meta/classes/testimage.bbclass | 2 +- meta/lib/oeqa/targetcontrol.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-)
meta/classes/testimage.bbclass +1 -1
@@ -209,8 +209,8 @@ def testimage_main(d): target.deploy() + target.start() try: - target.start() if export: exportTests(d,tc) else:
meta/lib/oeqa/targetcontrol.py +1 -0
@@ -124,6 +124,7 @@ class QemuTarget(BaseTarget): self.server_ip = self.runner.server_ip self.connection = SSHControl(ip=self.ip, logfile=self.sshlog) else: + self.stop() raise bb.build.FuncFailed("%s - FAILED to start qemu - check the task log and the boot log" % self.pn) def stop(self):
Signature · 1 line
1.9.0
PATCH 6/8 scripts/contrib/serdevtry: add script to handle transient serial terminals
In reply to Paul EggletonWhen running automated tests (or just generally interacting with) boards whose serial console devices are on the board itself and thus disappear when powered down or practically disconnected, such as the BeagleBone white, some terminal programs (e.g. picocom) will exit when the device disappears and need to be restarted after the serial device returns. This script handles this automatically for such terminal programs.
--- scripts/contrib/serdevtry | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100755 scripts/contrib/serdevtry
scripts/contrib/serdevtry new file +60 -0
@@ -0,0 +1,60 @@ +#!/bin/sh + +# Copyright (C) 2014 Intel Corporation +# +# Released under the MIT license (see COPYING.MIT) + +if [ "$1" = "" -o "$1" = "--help" ] ; then + echo "Usage: $0 <serial terminal command>" + echo + echo "Simple script to handle maintaining a terminal for serial devices that" + echo "disappear when a device is powered down or reset, such as the USB" + echo "serial console on the original BeagleBone (white version)." + echo + echo "e.g. $0 picocom -b 115200 /dev/ttyUSB0" + echo + exit +fi + +args="$@" +DEVICE="" +while [ "$1" != "" ]; do + case "$1" in + /dev/*) + DEVICE=$1 + break;; + esac + shift +done + +if [ "$DEVICE" != "" ] ; then + while true; do + if [ ! -e $DEVICE ] ; then + echo "serdevtry: waiting for $DEVICE to exist..." + while [ ! -e $DEVICE ]; do + sleep 0.1 + done + fi + if [ ! -w $DEVICE ] ; then + # Sometimes (presumably because of a race with udev) we get to + # the device before its permissions have been set up + RETRYNUM=0 + while [ ! -w $DEVICE ]; do + if [ "$RETRYNUM" = "2" ] ; then + echo "Device $DEVICE exists but is not writable!" + exit 1 + fi + RETRYNUM=$((RETRYNUM+1)) + sleep 0.1 + done + fi + $args + if [ -e $DEVICE ] ; then + break + fi + done +else + echo "Unable to determine device node from command: $args" + exit 1 +fi +
Signature · 1 line
1.9.0
PATCH 8/8 oeqa: add proper handling for command errors where needed
In reply to Paul EggletonFor use outside of tests themselves, we want a better error than AssertionError, so create one and allow us to request it when calling runCmd(). This enables us to avoid tracebacks during master image operations if the power control command fails.
--- meta/lib/oeqa/controllers/masterimage.py | 6 +++++- meta/lib/oeqa/utils/__init__.py | 12 ++++++++++++ meta/lib/oeqa/utils/commands.py | 10 +++++++--- 3 files changed, 24 insertions(+), 4 deletions(-)
meta/lib/oeqa/controllers/masterimage.py +5 -1
@@ -20,6 +20,7 @@ import subprocess import oeqa.targetcontrol import oeqa.utils.sshcontrol as sshcontrol import oeqa.utils.commands as commands +from oeqa.utils import CommandError from abc import ABCMeta, abstractmethod
@@ -94,7 +95,10 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): def power_ctl(self, msg): if self.powercontrol_cmd: cmd = "%s %s" % (self.powercontrol_cmd, msg) - commands.runCmd(cmd, preexec_fn=os.setsid, env=self.origenv) + try: + commands.runCmd(cmd, assert_error=False, preexec_fn=os.setsid, env=self.origenv) + except CommandError as e: + bb.fatal(str(e)) def power_cycle(self, conn): if self.powercontrol_cmd:
meta/lib/oeqa/utils/__init__.py +12 -0
@@ -1,3 +1,15 @@ # Enable other layers to have modules in the same named directory from pkgutil import extend_path __path__ = extend_path(__path__, __name__) + + +# Borrowed from CalledProcessError + +class CommandError(Exception): + def __init__(self, retcode, cmd, output = None): + self.retcode = retcode + self.cmd = cmd + self.output = output + def __str__(self): + return "Command '%s' returned non-zero exit status %d with output: %s" % (self.cmd, self.retcode, self.output) +
meta/lib/oeqa/utils/commands.py +7 -3
@@ -1,4 +1,4 @@ -# Copyright (c) 2013 Intel Corporation +# Copyright (c) 2013-2014 Intel Corporation # # Released under the MIT license (see COPYING.MIT)
@@ -14,6 +14,7 @@ import signal import subprocess import threading import logging +from oeqa.utils import CommandError class Command(object): def __init__(self, command, bg=False, timeout=None, data=None, **options):
@@ -84,8 +85,8 @@ class Command(object): class Result(object): pass -def runCmd(command, ignore_status=False, timeout=None, **options): +def runCmd(command, ignore_status=False, timeout=None, assert_error=True, **options): result = Result() cmd = Command(command, timeout=timeout, **options)
@@ -97,7 +98,10 @@ def runCmd(command, ignore_status=False, timeout=None, **options): result.pid = cmd.process.pid if result.status and not ignore_status: - raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output)) + if assert_error: + raise AssertionError("Command '%s' returned non-zero exit status %d:\n%s" % (command, result.status, result.output)) + else: + raise CommandError(result.status, command, result.output) return result
Signature · 1 line
1.9.0
PATCH 4/8 oeqa/controllers/masterimage: more robust master image startup
In reply to Paul EggletonInstead of powering up the target when the object is constructed, wait until deploy is called. Then there are basically two different scenarios:
a) The device is booted into the master image already, in which case we can just use it
b) The device is booted into another image or can't be contacted, in which case we need to power cycle it. Here we also now wait until it has booted up instead of trying to contact it immediately.
--- meta/lib/oeqa/controllers/masterimage.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-)
meta/lib/oeqa/controllers/masterimage.py +13 -2
@@ -87,7 +87,6 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): if self.powercontrol_cmd: if self.powercontrol_args: self.powercontrol_cmd = "%s %s" % (self.powercontrol_cmd, self.powercontrol_args) - self.power_ctl("on") if self.serialcontrol_cmd: if self.serialcontrol_args: self.serialcontrol_cmd = "%s %s" % (self.serialcontrol_cmd, self.serialcontrol_args)
@@ -108,13 +107,25 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): if status != 0: bb.error("Failed rebooting target and no power control command defined. You need to manually reset the device.\n%s" % output) + def _wait_until_booted(self): + ''' Waits until the target device has booted (if we have just power cycled it) ''' + # Subclasses with better methods of determining boot can override this + time.sleep(120) + def deploy(self): - bb.plain("%s - deploying image on target" % self.pn) # base class just sets the ssh log file for us super(MasterImageHardwareTarget, self).deploy() self.master = sshcontrol.SSHControl(ip=self.ip, logfile=self.sshlog, timeout=600, port=self.port) status, output = self.master.run("cat /etc/masterimage") if status != 0: + # We're not booted into the master image, so try rebooting + bb.plain("%s - booting into the master image" % self.pn) + self.power_ctl("cycle") + self._wait_until_booted() + + bb.plain("%s - deploying image on target" % self.pn) + status, output = self.master.run("cat /etc/masterimage") + if status != 0: bb.fatal("No ssh connectivity or target isn't running a master image.\n%s" % output) if self.user_cmds: self.deploy_cmds = self.user_cmds.split("\n")
Signature · 1 line
1.9.0
PATCH 3/8 oeqa/controllers/masterimage: add a serial control command
In reply to Paul EggletonFrom: Stefan Stanacar <[email protected]>
Similar to power control command, this depends on a user's setup to get to the serial port of a board. For a local connected board this could just be:
TEST_SERIALCONTROL_CMD = "picocom /dev/ttyUSB0 -b 115200"
and for a serial console server: "telnet 10.11.12.13 7003" or some conmux command.
--- meta/lib/oeqa/controllers/masterimage.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-)
meta/lib/oeqa/controllers/masterimage.py +12 -4
@@ -70,10 +70,12 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): # the command should take as the last argument "off" and "on" and "cycle" (off, on) self.powercontrol_cmd = d.getVar("TEST_POWERCONTROL_CMD", True) or None self.powercontrol_args = d.getVar("TEST_POWERCONTROL_EXTRA_ARGS") or "" + + self.serialcontrol_cmd = d.getVar("TEST_SERIALCONTROL_CMD", True) or None + self.serialcontrol_args = d.getVar("TEST_SERIALCONTROL_EXTRA_ARGS") or "" + self.origenv = os.environ - if self.powercontrol_cmd: - if self.powercontrol_args: - self.powercontrol_cmd = "%s %s" % (self.powercontrol_cmd, self.powercontrol_args) + if self.powercontrol_cmd or self.serialcontrol_cmd: # the external script for controlling power might use ssh # ssh + keys means we need the original user env bborigenv = d.getVar("BB_ORIGENV", False) or {}
@@ -81,7 +83,14 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): val = bborigenv.getVar(key, True) if val is not None: self.origenv[key] = str(val) + + if self.powercontrol_cmd: + if self.powercontrol_args: + self.powercontrol_cmd = "%s %s" % (self.powercontrol_cmd, self.powercontrol_args) self.power_ctl("on") + if self.serialcontrol_cmd: + if self.serialcontrol_args: + self.serialcontrol_cmd = "%s %s" % (self.serialcontrol_cmd, self.serialcontrol_args) def power_ctl(self, msg): if self.powercontrol_cmd:
@@ -172,4 +181,3 @@ class GummibootTarget(MasterImageHardwareTarget): self.power_cycle(self.master) # there are better ways than a timeout but this should work for now time.sleep(120) -
Signature · 1 line
1.9.0
PATCH 2/8 oeqa/targetcontrol: restart method shouldn't be abstract
In reply to Paul EggletonFrom: Stefan Stanacar <[email protected]>
And drop the un-needed and un-used restart methods. Only qemu ever used this and actually does it safely.
--- meta/lib/oeqa/controllers/masterimage.py | 3 --- meta/lib/oeqa/targetcontrol.py | 7 ++----- 2 files changed, 2 insertions(+), 8 deletions(-)
meta/lib/oeqa/controllers/masterimage.py +0 -3
@@ -133,9 +133,6 @@ class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): bb.plain("%s - reboot/powercycle target" % self.pn) self.power_cycle(self.connection) - def restart(self): - pass - class GummibootTarget(MasterImageHardwareTarget):
meta/lib/oeqa/targetcontrol.py +2 -5
@@ -70,9 +70,9 @@ class BaseTarget(object): def stop(self): pass - @abstractmethod def restart(self, params=None): - pass + self.stop() + self.start(params) def run(self, cmd, timeout=None): return self.connection.run(cmd, timeout)
@@ -170,6 +170,3 @@ class SimpleRemoteTarget(BaseTarget): self.connection = None self.ip = None self.server_ip = None - - def restart(self, params=None): - pass
Signature · 1 line
1.9.0
PATCH 1/8 oeqa/controllers/masterimage: add a base class for hw targets
In reply to Paul EggletonFrom: Stefan Stanacar <[email protected]>
Right now GummibootTarget is the only hardware TEST_TARGET with deployment, but we will add more, so let's make an abstract base class, that will do the common thing for all the hw targets.
--- meta/lib/oeqa/controllers/masterimage.py | 137 ++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 46 deletions(-)
meta/lib/oeqa/controllers/masterimage.py +91 -46
@@ -1,17 +1,50 @@ +# Copyright (C) 2014 Intel Corporation +# +# Released under the MIT license (see COPYING.MIT) + +# This module adds support to testimage.bbclass to deploy images and run +# tests using a "master image" - this is a "known good" image that is +# installed onto the device as part of initial setup and will be booted into +# with no interaction; we can then use it to deploy the image to be tested +# to a second partition before running the tests. +# +# For an example master image, see core-image-testmaster +# (meta/recipes-extended/images/core-image-testmaster.bb) + import os import bb import traceback import time +import subprocess import oeqa.targetcontrol import oeqa.utils.sshcontrol as sshcontrol import oeqa.utils.commands as commands -class GummibootTarget(oeqa.targetcontrol.SimpleRemoteTarget): +from abc import ABCMeta, abstractmethod + +class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget): + + __metaclass__ = ABCMeta def __init__(self, d): - # let our base class do the ip thing - super(GummibootTarget, self).__init__(d) + super(MasterImageHardwareTarget, self).__init__(d) + + # target ip + addr = d.getVar("TEST_TARGET_IP", True) or bb.fatal('Please set TEST_TARGET_IP with the IP address of the machine you want to run the tests on.') + self.ip = addr.split(":")[0] + try: + self.port = addr.split(":")[1] + except IndexError: + self.port = None + bb.note("Target IP: %s" % self.ip) + self.server_ip = d.getVar("TEST_SERVER_IP", True) + if not self.server_ip: + try: + self.server_ip = subprocess.check_output(['ip', 'route', 'get', self.ip ]).split("\n")[0].split()[-1] + except Exception as e: + bb.fatal("Failed to determine the host IP address (alternatively you can set TEST_SERVER_IP with the IP address of this machine): %s" % e) + bb.note("Server IP: %s" % self.server_ip) # test rootfs + kernel self.rootfs = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("IMAGE_LINK_NAME", True) + '.tar.gz')
@@ -26,36 +59,11 @@ class GummibootTarget(oeqa.targetcontrol.SimpleRemoteTarget): if not os.path.isfile(self.kernel): bb.fatal("No kernel found. Expected path: %s" % self.kernel) - # if the user knows what he's doing, then by all means... - # test-rootfs.tar.gz and test-kernel are hardcoded names in other places - # they really have to be used like that in commands though - cmds = d.getVar("TEST_DEPLOY_CMDS", True) - - # this the value we need to set in the LoaderEntryOneShot EFI variable - # so the system boots the 'test' bootloader label and not the default - # The first four bytes are EFI bits, and the rest is an utf-16le string - # (EFI vars values need to be utf-16) - # $ echo -en "test\0" | iconv -f ascii -t utf-16le | hexdump -C - # 00000000 74 00 65 00 73 00 74 00 00 00 |t.e.s.t...| - self.efivarvalue = r'\x07\x00\x00\x00\x74\x00\x65\x00\x73\x00\x74\x00\x00\x00' - - if cmds: - self.deploy_cmds = cmds.split("\n") - else: - self.deploy_cmds = [ - 'mount -L boot /boot', - 'mkdir -p /mnt/testrootfs', - 'mount -L testrootfs /mnt/testrootfs', - 'modprobe efivarfs', - 'mount -t efivarfs efivarfs /sys/firmware/efi/efivars', - 'cp ~/test-kernel /boot', - 'rm -rf /mnt/testrootfs/*', - 'tar xzvf ~/test-rootfs.tar.gz -C /mnt/testrootfs', - 'printf "%s" > /sys/firmware/efi/efivars/LoaderEntryOneShot-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f' % self.efivarvalue - ] - # master ssh connection self.master = None + # if the user knows what they are doing, then by all means... + self.user_cmds = d.getVar("TEST_DEPLOY_CMDS", True) + self.deploy_cmds = None # this is the name of the command that controls the power for a board # e.g: TEST_POWERCONTROL_CMD = "/home/user/myscripts/powercontrol.py ${MACHINE} what-ever-other-args-the-script-wants"
@@ -94,22 +102,67 @@ class GummibootTarget(oeqa.targetcontrol.SimpleRemoteTarget): def deploy(self): bb.plain("%s - deploying image on target" % self.pn) # base class just sets the ssh log file for us - super(GummibootTarget, self).deploy() + super(MasterImageHardwareTarget, self).deploy() self.master = sshcontrol.SSHControl(ip=self.ip, logfile=self.sshlog, timeout=600, port=self.port) + status, output = self.master.run("cat /etc/masterimage") + if status != 0: + bb.fatal("No ssh connectivity or target isn't running a master image.\n%s" % output) + if self.user_cmds: + self.deploy_cmds = self.user_cmds.split("\n") try: self._deploy() except Exception as e: bb.fatal("Failed deploying test image: %s" % e) + @abstractmethod def _deploy(self): - # make sure we are in the right image - status, output = self.master.run("cat /etc/masterimage") - if status != 0: - raise Exception("No ssh connectivity or target isn't running a master image.\n%s" % output) + pass + + def start(self, params=None): + bb.plain("%s - boot test image on target" % self.pn) + self._start() + # set the ssh object for the target/test image + self.connection = sshcontrol.SSHControl(self.ip, logfile=self.sshlog, port=self.port) + bb.plain("%s - start running tests" % self.pn) + + @abstractmethod + def _start(self): + pass + def stop(self): + bb.plain("%s - reboot/powercycle target" % self.pn) + self.power_cycle(self.connection) + + def restart(self): + pass + + +class GummibootTarget(MasterImageHardwareTarget): + + def __init__(self, d): + super(GummibootTarget, self).__init__(d) + # this the value we need to set in the LoaderEntryOneShot EFI variable + # so the system boots the 'test' bootloader label and not the default + # The first four bytes are EFI bits, and the rest is an utf-16le string + # (EFI vars values need to be utf-16) + # $ echo -en "test\0" | iconv -f ascii -t utf-16le | hexdump -C + # 00000000 74 00 65 00 73 00 74 00 00 00 |t.e.s.t...| + self.efivarvalue = r'\x07\x00\x00\x00\x74\x00\x65\x00\x73\x00\x74\x00\x00\x00' + self.deploy_cmds = [ + 'mount -L boot /boot', + 'mkdir -p /mnt/testrootfs', + 'mount -L testrootfs /mnt/testrootfs', + 'modprobe efivarfs', + 'mount -t efivarfs efivarfs /sys/firmware/efi/efivars', + 'cp ~/test-kernel /boot', + 'rm -rf /mnt/testrootfs/*', + 'tar xzvf ~/test-rootfs.tar.gz -C /mnt/testrootfs', + 'printf "%s" > /sys/firmware/efi/efivars/LoaderEntryOneShot-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f' % self.efivarvalue + ] + + def _deploy(self): # make sure these aren't mounted self.master.run("umount /boot; umount /mnt/testrootfs; umount /sys/firmware/efi/efivars;") - # from now on, every deploy cmd should return 0 # else an exception will be thrown by sshcontrol self.master.ignore_status = False
@@ -118,16 +171,8 @@ class GummibootTarget(oeqa.targetcontrol.SimpleRemoteTarget): for cmd in self.deploy_cmds: self.master.run(cmd) - - def start(self, params=None): - bb.plain("%s - boot test image on target" % self.pn) + def _start(self, params=None): self.power_cycle(self.master) # there are better ways than a timeout but this should work for now time.sleep(120) - # set the ssh object for the target/test image - self.connection = sshcontrol.SSHControl(self.ip, logfile=self.sshlog, port=self.port) - bb.plain("%s - start running tests" % self.pn) - def stop(self): - bb.plain("%s - reboot/powercycle target" % self.pn) - self.power_cycle(self.connection)
Signature · 1 line
1.9.0
cover Automated hardware testing improvements
These add functionality required to deploy images and run tests on a wider range of real hardware, as well as improving robustness in a couple of areas.
A related patchset to add testing for two of the Yocto Project reference platforms (beaglebone and edgerouter) will be sent for meta-yocto-bsp.
The following changes since commit 1e1b42f687b5cd34623fe2682218958e1947eb92:
feature-arm-thumb.inc: set ARMPKGSFX_THUMB only when thumb is in TUNE_FEATURES (2014-04-29 23:35:53 +0100)
are available in the git repository at:
git://git.openembedded.org/openembedded-core-contrib paule/testimage-hw
http://cgit.openembedded.org/cgit.cgi/openembedded-core-contrib/log/?h=paule/testimage-hw
Paul Eggleton (5):
oeqa/controllers/masterimage: more robust master image startup classes/testimage: if start fails, don't try to stop scripts/contrib/serdevtry: add script to handle transient serial
terminals
scripts/contrib/dialog-power-control: add a trivial power prompt
script
oeqa: add proper handling for command errors where needed
Stefan Stanacar (3):
oeqa/controllers/masterimage: add a base class for hw targets oeqa/targetcontrol: restart method shouldn't be abstract oeqa/controllers/masterimage: add a serial control command
meta/classes/testimage.bbclass | 2 +- meta/lib/oeqa/controllers/masterimage.py | 171 +++++++++++++++++++++---------- meta/lib/oeqa/targetcontrol.py | 8 +- meta/lib/oeqa/utils/__init__.py | 12 +++ meta/lib/oeqa/utils/commands.py | 10 +- scripts/contrib/dialog-power-control | 53 ++++++++++ scripts/contrib/serdevtry | 60 +++++++++++ 7 files changed, 254 insertions(+), 62 deletions(-) create mode 100755 scripts/contrib/dialog-power-control create mode 100755 scripts/contrib/serdevtry
Signature · 1 line
1.9.0