Skip to content

Commit ce322ba

Browse files
authored
Merge branch 'labgrid-project:master' into available-places
2 parents 06b414b + de5257d commit ce322ba

22 files changed

+181
-89
lines changed

CHANGES.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Release 24.1 (Unreleased)
1+
Release 25.0 (Unreleased)
22
-------------------------
33
As announced
44
`before <https://github.com/labgrid-project/labgrid/discussions/1467#discussioncomment-10314852>`_,
@@ -14,25 +14,25 @@ That's why labgrid moves to gRPC with this release. gRPC is a well maintained
1414
RPC framework with a lot of users. As a side effect, the message transfer is
1515
more performant and the import times are shorter.
1616

17-
New Features in 24.1
17+
New Features in 25.0
1818
~~~~~~~~~~~~~~~~~~~~
1919
- All components can be installed into the same virtualenv again.
2020
- The `QEMUDriver` now supports setting the ``display`` option to
2121
``qemu-default``, which will neither set the QEMU ``-display`` option
2222
or pass along ``-nographic``.
2323

24-
Bug fixes in 24.1
24+
Bug fixes in 25.0
2525
~~~~~~~~~~~~~~~~~
2626

2727
FIXME
2828

29-
Breaking changes in 24.1
29+
Breaking changes in 25.0
3030
~~~~~~~~~~~~~~~~~~~~~~~~
3131
Maintaining support for both crossbar/autobahn as well as gRPC in labgrid would
3232
be a lot of effort due to the different architectures of those frameworks.
3333
Therefore, a hard migration to gRPC is deemed the lesser issue.
3434

35-
Due to the migration, 24.1 includes the following breaking changes:
35+
Due to the migration, 25.0 includes the following breaking changes:
3636

3737
- The labgrid environment config option ``crossbar_url`` was renamed to
3838
``coordinator_address``. The environment variable ``LG_CROSSBAR`` was renamed
@@ -51,7 +51,7 @@ Other breaking changes include:
5151
removed) xdrlib. See
5252
`issue #1507 <https://github.com/labgrid-project/labgrid/issues/1507>`_.
5353

54-
Known issues in 24.1
54+
Known issues in 25.0
5555
~~~~~~~~~~~~~~~~~~~~
5656

5757
FIXME

debian/changelog

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
labgrid (24.1.0) UNRELEASED; urgency=low
1+
labgrid (25.0.0) UNRELEASED; urgency=low
22

33
* See https://github.com/labgrid-project/labgrid/blob/master/CHANGES.rst
44

5-
-- Bastian Krause <bst@pengutronix.de> Tue, 13 Aug 2024 12:23:25 +0200
5+
-- Bastian Krause <bst@pengutronix.de> Fri, 21 Jan 2024 10:43:45 +0100
66

77
labgrid (24.0.0) UNRELEASED; urgency=low
88

debian/copyright

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ Upstream-Name: labgrid
33
Source: https://github.com/labgrid-project/labgrid
44

55
Files: *
6-
Copyright: Copyright (C) 2016-2024 Pengutronix, Jan Luebbe <entwicklung@pengutronix.de>
7-
Copyright (C) 2016-2024 Pengutronix, Rouven Czerwinski <entwicklung@pengutronix.de>
6+
Copyright: Copyright (C) 2016-2025 Pengutronix, Jan Luebbe <entwicklung@pengutronix.de>
7+
Copyright (C) 2016-2025 Pengutronix, Rouven Czerwinski <entwicklung@pengutronix.de>
88
License: LGPL-2.1+
99

