diff --git a/csshX b/csshX index a9d6015..f9fbb2a 100755 --- a/csshX +++ b/csshX @@ -7,11 +7,19 @@ # # # This program is free software; you may redistribute it and/or modify it # # under the same terms as Perl itself. # +# # +# Local maintenance notes, 2026-05-25: # +# - Fixed shell quoting, chmod error handling, host-file open safety, and # +# scrollback path handling. # +# - Updated macOS version detection and replaced removed Ruby DL usage with # +# Fiddle for modern system Ruby. # +# - Added guards for empty slave grids and graceful TIOCSTI failure handling. # #==============================================================================# use strict; use warnings; +use File::Spec; use version; (our $VERSION = '$Rev: 0.73-38-g5c0f684$') =~ s/\$Rev:\s*(.*)\$/$1/; my $config; # Global configuration object.. @@ -77,7 +85,7 @@ sub new { clusters => {} }, ref($pack) || $pack; - ($obj->{osver} = (uname())[2]) =~ s/^(\d+)(\.\d+).*/"10.".($1-4)."$2"/e; + $obj->{osver} = $obj->detect_osver; $obj->load_clusters("/etc/clusters"); $obj->load_csshrc($_) foreach ("/etc/csshrc", "$ENV{HOME}/.csshrc"); @@ -105,9 +113,12 @@ sub new { sub load_hosts { my ($obj, $host_file) = @_; - open (my $fh, - $host_file eq "-" ? "<&STDIN" : "< $host_file" - ) || die "Can't read [$host_file]: $!"; + my $fh; + if ($host_file eq "-") { + open($fh, '<&', \*STDIN) || die "Can't read [$host_file]: $!"; + } else { + open($fh, '<', $host_file) || die "Can't read [$host_file]: $!"; + } while (defined(my $line = <$fh>)) { $line =~ s/#.*$//; @@ -117,6 +128,20 @@ sub load_hosts { } } +sub detect_osver { + my ($obj) = @_; + + my $product_version = `/usr/bin/sw_vers -productVersion 2>/dev/null`; + chomp $product_version; + return $product_version if $product_version =~ /^\d+(?:\.\d+){0,2}$/; + + my $kernel_version = (uname())[2] || ''; + return '0.0' unless $kernel_version =~ /^(\d+)(\.\d+)?/; + + my ($major, $minor) = ($1, $2 || '.0'); + return $major >= 20 ? "$major$minor" : "10.".($major-4).$minor; +} + sub load_clusters { my ($obj, $config_file) = @_; return unless -f $config_file; @@ -385,7 +410,7 @@ sub write_buffered { my ($obj) = @_; if (my $bwrote = $obj->syswrite(*$obj->{buf_write}, 1024)) { substr(*$obj->{buf_write},0,$bwrote,''); - return ! (length *$obj->{buf_write});; + return ! (length *$obj->{buf_write}); } else { $obj->terminate; } @@ -471,7 +496,7 @@ sub make_NSColor { # { 65535, 65535, 65535 } # FFFFFF - my ($r,$g,$b) = @_; + my ($r,$g,$b); if ($str =~ /^\{(\d+),(\d+),(\d+)\}$/) { ($r,$g,$b) = map { $_ / 65535 } ($1,$2,$3); } elsif ($str =~ /^(\w\w)(\w\w)(\w\w)$/) { @@ -513,38 +538,36 @@ sub open_window { my ($pack, @args) = @_; # Quote the command arguements - my $cmd = join ' ', map { s/(["'])/\\$1/g; "'$_'" } @args; + my $cmd = join ' ', map { my $arg = $_; $arg =~ s/'/'\\''/g; "'$arg'" } @args; # don't exec if debugging so we can see errors - unless ($config->debug) { - if (get_shell =~ /fish$/) { - $cmd = "clear; and exec $cmd" unless $config->debug; - } else { - $cmd = "clear && exec $cmd" unless $config->debug; - } - } + $cmd = "clear && exec $cmd" unless $config->debug; # Hide the command from any shell history $cmd = 'history -d $(($HISTCMD-1)) && '.$cmd if get_shell =~ m{/(ba)?sh$}; # TODO - (t)csh, ksh, zsh my $tabobj = $terminal->doScript_in_($cmd, undef) || return; - - # Get the window and tab IDs from the Apple Event itself - my $tab_ed = $tabobj->qualifiedSpecifier; # Undocumented call - my $tab_id = $tab_ed->descriptorForKeyword_(OSType 'seld')->int32Value-1; - my $win_ed = $tab_ed->descriptorForKeyword_(OSType 'from'); - my $win_id = $win_ed->descriptorForKeyword_(OSType 'seld')->int32Value.''; - - # Create an object unless we were passed one - my $obj = ref $pack ? $pack : $pack->SUPER::new(); - $obj->set_windowid($win_id); - $obj->set_tabid($tab_id); - - return $obj; + my $tty = $tabobj->tty->UTF8String || return; + + my $windows = $terminal->windows; + # Quickly check if the tty even exists, since the next code is REALLY slow + #return unless grep { $tty eq $_ } @{Foundation::perlRefFromObjectRef $windows->valueForKey_("tty")}; + for (my $n=0; $n<$windows->count; $n++) { + my $window = $windows->objectAtIndex_($n); + my $tabs = $window->tabs; + for (my $m=0; $m<$tabs->count; $m++) { + my $tab = $tabs->objectAtIndex_($m); + if ($tab->tty && ($tab->tty->UTF8String eq $tty)) { + my $obj = ref $pack ? $pack : $pack->SUPER::new(); + $obj->set_windowid("".$window->id); + $obj->set_tabid($m); + return $obj; + } + } + } } - sub set_windowid { *{$_[0]}->{windowid} = $_[1]; } sub windowid { *{$_[0]}->{windowid}; } @@ -738,25 +761,35 @@ sub run_ruby { sub set_space { my ($obj, $space) = @_; - my $I = (length pack('L!',0) == 4 ) ? 'I' : 'L'; $obj->run_ruby(" - require 'dl' - dl = DL::dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') - con = dl.sym('_CGSDefaultConnection', '${I}').call() - dl.sym('CGSMoveWorkspaceWindowList', '${I}${I}A${I}${I}').call(con[0], [Integer(ARGV[0])], 1, Integer(ARGV[1])) + require 'fiddle' + cgs = Fiddle.dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') + con = Fiddle::Function.new(cgs['_CGSDefaultConnection'], [], Fiddle::TYPE_VOIDP).call + move = Fiddle::Function.new( + cgs['CGSMoveWorkspaceWindowList'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_INT], + Fiddle::TYPE_INT + ) + wid = Fiddle::Pointer[[Integer(ARGV[0])].pack('i')] + move.call(con, wid, 1, Integer(ARGV[1])) ", $obj->windowid, $space); } sub space { my ($obj) = @_; - my $I = (length pack('L!',0) == 4 ) ? 'I' : 'L'; - my $i = lc $I; $obj->run_ruby(" - require 'dl' - dl = DL::dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') - con = dl.sym('_CGSDefaultConnection', '${I}').call() - r,rs = dl.sym('CGSGetWindowWorkspace', '${I}${I}${I}${i}').call(con[0], Integer(ARGV[0]), 0) - exit rs[2] + require 'fiddle' + cgs = Fiddle.dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') + con = Fiddle::Function.new(cgs['_CGSDefaultConnection'], [], Fiddle::TYPE_VOIDP).call + workspace = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT) + workspace[0, Fiddle::SIZEOF_INT] = [0].pack('i') + get = Fiddle::Function.new( + cgs['CGSGetWindowWorkspace'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_INT + ) + exit get.call(con, Integer(ARGV[0]), workspace) == 0 ? + workspace[0, Fiddle::SIZEOF_INT].unpack1('i') : 0 ", $obj->windowid); } @@ -883,15 +916,28 @@ sub bounds_as_size { sub move_slaves_to_master_space { my ($obj) = @_; - my $I = (length pack('L!',0) == 4 ) ? 'I' : 'L'; - my $i = lc $I; $obj->run_ruby(" - require 'dl'; + require 'fiddle' ARGV.map! {|wid| Integer(wid)} - dl = DL::dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') - con = dl.sym('_CGSDefaultConnection', '${I}').call() - r,rs = dl.sym('CGSGetWindowWorkspace', '${I}${I}${I}${i}').call(con[0], ARGV.shift, 0) - dl.sym('CGSMoveWorkspaceWindowList', '${I}${I}A${I}${I}').call(con[0], ARGV, ARGV.length, rs[2]) + cgs = Fiddle.dlopen('/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices') + con = Fiddle::Function.new(cgs['_CGSDefaultConnection'], [], Fiddle::TYPE_VOIDP).call + workspace = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT) + workspace[0, Fiddle::SIZEOF_INT] = [0].pack('i') + get = Fiddle::Function.new( + cgs['CGSGetWindowWorkspace'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], + Fiddle::TYPE_INT + ) + move = Fiddle::Function.new( + cgs['CGSMoveWorkspaceWindowList'], + [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_INT], + Fiddle::TYPE_INT + ) + get.call(con, ARGV.shift, workspace) + unless ARGV.empty? + windows = Fiddle::Pointer[ARGV.pack('i*')] + move.call(con, windows, ARGV.length, workspace[0, Fiddle::SIZEOF_INT].unpack1('i')) + end ", map { $_->windowid} $obj, CsshX::Master::Socket::Slave->slaves); } @@ -993,23 +1039,28 @@ sub selected_window { sub select_move { my ($pack, $x, $y, $move_count) = @_; + return unless CsshX::Master::Socket::Slave->slave_count; + $pack->selection_off; $move_count ||= 1; + my $cols = $pack->grid_cols || return; + my $rows = $pack->grid_rows || return; + # Add extra movement in the case that the row/col has no active windows my $extra_y = 0; - if ($x && ($move_count > $pack->grid_cols)) { + if ($x && ($move_count > $cols)) { $extra_y = 1; $move_count = 0; } my $extra_x = 0; - if ($y && ($move_count > $pack->grid_rows)) { + if ($y && ($move_count > $rows)) { $extra_x = 1; $move_count = 0; } - $current_selection->[0]=($current_selection->[0]+$x+$extra_x)%$pack->grid_cols; - $current_selection->[1]=($current_selection->[1]+$y+$extra_y)%$pack->grid_rows; + $current_selection->[0]=($current_selection->[0]+$x+$extra_x)%$cols; + $current_selection->[1]=($current_selection->[1]+$y+$extra_y)%$rows; if (my $obj = $pack->selected_window()) { $obj->set_selected(1); @@ -1189,9 +1240,9 @@ sub parse_user_host_port { package CsshX::Launcher; -use base qw(CsshX::Socket::Selectable); -use POSIX qw(tmpnam); -use FindBin qw($Bin $Script);; +use base qw(CsshX::Socket::Selectable); +use File::Temp qw(tmpnam); +use FindBin qw($Bin $Script);; sub new { my ($pack) = @_; @@ -1222,8 +1273,12 @@ sub new { ) or die "Master window failed to open"; $greeting .= $master->uid."\n"; + my $space_number_supported = 1; if ($config->space) { - use version; if ($config->osver ge qv(10.7.0)) { + use version; + $space_number_supported = !(($config->osver ge qv(10.7.0)) && + ($config->osver lt qv(10.8.0))); + if (!$space_number_supported) { warn "Currently space number not supported on 10.7 Lion\n"; } else { $master->set_space($config->space); @@ -1272,7 +1327,8 @@ sub new { (map { ('--config', $_) } @config), ) or next; $greeting .= "$slave_id ".$slave->uid."\n"; - $slave->set_space($config->space) if $config->space; + $slave->set_space($config->space) + if $config->space && $space_number_supported; $slave->set_settings_set($config->slave_settings_set) if $config->slave_settings_set; } @@ -1300,6 +1356,7 @@ use base qw(CsshX::Window::Slave); use base qw(CsshX::Process); my $TIOCSTI = 0x80017472; # 10.5/10.6 +my $tiocsti_failed; sub new { my ($pack) = @_; @@ -1339,7 +1396,8 @@ sub new { push @cmd, $config->remote_command if length $config->remote_command; print join(" ", @cmd)."\n" if $config->debug; - exec(@cmd) || die $!; + exec(@cmd); + die $!; } } @@ -1347,7 +1405,11 @@ sub can_read { my ($obj) = @_; my $buffer = $obj->read_buffered; foreach (split //, $buffer) { - ioctl(STDIN, $TIOCSTI, $_) == 0 || die; + ioctl(STDIN, $TIOCSTI, $_) || do { + warn "TIOCSTI failed: $!. csshX cannot inject terminal input on this system.\n" + unless $tiocsti_failed++; + last; + }; } $obj->set_read_buffer(''); } @@ -1385,7 +1447,7 @@ sub new { my $sock = $config->sock || die "--sock sockfile is required"; unlink $sock; my $obj = $pack->SUPER::new(Listen => 32, Local => $sock) || die $!; - chmod 0700, $sock || die "Chmod"; + chmod(0700, $sock) || die "Chmod: $!"; local $SIG{INT} = 'IGNORE'; local $SIG{TSTP} = 'IGNORE'; @@ -1861,6 +1923,10 @@ my $modes = { } elsif ($buffer =~ s/^(.*?)\r?\n//) { my $filename = $1; $filename = "Desktop/csshx_scrollback" unless length $filename; + my $home = $ENV{HOME} || (getpwuid($<))[7] || '.'; + $filename =~ s{^~(?=/|$)}{$home}; + $filename = File::Spec->catfile($home, $filename) + unless File::Spec->file_name_is_absolute($filename); my %seen; foreach my $window (CsshX::Master::Socket::Slave->slaves) { @@ -1875,10 +1941,14 @@ my $modes = { } $seen{$extension} = 1; - print "Writing to [$filename.$extension.txt]\n" if $config->debug; - open(my $out, ">", "$filename.$extension.txt") || warn $!; - print $out $window->tabobj->history->UTF8String; - close($out); + my $path = "$filename.$extension.txt"; + print "Writing to [$path]\n" if $config->debug; + if (open(my $out, ">", $path)) { + print $out $window->tabobj->history->UTF8String; + close($out); + } else { + warn "Can't write [$path]: $!"; + } } return $obj->set_mode_and_parse('input', $buffer); @@ -2240,7 +2310,7 @@ if ($config->help) { $config->pod(-verbose => 1) } elsif ($config->list_clusters){ CsshX::Env->list_clusters() } elsif ($config->bash_env){ CsshX::Env->bash() } elsif ($config->man) { $config->pod(-verbose => 2) } -elsif ($config->version) { die sprintf "csshX $VERSION\n", $VERSION } +elsif ($config->version) { die "csshX $VERSION\n" } elsif ($config->master) { CsshX::Master->new() } elsif ($config->slave) { CsshX::Slave->new() } else { CsshX::Launcher->new() }