Allow launched background process to work with --allow-remote-control

Use a dedicated socketpair for such processes. Fixes #6712
This commit is contained in:
Kovid Goyal 2023-10-18 19:56:07 +05:30
parent a9b412baba
commit 314fe4fe4a
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
11 changed files with 275 additions and 92 deletions

View file

@ -52,6 +52,8 @@ Detailed list of changes
- A new mouse action ``mouse_selection word_and_line_from_point`` to select the current word under the mouse cursor and extend to end of line (:pull:`6663`)
- Allow :option:`kitty @ launch --allow-remote-control` to work even with background processes, by using a dedicated socketpair for the process (:iss:`6712`)
- Allow using the full range of standard mouse cursor shapes when customizing the mouse cursor
- macOS: When running the default shell with the login program fix :file:`~/.hushlogin` not being respected when opening windows not in the home directory (:iss:`6689`)

View file

@ -180,7 +180,9 @@ Variables that kitty sets when running child programs
Set when the :doc:`remote control <remote-control>` facility is enabled and
the a socket is used for control via :option:`kitty --listen-on` or :opt:`listen_on`.
Contains the path to the socket. Avoid the need to use :option:`kitty @ --to` when
issuing remote control commands.
issuing remote control commands. Can also be a file descriptor of the form
fd:num instead of a socket address, in which case, remote control
communication should proceed over the specified file descriptor.
.. envvar:: KITTY_PIPE_DATA

View file

