Dup Ver Goto 📝

Simple Network Clipboard using TCP

To
597 lines, 1648 words, 13674 chars Page 'NetworkClipboard' does not exist.

Shorthands for Common Tasks

bput

#!/bin/bash
NAME="$1"
if [ -z "$NAME" ]; then
  echo "bput <name>"
  exit 1
fi
shift
cat "$@" | bclient put "$NAME"

bget

#!/bin/bash
for s; do
  bclient get "$s"
done

blist

#!/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

#!/bin/bash
# bget into clipboard
for s; do
  bclient get "$s"
done | pc

bpp

#!/bin/bash
NAME="$1"
shift
if [ -z "$NAME" ]; then
  echo "bpp <name>"
  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

#!/bin/bash
X="$1"
if [ -z "$X" ]; then
  echo "$0 <name>"
  exit 1
fi
while [ -n "$(bget "$X")" ]; do
  echo -n "Waiting until $X is free: "; date
  sleep 1
done

bwrap

#!/bin/bash
X="$1"; shift
CMD="$1"; shift
if [ -z "$X" ] || [ -z "$CMD" ]; then
  echo "$0 <name> <cmd> [<args>]"
  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

#!/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]} <cmd> <name>")
  print(f"  where <cmd> 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 = "<Failed to decode>"
    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

#!/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()