11import asyncio
22import logging
3+ import os
34from binascii import hexlify
45from hashlib import sha256
56from io import BytesIO
@@ -22,6 +23,7 @@ def __init__(self, cmd, args=[], client_id="localhost", **kwargs):
2223 f"Wrong number of arguments for { cmd } ; expected { self .argc } "
2324 )
2425
26+ self .globals = kwargs .get ("globals" , {})
2527 self .cmd = cmd
2628 self .args = args
2729 self .client_id = client_id
@@ -45,13 +47,21 @@ def ready(self):
4547 return True
4648
4749 @classmethod
48- def from_cmd (cls , cmd_str , client_id = None ):
49- """Create a job from a command string, e.g. 'get_file file1.txt'."""
50- cmd , * args = cmd_str .split ()
50+ def from_cmd (cls , cmd_str , client_id = None , globals = {}):
51+ """Create a job from a command string, e.g. 'cat file1.txt'."""
52+ try :
53+ cmd , remainder = cmd_str .split (" " , 1 )
54+ except ValueError :
55+ cmd , remainder = cmd_str , None
5156 if cmd not in COMMANDS :
5257 raise ValueError (f"Unknown command: '{ cmd } '" )
5358 job_cls = COMMANDS [cmd ]
54- return job_cls (cmd , args , client_id )
59+ # For eval, preserve the remainder as a single string argument
60+ if remainder :
61+ args = [remainder .strip ()] if cmd == "eval" else remainder .split (" " )
62+ else :
63+ args = []
64+ return job_cls (cmd , args , client_id , globals = globals )
5565
5666
5767class SequentialJob (Job ):
@@ -282,6 +292,39 @@ async def reboot_callback(op):
282292 return BytesIO (msg .encode ("utf-8" ))
283293
284294
295+ class RunPyJob (Job ):
296+ """A job to evaluate Python script on the device."""
297+
298+ argc = 1
299+
300+ def output (self ):
301+ """Eval or exec given Python and return the result."""
302+ expr = self .args [0 ]
303+ try :
304+ result = self .do_eval (expr )
305+ except SyntaxError : # Not an expression, try exec
306+ result = self .do_exec (expr )
307+ return BytesIO (result .encode ("utf-8" ))
308+
309+ def do_eval (self , expr ):
310+ """Evaluate a Python expression and return the result."""
311+ op = compile (expr , "<string>" , "eval" )
312+ result = eval (op , self .globals , None )
313+ return repr (result )
314+
315+ def do_exec (self , expr ):
316+ """Execute a Python statement and return the output."""
317+ out_buf = BytesIO ()
318+ old_term = os .dupterm (out_buf )
319+ try :
320+ op = compile (expr , "<string>" , "exec" )
321+ exec (op , self .globals , None )
322+ result = out_buf .getvalue ()
323+ finally :
324+ os .dupterm (old_term )
325+ return result
326+
327+
285328# Map commands to associated job names
286329COMMANDS = {
287330 "whoami" : WhoAmIJob ,
@@ -291,4 +334,5 @@ async def reboot_callback(op):
291334 "cp" : PutFileJob ,
292335 "ota" : FirmwareUpdateJob ,
293336 "reboot" : RebootJob ,
337+ "eval" : RunPyJob ,
294338}
0 commit comments