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,31 @@ 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+ # Split command string into command and following arguments
53+ try :
54+ cmd , remainder = cmd_str .split (" " , 1 )
55+ except ValueError :
56+ cmd , remainder = cmd_str , None
57+
58+ # Lookup command in command table
5159 if cmd not in COMMANDS :
5260 raise ValueError (f"Unknown command: '{ cmd } '" )
5361 job_cls = COMMANDS [cmd ]
54- return job_cls (cmd , args , client_id )
62+
63+ # For eval, preserve the remainder as a single string argument and de-quote it
64+ # Otherwise, split remainder into separate arguments
65+ if remainder :
66+ if cmd == "eval" :
67+ args = [remainder .strip ("\" '" )]
68+ else :
69+ args = remainder .split (" " )
70+ else :
71+ args = []
72+
73+ # Create and return the job instance
74+ return job_cls (cmd , args , client_id , globals = globals )
5575
5676
5777class SequentialJob (Job ):
@@ -266,10 +286,7 @@ def output(self):
266286 op = machine .reset if self .mode == "hard" else machine .soft_reset
267287 except AttributeError :
268288 raise OSError ("Operation not supported on this platform" )
269- if self .mode == "hard" :
270- logging .critical (msg )
271- else :
272- logging .warning (msg )
289+ logging .critical (msg )
273290
274291 # Schedule reboot in three seconds
275292 async def reboot_callback (op ):
@@ -282,6 +299,41 @@ async def reboot_callback(op):
282299 return BytesIO (msg .encode ("utf-8" ))
283300
284301
302+ class RunPyJob (Job ):
303+ """A job to evaluate Python script on the device."""
304+
305+ argc = 1
306+
307+ def output (self ):
308+ """Eval or exec given Python and return the result."""
309+ expr = self .args [0 ]
310+ try :
311+ result = self .do_eval (expr )
312+ except SyntaxError : # Not an expression, try exec
313+ result = self .do_exec (expr )
314+ if isinstance (result , str ): # Ensure bytes output
315+ result = result .encode ("utf-8" )
316+ return BytesIO (result )
317+
318+ def do_eval (self , expr ):
319+ """Evaluate a Python expression and return the result."""
320+ op = compile (expr , "<string>" , "eval" )
321+ result = eval (op , self .globals , None )
322+ return repr (result )
323+
324+ def do_exec (self , expr ):
325+ """Execute a Python statement and return the output."""
326+ out_buf = BytesIO ()
327+ old_term = os .dupterm (out_buf )
328+ try :
329+ op = compile (expr , "<string>" , "exec" )
330+ exec (op , self .globals , None )
331+ result = out_buf .getvalue ()
332+ finally :
333+ os .dupterm (old_term )
334+ return result
335+
336+
285337# Map commands to associated job names
286338COMMANDS = {
287339 "whoami" : WhoAmIJob ,
@@ -291,4 +343,5 @@ async def reboot_callback(op):
291343 "cp" : PutFileJob ,
292344 "ota" : FirmwareUpdateJob ,
293345 "reboot" : RebootJob ,
346+ "eval" : RunPyJob ,
294347}
0 commit comments