@ -23,6 +23,7 @@
Iterator,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
@ -333,6 +334,7 @@ def __init__(
self.clipboard = Clipboard()
self.primary_selection = Clipboard(ClipboardType.primary_selection)
self.update_check_started = False
self.peer_data_map: Dict[int, Optional[Dict[str, Sequence[str]]]] = {}
self.encryption_key = EllipticCurveKey()
self.encryption_public_key = f'{RC_ENCRYPTION_PROTOCOL_VERSION}:{base64.b85encode(self.encryption_key.public).decode("ascii")}'
self.clipboard_buffers: Dict[str, str] = {}
@ -597,14 +599,16 @@ def add_child(self, window: Window) -> None:
self.window_id_map[window.id] = window
def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer_id: int = 0) -> RCResponse:
from .remote_control import is_cmd_allowed, parse_cmd
from .remote_control import is_cmd_allowed, parse_cmd, remote_control_allowed
response = None
window = window or None
from_socket = peer_id > 0
is_fd_peer = from_socket and peer_id in self.peer_data_map
window_has_remote_control = bool(window and window.allow_remote_control)
if not window_has_remote_control:
if not window_has_remote_control and not is_fd_peer:
if self.allow_remote_control == 'n':
return {'ok': False, 'error': 'Remote control is disabled'}
if self.allow_remote_control == 'socket-only' and peer_id == 0:
if self.allow_remote_control == 'socket-only' and not from_socket:
return {'ok': False, 'error': 'Remote control is allowed over a socket only'}
try:
pcmd = parse_cmd(cmd, self.encryption_key)
@ -628,13 +632,16 @@ def _handle_remote_command(self, cmd: str, window: Optional[Window] = None, peer
extra_data: Dict[str, Any] = {}
try:
allowed_unconditionally = (
self.allow_remote_control == 'y' or (peer_id > 0 and self.allow_remote_control in ('socket-only', 'socket')) or
(window and window.remote_control_allowed(pcmd, extra_data)))
self.allow_remote_control == 'y' or
(from_socket and not is_fd_peer and self.allow_remote_control in ('socket-only', 'socket')) or
(window and window.remote_control_allowed(pcmd, extra_data)) or
(is_fd_peer and remote_control_allowed(pcmd, self.peer_data_map.get(peer_id), None, extra_data))
)
except PermissionError:
return {'ok': False, 'error': 'Remote control disallowed by window specific password'}
if allowed_unconditionally:
return self._execute_remote_command(pcmd, window, peer_id, self_window)
q = is_cmd_allowed(pcmd, window, peer_id > 0, extra_data)
q = is_cmd_allowed(pcmd, window, from_socket, extra_data)
if q is True:
return self._execute_remote_command(pcmd, window, peer_id, self_window)
if q is None:
@ -761,20 +768,30 @@ def call_remote_control(self, self_window: Optional[Window], args: Tuple[str, ..
return None
raise
def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes, bool, None]:
cmd_prefix = b'\x1bP@kitty-cmd'
terminator = b'\x1b\\'
if msg_bytes.startswith(cmd_prefix) and msg_bytes.endswith(terminator):
cmd = msg_bytes[len(cmd_prefix):-len(terminator)].decode('utf-8')
response = self._handle_remote_command(cmd, peer_id=peer_id)
if response is None:
return None
if isinstance(response, AsyncResponse):
return True
from kitty.remote_control import encode_response_for_peer
return encode_response_for_peer(response)
def peer_message_received(self, msg_bytes: bytes, peer_id: int, is_remote_control: bool) -> Union[bytes, bool, None]:
if peer_id > 0 and msg_bytes == b'peer_death':
self.peer_data_map.pop(peer_id, None)
return False
if is_remote_control:
cmd_prefix = b'\x1bP@kitty-cmd'
terminator = b'\x1b\\'
if msg_bytes.startswith(cmd_prefix) and msg_bytes.endswith(terminator):
cmd = msg_bytes[len(cmd_prefix):-len(terminator)].decode('utf-8')
response = self._handle_remote_command(cmd, peer_id=peer_id)
if response is None:
return None
if isinstance(response, AsyncResponse):
return True
from kitty.remote_control import encode_response_for_peer
return encode_response_for_peer(response)
log_error('Malformatted remote control message received from peer, ignoring')
return None
data:SingleInstanceData = json.loads(msg_bytes.decode('utf-8'))
try:
data:SingleInstanceData = json.loads(msg_bytes.decode('utf-8'))
except Exception:
log_error('Malformed command received over single instance socket, ignoring')
return None
if isinstance(data, dict) and data.get('cmd') == 'new_instance':
from .cli_stub import CLIOptions
startup_id = data['environ'].get('DESKTOP_STARTUP_ID', '')
@ -812,7 +829,7 @@ def peer_message_received(self, msg_bytes: bytes, peer_id: int) -> Union[bytes,
elif activation_token and is_wayland() and os_window_id:
focus_os_window(os_window_id, True, activation_token)
else:
log_error('Unknown message received from peer, ignoring')
log_error('Unknown message received over single instance socket, ignoring')
return None
def handle_remote_cmd(self, cmd: str, window: Optional[Window] = None) -> None:
@ -2232,7 +2249,9 @@ def run_background_process(
cwd: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
stdin: Optional[bytes] = None,
cwd_from: Optional[CwdRequest] = None
cwd_from: Optional[CwdRequest] = None,
allow_remote_control: bool = False,
remote_control_passwords: Optional[Dict[str, Sequence[str]]] = None,
) -> None:
import subprocess
env = env or None
@ -2244,28 +2263,56 @@ def run_background_process(
with suppress(Exception):
cwd = cwd_from.cwd_of_child
def add_env(key: str, val: str) -> None:
nonlocal env
if env is None:
env = default_env().copy()
env[key] = val
def doit(activation_token: str = '') -> None:
nonlocal env
if activation_token:
if env is None:
env = default_env().copy()
env['XDG_ACTIVATION_TOKEN'] = activation_token
if stdin:
r, w = safe_pipe(False)
pass_fds: Tuple[int, ...] = ()
if allow_remote_control:
import socket
local, remote = socket.socketpair()
os.set_inheritable(remote.fileno(), True)
lfd = os.dup(local.fileno())
local.close()
try:
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals)
peer_id = self.child_monitor.inject_peer(lfd)
except Exception:
os.close(w)
os.close(lfd)
remote.close()
raise
pass_fds = (remote.fileno(),)
add_env('KITTY_LISTEN_ON', f'fd:{remote.fileno()}')
self.peer_data_map[peer_id] = remote_control_passwords
if activation_token:
add_env('XDG_ACTIVATION_TOKEN', activation_token)
try:
if stdin:
r, w = safe_pipe(False)
try:
subprocess.Popen(cmd, env=env, stdin=r, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True)
except Exception:
os.close(w)
else:
thread_write(w, stdin)
finally:
os.close(r)
else:
thread_write(w, stdin)
finally:
os.close(r)
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals, pass_fds=pass_fds, close_fds=True)
finally:
if allow_remote_control:
remote.close()
try:
if is_wayland():
run_with_activation_token(doit)
else:
subprocess.Popen(cmd, env=env, cwd=cwd, preexec_fn=clear_handled_signals)
if is_wayland():
run_with_activation_token(doit)
else:
doit()
doit()
except Exception as err:
self.show_error(_('Failed to run background process'), _('Failed to run background process with error: {}').format(err))
def pipe(self, source: str, dest: str, exe: str, *args: str) -> Optional[Window]:
cmd = [exe] + list(args)

View file

@ -47,6 +47,7 @@ typedef struct {
char *data;
size_t sz;
id_type peer_id;
bool is_remote_control_peer;
} Message;
typedef struct {
@ -232,8 +233,49 @@ static void* io_loop(void *data);
static void* talk_loop(void *data);
static void send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz);
static void wakeup_talk_loop(bool);
static bool add_peer_to_injection_queue(int peer_fd, int pipe_fd);
static bool talk_thread_started = false;
static bool
simple_read_from_pipe(int fd, void *data, size_t sz) {
// read a small amount of data to a pipe handling only EINTR
while (true) {
ssize_t ret = read(fd, data, sz);
if (ret == -1 && errno == EINTR) continue;
return ret == (ssize_t)sz;
}
}
static PyObject*
inject_peer(PyObject *s, PyObject *a) {
#define inject_peer_doc "inject_peer(fd) -> Start communication with a peer over the specified file descriptor"
ChildMonitor *self = (ChildMonitor*)s;
if (!PyLong_Check(a)) { PyErr_SetString(PyExc_TypeError, "peer fd must be an int"); return NULL; }
long fd = PyLong_AsLong(a);
if (fd < 0) { PyErr_Format(PyExc_ValueError, "Invalid peer fd: %ld", fd); return NULL; }
if (!talk_thread_started) {
int ret;
if ((ret = pthread_create(&self->talk_thread, NULL, talk_loop, self)) != 0) {
return PyErr_Format(PyExc_OSError, "Failed to start talk thread with error: %s", strerror(ret));
}
talk_thread_started = true;
}
int fds[2] = {0};
if (!self_pipe(fds, false)) return PyErr_SetFromErrno(PyExc_OSError);
if (!add_peer_to_injection_queue(fd, fds[1])) {
safe_close(fds[0], __FILE__, __LINE__); safe_close(fds[1], __FILE__, __LINE__);
PyErr_SetString(PyExc_RuntimeError, "Too many peers waiting to be injected");
return NULL;
}
wakeup_talk_loop(false);
id_type peer_id = 0;
bool ok = simple_read_from_pipe(fds[0], &peer_id, sizeof(peer_id));
safe_close(fds[0], __FILE__, __LINE__);
if (!ok) { PyErr_SetString(PyExc_RuntimeError, "Failed to read peer id from self pipe"); return NULL; }
return PyLong_FromUnsignedLongLong(peer_id);
}
static PyObject *
start(PyObject *s, PyObject *a UNUSED) {
#define start_doc "start() -> Start the I/O thread"
@ -467,7 +509,7 @@ parse_input(ChildMonitor *self) {
Message *msg = msgs + i;
PyObject *resp = NULL;
if (msg->data) {
resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "y#K", msg->data, (int)msg->sz, msg->peer_id);
resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "y#KO", msg->data, (int)msg->sz, msg->peer_id, msg->is_remote_control_peer ? Py_True : Py_False);
free(msg->data);
if (!resp) PyErr_Print();
}
@ -1563,6 +1605,7 @@ typedef struct {
size_t capacity, used;
bool failed;
} write;
bool is_remote_control_peer;
} Peer;
static id_type peer_id_counter = 0;
@ -1577,24 +1620,33 @@ typedef struct pollfd PollFD;
#define PEER_LIMIT 256
#define nuke_socket(s) { shutdown(s, SHUT_RDWR); safe_close(s, __FILE__, __LINE__); }
static bool
accept_peer(int listen_fd, bool shutting_down) {
int peer = accept(listen_fd, NULL, NULL);
if (UNLIKELY(peer == -1)) {
if (errno == EINTR) return true;
if (!shutting_down) perror("accept() on talk socket failed!");
return false;
}
static id_type
add_peer(int peer, bool is_remote_control_peer) {
id_type ans = 0;
if (talk_data.num_peers < PEER_LIMIT) {
ensure_space_for(&talk_data, peers, Peer, talk_data.num_peers + 8, peers_capacity, 8, false);
Peer *p = talk_data.peers + talk_data.num_peers++;
memset(p, 0, sizeof(Peer));
p->fd = peer; p->id = ++peer_id_counter;
if (!p->id) p->id = ++peer_id_counter;
ans = p->id;
p->is_remote_control_peer = is_remote_control_peer;
} else {
log_error("Too many peers want to talk, ignoring one.");
nuke_socket(peer);
}
return ans;
}
static bool
accept_peer(int listen_fd, bool shutting_down, bool is_remote_control_peer) {
int peer = accept(listen_fd, NULL, NULL);
if (UNLIKELY(peer == -1)) {
if (errno == EINTR) return true;
if (!shutting_down) perror("accept() on talk socket failed!");
return false;
}
add_peer(peer, is_remote_control_peer);
return true;
}
@ -1621,11 +1673,23 @@ queue_peer_message(ChildMonitor *self, Peer *peer) {
}
}
m->peer_id = peer->id;
m->is_remote_control_peer = peer->is_remote_control_peer;
peer->num_of_unresponded_messages_sent_to_main_thread++;
talk_mutex(unlock);
wakeup_main_loop();
}
static void
notify_on_peer_removal(ChildMonitor *self, const Peer *p) {
ensure_space_for(self, messages, Message, self->messages_count + 16, messages_capacity, 16, true);
Message *m = self->messages + self->messages_count++;
memset(m, 0, sizeof(Message));
m->data = strdup("peer_death");
if (m->data) m->sz = strlen("peer_death");
m->peer_id = p->id;
m->is_remote_control_peer = p->id;
}
static bool
has_complete_peer_command(Peer *peer) {
peer->read.command_end = 0;
@ -1702,17 +1766,52 @@ wakeup_talk_loop(bool in_signal_handler) {
}
static void
prune_peers(void) {
static bool
prune_peers(ChildMonitor *self) {
bool pruned = false;
for (size_t idx = talk_data.num_peers; idx-- > 0;) {
Peer *p = talk_data.peers + idx;
if (p->read.finished && !p->num_of_unresponded_messages_sent_to_main_thread && !p->write.used) {
notify_on_peer_removal(self, p);
free_peer(p);
remove_i_from_array(talk_data.peers, idx, talk_data.num_peers);
pruned = true;
}
}
if (pruned) wakeup_main_loop();
return pruned;
}
static struct {
size_t num;
struct { int peer_fd, pipe_fd; } fds[16];
} peers_to_inject = {0};
static bool
add_peer_to_injection_queue(int peer_fd, int pipe_fd) {
bool added = false;
talk_mutex(lock);
if (peers_to_inject.num < arraysz(peers_to_inject.fds)) {
peers_to_inject.fds[peers_to_inject.num].peer_fd = peer_fd;
peers_to_inject.fds[peers_to_inject.num].pipe_fd = pipe_fd;
peers_to_inject.num++;
added = true;
}
talk_mutex(unlock);
return added;
}
static void
simple_write_to_pipe(int fd, void *data, size_t sz) {
// write a small amount of data to a pipe handling only EINTR
while (true) {
ssize_t ret = write(fd, data, sz);
if (ret == -1 && errno == EINTR) continue;
break;
}
}
static void*
talk_loop(void *data) {
// The talk thread loop
@ -1731,9 +1830,17 @@ talk_loop(void *data) {
while (LIKELY(!self->shutting_down)) {
num_peer_fds = 0;
talk_mutex(lock);
if (peers_to_inject.num) {
for (size_t i = 0; i < peers_to_inject.num; i++) {
id_type added_peer_id = add_peer(peers_to_inject.fds[i].peer_fd, true);
simple_write_to_pipe(peers_to_inject.fds[i].pipe_fd, &added_peer_id, sizeof(id_type));
safe_close(peers_to_inject.fds[i].pipe_fd, __FILE__, __LINE__);
}
peers_to_inject.num = 0;
}
if (talk_data.num_peers > 0) {
talk_mutex(lock);
prune_peers();
prune_peers(self);
for (size_t i = 0; i < talk_data.num_peers; i++) {
Peer *p = talk_data.peers + i;
if (!p->read.finished || p->write.used) {
@ -1746,14 +1853,14 @@ talk_loop(void *data) {
fds[p->fd_array_idx].events = flags;
} else p->fd_array_idx = 0;
}
talk_mutex(unlock);
}
talk_mutex(unlock);
for (size_t i = 0; i < num_listen_fds; i++) fds[i].revents = 0;
int ret = poll(fds, num_listen_fds + num_peer_fds, -1);
if (ret > 0) {
for (size_t i = 0; i < num_listen_fds - 1; i++) {
if (fds[i].revents & POLLIN) {
if (!accept_peer(fds[i].fd, self->shutting_down)) goto end;
if (!accept_peer(fds[i].fd, self->shutting_down, fds[i].fd == self->listen_fd)) goto end;
}
}
if (fds[num_listen_fds - 1].revents & POLLIN) {
@ -1814,6 +1921,7 @@ send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) {
// Boilerplate {{{
static PyMethodDef methods[] = {
METHOD(add_child, METH_VARARGS)
METHOD(inject_peer, METH_O)
METHOD(needs_write, METH_VARARGS)
METHOD(start, METH_NOARGS)
METHOD(wakeup, METH_NOARGS)

View file

@ -1271,6 +1271,8 @@ class ChildMonitor:
def shutdown_monitor(self) -> None:
pass
def inject_peer(self, fd: int) -> int: ...
class KeyEvent:

View file

@ -71,7 +71,11 @@ def options_spec() -> str:
intended to run for a long time as a primary window.
:code:`background`
The process will be run in the :italic:`background`, without a kitty window.
The process will be run in the :italic:`background`, without a kitty
window. Note that if :option:`kitty @ launch --allow-remote-control` is
specified the :envvar:`KITTY_LISTEN_ON` environment variable will be set to
a dedicated socket pair file descriptor that the process can use for remote
control.
:code:`clipboard`, :code:`primary`
These two are meant to work with :option:`--stdin-source <launch --stdin-source>` to copy
@ -583,7 +587,10 @@ def _launch(
cmd = kw['cmd']
if not cmd:
raise ValueError('The cmd to run must be specified when running a background process')
boss.run_background_process(cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin'])
boss.run_background_process(
cmd, cwd=kw['cwd'], cwd_from=kw['cwd_from'], env=env or None, stdin=kw['stdin'],
allow_remote_control=kw['allow_remote_control'], remote_control_passwords=kw['remote_control_passwords']
)
elif opts.type in ('clipboard', 'primary'):
stdin = kw.get('stdin')
if stdin is not None:

View file

@ -19,6 +19,7 @@
Iterator,
List,
Optional,
Sequence,
Tuple,
Union,
cast,
@ -104,6 +105,27 @@ def fnmatch_pattern(pat: str) -> 're.Pattern[str]':
return re.compile(translate(pat))
def remote_control_allowed(
pcmd: Dict[str, Any], remote_control_passwords: Optional[Dict[str, Sequence[str]]],
window: Optional['Window'], extra_data: Dict[str, Any]
) -> bool:
if not remote_control_passwords:
return True
pw = pcmd.get('password', '')
auth_items = remote_control_passwords.get(pw)
if pw == '!':
auth_items = None
if auth_items is None:
if '!' in remote_control_passwords:
raise PermissionError()
return False
from .remote_control import password_authorizer
pa = password_authorizer(auth_items)
if not pa.is_cmd_allowed(pcmd, window, False, extra_data):
raise PermissionError()
return True
class PasswordAuthorizer:
def __init__(self, auth_items: FrozenSet[str]) -> None:

View file

@ -557,21 +557,8 @@ def __init__(
def remote_control_allowed(self, pcmd: Dict[str, Any], extra_data: Dict[str, Any]) -> bool:
if not self.allow_remote_control:
return False
if not self.remote_control_passwords:
return True
pw = pcmd.get('password', '')
auth_items = self.remote_control_passwords.get(pw)
if pw == '!':
auth_items = None
if auth_items is None:
if '!' in self.remote_control_passwords:
raise PermissionError()
return False
from .remote_control import password_authorizer
pa = password_authorizer(auth_items)
if not pa.is_cmd_allowed(pcmd, self, False, extra_data):
raise PermissionError()
return True
from .remote_control import remote_control_allowed
return remote_control_allowed(pcmd, self.remote_control_passwords, self, extra_data)
@property
def file_transmission_control(self) -> 'FileTransmission':

View file

@ -55,14 +55,6 @@ func escape_list_of_strings(args []string) []escaped_string {
return ans
}
func escape_dict_of_strings(args map[string]string) map[escaped_string]escaped_string {
ans := make(map[escaped_string]escaped_string, len(args))
for k, v := range args {
ans[escaped_string(k)] = escaped_string(v)
}
return ans
}
func set_payload_string_field(io_data *rc_io_data, field, data string) {
payload_interface := reflect.ValueOf(&io_data.rc.Payload).Elem()
struct_in_interface := reflect.New(payload_interface.Elem().Type()).Elem()
@ -143,16 +135,6 @@ func simple_serializer(rc *utils.RemoteControlCmd) (ans []byte, err error) {
type serializer_func func(rc *utils.RemoteControlCmd) ([]byte, error)
func debug_to_log(args ...any) {
f, err := os.OpenFile("/tmp/kdlog", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666)
if err == nil {
fmt.Fprintln(f, args...)
f.Close()
}
}
var serializer serializer_func = simple_serializer
func create_serializer(password string, encoded_pubkey string, io_data *rc_io_data) (err error) {
io_data.serializer = simple_serializer
if password != "" {
@ -240,7 +222,7 @@ func get_response(do_io func(io_data *rc_io_data) ([]byte, error), io_data *rc_i
io_data.multiple_payload_generator = nil
io_data.rc.NoResponse = true
io_data.chunks_done = false
do_io(io_data)
_, _ = do_io(io_data)
err = fmt.Errorf("Timed out waiting for a response from kitty")
}
return

View file

@ -8,6 +8,8 @@ import (
"fmt"
"io"
"net"
"os"
"strconv"
"time"
"kitty/tools/tui/loop"
@ -151,9 +153,23 @@ func simple_socket_io(conn *net.Conn, io_data *rc_io_data) (serialized_response
}
func do_socket_io(io_data *rc_io_data) (serialized_response []byte, err error) {
conn, err := net.Dial(global_options.to_network, global_options.to_address)
if err != nil {
return
var conn net.Conn
if global_options.to_network == "fd" {
fd, _ := strconv.Atoi(global_options.to_address)
if err != nil {
return nil, err
}
f := os.NewFile(uintptr(fd), "fd:"+global_options.to_address)
conn, err = net.FileConn(f)
if err != nil {
return nil, err
}
defer f.Close()
} else {
conn, err = net.Dial(global_options.to_network, global_options.to_address)
if err != nil {
return
}
}
defer conn.Close()
return simple_socket_io(&conn, io_data)

View file

@ -5,6 +5,7 @@ package utils
import (
"fmt"
"runtime"
"strconv"
"strings"
"github.com/seancfoley/ipaddress-go/ipaddr"
@ -37,6 +38,13 @@ func ParseSocketAddress(spec string) (network string, addr string, err error) {
}
return
}
if network == "fd" {
fd := -1
if fd, err = strconv.Atoi(addr); err != nil || fd < 0 {
err = fmt.Errorf("Not a valid file descriptor number: %#v. Cannot use: %s", addr, spec)
}
return
}
err = fmt.Errorf("Unknown network type: %#v in socket address: %s", network, spec)
return
}