1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-09-22 11:51:53 +01:00

273 Commits

Author SHA1 Message Date
setrofim
bb7591e8fa Version bump. 2017-05-04 17:39:53 +01:00
setrofim
783669371d Merge pull request #120 from bjackman/speed-up-ssh-execute
ssh: Combine exit code and main command
2017-05-04 16:59:22 +01:00
Brendan Jackman
0a20cec2d9 ssh: Combine exit code and main command
This avoids a network round trip and speeds up SSH targets.
2017-05-04 16:47:37 +01:00
setrofim
d72049d35b Merge pull request #119 from bjackman/monsoon
instrument: Add support for Monsoon Power Monitor
2017-05-04 13:09:57 +01:00
Brendan Jackman
3cfbad19bd instrument: Add support for Monsoon Power Monitor
This adds support for the Monsoon Power Monitor:

https://www.msoon.com/LabEquipment/PowerMonitor/

This device officialy supports only MS Windows so we use a tool from AOSP to
communicate with it from Unix targets:

https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py

The user is required to install this tool and its dependencies, then pass the
path to it as the monsoon_bin parameter.
2017-05-04 11:32:20 +01:00
setrofim
4522fe8d23 Merge pull request #117 from bjackman/adb-push-error
android: Raise better error when trying to push non-existent file
2017-05-02 08:43:24 +01:00
Brendan Jackman
e17b9c33d1 android: Raise better error when trying to push non-existent file 2017-04-28 11:32:23 +01:00
Sergei Trofimov
d968098717 Add build/ and dist/ to .gitingore
These directories contain artifacts generated by setuptools.
2017-04-26 17:19:55 +01:00
setrofim
b5ecf63638 Merge pull request #116 from bjackman/instrument-sample-rate
Add sample_rate_hz attribute to CONTINUOUS instruments
2017-04-26 16:22:33 +01:00
Brendan Jackman
0d61ee5951 daq: Rename sampling_rate->sample_rate_hz
For consistency with the new Instrument.sample_rate_hz attribute.
2017-04-26 16:19:07 +01:00
Brendan Jackman
fc81477cf8 doc/instrumentation: Fix whitespace 2017-04-26 16:00:54 +01:00
Brendan Jackman
49b547a7f6 instrument: Add sample_rate_hz attribute for CONTINUOUS instruments 2017-04-26 16:00:54 +01:00
setrofim
f7d1b0fb13 Merge pull request #115 from bjackman/fix-get-channels
instrument: Fix get_channels method
2017-04-25 16:01:26 +01:00
Brendan Jackman
1bc29d7abf instrument: Fix get_channels method
- `Instrument.channels` is a dictionary, we want the values not the
   keys; this is provded by `list_channels`.

- `InstrumentChannel` objects do not have a `measure` attribute. Use
  `kind` instead.
2017-04-25 15:58:51 +01:00
setrofim
013fc59a41 Merge pull request #113 from marcbonnici/pull
Target: Add read permissions to tmp files before pulling
2017-04-24 14:37:51 +01:00
Marc Bonnici
f6d02c6611 Target: Add read permissions to tmp files before pulling
Previously trying to pull a file from an android target would fail if the file
was owned by root, this commit adds read permissions to the file before
attempting to pull.
2017-04-21 15:27:56 +01:00
setrofim
98cf7d00c7 Merge pull request #112 from bjackman/speed-up-perturb
shutils: Speed up cpuidle_wake_all_cpus
2017-04-21 11:44:25 +01:00
Sergei Trofimov
bd27de194c SshConnection: do not try to sudo as root
If a command is executed "as_root", SshConnection always prepended
"sudo" invocation to the command, even if the user name is "root". This
causes problems on patforms that do not have "sudo".

This commit changes this behavior so that sudo is never used for root
users.
2017-04-20 17:12:01 +01:00
Brendan Jackman
e276abfcb4 shutils: Speed up cpuidle_wake_all_cpus
This can currently take several hundred milliseconds on slow targets with many
CPUs. To speed it up, use '&' to spawn the perturbations in paralell.
2017-04-20 15:43:55 +01:00
setrofim
1ab8c25ff9 Merge pull request #110 from marcbonnici/kick_off
Target: Stopped `kick_off` raising an error if command didn't timeout
2017-04-20 15:30:26 +01:00
Marc Bonnici
95102d324b Target: Stopped kick_off raising an error if command didn't timeout
On some devices backgrounding a task results in the command returning
immediately with no error. This was falsely interpreted as the command failing
to run, therefore the additional check has been removed.
2017-04-20 09:04:08 +01:00
setrofim
d27c8e3362 Merge pull request #109 from bjackman/remove-pandas
energy_probe: Remove unused pandas try-import
2017-04-19 13:11:49 +01:00
Brendan Jackman
f0f1847c60 energy_probe: Remove unused pandas try-import
Looks like this is commit-leakage.
2017-04-19 10:36:57 +01:00
setrofim
b112ed424c Merge pull request #108 from bjackman/fix-ps-android
AndroidTarget: Fix `ps` when NAME column contains spaces
2017-04-12 16:51:33 +01:00
Brendan Jackman
55c27e2c54 AndroidTarget: Fix ps when NAME column contains spaces
Targets have been observed where `ps` output contains entries with NAME columns
of the form "[foo bar]". This means the `parts` list is too long and the PsEntry
call reports too many arguments. Since NAME is the rightmost column, just fix
the number of entries we recognise to 8.
2017-04-12 16:33:48 +01:00
setrofim
f4d3c60137 Merge pull request #107 from bjackman/cpufreq-expose-tunable-error
cpufreq: Expose error when writing invalid governor tunable
2017-04-12 15:46:58 +01:00
Brendan Jackman
5c036ea669 cpufreq: Expose error when writing invalid governor tunable
If we get an TargetError when trying to set a governor tunable we currently fall
back to an older sysfs layout under the assumption that the file we tried to
write doesn't exist. However it may be that the file exists, but we tried to
write an invalid value (or something else went wrong). In this case we fall back
to the old file location, fail, and produce a nonsense error about the old path
not existing.

Instead, when we get a TargetError, check if the file exists, and fall back to
the old location only if it does not.
2017-04-12 15:41:44 +01:00
setrofim
391e95cc75 Merge pull request #106 from bjackman/cpufreq-fix-docstring
cpufreq: Fix docstring for `cpu` parameter
2017-04-10 13:06:31 +01:00
Brendan Jackman
a6fb5b57ae cpufreq: Fix docstring for cpu parameter
`cpu` can be an `int`, _or_ the full CPU name used by sysfs.
2017-04-10 11:51:50 +01:00
setrofim
ae1bfccbe2 Merge pull request #105 from marcbonnici/docs
Docs: Removed duplicate 'Platform' entry.
2017-04-06 16:39:34 +01:00
Marc Bonnici
b131dc1e13 Docs: Removed duplicate 'Platform' entry. 2017-04-06 16:12:54 +01:00
setrofim
1061c94951 Merge pull request #103 from marcbonnici/issue_102
Target: Fixes __setup_list_directory issue on production devices.
2017-03-30 08:06:41 +01:00
Marc Bonnici
0655237217 Target: Fixes __setup_list_directory issue on production devices.
As per issue #102, not all devices have permission to list the root directory
causing the ls command to be incorrectly determined. This commit changes the
directory to be checked to the working directory to ensure sufficient
permissions.
2017-03-29 17:18:49 +01:00
setrofim
119c259e73 Merge pull request #101 from bjackman/energy-probe-get-caiman-error
energy_probe: Check if caimain was broken and raise an error
2017-03-24 15:15:38 +00:00
Brendan Jackman
fdc0c0477d energy_probe: Check if caimain was broken and raise an error
This means when caiman is broken you can see the error message, instead of just
finding out later that there is no data.

We still don't detect caiman being broken until we call stop(), but it's still
an improvement.
2017-03-24 13:35:00 +00:00
setrofim
8a9e0a4819 Merge pull request #99 from marcbonnici/fix_98
cpufreq: Fixed set_x_for cpus
2017-03-17 16:02:31 +00:00
Marc Bonnici
b444ae65c9 cpufreq: Fixed set_x_for cpus
Fixes issue #98 where the passed `cpus` parameter was ignored when setting the
governor and frequency and set for all cpus instead of those specified in the
passed parameter.
Also fixes kwargs not being correctly passed for governor tunables.
2017-03-17 15:53:24 +00:00
Basil Eljuse
dfc63a1cc0 readenergy: New flag to specify execution duration
- '-d' flag allow user to specify duration in sec
	- default is 0, which means run till user termination
	  signal is received
2017-03-16 08:07:11 +00:00
Sergei Trofimov
8300344f70 readenergy: fixing the Makefile
CFLAGS in the makefile were being specified incorrectly.

- the single quotes for forcing all options to be passed as a single
  token by the shell.
- Specifying -static and -lc via -Wl is not needed.
2017-03-15 12:58:50 +00:00
setrofim
32a975be74 Merge pull request #97 from marcbonnici/invalidate_as_root
Target: Invalidates _connected_as_root on reboot or reset.
2017-03-02 15:44:44 +00:00
setrofim
a068fb9b5b Merge pull request #96 from bjackman/devlib-error-name
DevlibError: Fix project name in docstring
2017-03-02 14:04:05 +00:00
setrofim
96ff1aa205 Merge pull request #95 from bjackman/android-setup-error
android: Improve error when _setup_ls fails
2017-03-02 12:03:57 +00:00
Brendan Jackman
3298205b42 android: Improve error when _setup_ls fails
If the ADB command fails (e.g. if you provide the wrong device ID), adb_command
raises a CalledProcessError. CalledProcessError doesn't print the output of the
failed command, so you get a useless error message.

This is relevent here in particular as _setup_ls is the first thing to run an
ADB command if a non-IP device ID is provided.

Catch the CalledProcessError and instead raise a HostError with the command
output in the exception message.
2017-03-01 18:56:05 +00:00
Brendan Jackman
1fa6f92064 DevlibError: Fix project name in docstring 2017-03-01 18:54:54 +00:00
setrofim
6410318b49 Merge pull request #94 from marcbonnici/typos
Modules Documentation: Fixed typos
2017-02-28 18:57:43 +00:00
Marc Bonnici
8733b9cb58 Modules Documentation: Fixed typos 2017-02-28 17:40:44 +00:00
Marc Bonnici
0687dac23b Target: Invalidates _connected_as_root on reboot or reset.
From #92, when rebooting a platform the internal connection state becomes
different from `_connected_as_root`, now clears the state upon device reboot or
reset.
2017-02-28 13:48:10 +00:00
Sergei Trofimov
08e36bf782 gem5: fix for the previous fix
Somehow went from plural to singular when passing base params. Bad
Sergei.
2017-02-27 17:24:01 +00:00
Sergei Trofimov
d40e70d7f4 gem5: updating plaform with base params
Gem5SimulationPlatfrom was not acceping base Platform's parameters, such
as core_names, making it impossible to configure them on construction.
This is rectified in this commit.
2017-02-27 17:17:31 +00:00
Sergei Trofimov
fef7c16b42 utils/misc: allow modules in walk_modules input
Previsously, the import path passed to walk_modules must have been
pointing to a package. This update allows the path to point to a single
module.
2017-02-27 17:12:20 +00:00
setrofim
05215e7e1b Merge pull request #92 from ionela-voinescu/force_adb_root
android: add support for forced adb root
2017-02-24 14:43:23 +00:00
Ionela Voinescu
66eaf15cdc android: add support for forced adb root
When rebooting a platform the internal connection state becomes
different from _connected_as_root, which was set before reboot.

Add the possibility to force adb root if you've rebooted or know
to have done something that would result in the platform having
a different connection state than the one of _connected_as_root.

Signed-off-by: Ionela Voinescu <ionela.voinescu@arm.com>
2017-02-24 14:19:25 +00:00
Sergei Trofimov
1dd6950177 utils/misc: attach exc_info on error in walk_modules
Commit 28891a82 made it easier to debug issues during walk_modules by
attaching the specific name of the module being loaded to the exception.
However, the error may not originite in the imported module, but one of
the modules that module itself is importing.

To further facilitate debugging, exc_info (which contains the traceback)
for the orginal ImportError is now also attached to the raised
HostError.
2017-02-23 09:25:23 +00:00
setrofim
23087d14f5 Merge pull request #91 from bjackman/kernelversion-fixup
Fixup KernelVersion API
2017-02-21 08:05:03 +00:00
Brendan Jackman
6665693e8f target: Fix kernel version regex for -rc field
This regex currently matches the `rc` group against the added-patches-count,
e.g. "00005" in "4.4.41-00005-g17b74bafccbe-dirty" and "59" in
"4.4.0-59-generic". Fix this by requiring "rc" in the match.
2017-02-20 18:32:12 +00:00
Brendan Jackman
18b77b8808 target: Add parts field to KernelVersion 2017-02-20 18:32:12 +00:00
Brendan Jackman
54adf80eab target: Add KernelVersion docstring 2017-02-20 18:11:52 +00:00
Brendan Jackman
03561ee72c target: Make KernelVersion fields numerical 2017-02-20 18:11:52 +00:00
setrofim
e968901fe6 Merge pull request #90 from derkling/improve-kversion-parsing
target: better parse kernel version numbers
2017-02-20 16:08:47 +00:00
Patrick Bellasi
9a8d539e03 target: better parse kernel version numbers
This adds a regexp to better parse all the kernel version numbers as well
as additional (optional) fields like SHA1 and RC.

The regexp has been made generic enough to match these examples:

4.9.0-rc6-00202-g3a60597
4.9.0-rc6-00202
4.9.0-rc6-00202-g3a60597 #321 SMP PREEMPT Mon Feb 13 12:30:59 GMT 2017 aarch64 GNU/Linux
3.18.31-g226dafe #1 SMP PREEMPT Wed Feb 15 16:34:14 GMT 2017
4.4.0-59-generic #80~14.04.1-Ubuntu SMP Fri Jan 6 18:02:02 UTC 2017
3.11.0-26-generic #45~precise1-Ubuntu SMP Tue Jul 15 04:02:35 UTC 2014
3.13.0-107-generic #154-Ubuntu SMP Tue Dec 20 09:57:27 UTC 2016
3.13.0-generic #154-Ubuntu SMP Tue Dec 20 09:57:27 UTC 2016
4.8.0 #1 SMP Fri Jan 6 18:06:24 GMT 2017

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-02-20 12:25:08 +00:00
setrofim
b7ac9e7edc Merge pull request #89 from derkling/android-support-updates
Android support updates
2017-02-16 15:21:08 +00:00
Brendan Jackman
baa32ec716 AndroidTarget: Add charging_enabled property
Disabling battery charging can be useful for measuring power efficiency of
Android devices with hardware modified for this purpose.
2017-02-16 15:09:44 +00:00
Patrick Bellasi
9ce57c0875 AndroidTarget: add support to reboot a device via ADB
This adds a couple of commodity ADB commands to reboot in bootloader
mode and to restart ADB in root mode.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-02-16 15:07:18 +00:00
Patrick Bellasi
da588ea091 android: allow to specify a target for fastboot commands
ADB commands always expect a device ID as first parameter, here
unfortunately we need to pass it as an optional one to avoid breaking
existing clients.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2017-02-16 13:08:31 +00:00
setrofim
9d5b1062dd Merge pull request #88 from bjackman/localconnection-platform-param
LocalConnection: Add ignored `platform` param to __init__ method
2017-02-14 12:05:26 +00:00
Brendan Jackman
680406bc37 LocalConnection: Add ignored platform param to __init__ method
This parameter is used by the other connection classes and as of
21f40035d7 ("gem5: Small tweaks in target to allow gem5 simulations") is
passed by to the connection class constructor via expansion of
Target.connection_settings (see Target.get_connection)
2017-02-14 11:30:32 +00:00
Sergei Trofimov
28891a822b utils/misc: walk_modules always raises HostError
Previously, if an error had occured while attempting to load a module
during walk_modules, it was propagated to the loading code. This
meant that walk_modules could, in theory, raise any exception at all,
and that it was not always possible to tell exactly where the exception
originated.

Now, all encounted errors are re-raised as HostError, which preserves
the original exception in an attribute and sets "modpath" attribute to
point to the module that originated it.
2017-02-08 11:30:33 +00:00
Sergei Trofimov
a9265031ba TimeoutError: moved to devlib.exception
Moved TimeoutError from devlib.util.misc to devlib.exception. It was
previously defined in misc because it needed to be there when the code
was part of WA in order to prevent an import cycle. This is not
necessary in devlib, so it is moved to be with the other exception
definitions.
2017-02-08 11:29:42 +00:00
marcbonnici
8abdfdc1ef Merge pull request #83 from avanlaer/gem5_integration
Gem5 integration
2017-02-08 11:24:44 +00:00
Anouk Van Laer
a02d68decd gem5: Updated the documentation on the use of gem5 in devlib
modified:   doc/connection.rst
	modified:   doc/index.rst
	modified:   doc/platform.rst
2017-02-08 11:16:23 +00:00
Anouk Van Laer
c6b77432ba gem5: Addition of m5 binaries
The m5 binary allows the user to interact with gem5 simulation (e.g. reset stats).
See www.m5sim.org/M5ops for more information

 Changes to be committed:
	new file:   devlib/bin/arm64/m5
	new file:   devlib/bin/armeabi/m5
2017-02-08 11:12:18 +00:00
Anouk Van Laer
21f40035d7 gem5: Small tweaks in target to allow gem5 simulations
The target knows as little as possible about gem5 as a platform
but these tweaks add gem5 as a valid platform.

	modified:   devlib/target.py
2017-02-08 11:12:18 +00:00
Anouk Van Laer
e9cf93e754 gem5: Addition of gem5 simulation platform
This commit adds a gem5 simulation platform. The platform is responsible for starting the gem5 simulation.

 Changes to be committed:
	modified:   devlib/__init__.py
	modified:   devlib/platform/__init__.py
	new file:   devlib/platform/gem5.py
2017-02-08 11:12:17 +00:00
Anouk Van Laer
29a7940731 gem5: Addition of a gem5 connection
This is the first in a series of commits to allow for the use of gem5 as a target in devlib.
The interaction with the gem5 simulation is done by the Gem5Connection, a type of TelnetConnection.
There are two types of Gem5Connections (Linux and Android) as they differ slightly in the implementation
of some methods. Other types of connections have only been changed to make them uniform (e.g. arguments for init method)

 Changes to be committed:
	modified:   devlib/__init__.py
	modified:   devlib/host.py
	modified:   devlib/utils/android.py
	modified:   devlib/utils/ssh.py
2017-02-08 11:12:17 +00:00
setrofim
5472b671ef Merge pull request #87 from bjackman/localconnection-glob
Enhancements for LocalConnection
2017-02-01 08:50:38 +00:00
Brendan Jackman
179e45f98e LocalConnection: Enrich TargetError message from execute method
Add exit code, command, and output to TargetError exception message when execute
fails. This brings LocalConnection into line with AdbConnection and
SshConnection.
2017-01-31 19:21:11 +00:00
Brendan Jackman
b587049eb9 LocalConnection: Support glob patterns in pull 2017-01-31 18:38:03 +00:00
Brendan Jackman
1cb4eb2285 doc/connection: Document the fact that pull supports globs
This feature is supported implicitly by SshConnection and explicitly by
AdbConnection. A subsequent commit implements it for LocalConnection.
2017-01-31 18:38:00 +00:00
Sergei Trofimov
6351a3bad9 Target.killall: catch TargetError on individual kills
killall() is implemented by discovering all PIDs who's name matches the
specified process, and then sending individual kills for each PID. If a
PID no longer exists by the time the kill is sent (e.g. if it was a
child of one of the previously killed PIDs), this will result in a
TargetError, which for the purposes of killall() should be ignored.
2017-01-30 11:14:36 +00:00
Brendan Jackman
adedad8e32 doc/connection: Fix missing word 2017-01-27 19:32:02 +00:00
setrofim
9038339373 Merge pull request #86 from bjackman/adb-exit-code-fix
android: Handle variation in ADB behaviour
2017-01-25 15:58:38 +00:00
Brendan Jackman
44fe0370f8 android: Handle variation in ADB behaviour
On older combinations of ADB/Android versions, the adb host command always
exits with 0 if it was able to run the command on the target, even if the
command failed (https://code.google.com/p/android/issues/detail?id=3254).

When we need the target's exit code (check_exit_code=True), we currently work
around this behaviour by echoing the target's exit code after the command and
parsing it out of the output.

The ADB behaviour is now "fixed" on newer versions with newer Androids (It's not
clear which versions these are and it appears that different builds of ADB with
the same version number differ in this respect). For those version combinations
adb_shell will currently raise a CalledProcessError when check_exit_code=False
and the target command fails.

So lets now use the "echo $?" trick whether or not we need the exit code.

Fixes https://github.com/ARM-software/devlib/issues/85
2017-01-25 15:27:13 +00:00
setrofim
76c4a725ed Merge pull request #84 from bjackman/cgroups-fix-run-into
Some fixes for running commands into cgroups
2017-01-12 17:04:58 +00:00
Brendan Jackman
de61937d09 cgroups: Strip shutils log message from run_into output 2017-01-12 17:03:10 +00:00
Brendan Jackman
d0e28f0a89 cgroups: Fix run_into method
The current run_into method attempts to execute:

  <...>/shutils CGMOUNT=<...> cgroups_run_into <...>

Which should instead be:

  CGMOUNT=<...> <...>/shutils cgroups_run_into <...>

So just use cgroups_run_into_cmd to generate the command, then execute that.
2017-01-12 16:49:24 +00:00
Brendan Jackman
dbd12994fb cgroups: Extend docstrings for run_into[_cmd] 2017-01-12 16:49:24 +00:00
marcbonnici
9f9910bc64 Merge pull request #82 from bjackman/hwmon-root
hwmon: Use as_root=True when listing directories
2017-01-11 10:23:20 +00:00
Brendan Jackman
beaa229279 hwmon: Use as_root=True when listing directories 2017-01-10 14:00:42 +00:00
setrofim
e88c6880ab Merge pull request #81 from bjackman/master
LocalConnection: Add ignored 'timeout' __init__ param
2016-12-14 14:46:12 +00:00
Brendan Jackman
be2775a29a LocalConnection: Add ignored 'timeout' __init__ param
Target::get_connection requires this.
2016-12-14 14:41:04 +00:00
Sergei Trofimov
d5460e1185 android: pick ANDROID_HOME/platform-tools over PATH
When ANDROID_HOME is defined, ANDROID_HOME/platform-tools was appended
to the PATH in the environment of the Python interpreter. That meant
that if an adb binary was in PATH, it would be picked in preference to
the one under ANDROID_HOME. This is now reversed so that ANDROID_HOME
takes precedence.
2016-12-13 11:26:32 +00:00
Sergei Trofimov
1ba7fbdc9a doc: documenting Platforms and Modules
Adding documentation for Platforms and Modules API.
2016-12-09 15:06:35 +00:00
Sergei Trofimov
b3cea0c0d2 TelnetConnction: splitting from SshConnection
Perviously, a parameter passed into SshConnection controlled whether the
connection was established over SSH or Telnet. Now, there is a separate
class for Telnet connections.
2016-12-08 11:50:17 +00:00
Sergei Trofimov
1ed29a8385 JunoEnergyInstrument: removed a stray print
Removed a stray print statement from take_measurement() method.
2016-12-08 09:54:30 +00:00
Sergei Trofimov
8528568c1c gitignore update. 2016-12-07 18:13:55 +00:00
Sergei Trofimov
01253100cd doc: documeting connections 2016-12-07 18:13:55 +00:00
Sergei Trofimov
925fccb4f9 LocalConnection: remove unused timeout parameter
`timeout` parameter passed to LocalConnection gets assigned to an
instance variable, however it is not used for anything.
2016-12-07 18:13:55 +00:00
Sergei Trofimov
889f72c883 Adding connection classes to main __init__.py
Now that connection classes can be passed as arguments on Target
instatiation, they are part of the user-facing API and are marked as
such by exporting them in the main __init__.py
2016-12-07 18:13:55 +00:00
Sergei Trofimov
beaf8d48ac target: switching conn_cls to an __init__ parameter
conn_cls is no longer a class attribute and is specified on
instantiaation as well. This more flexible -- new connection types no
longer require a corresponding new Target subclass but can be used with
existing targets. This is also more consistent with how Platforms are
handled.
2016-12-07 18:13:55 +00:00
Sergei Trofimov
c35230890e AndroidTarget: removing unused parameter
external_storage_directory parmeter of __init__ was not being set is
entrirely used.
2016-12-07 18:13:55 +00:00
setrofim
689c478ca8 Merge pull request #79 from bjackman/cpuidle-additions
Cpuidle additions
2016-12-07 17:46:44 +00:00
Brendan Jackman
c9f7e0e066 cpuidle: Add docstrings for properties requiring units
Unit info from comments on `struct cpuidle_state` definition in
include/linux/cpuidle.h in Linux 4.9 source (see drivers/cpuidle/sysfs.c
for the code that exposes that data to userspace)
2016-12-07 17:44:55 +00:00
Brendan Jackman
7cc8675fa0 cpuidle: Add getter for target residency 2016-12-06 11:46:16 +00:00
setrofim
d1263567d0 Merge pull request #78 from derkling/cgroups-multi-controller
Cgroups multi controller
2016-11-29 13:17:19 +00:00
Sergei Trofimov
5d492ca957 utils/misc: Adding Kryo cores to CPU part map
Adding part IDs of Qualcomm Kryo Gold and Silver cores to the CPU part
map.
2016-11-29 13:11:04 +00:00
Patrick Bellasi
b569a561a4 cgroups: add support for multi-controller hierarchies
Some systems mount multiple CGroup controllers in the same hierarchy, this
happens in desktop systems using systemd but it's also the default for
systems using CGroups v2. Unfortunately the current CGroup modules can
mount only single-controller hierarchies, thus failing when a controller
is already mounted with others in a single hierarchy.

This patch fixes this issue by:
- keeping track of which controllers are mounted in each hierarchy
- muting hierarchies instead of controllers
- creating Controller objects which reference the hierarchy they belong to

Thus, the new constructor of the Controller object requires to specify
the hierarchy ID as well as the list of controller which have to be
mounted in that hierarchy. The hierarchy ID is used to create a single
mount point for all the controllers in that hierarchy. This single mount
points are now named:
  <CGMOUNT>/devlib_cgh<ID>
where "cgh" stands for "CGroup Hierarchy" and <ID> is the specified
numerical identified of that hierarchy.

This patch removes also the Controller::__new__() method, which seems not to
have sense anymore, as well as the Controller::probe() method, which is
not more used. Indeed, all and only the available hierarchies are pre-mounted
at module initialization time.



Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-11-29 10:39:28 +00:00
Patrick Bellasi
103f792736 cgroups: use subsystems namedtuple in the controllers mounting loop
This is just a simple re-factoring patch in preparation of the following
one. Since we have a list of CGroups subsystems, which includes the
hierarchy ID for each entry, let's use directly these namedtuples in the
mouting loop. The following patch will make use of the hierarchy ID to
properly mount each controller.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-11-29 10:13:01 +00:00
Patrick Bellasi
b2ec957bf8 shutils/cgroups: fix run_into support
In the previous patch:
  cgroups: Mount cgroups controllers in devlib working dir
we changed the default mount point for devlib managed CGroups but forgot to
update the support for execution of a workload within a specified CGroup.

The run_into support is provide by a shutil script, which still has hardcoded
the old path (i.e. /sys/fs/cgroup/devlib_*). This patch fixes this by:
- resetting the default path to the Linux standard /sys/fs/cgroup
- use-sing the existing CGMOUNT env variable to specify which CGroups mount
  point to use

The cgroups::run_into is also updated to use the self.cgroup_root via
CGMOUNT when the shutils' script is called.
Moreover, an additional cgroups::run_into_cmd method is added which just
returns a properly formatted run_into shutils' call.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-11-29 10:02:57 +00:00
setrofim
68f7585ac2 Merge pull request #77 from bjackman/cgroups-shutils-fix
shutils/cgroups: Don't fail when racing with process exit
2016-11-28 14:40:42 +00:00
Brendan Jackman
454a2d5db5 shutils/cgroups: Don't fail when racing with process exit
If a process is added to $PIDS but then exits before the
cgroups_task_move invokation finishes, then echoing its PID to
cgroups.procs results in an I/O error. If that process is the last in
$PIDS, then that failing echo command is the last of the function, so
the script exits with an error and devlib raises an exception.

Add `|| true` to avoid this problem.
2016-11-28 12:41:41 +00:00
setrofim
b35a283592 Merge pull request #73 from bjackman/file-exists-root
target: Use root in file_exists check
2016-11-28 12:22:37 +00:00
Brendan Jackman
c1b5152790 target: Use root in file_exists check
Use case: To check if a kernel supports function profiling we need to
check for the presence of
"/sys/kernel/debug/tracing/function_profiler_enabled", but
"/sys/kernel/debug/"'s permissions are 700.
2016-11-28 11:47:44 +00:00
setrofim
78ac92bd84 Merge pull request #66 from mdigiorgio/cpu_freq-trace-event
shutils.in: rename manually injected cpu_frequency trace event
2016-11-28 11:39:00 +00:00
setrofim
7949b93114 Merge pull request #71 from bjackman/master
target: Note return value in Target::invoke docstring
2016-11-28 11:31:58 +00:00
setrofim
bfdfc0e311 Merge pull request #72 from bjackman/adb-pull-whitespace
android: Fix whitespace in wildcard match for ADB pull
2016-11-28 11:30:57 +00:00
setrofim
6eabf7fc56 Merge pull request #76 from bjackman/cgroups-fixes
Fixes for cgroups
2016-11-28 11:27:32 +00:00
Brendan Jackman
ee38a4244a android: Fix whitespace in wildcard match for ADB pull
When there are multiple matches for the wildcard, the output has been
observed to have a space at the end. That means the pull command doesn't
work. This commit fixes that.
2016-11-25 18:29:59 +00:00
Brendan Jackman
0dc65bddb6 shutils/cgroups: Use busybox ps to fix output format
Different `ps` implementations (e.g. Android vs GNU) have different
default columns and process selections.
2016-11-25 18:11:23 +00:00
Brendan Jackman
3dd4ea69b4 cgroups: Remove quotes from grep expressions in move_all_tasks_to
These quotes end up being passed literally into the shutils function
arguments (i.e $3 is '-e', $4 is '"foo"') which means that the grep
command never finds matches. `exclude` is a list of comms, which don't
have spaces in them, so we can just remove the quotes.
2016-11-25 18:11:23 +00:00
Brendan Jackman
e45fcca385 shutils/cgroups: Fix typo 2016-11-25 18:11:23 +00:00
Brendan Jackman
2f35999f37 cgroups: Mount cgroups controllers in devlib working dir
Android seems to have a buggy `mount` command which causes
`mount -o remount` to result in duplicated mounts. We can't unmonunt and
then re-mount /sys/fs/cgroup because there may be pre-existing mounts at
subdirectories. So we create a 'cgroups' directory in the devlib working
directory and mount cgroup controller FS's there.
2016-11-25 18:11:23 +00:00
Brendan Jackman
c89f712923 cgroups: Raise RuntimeError in freeze if no freezer controller
Without this patch, this will result in an error due to trying to call
`.cgroup` on None. Making this a RuntimeError instaed means that a) you
get a more useful error message and b) you can catch the exception
without blanket `except Exception as e`.
2016-11-25 18:10:06 +00:00
Brendan Jackman
27f545f3f6 target: Note return value in Target::invoke docstring 2016-11-15 16:58:57 +00:00
setrofim
290af6619d Merge pull request #70 from derkling/cgroups-fix-mount-rw
cgroups: ensure CGroups are mounted RW
2016-11-10 14:14:24 +00:00
setrofim
02696e99e0 Merge pull request #68 from bjackman/cpuidle-trace-all
ftrace: Poke all CPUs before collecting trace
2016-11-08 17:19:02 +00:00
Brendan Jackman
6cdae6bbe1 ftrace: Poke all CPUs for cpu_idle events before collecting trace
On some systems CPUs sometimes remain idle for several seconds. If a
trace capture begins during one of these long idle periods, that CPU's
idle state is unknown (in practice it is probably in its deepest
available state from cpuidle's perspective but that can't be known for
sure).