1010
Files: man/*
11-
Copyright: Copyright (C) 2016-2024 Pengutronix
11+
Copyright: Copyright (C) 2016-2025 Pengutronix
1212
License: LGPL-2.1+
1313

1414
License: LGPL-2.1+

doc/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757

5858
# General information about the project.
5959
project = 'labgrid'
60-
copyright = '2016-2024 Pengutronix, Jan Luebbe and Rouven Czerwinski'
60+
copyright = '2016-2025 Pengutronix, Jan Luebbe and Rouven Czerwinski'
6161
author = 'Jan Luebbe, Rouven Czerwinski'
6262

6363
# The version info for the project you're documenting, acts as replacement for

dockerfiles/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ has_buildx() {
3131
local docker_cmd
3232
docker_cmd="${1}"
3333

34-
"${docker_cmd}" buildx --help >/dev/null 2>&1
34+
"${docker_cmd}" buildx version >/dev/null 2>&1
3535
}
3636

3737
get_docker_cmd() {

labgrid/driver/networkinterfacedriver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def configure(self, settings):
6060
self.proxy.configure(self.iface.ifname, settings)
6161

6262
@Driver.check_active
63-
@step()
63+
@step(args=["expected"])
6464
def wait_state(self, expected, timeout=60):
6565
"""Wait until the expected state is reached or the timeout expires.
6666

labgrid/remote/client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,9 +450,13 @@ def _check_allowed(self, place):
450450
if f"{self.gethostname()}/{self.getuser()}" not in place.allowed:
451451
host, user = place.acquired.split("/")
452452
if user != self.getuser():
453-
raise UserError(f"place {place.name} is not acquired by your user, acquired by {user}")
453+
raise UserError(
454+
f"place {place.name} is not acquired by your user, acquired by {user}. To work simultaneously, {user} can execute labgrid-client -p {place.name} allow {self.gethostname()}/{self.getuser()}"
455+
)
454456
if host != self.gethostname():
455-
raise UserError(f"place {place.name} is not acquired on this computer, acquired on {host}")
457+
raise UserError(
458+
f"place {place.name} is not acquired on this computer, acquired on {host}. To allow this host, use labgrid-client -p {place.name} allow {self.gethostname()}/{self.getuser()} on the other host"
459+
)
456460

457461
def get_place(self, place=None):
458462
pattern = place or self.args.place
@@ -470,7 +474,10 @@ def get_place(self, place=None):
470474
def get_idle_place(self, place=None):
471475
place = self.get_place(place)
472476
if place.acquired:
473-
raise UserError(f"place {place.name} is not idle (acquired by {place.acquired})")
477+
_, user = place.acquired.split("/")
478+
raise UserError(
479+
f"place {place.name} is not idle (acquired by {place.acquired}). To work simultaneously, {user} can execute labgrid-client -p {place.name} allow {self.gethostname()}/{self.getuser()}"
480+
)
474481
return place
475482

476483
def get_acquired_place(self, place=None):
@@ -1427,7 +1434,7 @@ async def create_reservation(self):
14271434
raise UserError(f"'{pair}' is not a valid filter (must contain a '=')")
14281435
if not TAG_KEY.match(k):
14291436
raise UserError(f"Key '{k}' in filter '{pair}' is invalid")
1430-
if not TAG_KEY.match(v):
1437+
if not TAG_VAL.match(v):
14311438
raise UserError(f"Value '{v}' in filter '{pair}' is invalid")
14321439
fltr[k] = v
14331440

labgrid/remote/coordinator.py

Lines changed: 125 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import time
99
from contextlib import contextmanager
1010
import copy
11+
import random
1112

1213
import attr
1314
import grpc
@@ -26,7 +27,7 @@
2627
from .scheduler import TagSet, schedule
2728
from .generated import labgrid_coordinator_pb2
2829
from .generated import labgrid_coordinator_pb2_grpc
29-
from ..util import atomic_replace, labgrid_version, yaml
30+
from ..util import atomic_replace, labgrid_version, yaml, Timeout
3031

3132

3233
@contextmanager
@@ -220,7 +221,7 @@ def __init__(self) -> None:
220221
self.load()
221222

222223
self.loop = asyncio.get_running_loop()
223-
for name in ["save", "reacquire", "schedule"]:
224+
for name in ["save", "sync_resources", "schedule"]:
224225
step_func = getattr(self, f"_poll_step_{name}")
225226
task = self.loop.create_task(self.poll(step_func), name=f"coordinator-poll-{name}")
226227
self.poll_tasks.append(task)
@@ -231,11 +232,11 @@ async def _poll_step_save(self):
231232
with warn_if_slow("save changes", level=logging.DEBUG):
232233
await self.save()
233234

234-
async def _poll_step_reacquire(self):
235-
# try to re-acquire orphaned resources
235+
async def _poll_step_sync_resources(self):
236+
# try to synchronize resources
236237
async with self.lock:
237-
with warn_if_slow("reacquire orphaned resources", limit=3.0):
238-
await self._reacquire_orphaned_resources()
238+
with warn_if_slow("synchronize resources", limit=3.0):
239+
await self._synchronize_resources()
239240

240241
async def _poll_step_schedule(self):
241242
# update reservations
@@ -638,6 +639,14 @@ async def _acquire_resources(self, place, resources):
638639
if resource.acquired:
639640
return False
640641

642+
for otherplace in self.places.values():
643+
for oldres in otherplace.acquired_resources:
644+
if resource.path == oldres.path:
645+
logging.info(
646+
"Conflicting orphaned resource %s for acquire request for place %s", oldres, place.name
647+
)
648+
return False
649+
641650
# acquire resources
642651
acquired = []
643652
try:
@@ -692,47 +701,124 @@ async def _release_resources(self, place, resources, callback=True):
692701
except:
693702
logging.exception("failed to publish released resource %s", resource)
694703

695-
async def _reacquire_orphaned_resources(self):
704+
async def _synchronize_resources(self):
696705
assert self.lock.locked()
697706

698-
for place in self.places.values():
699-
changed = False
707+
# fix:
708+
# - a resource is acquired for a place that is not acquired
709+
# * perhaps caused by a resource acquire timeout (during normal lock)
710+
# -> release()
711+
# - a resource is acquired for a place that still has it as orphaned
712+
# * perhaps caused by a resource acquire timeout (during reacquire)
713+
# -> replace orphaned resource
714+
# - a resource is released, but a place still has it as orphaned
715+
# * perhaps caused by a exporter restart
716+
# -> acquire() and replace orphaned resource
717+
718+
acquired_resources = {}
719+
used_resources = {}
720+
orphaned_resources = {}
721+
722+
# find acquired resources
723+
for exporter in self.exporters.values():
724+
for group in exporter.groups.values():
725+
for resource in group.values():
726+
if resource.acquired:
727+
acquired_resources[resource.path] = resource
700728

701-
for idx, resource in enumerate(place.acquired_resources):
729+
# find resources used by places
730+
for place in self.places.values():
731+
for resource in place.acquired_resources:
702732
if not resource.orphaned:
703-
continue
733+
used_resources[resource.path] = resource
734+
else:
735+
orphaned_resources[resource.path] = resource
736+
737+
timeout = Timeout(5.0)
738+
739+
# find resources to be released
740+
to_release = list(acquired_resources.keys() - used_resources.keys() - orphaned_resources.keys())
741+
if to_release:
742+
logging.info("synchronize resources: %s acquired resource(s) should be released", len(to_release))
743+
random.shuffle(to_release) # don't get stuck on a problematic resource
744+
for resource_path in to_release:
745+
if timeout.expired:
746+
continue # release the coordinator lock
747+
748+
resource = acquired_resources[resource_path]
749+
if resource.acquired == "<broken>":
750+
continue
751+
place = self.places.get(resource.acquired)
752+
print(f"should release {resource} for {place}?")
704753

705-
# is the exporter connected again?
706-
exporter = self.get_exporter_by_name(resource.path[0])
707-
if not exporter:
708-
continue
754+
if place is None:
755+
logging.warning("resource %s claims to be acquired by unknown place", resource)
756+
elif not place.acquired:
757+
logging.warning("resource %s claims to be acquired by unacquired place", resource)
758+
else:
759+
continue
760+
try:
761+
await self._release_resources(place, [resource])
762+
del acquired_resources[resource_path]
763+
except Exception:
764+
logging.exception("failed to release unused resource %s", resource)
765+
break
709766

710-
# does the resource exist again?
711-
try:
712-
new_resource = exporter.groups[resource.path[1]][resource.path[3]]
713-
except KeyError:
714-
continue
767+
# find orphaned resources to be acquired
768+
to_acquire = list(orphaned_resources.keys() - acquired_resources.keys())
769+
if to_acquire:
770+
logging.info("synchronize resources: %s orphaned resource(s) should be acquired", len(to_acquire))
771+
random.shuffle(to_acquire) # don't get stuck on a problematic resource
772+
for resource_path in to_acquire:
773+
if timeout.expired:
774+
continue # release the coordinator lock
775+
776+
resource = orphaned_resources[resource_path]
777+
if resource.acquired == "<broken>":
778+
continue
779+
place = self.places.get(resource.acquired)
780+
assert place is not None
781+
assert place.acquired
782+
print(f"should acquire {resource} for {place}?")
783+
784+
# is the exporter connected again?
785+
exporter = self.get_exporter_by_name(resource.path[0])
786+
if not exporter:
787+
continue
715788

716-
if new_resource.acquired:
717-
# this should only happen when resources become broken
718-
logging.debug("ignoring acquired/broken resource %s for place %s", new_resource, place.name)
719-
continue
789+
# does the resource exist again?
790+
try:
791+
new_resource = exporter.groups[resource.path[1]][resource.path[3]]
792+
except KeyError:
793+
continue
720794

721-
try:
722-
await self._acquire_resource(place, new_resource)
723-
place.acquired_resources[idx] = new_resource
724-
except Exception:
725-
logging.exception(
726-
"failed to reacquire orphaned resource %s for place %s", new_resource, place.name
727-
)
728-
break
729-
730-
logging.info("reacquired orphaned resource %s for place %s", new_resource, place.name)
731-
changed = True
732-
733-
if changed:
734-
self._publish_place(place)
735-
self.save_later()
795+
if new_resource.acquired:
796+
# this should only happen when resources become broken
797+
logging.warning("ignoring acquired/broken resource %s for place %s", new_resource, place.name)
798+
continue
799+
800+
try:
801+
await self._acquire_resource(place, new_resource)
802+
acquired_resources[new_resource.path] = new_resource
803+
except Exception:
804+
logging.exception("failed to reacquire orphaned resource %s for place %s", new_resource, place.name)
805+
break
806+
807+
# find orphaned resources to be replaced in the places
808+
to_replace = set(orphaned_resources.keys() & acquired_resources.keys())
809+
if to_replace:
810+
logging.info("synchronize resources: %s orphaned resource(s) should be replaced", len(to_replace))
811+
for resource_path in set(orphaned_resources.keys() & acquired_resources.keys()):
812+
oldresource = orphaned_resources[resource_path]
813+
newresource = acquired_resources[resource_path]
814+
assert oldresource.acquired == newresource.acquired
815+
816+
place = self.places.get(newresource.acquired)
817+
assert place is not None
818+
assert place.acquired
819+
820+
idx = place.acquired_resources.index(oldresource)
821+
place.acquired_resources[idx] = newresource
736822

737823
@locked
738824
async def AcquirePlace(self, request, context):
@@ -755,9 +841,6 @@ async def AcquirePlace(self, request, context):
755841
if not res.owner == username:
756842
await context.abort(grpc.StatusCode.PERMISSION_DENIED, f"Place {name} was not reserved for {username}")
757843

758-
# First try to reacquire orphaned resources to avoid conflicts.
759-
await self._reacquire_orphaned_resources()
760-
761844
# FIXME use the session object instead? or something else which
762845
# survives disconnecting clients?
763846
place.acquired = username

labgrid/resource/remote.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ class NetworkSigrokUSBDevice(RemoteUSBResource):
186186
default=None,
187187
validator=attr.validators.optional(attr.validators.instance_of(str))
188188
)
189+
channel_group = attr.ib(
190+
default=None,
191+
validator=attr.validators.optional(attr.validators.instance_of(str))
192+
)
189193
def __attrs_post_init__(self):
190194
self.timeout = 10.0
191195
super().__attrs_post_init__()
@@ -203,6 +207,10 @@ class NetworkSigrokUSBSerialDevice(RemoteUSBResource):
203207
default=None,
204208
validator=attr.validators.optional(attr.validators.instance_of(str))
205209
)
210+
channel_group = attr.ib(
211+
default=None,
212+
validator=attr.validators.optional(attr.validators.instance_of(str))
213+
)
206214
def __attrs_post_init__(self):
207215
self.timeout = 10.0
208216
super().__attrs_post_init__()

0 commit comments

Comments
 (0)