diff --git a/home/bin/cssh b/home/bin/cssh index 8bf1cb4..fac22d5 100755 --- a/home/bin/cssh +++ b/home/bin/cssh @@ -5,17 +5,21 @@ import argparse import re import subprocess import abc +import atexit class RgbColor(): red: int green: int blue: int + def __init__(self, red: int, green: int, blue: int): self.red = red self.green = green self.blue = blue + def __str__(self): + return f'[{self.red}, {self.green}, {self.blue}]' UserId = str # eg 'root' HostId = str # eg alambix50.ipr.univ-rennes.fr or alambix50 @@ -54,70 +58,72 @@ class ITerminalLauncher(abc.ABC): ''' +OsxWindowId = str -# ''' -# function getHostRealFqdn() -# { -# local strHostname="$1" - -# local strHostFqdn="$(host $strHostname | tail -1 | awk '{print $1}')" -# echo "$strHostFqdn" -# } - -# HOST_FQDN=$(getHostRealFqdn $HOSTNAME) - -# SOLARIZED_BACKGROUND='{0, 8256, 10368}' #002b36 (solarized dark base03) -# SAILOR_BLUE='{8832, 14208, 18816}' # 2e4a62 -# PRUNE='{15360, 10944, 14592}' #50394c -# TAUPE='{19968, 18816, 16512}' #686256 -# DARK_ORANGE='{27456, 12864, 0}' #8f4300 -# DARK_OLIVE='{8832, 11904, 5568}' #2e3e1d - -# function get_current_terminal_window_id() -# { -# local terminal_windows_id='' -# terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1") -# echo "$terminal_windows_id" -# } - -# function set_bg () -# { -# local strOsxTermWindowId="$1" -# local strColor="$2" # eg {45000, 0, 0, 50000} - -# osascript -e "tell application \"Terminal\" to set background color of (every window whose id is $strOsxTermWindowId) to $strColor" -# } - -# function on_exit () -# { -# set_bg "$THIS_OSX_TERM_WINDOW_ID" "$SOLARIZED_BACKGROUND" -# } +class DarwinTerminalLauncher(ITerminalLauncher): + ''' + macos terminal launcher + ''' + THIS_OSX_TERM_WINDOW_ID = None + SOLARIZED_BACKGROUND = RgbColor( 0 / 256, 8256 / 256, 10368 / 256) # '{0, 8256, 10368}' #002b36 (solarized dark base03) + SAILOR_BLUE = RgbColor( 8832 / 256, 14208 / 256, 18816 / 256) # '{8832, 14208, 18816}' # 2e4a62 + PRUNE = RgbColor(15360 / 256, 10944 / 256, 14592 / 256) # '{15360, 10944, 14592}' #50394c + TAUPE = RgbColor(19968 / 256, 18816 / 256, 16512 / 256) # '{19968, 18816, 16512}' #686256 + DARK_ORANGE = RgbColor(27456 / 256, 12864 / 256, 0 / 256) # '{27456, 12864, 0}' #8f4300 + DARK_OLIVE = RgbColor( 8832 / 256, 11904 / 256, 5568 / 256) # '{8832, 11904, 5568}' #2e3e1d + + + @staticmethod + def get_current_terminal_window_id() -> OsxWindowId: + # local terminal_windows_id='' + # terminal_windows_id=$(osascript -e "tell application \"Terminal\" to get id of window 1") + # echo "$terminal_windows_id" + osacommand = 'tell application "Terminal" to get id of window 1' + command = ['osascript', '-e', osacommand] + proc = subprocess.run(command, check=True, stdout=subprocess.PIPE) + lines = proc.stdout.decode('utf8').split('\n') + assert len(lines) == 2 + window_id = lines[0] + return window_id + + + @staticmethod + def color_as_osx_str(color: RgbColor) -> str: + ''' + SOLARIZED_BACKGROUND = '{0, 8256, 10368}' + ''' + return '{%d, %d, %d}' % (color.red * 256, color.green * 256, color.blue * 256) + + @staticmethod + def set_bg(window_id: OsxWindowId, color: RgbColor): + # window_id + # color: eg {45000, 0, 0, 50000} + osacommand = f'tell application "Terminal" to set background color of (every window whose id is {window_id}) to {DarwinTerminalLauncher.color_as_osx_str()}' + command = ['osascript', '-e', osacommand] + subprocess.run(command, check=True) + + @staticmethod + def on_exit(): + DarwinTerminalLauncher.set_bg(DarwinTerminalLauncher.THIS_OSX_TERM_WINDOW_ID, DarwinTerminalLauncher.SOLARIZED_BACKGROUND) + + + def launch_terminal(self, terminal_label: TerminalLabel, bg_color: RgbColor, command: List[str]): + ''' + command: the command to execute in the terminal eg ['/usr/bin/ssh', f'{str(ssh_target)}'] + ''' + raise NotImplementedError() + DarwinTerminalLauncher.THIS_OSX_TERM_WINDOW_ID = DarwinTerminalLauncher.get_current_terminal_window_id() + # echo "THIS_OSX_TERM_WINDOW_ID=$THIS_OSX_TERM_WINDOW_ID" + DarwinTerminalLauncher.set_bg(THIS_OSX_TERM_WINDOW_ID, bg_color) + + # make cssh restore the default terminal color when exiting + atexit.register(DarwinTerminalLauncher.on_exit) + + # /usr/bin/ssh "$@" + subprocess.run(command, check=True) -# # if [ $(echo $HOST_FQDN | grep '^simpatix') ] -# # then -# # BG_COLOR="$SAILOR_BLUE" -# # elif [ $(echo $HOST_FQDN | grep '^physix') ] -# # then -# # BG_COLOR="$TAUPE" -# # else -# # case $HOST_FQDN in -# # 'pr079234.spm.univ-rennes1.fr') -# # BG_COLOR="$SOLARIZED_BACKGROUND" -# # ;; -# # 'puppet3.ipr.univ-rennes1.fr') -# # BG_COLOR="$PRUNE" -# # ;; -# # *) -# # BG_COLOR="$DARK_OLIVE" -# # ;; -# # esac -# # fi -# #if [ "$AS_ROOT" = 'true' ] -# #then -# # BG_COLOR="$DARK_ORANGE" -# #fi def is_valid_uuid(uuid: str) -> bool: ''' @@ -161,12 +167,13 @@ class GnomeSettings(): ''' profile_uuid: eg '2e0dec33-5ec2-4355-b671-7922010cee9f' ''' + assert is_valid_uuid(profile_uuid), f'invalid uuid: "{profile_uuid}"' profiles_key_id = 'org.gnome.Terminal.ProfilesList' profile_uuids = self.get_list(profiles_key_id) assert profile_uuid not in profile_uuids profile_uuids.append(profile_uuid) - self.set_list(profiles_key_id, profile_uuids) + self.set_list(profiles_key_id, profile_uuids) # side effect: this adds the dconf key "/org/gnome/terminal/legacy/profiles:/list" class GnomeTerminal(): @@ -202,11 +209,16 @@ class GnomeTerminal(): logging.info("creating profile profile_uuid=%s with profile_name=%s", profile_uuid, profile_name) # echo "new_profiles_list=$new_profiles_list" # dconf write $DCONF_PROFILES_PATH/list "$new_profiles_list" - proc = subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name", f"'{profile_name}'"], check=True) - - assert GnomeTerminal.get_term_profile_name(profile_name) == profile_uuid + proc = subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/visible-name", f"'{profile_name}'"], check=True) # dconf write /org/gnome/terminal/legacy/profiles:/:dc932a58-6a38-4511-876a-d453b5e33a26/visible-name "'toto'" + if True: + # check that the profile was successfully created + assert GnomeTerminal.get_term_profile_name(profile_uuid) == profile_name + existing_profile_uuid = GnomeTerminal.get_terminal_profile_uuid(profile_name) + assert existing_profile_uuid is not None, f'failed to find the profile for name {profile_name}, it is expected to be {profile_uuid}' + assert existing_profile_uuid == profile_uuid + gsettings = GnomeSettings() gsettings.register_profile_uuid(profile_uuid) return profile_uuid @@ -216,9 +228,8 @@ class GnomeTerminal(): proc = subprocess.run(['dconf', 'list', f"{GnomeTerminal.DCONF_PROFILES_PATH}/"], check=True, stdout=subprocess.PIPE) profile_uuids = [] for line in proc.stdout.decode('utf8').split('\n'): - print(line) # eg ':b1dcc9dd-5262-4d8d-a863-c897e6d979b9/' - m = re.match(r'^:?(?P[0-9a-f\-]+)/$', line) + m = re.match(r'^:(?P[0-9a-f\-]+)/$', line) if m is None: assert line in ['list', ''], f'unexpected format for line: {line}' else: @@ -229,7 +240,7 @@ class GnomeTerminal(): @staticmethod def get_term_profile_name(profile_uuid: TermProfileUuid) -> Optional[TermProfileName]: profile_name = None - proc = subprocess.run(['dconf', 'read', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/visible-name"], check=True, stdout=subprocess.PIPE) + proc = subprocess.run(['dconf', 'read', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/visible-name"], check=True, stdout=subprocess.PIPE) lines = proc.stdout.decode('utf8').split('\n') if (len(lines) >= 2): @@ -239,6 +250,7 @@ class GnomeTerminal(): assert m, f'unexpected line : {line}' profile_name = m['profile_name'] else: + logging.debug('name lines for profile %s : %s', profile_uuid, str(lines)) pass # (the default profile has no name) #assert False, f'unexpected case: the terminal profile profile_uuid={profile_uuid} has no name!!!' return profile_name @@ -255,12 +267,14 @@ class GnomeTerminal(): for profile_uuid in GnomeTerminal.get_term_profiles(): logging.debug(f'processing profile_uuid={profile_uuid}') prof_name = GnomeTerminal.get_term_profile_name(profile_uuid) - logging.debug('prof_name = %s', prof_name) + logging.debug('profile_uuid = %s, prof_name = %s', profile_uuid, prof_name) if profile_name == prof_name: return profile_uuid logging.info('the profile named %s doesnt exist... create it then', profile_name) + GnomeTerminal.check_validity() profile_uuid = GnomeTerminal.create_new_profile(profile_name) + GnomeTerminal.check_validity() return profile_uuid @@ -269,13 +283,38 @@ class GnomeTerminal(): # function set_terminal_profile_bg_color() # profile_name # eg physix.ipr.univ-rennes1.fr # bg_color # eg RgbColor(17, 25, 24) - + logging.debug('setting color for profile %s : %s', profile_name, str(bg_color)) profile_uuid = GnomeTerminal.get_terminal_profile_uuid(profile_name) logging.debug("profile_name=%s profile_uuid=%s", profile_name, profile_uuid) - subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/use-theme-colors", 'false'], check=True, stdout=subprocess.PIPE) - subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/{profile_uuid}/background-color", f'({bg_color.red}, {bg_color.green}, {bg_color.blue})'], check=True, stdout=subprocess.PIPE) - + fg_color = RgbColor(255, 255, 255) + subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/use-theme-colors", 'false'], check=True, stdout=subprocess.PIPE) + subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/background-color", f"'rgb({bg_color.red}, {bg_color.green}, {bg_color.blue})'"], check=True, stdout=subprocess.PIPE) + subprocess.run(['dconf', 'write', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/foreground-color", f"'rgb({fg_color.red}, {fg_color.green}, {fg_color.blue})'"], check=True, stdout=subprocess.PIPE) + + @staticmethod + def delete_term_profile(profile_uuid: TermProfileUuid): + # dconf reset -f /org/gnome/terminal/legacy/profiles:/2a05665f-baa7-484e-b179-4e94f3814238 + logging.warning('deleting terminal profile %s', profile_uuid) + subprocess.run(['dconf', 'reset', '-f', f"{GnomeTerminal.DCONF_PROFILES_PATH}/:{profile_uuid}/"], check=True) + + @staticmethod + def delete_all_term_profiles(): + for term_profile_uuid in GnomeTerminal.get_term_profiles(): + GnomeTerminal.delete_term_profile(term_profile_uuid) + subprocess.run(['dconf', 'reset', '-f', f"{GnomeTerminal.DCONF_PROFILES_PATH}/list"], check=True) + + @staticmethod + def check_validity(): + proc = subprocess.run(['dconf', 'list', f"{GnomeTerminal.DCONF_PROFILES_PATH}/"], check=True, stdout=subprocess.PIPE) + profile_uuids = [] + for line in proc.stdout.decode('utf8').split('\n'): + m = re.match(r'^:(?P[0-9a-f\-]+)/$', line) + if m: + uuid = m['uuid'] + assert is_valid_uuid(uuid) + else: + assert line in ['list', ''], f'unexpected key: {line}' class GnomeTerminalLauncher(ITerminalLauncher): @@ -284,31 +323,78 @@ class GnomeTerminalLauncher(ITerminalLauncher): command: the command to execute in the terminal eg ['/usr/bin/ssh', f'{str(ssh_target)}'] ''' + if False: # deleting the term profiles affects the already open terminals, that then switch to default colors + # delete all profiles to delete the broken ones and the duplicates + GnomeTerminal.delete_all_term_profiles() + GnomeTerminal.set_terminal_profile_bg_color(terminal_label, bg_color) profile_uuid = GnomeTerminal.get_terminal_profile_uuid(terminal_label) + logging.info('using terminal profile %s (uuid=%s)', terminal_label, profile_uuid) launch_term_command = ['gnome-terminal', f'--window-with-profile={profile_uuid}', f'--title={terminal_label}', '--'] + command logging.debug('launch_term_command = %s', launch_term_command) subprocess.run(launch_term_command, check=True) +def is_valid_host_fqdn(host_fqdn: str) -> bool: + m = re.match(r'^[a-z0-9.\-]+$', host_fqdn) + return m is not None + + +def get_host_real_fqdn(host_id: HostId) -> HostId: + # 20260617-11:47:29 graffy@graffy-ws2:~/work/graffyworkenv.git$ host alambix.ipr.univ-rennes.fr + # alambix.ipr.univ-rennes.fr is an alias for alambix-frontal.ipr.univ-rennes.fr. + # alambix-frontal.ipr.univ-rennes.fr has address 129.20.27.230 + # last command status : [0] + command = "host %s | tail -1 | awk '{print $1}'" % host_id + proc = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE) + lines = proc.stdout.decode('utf8').split('\n') + host_real_fqdn = lines[0] + assert is_valid_host_fqdn(host_real_fqdn), f'invalid host name: {host_real_fqdn}' + return host_real_fqdn + def main(): - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.INFO) arg_parser = argparse.ArgumentParser('cssh (colored ssh) opens a terminal window with a background color that depends on the fully qualified domain name of the target machine') arg_parser.add_argument('ssh_target', help='eg root@alambix50.ipr.univ-rennes.fr') args = arg_parser.parse_args() logging.debug(args) ssh_target = SshTarget(args.ssh_target) # eg 'root@alambix50.ipr.univ-rennes.fr' - + ssh_target.host_id = get_host_real_fqdn(ssh_target.host_id) + logging.debug('ssh_target.host_id = %s', ssh_target.host_id) # AS_ROOT='false' # if [ "$(echo $@ | grep '^root@' > /dev/null)" ] # then # AS_ROOT='true' # fi + # if [ $(echo $HOST_FQDN | grep '^simpatix') ] + # then + # BG_COLOR="$SAILOR_BLUE" + # elif [ $(echo $HOST_FQDN | grep '^physix') ] + # then + # BG_COLOR="$TAUPE" + # else + # case $HOST_FQDN in + # 'pr079234.spm.univ-rennes1.fr') + # BG_COLOR="$SOLARIZED_BACKGROUND" + # ;; + # 'puppet3.ipr.univ-rennes1.fr') + # BG_COLOR="$PRUNE" + # ;; + # *) + # BG_COLOR="$DARK_OLIVE" + # ;; + # esac + # fi + + #if [ "$AS_ROOT" = 'true' ] + #then + # BG_COLOR="$DARK_ORANGE" + #fi color_saturation = 0.3 color_value = 0.2 @@ -316,29 +402,19 @@ def main(): os_name = proc.stdout.decode('utf8').strip() terminal_launcher: Optional[ITerminalLauncher] = None if os_name == 'Darwin': - raise NotImplementedError() - # 'Darwin') - # BG_COLOR=$(make_color.py $HOST_FQDN $COLOR_VALUE $COLOR_SATURATION osx) - # THIS_OSX_TERM_WINDOW_ID=$(get_current_terminal_window_id) - # echo "THIS_OSX_TERM_WINDOW_ID=$THIS_OSX_TERM_WINDOW_ID" - # set_bg "$THIS_OSX_TERM_WINDOW_ID" "$BG_COLOR" - # trap on_exit EXIT - # /usr/bin/ssh "$@" - # ;; + terminal_launcher = DarwinTerminalLauncher() elif os_name == 'Linux': - terminal_launcher = GnomeTerminalLauncher() else: raise NotImplementedError(f'unhandled os : "{os_name}"') - proc = subprocess.run(['make_color.py', ssh_target.host_id, str(color_value), str(color_saturation), os_name.lower()], check=True, stdout=subprocess.PIPE) + proc = subprocess.run(['make_color.py', ssh_target.host_id, str(color_value), str(color_saturation), 'linux'], check=True, stdout=subprocess.PIPE) bg_color_as_str = proc.stdout.decode('utf8').strip() match = re.match(r'\((?P[0-9]+), (?P[0-9]+), (?P[0-9]+)\)', bg_color_as_str) assert match, f'unexpected color as string: {bg_color_as_str}' bg_color = RgbColor(match['red'], match['green'], match['blue']) logging.debug('bg_color = %s', str(bg_color)) # eg (35, 43, 51) - command = ['/usr/bin/ssh', f'{str(ssh_target)}'] terminal_launcher.launch_terminal(terminal_label=ssh_target.host_id, bg_color=bg_color, command=command)