The solution to the equivalent problem for cpufreq is to read the
current frequencies from sysfs and inject artificial cpu_frequency
events using trace_marker (see cpu_freq_trace_all_frequencies in
bin/scripts/shutils.in). However we can't read the current idle state
from sysfs.

Instead, wake up each CPU by executing the "true" command on it via
taskset.
2016-11-08 17:00:23 +00:00
setrofim
df9b23aa4f Merge pull request #69 from marcbonnici/master
android: Fixed issue using single quoted command with adb_shell
2016-11-02 15:21:13 +00:00
Marc Bonnici
b59f7c360e android: Fixed issue using single quoted command with adb_shell
When using 'check_exit_code' and 'as_root' options for adb_shell with
a command containing single quotes, the provided command was escaped
twice which has now been avoided.
2016-11-02 14:36:03 +00:00
Patrick Bellasi
da128f917b cgroups: ensure CGroups are mounted RW
Some systems mount CGroups RO, thus we need to ensure we remount that
FS as root otherwise we cannot create new groups.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-10-28 18:54:10 +01:00
Michele Di Giorgio
d7f3092b46 shutils.in: rename manually injected cpu_frequency trace event
The injection of a fake cpu_frequency event in the trace may end up getting
interleaved with trace events generated by the cpufreq governor, for example:

  sh	     ... cpu_frequency: state=120000 cpu_id=0
  kschedfreq ... cpu_frequency: state=120000 cpu_id=0
  kschedfreq ... cpu_frequency: state=120000 cpu_id=1
  kschedfreq ... cpu_frequency: state=120000 cpu_id=2
  kschedfreq ... cpu_frequency: state=120000 cpu_id=3
  sh         ... cpu_frequency: state=120000 cpu_id=1
  sh         ... cpu_frequency: state=120000 cpu_id=2
  sh         ... cpu_frequency: state=120000 cpu_id=3

Such a trace may confuse trace parsers during postprocessing and lead to wrong
or no results.

This patch renames the events injected by devlib so that they can be easily
recognized and then used by the trace parser in the best way.

Signed-off-by: Michele Di Giorgio <michele.digiorgio@arm.com>
2016-10-17 08:35:02 +01:00
setrofim
a89c3fb009 Merge pull request #65 from credp/more_android7_ls
Fix adb pull with wildcard on Android v7+
2016-10-14 14:44:51 +01:00
Chris Redpath
e8e945a700 Fix adb pull with wildcard on Android v7+
Similarly to other uses of ls, the change to multi-column default output
format has confused this API. Add in a similar routine to other objects
which use ls, so that we can try to figure out if we have a multi-column
default and control the output if so, or just use the plain command if
that doesn't work and hope it is still single column output.

Signed-off-by: Chris Redpath <chris.redpath@arm.com>
2016-10-14 14:35:58 +01:00
setrofim
934075c76c Merge pull request #64 from bjackman/memoize-cpuidle-info
cpuidle: Make desc, name, latency, power memoized properties
2016-10-13 10:20:17 +01:00
Brendan Jackman
d3a02d9d9e cpuidle: Make desc, name, latency, power memoized properties
When this information is not needed, this avoids executing 4 commands on
the host for each CPU, which significantly speeds up initialising the
cpuidle module.
2016-10-12 18:31:12 +01:00
setrofim
1a47cadfa7 Merge pull request #63 from bjackman/get-domain-cpus
cpufreq: Add function to get CPUs in frequency domain
2016-10-12 08:29:14 +01:00
Brendan Jackman
f1b4bf2845 cpufreq: Add function to get CPUs in frequency domain 2016-10-11 20:40:10 +01:00
setrofim
25818b035e Merge pull request #62 from bjackman/check-governor-supported
cpufreq: Improve error when setting all CPUs to unsupported governor
2016-10-11 14:36:12 +01:00
Brendan Jackman
af4214c3fb cpufreq: Improve error when setting all CPUs to unsupported governor
Currently when using set_all_governors to try to set an unsupported
governor, the error is a somewhat cryptic "sh: echo: I/O error". Add a
check that diagnoses this error.

In order to avoid slowing down the "good" path, the check for
unsupported governors is only done if the failure has already occured.
2016-10-11 14:28:58 +01:00
setrofim
1cc6ddf140 Merge pull request #61 from credp/android_disconnect
android: don't try to disconnect devices which aren't connected
2016-10-07 08:24:11 +01:00
Chris Redpath
119fd7dc24 android: don't try to disconnect devices which aren't connected
If you try to disconnect a device and there are none, certain versions
of adb return 1, which leads to a TargetError and stops everything in
its tracks.

Try to mitigate this by checking if the device we want to disconnect is
connected before we make the disconnect call.

Signed-off-by: Chris Redpath <chris.redpath@arm.com>
2016-10-06 16:34:14 +01:00
Sergei Trofimov
cae239d1dc memoized: detect hashability by catching TypeError
This is a fix for

https://github.com/ARM-software/devlib/issues/60

Apprently, being an instance of Hashable does not mean that the object
is, in fact, hashable. So rather than testing for hashability, try to
hash the object and handle the potential error by falling back to the
old method.
2016-10-06 08:44:42 +01:00
Sergei Trofimov
09ec88e946 memoized: further fixes; hash objects whre possible
This is further to commit 6d854fd4dc

Due to the way certain objects are handled, getting the frist few bytes
of an object may not be enough (e.g. strings do not store their values
inline). To further mitigate the issue, hash the object where possible.
2016-10-04 17:57:46 +01:00
setrofim
f8440cf354 Merge pull request #59 from derkling/fix-ftrace
ftrace: ensure ftrace commands are executed as root
2016-09-28 08:20:10 +01:00
setrofim
46d65c8237 Merge pull request #58 from credp/android7_ls
AndroidTarget: Handle ls format change in Android 7.0
2016-09-27 16:28:32 +01:00
Chris Redpath
2a4eafae6e AndroidTarget: Handle ls format change in Android 7.0
We expect ls to output single column listings. Previous to Linaro Android
16.09 release, this was the default. From the 16.09 release onwards, the
default is multi-column. Unfortunately, pre-16.09 ls does not support the
'-1' option to guarantee single column output.

Test for ls supporting '-1' and use it if possible.

Signed-off-by: Chris Redpath <chris.redpath@arm.com>
2016-09-27 12:08:33 +01:00
Patrick Bellasi
3e6a040863 ftrace: ensure ftrace commands are executed as root
Configuration sysfs files required by ftrace are usually accessible only by
the root user. In systems where ADB does not run as root by default, the
initialisation of the ftrace module fails.

This patch ensure that all ftrace commands are executed with root privileges.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-09-21 16:17:45 +01:00
Sergei Trofimov
08b36e71cb utils; adding more CPU part numbers 2016-09-06 16:01:09 +01:00
Sergei Trofimov
6d854fd4dc memoized: fix for bug where wrong cached results were returned
memoized() used id() to get "unique" representations for arguments
passed to a function, in order to ensure that results for the same
function called with different arguments are treated differently.

However, Python object identities are only guaranteed to be unique at a
particular point in time. It is possible than a particular ID gets
reused for a different object if the previous object associated with
that ID no longer exists. In particular, in CPython, the IDs are just
addresses of the corresponding PyObject's in memory. If a PyObject gets
garbage collected, another object may get allocated in its place, and
the new object will "inherit" the ID by virtue of being in the same
memory location. If the new object is then used to call a memoized
function that was previously called with the old object, the old cached
result will be incorrectly returned.

To get around this issue, the cache is now indexed not only by the ID of
an object but also but the first few bytes of its value.

NOTE: there is still a potential issue it two relatively large objects
gets allocated in the same place and happen to share the first few
(currently 32) bytes, and are then both used as parameters to the same
memoized function. The only way around that is to hash the entire value
of the object. However, given the performance penalty that would incur
for larger object, and the unlikeliness of this situation actually
arising, it is currently deemed not worth it.
2016-09-06 09:57:58 +01:00
Sergei Trofimov
390a544a92 instrument: allow specifying channels in reset()
Prior to this commit, measurements to be collected were specified via
"sites" and "kinds" parameters. This has the limitation that if you
wanted measurments of kind X from site A and kind Y from site B, you'd
have to specify them as

	reset(sites=['A', 'B'], kinds=['X', 'Y'])

Which would have the effect of also collecting measurments Y for site A
and measurments X for site B. This commit adds the option of specifying
individual channels, via thier labels, e.g.

	reset(channels=['A_X', 'B_Y'])

so that only the channels you're interested in will be collected.
2016-09-02 14:03:33 +01:00
Sergei Trofimov
d7aac2b5df utils: added a function to reset memoized cache
Added a function to reset the memo cache used by memoized decorator,
which would force all memoized functions to re-evalued their results
when they are next called. This is primarily intended for debugging.
2016-09-02 13:24:30 +01:00
setrofim
0e8fc0d732 Merge pull request #57 from derkling/cgroups-cosmetics
Cgroups cosmetics
2016-08-31 11:53:19 +01:00
Patrick Bellasi
730bb606b1 cgroup: fix documentation of freeze function
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-31 11:40:10 +01:00
Patrick Bellasi
c8f118da4f cgroups: change log level to avoid cluttering caller output
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-31 11:39:33 +01:00
Sergei Trofimov
ca0b6e88a1 AndroidTarget: fixed kick_off()
kick_off relies on nohup, which may not work properly unless the command
for it is  explicitly backgrounded with "&", which was not being done in
the AndroidTarget implementation of kick_off().
2016-08-30 14:27:16 +01:00
setrofim
c307ffab15 Merge pull request #56 from derkling/cgroups-isolation-and-freeze
Add support for CPUs isolation and tasks freezing
2016-08-30 09:14:39 +01:00
Patrick Bellasi
23ad61fcae cgroups: add support for tasks freezing
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:27:08 +01:00
Patrick Bellasi
75a086d77a cgroups: add support for CPUs isolation
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:27:08 +01:00
Patrick Bellasi
21d18f8b78 cgroups: fix support to filter tasks to move into a specified CGroup
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:20:11 +01:00
Patrick Bellasi
3cab786d03 cgroups: use shutils for move_tasks
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:17:32 +01:00
Patrick Bellasi
d8ae3aba1a cgroups: add couple of methods to return the tasks of a CGroup
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:17:32 +01:00
Patrick Bellasi
42efd0a2e2 cgroups: add support list tasks in a specified CGroup
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 18:15:41 +01:00
Patrick Bellasi
83c1312b22 shutils: ensure we use "cat" version provided by Busybox
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-08-26 16:12:23 +01:00
setrofim
b9a16982d8 Merge pull request #53 from mdigiorgio/fix-cgroups_get_states
cgroups: fix get_states() return value when passing integer state
2016-08-01 10:46:24 +01:00
Michele Di Giorgio
f9cb932d9c cgroups: fix get_states() return value when passing integer state
When passing an idle state ID to get_states(), nothing is returned. Also, there
is a wrong call to enable() in the method.

Signed-off-by: Michele Di Giorgio <michele.digiorgio@arm.com>
2016-08-01 10:41:38 +01:00
setrofim
0c8f26763b Merge pull request #49 from derkling/fix-adb-connect
android: avoid connect for USB target
2016-07-29 16:09:17 +01:00
setrofim
2d496486bf Merge pull request #52 from garethstockwell/master
TelnetConnection: support for systems which do not prompt for a password
2016-07-29 16:08:30 +01:00
Gareth Stockwell
f24493676c TelnetConnection: allow username and/or password to be optional
If username is set to None, no '-l' option is appended to the telnet command.
If password is set to None, devlib does not wait for a password prompt when
connecting.
2016-07-29 14:52:52 +00:00
Gareth Stockwell
76b059c6b1 TelnetConnection: allow client to specify the original prompt string 2016-07-29 14:52:52 +00:00
setrofim
baab8ab131 Merge pull request #51 from ep1cman/master
TelnetConnection: Added the ability to specify connection port
2016-07-29 15:21:26 +01:00
Sebastian Goscik
73f2e28a06 TelnetConnection: Added the ability to specify connection port 2016-07-29 15:14:41 +01:00
Patrick Bellasi
f714dd39f1 android: avoid connect for USB target
The "adb connect" command is not required for USB connected devices.
This patch is a small update to use "adb connect" only for android devices
accessed by IP address.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-07-15 11:18:09 +01:00
Sergei Trofimov
c4784e0993 Added documentation for Target.extract() 2016-07-14 11:15:08 +01:00
Sergei Trofimov
baaa67bfcc target: added extract() method
Added a method for extracting compressed on-target files and archives.
The method extacts the specified on-target path (the method is based on
the extension) and return the path to extracted content.
2016-07-14 11:07:19 +01:00
setrofim
17692891ef Merge pull request #46 from JaviMerino/needs_su
devlib: don't use sudo/su if you are root
2016-06-24 09:18:01 +01:00
Javi Merino
16d87c6924 devlib: don't use sudo/su if you are root
Most invocations of target.execute() pass as_root=self.is_rooted .
However, is_rooted is not what you want to do here.  as_root tells the
connection to wrap the command around sudo/su to execute the command as
root.  is_rooted returns True if the device can run commands as
root (for example, if we are connected as root).  If you are already
connected as root, there is no need to wrap the command around sudo, you
are already root.  In that case, as_root should always be false.

Define a new property for the target called needs_su that returns true
if the target needs to run a command to get superuser privileges.
2016-06-23 14:55:19 +01:00
setrofim
fa20e7c28d Merge pull request #45 from mdigiorgio/improve-memoize
Improve memoize decorator
2016-06-23 11:13:36 +01:00
Michele Di Giorgio
539e9b34b9 devlib/utils/misc: improve memoize decorator
Using the wrapt module we can improve the memoize decorator. In fact, it allows
to preserve the attributes of the memoized function, such as signature,
docstring, path to the file where the function is implemented, and so on.

Signed-off-by: Michele Di Giorgio <michele.digiorgio@arm.com>
2016-06-23 09:51:47 +01:00
setrofim
ee521f64e6 Merge pull request #42 from ep1cman/fixes
LinuxTarget: now used 'uname' instead of 'busybox uname'
2016-06-16 14:50:49 +01:00
Sebastian Goscik
5880f6e9ef LinuxTarget: now used 'uname' instead of 'busybox uname'
To install busybox we need to know the ABI of the device to push the
correct binary but to know the ABI we need busybox.

Since uname is part of the POSIX standard and this issue only effects
the LinuxTarget (AndroidTarget gets this from build.prop) it is safe
to assume all LinuxTargets should have uname.
2016-06-16 13:56:13 +01:00
setrofim
cf791d1e64 Merge pull request #35 from derkling/cgroup-fix-setup
Cgroup fix setup
2016-05-27 16:39:50 +01:00
setrofim
bbee251547 Merge pull request #39 from ep1cman/fixes
AndroidDevice: kick-off no longer requires root
2016-05-27 16:37:55 +01:00
Sebastian Goscik
9af32ec485 AndroidDevice: kick-off no longer requires root
kick off will now use root if the device is rooted or if manually
specified otherwise its run without root.
2016-05-27 16:36:45 +01:00
Sergei Trofimov
89256fd408 connetion: fixing None timeout issue
Connection objects set timeout to a default value in case a timeout is
not specified. However, Target defaults timeout to None and passes that
to connection, overridng the default.

This commit ensures that default timeout remains set if calling code
specified timemout as None.

Fix for issue

https://github.com/ARM-software/devlib/issues/34
2016-05-17 14:00:01 +01:00
Patrick Bellasi
616f229949 cgroups: requires initialization after the "setup" stage
The cgroups module requires busybox and shutil to properly initialise.
This patch required the module to be initialized once the setup has
completed.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-05-13 18:17:22 +01:00
Patrick Bellasi
c4e46b7c26 target: add a "setup" stage for modules initialisation
Some modules could requires assets available on a target before being
initialised. For example, the cgroups module requires busybox and shutil
to properly initialise.

This patch adds a new stage to Target which allows to post-pone the
initialisation of some modules till Target.setup() has been executed.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-05-13 18:15:51 +01:00
setrofim
1dc1e1364c Merge pull request #28 from msrasmussen/patch-1
Fix typos in overview documentation
2016-04-27 11:01:42 +01:00
setrofim
232204633f Merge pull request #33 from derkling/fix-cgroups-noprefix
Fix cgroups noprefix
2016-04-27 08:55:18 +01:00
Patrick Bellasi
4b58c573a5 cgroups: make probe method more robust
In case a target does not report a configuration file, we can still check
the user-space API to verify it CGroups are supported.

NOTE: a rooted target is still a mandatory requirement because some commands
are still dependant on the possibility to run them with root permissions.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-26 16:38:30 +01:00
Patrick Bellasi
3acf5d56df cgroups: run some commands with root permissions
Certain commands requires in general root permissions to be properly
executed (for example on an Android target).

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-26 16:38:30 +01:00
Patrick Bellasi
96392fd6b5 cgroups: fix run_into shutils
On some targets the run_into function is not passing all params to the
called application. This patch should fix that using shift to get a list
of all command parameters.

Moreovere, we use now exec to spawn the command execution, which avoids to
generate yet another shell to run the required command.
2016-04-26 16:38:30 +01:00
Patrick Bellasi
c976189444 cgroups: fix get attributes for controller noprefix mounted
CGroups controller can be mounted by specifying a "noprefix" option,
in which case attribute names are named as:
   <mountpoint>/<attribute_name>
instead of the (more recent) naming schema using:
   <mountpoint>/<contoller_name>.<attribute_name>

For example, Android uses the old format for backward compatibility
with user-space. Thus, it's possible in general to work on a target
system where some controller are mounted "noprefix" while others not.

This patchset adds a set of updates which allows to use the proper
attributes naming schema based on how the controller has been mounted.

This patch provides a more generic implementation of the get attributes
shutils which is working also for noprefix mounted controllers.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-26 16:38:28 +01:00
Patrick Bellasi
658005a178 cgroups: properly format attributes path and check for being supported
CGroups controller can be mounted by specifying a "noprefix" option,
in which case attribute names are named as:
   <mountpoint>/<attribute_name>
instead of the (more recent) naming schema using:
   <mountpoint>/<contoller_name>.<attribute_name>

For example, Android uses the old format for backward compatibility
with user-space. Thus, it's possible in general to work on a target
system where some controller are mounted "noprefix" while others not.

This patchset adds a set of updates which allows to use the proper
attributes naming schema based on how the controller has been mounted.

This patch makes use of the Controller::_noprefix option to properly
build the attribute path. It adds also a check which reports a more
clear error in case an attribute is set which is not provided by the
controller.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-26 16:38:25 +01:00
Patrick Bellasi
15f9c03b45 cgroups: keep track if a controller is mounted with noprefix option
CGroups controller can be mounted by specifying a "noprefix" option,
in which case attribute names are named as:
   <mountpoint>/<attribute_name>
instead of the (more recent) naming schema using:
   <mountpoint>/<contoller_name>.<attribute_name>

For example, Android uses the old format for backward compatibility
with user-space. Thus, it's possible in general to work on a target
system where some controller are mounted "noprefix" while others not.

This patchset adds a set of updates which allows to use the proper
attributes naming schema based on how the controller has been mounted.

This first patch keeps track of whatever a controller has been mounted
using the noprefix option.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-26 16:38:21 +01:00
Patrick Bellasi
28739397c0 cgroups: add a couple of functions to move tasks among groups
This patch provides a couple of utility functions which makes it
easy to run a command under a specific CGroup or to move all tasks
from a group to another.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-04-22 11:43:49 +01:00
setrofim
741157c000 Merge pull request #31 from derkling/fix-newline-separator
fix: AdbConnection: added automatic detection of new line separators
2016-03-29 09:26:06 +01:00
Patrick Bellasi
c2329bd80e fix: AdbConnection: added automatic detection of new line separators
The newline separator is a property of AdbConnection while the adb_shell is
just a method of the android module, thus we do not have a "self" pointer
to and AdbConnection from within the adb_shell function.

This patch fixes 8de24b5 by exposing the newline_separator used by adb_shell
as a parameter of that method and using the AdbConnection::newline_separator
to properly initialize it at executue() time.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-03-28 12:29:45 +01:00
setrofim
10978b0fd7 Merge pull request #29 from ep1cman/fixes
AdbConnection: added automatic detection of new line separators
2016-03-23 15:20:58 +00:00
Sebastian Goscik
8de24b5601 AdbConnection: added automatic detection of new line separators 2016-03-23 15:10:26 +00:00
Morten Rasmussen
192fb52cae Fix typos in overview documentation 2016-03-21 15:15:26 +00:00
Sergei Trofimov
6bda8cb867 AndroidTarget: do not set executables_directory inside __init__
This is already being resolved inside the base's __init__.
2016-03-04 18:34:09 +00:00
setrofim
91f4f97a0b Merge pull request #25 from mdigiorgio/bg-as_root
ssh: add possibility to run command in background as root
2016-03-01 14:37:51 +00:00
Michele Di Giorgio
3bf3017f85 ssh: add possibility to run command in background as root
Executing a command as root was not possible when running it in background.
This is done in a similar way as with a standard execute call.

This was also a bug in ssh.py because if you tried to run a background command
on the target, the background function in target.py was passing the as_root
parameter which was not present in ssh.py.

Signed-off-by: Michele Di Giorgio <michele.digiorgio@arm.com>
2016-03-01 11:12:10 +00:00
setrofim
95aaa2662e Merge pull request #17 from derkling/ftrace_optional_confs
FTrace: optional function and events
2016-02-26 08:23:46 +00:00
setrofim
fbe4c4b730 Merge pull request #24 from derkling/cgroup-fixups
cgroup: demote info logging statement
2016-02-26 08:23:05 +00:00
Patrick Bellasi
7112cfef3a cgroup: demote info logging statement
The info statements are clobbering the "normal" output of devlib users.
This patch demote the logging to debug level. The user can still
log-report these information from the corresponding functions call site.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-25 18:47:39 +00:00
Patrick Bellasi
32defe1ce3 ftrace: demote info logging statement
This is the only info statement which is thus clobbering the "normal"
output of a client ueser for that devlib. This patch demote the logging
to debug level. The user can still log-report the output file which is also
an input parameter.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-25 18:47:31 +00:00
Patrick Bellasi
78aa774e25 ftrace: disable trace events not available in the target kernel
In general we could be interested to define a common configuration to use
across different kernels. Thus, for a given set of experiments, some
trace events can be present only on some kernels and not others.

This patch introduces a check for the availability of trace events by
ensuring that we consider only those available in the kernel in use
in the target. In case of a trace event is not supported in the target
kernel we still log a warning.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-25 18:47:31 +00:00
Patrick Bellasi
d7bbad3aac ftrace: disable tracing of functions not available in the target kernel
In general we could be interested to defined a common configuration to use
across different kernels. Thus, for a given set of experiments, some
functions can be present only on some kernels and not others.

This patch updates the check for the availability of functions to profile by
ensuring that we consider only functions available in the kernel in use
in the target. In case of a function cannot be profiled in the target kernel
we log a warning instead of raising an exception.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-25 18:47:31 +00:00
Patrick Bellasi
a8dfd2e744 ftrace: bugfix start marker injection
The start marker must be inserted when tracing has been already enabled.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-25 18:47:31 +00:00
Sergei Trofimov
ebe3a8a0a8 LinuxTarget: fixing reboot sequence
When issuing a target.reboot(), the reset was immediately followed by a
boot() (for platforms that have it) and an attempt to connect. When
issuing a soft reset, it's possible the target is still shutting down
when the attempt to connect is made. This results in the connection
succeeding but being severed shortly thereafter.

