import os as __os import sys as __sys import pwd as __pwd import time as __time import traceback from .config import app_path debug = 'GRPC_DEBUG' in __os.environ ### ### TODO: (1) perhaps add __main__ which execs casaviewer app ### https://stackoverflow.com/a/55346918 ### ### ### Load the necessary gRPC libraries and bindings. ### ### The generated wrappers assume that all of the wrappers can be ### loaded using the standard sys.path so here we add the path to ### the wrappers, load all of the needed wrappers, and then remove ### the private path from sys.path ### __sys.path.insert(0, __os.path.dirname(__os.path.abspath(__file__))) import grpc as __grpc from . import shutdown_pb2_grpc as __sd from . import ping_pb2_grpc as __ping from google.protobuf import empty_pb2 as __empty_p from . import img_pb2 as __img_p from . import img_pb2_grpc as __img_rpc __sys.path.pop(0) ### ### Tried to subclass to add __hash__( ) function to Id message (needed ### to be able to build a dict of Ids), but got error "TypeError: A ### Message class can only inherit from Message". At first I assumed ### this must be Google (gRPC) trying to add some structure to Python, ### but after brief searching, it seems like just Python brokenness... ### def __h(id): return id.id def make_id(v): i = __img_p.Id( ) i.id = v return i def is_id(o): ### "type(o) == __img_p.Id" fails... return o.__class__.__name__ == __img_p.Id.__name__ and o.__class__.__module__ == __img_p.Id.__module__ __proc = { '--server': None, '--nogui': None } __stub = { '--server': None, '--nogui': None } __channel = { '--server': None, '--nogui': None } __uri = { '--server': None, '--nogui': None } __stub_id = { '--server': None, '--nogui': None } __health_check = { '--server': 0, '--nogui': 0 } __id_gui_state = { } __registered = False __try_check_health = True ### ### When casaviewer is started (via __launch) without casatools, this function ### is called to shutdown the casaviewer app when the user exits python. ### def __shutdown_sans_casatools( ): global __uri for k in __uri: if __uri[k] is not None: channel = __grpc.insecure_channel(__uri[k]) shutdown = __sd.ShutdownStub(channel) shutdown.now(__empty_p.Empty( )) ### ### A named-pipe is used for communication when casaviewer is started ### without casatools. The --server=... flag to the casaviewer app ### accepts wither a named pipe (path) or a gRPC URI. The URI for ### the casaviewer app is passed back through the named pipe. ### def __fifo_name(index): count = 0 path = "/tmp/._casaviewer_%s_%s_%s_" % (__pwd.getpwuid(__os.getuid()).pw_name, __os.getpid( ), count) while __os.path.exists(path): count = count + 1 path = "/tmp/._casaviewer_%s_%s_%s_" % (__pwd.getpwuid(__os.getuid()).pw_name, __os.getpid( ), count) return path ### ### Create a named pipe... ### def __mkfifo( ): path = __fifo_name(0) __os.mkfifo(path) return path ### ### Launch the casaviewer app in either the casatools context (gRPC URI) ### or the stand-alone context (named pipe). ### def __launch(server="--server"): from subprocess import Popen, STDOUT import argparse as _argparse global __proc global __uri np_path = None # need to fetch any --rcdir and --nogui from __sys.argv parse = _argparse.ArgumentParser(add_help=False) parse.add_argument("--rcdir",dest='rcdir',default=None) parse.add_argument("--nogui",dest='nogui',action='store_const',const=True,default=False) _flags,_args = parse.parse_known_args(__sys.argv) rcdir = [ ] if _flags.rcdir is not None: rcdir = [ "--rcdir=%s" % __os.path.expanduser(_flags.rcdir) ] nogui = [ ] if _flags.nogui: nogui = [ "--nogui" ] data_path = [ ] try: from casatools import ctsys as ct data_path = [ "--datapath=%s" % ct.rundata( ) ] except: pass if __uri[server] is None or __proc[server] is None: try: from casatasks import casalog from casatools import ctsys with open(__os.devnull, 'r+b', 0) as DEVNULL: __proc[server] = Popen( [ app_path, '--casalogfile=%s' % casalog.logfile( ), '%s=%s' % (server,ctsys.registry( )['uri']) ] + data_path + rcdir + nogui, # stdin=DEVNULL, stdout=DEVNULL, stderr=STDOUT, # stdin=DEVNULL, stdout=STDOUT, stderr=STDOUT, close_fds=True, env={k:v for k,v in __os.environ.copy().items() if 'MPI' not in k} ) __time.sleep(2) # give it a second to launch count = 0 while __uri[server] is None and count < 50: # search for registered viewer print("(%s) waiting for viewer process..." % count) for k,v in ctsys.services( ).items( ): if 'id' in v: print("\t...%s" % repr(v)) id = v['id'].split(':')[0] if id == 'casaviewer': if debug: print("located casaviewer... %s" % v['id']) __uri[server] = v['uri'] __stub_id[server] = v['id'] break count = count + 1 __time.sleep(1) if __uri[server] is None: print("could not sync with casaviewer...") except ModuleNotFoundError: try: np_path = __mkfifo( ) with open(__os.devnull, 'r+b', 0) as DEVNULL: # __proc = Popen( [ app_path, '%s=%s' % (server,np_path) ], # stdin=DEVNULL, stdout=STDOUT, stderr=STDOUT, # close_fds=True ) __proc[server] = Popen( [ app_path, '--casalogfile=%s' % casalog.logfile(), '%s=%s' % (server,np_path) ] + data_path + rcdir + nogui, env={k:v for k,v in __os.environ.copy().items() if 'MPI' not in k} ) with open( np_path, 'r' ) as input: __uri[server] = input.readline( ).rstrip( ) print("casaviewer: %s" % __uri[server]) __os.remove(np_path) global __registered if not __registered: import atexit atexit.register(__shutdown_sans_casatools) __registered = True except: print("error: casaviewer launch failed...") __uri[server] = None __os.remove(np_path) return __uri[server] def __extract_region_box( reg ): if 'regions' in reg : if type(reg['regions']) != dict or '*1' not in reg['regions'] : raise Exception("invalid region, has 'regions' field but wrong format") reg=reg['regions']['*1'] if 'trc' not in reg or 'blc' not in reg : raise Exception("region must have a 'blc' and 'trc' field") blc_r = reg['blc'] trc_r = reg['trc'] if type(blc_r) != dict or type(trc_r) != dict : raise Exception("region blc/trc of wrong type") blc_k = list(blc_r.keys( )) trc_k = list(trc_r.keys( )) if len(blc_k) < 2 or len(trc_k) < 2: raise Exception("degenerate region") blc_k.sort( ) trc_k.sort( ) if type(blc_r[blc_k[0]]) != dict or type(blc_r[blc_k[1]]) != dict or \ type(trc_r[trc_k[0]]) != dict or type(trc_r[trc_k[1]]) != dict : raise Exception("invalid blc/trc in region") if 'value' not in blc_r[blc_k[0]] or 'value' not in blc_r[blc_k[1]] or \ 'value' not in trc_r[trc_k[0]] or 'value' not in trc_r[trc_k[1]]: raise Exception("invalid shape for blc/trc in region") if (type(blc_r[blc_k[0]]['value']) != float and type(blc_r[blc_k[0]]['value']) != int) or \ (type(blc_r[blc_k[1]]['value']) != float and type(blc_r[blc_k[1]]['value']) != int) or \ (type(trc_r[trc_k[0]]['value']) != float and type(trc_r[trc_k[0]]['value']) != int) or \ (type(trc_r[trc_k[0]]['value']) != float and type(trc_r[trc_k[0]]['value']) != int) : raise Exception("invalid type for blc/trc value in region") blc = [ float(blc_r[blc_k[0]]['value']), float(blc_r[blc_k[1]]['value']) ] trc = [ float(trc_r[trc_k[0]]['value']), float(trc_r[trc_k[1]]['value']) ] coord = "pixel" if 'name' in reg and reg['name'] == "WCBox": coord = "world" return ( blc, trc, coord ) def __stub_check(serv_str): from time import time global __stub, __health_check, __proc, __try_check_health s = __stub[serv_str] if s is None: raise RuntimeError("invalid service string") else: cur = time( ) if __try_check_health: __health_check[serv_str] = cur try: channel = __grpc.insecure_channel(__uri[serv_str]) ping = __ping.PingStub(channel) if debug: print("pinging viewer...") ping.now(__empty_p.Empty( ),timeout=5) if debug: print("viewer responded to ping...") return s except: from casatasks import casalog from casatools import ctsys print("viewer did not responded to ping... restarting...") __proc[serv_str].kill( ) __proc[serv_str] = None __stub[serv_str] = None # unregister presumed defunct casaviewer process... casalog.origin('viewertool') if ctsys.remove_service( __uri[serv_str] ): casalog.post("successfully removed defunct viewer: %s" % __uri[serv_str]) __uri[serv_str] = None else: casalog.post("failed to remove defunct viewer: %s" % __uri[serv_str]) __try_check_health = False return stub(True if serv_str == "--server" else False) else: return s ### ### Get the casaviewer app proxy; if the casaviewer app has not been ### launched, then the first time stub is called it will launch the ### casaviewer app and create the stub either by reading the app's ### URI through a named pipe or retrieving it from the casatools ### registry. ### def stub(context={}): global __stub, __channel if type(context) is bool: serv_str = "--server" if context else "--nogui" if __stub[serv_str] is None: uri = __launch(serv_str) if uri is None: print("error: casaviewer launch failed...") else: __channel[serv_str] = __grpc.insecure_channel(uri) __stub[serv_str] = __img_rpc.viewStub(__channel[serv_str]) return __stub_check(serv_str) elif is_id(context): if __h(context) in __id_gui_state: serv_str = "--server" if __id_gui_state[__h(context)] else "--nogui" return __stub_check(serv_str) elif panel == make_id(0): return stub(True) else: raise Exception("Id %s not found" % context) else: raise Exception("context must either be a boolean or an id") ### ### Get/set viewer's current working directory... ### def cwd( new_path='', gui=True ): if type(new_path) != str: raise Exception("cwd() takes a single (optional) string...") if type(gui) != bool: raise Exception("gui parameter should be a boolean") if gui is False and __sys.platform != 'linux': raise Exception("non-gui operation is only supported on Linux") pin = __img_p.Path( ) pin.path = new_path return stub(gui).cwd(pin).path ### ### get the type of a particular file/dir ### def fileinfo( path, gui=True ): if type(path) != str: raise Exception("fileinfo() takes a single path...") if type(gui) != bool: raise Exception("gui parameter should be a boolean") if gui is False and __sys.platform != 'linux': raise Exception("non-gui operation is only supported on Linux") pin = __img_p.Path( ) pin.path = path return stub(gui).fileinfo(pin).type ### ### get info about a casaviewer id ### def keyinfo( key ): if not is_id(key): raise Exception("keyinfo() takes a casaviewer id...") return stub(key).keyinfo(key).type ### ### Create a new panel in the casaviewer app... returns panel id ### def panel( paneltype="viewer", gui=True ) : if type(paneltype) != str or (paneltype != "viewer" and paneltype != "clean"): if not (paneltype.endswith('.rstr') and __os.path.isfile(paneltype)): raise Exception("the only valid panel types are 'viewer' and 'clean' or path to restore file") if type(gui) != bool: raise Exception("gui parameter should be a boolean") if gui is False and __sys.platform != 'linux': raise Exception("non-gui operation is only supported on Linux") panel_req = __img_p.NewPanel( ) panel_req.hidden = False panel_req.type = paneltype result = stub(gui).panel(panel_req) if result is not None: __id_gui_state[__h(result)] = gui return result ### ### load data into a panel... returns data id ### def load( path, displaytype="raster", panel=make_id(0), scaling=0 ): if type(path) != str or type(displaytype) != str or \ (type(scaling) != float and not is_id(panel)) : raise Exception("load() takes two strings; only the first arg is required...") nd = __img_p.NewData( ) nd.panel.CopyFrom(panel) nd.path = path nd.type = displaytype nd.scale = scaling result = stub(panel).load(nd) if result is not None: __id_gui_state[__h(result)] = __id_gui_state[__h(panel)] return result ### ### close panel... no return value ### def close( panel=make_id(0) ): if not is_id(panel) : raise Exception("close() takes one optional integer...") stub(panel).close(panel) ### ### set data range to data which should be [min, max]... no return vaue ### def datarange( range, data=make_id(0) ): if type(range) != list or not is_id(data) or \ all( map( lambda x: type(x) == int or type(x) == float, range ) ) == False: raise Exception("datarange() takes (numeric list,int)...") if len(range) != 2 or range[0] > range[1] : raise Exception("range should be [ min, max ]...") rng = __img_p.DataRange( ) rng.data.CopyFrom(data) rng.min = range[0] rng.max = range[1] stub(data).datarange(rng) ### ### set the channel that is displayed... no return value ### def channel( num=-1, panel=make_id(0) ): if type(num) != int or not is_id(panel): raise Exception("frame() takes (int,id); each argument is optional...") sc = __img_p.SetChannel( ) sc.panel.CopyFrom(panel) sc.number = num stub(panel).channel(sc) ### ### set colormap for a particular panel... no return value ### def colormap( map, data_or_panel=make_id(0) ): if type(map) != str or not is_id(data_or_panel): raise Exception("colormap() takes a colormap name and an optional panel or data id...") cm = __img_p.ColorMap( ) cm.id.CopyFrom(data_or_panel) cm.map = map stub(data_or_panel).colormap(cm) ### ### show or hide colorwedge... no return value ### def colorwedge( show, data_or_panel=make_id(0) ): if type(show) != bool or not is_id(data_or_panel): raise Exception("colorwedge() takes a boolean and an optional panel or data id...") cw = __img_p.Toggle( ) cw.id.CopyFrom(data_or_panel) cw.state = show stub(data_or_panel).colorwedge(cw) ### ### freeze gui for multiple changes... no return value ### def freeze( panel=make_id(0) ): if not is_id(panel) : raise Exception("freeze() takes only a panel id...") stub(panel).freeze(panel) ### ### unfreeze gui after multiple changes... no return value ### def unfreeze( panel=make_id(0) ): if not is_id(panel) : raise Exception("unfreeze() takes only a panel id...") stub(panel).unfreeze(panel) ### ### popup gui tool... no return value ### def popup( what, panel=make_id(0) ): if type(what) != str or not is_id(panel): raise Exception("popup() takes a string followed by one optional integer...") pu = __img_p.PopUp( ) pu.panel.CopyFrom(panel) pu.name = what stub(panel).popup(pu) ### ### restore restore file to a new panel... returns id ### def restore( path, panel=make_id(0) ): if type(path) != str or not is_id(panel): ### probably should check for file existence raise Exception("restore() takes a string and an integer; only the first arg is required...") rs = __img_p.Restore( ) rs.panel.CopyFrom(panel) rs.path = path result = stub(panel).restore(rs) if result is not None: __id_gui_state[__h(result)] = __id_gui_state[__h(panel)] return result ### ### generate an output file... no return value ### def output( device, devicetype='file', panel=make_id(0), scale=1.0, dpi=300, format="jpg", \ orientation="portrait", media="letter" ): if type(device) != str or not is_id(panel) or type(scale) != float or \ type(dpi) != int or type(format) != str or type(orientation) != str or type( media ) != str: raise Exception("output() takes (str,int,float,int,str,str,str); only the first is required...") out = __img_p.Output( ) out.panel.CopyFrom(panel) out.device = device out.devicetype = devicetype out.orientation = orientation out.media = media out.format = format out.scale = scale out.dpi = dpi stub(panel).output(out) ### ### set axes... no return value ### def axes( x='', y='', z='', panel=make_id(0) ): if type(x) != str or type(y) != str or type(z) != str or not is_id(panel) : raise Exception("axes() takes one to three strings and an optional panel id...") ax = __img_p.Axes( ) ax.panel.CopyFrom(panel) ax.x = x ax.y = y ax.z = z stub(panel).axes(ax) ### ### set contour levels... no return value ### def contourlevels( levels=[], baselevel=2147483648.0, unitlevel=2147483648.0, data=make_id(0) ): if type(levels) != list or not is_id(data) or \ all( map( lambda x: type(x) == int or type(x) == float, levels ) ) == False: raise Exception("contorlevels() takes (numeric list,id)...") cl = __img_p.ContourLevels( ) cl.id.CopyFrom(data) cl.levels.extend(levels) cl.baselevel = baselevel cl.unitlevel = unitlevel stub(data).contourlevels(cl) ### ### set the contour color... no return value ### def contourcolor( color="foreground", data=make_id(0) ): if type(color) != str or not is_id(data): raise Exception("contorcolor() takes color name and data id...") cc = __img_p.ContourColor( ) cc.id.CopyFrom(data) cc.color = color stub(data).contourcolor(cc) ### ### set contour thickness (0-5)...no return value ### def contourthickness( thickness=0.0, data=make_id(0) ): if type(thickness) != float or not is_id(data): raise Exception("contourthickness() takes a float representing the thickness and data id...") if thickness < 0 or thickness > 5: raise Exception("the thickness supplied to contourthickness() should between 0 and 5") ct = __img_p.ContourThickness( ) ct.id.CopyFrom(data) ct.thickness = thickness stub(data).contourthickness(ct) ### ### set the zoom level... no return value ### def zoom( level=None, blc=[], trc=[], coordinates="pixel", region="", panel=make_id(0) ): if ( type(level) != int and level is not None) or \ type(blc) != list or type(trc) != list or not is_id(panel) or \ type(coordinates) != str or (type(region) != str and type(region) != dict) : raise Exception("zoom() takes (int|None,list,list,str,id); each argument is optional...") if (type(region) == str and __os.path.isfile( region )): raise Exception("zoom( ) does not yet support loading region files (but does accept a region dictionary)") if type(region) is dict: ( _blc, _trc, _coord ) = __extract_region_box( reg ) zoom( level=None, blc=_blc, trc=_trc, coordinates=_coord, region="", panel=panel ) if level is not None: zl = __img_p.SetZoomLevel( ) zl.panel.CopyFrom(panel) zl.level = level stub(panel).zoomlevel(zl) else: if len(blc) != 2 or len(trc) != 2: raise Exception("blc/tlc in zoom() should each be a list of two integers") zb = __img_p.SetZoomBox( ) zb.panel.CopyFrom(panel) zb.blc.x = blc[0] zb.blc.y = blc[1] zb.trc.x = trc[0] zb.trc.y = trc[1] zb.coord_type = coordinates stub(panel).zoombox(zb) ### ### hide a panel... no return value ### def hide( panel=make_id(0) ): if not is_id(panel) : raise Exception("hide() takes a single panel identifier ...") stub(panel).hide(panel) ### ### show (unhide) a panel... no return value ### def show( panel=make_id(0) ): if not is_id(panel) : raise Exception("show() takes a single panel identifier ...") stub(panel).show(panel)