From 0b0029563acca71d623901eb65bf94a9abb61026 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 14:38:10 -0700 Subject: [PATCH 01/14] Maybe fix race condition when using multiple nodes in run_command --- src/meshctrl/session.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 5b7bdcb..354b3f0 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1506,7 +1506,6 @@ class Session(object): async def __(command, tg, tasks): data = await self._send_command(command, "run_command", timeout=timeout) - if data.get("type", None) != "runcommands" and data.get("result", "ok").lower() != "ok": raise exceptions.ServerError(data["result"]) elif data.get("type", None) != "runcommands" and data.get("result", "ok").lower() == "ok": @@ -1533,6 +1532,9 @@ class Session(object): console_task.cancel() elif data.get("type", None) == "runcommands" and not ignore_output: tasks.append(tg.create_task(asyncio.wait_for(_reply(data["responseid"], start_data=data), timeout=timeout))) + # Force this to run immediately? This might be odd; but we want to make sure we get don't lose the race condition with the srever. + # Not sure if this actually works but I haven't yet seen it fail. *shrug* + await asyncio.sleep(0) tasks = [] async with asyncio.TaskGroup() as tg: From bc1db8f2b3cb38c831f5c95abdbc3cbe8a493186 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 14:40:53 -0700 Subject: [PATCH 02/14] Update documentation for `files.rm` Resolves #53 --- src/meshctrl/files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshctrl/files.py b/src/meshctrl/files.py index b8958ac..3a74bb5 100644 --- a/src/meshctrl/files.py +++ b/src/meshctrl/files.py @@ -157,7 +157,7 @@ class Files(tunnel.Tunnel): async def rm(self, path, files, recursive=False, timeout=None): """ - Create a directory on the device. This API doesn't error if the file doesn't exist. + Remove a set of files or directories from the device. This API doesn't error if the file doesn't exist. Args: path (str): Directory from which to delete files From d4b952481460938daab9b089708d3829d9828d9a Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Wed, 10 Sep 2025 16:53:48 +0200 Subject: [PATCH 03/14] feat(lib): draft function for remove_device --- src/meshctrl/session.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index f047776..8ef3c40 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1062,6 +1062,27 @@ class Session(object): raise exceptions.ServerError(data["result"]) return True + async def remove_device(self, nodeids, timeout=None): + ''' + Remove device from MeshCentral + + Args: + nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed + timeout (int): duration in seconds to wait for a response before throwing an error + + Returns: + bool: True on success, raise otherwise + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' + if isinstance(nodeids, str): + nodeids = [nodeids] + + data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_device_from_server", timeout=timeout) + print(data) async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None): ''' From 1f9979ddd1179c0a6d4fce87e7ae14aa2e399a99 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Mon, 15 Sep 2025 22:28:57 +0200 Subject: [PATCH 04/14] feat: add remove_device function --- src/meshctrl/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 8ef3c40..7b345b5 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1082,7 +1082,8 @@ class Session(object): nodeids = [nodeids] data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_device_from_server", timeout=timeout) - print(data) + + return data["result"] == "ok" async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None): ''' From c7d628716e16e2ce63e90da26eebe5af3b57e236 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Tue, 16 Sep 2025 23:12:35 +0200 Subject: [PATCH 05/14] refac: renamed and added device class impl --- src/meshctrl/device.py | 21 +++++++++++++++++++++ src/meshctrl/session.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/meshctrl/device.py b/src/meshctrl/device.py index 3efefe8..2b20619 100644 --- a/src/meshctrl/device.py +++ b/src/meshctrl/device.py @@ -295,6 +295,27 @@ class Device(object): ''' return await self._session.reset_devices(self.nodeid, timeout=timeout) + async def remove(self, nodeids, timeout=None): + ''' + Remove device from MeshCentral + + Args: + nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed + timeout (int): duration in seconds to wait for a response before throwing an error + + Returns: + bool: True on success, raise otherwise + + Raises: + :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure + :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure + asyncio.TimeoutError: Command timed out + ''' + if isinstance(nodeids, str): + nodeids = [nodeids] + + return self._session.remove_devices(self, nodeids, timeout) + async def sleep(self, timeout=None): ''' Sleep device diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 7b345b5..57196bd 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1062,7 +1062,7 @@ class Session(object): raise exceptions.ServerError(data["result"]) return True - async def remove_device(self, nodeids, timeout=None): + async def remove_devices(self, nodeids, timeout=None): ''' Remove device from MeshCentral From 6dae40eb4009fa0add89aeb1907e9c1392418675 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Tue, 16 Sep 2025 23:14:23 +0200 Subject: [PATCH 06/14] refac: copy other style --- src/meshctrl/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshctrl/device.py b/src/meshctrl/device.py index 2b20619..93b40a1 100644 --- a/src/meshctrl/device.py +++ b/src/meshctrl/device.py @@ -314,7 +314,7 @@ class Device(object): if isinstance(nodeids, str): nodeids = [nodeids] - return self._session.remove_devices(self, nodeids, timeout) + return self._session.remove_devices(self.nodeid, timeout) async def sleep(self, timeout=None): ''' From 748e39d5b429ccc500ed939a644df87011bd38ca Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Tue, 16 Sep 2025 23:15:23 +0200 Subject: [PATCH 07/14] refac: remove nodeid parameter --- src/meshctrl/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshctrl/device.py b/src/meshctrl/device.py index 93b40a1..adc58b7 100644 --- a/src/meshctrl/device.py +++ b/src/meshctrl/device.py @@ -295,7 +295,7 @@ class Device(object): ''' return await self._session.reset_devices(self.nodeid, timeout=timeout) - async def remove(self, nodeids, timeout=None): + async def remove(self, timeout=None): ''' Remove device from MeshCentral From 7ba6989325eda6c5f539e8ea471dff2733fa02d3 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Tue, 16 Sep 2025 23:16:59 +0200 Subject: [PATCH 08/14] refac: I lied, this is the last... --- src/meshctrl/device.py | 4 ---- src/meshctrl/session.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/meshctrl/device.py b/src/meshctrl/device.py index adc58b7..ee9a651 100644 --- a/src/meshctrl/device.py +++ b/src/meshctrl/device.py @@ -307,13 +307,9 @@ class Device(object): bool: True on success, raise otherwise Raises: - :py:class:`~meshctrl.exceptions.ServerError`: Error text from server if there is a failure :py:class:`~meshctrl.exceptions.SocketError`: Info about socket closure asyncio.TimeoutError: Command timed out ''' - if isinstance(nodeids, str): - nodeids = [nodeids] - return self._session.remove_devices(self.nodeid, timeout) async def sleep(self, timeout=None): diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 57196bd..4638863 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1064,7 +1064,7 @@ class Session(object): async def remove_devices(self, nodeids, timeout=None): ''' - Remove device from MeshCentral + Remove device(s) from MeshCentral Args: nodeids (str|list[str]): nodeid(s) of the device(s) that have to be removed From 9c7a8c39b056e84348975aff4d2759ece706b4fc Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 15:19:57 -0700 Subject: [PATCH 09/14] Modified some implementation details --- src/meshctrl/session.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 4638863..32740de 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -571,7 +571,7 @@ class Session(object): while True: data = await event_queue.get() if filter and not util.compare_dict(filter, data): - continue + continue yield data finally: self._eventer.off("server_event", _) @@ -1081,9 +1081,11 @@ class Session(object): if isinstance(nodeids, str): nodeids = [nodeids] - data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_device_from_server", timeout=timeout) + data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_devices", timeout=timeout) - return data["result"] == "ok" + if data.get("result", "ok").lower() != "ok": + raise exceptions.ServerError(data["result"]) + return True async def add_device_group(self, name, description="", amtonly=False, features=0, consent=0, timeout=None): ''' From 3bcedf5610cbed1384c19ce447e40500ce029c62 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 15:20:25 -0700 Subject: [PATCH 10/14] Kinda added a test for remove device --- tests/test_session.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_session.py b/tests/test_session.py index dd1c973..13bd48b 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -336,6 +336,10 @@ async def test_mesh_device(env): r = await admin_session.remove_users_from_device_group((await privileged_session.user_info())["_id"], mesh.meshid, timeout=10) print("\ninfo remove_users_from_device_group: {}\n".format(r)) assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from device group" + + # Dunno how to test if this actually works, so just check for errors, I guess. + admin_session.remove_devices(agent.nodeid, timeout=10) + assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device" From b0d071d87f38b3a8e0229a2566a5dd2c203e7711 Mon Sep 17 00:00:00 2001 From: Daan Selen Date: Mon, 15 Sep 2025 22:28:57 +0200 Subject: [PATCH 11/14] feat: add remove_device function --- src/meshctrl/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshctrl/session.py b/src/meshctrl/session.py index 32740de..6aebdf1 100644 --- a/src/meshctrl/session.py +++ b/src/meshctrl/session.py @@ -1080,7 +1080,7 @@ class Session(object): ''' if isinstance(nodeids, str): nodeids = [nodeids] - + data = await self._send_command({ "action": 'removedevices', "nodeids": nodeids}, "remove_devices", timeout=timeout) if data.get("result", "ok").lower() != "ok": From c13985739bb9547cdcc25754d9891fbb5c55b7d4 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 15:37:55 -0700 Subject: [PATCH 12/14] Added release notes for 1.3.0 --- CHANGELOG.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 844025c..9f5673a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,17 @@ Changelog ========= +version 1.3.0 +============= + +Improvements: + * Improved how run_commands was handled (#51) + * Added remove device functionality (#52) + * Added run_console_commands functionality (#55) + +Bugs: + * Silly documentation being wrong (#53) + version 1.2.2 ============= From fb3d0434317cafab0d80f98f4137804282905944 Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 15:39:57 -0700 Subject: [PATCH 13/14] Added Daan to contributors --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 67ce357..8228781 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -3,3 +3,4 @@ Contributors ============ * Josiah Baldwin +* Daan Selen \ No newline at end of file From 61053549f2e03fc56da7954770c98f11a83eff6e Mon Sep 17 00:00:00 2001 From: Josiah Baldwin Date: Fri, 26 Sep 2025 15:54:25 -0700 Subject: [PATCH 14/14] Fixed test for remove device --- tests/test_session.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index 13bd48b..564e4f9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -337,8 +337,13 @@ async def test_mesh_device(env): print("\ninfo remove_users_from_device_group: {}\n".format(r)) assert (r[(await privileged_session.user_info())["_id"]]["success"]), "Failed to remove user from device group" - # Dunno how to test if this actually works, so just check for errors, I guess. - admin_session.remove_devices(agent.nodeid, timeout=10) + await admin_session.remove_devices(agent2.nodeid, timeout=10) + try: + await admin_session.device_info(agent2.nodeid, timeout=10) + except ValueError: + pass + else: + raise Exception("Device not deleted") assert (await admin_session.remove_users_from_device(agent.nodeid, (await unprivileged_session.user_info())["_id"], timeout=10)), "Failed to remove user from device"