Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 134 additions & 64 deletions csshX
Original file line number Diff line number Diff line change
Expand Up @@ -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..
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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/#.*$//;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)$/) {
Expand Down Expand Up @@ -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}; }

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) = @_;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) = @_;
Expand Down Expand Up @@ -1339,15 +1396,20 @@ 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 $!;
}
}

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('');
}
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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() }
Expand Down