This introduces a delay after the reset to the reboot sequence, giving
the target time to shutdown and improves the handling of EOF's that
result from failed reconnection attempts (while still being with the
allotted timeout period.
2016-02-25 10:28:45 +00:00
setrofim
9c89ca0437 Merge pull request #22 from derkling/cgroup-fixups
Cgroup fixups
2016-02-24 14:32:13 +00:00
setrofim
3f804a42fe Merge pull request #23 from ep1cman/fixes
uname: Fixed calls to uname to use busybox
2016-02-24 14:30:46 +00:00
Sebastian Goscik
bdbf474023 uname: Fixed calls to uname to use busybox
Not all devices have uname, since devlib deploys its own busybox binary
the most portable way to use uname is to use it via busybox.
2016-02-24 14:29:05 +00:00
Patrick Bellasi
e2e5e687e9 cgroups: use splitlines instead of split('\n')
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-24 11:22:48 +00:00
Patrick Bellasi
a65ff13617 cgroups: fix attributes reporting for controller with only one attribute
The current code used to read the attributes values for a controller uses
a "grep '' CONTROLLER.*" under the assumption that the output is a list of
   file:value
However, if there is a single controller attribute, grep does not report
the file name in output thus returning an empty list at the python side.

This patch fix that issue by also switching to the usage of a shutil
implementation of the attributes parsing code.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-02-24 11:22:48 +00:00
setrofim
615f1ce5e8 Merge pull request #21 from ep1cman/fixes
Fixes
2016-02-23 17:15:00 +00:00
Sebastian Goscik
f5b7c82f52 AndroidTarget & LinuxTarget: Added a model property 2016-02-23 17:11:26 +00:00
Sebastian Goscik
a7f6ddb05a LocalConnection: Added a password parameter
This means it can now be set programatically and not always require
a prompt.
2016-02-23 17:11:26 +00:00
setrofim
f420612b5b Merge pull request #19 from ep1cman/fixes
AndroidTarget: Added package data and extrenal storage path settings
2016-02-23 10:35:46 +00:00
Sebastian Goscik
040daab2cb Target: Fixed fstab parsing 2016-02-23 10:30:02 +00:00
Sebastian Goscik
0a8b0c6989 AndroidTarget: Added package data and extrenal storage path settings
`package_data_directory` and `external_storage_directory` are now arguments of AndroidTarget
2016-02-23 10:30:02 +00:00
setrofim
e7aea717cc Merge pull request #18 from ep1cman/fixes
Updates to support WA integration
2016-02-16 12:51:08 +00:00
Sebastian Goscik
0c11289e18 Android: Updated ANDROID_VERSION_MAP 2016-02-15 15:55:59 +00:00
Sebastian Goscik
ff8261e44b AndroidTarget: Updated default executables_directory
Now defaults to '/data/local/tmp' which is both executable and writable
on all android devices, including production ones.
2016-02-15 15:55:59 +00:00
Sebastian Goscik
1424cebb90 Added quotes around commands using raw paths
This fixes issues with spaces in path names.
2016-02-15 15:46:35 +00:00
Sebastian Goscik
aab487c1ac pylint 2016-02-15 15:44:38 +00:00
Sebastian Goscik
880a0bcb7c AndroidTarget: Added swipe direction to swipe_to_unlock
swipe_to_unlock can now do either horizontal or vertical swipes to
unlock a target
2016-02-15 15:44:38 +00:00
Sebastian Goscik
cafeb81b83 AndroidTarget: Added android_id property
Get the device's ANDROID_ID. Which is
  "A 64-bit number (as a hex string) that is randomly generated when the user
   first sets up the device and should remain constant for the lifetime of the
   user's device."
2016-02-15 15:44:38 +00:00
Sebastian Goscik
be8f972f60 Target: Added install_if_needed method
This method will first search the target for a binary, only installing
it, if one was not found.
2016-02-15 15:44:33 +00:00
Sebastian Goscik
84151f953a Target: Modified get_installed search order
Changed get_installed to search self.executables_directory first.
This means that user provided binaries will be used over system ones.

Also added the option to not search system folders for a binary.
2016-02-15 15:09:27 +00:00
Sebastian Goscik
33603c6648 Target: Added directory_exists method 2016-02-15 15:07:19 +00:00
Sebastian Goscik
1890db7c04 AndroidTarget: Updated ANDROID_SCREEN_STATE_REGEX
The format of "dumpsys power" changed in Android M
2016-02-15 14:43:30 +00:00
Sergei Trofimov
40fce1392a ARM platforms: fixing big_core setting.
Juno and TC2 platfroms where ignorring big_core parameter passed in,
always setting it to their corresponding defaults. This removes that
problem, and removes the setting of the big core defaults entirely, as
they would be adentified through the regular Platform mechanism.
2016-02-02 12:36:56 +00:00
Sergei Trofimov
10a80d2335 target: added insmod() method.
This allows to install a kernel module from a host-side path.
2016-01-27 17:02:59 +00:00
Sergei Trofimov
5a81fe9888 target: added lsmod method
Added a wrapper for lsmod that parses the output into named tuples.
2016-01-27 16:51:48 +00:00
setrofim
798745ff4e Merge pull request #16 from derkling/add_ftrace_function_profiling
Add ftrace function profiling
2016-01-27 10:41:59 +00:00
Patrick Bellasi
082a82c7c5 ftrace: add support to report function profiling data
Functions profiling data are reported in a set of files, one for each CPU.

This patch adds the required support to collect these data into a single
JSON formatted file. Data are collected using a shutils routing on the
target side and formatted in JSON on the host side.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 18:37:53 +00:00
Patrick Bellasi
7f5a150b4f ftrace: add support to collect function stats
Function stats can be collected while events are accumulated into a trace.

This function adds the required support to start and stop functions
profiling across a trace collection.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 18:37:51 +00:00
Patrick Bellasi
c5bc987226 ftrace: add initial support for function profiling
FTrace allows to collect profiling stats for kernel functions.
This patch adds the initial support which allows to specify a list of
kernel function we would like to profile.

If a list of functions to profile is specified, for each specified function
we check for that function to be actually one of the available instrumented
functions which can be monitored by FTrace.
If a function is not supported, we throw an expection so that the user
is aware about the analysis which is going to do.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 18:37:24 +00:00
Patrick Bellasi
bda7a16656 ftrace: move file path definitions
All ftrace generated files are present under a common base folder.

This patch updates the FtraceCollector API to exposes just the common
base folder from where all the other paths can be generated.
This is a refactoring patch which makes it easier to add further attributes.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 18:36:39 +00:00
setrofim
3f1577dd02 Merge pull request #15 from derkling/add_shutils_support
Add shutils support
2016-01-26 17:41:02 +00:00
setrofim
c2d81ea538 Merge pull request #14 from derkling/add_x86_support
bins: add busybox binary for x86_64 targets
2016-01-26 17:37:38 +00:00
setrofim
b1a7f3fcd0 Merge pull request #13 from derkling/fix_ftrace
ftrace: give more time for a trace to be collected
2016-01-26 17:36:52 +00:00
setrofim
d4686d08d1 Merge pull request #12 from derkling/fix_andoid_dirpull
android: add support to pull multiple files using wildcard expressions
2016-01-26 17:36:18 +00:00
Patrick Bellasi
ebd4349786 bins: add busybox binary for x86_64 targets
Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:59:47 +00:00
Patrick Bellasi
82e951b4ce ftrace: give more time for a trace to be collected
The time required to fetch a trace depends not only on it time length but
also on the amount of events collected and the processing power of the
target device. This patch tries to factorise these last two components
into a 5x constant which is good enough to collect a relatively big trace
on a relatively slow device.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:59:14 +00:00
Patrick Bellasi
51b7f01d36 cpufreq: add functions to get frequencies and governors for all online CPUs
This patch adds a couple of shutils functions to efficiently read all
the frequencies/governors and returne them into a dictionary indexed by
CPU id.

The shutils functions are returning a line per each CPU that can be easily
converted into a dictionary in the host side.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:58:41 +00:00
Patrick Bellasi
cf761317bd cpufreq: switch to usage of shutils functions
This patch convert some functions to the usage of shutils provided calls.
This approach is more portable and allows to use these calls also on
an Android target.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:58:41 +00:00
Patrick Bellasi
f2eac51c69 target: add support for target side scripts
Some complex operations are faster if executed on the target side with the
help of a small script. For example, reading the current frequency and
or governor for all the online CPUs.

This patch adds a support script to be deployed on the target as well as
an internal utility function which allows to call functions provided by
that support script.

The support script has to be cross platform, thus:
1. only a limited set of shell functions can be used, which must be supported
   both in BASH and the Android shell
2. some paths needs to be defined depending on the specific target

To address the last constrain, this patch provides a "template" script which
contains some tokens "__DEVLIB_<TOKEN>__" that are replaced right before to
push the script on the target.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:58:41 +00:00
Patrick Bellasi
c93e3d6d83 android: add support to pull multiple files using wildcard expressions
The ADB pull command allows only to pull a single file or a whole directory.

This patch adds the required support to pull only a selection of files, from
a target folder, which match a path specified using '*' and/or '?' wildcards.
In this case we first get the list of files on the target, using the
wildcard expansion support provided by the "ls" command, and than we pull
each and every file returned from the previous command.

This operation mode is available only if the 'dest' parameter is a valid
host-side folder.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2016-01-26 15:57:16 +00:00
setrofim
dcf239b06c Merge pull request #11 from JaviMerino/warn_unsupported_module
target: warn if a module is not supported for the target
2016-01-18 18:07:06 +00:00
Javi Merino
d0c71fbc86 target: warn if a module is not supported for the target
When you create a target using devlib, you may not set
load_default_modules because you want to load a specific list of
modules.  In that case, raise a warning if one of those modules failed
to load, as it is something you are not expecting.

In the general case, the standard set of modules are tried to be loaded.
We know that they are not supported for all platforms so just emit a
debug message.
2016-01-18 17:31:00 +00:00
setrofim
217a97485b Merge pull request #10 from Leo-Yan/use_ordered_dictionary
Instrument: Add channel with ordered sequence
2016-01-07 10:40:58 +00:00
Leo Yan
3229bb181a Instrument: Add channel with ordered sequence
When add channel for power meter with specific order, it also imply the
order with corresponding fields in captured data. So later need read
back the index for channel and use it to reference power data.

So need use ordered dictionary object for channel.

Signed-off-by: Leo Yan <leo.yan@linaro.org>
2016-01-06 10:18:21 +08:00
Sergei Trofimov
47bf915b7c platform: smarter detection of big cores
If big_core  is not explicitly specified by the user, it is populated by
Platform (provided the target has exactly two clusters). Perviously this
was done bu assuming that the target boots on little cluster and that
the last CPU is big. This is still used as a fallback, but now Platform
is ware of the names of big CPUs in currently existing big.LITTLE
configurations and uses that first to identify the big.
2015-12-16 17:19:40 +00:00
Sergei Trofimov
59f4f81447 Cpuinfo: extract CPU featurs from "flags" entry
get_cpu_features() now checks "flags" entry as well as "Features"
entries in cpuinfo section.
2015-12-15 18:08:53 +00:00
Sergei Trofimov
a1e991c12f vexpress-uboot: only set env if bootargs are specified
If env is specified, U-Boot module will wait for U-Boot prompt and then
interrupt the boot sequence so it should be updated. The vexpress module
that used it always set the env to contain bootargs (even if they were
None), always causing boot to be interrupted, even when that is not
necessary. This update means that boot will be interrupted only if
bootargs were specified (and therefore env needs to be updated).
2015-12-15 18:08:53 +00:00
setrofim
4c4d7f177e Merge pull request #9 from credp/junoenergymeter_instantaneous
JunoEnergyInstrument: Provide INSTANTANEOUS interface
2015-12-15 17:52:01 +00:00
setrofim
485b4a62e3 Merge pull request #8 from JaviMerino/fix_android_without_home
android: fix initialization without android
2015-12-15 17:46:32 +00:00
Chris Redpath
09915101d8 JunoEnergyInstrument: Provide INSTANTANEOUS interface
This allows clients to use the instrument more easily from their code
if they were previously used to using an HWMON-style interface.

In order to provide the measurement, the existing readenergy binary
is updated and changed so that if no output option is specified then
we just print the current values and exit.

Signed-off-by: Chris Redpath <chris.redpath@arm.com>
2015-12-15 17:09:55 +00:00
Javi Merino
7f32efcb64 android: fix initialization without android
In workload automation, utils.android._initialize_without_android_home()
gets android_home from adb's path.  When this code was copied to devlib,
we mistakenly dropped parsing the output of "which" and instead call
os.path.dirname() on "adb", which always returns "" and makes
_initialize_without_android_home() fail.

Make _initialize_without_android_home() parse the output of "which"
again.
2015-12-15 13:43:56 +00:00
Sergei Trofimov
171cc25d50 AndroidTarget: fixed use of variables in as_root=True commands.
In order to execute as root, the command string gets echo'd into so;
previusly, double quotes were used in echo, which caused any veriables
in the command string to be expanded _before_ it was echoed.
2015-12-14 17:21:47 +00:00
Sergei Trofimov
f52bf79eb6 android: fixing exit code checking
This fixes an issue introduced in
64261a65cb

The addtional echo means that $? will always have "0" (the exit code for
the echo). This removes the extra echo, prepending \n to $? instead
2015-12-11 17:18:18 +00:00
Sergei Trofimov
2d9c0bf8a5 platform: adding default big core for ARM platforms.
Since for specific platforms (Juno/TC2) we know what the big core is, we
can set the big_core default so it doesn't have to be specified by the
user. Also remove validation prior to being updated from target and
setting little_core based on the value of big_core rather than by
indexing into core_names.
2015-12-10 14:21:02 +00:00
Sergei Trofimov
64261a65cb adb_shell: fixing handling of line breaks at the end of the output
- adb protcol uses "\r\n" for line breaks. This is not handled by
  Python's line break translation, as not a file. So spliting on '\n'
  when extracting the exit code resulted in stray '\r' in the output.
- adb_shell expects exit code to be echoed on the same line. This may
  not have been the case if latest output from executed command was not
  a complete line. An extra echo will now ensure that the exit code will
  be on its own line even in that case.
2015-11-24 12:50:02 +00:00
setrofim
42d41e9345 Merge pull request #5 from punitagrawal/master
module: Add thermal support
2015-11-24 11:52:23 +00:00
Punit Agrawal
c7fc01c6b5 module: Add thermal support
Add a thermal module that will allow querying for thermal zones and trip
points in the target. The module allow supports enable/disabling thermal
zones as well as change trip temperatures.

Signed-off-by: Punit Agrawal <punit.agrawal@arm.com>
2015-11-24 11:43:05 +00:00
Sergei Trofimov
40274101ad Version bump. 2015-11-19 09:41:24 +00:00
Sergei Trofimov
b53245344b target: fixed get_installed() on new targets
get_installed() looks in self.executables_directory for the binary. This
may not exist on a target (this is created when setup() is invoked).
This commit updated get_installed() to check whether target_directory
exists first, avoiding the error.
2015-11-18 18:11:59 +00:00
Sergei Trofimov
961f9576e5 target: resolve default paths eariler during the connection
Default paths for working_directory and exectuables_directory used to be
resolved at the end of the connect (because for Linux targets, the
default changes depending on the connection). This is now offloaded to a
separate internal method that is invoked earlier during connect() (after
the connection is established but before other connection actions are
resolved, as some of those actions rely on the directories being set).
2015-11-18 17:38:42 +00:00
setrofim
d4c8b0f222 Merge pull request #4 from derkling/cpufreq-tracing
Add support to properly trace CPUs frequencies
2015-11-16 18:06:10 +00:00
Patrick Bellasi
a7cfd28bd0 ftrace: force report CPU frequencies at trace start and stop
When the cpufreq module is loaded, quite likely we want to run experiments
which involve an analysis of frequencies transitions. However, if during
an experiment there are not cpu_frequency events, the generated trace
does not allows to know at which frequency the experiment has been executed.
This happens for example when we are running at constant frequency or just
because the workload is not generating a variable capacity request in the
system.

This path ensure the a "cpu_frequency" events is always generated at the
beginning and at the end of a collected trace. This support is required
for example to properly plot CPUs frequencies event when there are not
CPUFreq generated changes of OPP.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2015-11-16 17:47:58 +00:00
Patrick Bellasi
701e6adf7a cpufreq: add method to trace current frequencies
This patch add a method which allows to inject into a trace a "cpu_frequency"
event for each CPU reporting the current frequency the CPU is running at.
Such a method could be useful to force report CPUs frequency into a trace
file, for example at tracing start and stop, thus allowing to know always
at which frequency an experiment has been executed.

Signed-off-by: Patrick Bellasi <patrick.bellasi@arm.com>
2015-11-16 17:47:58 +00:00
37 changed files with 3345 additions and 380 deletions

4
.gitignore vendored
View File

@@ -3,3 +3,7 @@
*.orig
.ropeproject
*.egg-info
devlib/bin/scripts/shutils
doc/_build/
build/
dist/

View File

@@ -7,12 +7,18 @@ from devlib.module import get_module, register_module
from devlib.platform import Platform
from devlib.platform.arm import TC2, Juno, JunoEnergyInstrument
from devlib.platform.gem5 import Gem5SimulationPlatform
from devlib.instrument import Instrument, InstrumentChannel, Measurement, MeasurementsCsv
from devlib.instrument import MEASUREMENT_TYPES, INSTANTANEOUS, CONTINUOUS
from devlib.instrument.daq import DaqInstrument
from devlib.instrument.energy_probe import EnergyProbeInstrument
from devlib.instrument.hwmon import HwmonInstrument
from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument
from devlib.trace.ftrace import FtraceCollector
from devlib.host import LocalConnection
from devlib.utils.android import AdbConnection
from devlib.utils.ssh import SshConnection, TelnetConnection, Gem5Connection

BIN
devlib/bin/arm64/m5 Executable file

Binary file not shown.

Binary file not shown.

BIN
devlib/bin/armeabi/m5 Executable file

Binary file not shown.

224
devlib/bin/scripts/shutils.in Executable file
View File

@@ -0,0 +1,224 @@
#!__DEVLIB_SHELL__
CMD=$1
shift
BUSYBOX=${BUSYBOX:-__DEVLIB_BUSYBOX__}
FIND=${FIND:-$BUSYBOX find}
GREP=${GREP:-$BUSYBOX grep}
SED=${SED:-$BUSYBOX sed}
CAT=${CAT:-$BUSYBOX cat}
AWK=${AWK:-$BUSYBOX awk}
PS=${PS:-$BUSYBOX ps}
################################################################################
# CPUFrequency Utility Functions
################################################################################
cpufreq_set_all_frequencies() {
FREQ=$1
for CPU in /sys/devices/system/cpu/cpu[0-9]*; do
echo $FREQ > $CPU/cpufreq/scaling_cur_freq
done
}
cpufreq_get_all_frequencies() {
$GREP '' /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq | \
$SED -e 's|/sys/devices/system/cpu/cpu||' -e 's|/cpufreq/scaling_cur_freq:| |'
}
cpufreq_set_all_governors() {
GOV=$1
for CPU in /sys/devices/system/cpu/cpu[0-9]*; do
echo $GOV > $CPU/cpufreq/scaling_governor
done
}
cpufreq_get_all_governors() {
$GREP '' /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | \
$SED -e 's|/sys/devices/system/cpu/cpu||' -e 's|/cpufreq/scaling_governor:| |'
}
cpufreq_trace_all_frequencies() {
FREQS=$($CAT /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq)
CPU=0; for F in $FREQS; do
echo "cpu_frequency_devlib: state=$F cpu_id=$CPU" > /sys/kernel/debug/tracing/trace_marker
CPU=$((CPU + 1))
done
}
################################################################################
# CPUIdle Utility Functions
################################################################################
cpuidle_wake_all_cpus() {
CPU_PATHS=/sys/devices/system/cpu/cpu[0-9]*
MASK=0x1; for F in $CPU_PATHS; do
$BUSYBOX taskset $MASK true &
MASK=$($BUSYBOX printf '0x%x' $((MASK * 2)))
done
}
################################################################################
# FTrace Utility Functions
################################################################################
ftrace_get_function_stats() {
for CPU in $(ls /sys/kernel/debug/tracing/trace_stat | sed 's/function//'); do
REPLACE_STRING="s/ Function/\n Function (CPU$CPU)/"
$CAT /sys/kernel/debug/tracing/trace_stat/function$CPU \
| sed "$REPLACE_STRING"
done
}
################################################################################
# CGroups Utility Functions
################################################################################
cgroups_get_attributes() {
test $# -eq 2 || exit -1
CGROUP="$1"
CONTROLLER="$2"
# Check if controller is mounted with "noprefix" option, which is quite
# common on Android for backward compatibility
ls $CGROUP/$CONTROLLER\.* 2>&1 >/dev/null
if [ $? -eq 0 ]; then
# no "noprefix" option, attributes format is:
# mnt_point/controller.attribute_name
$GREP '' $CGROUP/* | \
$GREP "$CONTROLLER\." | \
$SED -e "s|$CONTROLLER\.||" -e "s|$CGROUP/||"
else
# "noprefix" option, attribute format is:
# mnt_point/attribute_name
$GREP '' $(\
$FIND $CGROUP -type f -maxdepth 1 |
$GREP -v -e ".*tasks" -e ".*cgroup\..*") | \
$SED "s|$CGROUP/||"
fi
}
cgroups_run_into() {
# Control groups mount point
CGMOUNT=${CGMOUNT:-/sys/fs/cgroup}
# The control group we want to run into
CGP=${1}
shift 1
# The command to run
CMD="${@}"
# Execution under root CGgroup
if [ "x/" == "x$CGP" ]; then
$FIND $CGMOUNT -type d -maxdepth 0 | \
while read CGPATH; do
# Move this shell into that control group
echo $$ > $CGPATH/cgroup.procs
echo "Moving task into root CGroup ($CGPATH)"
done
# Execution under specified CGroup
else
# Check if the required CGroup exists
$FIND $CGMOUNT -type d -mindepth 1 | \
$GREP "$CGP" &>/dev/null
if [ $? -ne 0 ]; then
echo "ERROR: could not find any $CGP cgroup under $CGMOUNT"
exit 1
fi
$FIND $CGMOUNT -type d -mindepth 1 | \
$GREP "$CGP" | \
while read CGPATH; do
# Move this shell into that control group
echo $$ > $CGPATH/cgroup.procs
echo "Moving task into $CGPATH"
done
fi
# Execute the command
exec $CMD
}
cgroups_tasks_move() {
SRC_GRP=${1}
DST_GRP=${2}
shift 2
FILTERS=$*
$CAT $SRC_GRP/tasks | while read TID; do
echo $TID > $DST_GRP/cgroup.procs
done
[ "x$FILTERS" = "x" ] && exit 0
PIDS=`$PS -o comm,pid | $GREP $FILTERS | $AWK '{print $2}'`
PIDS=`echo $PIDS`
echo "PIDs to save: [$PIDS]"
for TID in $PIDS; do
COMM=`$CAT /proc/$TID/comm`
echo "$TID : $COMM"
echo $TID > $SRC_GRP/cgroup.procs || true
done
}
cgroups_tasks_in() {
GRP=${1}
for TID in $($CAT $GRP/tasks); do
COMM=`$CAT /proc/$TID/comm 2>/dev/null`
[ "$COMM" != "" ] && CMDL=`$CAT /proc/$TID/cmdline 2>/dev/null`
[ "$COMM" != "" ] && echo "$TID,$COMM,$CMDL"
done
exit 0
}
################################################################################
# Main Function Dispatcher
################################################################################
case $CMD in
cpufreq_set_all_frequencies)
cpufreq_set_all_frequencies $*
;;
cpufreq_get_all_frequencies)
cpufreq_get_all_frequencies
;;
cpufreq_set_all_governors)
cpufreq_set_all_governors $*
;;
cpufreq_get_all_governors)
cpufreq_get_all_governors
;;
cpufreq_trace_all_frequencies)
cpufreq_trace_all_frequencies $*
;;
cpuidle_wake_all_cpus)
cpuidle_wake_all_cpus $*
;;
cgroups_get_attributes)
cgroups_get_attributes $*
;;
cgroups_run_into)
cgroups_run_into $*
;;
cgroups_tasks_move)
cgroups_tasks_move $*
;;
cgroups_tasks_in)
cgroups_tasks_in $*
;;
ftrace_get_function_stats)
ftrace_get_function_stats
;;
*)
echo "Command [$CMD] not supported"
exit -1
esac
# vim: tabstop=4 shiftwidth=4

BIN
devlib/bin/x86_64/busybox Executable file

Binary file not shown.

View File

@@ -14,11 +14,8 @@
#
from devlib.utils.misc import TimeoutError # NOQA pylint: disable=W0611
class DevlibError(Exception):
"""Base class for all Workload Automation exceptions."""
"""Base class for all Devlib exceptions."""
pass
@@ -38,3 +35,17 @@ class HostError(DevlibError):
"""An error has occured on the host"""
pass
class TimeoutError(DevlibError):
"""Raised when a subprocess command times out. This is basically a ``DevlibError``-derived version
of ``subprocess.CalledProcessError``, the thinking being that while a timeout could be due to
programming error (e.g. not setting long enough timers), it is often due to some failure in the
environment, and there fore should be classed as a "user error"."""
def __init__(self, command, output):
super(TimeoutError, self).__init__('Timed out: {}'.format(command))
self.command = command
self.output = output
def __str__(self):
return '\n'.join([self.message, 'OUTPUT:', self.output or ''])

View File

@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from glob import iglob
import os
import shutil
import subprocess
@@ -28,12 +29,12 @@ class LocalConnection(object):
name = 'local'
def __init__(self, timeout=10, keep_password=True, unrooted=False):
def __init__(self, platform=None, keep_password=True, unrooted=False,
password=None, timeout=None):
self.logger = logging.getLogger('local_connection')
self.timeout = timeout
self.keep_password = keep_password
self.unrooted = unrooted
self.password = None
self.password = password
def push(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
self.logger.debug('cp {} {}'.format(source, dest))
@@ -41,9 +42,15 @@ class LocalConnection(object):
def pull(self, source, dest, timeout=None, as_root=False): # pylint: disable=unused-argument
self.logger.debug('cp {} {}'.format(source, dest))
shutil.copy(source, dest)
if ('*' in source or '?' in source) and os.path.isdir(dest):
# Pull all files matching a wildcard expression
for each_source in iglob(source):
shutil.copy(each_source, dest)
else:
shutil.copy(source, dest)
def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
def execute(self, command, timeout=None, check_exit_code=True,
as_root=False, strip_colors=True):
self.logger.debug(command)
if as_root:
if self.unrooted:
@@ -54,7 +61,9 @@ class LocalConnection(object):
try:
return check_output(command, shell=True, timeout=timeout, ignore=ignore)[0]
except subprocess.CalledProcessError as e:
raise TargetError(e)
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'.format(
e.returncode, command, e.output)
raise TargetError(message)
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
if as_root:
@@ -77,4 +86,3 @@ class LocalConnection(object):
if self.keep_password:
self.password = password
return password

View File

@@ -14,6 +14,7 @@
#
import csv
import logging
import collections
from devlib.utils.types import numeric
@@ -167,8 +168,9 @@ class Instrument(object):
def __init__(self, target):
self.target = target
self.logger = logging.getLogger(self.__class__.__name__)
self.channels = {}
self.channels = collections.OrderedDict()
self.active_channels = []
self.sample_rate_hz = None
# channel management
@@ -178,7 +180,7 @@ class Instrument(object):
def get_channels(self, measure):
if hasattr(measure, 'name'):
measure = measure.name
return [c for c in self.channels if c.measure.name == measure]
return [c for c in self.list_channels() if c.kind == measure]
def add_channel(self, site, measure, name=None, **attrs):
if name is None:
@@ -194,8 +196,8 @@ class Instrument(object):
def teardown(self):
pass
def reset(self, sites=None, kinds=None):
if kinds is None and sites is None:
def reset(self, sites=None, kinds=None, channels=None):
if kinds is None and sites is None and channels is None:
self.active_channels = sorted(self.channels.values(), key=lambda x: x.label)
else:
if isinstance(sites, basestring):
@@ -203,6 +205,12 @@ class Instrument(object):
if isinstance(kinds, basestring):
kinds = [kinds]
self.active_channels = []
for chan_name in (channels or []):
try:
self.active_channels.append(self.channels[chan_name])
except KeyError:
msg = 'Unexpected channel "{}"; must be in {}'
raise ValueError(msg.format(chan_name, self.channels.keys()))
for chan in self.channels.values():
if (kinds is None or chan.kind in kinds) and \
(sites is None or chan.site in sites):

View File

@@ -27,7 +27,7 @@ class DaqInstrument(Instrument):
device_id='Dev1',
v_range=2.5,
dv_range=0.2,
sampling_rate=10000,
sample_rate_hz=10000,
channel_map=(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23),
):
# pylint: disable=no-member
@@ -51,17 +51,18 @@ class DaqInstrument(Instrument):
self.device_config = DeviceConfiguration(device_id=device_id,
v_range=v_range,
dv_range=dv_range,
sampling_rate=sampling_rate,
sampling_rate=sample_rate_hz,
resistor_values=resistor_values,
channel_map=channel_map,
labels=labels)
self.sample_rate_hz = sample_rate_hz
for label in labels:
for kind in ['power', 'voltage']:
self.add_channel(label, kind)
def reset(self, sites=None, kinds=None):
super(DaqInstrument, self).reset(sites, kinds)
def reset(self, sites=None, kinds=None, channels=None):
super(DaqInstrument, self).reset(sites, kinds, channels)
self.execute('close')
result = self.execute('configure', config=self.device_config)
if not result.status == Status.OK: # pylint: disable=no-member

View File

@@ -20,11 +20,6 @@ import tempfile
import struct
import subprocess
try:
import pandas
except ImportError:
pandas = None
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError
from devlib.utils.misc import which
@@ -50,22 +45,20 @@ class EnergyProbeInstrument(Instrument):
if self.caiman is None:
raise HostError('caiman must be installed on the host '
'(see https://github.com/ARM-software/caiman)')
if pandas is None:
self.logger.info("pandas package will significantly speed up this instrument")
self.logger.info("to install it try: pip install pandas")
self.attributes_per_sample = 3
self.bytes_per_sample = self.attributes_per_sample * 4
self.attributes = ['power', 'voltage', 'current']
self.command = None
self.raw_output_directory = None
self.process = None
self.sample_rate_hz = 10000 # Determined empirically
for label in self.labels:
for kind in self.attributes:
self.add_channel(label, kind)
def reset(self, sites=None, kinds=None):
super(EnergyProbeInstrument, self).reset(sites, kinds)
def reset(self, sites=None, kinds=None, channels=None):
super(EnergyProbeInstrument, self).reset(sites, kinds, channels)
self.raw_output_directory = tempfile.mkdtemp(prefix='eprobe-caiman-')
parts = ['-r {}:{} '.format(i, int(1000 * rval))
for i, rval in enumerate(self.resistor_values)]
@@ -82,6 +75,13 @@ class EnergyProbeInstrument(Instrument):
shell=True)
def stop(self):
self.process.poll()
if self.process.returncode is not None:
stdout, stderr = self.process.communicate()
raise HostError(
'Energy Probe: Caiman exited unexpectedly with exit code {}.\n'
'stdout:\n{}\nstderr:\n{}'.format(self.process.returncode,
stdout, stderr))
os.killpg(self.process.pid, signal.SIGTERM)
def get_data(self, outfile): # pylint: disable=R0914

View File

@@ -0,0 +1,132 @@
import csv
import os
import signal
from subprocess import Popen, PIPE
from tempfile import NamedTemporaryFile
from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
from devlib.exception import HostError
from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.utils.misc import which
INSTALL_INSTRUCTIONS="""
MonsoonInstrument requires the monsoon.py tool, available from AOSP:
https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py
Download this script and put it in your $PATH (or pass it as the monsoon_bin
parameter to MonsoonInstrument). `pip install gflags pyserial` to install the
dependencies.
"""
class MonsoonInstrument(Instrument):
"""Instrument for Monsoon Solutions power monitor
To use this instrument, you need to install the monsoon.py script available
from the Android Open Source Project. As of May 2017 this is under the CTS
repository:
https://android.googlesource.com/platform/cts/+/master/tools/utils/monsoon.py
Collects power measurements only, from a selection of two channels, the USB
passthrough channel and the main output channel.
:param target: Ignored
:param monsoon_bin: Path to monsoon.py executable. If not provided,
``$PATH`` is searched.
:param tty_device: TTY device to use to communicate with the Power
Monitor. If not provided, a sane default is used.
"""
mode = CONTINUOUS
def __init__(self, target, monsoon_bin=None, tty_device=None):
super(MonsoonInstrument, self).__init__(target)
self.monsoon_bin = monsoon_bin or which('monsoon.py')
if not self.monsoon_bin:
raise HostError(INSTALL_INSTRUCTIONS)
self.tty_device = tty_device
self.process = None
self.output = None
self.sample_rate_hz = 500
self.add_channel('output', 'power')
self.add_channel('USB', 'power')
def reset(self, sites=None, kinds=None, channels=None):
super(MonsoonInstrument, self).reset(sites, kinds)
def start(self):
if self.process:
self.process.kill()
cmd = [self.monsoon_bin,
'--hz', str(self.sample_rate_hz),
'--samples', '-1', # -1 means sample indefinitely
'--includeusb']
if self.tty_device:
cmd += ['--device', self.tty_device]
self.logger.debug(' '.join(cmd))
self.buffer_file = NamedTemporaryFile(prefix='monsoon', delete=False)
self.process = Popen(cmd, stdout=self.buffer_file, stderr=PIPE)
def stop(self):
process = self.process
self.process = None
if not process:
raise RuntimeError('Monsoon script not started')
process.poll()
if process.returncode is not None:
stdout, stderr = process.communicate()
raise HostError(
'Monsoon script exited unexpectedly with exit code {}.\n'
'stdout:\n{}\nstderr:\n{}'.format(process.returncode,
stdout, stderr))
process.send_signal(signal.SIGINT)
stderr = process.stderr.read()
self.buffer_file.close()
with open(self.buffer_file.name) as f:
stdout = f.read()
os.remove(self.buffer_file.name)
self.buffer_file = None
self.output = (stdout, stderr)
def get_data(self, outfile):
if self.process:
raise RuntimeError('`get_data` called before `stop`')
stdout, stderr = self.output
with open(outfile, 'wb') as f:
writer = csv.writer(f)
active_sites = [c.site for c in self.active_channels]
# Write column headers
row = []
if 'output' in active_sites:
row.append('output_power')
if 'USB' in active_sites:
row.append('USB_power')
writer.writerow(row)
# Write data
for line in stdout.splitlines():
# Each output line is a main_output, usb_output measurement pair.
# (If our user only requested one channel we still collect both,
# and just ignore one of them)
output, usb = line.split()
row = []
if 'output' in active_sites:
row.append(output)
if 'USB' in active_sites:
row.append(usb)
writer.writerow(row)
return MeasurementsCsv(outfile, self.active_channels)

View File

@@ -98,8 +98,8 @@ class NetstatsInstrument(Instrument):
self.logger.debug('Deploying {} to target'.format(self.package))
self.target.install(self.apk)
def reset(self, sites=None, kinds=None, period=None): # pylint: disable=arguments-differ
super(NetstatsInstrument, self).reset(sites, kinds)
def reset(self, sites=None, kinds=None, channels=None, period=None): # pylint: disable=arguments-differ
super(NetstatsInstrument, self).reset(sites, kinds, channels)
period_arg, packages_arg = '', ''
self.tag = 'netstats-{}'.format(datetime.now().strftime('%Y%m%d%H%M%s'))
tag_arg = ' --es tag {}'.format(self.tag)

View File

@@ -24,29 +24,33 @@ from devlib.utils.types import boolean
class Controller(object):
def __new__(cls, arg):
if isinstance(arg, cls):
return arg
else:
return object.__new__(cls, arg)
def __init__(self, kind, hid, clist):
"""
Initialize a controller given the hierarchy it belongs to.
def __init__(self, kind):
self.mount_name = 'devlib_'+kind
:param kind: the name of the controller
:type kind: str
:param hid: the Hierarchy ID this controller is mounted on
:type hid: int
:param clist: the list of controller mounted in the same hierarchy
:type clist: list(str)
"""
self.mount_name = 'devlib_cgh{}'.format(hid)
self.kind = kind
self.hid = hid
self.clist = clist
self.target = None
self._noprefix = False
self.logger = logging.getLogger('CGroup.'+self.kind)
self.logger.debug('Initialized [%s, %d, %s]',
self.kind, self.hid, self.clist)
self.logger = logging.getLogger('cgroups.'+self.kind)
self.mount_point = None
self._cgroups = {}
def probe(self, target):
try:
exists = target.execute('{} grep {} /proc/cgroups'\
.format(target.busybox, self.kind))
except TargetError:
return False
return True
def mount(self, target, mount_root):
mounted = target.list_file_systems()
@@ -63,13 +67,20 @@ class Controller(object):
target.execute('mkdir -p {} 2>/dev/null'\
.format(self.mount_point), as_root=True)
target.execute('mount -t cgroup -o {} {} {}'\
.format(self.kind,
.format(','.join(self.clist),
self.mount_name,
self.mount_point),
as_root=True)
self.logger.info('Controller %s mounted under: %s',
self.kind, self.mount_point)
# Check if this controller uses "noprefix" option
output = target.execute('mount | grep "{} "'.format(self.mount_name))
if 'noprefix' in output:
self._noprefix = True
# self.logger.debug('Controller %s using "noprefix" option',
# self.kind)
self.logger.debug('Controller %s mounted under: %s (noprefix=%s)',
self.kind, self.mount_point, self._noprefix)
# Mark this contoller as available
self.target = target
@@ -96,9 +107,10 @@ class Controller(object):
def list_all(self):
self.logger.debug('Listing groups for %s controller', self.kind)
output = self.target.execute('{} find {} -type d'\
.format(self.target.busybox, self.mount_point))
.format(self.target.busybox, self.mount_point),
as_root=True)
cgroups = []
for cg in output.split('\n'):
for cg in output.splitlines():
cg = cg.replace(self.mount_point + '/', '/')
cg = cg.replace(self.mount_point, '/')
cg = cg.strip()
@@ -108,24 +120,92 @@ class Controller(object):
cgroups.append(cg)
return cgroups
def move_tasks(self, source, dest):
def move_tasks(self, source, dest, exclude=[]):
try:
srcg = self._cgroups[source]
dstg = self._cgroups[dest]
command = 'for task in $(cat {}); do echo $task>{}; done'
self.target.execute(command.format(srcg.tasks_file, dstg.tasks_file),
# this will always fail as some of the tasks
# are kthreads that cannot be migrated, but we
# don't care about those, so don't check exit
# code.
check_exit_code=False, as_root=True)
except KeyError as e:
raise ValueError('Unkown group: {}'.format(e))
output = self.target._execute_util(
'cgroups_tasks_move {} {} \'{}\''.format(
srcg.directory, dstg.directory, exclude),
as_root=True)
def move_all_tasks_to(self, dest, exclude=[]):
"""
Move all the tasks to the specified CGroup
Tasks are moved from all their original CGroup the the specified on.
The tasks which name matches one of the string in exclude are moved
instead in the root CGroup for the controller.
The name of a tasks to exclude must be a substring of the task named as
reported by the "ps" command. Indeed, this list will be translated into
a: "ps | grep -e name1 -e name2..." in order to obtain the PID of these
tasks.
:param exclude: list of commands to keep in the root CGroup
:type exlude: list(str)
"""
if isinstance(exclude, str):
exclude = [exclude]
if not isinstance(exclude, list):
raise ValueError('wrong type for "exclude" parameter, '
'it must be a str or a list')
logging.debug('Moving all tasks into %s', dest)
# Build list of tasks to exclude
grep_filters = ''
for comm in exclude:
grep_filters += '-e {} '.format(comm)
logging.debug(' using grep filter: %s', grep_filters)
if grep_filters != '':
logging.debug(' excluding tasks which name matches:')
logging.debug(' %s', ', '.join(exclude))
def move_all_tasks_to(self, dest):
for cgroup in self._cgroups:
if cgroup != dest:
self.move_tasks(cgroup, dest)
self.move_tasks(cgroup, dest, grep_filters)
def tasks(self, cgroup):
try:
cg = self._cgroups[cgroup]
except KeyError as e:
raise ValueError('Unkown group: {}'.format(e))
output = self.target._execute_util(
'cgroups_tasks_in {}'.format(cg.directory),
as_root=True)
entries = output.splitlines()
tasks = {}
for task in entries:
tid = task.split(',')[0]
try:
tname = task.split(',')[1]
except: continue
try:
tcmdline = task.split(',')[2]
except:
tcmdline = ''
tasks[int(tid)] = (tname, tcmdline)
return tasks
def tasks_count(self, cgroup):
try:
cg = self._cgroups[cgroup]
except KeyError as e:
raise ValueError('Unkown group: {}'.format(e))
output = self.target.execute(
'{} wc -l {}/tasks'.format(
self.target.busybox, cg.directory),
as_root=True)
return int(output.split()[0])
def tasks_per_group(self):
tasks = {}
for cg in self.list_all():
tasks[cg] = self.tasks_count(cg)
return tasks
class CGroup(object):
@@ -147,14 +227,14 @@ class CGroup(object):
if not create:
return
self.logger.info('Creating cgroup %s', self.directory)
self.logger.debug('Creating cgroup %s', self.directory)
self.target.execute('[ -d {0} ] || mkdir -p {0}'\
.format(self.directory), as_root=True)
def exists(self):
try:
self.target.execute('[ -d {0} ]'\
.format(self.directory))
.format(self.directory), as_root=True)
return True
except TargetError:
return False
@@ -166,14 +246,11 @@ class CGroup(object):
self.controller.kind)
logging.debug(' %s',
self.directory)
output = self.target.execute('{} grep \'\' {}/{}.*'.format(
self.target.busybox,
self.directory,
self.controller.kind))
for res in output.split('\n'):
if res.find(self.controller.kind) < 0:
continue
res = res.split('.')[1]
output = self.target._execute_util(
'cgroups_get_attributes {} {}'.format(
self.directory, self.controller.kind),
as_root=True)
for res in output.splitlines():
attr = res.split(':')[0]
value = res.split(':')[1]
conf[attr] = value
@@ -185,14 +262,25 @@ class CGroup(object):
if isiterable(attrs[idx]):
attrs[idx] = list_to_ranges(attrs[idx])
# Build attribute path
path = '{}.{}'.format(self.controller.kind, idx)
path = self.target.path.join(self.directory, path)
if self.controller._noprefix:
attr_name = '{}'.format(idx)
else:
attr_name = '{}.{}'.format(self.controller.kind, idx)
path = self.target.path.join(self.directory, attr_name)
self.logger.debug('Set attribute [%s] to: %s"',
path, attrs[idx])
# Set the attribute value
self.target.write_value(path, attrs[idx])
try:
self.target.write_value(path, attrs[idx])
except TargetError:
# Check if the error is due to a non-existing attribute
attrs = self.get()
if idx not in attrs:
raise ValueError('Controller [{}] does not provide attribute [{}]'\
.format(self.controller.kind, attr_name))
raise
def get_tasks(self):
task_ids = self.target.read_value(self.tasks_file).split()
@@ -214,54 +302,59 @@ CgroupSubsystemEntry = namedtuple('CgroupSubsystemEntry', 'name hierarchy num_cg
class CgroupsModule(Module):
name = 'cgroups'
cgroup_root = '/sys/fs/cgroup'
stage = 'setup'
@staticmethod
def probe(target):
return target.config.has('cgroups') and target.is_rooted
if not target.is_rooted:
return False
if target.file_exists('/proc/cgroups'):
return True
return target.config.has('cgroups')
def __init__(self, target):
super(CgroupsModule, self).__init__(target)
self.logger = logging.getLogger('CGroups')
# Initialize controllers mount point
mounted = self.target.list_file_systems()
if self.cgroup_root not in [e.mount_point for e in mounted]:
self.target.execute('mount -t tmpfs {} {}'\
.format('cgroup_root',
self.cgroup_root),
as_root=True)
else:
self.logger.debug('cgroup_root already mounted at %s',
self.cgroup_root)
# Set Devlib's CGroups mount point
self.cgroup_root = target.path.join(
target.working_directory, 'cgroups')
# Load list of available controllers
controllers = []
# Get the list of the available controllers
subsys = self.list_subsystems()
for (n, h, c, e) in subsys:
controllers.append(n)
self.logger.info('Available controllers: %s', controllers)
if len(subsys) == 0:
self.logger.warning('No CGroups controller available')
return
# Map hierarchy IDs into a list of controllers
hierarchy = {}
for ss in subsys:
try:
hierarchy[ss.hierarchy].append(ss.name)
except KeyError:
hierarchy[ss.hierarchy] = [ss.name]
self.logger.debug('Available hierarchies: %s', hierarchy)
# Initialize controllers
self.logger.info('Available controllers:')
self.controllers = {}
for idx in controllers:
controller = Controller(idx)
self.logger.debug('Init %s controller...', controller.kind)
if not controller.probe(self.target):
continue
for ss in subsys:
hid = ss.hierarchy
controller = Controller(ss.name, hid, hierarchy[hid])
try:
controller.mount(self.target, self.cgroup_root)
except TargetError:
message = 'cgroups {} controller is not supported by the target'
message = 'Failed to mount "{}" controller'
raise TargetError(message.format(controller.kind))
self.logger.debug('Controller %s enabled', controller.kind)
self.controllers[idx] = controller
self.logger.info(' %-12s : %s', controller.kind,
controller.mount_point)
self.controllers[ss.name] = controller
def list_subsystems(self):
subsystems = []
for line in self.target.execute('{} cat /proc/cgroups'\
.format(self.target.busybox)).split('\n')[1:]:
.format(self.target.busybox)).splitlines()[1:]:
line = line.strip()
if not line or line.startswith('#'):
continue
@@ -279,3 +372,117 @@ class CgroupsModule(Module):
return None
return self.controllers[kind]
def run_into_cmd(self, cgroup, cmdline):
"""
Get the command to run a command into a given cgroup
:param cmdline: Commdand to be run into cgroup
:param cgroup: Name of cgroup to run command into
:returns: A command to run `cmdline` into `cgroup`
"""
return 'CGMOUNT={} {} cgroups_run_into {} {}'\
.format(self.cgroup_root, self.target.shutils,
cgroup, cmdline)
def run_into(self, cgroup, cmdline):
"""
Run the specified command into the specified CGroup
:param cmdline: Command to be run into cgroup
:param cgroup: Name of cgroup to run command into
:returns: Output of command.
"""
cmd = self.run_into_cmd(cgroup, cmdline)
raw_output = self.target.execute(cmd)
# First line of output comes from shutils; strip it out.
return raw_output.split('\n', 1)[1]
def cgroups_tasks_move(self, srcg, dstg, exclude=''):
"""
Move all the tasks from the srcg CGroup to the dstg one.
A regexps of tasks names can be used to defined tasks which should not
be moved.
"""
return self.target._execute_util(
'cgroups_tasks_move {} {} {}'.format(srcg, dstg, exclude),
as_root=True)
def isolate(self, cpus, exclude=[]):
"""
Remove all userspace tasks from specified CPUs.
A list of CPUs can be specified where we do not want userspace tasks
running. This functions creates a sandbox cpuset CGroup where all
user-space tasks and not-pinned kernel-space tasks are moved into.
This should allows to isolate the specified CPUs which will not get
tasks running unless explicitely moved into the isolated group.
:param cpus: the list of CPUs to isolate
:type cpus: list(int)
:return: the (sandbox, isolated) tuple, where:
sandbox is the CGroup of sandboxed CPUs
isolated is the CGroup of isolated CPUs
"""
all_cpus = set(range(self.target.number_of_cpus))
sbox_cpus = list(all_cpus - set(cpus))
isol_cpus = list(all_cpus - set(sbox_cpus))
# Create Sandbox and Isolated cpuset CGroups
cpuset = self.controller('cpuset')
sbox_cg = cpuset.cgroup('/DEVLIB_SBOX')
isol_cg = cpuset.cgroup('/DEVLIB_ISOL')
# Set CPUs for Sandbox and Isolated CGroups
sbox_cg.set(cpus=sbox_cpus, mems=0)
isol_cg.set(cpus=isol_cpus, mems=0)
# Move all currently running tasks to the Sandbox CGroup
cpuset.move_all_tasks_to('/DEVLIB_SBOX', exclude)
return sbox_cg, isol_cg
def freeze(self, exclude=[], thaw=False):
"""
Freeze all user-space tasks but the specified ones
A freezer cgroup is used to stop all the tasks in the target system but
the ones which name match one of the path specified by the exclude
paramater. The name of a tasks to exclude must be a substring of the
task named as reported by the "ps" command. Indeed, this list will be
translated into a: "ps | grep -e name1 -e name2..." in order to obtain
the PID of these tasks.
:param exclude: list of commands paths to exclude from freezer
:type exclude: list(str)
:param thaw: if true thaw tasks instead
:type thaw: bool
"""
# Create Freezer CGroup
freezer = self.controller('freezer')
if freezer is None:
raise RuntimeError('freezer cgroup controller not present')
freezer_cg = freezer.cgroup('/DEVLIB_FREEZER')
thawed_cg = freezer.cgroup('/')
if thaw:
# Restart froozen tasks
freezer_cg.set(state='THAWED')
# Remove all tasks from freezer
freezer.move_all_tasks_to('/')
return
# Move all tasks into the freezer group
freezer.move_all_tasks_to('/DEVLIB_FREEZER', exclude)
# Get list of not frozen tasks, which is reported as output
tasks = freezer.tasks('/')
# Freeze all tasks
freezer_cg.set(state='FROZEN')
return tasks

View File

@@ -133,7 +133,7 @@ class CpufreqModule(Module):
keyword arguments. Which tunables and values are valid depends on the
governor.
:param cpu: The cpu for which the governor will be set. This must be the
:param cpu: The cpu for which the governor will be set. ``int`` or
full cpu name as it appears in sysfs, e.g. ``cpu0``.
:param governor: The name of the governor. Must be all lower case.
@@ -152,10 +152,14 @@ class CpufreqModule(Module):
valid_tunables = self.list_governor_tunables(cpu)
for tunable, value in kwargs.iteritems():
if tunable in valid_tunables:
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
try:
path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
self.target.write_value(path, value)
except TargetError: # May be an older kernel
except TargetError:
if self.target.file_exists(path):
# File exists but we did something wrong
raise
# Expected file doesn't exist, try older sysfs layout.
path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
self.target.write_value(path, value)
else:
@@ -334,9 +338,8 @@ class CpufreqModule(Module):
:param cpus: The list of CPU for which the governor is to be set.
"""
online_cpus = self.target.list_online_cpus()
for cpu in online_cpus:
self.set_governor(cpu, governor, kwargs)
for cpu in cpus:
self.set_governor(cpu, governor, **kwargs)
def set_frequency_for_cpus(self, cpus, freq, exact=False):
"""
@@ -345,21 +348,76 @@ class CpufreqModule(Module):
:param cpus: The list of CPU for which the frequency has to be set.
"""
online_cpus = self.target.list_online_cpus()
for cpu in online_cpus:
for cpu in cpus:
self.set_frequency(cpu, freq, exact)
def set_all_frequencies(self, freq, exact=False):
self.target.execute(
"for CPU in /sys/devices/system/cpu/cpu[0-9]*; do "\
"echo {} > $CPU/cpufreq/scaling_cur_freq; "\
"done"\
.format(freq), as_root=True)
def set_all_frequencies(self, freq):
"""
Set the specified (minimum) frequency for all the (online) CPUs
"""
return self.target._execute_util(
'cpufreq_set_all_frequencies {}'.format(freq),
as_root=True)
def get_all_frequencies(self):
"""
Get the current frequency for all the (online) CPUs
"""
output = self.target._execute_util(
'cpufreq_get_all_frequencies', as_root=True)
frequencies = {}
for x in output.splitlines():
kv = x.split(' ')
if kv[0] == '':
break
frequencies[kv[0]] = kv[1]
return frequencies
def set_all_governors(self, governor):
self.target.execute(
"for CPU in /sys/devices/system/cpu/cpu[0-9]*; do "\
"echo {} > $CPU/cpufreq/scaling_governor; "\
"done"\
.format(governor), as_root=True)
"""
Set the specified governor for all the (online) CPUs
"""
try:
return self.target._execute_util(
'cpufreq_set_all_governors {}'.format(governor),
as_root=True)
except TargetError as e:
if "echo: I/O error" in str(e):
cpus_unsupported = [c for c in self.target.list_online_cpus()
if governor not in self.list_governors(c)]
raise TargetError("Governor {} unsupported for CPUs {}".format(
governor, cpus_unsupported))
else:
raise
def get_all_governors(self):
"""
Get the current governor for all the (online) CPUs
"""
output = self.target._execute_util(
'cpufreq_get_all_governors', as_root=True)
governors = {}
for x in output.splitlines():
kv = x.split(' ')
if kv[0] == '':
break
governors[kv[0]] = kv[1]
return governors
def trace_frequencies(self):
"""
Report current frequencies on trace file
"""
return self.target._execute_util('cpufreq_trace_all_frequencies', as_root=True)
@memoized
def get_domain_cpus(self, cpu):
"""
Get the CPUs that share a frequency domain with the given CPU
"""
if isinstance(cpu, int):
cpu = 'cpu{}'.format(cpu)
sysfile = '/sys/devices/system/cpu/{}/cpufreq/affected_cpus'.format(cpu)
return [int(c) for c in self.target.read_value(sysfile).split()]

View File

@@ -47,10 +47,44 @@ class CpuidleState(object):
self.path = path
self.id = self.target.path.basename(self.path)
self.cpu = self.target.path.basename(self.target.path.dirname(path))
self.desc = self.get('desc')
self.name = self.get('name')
self.latency = self.get('latency')
self.power = self.get('power')
@property
@memoized
def desc(self):
return self.get('desc')
@property
@memoized
def name(self):
return self.get('name')
@property
@memoized
def latency(self):
"""Exit latency in uS"""
return self.get('latency')
@property
@memoized
def power(self):
"""Power usage in mW
..note::
This value is not always populated by the kernel and may be garbage.
"""
return self.get('power')
@property
@memoized
def target_residency(self):
"""Target residency in uS
This is the amount of time in the state required to 'break even' on
power - the system should avoid entering the state for less time than
this.
"""
return self.get('residency')
def enable(self):
self.set('disable', 0)
@@ -113,7 +147,7 @@ class Cpuidle(Module):
def get_state(self, state, cpu=0):
if isinstance(state, int):
try:
self.get_states(cpu)[state].enable()
return self.get_states(cpu)[state]
except IndexError:
raise ValueError('Cpuidle state {} does not exist'.format(state))
else: # assume string-like
@@ -136,3 +170,9 @@ class Cpuidle(Module):
for state in self.get_states(cpu):
state.disable()
def perturb_cpus(self):
"""
Momentarily wake each CPU. Ensures cpu_idle events in trace file.
"""
output = self.target._execute_util('cpuidle_wake_all_cpus')
print(output)

View File

@@ -85,7 +85,8 @@ class HwmonDevice(object):
path = self.path
if not path.endswith(self.target.path.sep):
path += self.target.path.sep
for entry in self.target.list_directory(path):
for entry in self.target.list_directory(path,
as_root=self.target.is_rooted):
match = HWMON_FILE_REGEX.search(entry)
if match:
kind = match.group('kind')
@@ -131,7 +132,8 @@ class HwmonModule(Module):
self.scan()
def scan(self):
for entry in self.target.list_directory(self.root):
for entry in self.target.list_directory(self.root,
as_root=self.target.is_rooted):
if entry.startswith('hwmon'):
entry_path = self.target.path.join(self.root, entry)
if self.target.file_exists(self.target.path.join(entry_path, 'name')):

104
devlib/module/thermal.py Normal file
View File

@@ -0,0 +1,104 @@
# Copyright 2015 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from devlib.module import Module
class TripPoint(object):
def __init__(self, zone, _id):
self._id = _id
self.zone = zone
self.temp_node = 'trip_point_' + _id + '_temp'
self.type_node = 'trip_point_' + _id + '_type'
@property
def target(self):
return self.zone.target
def get_temperature(self):
"""Returns the currently configured temperature of the trip point"""
temp_file = self.target.path.join(self.zone.path, self.temp_node)
return self.target.read_int(temp_file)
def set_temperature(self, temperature):
temp_file = self.target.path.join(self.zone.path, self.temp_node)
self.target.write_value(temp_file, temperature)
def get_type(self):
"""Returns the type of trip point"""
type_file = self.target.path.join(self.zone.path, self.type_node)
return self.target.read_value(type_file)
class ThermalZone(object):
def __init__(self, target, root, _id):
self.target = target
self.name = 'thermal_zone' + _id
self.path = target.path.join(root, self.name)
self.trip_points = {}
for entry in self.target.list_directory(self.path):
re_match = re.match('^trip_point_([0-9]+)_temp', entry)
if re_match is not None:
self.add_trip_point(re_match.group(1))
def add_trip_point(self, _id):
self.trip_points[int(_id)] = TripPoint(self, _id)
def is_enabled(self):
"""Returns a boolean representing the 'mode' of the thermal zone"""
value = self.target.read_value(self.target.path.join(self.path, 'mode'))
return value == 'enabled'
def set_mode(self, enable):
value = 'enabled' if enable else 'disabled'
self.target.write_value(self.target.path.join(self.path, 'mode'), value)
def get_temperature(self):
"""Returns the temperature of the thermal zone"""
temp_file = self.target.path.join(self.path, 'temp')
return self.target.read_int(temp_file)
class ThermalModule(Module):
name = 'thermal'
thermal_root = '/sys/class/thermal'
@staticmethod
def probe(target):
if target.file_exists(ThermalModule.thermal_root):
return True
def __init__(self, target):
super(ThermalModule, self).__init__(target)
self.zones = {}
self.cdevs = []
for entry in target.list_directory(self.thermal_root):
re_match = re.match('^(thermal_zone|cooling_device)([0-9]+)', entry)
if re_match.group(1) == 'thermal_zone':
self.add_thermal_zone(re_match.group(2))
elif re_match.group(1) == 'cooling_device':
# TODO
pass
def add_thermal_zone(self, _id):
self.zones[int(_id)] = ThermalZone(self.target, self.thermal_root, _id)
def disable_all_zones(self):
"""Disables all the thermal zones in the target"""
for zone in self.zones:
zone.set_mode('disabled')

View File

@@ -1,6 +1,9 @@
import logging
BIG_CPUS = ['A15', 'A57', 'A72']
class Platform(object):
@property
@@ -25,7 +28,6 @@ class Platform(object):
self.logger = logging.getLogger(self.name)
if not self.core_clusters and self.core_names:
self._set_core_clusters_from_core_names()
self._validate()
def init_target_connection(self, target):
# May be ovewritten by subclasses to provide target-specific
@@ -37,8 +39,7 @@ class Platform(object):
self.core_names = target.cpuinfo.cpu_names
self._set_core_clusters_from_core_names()
if not self.big_core and self.number_of_clusters == 2:
big_idx = self.core_clusters.index(max(self.core_clusters))
self.big_core = self.core_names[big_idx]
self.big_core = self._identify_big_core()
if not self.core_clusters and self.core_names:
self._set_core_clusters_from_core_names()
if not self.model:
@@ -47,6 +48,11 @@ class Platform(object):
self.name = self.model
self._validate()
def setup(self, target):
# May be overwritten by subclasses to provide platform-specific
# setup procedures.
pass
def _set_core_clusters_from_core_names(self):
self.core_clusters = []
clusters = []
@@ -65,6 +71,13 @@ class Platform(object):
except Exception: # pylint: disable=broad-except
pass # this is best-effort
def _identify_big_core(self):
for core in self.core_names:
if core.upper() in BIG_CPUS:
return core
big_idx = self.core_clusters.index(max(self.core_clusters))
return self.core_names[big_idx]
def _validate(self):
if len(self.core_names) != len(self.core_clusters):
raise ValueError('core_names and core_clusters are of different lengths.')
@@ -76,6 +89,7 @@ class Platform(object):
raise ValueError(message.format(self.big_core,
', '.join(set(self.core_names))))
if self.big_core:
little_idx = self.core_clusters.index(min(self.core_clusters))
self.little_core = self.core_names[little_idx]
for core in self.core_names:
if core != self.big_core:
self.little_core = core
break

View File

@@ -20,7 +20,7 @@ import time
import pexpect
from devlib.platform import Platform
from devlib.instrument import Instrument, InstrumentChannel, MeasurementsCsv, CONTINUOUS
from devlib.instrument import Instrument, InstrumentChannel, MeasurementsCsv, Measurement, CONTINUOUS, INSTANTANEOUS
from devlib.exception import TargetError, HostError
from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.utils.serial_port import open_serial_connection
@@ -145,9 +145,12 @@ class VersatileExpressPlatform(Platform):
'bootargs': self.bootargs,
}})
elif self.bootloader == 'u-boot':
uboot_env = None
if self.bootargs:
uboot_env = {'bootargs': self.bootargs}
self.modules.append({'vexpress-u-boot': {'port': self.serial_port,
'baudrate': self.baudrate,
'env': {'bootargs': self.bootargs},
'env': uboot_env,
}})
elif self.bootloader == 'bootmon':
self.modules.append({'vexpress-bootmon': {'port': self.serial_port,
@@ -204,7 +207,7 @@ class TC2(VersatileExpressPlatform):
class JunoEnergyInstrument(Instrument):
binname = 'readenergy'
mode = CONTINUOUS
mode = CONTINUOUS | INSTANTANEOUS
_channels = [
InstrumentChannel('sys_curr', 'sys', 'current'),
@@ -233,7 +236,9 @@ class JunoEnergyInstrument(Instrument):
for chan in self._channels:
self.channels[chan.name] = chan
self.on_target_file = self.target.tempfile('energy', '.csv')
self.sample_rate_hz = 10 # DEFAULT_PERIOD is 100[ms] in readenergy.c
self.command = '{} -o {}'.format(self.binary, self.on_target_file)
self.command2 = '{}'.format(self.binary)
def setup(self):
self.binary = self.target.install(os.path.join(PACKAGE_BIN_DIRECTORY,
@@ -277,4 +282,14 @@ class JunoEnergyInstrument(Instrument):
return MeasurementsCsv(output_file, self.active_channels)
def take_measurement(self):
result = []
output = self.target.execute(self.command2).split()
reader=csv.reader(output)
headings=reader.next()
values = reader.next()
for chan in self.active_channels:
value = values[headings.index(chan.name)]
result.append(Measurement(value, chan))
return result

292
devlib/platform/gem5.py Normal file
View File

@@ -0,0 +1,292 @@
# Copyright 2016 ARM Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
import subprocess
import sys
import shutil
import time
import types
from devlib.exception import TargetError
from devlib.host import PACKAGE_BIN_DIRECTORY
from devlib.platform import Platform
from devlib.utils.ssh import AndroidGem5Connection, LinuxGem5Connection
class Gem5SimulationPlatform(Platform):
def __init__(self, name,
host_output_dir,
gem5_bin,
gem5_args,
gem5_virtio,
core_names=None,
core_clusters=None,
big_core=None,
model=None,
modules=None,
gem5_telnet_port=None):
# First call the parent class
super(Gem5SimulationPlatform, self).__init__(name, core_names, core_clusters,
big_core, model, modules)
# Start setting up the gem5 parameters/directories
# The gem5 subprocess
self.gem5 = None
self.gem5_port = gem5_telnet_port or None
self.stats_directory = host_output_dir
self.gem5_out_dir = os.path.join(self.stats_directory, "gem5")
self.gem5_interact_dir = '/tmp' # Host directory
self.executable_dir = None # Device directory
self.working_dir = None # Device directory
self.stdout_file = None
self.stderr_file = None
self.stderr_filename = None
if self.gem5_port is None:
# Allows devlib to pick up already running simulations
self.start_gem5_simulation = True
else:
self.start_gem5_simulation = False
# Find the first one that does not exist. Ensures that we do not re-use
# the directory used by someone else.
for i in xrange(sys.maxint):
directory = os.path.join(self.gem5_interact_dir, "wa_{}".format(i))
try:
os.stat(directory)
continue
except OSError:
break
self.gem5_interact_dir = directory
self.logger.debug("Using {} as the temporary directory."
.format(self.gem5_interact_dir))
# Parameters passed onto gem5
self.gem5args_binary = gem5_bin
self.gem5args_args = gem5_args
self.gem5args_virtio = gem5_virtio
self._check_gem5_command()
# Start the interaction with gem5
self._start_interaction_gem5()
def _check_gem5_command(self):
"""
Check if the command to start gem5 makes sense
"""
if self.gem5args_binary is None:
raise TargetError('Please specify a gem5 binary.')
if self.gem5args_args is None:
raise TargetError('Please specify the arguments passed on to gem5.')
self.gem5args_virtio = str(self.gem5args_virtio).format(self.gem5_interact_dir)
if self.gem5args_virtio is None:
raise TargetError('Please specify arguments needed for virtIO.')
def _start_interaction_gem5(self):
"""
Starts the interaction of devlib with gem5.
"""
# First create the input and output directories for gem5
if self.start_gem5_simulation:
# Create the directory to send data to/from gem5 system
self.logger.info("Creating temporary directory for interaction "
" with gem5 via virtIO: {}"
.format(self.gem5_interact_dir))
os.mkdir(self.gem5_interact_dir)
# Create the directory for gem5 output (stats files etc)
if not os.path.exists(self.stats_directory):
os.mkdir(self.stats_directory)
if os.path.exists(self.gem5_out_dir):
raise TargetError("The gem5 stats directory {} already "
"exists.".format(self.gem5_out_dir))
else:
os.mkdir(self.gem5_out_dir)
# We need to redirect the standard output and standard error for the
# gem5 process to a file so that we can debug when things go wrong.
f = os.path.join(self.gem5_out_dir, 'stdout')
self.stdout_file = open(f, 'w')
f = os.path.join(self.gem5_out_dir, 'stderr')
self.stderr_file = open(f, 'w')
# We need to keep this so we can check which port to use for the
# telnet connection.
self.stderr_filename = f
# Start gem5 simulation
self.logger.info("Starting the gem5 simulator")
command_line = "{} --outdir={} {} {}".format(self.gem5args_binary,
self.gem5_out_dir,
self.gem5args_args,
self.gem5args_virtio)
self.logger.debug("gem5 command line: {}".format(command_line))
self.gem5 = subprocess.Popen(command_line.split(),
stdout=self.stdout_file,
stderr=self.stderr_file)
else:
# The simulation should already be running
# Need to dig up the (1) gem5 simulation in question (2) its input
# and output directories (3) virtio setting
self._intercept_existing_gem5()
# As the gem5 simulation is running now or was already running
# we now need to find out which telnet port it uses
self._intercept_telnet_port()
def _intercept_existing_gem5(self):
"""
Intercept the information about a running gem5 simulation
e.g. pid, input directory etc
"""
self.logger("This functionality is not yet implemented")
raise TargetError()
def _intercept_telnet_port(self):
"""
Intercept the telnet port of a running gem5 simulation
"""
if self.gem5 is None:
raise TargetError('The platform has no gem5 simulation! '
'Something went wrong')
while self.gem5_port is None:
# Check that gem5 is running!
if self.gem5.poll():
raise TargetError("The gem5 process has crashed with error code {}!".format(self.gem5.poll()))
# Open the stderr file
with open(self.stderr_filename, 'r') as f:
for line in f:
m = re.search(r"Listening for system connection on port (?P<port>\d+)", line)
if m:
port = int(m.group('port'))
if port >= 3456 and port < 5900:
self.gem5_port = port
break
# Check if the sockets are not disabled
m = re.search(r"Sockets disabled, not accepting terminal connections", line)
if m:
raise TargetError("The sockets have been disabled!"
"Pass --listener-mode=on to gem5")
else:
time.sleep(1)
def init_target_connection(self, target):
"""
Update the type of connection in the target from here
"""
if target.os == 'linux':
target.conn_cls = LinuxGem5Connection
else:
target.conn_cls = AndroidGem5Connection
def setup(self, target):
"""
Deploy m5 if not yet installed
"""
m5_path = target.get_installed('m5')
if m5_path is None:
m5_path = self._deploy_m5(target)
target.conn.m5_path = m5_path
# Set the terminal settings for the connection to gem5
self._resize_shell(target)
def update_from_target(self, target):
"""
Set the m5 path and if not yet installed, deploy m5
Overwrite certain methods in the target that either can be done
more efficiently by gem5 or don't exist in gem5
"""
m5_path = target.get_installed('m5')
if m5_path is None:
m5_path = self._deploy_m5(target)
target.conn.m5_path = m5_path
# Overwrite the following methods (monkey-patching)
self.logger.debug("Overwriting the 'capture_screen' method in target")
# Housekeeping to prevent recursion
setattr(target, 'target_impl_capture_screen', target.capture_screen)
target.capture_screen = types.MethodType(_overwritten_capture_screen, target)
self.logger.debug("Overwriting the 'reset' method in target")
target.reset = types.MethodType(_overwritten_reset, target)
self.logger.debug("Overwriting the 'reboot' method in target")
target.reboot = types.MethodType(_overwritten_reboot, target)
# Call the general update_from_target implementation
super(Gem5SimulationPlatform, self).update_from_target(target)
def gem5_capture_screen(self, filepath):
file_list = os.listdir(self.gem5_out_dir)
screen_caps = []
for f in file_list:
if '.bmp' in f:
screen_caps.append(f)
successful_capture = False
if len(screen_caps) == 1:
# Bail out if we do not have image, and resort to the slower, built
# in method.
try:
import Image
gem5_image = os.path.join(self.gem5_out_dir, screen_caps[0])
temp_image = os.path.join(self.gem5_out_dir, "file.png")
im = Image.open(gem5_image)
im.save(temp_image, "PNG")
shutil.copy(temp_image, filepath)
os.remove(temp_image)
gem5_logger.info("capture_screen: using gem5 screencap")
successful_capture = True
except (shutil.Error, ImportError, IOError):
pass
return successful_capture
def _deploy_m5(self, target):
# m5 is not yet installed so install it
host_executable = os.path.join(PACKAGE_BIN_DIRECTORY,
target.abi, 'm5')
return target.install(host_executable)
def _resize_shell(self, target):
"""
Resize the shell to avoid line wrapping issues.
"""
# Try and avoid line wrapping as much as possible.
target.execute('{} stty columns 1024'.format(target.busybox))
target.execute('reset', check_exit_code=False)
# Methods that will be monkey-patched onto the target
def _overwritten_reset(self):
raise TargetError('Resetting is not allowed on gem5 platforms!')
def _overwritten_reboot(self):
raise TargetError('Rebooting is not allowed on gem5 platforms!')
def _overwritten_capture_screen(self, filepath):
connection_screencapped = self.platform.gem5_capture_screen(filepath)
if connection_screencapped == False:
# The connection was not able to capture the screen so use the target
# implementation
self.logger.debug('{} was not able to screen cap, using the original target implementation'.format(self.platform.__class__.__name__))
self.target_impl_capture_screen(filepath)

View File

@@ -19,18 +19,20 @@ from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list, escape_doub
from devlib.utils.types import integer, boolean, bitmask, identifier, caseless_string
FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (\S+) type (\S+) \((\S+)\)')
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn)=([0-9]+|true|false)',
FSTAB_ENTRY_REGEX = re.compile(r'(\S+) on (.+) type (\S+) \((\S+)\)')
ANDROID_SCREEN_STATE_REGEX = re.compile('(?:mPowerState|mScreenOn|Display Power: state)=([0-9]+|true|false|ON|OFF)',
re.IGNORECASE)
ANDROID_SCREEN_RESOLUTION_REGEX = re.compile(r'mUnrestrictedScreen=\(\d+,\d+\)'
r'\s+(?P<width>\d+)x(?P<height>\d+)')
DEFAULT_SHELL_PROMPT = re.compile(r'^.*(shell|root)@.*:/\S* [#$] ',
re.MULTILINE)
KVERSION_REGEX =re.compile(
r'(?P<version>\d+)(\.(?P<major>\d+)(\.(?P<minor>\d+)(-rc(?P<rc>\d+))?)?)?(.*-g(?P<sha1>[0-9a-fA-F]{7,}))?'
)
class Target(object):
conn_cls = None
path = None
os = None
@@ -63,10 +65,11 @@ class Target(object):
return self.conn is not None
@property
@memoized
def connected_as_root(self):
result = self.execute('id')
return 'uid=0(' in result
if self._connected_as_root is None:
result = self.execute('id')
self._connected_as_root = 'uid=0(' in result
return self._connected_as_root
@property
@memoized
@@ -79,10 +82,15 @@ class Target(object):
except (TargetError, TimeoutError):
return False
@property
@memoized
def needs_su(self):
return not self.connected_as_root and self.is_rooted
@property
@memoized
def kernel_version(self):
return KernelVersion(self.execute('uname -r -v').strip())
return KernelVersion(self.execute('{} uname -r -v'.format(self.busybox)).strip())
@property
def os_version(self): # pylint: disable=no-self-use
@@ -145,14 +153,32 @@ class Target(object):
modules=None,
load_default_modules=True,
shell_prompt=DEFAULT_SHELL_PROMPT,
conn_cls=None,
):
self._connected_as_root = None
self.connection_settings = connection_settings or {}
self.platform = platform or Platform()
# Set self.platform: either it's given directly (by platform argument)
# or it's given in the connection_settings argument
# If neither, create default Platform()
if platform is None:
self.platform = self.connection_settings.get('platform', Platform())
else:
self.platform = platform
# Check if the user hasn't given two different platforms
if 'platform' in self.connection_settings:
if connection_settings['platform'] is not platform:
raise TargetError('Platform specified in connection_settings '
'({}) differs from that directly passed '
'({})!)'
.format(connection_settings['platform'],
self.platform))
self.connection_settings['platform'] = self.platform
self.working_directory = working_directory
self.executables_directory = executables_directory
self.modules = modules or []
self.load_default_modules = load_default_modules
self.shell_prompt = shell_prompt
self.conn_cls = conn_cls
self.logger = logging.getLogger(self.__class__.__name__)
self._installed_binaries = {}
self._installed_modules = {}
@@ -176,6 +202,7 @@ class Target(object):
self.platform.init_target_connection(self)
tid = id(threading.current_thread())
self._connections[tid] = self.get_connection(timeout=timeout)
self._resolve_paths()
self.busybox = self.get_installed('busybox')
self.platform.update_from_target(self)
self._update_modules('connected')
@@ -188,17 +215,39 @@ class Target(object):
self._connections = {}
def get_connection(self, timeout=None):
if self.conn_cls is None:
raise NotImplementedError('conn_cls must be set by the subclass of Target')
if self.conn_cls == None:
raise ValueError('Connection class not specified on Target creation.')
return self.conn_cls(timeout=timeout, **self.connection_settings) # pylint: disable=not-callable
def setup(self, executables=None):
self.execute('mkdir -p {}'.format(self.working_directory))
self.execute('mkdir -p {}'.format(self.executables_directory))
self.busybox = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, self.abi, 'busybox'))
# Setup shutils script for the target
shutils_ifile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils.in')
shutils_ofile = os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils')
shell_path = '/bin/sh'
if self.os == 'android':
shell_path = '/system/bin/sh'
with open(shutils_ifile) as fh:
lines = fh.readlines()
with open(shutils_ofile, 'w') as ofile:
for line in lines:
line = line.replace("__DEVLIB_SHELL__", shell_path)
line = line.replace("__DEVLIB_BUSYBOX__", self.busybox)
ofile.write(line)
self.shutils = self.install(os.path.join(PACKAGE_BIN_DIRECTORY, 'scripts', 'shutils'))
for host_exe in (executables or []): # pylint: disable=superfluous-parens
self.install(host_exe)
# Check for platform dependent setup procedures
self.platform.setup(self)
# Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
self._update_modules('setup')
def reboot(self, hard=False, connect=True, timeout=180):
if hard:
if not self.has('hard_reset'):
@@ -211,8 +260,16 @@ class Target(object):
'(in which case, a hard_reset module must be installed)'
raise TargetError(message)
self.reset()
# Wait a fixed delay before starting polling to give the target time to
# shut down, otherwise, might create the connection while it's still shutting
# down resulting in subsequenct connection failing.
self.logger.debug('Waiting for target to power down...')
reset_delay = 20
time.sleep(reset_delay)
timeout = max(timeout - reset_delay, 10)
if self.has('boot'):
self.boot() # pylint: disable=no-member
self._connected_as_root = None
if connect:
self.connect(timeout=timeout)
@@ -226,6 +283,10 @@ class Target(object):
# execution
def _execute_util(self, command, timeout=None, check_exit_code=True, as_root=False):
command = '{} {}'.format(self.shutils, command)
return self.conn.execute(command, timeout, check_exit_code, as_root)
def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
return self.conn.execute(command, timeout, check_exit_code, as_root)
@@ -252,6 +313,7 @@ class Target(object):
a ``TimeoutError`` exception will be raised. Set to ``None`` if the
invocation should not timeout.
:returns: output of command.
"""
command = binary
if args:
@@ -271,7 +333,7 @@ class Target(object):
# sysfs interaction
def read_value(self, path, kind=None):
output = self.execute('cat \'{}\''.format(path), as_root=self.is_rooted).strip() # pylint: disable=E1103
output = self.execute('cat \'{}\''.format(path), as_root=self.needs_su).strip() # pylint: disable=E1103
if kind:
return kind(output)
else:
@@ -294,10 +356,11 @@ class Target(object):
def reset(self):
try:
self.execute('reboot', as_root=self.is_rooted, timeout=2)
self.execute('reboot', as_root=self.needs_su, timeout=2)
except (TargetError, TimeoutError, subprocess.CalledProcessError):
# on some targets "reboot" doesn't return gracefully
pass
self._connected_as_root = None
def check_responsive(self):
try:
@@ -313,7 +376,10 @@ class Target(object):
def killall(self, process_name, signal=None, as_root=False):
for pid in self.get_pids_of(process_name):
self.kill(pid, signal=signal, as_root=as_root)
try:
self.kill(pid, signal=signal, as_root=as_root)
except TargetError:
pass
def get_pids_of(self, process_name):
raise NotImplementedError()
@@ -325,7 +391,14 @@ class Target(object):
def file_exists(self, filepath):
command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
return boolean(self.execute(command.format(filepath)).strip())
output = self.execute(command.format(filepath), as_root=self.is_rooted)
return boolean(output.strip())
def directory_exists(self, filepath):
output = self.execute('if [ -d \'{}\' ]; then echo 1; else echo 0; fi'.format(filepath))
# output from ssh my contain part of the expression in the buffer,
# split out everything except the last word.
return boolean(output.split()[-1]) # pylint: disable=maybe-no-member
def list_file_systems(self):
output = self.execute('mount')
@@ -394,18 +467,30 @@ class Target(object):
def uninstall(self, name):
raise NotImplementedError()
def get_installed(self, name):
for path in self.getenv('PATH').split(self.path.pathsep):
try:
if name in self.list_directory(path):
return self.path.join(path, name)
except TargetError:
pass # directory does not exist or no executable premssions
if name in self.list_directory(self.executables_directory):
return self.path.join(self.executables_directory, name)
def get_installed(self, name, search_system_binaries=True):
# Check user installed binaries first
if self.file_exists(self.executables_directory):
if name in self.list_directory(self.executables_directory):
return self.path.join(self.executables_directory, name)
# Fall back to binaries in PATH
if search_system_binaries:
for path in self.getenv('PATH').split(self.path.pathsep):
try:
if name in self.list_directory(path):
return self.path.join(path, name)
except TargetError:
pass # directory does not exist or no executable premssions
which = get_installed
def install_if_needed(self, host_path, search_system_binaries=True):
binary_path = self.get_installed(os.path.split(host_path)[1],
search_system_binaries=search_system_binaries)
if not binary_path:
binary_path = self.install(host_path)
return binary_path
def is_installed(self, name):
return bool(self.get_installed(name))
@@ -415,6 +500,87 @@ class Target(object):
def has(self, modname):
return hasattr(self, identifier(modname))
def lsmod(self):
lines = self.execute('lsmod').splitlines()
entries = []
for line in lines[1:]: # first line is the header
if not line.strip():
continue
parts = line.split()
name = parts[0]
size = int(parts[1])
use_count = int(parts[2])
if len(parts) > 3:
used_by = ''.join(parts[3:]).split(',')
else:
used_by = []
entries.append(LsmodEntry(name, size, use_count, used_by))
return entries
def insmod(self, path):
target_path = self.get_workpath(os.path.basename(path))
self.push(path, target_path)
self.execute('insmod {}'.format(target_path), as_root=True)
def extract(self, path, dest=None):
"""
Extact the specified on-target file. The extraction method to be used
(unzip, gunzip, bunzip2, or tar) will be based on the file's extension.
If ``dest`` is specified, it must be an existing directory on target;
the extracted contents will be placed there.
Note that, depending on the archive file format (and therfore the
extraction method used), the original archive file may or may not exist
after the extraction.
The return value is the path to the extracted contents. In case of
gunzip and bunzip2, this will be path to the extracted file; for tar
and uzip, this will be the directory with the extracted file(s)
(``dest`` if it was specified otherwise, the directory that cotained
the archive).
"""
for ending in ['.tar.gz', '.tar.bz', '.tar.bz2',
'.tgz', '.tbz', '.tbz2']:
if path.endswith(ending):
return self._extract_archive(path, 'tar xf {} -C {}', dest)
ext = self.path.splitext(path)[1]
if ext in ['.bz', '.bz2']:
return self._extract_file(path, 'bunzip2 -f {}', dest)
elif ext == '.gz':
return self._extract_file(path, 'gunzip -f {}', dest)
elif ext == '.zip':
return self._extract_archive(path, 'unzip {} -d {}', dest)
else:
raise ValueError('Unknown compression format: {}'.format(ext))
# internal methods
def _extract_archive(self, path, cmd, dest=None):
cmd = '{} ' + cmd # busybox
if dest:
extracted = dest
else:
extracted = self.path.dirname(path)
cmdtext = cmd.format(self.busybox, path, extracted)
self.execute(cmdtext)
return extracted
def _extract_file(self, path, cmd, dest=None):
cmd = '{} ' + cmd # busybox
cmdtext = cmd.format(self.busybox, path)
self.execute(cmdtext)
extracted = self.path.splitext(path)[0]
if dest:
self.execute('mv -f {} {}'.format(extracted, dest))
if dest.endswith('/'):
extracted = self.path.join(dest, self.path.basename(extracted))
else:
extracted = dest
return extracted
def _update_modules(self, stage):
for mod in self.modules:
if isinstance(mod, dict):
@@ -427,7 +593,11 @@ class Target(object):
if mod.probe(self):
self._install_module(mod, **params)
else:
self.logger.debug('Module {} is not supported by the target'.format(mod.name))
msg = 'Module {} is not supported by the target'.format(mod.name)
if self.load_default_modules:
self.logger.debug(msg)
else:
self.logger.warning(msg)
def _install_module(self, mod, **params):
if mod.name not in self._installed_modules:
@@ -437,10 +607,12 @@ class Target(object):
else:
self.logger.debug('Module {} is already installed.'.format(mod.name))
def _resolve_paths(self):
raise NotImplementedError()
class LinuxTarget(Target):
conn_cls = SshConnection
path = posixpath
os = 'linux'
@@ -471,15 +643,39 @@ class LinuxTarget(Target):
raise
return os_version
@property
@memoized
# There is currently no better way to do this cross platform.
# ARM does not have dmidecode
def model(self):
if self.file_exists("/proc/device-tree/model"):
raw_model = self.execute("cat /proc/device-tree/model")
return '_'.join(raw_model.split()[:2])
return None
def __init__(self,
connection_settings=None,
platform=None,
working_directory=None,
executables_directory=None,
connect=True,
modules=None,
load_default_modules=True,
shell_prompt=DEFAULT_SHELL_PROMPT,
conn_cls=SshConnection,
):
super(LinuxTarget, self).__init__(connection_settings=connection_settings,
platform=platform,
working_directory=working_directory,
executables_directory=executables_directory,
connect=connect,
modules=modules,
load_default_modules=load_default_modules,
shell_prompt=shell_prompt,
conn_cls=conn_cls)
def connect(self, timeout=None):
super(LinuxTarget, self).connect(timeout=timeout)
if self.working_directory is None:
if self.connected_as_root:
self.working_directory = '/root/devlib-target'
else:
self.working_directory = '/home/{}/devlib-target'.format(self.user)
if self.executables_directory is None:
self.executables_directory = self.path.join(self.working_directory, 'bin')
def kick_off(self, command, as_root=False):
command = 'sh -c "{}" 1>/dev/null 2>/dev/null &'.format(escape_double_quotes(command))
@@ -547,12 +743,21 @@ class LinuxTarget(Target):
message = e.message.split('OUTPUT:', 1)[1].strip() # pylint: disable=no-member
self.logger.debug('Could not take screenshot: {}'.format(message))
def _resolve_paths(self):
if self.working_directory is None:
if self.connected_as_root:
self.working_directory = '/root/devlib-target'
else:
self.working_directory = '/home/{}/devlib-target'.format(self.user)
if self.executables_directory is None:
self.executables_directory = self.path.join(self.working_directory, 'bin')
class AndroidTarget(Target):
conn_cls = AdbConnection
path = posixpath
os = 'android'
ls_command = ''
@property
@memoized
@@ -573,6 +778,30 @@ class AndroidTarget(Target):
def adb_name(self):
return self.conn.device
@property
@memoized
def android_id(self):
"""
Get the device's ANDROID_ID. Which is
"A 64-bit number (as a hex string) that is randomly generated when the user
first sets up the device and should remain constant for the lifetime of the
user's device."
.. note:: This will get reset on userdata erasure.
"""
output = self.execute('content query --uri content://settings/secure --projection value --where "name=\'android_id\'"').strip()
return output.split('value=')[-1]
@property
@memoized
def model(self):
try:
return self.getprop(prop='ro.product.device')
except KeyError:
return None
@property
@memoized
def screen_resolution(self):
@@ -584,18 +813,37 @@ class AndroidTarget(Target):
else:
return (0, 0)
def __init__(self, *args, **kwargs):
super(AndroidTarget, self).__init__(*args, **kwargs)
self._file_transfer_cache = None
def __init__(self,
connection_settings=None,
platform=None,
working_directory=None,
executables_directory=None,
connect=True,
modules=None,
load_default_modules=True,
shell_prompt=DEFAULT_SHELL_PROMPT,
conn_cls=AdbConnection,
package_data_directory="/data/data",
):
super(AndroidTarget, self).__init__(connection_settings=connection_settings,
platform=platform,
working_directory=working_directory,
executables_directory=executables_directory,
connect=connect,
modules=modules,
load_default_modules=load_default_modules,
shell_prompt=shell_prompt,
conn_cls=conn_cls)
self.package_data_directory = package_data_directory
def reset(self, fastboot=False): # pylint: disable=arguments-differ
try:
self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
as_root=self.is_rooted, timeout=2)
as_root=self.needs_su, timeout=2)
except (TargetError, TimeoutError, subprocess.CalledProcessError):
# on some targets "reboot" doesn't return gracefully
pass
self._connected_as_root = None
def connect(self, timeout=10, check_boot_completed=True): # pylint: disable=arguments-differ
start = time.time()
@@ -608,11 +856,6 @@ class AndroidTarget(Target):
# always disconnect first.
adb_disconnect(device)
super(AndroidTarget, self).connect(timeout=timeout)
if self.working_directory is None:
self.working_directory = '/data/local/tmp/devlib-target'
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
if self.executables_directory is None:
self.executables_directory = self.path.join(self.working_directory, 'bin')
if check_boot_completed:
boot_completed = boolean(self.getprop('sys.boot_completed'))
@@ -626,27 +869,37 @@ class AndroidTarget(Target):
super(AndroidTarget, self).setup(executables)
self.execute('mkdir -p {}'.format(self._file_transfer_cache))
def kick_off(self, command, as_root=False):
def kick_off(self, command, as_root=None):
"""
Like execute but closes adb session and returns immediately, leaving the command running on the
device (this is different from execute(background=True) which keeps adb connection open and returns
a subprocess object).
.. note:: This relies on busybox's nohup applet and so won't work on unrooted devices.
"""
if not self.is_rooted:
raise TargetError('kick_off uses busybox\'s nohup applet and so can only be run a rooted device.')
if as_root is None:
as_root = self.needs_su
try:
command = 'cd {} && {} nohup {}'.format(self.working_directory, self.bin('busybox'), command)
command = 'cd {} && {} nohup {} &'.format(self.working_directory, self.busybox, command)
output = self.execute(command, timeout=1, as_root=as_root)
except TimeoutError:
pass
else:
raise ValueError('Background command exited before timeout; got "{}"'.format(output))
def __setup_list_directory(self):
# In at least Linaro Android 16.09 (which was their first Android 7 release) and maybe
# AOSP 7.0 as well, the ls command was changed.
# Previous versions default to a single column listing, which is nice and easy to parse.
# Newer versions default to a multi-column listing, which is not, but it does support
# a '-1' option to get into single column mode. Older versions do not support this option
# so we try the new version, and if it fails we use the old version.
self.ls_command = 'ls -1'
try:
self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
except TargetError:
self.ls_command = 'ls'
def list_directory(self, path, as_root=False):
contents = self.execute('ls {}'.format(path), as_root=as_root)
if self.ls_command == '':
self.__setup_list_directory()
contents = self.execute('{} {}'.format(self.ls_command, path), as_root=as_root)
return [x.strip() for x in contents.split('\n') if x.strip()]
def install(self, filepath, timeout=None, with_name=None): # pylint: disable=W0221
@@ -674,7 +927,7 @@ class AndroidTarget(Target):
lines.next() # header
result = []
for line in lines:
parts = line.split()
parts = line.split(None, 8)
if parts:
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
if not kwargs:
@@ -697,28 +950,36 @@ class AndroidTarget(Target):
self.conn.push(source, dest, timeout=timeout)
else:
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
self.conn.push(source, device_tempfile, timeout=timeout)
self.execute('cp {} {}'.format(device_tempfile, dest), as_root=True)
self.execute("cp '{}' '{}'".format(device_tempfile, dest), as_root=True)
def pull(self, source, dest, as_root=False, timeout=None): # pylint: disable=arguments-differ
if not as_root:
self.conn.pull(source, dest, timeout=timeout)
else:
device_tempfile = self.path.join(self._file_transfer_cache, source.lstrip(self.path.sep))
self.execute('mkdir -p {}'.format(self.path.dirname(device_tempfile)))
self.execute('cp {} {}'.format(source, device_tempfile), as_root=True)
self.execute("mkdir -p '{}'".format(self.path.dirname(device_tempfile)))
self.execute("cp '{}' '{}'".format(source, device_tempfile), as_root=True)
self.execute("chmod 0644 '{}'".format(device_tempfile), as_root=True)
self.conn.pull(device_tempfile, dest, timeout=timeout)
# Android-specific
def swipe_to_unlock(self):
def swipe_to_unlock(self, direction="horizontal"):
width, height = self.screen_resolution
swipe_heigh = height * 2 // 3
start = 100
stop = width - start
command = 'input swipe {} {} {} {}'
self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
if direction == "horizontal":
swipe_heigh = height * 2 // 3
start = 100
stop = width - start
self.execute(command.format(start, swipe_heigh, stop, swipe_heigh))
if direction == "vertical":
swipe_middle = height / 2
swipe_heigh = height * 2 // 3
self.execute(command.format(swipe_middle, swipe_heigh, swipe_middle, 0))
else:
raise DeviceError("Invalid swipe direction: {}".format(self.swipe_to_unlock))
def getprop(self, prop=None):
props = AndroidProperties(self.execute('getprop'))
@@ -747,7 +1008,7 @@ class AndroidTarget(Target):
def install_apk(self, filepath, timeout=None): # pylint: disable=W0221
ext = os.path.splitext(filepath)[1].lower()
if ext == '.apk':
return adb_command(self.adb_name, "install {}".format(filepath), timeout=timeout)
return adb_command(self.adb_name, "install '{}'".format(filepath), timeout=timeout)
else:
raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
@@ -758,9 +1019,9 @@ class AndroidTarget(Target):
on_device_executable = self.path.join(self.executables_directory, executable_name)
self.push(filepath, on_device_file)
if on_device_file != on_device_executable:
self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.is_rooted)
self.remove(on_device_file, as_root=self.is_rooted)
self.execute('chmod 0777 {}'.format(on_device_executable), as_root=self.is_rooted)
self.execute('cp {} {}'.format(on_device_file, on_device_executable), as_root=self.needs_su)
self.remove(on_device_file, as_root=self.needs_su)
self.execute("chmod 0777 '{}'".format(on_device_executable), as_root=self.needs_su)
self._installed_binaries[executable_name] = on_device_executable
return on_device_executable
@@ -770,10 +1031,10 @@ class AndroidTarget(Target):
def uninstall_executable(self, executable_name):
on_device_executable = self.path.join(self.executables_directory, executable_name)
self._ensure_executables_directory_is_writable()
self.remove(on_device_executable, as_root=self.is_rooted)
self.remove(on_device_executable, as_root=self.needs_su)
def dump_logcat(self, filepath, filter=None, append=False, timeout=30): # pylint: disable=redefined-builtin
op = '>>' if append == True else '>'
op = '>>' if append else '>'
filtstr = ' -s {}'.format(filter) if filter else ''
command = 'logcat -d{} {} {}'.format(filtstr, op, filepath)
adb_command(self.adb_name, command, timeout=timeout)
@@ -781,6 +1042,19 @@ class AndroidTarget(Target):
def clear_logcat(self):
adb_command(self.adb_name, 'logcat -c', timeout=30)
def adb_reboot_bootloader(self, timeout=30):
adb_command(self.adb_name, 'reboot-bootloader', timeout)
def adb_root(self, enable=True, force=False):
if enable:
if self._connected_as_root and not force:
return
adb_command(self.adb_name, 'root', timeout=30)
self._connected_as_root = True
return
adb_command(self.adb_name, 'unroot', timeout=30)
self._connected_as_root = False
def is_screen_on(self):
output = self.execute('dumpsys power')
match = ANDROID_SCREEN_STATE_REGEX.search(output)
@@ -793,6 +1067,13 @@ class AndroidTarget(Target):
if not self.is_screen_on():
self.execute('input keyevent 26')
def _resolve_paths(self):
if self.working_directory is None:
self.working_directory = '/data/local/tmp/devlib-target'
self._file_transfer_cache = self.path.join(self.working_directory, '.file-cache')
if self.executables_directory is None:
self.executables_directory = '/data/local/tmp/bin'
def _ensure_executables_directory_is_writable(self):
matched = []
for entry in self.list_file_systems():
@@ -808,9 +1089,35 @@ class AndroidTarget(Target):
message = 'Could not find mount point for executables directory {}'
raise TargetError(message.format(self.executables_directory))
_charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
@property
def charging_enabled(self):
"""
Whether drawing power to charge the battery is enabled
Not all devices have the ability to enable/disable battery charging
(e.g. because they don't have a battery). In that case,
``charging_enabled`` is None.
"""
if not self.file_exists(self._charging_enabled_path):
return None
return self.read_bool(self._charging_enabled_path)
@charging_enabled.setter
def charging_enabled(self, enabled):
"""
Enable/disable drawing power to charge the battery
Not all devices have this facility. In that case, do nothing.
"""
if not self.file_exists(self._charging_enabled_path):
return
self.write_value(self._charging_enabled_path, int(bool(enabled)))
FstabEntry = namedtuple('FstabEntry', ['device', 'mount_point', 'fs_type', 'options', 'dump_freq', 'pass_num'])
PsEntry = namedtuple('PsEntry', 'user pid ppid vsize rss wchan pc state name')
LsmodEntry = namedtuple('LsmodEntry', ['name', 'size', 'use_count', 'used_by'])
class Cpuinfo(object):
@@ -855,8 +1162,12 @@ class Cpuinfo(object):
continue
if 'Features' in section:
return section.get('Features').split()
elif 'flags' in section:
return section.get('flags').split()
elif 'Features' in section:
global_features = section.get('Features').split()
elif 'flags' in section:
global_features = section.get('flags').split()
return global_features
def parse(self, text):
@@ -880,7 +1191,33 @@ class Cpuinfo(object):
class KernelVersion(object):
"""
Class representing the version of a target kernel
Not expected to work for very old (pre-3.0) kernel version numbers.
:ivar release: Version number/revision string. Typical output of
``uname -r``
:type release: str
:ivar version: Extra version info (aside from ``release``) reported by
``uname``
:type version: str
:ivar version_number: Main version number (e.g. 3 for Linux 3.18)
:type version_number: int
:ivar major: Major version number (e.g. 18 for Linux 3.18)
:type major: int
:ivar minor: Minor version number for stable kernels (e.g. 9 for 4.9.9). May
be None
:type minor: int
:ivar rc: Release candidate number (e.g. 3 for Linux 4.9-rc3). May be None.
:type rc: int
:ivar sha1: Kernel git revision hash, if available (otherwise None)
:type sha1: str
:ivar parts: Tuple of version number components. Can be used for
lexicographically comparing kernel versions.
:type parts: tuple(int)
"""
def __init__(self, version_string):
if ' #' in version_string:
release, version = version_string.split(' #')
@@ -893,6 +1230,25 @@ class KernelVersion(object):
self.release = version_string
self.version = ''
self.version_number = None
self.major = None
self.minor = None
self.sha1 = None
self.rc = None
match = KVERSION_REGEX.match(version_string)
if match:
groups = match.groupdict()
self.version_number = int(groups['version'])
self.major = int(groups['major'])
if groups['minor'] is not None:
self.minor = int(groups['minor'])
if groups['rc'] is not None:
self.rc = int(groups['rc'])
if groups['sha1'] is not None:
self.sha1 = match.group('sha1')
self.parts = (self.version_number, self.major, self.minor)
def __str__(self):
return '{} {}'.format(self.release, self.version)
@@ -952,14 +1308,32 @@ class KernelConfig(object):
class LocalLinuxTarget(LinuxTarget):
conn_cls = LocalConnection
def __init__(self,
connection_settings=None,
platform=None,
working_directory=None,
executables_directory=None,
connect=True,
modules=None,
load_default_modules=True,
shell_prompt=DEFAULT_SHELL_PROMPT,
conn_cls=LocalConnection,
):
super(LocalLinuxTarget, self).__init__(connection_settings=connection_settings,
platform=platform,
working_directory=working_directory,
executables_directory=executables_directory,
connect=connect,
modules=modules,
load_default_modules=load_default_modules,
shell_prompt=shell_prompt,
conn_cls=conn_cls)
def connect(self, timeout=None):
def _resolve_paths(self):
if self.working_directory is None:
self.working_directory = '/tmp'
if self.executables_directory is None:
self.executables_directory = '/tmp'
super(LocalLinuxTarget, self).connect(timeout)
def _get_model_name(section):
@@ -977,4 +1351,3 @@ def _get_part_name(section):
if name is None:
name = '{}/{}/{}'.format(implementer, part, variant)
return name

View File

@@ -15,7 +15,9 @@
from __future__ import division
import os
import json
import time
import re
import subprocess
from devlib.trace import TraceCollector
@@ -27,6 +29,7 @@ from devlib.utils.misc import check_output, which
TRACE_MARKER_START = 'TRACE_MARKER_START'
TRACE_MARKER_STOP = 'TRACE_MARKER_STOP'
OUTPUT_TRACE_FILE = 'trace.dat'
OUTPUT_PROFILE_FILE = 'trace_stat.dat'
DEFAULT_EVENTS = [
'cpu_frequency',
'cpu_idle',
@@ -40,26 +43,30 @@ DEFAULT_EVENTS = [
]
TIMEOUT = 180
# Regexps for parsing of function profiling data
CPU_RE = re.compile(r' Function \(CPU([0-9]+)\)')
STATS_RE = re.compile(r'([^ ]*) +([0-9]+) +([0-9.]+) us +([0-9.]+) us +([0-9.]+) us')
class FtraceCollector(TraceCollector):
def __init__(self, target,
events=None,
functions=None,
buffer_size=None,
buffer_size_step=1000,
buffer_size_file='/sys/kernel/debug/tracing/buffer_size_kb',
marker_file='/sys/kernel/debug/tracing/trace_marker',
tracing_path='/sys/kernel/debug/tracing',
automark=True,
autoreport=True,
autoview=False,
no_install=False,
strict=False,
):
super(FtraceCollector, self).__init__(target)
self.events = events if events is not None else DEFAULT_EVENTS
self.functions = functions
self.buffer_size = buffer_size
self.buffer_size_step = buffer_size_step
self.buffer_size_file = buffer_size_file
self.marker_file = marker_file
self.tracing_path = tracing_path
self.automark = automark
self.autoreport = autoreport
self.autoview = autoview
@@ -68,9 +75,19 @@ class FtraceCollector(TraceCollector):
self.host_binary = None
self.start_time = None
self.stop_time = None
self.event_string = _build_trace_events(self.events)
self.event_string = None
self.function_string = None
self._reset_needed = True
# Setup tracing paths
self.available_events_file = self.target.path.join(self.tracing_path, 'available_events')
self.available_functions_file = self.target.path.join(self.tracing_path, 'available_filter_functions')
self.buffer_size_file = self.target.path.join(self.tracing_path, 'buffer_size_kb')
self.current_tracer_file = self.target.path.join(self.tracing_path, 'current_tracer')
self.function_profile_file = self.target.path.join(self.tracing_path, 'function_profile_enabled')
self.marker_file = self.target.path.join(self.tracing_path, 'trace_marker')
self.ftrace_filter_file = self.target.path.join(self.tracing_path, 'set_ftrace_filter')
self.host_binary = which('trace-cmd')
self.kernelshark = which('kernelshark')
@@ -88,25 +105,99 @@ class FtraceCollector(TraceCollector):
raise TargetError('No trace-cmd found on device and no_install=True is specified.')
self.target_binary = 'trace-cmd'
# Validate required events to be traced
available_events = self.target.execute(
'cat {}'.format(self.available_events_file),
as_root=True).splitlines()
selected_events = []
for event in self.events:
# Convert globs supported by FTrace into valid regexp globs
_event = event
if event[0] != '*':
_event = '*' + event
event_re = re.compile(_event.replace('*', '.*'))
# Select events matching the required ones
if len(filter(event_re.match, available_events)) == 0:
message = 'Event [{}] not available for tracing'.format(event)
if strict:
raise TargetError(message)
self.target.logger.warning(message)
else:
selected_events.append(event)
# If function profiling is enabled we always need at least one event.
# Thus, if not other events have been specified, try to add at least
# a tracepoint which is always available and possibly triggered few
# times.
if self.functions and len(selected_events) == 0:
selected_events = ['sched_wakeup_new']
self.event_string = _build_trace_events(selected_events)
# Check for function tracing support
if self.functions:
if not self.target.file_exists(self.function_profile_file):
raise TargetError('Function profiling not supported. '\
'A kernel build with CONFIG_FUNCTION_PROFILER enable is required')
# Validate required functions to be traced
available_functions = self.target.execute(
'cat {}'.format(self.available_functions_file),
as_root=True).splitlines()
selected_functions = []
for function in self.functions:
if function not in available_functions:
message = 'Function [{}] not available for profiling'.format(function)
if strict:
raise TargetError(message)
self.target.logger.warning(message)
else:
selected_functions.append(function)
self.function_string = _build_trace_functions(selected_functions)
def reset(self):
if self.buffer_size:
self._set_buffer_size()
self.target.execute('{} reset'.format(self.target_binary), as_root=True, timeout=TIMEOUT)
self.target.execute('{} reset'.format(self.target_binary),
as_root=True, timeout=TIMEOUT)
self._reset_needed = False
def start(self):
self.start_time = time.time()
if self._reset_needed:
self.reset()
self.target.execute('{} start {}'.format(self.target_binary, self.event_string),
as_root=True)
if self.automark:
self.mark_start()
self.target.execute('{} start {}'.format(self.target_binary, self.event_string), as_root=True)
if 'cpufreq' in self.target.modules:
self.logger.debug('Trace CPUFreq frequencies')
self.target.cpufreq.trace_frequencies()
if 'cpuidle' in self.target.modules:
self.logger.debug('Trace CPUIdle states')
self.target.cpuidle.perturb_cpus()
# Enable kernel function profiling
if self.functions:
self.target.execute('echo nop > {}'.format(self.current_tracer_file),
as_root=True)
self.target.execute('echo 0 > {}'.format(self.function_profile_file),
as_root=True)
self.target.execute('echo {} > {}'.format(self.function_string, self.ftrace_filter_file),
as_root=True)
self.target.execute('echo 1 > {}'.format(self.function_profile_file),
as_root=True)
def stop(self):
# Disable kernel function profiling
if self.functions:
self.target.execute('echo 1 > {}'.format(self.function_profile_file),
as_root=True)
if 'cpufreq' in self.target.modules:
self.logger.debug('Trace CPUFreq frequencies')
self.target.cpufreq.trace_frequencies()
self.stop_time = time.time()
if self.automark:
self.mark_stop()
self.target.execute('{} stop'.format(self.target_binary), timeout=TIMEOUT, as_root=True)
self.target.execute('{} stop'.format(self.target_binary),
timeout=TIMEOUT, as_root=True)
self._reset_needed = True
def get_trace(self, outfile):
@@ -118,7 +209,7 @@ class FtraceCollector(TraceCollector):
# The size of trace.dat will depend on how long trace-cmd was running.
# Therefore timout for the pull command must also be adjusted
# accordingly.
pull_timeout = self.stop_time - self.start_time
pull_timeout = 5 * (self.stop_time - self.start_time)
self.target.pull(self.target_output_file, outfile, timeout=pull_timeout)
if not os.path.isfile(outfile):
self.logger.warning('Binary trace not pulled from device.')
@@ -129,6 +220,44 @@ class FtraceCollector(TraceCollector):
if self.autoview:
self.view(outfile)
def get_stats(self, outfile):
if not self.functions:
return
if os.path.isdir(outfile):
outfile = os.path.join(outfile, OUTPUT_PROFILE_FILE)
output = self.target._execute_util('ftrace_get_function_stats',
as_root=True)
function_stats = {}
for line in output.splitlines():
# Match a new CPU dataset
match = CPU_RE.search(line)
if match:
cpu_id = int(match.group(1))
function_stats[cpu_id] = {}
self.logger.debug("Processing stats for CPU%d...", cpu_id)
continue
# Match a new function dataset
match = STATS_RE.search(line)
if match:
fname = match.group(1)
function_stats[cpu_id][fname] = {
'hits' : int(match.group(2)),
'time' : float(match.group(3)),
'avg' : float(match.group(4)),
's_2' : float(match.group(5)),
}
self.logger.debug(" %s: %s",
fname, function_stats[cpu_id][fname])
self.logger.debug("FTrace stats output [%s]...", outfile)
with open(outfile, 'w') as fh:
json.dump(function_stats, fh, indent=4)
self.logger.debug("FTrace function stats save in [%s]", outfile)
return function_stats
def report(self, binfile, destfile):
# To get the output of trace.dat, trace-cmd must be installed
# This is done host-side because the generated file is very large
@@ -197,3 +326,6 @@ def _build_trace_events(events):
event_string = ' '.join(['-e {}'.format(e) for e in events])
return event_string
def _build_trace_functions(functions):
function_string = " ".join(functions)
return function_string

View File

@@ -26,8 +26,8 @@ import logging
import re
from collections import defaultdict
from devlib.exception import TargetError, HostError
from devlib.utils.misc import check_output, which
from devlib.exception import TargetError, HostError, DevlibError
from devlib.utils.misc import check_output, which, memoized
from devlib.utils.misc import escape_single_quotes, escape_double_quotes
@@ -39,6 +39,7 @@ AM_START_ERROR = re.compile(r"Error: Activity class {[\w|.|/]*} does not exist")
# See:
# http://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels
ANDROID_VERSION_MAP = {
23: 'MARSHMALLOW',
22: 'LOLLYPOP_MR1',
21: 'LOLLYPOP',
20: 'KITKAT_WATCH',
@@ -151,33 +152,81 @@ class AdbConnection(object):
# maintains the count of parallel active connections to a device, so that
# adb disconnect is not invoked untill all connections are closed
active_connections = defaultdict(int)
default_timeout = 10
ls_command = 'ls'
@property
def name(self):
return self.device
def __init__(self, device=None, timeout=10):
self.timeout = timeout
@property
@memoized
def newline_separator(self):
output = adb_command(self.device,
"shell '({}); echo \"\n$?\"'".format(self.ls_command))
if output.endswith('\r\n'):
return '\r\n'
elif output.endswith('\n'):
return '\n'
else:
raise DevlibError("Unknown line ending")
# Again, we need to handle boards where the default output format from ls is
# single column *and* boards where the default output is multi-column.
# We need to do this purely because the '-1' option causes errors on older
# versions of the ls tool in Android pre-v7.
def _setup_ls(self):
command = "shell '(ls -1); echo \"\n$?\"'"
try:
output = adb_command(self.device, command, timeout=self.timeout)
except subprocess.CalledProcessError as e:
raise HostError(
'Failed to set up ls command on Android device. Output:\n'
+ e.output)
lines = output.splitlines()
retval = lines[-1].strip()
if int(retval) == 0:
self.ls_command = 'ls -1'
else:
self.ls_command = 'ls'
logger.info("ls command is set to {}".format(self.ls_command))
def __init__(self, device=None, timeout=None, platform=None):
self.timeout = timeout if timeout is not None else self.default_timeout
if device is None:
device = adb_get_device(timeout=timeout)
self.device = device
adb_connect(self.device)
AdbConnection.active_connections[self.device] += 1
self._setup_ls()
def push(self, source, dest, timeout=None):
if timeout is None:
timeout = self.timeout
command = 'push {} {}'.format(source, dest)
command = "push '{}' '{}'".format(source, dest)
if not os.path.exists(source):
raise HostError('No such file "{}"'.format(source))
return adb_command(self.device, command, timeout=timeout)
def pull(self, source, dest, timeout=None):
if timeout is None:
timeout = self.timeout
command = 'pull {} {}'.format(source, dest)
# Pull all files matching a wildcard expression
if os.path.isdir(dest) and \
('*' in source or '?' in source):
command = 'shell {} {}'.format(self.ls_command, source)
output = adb_command(self.device, command, timeout=timeout)
for line in output.splitlines():
command = "pull '{}' '{}'".format(line.strip(), dest)
adb_command(self.device, command, timeout=timeout)
return
command = "pull '{}' '{}'".format(source, dest)
return adb_command(self.device, command, timeout=timeout)
def execute(self, command, timeout=None, check_exit_code=False, as_root=False):
return adb_shell(self.device, command, timeout, check_exit_code, as_root)
def execute(self, command, timeout=None, check_exit_code=False,
as_root=False, strip_colors=True):
return adb_shell(self.device, command, timeout, check_exit_code,
as_root, self.newline_separator)
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
return adb_background_shell(self.device, command, stdout, stderr, as_root)
@@ -195,9 +244,10 @@ class AdbConnection(object):
pass
def fastboot_command(command, timeout=None):
def fastboot_command(command, timeout=None, device=None):
_check_env()
full_command = "fastboot {}".format(command)
target = '-s {}'.format(device) if device else ''
full_command = 'fastboot {} {}'.format(target, command)
logger.debug(full_command)
output, _ = check_output(full_command, timeout, shell=True)
return output
@@ -232,7 +282,7 @@ def adb_get_device(timeout=None):
return output[1].split('\t')[0]
elif output_length > 3:
message = '{} Android devices found; either explicitly specify ' +\
'the device you want, or make sure only one is connected.'
'the device you want, or make sure only one is connected.'
raise HostError(message.format(output_length - 2))
else:
if timeout < time.time() - start:
@@ -242,6 +292,10 @@ def adb_get_device(timeout=None):
def adb_connect(device, timeout=None, attempts=MAX_ATTEMPTS):
_check_env()
# Connect is required only for ADB-over-IP
if "." not in device:
logger.debug('Device connected via USB, connect not required')
return
tries = 0
output = None
while tries <= attempts:
@@ -264,7 +318,7 @@ def adb_disconnect(device):
_check_env()
if not device:
return
if ":" in device:
if ":" in device and device in adb_list_devices():
command = "adb disconnect " + device
logger.debug(command)
retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
@@ -284,33 +338,39 @@ def _ping(device):
return False
def adb_shell(device, command, timeout=None, check_exit_code=False, as_root=False): # NOQA
def adb_shell(device, command, timeout=None, check_exit_code=False,
as_root=False, newline_separator='\r\n'): # NOQA
_check_env()
if as_root:
command = 'echo "{}" | su'.format(escape_double_quotes(command))
device_string = ' -s {}'.format(device) if device else ''
full_command = 'adb{} shell "{}"'.format(device_string,
escape_double_quotes(command))
logger.debug(full_command)
if check_exit_code:
actual_command = "adb{} shell '({}); echo $?'".format(device_string,
escape_single_quotes(command))
raw_output, error = check_output(actual_command, timeout, shell=True)
if raw_output:
try:
output, exit_code, _ = raw_output.rsplit('\n', 2)
except ValueError:
exit_code, _ = raw_output.rsplit('\n', 1)
output = ''
else: # raw_output is empty
exit_code = '969696' # just because
output = ''
command = 'echo \'{}\' | su'.format(escape_single_quotes(command))
device_part = ['-s', device] if device else []
# On older combinations of ADB/Android versions, the adb host command always
# exits with 0 if it was able to run the command on the target, even if the
# command failed (https://code.google.com/p/android/issues/detail?id=3254).
# Homogenise this behaviour by running the command then echoing the exit
# code.
adb_shell_command = '({}); echo \"\n$?\"'.format(command)
actual_command = ['adb'] + device_part + ['shell', adb_shell_command]
logger.debug('adb {} shell {}'.format(' '.join(device_part), command))
raw_output, error = check_output(actual_command, timeout, shell=False)
if raw_output:
try:
output, exit_code, _ = raw_output.rsplit(newline_separator, 2)
except ValueError:
exit_code, _ = raw_output.rsplit(newline_separator, 1)
output = ''
else: # raw_output is empty
exit_code = '969696' # just because
output = ''
if check_exit_code:
exit_code = exit_code.strip()
if exit_code.isdigit():
if int(exit_code):
message = 'Got exit code {}\nfrom: {}\nSTDOUT: {}\nSTDERR: {}'
raise TargetError(message.format(exit_code, full_command, output, error))
message = ('Got exit code {}\nfrom target command: {}\n'
'STDOUT: {}\nSTDERR: {}')
raise TargetError(message.format(exit_code, command, output, error))
elif AM_START_ERROR.findall(output):
message = 'Could not start activity; got the following:'
message += '\n{}'.format(AM_START_ERROR.findall(output)[0])
@@ -323,8 +383,7 @@ def adb_shell(device, command, timeout=None, check_exit_code=False, as_root=Fals
message = 'adb has returned early; did not get an exit code. '\
'Was kill-server invoked?'
raise TargetError(message)
else: # do not check exit code
output, _ = check_output(full_command, timeout, shell=True)
return output
@@ -377,19 +436,20 @@ def _initialize_with_android_home(env):
logger.debug('Using ANDROID_HOME from the environment.')
env.android_home = android_home
env.platform_tools = os.path.join(android_home, 'platform-tools')
os.environ['PATH'] += os.pathsep + env.platform_tools
os.environ['PATH'] = env.platform_tools + os.pathsep + os.environ['PATH']
_init_common(env)
return env
def _initialize_without_android_home(env):
if which('adb'):
adb_full_path = which('adb')
if adb_full_path:
env.adb = 'adb'
else:
raise HostError('ANDROID_HOME is not set and adb is not in PATH. '
'Have you installed Android SDK?')
logger.debug('Discovering ANDROID_HOME from adb path.')
env.platform_tools = os.path.dirname(env.adb)
env.platform_tools = os.path.dirname(adb_full_path)
env.android_home = os.path.dirname(env.platform_tools)
_init_common(env)
return env

View File

@@ -29,10 +29,15 @@ import subprocess
import pkgutil
import logging
import random
import ctypes
from operator import itemgetter
from itertools import groupby
from functools import partial
import wrapt
from devlib.exception import HostError, TimeoutError
# ABI --> architectures list
ABI_MAP = {
@@ -55,17 +60,24 @@ CPU_PART_MAP = {
0xc07: {None: 'A7'},
0xc08: {None: 'A8'},
0xc09: {None: 'A9'},
0xc0e: {None: 'A17'},
0xc0f: {None: 'A15'},
0xc14: {None: 'R4'},
0xc15: {None: 'R5'},
0xc17: {None: 'R7'},
0xc18: {None: 'R8'},
0xc20: {None: 'M0'},
0xc60: {None: 'M0+'},
0xc21: {None: 'M1'},
0xc23: {None: 'M3'},
0xc24: {None: 'M4'},
0xc27: {None: 'M7'},
0xd01: {None: 'A32'},
0xd03: {None: 'A53'},
0xd04: {None: 'A35'},
0xd07: {None: 'A57'},
0xd08: {None: 'A72'},
0xd09: {None: 'A73'},
},
0x4e: { # Nvidia
0x0: {None: 'Denver'},
@@ -77,6 +89,8 @@ CPU_PART_MAP = {
0x2: 'Krait400',
0x3: 'Krait450',
},
0x205: {0x1: 'KryoSilver'},
0x211: {0x1: 'KryoGold'},
},
0x56: { # Marvell
0x131: {
@@ -109,22 +123,6 @@ def preexec_function():
check_output_logger = logging.getLogger('check_output')
# Defined here rather than in devlib.exceptions due to module load dependencies
class TimeoutError(Exception):
"""Raised when a subprocess command times out. This is basically a ``WAError``-derived version
of ``subprocess.CalledProcessError``, the thinking being that while a timeout could be due to
programming error (e.g. not setting long enough timers), it is often due to some failure in the
environment, and there fore should be classed as a "user error"."""
def __init__(self, command, output):
super(TimeoutError, self).__init__('Timed out: {}'.format(command))
self.command = command
self.output = output
def __str__(self):
return '\n'.join([self.message, 'OUTPUT:', self.output or ''])
def check_output(command, timeout=None, ignore=None, inputtext=None, **kwargs):
"""This is a version of subprocess.check_output that adds a timeout parameter to kill
the subprocess if it does not return within the specified time."""
@@ -174,15 +172,35 @@ def walk_modules(path):
Given package name, return a list of all modules (including submodules, etc)
in that package.
:raises HostError: if an exception is raised while trying to import one of the
modules under ``path``. The exception will have addtional
attributes set: ``module`` will be set to the qualified name
of the originating module, and ``orig_exc`` will contain
the original exception.
"""
root_mod = __import__(path, {}, {}, [''])
def __try_import(path):
try:
return __import__(path, {}, {}, [''])
except Exception as e:
he = HostError('Could not load {}: {}'.format(path, str(e)))
he.module = path
he.exc_info = sys.exc_info()
he.orig_exc = e
raise he
root_mod = __try_import(path)
mods = [root_mod]
if not hasattr(root_mod, '__path__'):
# root is a module not a package -- nothing to walk
return mods
for _, name, ispkg in pkgutil.iter_modules(root_mod.__path__):
submod_path = '.'.join([path, name])
if ispkg:
mods.extend(walk_modules(submod_path))
else:
submod = __import__(submod_path, {}, {}, [''])
submod = __try_import(submod_path)
mods.append(submod)
return mods
@@ -536,17 +554,48 @@ def mask_to_list(mask):
__memo_cache = {}
def memoized(func):
def reset_memo_cache():
__memo_cache.clear()
def __get_memo_id(obj):
"""
An object's id() may be re-used after an object is freed, so it's not
sufficiently unique to identify params for the memo cache (two different
params may end up with the same id). this attempts to generate a more unique
ID string.
"""
obj_id = id(obj)
try:
return '{}/{}'.format(obj_id, hash(obj))
except TypeError: # obj is not hashable
obj_pyobj = ctypes.cast(obj_id, ctypes.py_object)
# TODO: Note: there is still a possibility of a clash here. If Two
# different objects get assigned the same ID, an are large and are
# identical in the first thirty two bytes. This shouldn't be much of an
# issue in the current application of memoizing Target calls, as it's very
# unlikely that a target will get passed large params; but may cause
# problems in other applications, e.g. when memoizing results of operations
# on large arrays. I can't really think of a good way around that apart
# form, e.g., md5 hashing the entire raw object, which will have an
# undesirable impact on performance.
num_bytes = min(ctypes.sizeof(obj_pyobj), 32)
obj_bytes = ctypes.string_at(ctypes.addressof(obj_pyobj), num_bytes)
return '{}/{}'.format(obj_id, obj_bytes)
@wrapt.decorator
def memoized(wrapped, instance, args, kwargs):
"""A decorator for memoizing functions and methods."""
func_id = repr(func)
func_id = repr(wrapped)
def memoize_wrapper(*args, **kwargs):
id_string = func_id + ','.join([str(id(a)) for a in args])
id_string = func_id + ','.join([__get_memo_id(a) for a in args])
id_string += ','.join('{}={}'.format(k, v)
for k, v in kwargs.iteritems())
if id_string not in __memo_cache:
__memo_cache[id_string] = func(*args, **kwargs)
__memo_cache[id_string] = wrapped(*args, **kwargs)
return __memo_cache[id_string]
return memoize_wrapper
return memoize_wrapper(*args, **kwargs)

View File

@@ -22,6 +22,8 @@ import re
import threading
import tempfile
import shutil
import socket
import time
import pexpect
from distutils.version import StrictVersion as V
@@ -33,30 +35,41 @@ from pexpect import EOF, TIMEOUT, spawn
from devlib.exception import HostError, TargetError, TimeoutError
from devlib.utils.misc import which, strip_bash_colors, escape_single_quotes, check_output
from devlib.utils.types import boolean
ssh = None
scp = None
sshpass = None
logger = logging.getLogger('ssh')
gem5_logger = logging.getLogger('gem5-connection')
def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False):
def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeout=10, telnet=False, original_prompt=None):
_check_env()
if telnet:
if keyfile:
raise ValueError('keyfile may not be used with a telnet connection.')
conn = TelnetConnection()
else: # ssh
conn = pxssh.pxssh()
try:
if keyfile:
conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout)
else:
conn.login(host, username, password, port=port, login_timeout=timeout)
except EOF:
raise TargetError('Could not connect to {}; is the host name correct?'.format(host))
start_time = time.time()
while True:
if telnet:
if keyfile:
raise ValueError('keyfile may not be used with a telnet connection.')
conn = TelnetPxssh(original_prompt=original_prompt)
else: # ssh
conn = pxssh.pxssh()
try:
if keyfile:
conn.login(host, username, ssh_key=keyfile, port=port, login_timeout=timeout)
else:
conn.login(host, username, password, port=port, login_timeout=timeout)
break
except EOF:
timeout -= time.time() - start_time
if timeout <= 0:
message = 'Could not connect to {}; is the host name correct?'
raise TargetError(message.format(host))
time.sleep(5)
conn.setwinsize(500,200)
conn.sendline('')
conn.prompt()
@@ -64,23 +77,37 @@ def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeou
return conn
class TelnetConnection(pxssh.pxssh):
class TelnetPxssh(pxssh.pxssh):
# pylint: disable=arguments-differ
def login(self, server, username, password='', original_prompt=r'[#$]', login_timeout=10,
auto_prompt_reset=True, sync_multiplier=1):
cmd = 'telnet -l {} {}'.format(username, server)
def __init__(self, original_prompt):
super(TelnetPxssh, self).__init__()
self.original_prompt = original_prompt or r'[#$]'
def login(self, server, username, password='', login_timeout=10,
auto_prompt_reset=True, sync_multiplier=1, port=23):
args = ['telnet']
if username is not None:
args += ['-l', username]
args += [server, str(port)]
cmd = ' '.join(args)
spawn._spawn(self, cmd) # pylint: disable=protected-access
i = self.expect('(?i)(?:password)', timeout=login_timeout)
if i == 0:
self.sendline(password)
i = self.expect([original_prompt, 'Login incorrect'], timeout=login_timeout)
else:
raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
if i:
raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
try:
i = self.expect('(?i)(?:password)', timeout=login_timeout)
if i == 0:
self.sendline(password)
i = self.expect([self.original_prompt, 'Login incorrect'], timeout=login_timeout)
if i:
raise pxssh.ExceptionPxssh('could not log in: password was incorrect')
except TIMEOUT:
if not password:
# No password promt before TIMEOUT & no password provided
# so assume everything is okay
pass
else:
raise pxssh.ExceptionPxssh('could not log in: did not see a password prompt')
if not self.sync_original_prompt(sync_multiplier):
self.close()
@@ -117,6 +144,7 @@ class SshConnection(object):
default_password_prompt = '[sudo] password'
max_cancel_attempts = 5
default_timeout=10
@property
def name(self):
@@ -128,9 +156,11 @@ class SshConnection(object):
password=None,
keyfile=None,
port=None,
timeout=10,
timeout=None,
telnet=False,
password_prompt=None,
original_prompt=None,
platform=None
):
self.host = host
self.username = username
@@ -140,7 +170,8 @@ class SshConnection(object):
self.lock = threading.Lock()
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
logger.debug('Logging in {}@{}'.format(username, host))
self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, telnet)
timeout = timeout if timeout is not None else self.default_timeout
self.conn = ssh_get_shell(host, username, password, self.keyfile, port, timeout, False, None)
def push(self, source, dest, timeout=30):
dest = '{}@{}:{}'.format(self.username, self.host, dest)
@@ -150,28 +181,45 @@ class SshConnection(object):
source = '{}@{}:{}'.format(self.username, self.host, source)
return self._scp(source, dest, timeout)
def execute(self, command, timeout=None, check_exit_code=True, as_root=False, strip_colors=True):
with self.lock:
output = self._execute_and_wait_for_prompt(command, timeout, as_root, strip_colors)
if check_exit_code:
exit_code_text = self._execute_and_wait_for_prompt('echo $?', strip_colors=strip_colors, log=False)
try:
exit_code = int(exit_code_text.split()[0])
if exit_code:
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
raise TargetError(message.format(exit_code, command, output))
except (ValueError, IndexError):
logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
return output
def execute(self, command, timeout=None, check_exit_code=True,
as_root=False, strip_colors=True): #pylint: disable=unused-argument
if command == '':
# Empty command is valid but the __devlib_ec stuff below will
# produce a syntax error with bash. Treat as a special case.
return ''
try:
with self.lock:
_command = '({}); __devlib_ec=$?; echo; echo $__devlib_ec'.format(command)
raw_output = self._execute_and_wait_for_prompt(
_command, timeout, as_root, strip_colors)
output, exit_code_text, _ = raw_output.rsplit('\r\n', 2)
if check_exit_code:
try:
exit_code = int(exit_code_text)
if exit_code:
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
raise TargetError(message.format(exit_code, command, output))
except (ValueError, IndexError):
logger.warning(
'Could not get exit code for "{}",\ngot: "{}"'\
.format(command, exit_code_text))
return output
except EOF:
raise TargetError('Connection lost.')
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE):
port_string = '-p {}'.format(self.port) if self.port else ''
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
logger.debug(command)
if self.password:
command = _give_password(self.password, command)
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
try:
port_string = '-p {}'.format(self.port) if self.port else ''
keyfile_string = '-i {}'.format(self.keyfile) if self.keyfile else ''
if as_root:
command = "sudo -- sh -c '{}'".format(command)
command = '{} {} {} {}@{} {}'.format(ssh, keyfile_string, port_string, self.username, self.host, command)
logger.debug(command)
if self.password:
command = _give_password(self.password, command)
return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
except EOF:
raise TargetError('Connection lost.')
def close(self):
logger.debug('Logging out {}@{}'.format(self.username, self.host))
@@ -188,6 +236,9 @@ class SshConnection(object):
def _execute_and_wait_for_prompt(self, command, timeout=None, as_root=False, strip_colors=True, log=True):
self.conn.prompt(0.1) # clear an existing prompt if there is one.
if self.username == 'root':
# As we're already root, there is no need to use sudo.
as_root = False
if as_root:
command = "sudo -- sh -c '{}'".format(escape_single_quotes(command))
if log:
@@ -243,6 +294,526 @@ class SshConnection(object):
raise TimeoutError(e.command.replace(pass_string, ''), e.output)
class TelnetConnection(SshConnection):
def __init__(self,
host,
username,
password=None,
port=None,
timeout=None,
password_prompt=None,
original_prompt=None,
platform=None):
self.host = host
self.username = username
self.password = password
self.port = port
self.keyfile = None
self.lock = threading.Lock()
self.password_prompt = password_prompt if password_prompt is not None else self.default_password_prompt
logger.debug('Logging in {}@{}'.format(username, host))
timeout = timeout if timeout is not None else self.default_timeout
self.conn = ssh_get_shell(host, username, password, None, port, timeout, True, original_prompt)
class Gem5Connection(TelnetConnection):
def __init__(self,
platform,
host=None,
username=None,
password=None,
port=None,
timeout=None,
password_prompt=None,
original_prompt=None,
):
if host is not None:
host_system = socket.gethostname()
if host_system != host:
raise TargetError("Gem5Connection can only connect to gem5 "
"simulations on your current host, which "
"differs from the one given {}!"
.format(host_system, host))
if username is not None and username != 'root':
raise ValueError('User should be root in gem5!')
if password is not None and password != '':
raise ValueError('No password needed in gem5!')
self.username = 'root'
self.is_rooted = True
self.password = None
self.port = None
# Long timeouts to account for gem5 being slow
# Can be overriden if the given timeout is longer
self.default_timeout = 3600
if timeout is not None:
if timeout > self.default_timeout:
logger.info('Overwriting the default timeout of gem5 ({})'
' to {}'.format(self.default_timeout, timeout))
self.default_timeout = timeout
else:
logger.info('Ignoring the given timeout --> gem5 needs longer timeouts')
self.ready_timeout = self.default_timeout * 3
# Counterpart in gem5_interact_dir
self.gem5_input_dir = '/mnt/host/'
# Location of m5 binary in the gem5 simulated system
self.m5_path = None
# Actual telnet connection to gem5 simulation
self.conn = None
# Flag to indicate the gem5 device is ready to interact with the
# outer world
self.ready = False
# Lock file to prevent multiple connections to same gem5 simulation
# (gem5 does not allow this)
self.lock_directory = '/tmp/'
self.lock_file_name = None # Will be set once connected to gem5
# These parameters will be set by either the method to connect to the
# gem5 platform or directly to the gem5 simulation
# Intermediate directory to push things to gem5 using VirtIO
self.gem5_interact_dir = None
# Directory to store output from gem5 on the host
self.gem5_out_dir = None
# Actual gem5 simulation
self.gem5simulation = None
# Connect to gem5
if platform:
self._connect_gem5_platform(platform)
# Wait for boot
self._wait_for_boot()
# Mount the virtIO to transfer files in/out gem5 system
self._mount_virtio()
def set_hostinteractdir(self, indir):
logger.info('Setting hostinteractdir from {} to {}'
.format(self.gem5_input_dir, indir))
self.gem5_input_dir = indir
def push(self, source, dest, timeout=None):
"""
Push a file to the gem5 device using VirtIO
The file to push to the device is copied to the temporary directory on
the host, before being copied within the simulation to the destination.
Checks, in the form of 'ls' with error code checking, are performed to
ensure that the file is copied to the destination.
"""
# First check if the connection is set up to interact with gem5
self._check_ready()
filename = os.path.basename(source)
logger.debug("Pushing {} to device.".format(source))
logger.debug("gem5interactdir: {}".format(self.gem5_interact_dir))
logger.debug("dest: {}".format(dest))
logger.debug("filename: {}".format(filename))
# We need to copy the file to copy to the temporary directory
self._move_to_temp_dir(source)
# Dest in gem5 world is a file rather than directory
if os.path.basename(dest) != filename:
dest = os.path.join(dest, filename)
# Back to the gem5 world
self._gem5_shell("ls -al {}{}".format(self.gem5_input_dir, filename))
self._gem5_shell("cat '{}''{}' > '{}'".format(self.gem5_input_dir,
filename,
dest))
self._gem5_shell("sync")
self._gem5_shell("ls -al {}".format(dest))
self._gem5_shell("ls -al {}".format(self.gem5_input_dir))
logger.debug("Push complete.")
def pull(self, source, dest, timeout=0): #pylint: disable=unused-argument
"""
Pull a file from the gem5 device using m5 writefile
The file is copied to the local directory within the guest as the m5
writefile command assumes that the file is local. The file is then
written out to the host system using writefile, prior to being moved to
the destination on the host.
"""
# First check if the connection is set up to interact with gem5
self._check_ready()
filename = os.path.basename(source)
logger.debug("pull_file {} {}".format(source, filename))
# We don't check the exit code here because it is non-zero if the source
# and destination are the same. The ls below will cause an error if the
# file was not where we expected it to be.
if os.path.dirname(source) != os.getcwd():
self._gem5_shell("cat '{}' > '{}'".format(source, filename))
self._gem5_shell("sync")
self._gem5_shell("ls -la {}".format(filename))
logger.debug('Finished the copy in the simulator')
self._gem5_util("writefile {}".format(filename))
if 'cpu' not in filename:
while not os.path.exists(os.path.join(self.gem5_out_dir, filename)):
time.sleep(1)
# Perform the local move
shutil.move(os.path.join(self.gem5_out_dir, filename), dest)
logger.debug("Pull complete.")
def execute(self, command, timeout=1000, check_exit_code=True,
as_root=False, strip_colors=True):
"""
Execute a command on the gem5 platform
"""
# First check if the connection is set up to interact with gem5
self._check_ready()
output = self._gem5_shell(command, as_root=as_root)
if strip_colors:
output = strip_bash_colors(output)
return output
def background(self, command, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, as_root=False):
# First check if the connection is set up to interact with gem5
self._check_ready()
# Create the logfile for stderr/stdout redirection
command_name = command.split(' ')[0].split('/')[-1]
redirection_file = 'BACKGROUND_{}.log'.format(command_name)
trial = 0
while os.path.isfile(redirection_file):
# Log file already exists so add to name
redirection_file = 'BACKGROUND_{}{}.log'.format(command_name, trial)
trial += 1
# Create the command to pass on to gem5 shell
complete_command = '{} >> {} 2>&1 &'.format(command, redirection_file)
output = self._gem5_shell(complete_command, as_root=as_root)
output = strip_bash_colors(output)
gem5_logger.info('STDERR/STDOUT of background command will be '
'redirected to {}. Use target.pull() to '
'get this file'.format(redirection_file))
return output
def close(self):
"""
Close and disconnect from the gem5 simulation. Additionally, we remove
the temporary directory used to pass files into the simulation.
"""
gem5_logger.info("Gracefully terminating the gem5 simulation.")
try:
self._gem5_util("exit")
self.gem5simulation.wait()
except EOF:
pass
gem5_logger.info("Removing the temporary directory")
try:
shutil.rmtree(self.gem5_interact_dir)
except OSError:
gem5_logger.warn("Failed to remove the temporary directory!")
# Delete the lock file
os.remove(self.lock_file_name)
# Functions only to be called by the Gem5 connection itself
def _connect_gem5_platform(self, platform):
port = platform.gem5_port
gem5_simulation = platform.gem5
gem5_interact_dir = platform.gem5_interact_dir
gem5_out_dir = platform.gem5_out_dir
self.connect_gem5(port, gem5_simulation, gem5_interact_dir, gem5_out_dir)
# This function connects to the gem5 simulation
def connect_gem5(self, port, gem5_simulation, gem5_interact_dir,
gem5_out_dir):
"""
Connect to the telnet port of the gem5 simulation.
We connect, and wait for the prompt to be found. We do not use a timeout
for this, and wait for the prompt in a while loop as the gem5 simulation
can take many hours to reach a prompt when booting the system. We also
inject some newlines periodically to try and force gem5 to show a
prompt. Once the prompt has been found, we replace it with a unique
prompt to ensure that we are able to match it properly. We also disable
the echo as this simplifies parsing the output when executing commands
on the device.
"""
host = socket.gethostname()
gem5_logger.info("Connecting to the gem5 simulation on port {}".format(port))
# Check if there is no on-going connection yet
lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port)
if os.path.isfile(lock_file_name):
# There is already a connection to this gem5 simulation
raise TargetError('There is already a connection to the gem5 '
'simulation using port {} on {}!'
.format(port, host))
# Connect to the gem5 telnet port. Use a short timeout here.
attempts = 0
while attempts < 10:
attempts += 1
try:
self.conn = TelnetPxssh(original_prompt=None)
self.conn.login(host, self.username, port=port,
login_timeout=10, auto_prompt_reset=False)
break
except pxssh.ExceptionPxssh:
pass
else:
gem5_simulation.kill()
raise TargetError("Failed to connect to the gem5 telnet session.")
gem5_logger.info("Connected! Waiting for prompt...")
# Create the lock file
self.lock_file_name = lock_file_name
open(self.lock_file_name, 'w').close() # Similar to touch
gem5_logger.info("Created lock file {} to prevent reconnecting to "
"same simulation".format(self.lock_file_name))
# We need to find the prompt. It might be different if we are resuming
# from a checkpoint. Therefore, we test multiple options here.
prompt_found = False
while not prompt_found:
try:
self._login_to_device()
except TIMEOUT:
pass
try:
# Try and force a prompt to be shown
self.conn.send('\n')
self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT, r'\[PEXPECT\][\\\$\#]+ '], timeout=60)
prompt_found = True
except TIMEOUT:
pass
gem5_logger.info("Successfully logged in")
gem5_logger.info("Setting unique prompt...")
self.conn.set_unique_prompt()
self.conn.prompt()
gem5_logger.info("Prompt found and replaced with a unique string")
# We check that the prompt is what we think it should be. If not, we
# need to update the regex we use to match.
self._find_prompt()
self.conn.setecho(False)
self._sync_gem5_shell()
# Fully connected to gem5 simulation
self.gem5_interact_dir = gem5_interact_dir
self.gem5_out_dir = gem5_out_dir
self.gem5simulation = gem5_simulation
# Ready for interaction now
self.ready = True
def _login_to_device(self):
"""
Login to device, will be overwritten if there is an actual login
"""
pass
def _find_prompt(self):
prompt = r'\[PEXPECT\][\\\$\#]+ '
synced = False
while not synced:
self.conn.send('\n')
i = self.conn.expect([prompt, self.conn.UNIQUE_PROMPT, r'[\$\#] '], timeout=self.default_timeout)
if i == 0:
synced = True
elif i == 1:
prompt = self.conn.UNIQUE_PROMPT
synced = True
else:
prompt = re.sub(r'\$', r'\\\$', self.conn.before.strip() + self.conn.after.strip())
prompt = re.sub(r'\#', r'\\\#', prompt)
prompt = re.sub(r'\[', r'\[', prompt)
prompt = re.sub(r'\]', r'\]', prompt)
self.conn.PROMPT = prompt
def _sync_gem5_shell(self):
"""
Synchronise with the gem5 shell.
Write some unique text to the gem5 device to allow us to synchronise
with the shell output. We actually get two prompts so we need to match
both of these.
"""
gem5_logger.debug("Sending Sync")
self.conn.send("echo \*\*sync\*\*\n")
self.conn.expect(r"\*\*sync\*\*", timeout=self.default_timeout)
self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
def _gem5_util(self, command):
""" Execute a gem5 utility command using the m5 binary on the device """
if self.m5_path is None:
raise TargetError('Path to m5 binary on simulated system is not set!')
self._gem5_shell('{} {}'.format(self.m5_path, command))
def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True): # pylint: disable=R0912
"""
Execute a command in the gem5 shell
This wraps the telnet connection to gem5 and processes the raw output.
This method waits for the shell to return, and then will try and
separate the output from the command from the command itself. If this
fails, warn, but continue with the potentially wrong output.
The exit code is also checked by default, and non-zero exit codes will
raise a TargetError.
"""
if sync:
self._sync_gem5_shell()
gem5_logger.debug("gem5_shell command: {}".format(command))
# Send the actual command
self.conn.send("{}\n".format(command))
# Wait for the response. We just sit here and wait for the prompt to
# appear, as gem5 might take a long time to provide the output. This
# avoids timeout issues.
command_index = -1
while command_index == -1:
if self.conn.prompt():
output = re.sub(r' \r([^\n])', r'\1', self.conn.before)
output = re.sub(r'[\b]', r'', output)
# Deal with line wrapping
output = re.sub(r'[\r].+?<', r'', output)
command_index = output.find(command)
# If we have -1, then we cannot match the command, but the
# prompt has returned. Hence, we have a bit of an issue. We
# warn, and return the whole output.
if command_index == -1:
gem5_logger.warn("gem5_shell: Unable to match command in "
"command output. Expect parsing errors!")
command_index = 0
output = output[command_index + len(command):].strip()
# It is possible that gem5 will echo the command. Therefore, we need to
# remove that too!
command_index = output.find(command)
if command_index != -1:
output = output[command_index + len(command):].strip()
gem5_logger.debug("gem5_shell output: {}".format(output))
# We get a second prompt. Hence, we need to eat one to make sure that we
# stay in sync. If we do not do this, we risk getting out of sync for
# slower simulations.
self.conn.expect([self.conn.UNIQUE_PROMPT, self.conn.PROMPT], timeout=self.default_timeout)
if check_exit_code:
exit_code_text = self._gem5_shell('echo $?', as_root=as_root,
timeout=timeout, check_exit_code=False,
sync=False)
try:
exit_code = int(exit_code_text.split()[0])
if exit_code:
message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
raise TargetError(message.format(exit_code, command, output))
except (ValueError, IndexError):
gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
return output
def _mount_virtio(self):
"""
Mount the VirtIO device in the simulated system.
"""
gem5_logger.info("Mounting VirtIO device in simulated system")
self._gem5_shell('su -c "mkdir -p {}" root'.format(self.gem5_input_dir))
mount_command = "mount -t 9p -o trans=virtio,version=9p2000.L,aname={} gem5 {}".format(self.gem5_interact_dir, self.gem5_input_dir)
self._gem5_shell(mount_command)
def _move_to_temp_dir(self, source):
"""
Move a file to the temporary directory on the host for copying to the
gem5 device
"""
command = "cp {} {}".format(source, self.gem5_interact_dir)
gem5_logger.debug("Local copy command: {}".format(command))
subprocess.call(command.split())
subprocess.call("sync".split())
def _check_ready(self):
"""
Check if the gem5 platform is ready
"""
if not self.ready:
raise TargetError('Gem5 is not ready to interact yet')
def _wait_for_boot(self):
pass
def _probe_file(self, filepath):
"""
Internal method to check if the target has a certain file
"""
command = 'if [ -e \'{}\' ]; then echo 1; else echo 0; fi'
output = self.execute(command.format(filepath), as_root=self.is_rooted)
return boolean(output.strip())
class LinuxGem5Connection(Gem5Connection):
def _login_to_device(self):
gem5_logger.info("Trying to log in to gem5 device")
login_prompt = ['login:', 'AEL login:', 'username:', 'aarch64-gem5 login:']
login_password_prompt = ['password:']
# Wait for the login prompt
prompt = login_prompt + [self.conn.UNIQUE_PROMPT]
i = self.conn.expect(prompt, timeout=10)
# Check if we are already at a prompt, or if we need to log in.
if i < len(prompt) - 1:
self.conn.sendline("{}".format(self.username))
password_prompt = login_password_prompt + [r'# ', self.conn.UNIQUE_PROMPT]
j = self.conn.expect(password_prompt, timeout=self.default_timeout)
if j < len(password_prompt) - 2:
self.conn.sendline("{}".format(self.password))
self.conn.expect([r'# ', self.conn.UNIQUE_PROMPT], timeout=self.default_timeout)
class AndroidGem5Connection(Gem5Connection):
def _wait_for_boot(self):
"""
Wait for the system to boot
We monitor the sys.boot_completed and service.bootanim.exit system
properties to determine when the system has finished booting. In the
event that we cannot coerce the result of service.bootanim.exit to an
integer, we assume that the boot animation was disabled and do not wait
for it to finish.
"""
gem5_logger.info("Waiting for Android to boot...")
while True:
booted = False
anim_finished = True # Assume boot animation was disabled on except
try:
booted = (int('0' + self._gem5_shell('getprop sys.boot_completed', check_exit_code=False).strip()) == 1)
anim_finished = (int(self._gem5_shell('getprop service.bootanim.exit', check_exit_code=False).strip()) == 1)
except ValueError:
pass
if booted and anim_finished:
break
time.sleep(60)
gem5_logger.info("Android booted")
def _give_password(password, command):
if not sshpass:
raise HostError('Must have sshpass installed on the host in order to use password-based auth.')

240
doc/connection.rst Normal file
View File

@@ -0,0 +1,240 @@
Connection
==========
A :class:`Connection` abstracts an actual physical connection to a device. The
first connection is created when :func:`Target.connect` method is called. If a
:class:`Target` is used in a multi-threaded environment, it will maintain a
connection for each thread in which it is invoked. This allows the same target
object to be used in parallel in multiple threads.
:class:`Connection`\ s will be automatically created and managed by
:class:`Target`\ s, so there is usually no reason to create one manually.
Instead, configuration for a :class:`Connection` is passed as
`connection_settings` parameter when creating a :class:`Target`. The connection
to be used target is also specified on instantiation by `conn_cls` parameter,
though all concrete :class:`Target` implementations will set an appropriate
default, so there is typically no need to specify this explicitly.
:class:`Connection` classes are not a part of an inheritance hierarchy, i.e.
they do not derive from a common base. Instead, a :class:`Connection` is any
class that implements the following methods.
.. method:: push(self, source, dest, timeout=None)
Transfer a file from the host machine to the connected device.
:param source: path of to the file on the host
:param dest: path of to the file on the connected device.
:param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised.
.. method:: pull(self, source, dest, timeout=None)
Transfer a file, or files matching a glob pattern, from the connected device
to the host machine.
:param source: path of to the file on the connected device. If ``dest`` is a
directory, may be a glob pattern.
:param dest: path of to the file on the host
:param timeout: timeout (in seconds) for the transfer; if the transfer does
not complete within this period, an exception will be raised.
.. method:: execute(self, command, timeout=None, check_exit_code=False, as_root=False)
Execute the specified command on the connected device and return its output.
:param command: The command to be executed.
:param timeout: Timeout (in seconds) for the execution of the command. If
specified, an exception will be raised if execution does not complete
with the specified period.
:param check_exit_code: If ``True`` the exit code (on connected device)
from execution of the command will be checked, and an exception will be
raised if it is not ``0``.
:param as_root: The command will be executed as root. This will fail on
unrooted connected devices.
.. method:: background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False)
Execute the command on the connected device, invoking it via subprocess on the host.
This will return :class:`subprocess.Popen` instance for the command.
:param command: The command to be executed.
:param stdout: By default, standard output will be piped from the subprocess;
this may be used to redirect it to an alternative file handle.
:param stderr: By default, standard error will be piped from the subprocess;
this may be used to redirect it to an alternative file handle.
:param as_root: The command will be executed as root. This will fail on
unrooted connected devices.
.. note:: This **will block the connection** until the command completes.
.. note:: The above methods are directly wrapped by :class:`Target` methods,
however note that some of the defaults are different.
.. method:: cancel_running_command(self)
Cancel a running command (previously started with :func:`background`) and free up the connection.
It is valid to call this if the command has already terminated (or if no
command was issued), in which case this is a no-op.
.. method:: close(self)
Close the connection to the device. The :class:`Connection` object should not
be used after this method is called. There is no way to reopen a previously
closed connection, a new connection object should be created instead.
.. note:: There is no :func:`open` method, as the connection is assumed to be
opened on instantiation.
.. _connection-types:
Connection Types
----------------
.. class:: AdbConnection(device=None, timeout=None)
A connection to an android device via ``adb`` (Android Debug Bridge).
``adb`` is part of the Android SDK (though stand-alone versions are also
available).
:param device: The name of the adb divice. This is usually a unique hex
string for USB-connected devices, or an ip address/port
combination. To see connected devices, you can run ``adb
devices`` on the host.
:param timeout: Connection timeout in seconds. If a connection to the device
is not esblished within this period, :class:`HostError`
is raised.
.. class:: SshConnection(host, username, password=None, keyfile=None, port=None,\
timeout=None, password_prompt=None)
A connectioned to a device on the network over SSH.
:param host: SSH host to which to connect
:param username: username for SSH login
:param password: password for the SSH connection
.. note:: In order to user password-based authentication,
``sshpass`` utility must be installed on the
system.
:param keyfile: Path to the SSH private key to be used for the connection.
.. note:: ``keyfile`` and ``password`` can't be specified
at the same time.
:param port: TCP port on which SSH server is litening on the remoted device.
Omit to use the default port.
:param timeout: Timeout for the connection in seconds. If a connection
cannot be established within this time, an error will be
raised.
:param password_prompt: A string with the password prompt used by
``sshpass``. Set this if your version of ``sshpass``
uses somethin other than ``"[sudo] password"``.
.. class:: TelnetConnection(host, username, password=None, port=None,\
timeout=None, password_prompt=None,\
original_prompt=None)
A connectioned to a device on the network over Telenet.
.. note:: Since Telenet protocol is does not support file transfer, scp is
used for that purpose.
:param host: SSH host to which to connect
:param username: username for SSH login
:param password: password for the SSH connection
.. note:: In order to user password-based authentication,
``sshpass`` utility must be installed on the
system.
:param port: TCP port on which SSH server is litening on the remoted device.
Omit to use the default port.
:param timeout: Timeout for the connection in seconds. If a connection
cannot be established within this time, an error will be
raised.
:param password_prompt: A string with the password prompt used by
``sshpass``. Set this if your version of ``sshpass``
uses somethin other than ``"[sudo] password"``.
:param original_prompt: A regex for the shell prompted presented in the Telenet
connection (the prompt will be reset to a
randomly-generated pattern for the duration of the
connection to reduce the possibility of clashes).
This paramer is ignored for SSH connections.
.. class:: LocalConnection(keep_password=True, unrooted=False, password=None)
A connection to the local host allowing it to be treated as a Target.
:param keep_password: If this is ``True`` (the default) user's password will
be cached in memory after it is first requested.
:param unrooted: If set to ``True``, the platform will be assumed to be
unrooted without testing for root. This is useful to avoid
blocking on password request in scripts.
:param password: Specify password on connection creation rather than
prompting for it.
.. class:: Gem5Connection(platform, host=None, username=None, password=None,\
timeout=None, password_prompt=None,\
original_prompt=None)
A connection to a gem5 simulation using a local Telnet connection.
.. note:: Some of the following input parameters are optional and will be ignored during
initialisation. They were kept to keep the anology with a :class:`TelnetConnection`
(i.e. ``host``, `username``, ``password``, ``port``,
``password_prompt`` and ``original_promp``)
:param host: Host on which the gem5 simulation is running
.. note:: Even thought the input parameter for the ``host``
will be ignored, the gem5 simulation needs to on
the same host as the user as the user is
currently on, so if the host given as input
parameter is not the same as the actual host, a
``TargetError`` will be raised to prevent
confusion.
:param username: Username in the simulated system
:param password: No password required in gem5 so does not need to be set
:param port: Telnet port to connect to gem5. This does not need to be set
at initialisation as this will either be determined by the
:class:`Gem5SimulationPlatform` or can be set using the
:func:`connect_gem5` method
:param timeout: Timeout for the connection in seconds. Gem5 has high
latencies so unless the timeout given by the user via
this input parameter is higher than the default one
(3600 seconds), this input parameter will be ignored.
:param password_prompt: A string with password prompt
:param original_prompt: A regex for the shell prompt
There are two classes that inherit from :class:`Gem5Connection`:
:class:`AndroidGem5Connection` and :class:`LinuxGem5Connection`.
They inherit *almost* all methods from the parent class, without altering them.
The only methods discussed belows are those that will be overwritten by the
:class:`LinuxGem5Connection` and :class:`AndroidGem5Connection` respectively.
.. class:: LinuxGem5Connection
A connection to a gem5 simulation that emulates a Linux system.
.. method:: _login_to_device(self)
Login to the gem5 simulated system.
.. class:: AndroidGem5Connection
A connection to a gem5 simulation that emulates an Android system.
.. method:: _wait_for_boot(self)
Wait for the gem5 simulated system to have booted and finished the booting animation.

View File

@@ -19,8 +19,8 @@ Contents:
target
modules
instrumentation
platform
connection
Indices and tables
==================

View File

@@ -28,7 +28,7 @@ Android target.
# a no-op, but is included here for completeness.
In [4]: i.setup()
# Find out what the instrument is capable collecting from the
# Find out what the instrument is capable collecting from the
# target.
In [5]: i.list_channels()
Out[5]:
@@ -40,7 +40,7 @@ Android target.
In [6]: i.reset(sites=['exynos-therm'])
# HWMON instrument supports INSTANTANEOUS collection, so invoking
# take_measurement() will return a list of measurements take from
# take_measurement() will return a list of measurements take from
# each of the channels configured during reset()
In [7]: i.take_measurement()
Out[7]: [exynos-therm_temperature: 36.0 degrees]
@@ -68,7 +68,7 @@ Instrument
period of time via ``start()``, ``stop()``, and
``get_data()`` methods.
.. note:: It's possible for one instrument to support more than a single
.. note:: It's possible for one instrument to support more than a single
mode.
.. attribute:: Instrument.active_channels
@@ -133,9 +133,9 @@ Instrument
.. method:: Instrument.get_data(outfile)
Write collected data into ``outfile``. Must be called after :func:`stop()`.
Write collected data into ``outfile``. Must be called after :func:`stop()`.
Data will be written in CSV format with a column for each channel and a row
for each sample. Column heading will be channel, labels in the form
for each sample. Column heading will be channel, labels in the form
``<site>_<kind>`` (see :class:`InstrumentChannel`). The order of the coluns
will be the same as the order of channels in ``Instrument.active_channels``.
@@ -146,6 +146,12 @@ Instrument
.. note:: This method is only implemented by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment.
.. attribute:: Instrument.sample_rate_hz
Sample rate of the instrument in Hz. Assumed to be the same for all channels.
.. note:: This attribute is only provided by :class:`Instrument`\ s that
support ``CONTINUOUS`` measurment.
Instrument Channel
~~~~~~~~~~~~~~~~~~

View File

@@ -1,3 +1,5 @@
.. _modules:
Modules
=======
@@ -9,7 +11,7 @@ hotplug
-------
Kernel ``hotplug`` subsystem allows offlining ("removing") cores from the
system, and onlining them back int. The ``devlib`` module exposes a simple
system, and onlining them back in. The ``devlib`` module exposes a simple
interface to this subsystem
.. code:: python
@@ -35,10 +37,10 @@ policies (governors). The ``devlib`` module exposes the following interface
.. note:: On ARM big.LITTLE systems, all cores on a cluster (usually all cores
of the same type) are in the same frequency domain, so setting
``cpufreq`` state on one core on a cluter will affect all cores on
``cpufreq`` state on one core on a cluster will affect all cores on
that cluster. Because of this, some devices only expose cpufreq sysfs
interface (which is what is used by the ``devlib`` module) on the
first cpu in a cluster. So to keep your scripts proable, always use
first cpu in a cluster. So to keep your scripts portable, always use
the fist (online) CPU in a cluster to set ``cpufreq`` state.
.. method:: target.cpufreq.list_governors(cpu)
@@ -64,26 +66,26 @@ policies (governors). The ``devlib`` module exposes the following interface
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
``1`` or ``"cpu1"``).
.. method:: target.cpufreq.set_governor(cpu, governor, **kwargs)
.. method:: target.cpufreq.set_governor(cpu, governor, \*\*kwargs)
Sets the governor for the specified cpu.
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
``1`` or ``"cpu1"``).
:param governor: The name of the governor. This must be one of the governors
supported by the CPU (as retrunted by ``list_governors()``.
supported by the CPU (as returned by ``list_governors()``.
Keyword arguments may be used to specify governor tunable values.
.. method:: target.cpufreq.get_governor_tunables(cpu)
Return a dict with the values of the specfied CPU's current governor.
Return a dict with the values of the specified CPU's current governor.
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
``1`` or ``"cpu1"``).
.. method:: target.cpufreq.set_governor_tunables(cpu, **kwargs)
.. method:: target.cpufreq.set_governor_tunables(cpu, \*\*kwargs)
Set the tunables for the current governor on the specified CPU.
@@ -92,7 +94,7 @@ policies (governors). The ``devlib`` module exposes the following interface
Keyword arguments should be used to specify tunable values.
.. method:: target.cpufreq.list_frequencie(cpu)
.. method:: target.cpufreq.list_frequencies(cpu)
List DVFS frequencies supported by the specified CPU. Returns a list of ints.
@@ -104,8 +106,8 @@ policies (governors). The ``devlib`` module exposes the following interface
target.cpufreq.set_min_frequency(cpu, frequency[, exact=True])
target.cpufreq.set_max_frequency(cpu, frequency[, exact=True])
Get and set min and max frequencies on the specfied CPU. "set" functions are
avialable with all governors other than ``userspace``.
Get and set min and max frequencies on the specified CPU. "set" functions are
available with all governors other than ``userspace``.
:param cpu: The cpu; could be a numeric or the corresponding string (e.g.
``1`` or ``"cpu1"``).
@@ -126,11 +128,11 @@ cpuidle
``cpufreq`` is the kernel subsystem for managing CPU low power (idle) states.
.. method:: taget.cpuidle.get_driver()
.. method:: target.cpuidle.get_driver()
Return the name current cpuidle driver.
.. method:: taget.cpuidle.get_governor()
.. method:: target.cpuidle.get_governor()
Return the name current cpuidle governor (policy).
@@ -169,4 +171,192 @@ TODO
API
---
TODO
Generic Module API Description
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Modules implement discrete, optional pieces of functionality ("optional" in the
sense that the functionality may or may not be present on the target device, or
that it may or may not be necessary for a particular application).
Every module (ultimately) derives from :class:`Module` class. A module must
define the following class attributes:
:name: A unique name for the module. This cannot clash with any of the existing
names and must be a valid Python identifier, but is otherwise free-from.
:kind: This identifies the type of functionality a module implements, which in
turn determines the interface implemented by the module (all modules of
the same kind must expose a consistent interface). This must be a valid
Python identifier, but is otherwise free-form, though, where possible,
one should try to stick to an already-defined kind/interface, lest we end
up with a bunch of modules implementing similar functionality but
exposing slightly different interfaces.
.. note:: It is possible to omit ``kind`` when defining a module, in
which case the module's ``name`` will be treated as its
``kind`` as well.
:stage: This defines when the module will be installed into a :class:`Target`.
Currently, the following values are allowed:
:connected: The module is installed after a connection to the target has
been established. This is the default.
:early: The module will be installed when a :class:`Target` is first
created. This should be used for modules that do not rely on a
live connection to the target.
Additionally, a module must implement a static (or class) method :func:`probe`:
.. method:: Module.probe(target)
This method takes a :class:`Target` instance and returns ``True`` if this
module is supported by that target, or ``False`` otherwise.
.. note:: If the module ``stage`` is ``"early"``, this method cannot assume
that a connection has been established (i.e. it can only access
attributes of the Target that do not rely on a connection).
Installation and invocation
***************************
The default installation method will create an instance of a module (the
:class:`Target` instance being the sole argument) and assign it to the target
instance attribute named after the module's ``kind`` (or ``name`` if ``kind`` is
``None``).
It is possible to change the installation procedure for a module by overriding
the default :func:`install` method. The method must have the following
signature:
.. method:: Module.install(cls, target, **kwargs)
Install the module into the target instance.
Implementation and Usage Patterns
*********************************
There are two common ways to implement the above API, corresponding to the two
common uses for modules:
- If a module provides an interface to a particular set of functionality (e.g.
an OS subsystem), that module would typically derive directly form
:class:`Module` and would leave ``kind`` unassigned, so that it is accessed
by it name. Its instance's methods and attributes provide the interface for
interacting with its functionality. For examples of this type of module, see
the subsystem modules listed above (e.g. ``cpufreq``).
- If a module provides a platform- or infrastructure-specific implementation of
a common function, the module would derive from one of :class:`Module`
subclasses that define the interface for that function. In that case the
module would be accessible via the common ``kind`` defined its super. The
module would typically implement :func:`__call__` and be invoked directly. For
examples of this type of module, see common function interface definitions
below.
Common Function Interfaces
~~~~~~~~~~~~~~~~~~~~~~~~~~
This section documents :class:`Module` classes defining interface for common
functions. Classes derived from them provide concrete implementations for
specific platforms.
HardResetModule
***************
.. attribute:: HardResetModule.kind
"hard_reset"
.. method:: HardResetModule.__call__()
Must be implemented by derived classes.
Implements hard reset for a target devices. The equivalent of physically
power cycling the device. This may be used by client code in situations
where the target becomes unresponsive and/or a regular reboot is not
possible.
BootModule
**********
.. attribute:: BootModule.kind
"hard_reset"
.. method:: BootModule.__call__()
Must be implemented by derived classes.
Implements a boot procedure. This takes the device from (hard or soft)
reset to a booted state where the device is ready to accept connections. For
a lot of commercial devices the process is entirely automatic, however some
devices (e.g. development boards), my require additional steps, such as
interactions with the bootloader, in order to boot into the OS.
.. method:: Bootmodule.update(\*\*kwargs)
Update the boot settings. Some boot sequences allow specifying settings
that will be utilized during boot (e.g. linux kernel boot command line). The
default implementation will set each setting in ``kwargs`` as an attribute of
the boot module (or update the existing attribute).
FlashModule
***********
.. attribute:: FlashModule.kind
"flash"
.. method:: __call__(image_bundle=None, images=None, boot_config=None)
Must be implemented by derived classes.
Flash the target platform with the specified images.
:param image_bundle: A compressed bundle of image files with any associated
metadata. The format of the bundle is specific to a
particular implementation.
:param images: A dict mapping image names/identifiers to the path on the
host file system of the corresponding image file. If both
this and ``image_bundle`` are specified, individual images
will override those in the bundle.
:param boot_config: Some platforms require specifying boot arguments at the
time of flashing the images, rather than during each
reboot. For other platforms, this will be ignored.
Module Registration
~~~~~~~~~~~~~~~~~~~
Modules are specified on :class:`Target` or :class:`Platform` creation by name.
In order to find the class associated with the name, the module needs to be
registered with ``devlib``. This is accomplished by passing the module class
into :func:`register_module` method once it is defined.
.. note:: If you're wiring a module to be included as part of ``devlib`` code
base, you can place the file with the module class under
``devlib/modules/`` in the source and it will be automatically
enumerated. There is no need to explicitly register it in that case.
The code snippet below illustrates an implementation of a hard reset function
for an "Acme" device.
.. code:: python
import os
from devlib import HardResetModule, register_module
class AcmeHardReset(HardResetModule):
name = 'acme_hard_reset'
def __call__(self):
# Assuming Acme board comes with a "reset-acme-board" utility
os.system('reset-acme-board {}'.format(self.target.name))
register_module(AcmeHardReset)

View File

@@ -32,7 +32,7 @@ instantiating each of the three target types.
# For a Linux device, you will need to provide the normal SSH credentials.
# Both password-based, and key-based authentication is supported (password
# authentication requires sshpass to be installed on your host machine).'
t2 = LinuxTarget(connetion_settings={'host': '192.168.0.5',
t2 = LinuxTarget(connection_settings={'host': '192.168.0.5',
'username': 'root',
'password': 'sekrit',
# or
@@ -57,7 +57,7 @@ Target Interface
----------------
This is a quick overview of the basic interface to the device. See
:class:`Targeet` API documentation for the full list of supported methods and
:class:`Target` API documentation for the full list of supported methods and
more detailed documentation.
One-time Setup

171
doc/platform.rst Normal file
View File

@@ -0,0 +1,171 @@
.. _platform:
Platform
========
:class:`Platform`\ s describe the system underlying the OS. They encapsulate
hardware- and firmware-specific details. In most cases, the generic
:class:`Platform` class, which gets used if a platform is not explicitly
specified on :class:`Target` creation, will be sufficient. It will automatically
query as much platform information (such CPU topology, hardware model, etc) if
it was not specified explicitly by the user.
.. class:: Platform(name=None, core_names=None, core_clusters=None,\
big_core=None, model=None, modules=None)
:param name: A user-friendly identifier for the platform.
:param core_names: A list of CPU core names in the order they appear
registered with the OS. If they are not specified,
they will be queried at run time.
:param core_clusters: Alist with cluster ids of each core (starting with
0). If this is not specified, clusters will be
inferred from core names (cores with the same name are
assumed to be in a cluster).
:param big_core: The name of the big core in a big.LITTLE system. If this is
not specified it will be inferred (on systems with exactly
two clasters).
:param model: Model name of the hardware system. If this is not specified it
will be queried at run time.
:param modules: Modules with additional functionality supported by the
platfrom (e.g. for handling flashing, rebooting, etc). These
would be added to the Target's modules. (See :ref:`modules`\ ).
Versatile Express
-----------------
The generic platform may be extended to support hardware- or
infrastructure-specific functionality. Platforms exist for ARM
VersatileExpress-based :class:`Juno` and :class:`TC2` development boards. In
addition to the standard :class:`Platform` parameters above, these platfroms
support additional configuration:
.. class:: VersatileExpressPlatform
Normally, this would be instatiated via one of its derived classes
(:class:`Juno` or :class:`TC2`) that set appropriate defaults for some of
the parameters.
:param serial_port: Identifies the serial port (usual a /dev node) on which the
device is connected.
:param baudrate: Baud rate for the serial connection. This defaults to
``115200`` for :class:`Juno` and ``38400`` for
:class:`TC2`.
:param vemsd_mount: Mount point for the VEMSD (Versatile Express MicroSD card
that is used for board configuration files and firmware
images). This defaults to ``"/media/JUNO"`` for
:class:`Juno` and ``"/media/VEMSD"`` for :class:`TC2`,
though you would most likely need to change this for
your setup as it would depend both on the file system
label on the MicroSD card, and on how the card was
mounted on the host system.
:param hard_reset_method: Specifies the method for hard-resetting the devices
(e.g. if it becomes unresponsive and normal reboot
method doesn not work). Currently supported methods
are:
:dtr: reboot by toggling DTR line on the serial
connection (this is enabled via a DIP switch
on the board).
:reboottxt: reboot by writing a filed called
``reboot.txt`` to the root of the VEMSD
mount (this is enabled via board
configuration file).
This defaults to ``dtr`` for :class:`Juno` and
``reboottxt`` for :class:`TC2`.
:param bootloader: Specifies the bootloader configuration used by the board.
The following values are currently supported:
:uefi: Boot via UEFI menu, by selecting the entry
specified by ``uefi_entry`` paramter. If this
entry does not exist, it will be automatically
created based on values provided for ``image``,
``initrd``, ``fdt``, and ``bootargs`` parameters.
:uefi-shell: Boot by going via the UEFI shell.
:u-boot: Boot using Das U-Boot.
:bootmon: Boot directly via Versatile Express Bootmon
using the values provided for ``image``,
``initrd``, ``fdt``, and ``bootargs``
parameters.
This defaults to ``u-boot`` for :class:`Juno` and
``bootmon`` for :class:`TC2`.
:param flash_method: Specifies how the device is flashed. Currently, only
``"vemsd"`` method is supported, which flashes by
writing firmware images to an appropriate location on
the VEMSD.
:param image: Specfies the kernel image name for ``uefi`` or ``bootmon`` boot.
:param fdt: Specifies the device tree blob for ``uefi`` or ``bootmon`` boot.
:param initrd: Specifies the ramdisk image for ``uefi`` or ``bootmon`` boot.
:param bootargs: Specifies the boot arguments that will be pass to the
kernel by the bootloader.
:param uefi_entry: Then name of the UEFI entry to be used/created by
``uefi`` bootloader.
:param ready_timeout: Timeout, in seconds, for the time it takes the
platform to become ready to accept connections. Note:
this does not mean that the system is fully booted;
just that the services needed to establish a
connection (e.g. sshd or adbd) are up.
.. _gem5-platform:
Gem5 Simulation Platform
------------------------
By initialising a Gem5SimulationPlatform, devlib will start a gem5 simulation (based upon the
arguments the user provided) and then connect to it using :class:`Gem5Connection`.
Using the methods discussed above, some methods of the :class:`Target` will be altered
slightly to better suit gem5.
.. class:: Gem5SimulationPlatform(name, host_output_dir, gem5_bin, gem5_args, gem5_virtio, gem5_telnet_port=None)
During initialisation the gem5 simulation will be kicked off (based upon the arguments
provided by the user) and the telnet port used by the gem5 simulation will be intercepted
and stored for use by the :class:`Gem5Connection`.
:param name: Platform name
:param host_output_dir: Path on the host where the gem5 outputs will be placed (e.g. stats file)
:param gem5_bin: gem5 binary
:param gem5_args: Arguments to be passed onto gem5 such as config file etc.
:param gem5_virtio: Arguments to be passed onto gem5 in terms of the virtIO device used
to transfer files between the host and the gem5 simulated system.
:param gem5_telnet_port: Not yet in use as it would be used in future implementations
of devlib in which the user could use the platform to pick
up an existing and running simulation.
.. method:: Gem5SimulationPlatform.init_target_connection([target])
Based upon the OS defined in the :class:`Target`, the type of :class:`Gem5Connection`
will be set (:class:`AndroidGem5Connection` or :class:`AndroidGem5Connection`).
.. method:: Gem5SimulationPlatform.update_from_target([target])
This method provides specific setup procedures for a gem5 simulation. First of all, the m5
binary will be installed on the guest (if it is not present). Secondly, three methods
in the :class:`Target` will be monkey-patched:
- **reboot**: this is not supported in gem5
- **reset**: this is not supported in gem5
- **capture_screen**: gem5 might already have screencaps so the
monkey-patched method will first try to
transfer the existing screencaps.
In case that does not work, it will fall back
to the original :class:`Target` implementation
of :func:`capture_screen`.
Finally, it will call the parent implementation of :func:`update_from_target`.
.. method:: Gem5SimulationPlatform.setup([target])
The m5 binary be installed, if not yet installed on the gem5 simulated system.
It will also resize the gem5 shell, to avoid line wrapping issues.

View File

@@ -10,15 +10,15 @@ Target
:class:`Instrument`).
:param connection_settings: A ``dict`` that specifies how to connect to the remote
device. Its contents depend on the specific :class:`Target` type used (e.g.
:class:`AndroidTarget` expects the adb ``device`` name).
device. Its contents depend on the specific :class:`Target` type (used see
:ref:`connection-types`\ ).
:param platform: A :class:`Target` defines interactions at Operating System level. A
:class:`Platform` describes the underlying hardware (such as CPUs
available). If a :class:`Platform` instance is not specified on
:class:`Target` creation, one will be created automatically and it will
dynamically probe the device to discover as much about the underlying
hardware as it can.
hardware as it can. See also :ref:`platform`\ .
:param working_directory: This is primary location for on-target file system
interactions performed by ``devlib``. This location *must* be readable and
@@ -53,7 +53,7 @@ Target
:param modules: a list of additional modules to be installed. Some modules will
try to install by default (if supported by the underlying target).
Current default modules are ``hotplug``, ``cpufreq``, ``cpuidle``,
``cgroups``, and ``hwmon``.
``cgroups``, and ``hwmon`` (See :ref:`modules`\ ).
See modules documentation for more detail.
@@ -420,3 +420,15 @@ Target
Returns ``True`` if an executable with the specified name is installed on the
target and ``False`` other wise.
.. method:: Target.extract(path, dest=None)
Extracts the specified archive/file and returns the path to the extrated
contents. The extraction method is determined based on the file extension.
``zip``, ``tar``, ``gzip``, and ``bzip2`` are supported.
:param dest: Specified an on-target destination directory (which must exist)
for the extrated contents.
Returns the path to the extracted contents. In case of files (gzip and
bzip2), the path to the decompressed file is returned; for archives, the
path to the directory with the archive's contents is returned.

View File

@@ -59,7 +59,7 @@ for root, dirs, files in os.walk(devlib_dir):
params = dict(
name='devlib',
description='A framework for automating workload execution and measurment collection on ARM devices.',
version='0.0.1',
version='0.0.4',
packages=packages,
package_data=data_files,
url='N/A',
@@ -69,6 +69,7 @@ params = dict(
'python-dateutil', # converting between UTC and local time.
'pexpect>=3.3', # Send/recieve to/from device
'pyserial', # Serial port interface
'wrapt', # Basic for construction of decorator functions
],
extras_require={
'daq': ['daqpower'],

View File

@@ -4,7 +4,7 @@
#
CROSS_COMPILE?=aarch64-linux-gnu-
CC=$(CROSS_COMPILE)gcc
CFLAGS='-Wl,-static -Wl,-lc'
CFLAGS=-static -lc
readenergy: readenergy.c
$(CC) $(CFLAGS) readenergy.c -o readenergy

View File

@@ -89,6 +89,9 @@
// Default counter poll period (in milliseconds).
#define DEFAULT_PERIOD 100
// Default duration for the instrument execution (in seconds); 0 means 'forever'
#define DEFAULT_DURATION 0
// A single reading from the energy meter. The values are the proper readings converted
// to appropriate units (e.g. Watts for power); they are *not* raw counter values.
struct reading
@@ -141,12 +144,17 @@ int nsleep(const struct timespec *req, struct timespec *rem)
void print_help()
{
fprintf(stderr, "Usage: readenergy [-t PERIOD] -o OUTFILE\n\n"
fprintf(stderr, "Usage: readenergy [-t PERIOD] [-o OUTFILE]\n\n"
"Read Juno energy counters every PERIOD milliseconds, writing them\n"
"to OUTFILE in CSV format until SIGTERM is received.\n\n"
"to OUTFILE in CSV format either until SIGTERM is received OR\n"
"till the specified duration elapsed.\n"
"If OUTFILE is not specified, stdout will be used.\n\n"
"Parameters:\n"
" PERIOD is the counter poll period in milliseconds.\n"
" (Defaults to 100 milliseconds.)\n"
" DURATION is the duration before execution terminates.\n"
" (Defaults to 0 seconds, meaning run till user\n"
" terminates execution.\n"
" OUTFILE is the output file path\n");
}
@@ -163,6 +171,7 @@ struct config
{
struct timespec period;
char *output_file;
long duration_in_sec;
};
void config_init_period_from_millis(struct config *this, long millis)
@@ -175,9 +184,10 @@ void config_init(struct config *this, int argc, char *argv[])
{
this->output_file = NULL;
config_init_period_from_millis(this, DEFAULT_PERIOD);
this->duration_in_sec = DEFAULT_DURATION;
int opt;
while ((opt = getopt(argc, argv, "ht:o:")) != -1)
while ((opt = getopt(argc, argv, "ht:o:d:")) != -1)
{
switch(opt)
{
@@ -187,6 +197,9 @@ void config_init(struct config *this, int argc, char *argv[])
case 'o':
this->output_file = optarg;
break;
case 'd':
this->duration_in_sec = atol(optarg);
break;
case 'h':
print_help();
exit(EXIT_SUCCESS);
@@ -197,13 +210,6 @@ void config_init(struct config *this, int argc, char *argv[])
exit(EXIT_FAILURE);
}
}
if (this->output_file == NULL)
{
fprintf(stderr, "ERROR: Mandatory -o option not specified.\n\n");
print_help();
exit(EXIT_FAILURE);
}
}
// -------------------------------------- /config ---------------------------------------------------
@@ -219,13 +225,17 @@ struct emeter
void emeter_init(struct emeter *this, char *outfile)
{
this->out = fopen(outfile, "w");
if (this->out == NULL)
if(outfile)
{
fprintf(stderr, "ERROR: Could not open output file %s; got %s\n", outfile, strerror(errno));
exit(EXIT_FAILURE);
this->out = fopen(outfile, "w");
if (this->out == NULL)
{
fprintf(stderr, "ERROR: Could not open output file %s; got %s\n", outfile, strerror(errno));
exit(EXIT_FAILURE);
}
} else {
this->out = stdout;
}
this->fd = open("/dev/mem", O_RDONLY);
if(this->fd < 0)
{
@@ -243,10 +253,12 @@ void emeter_init(struct emeter *this, char *outfile)
exit(EXIT_FAILURE);
}
fprintf(this->out, "sys_curr,a57_curr,a53_curr,gpu_curr,"
"sys_volt,a57_volt,a53_volt,gpu_volt,"
"sys_pow,a57_pow,a53_pow,gpu_pow,"
"sys_cenr,a57_cenr,a53_cenr,gpu_cenr\n");
if(this->out) {
fprintf(this->out, "sys_curr,a57_curr,a53_curr,gpu_curr,"
"sys_volt,a57_volt,a53_volt,gpu_volt,"
"sys_pow,a57_pow,a53_pow,gpu_pow,"
"sys_cenr,a57_cenr,a53_cenr,gpu_cenr\n");
}
}
void emeter_read_measurements(struct emeter *this, struct reading *reading)
@@ -314,13 +326,19 @@ void emeter_finalize(struct emeter *this)
// -------------------------------------- /emeter ----------------------------------------------------
int done = 0;
volatile int done = 0;
void term_handler(int signum)
{
done = 1;
}
void sigalrm_handler(int signum)
{
done = 1;
}
int main(int argc, char *argv[])
{
struct sigaction action;
@@ -333,11 +351,27 @@ int main(int argc, char *argv[])
config_init(&config, argc, argv);
emeter_init(&emeter, config.output_file);
struct timespec remaining;
while (!done)
if (0 != config.duration_in_sec)
{
/*Set the alarm with the duration from use only if a non-zero value is specified
else it will run forever until SIGTERM signal received from user*/
/*Set the signal handler first*/
signal(SIGALRM, sigalrm_handler);
/*Now set the alarm for the duration specified by the user*/
alarm(config.duration_in_sec);
}
if(config.output_file)
{
struct timespec remaining;
while (!done)
{
emeter_take_reading(&emeter);
nsleep(&config.period, &remaining);
}
} else {
emeter_take_reading(&emeter);
nsleep(&config.period, &remaining);
}
emeter_finalize(&emeter);