tags: python net clipboard net-clipboard title: Simple Network Clipboard using TCP # Shorthands for Common Tasks ## bput ```py #!/bin/bash NAME="$1" if [ -z "$NAME" ]; then echo "bput " exit 1 fi shift cat "$@" | bclient put "$NAME" ``` ## bget ```py #!/bin/bash for s; do bclient get "$s" done ``` ## blist ```py #!/bin/bash if [ -n "$1" ]; then bclient list | grep "$@" else bclient list fi ``` # Link with clipboard I have the commands `pc` and `pp` (shorted from macos' `pbcopy` and `pbpaste`, but do the right thing on Linux, Windows, and mac). ## bpc ```py #!/bin/bash # bget into clipboard for s; do bclient get "$s" done | pc ``` ## bpp ```py #!/bin/bash NAME="$1" shift if [ -z "$NAME" ]; then echo "bpp " exit 1 fi pp | bclient put "$NAME" ``` ## Signalling Task Completion Something I sometimes want to do is to start a task on one machine, and then have another machine wait until it is done before doing something else. A simple solution is to create a named clip for the task, and then delete it when done. ### bwait ```bash #!/bin/bash X="$1" if [ -z "$X" ]; then echo "$0 " exit 1 fi while [ -n "$(bget "$X")" ]; do echo -n "Waiting until $X is free: "; date sleep 1 done ``` ### bwrap ```bash #!/bin/bash X="$1"; shift CMD="$1"; shift if [ -z "$X" ] || [ -z "$CMD" ]; then echo "$0 []" exit 1 fi while [ -n "$(agt "$X")" ]; do echo -n "Waiting until $X is free: "; date sleep 1 done echo "Starting $X" echo $$ | bput "$X" "$CMD" "$@" bclient del "$X" ``` # The Client ## bclient ```py #!/usr/bin/env python3 import sys import subprocess import socket import os import re from datetime import datetime # specifying . as source file pastes from clipoard HOST = os.getenv("BHOST","127.0.0.1") # The server's hostname or IP address PORT = os.getenv("BPORT",9999) # The port used by the server try: PORT = int(PORT) except ValueError: print(f"Invalid port PORT={PORT}") PORT=None def paste(): m = subprocess.run(["pp"],capture_output=True) f = sys.stderr if (x := m.returncode) > 0: out = m.stdout.splitlines() err = m.stderr.splitlines() print(f"pp returned {x}:",file=f) print(f"stdout:",file=f) for line in out: line = b" " + line + b"\n" f.buffer.write(line) print(f"stderr:",file=f) for line in err: line = b" " + line + b"\n" f.buffer.write(line) return "" else: return m.stdout.decode() def helpexit(n): print(f"{sys.argv[0]} ") print(f" where is one of:") for k in cmds.keys(): v = cmds[k] if type(v) == Command: print(f" {k} -- {v.helptext}") else: print(f" {k}") print(f"When putting or appending, send content to stdin") exit(n) def main(): global PORT, HOST append = False args = sys.argv[1:] argi = iter(args) args = [] for arg in argi: if arg == "-h": try: HOST = next(argi) except StopIteration: print(f"-h needs argument",file=sys.stderr) exit(1) elif arg == "-a": append = True else: args.append(arg) try: cmd, *params = args except ValueError: helpexit(1 if len(args) > 0 else 0) if len(args) > 0: exit(1) else: exit(0) if cmd == "put" and append: cmd = "append" if cmd in cmds: try: cmds[cmd](params) except BException as e: print(f"BException: {e} running {cmd}({', '.join(params)})") else: print(f"No command {cmd}") helpexit(1) class BException(Exception): pass def do_get(params): if len(params) == 0: raise BException("No names to get") for name in params: msg = f"get {name}" print(query(msg),end="") def do_save(params): try: name, = params except ValueError: raise BException("Invalid params for save") msg = f"save {name}" print(query(msg),end="") def do_load(params): try: name, = params except ValueError: raise BException("Invalid params for load") msg = f"load {name}" print(query(msg)) def do_list(params): msg = f"list" result = query(msg) if len(params) > 0: matches = set() lines = result.strip().splitlines() for x in params: r = re.compile(x) for y in lines: z = y.split(":")[0] if r.search(z): matches.add(y) result = "\n".join(sorted(matches)) print(result) def do_listsaved(params): msg = f"listsaved" result = query(msg) lines = result.rstrip().splitlines() namel = 0 out = [] for line in lines: xs = line.split(",") mtime = xs.pop() name = ",".join(xs) namel = max(namel,len(name)) mtime = float(mtime) mdate = datetime.fromtimestamp(mtime).strftime("%Y/%m/%d %H:%M:%S") out.append((name,mdate)) for name, mdate in out: print(f"{name: >{namel}} -- {mdate}") def do_clear(params): msg = f"clear" print(query(msg)) def do_del(params): if len(params) == 0: raise BException("No names to delete") for name in params: msg = f"del {name}" print(query(msg)) def do_put(params): try: name,*xs = params except ValueError: raise BException("Invalid params for put") if len(xs) == 0: payload = sys.stdin.read() else: stdin = None payloads = [] for x in xs: if x == ".": payloads.append(paste()) elif x == "-": if stdin is None: stdin = sys.stdin.read() payloads.append(stdin) elif os.path.exists(x): with open(x) as f: payloads.append(f.read()) else: print(f"Cannot put {x}",file=sys.stderr) payload = "\n".join(payloads) msg = f"put {name}\n" + payload print(query(msg)) def do_append(params): try: name,*xs = params except ValueError: raise BException("Invalid params for put") if len(xs) == 0: payload = sys.stdin.read() else: stdin = None payloads = [] for x in xs: if x == ".": payloads.append(paste()) elif x == "-": if stdin is None: stdin = sys.stdin.read() payloads.append(stdin) elif os.path.exists(x): with open(x) as f: payloads.append(f.read()) else: print(f"Cannot put {x}",file=sys.stderr) payload = "\n".join(payloads) msg = f"append {name}\n" + payload print(query(msg)) def do_quit(name): print(query("quit")) def query(msg): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((HOST, PORT)) data = msg.encode() + b'\x00' s.sendall(data) data = b"" while True: dx = s.recv(1024) #print(f"Received {len(dx)} total {len(data)}:: {dx}") if len(dx) == 0: break data += dx try: msg = data.decode() except Exception: msg = "" return msg class Command: def __init__(self,func,helptext): self.func = func self.helptext = helptext def __call__(self,*xs,**kw): return self.func(*xs,**kw) cmds = { "get": Command(do_get, "Get contents of named clipbaord"), "clear": Command(do_clear, "Delete all clips"), "save": Command(do_save, "Dump all clips to file local to server"), "load": Command(do_load, "Load all cliips from file local to server"), "put": Command(do_put, "Store text to a clip"), "append": Command(do_append, "Append text to a clip"), "del": Command(do_del, "Delete a single clip"), "list": Command(do_list, "List clips"), "listsaved": Command(do_listsaved, "List saved clips"), "quit": Command(do_quit, "Tell server to terminate") } if __name__ == "__main__": main() ``` # The Server ## bsrv ```py #!/usr/bin/env python import socket import sys import json import os from datetime import datetime from glob import glob from setproctitle import setproctitle setproctitle("bsrv") HOST = "0.0.0.0" # Standard loopback interface address (localhost) PORT = 9999 # Port to listen on (non-privileged ports are > 1023) bclip_path = os.path.expanduser("~/.bclip") clips = {} def load_latest(): if not os.path.isdir(bclip_path): print("No bclip path") return cs = glob(os.path.join(bclip_path,"*.json")) if len(cs) == 0: print("No saved clips in bclip path") return css = list(sorted(cs, key = lambda t: os.path.getmtime(t))) latest = css[0].split("/")[-1][:-len(".json")] result = load_from(latest) print("Load from latest result:",result) def main(): args = sys.argv[1:] if "-l" in args: print("Latest requested") args.pop(args.index("-l")) load_latest() with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST, PORT)) s.listen() try: while True: print("Waiting") conn, addr = s.accept() with conn: if handle(conn,addr) == False: break except KeyboardInterrupt: print("KeyboardInterrupt") def handle(conn,addr): print(f"Connected by {addr}") data = b"" while True: dx = conn.recv(1024) print(f"Received {len(dx)}") data += dx if len(dx) == 0: break if b'\x00' in data: break print(f"Received {len(data)} in total") data = data.split(b'\x00')[0] try: txt = data.decode() except Exception(): print(f"Failed to decode {data}") return xs = txt.split("\n",1) x = xs[0] if x == "quit": print("Quit") handle_quit(conn,addr) print("Return False") return False elif x == "list": handle_list(conn,addr) elif x == "listsaved": handle_listsaved(conn,addr) elif x == "clear": handle_clear(conn,addr) elif x.startswith("load "): handle_load(conn,addr,x) elif x.startswith("save "): handle_save(conn,addr,x) elif x.startswith("get "): handle_get(conn,addr,x) elif x.startswith("put "): y = xs[1] handle_put(conn,addr,x,y) elif x.startswith("append "): y = xs[1] handle_append(conn,addr,x,y) elif x.startswith("del "): handle_del(conn,addr,x) else: print(f"Invalid request no match '{x}'") handle_invalid(conn,addr,x) return True def handle_quit(conn,addr): msg = "Quitting\n" data = msg.encode() conn.sendall(data) def handle_invalid(conn,addr,x): msg = f"Invalid\n" data = msg.encode() conn.sendall(data) def handle_get(conn,addr,x): get, name = x.split(" ",1) name = name.strip() if not name in clips: result = "" else: result = clips[name] data = result.encode() print(f"Sending {len(data)} bytes") conn.sendall(data) def handle_save(conn,addr,x): bclip_path = os.path.expanduser("~/.bclip") try: if "/" in x: raise Exception("can't have / in name") get, name = x.split(" ",1) name = name.strip() fn = name + ".json" os.makedirs(bclip_path,exist_ok=True) with open(os.path.join(bclip_path,fn),"wt") as f: json.dump(clips,f) result = f"Saved to {name}" except Exception as e: result = f"Exception: {e.__class__.__name__} {e}" data = result.encode() print(f"Sending {len(data)} bytes") conn.sendall(data) def load_from(name): if "/" in name: raise Exception("can't have / in name") fn = name + ".json" path = os.path.join(bclip_path,fn) with open(path) as f: data = json.load(f) for k,v in data.items(): clips[k] = v print(f"Loaded {k}") return f"Loaded from {name}" def handle_load(conn,addr,x): try: if "/" in x: raise Exception("can't have / in name") get, name = x.split(" ",1) name = name.strip() result = load_from(name) except FileNotFoundError: result = f"No stored data named {name}" except Exception as e: result = f"Exception: {e.__class__.__name__} {e}" data = result.encode() print(f"Sending {len(data)} bytes") conn.sendall(data) def handle_del(conn,addr,x): get, name = x.split(" ",1) name = name.strip() if not name in clips: result = "Not found" else: del(clips[name]) result = f"Deleted {name}" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) def handle_list(conn,addr): print(list(clips.keys())) xs = sorted(clips.keys()) ys = [ f"{k}: ({len(clips[k])} bytes)" for k in xs ] result = "\n".join(ys)+"\n" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) def handle_listsaved(conn,addr): if not os.path.isdir(bclip_path): result = "No bclip path" else: cs = glob(os.path.join(bclip_path,"*.json")) cs = list(sorted(cs, key = lambda t: os.path.getmtime(t))) l = len(".json") ccs = [ f"{x.split('/')[-1][:-l]},{os.path.getmtime(x)}" for x in cs ] if len(cs) == 0: result = "No clips in bclip path" else: result = "\n".join(ccs) + "\n" #result = "\n".join(x[:-l].split("/")[-1] for x in cs) + "\n" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) def handle_clear(conn,addr): global clips clips = {} result = "Cleared\n" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) def handle_put(conn,addr,x,y,append=False): put, name = x.split(" ",1) name = name.strip() y = y.replace("\r","").strip("\n")+"\n" clips[name] = y result = f"Inserted {len(y)} bytes to {name}" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) def handle_append(conn,addr,x,y): put, name = x.split(" ",1) name = name.strip() y = y.replace("\r","").strip("\n")+"\n" if not name in clips: clips[name] = y else: clips[name] += y result = f"Appended {len(y)} bytes to {name}" data = result.encode() print(f"Sending {len(data)} bytes:",data) conn.sendall(data) if __name__ == "__main__": main() ```