notesy::ipc

Talking to a program on this computer, through the sockets the plugin names in [ipc] (plugin.toml). Naming sockets there asks for the ipc permission; the module is there only once the user grants it.

The plugin owns the connection. It speaks whatever the program on the other end speaks, and notesy stays out of the middle: it never reads what passes, and never writes anything of its own. What notesy keeps is the decision about which sockets may be opened at all, and how much may go through one.

rustuse notesy::{ipc, log};

pub fn ready() {
    let got = ipc::connect("run:discord-ipc-0", |event| {
        match event.kind {
            ipc::Event::Open => event.socket.send("hello"),
            ipc::Event::Data => log::info(`${event.bytes.len()} bytes back`),
            ipc::Event::Closed => log::warn(`it hung up: ${event.says}`),
            _ => {}
        }
    });
    if let Err(why) = got {
        log::info(`Discord isn't running: ${why}`);
    }
}

#ipc::connect(name, then)

Opens the socket name, one its manifest names. Ok(socket), or Err(why) when it wouldn't open. then(event) is called on the script's thread for everything that happens on it, until it closes.

A program that isn't running has no socket, so Err is an ordinary answer rather than a fault: Discord numbers its own 0 to 9 and a plugin tries them in turn. Asking for a socket the manifest doesn't name is the plugin's own mistake, and is raised where it's written.

name is the socket as the manifest writes it (run:discord-ipc-0), never a path: a path can't be written once for every platform, and one in a prompt tells the user nothing. A name that isn't one of its own is an error where connect is called, not in then.

Opening is judged twice. The name has to be one the manifest named, and then whatever answers has to pass: it has to be a socket, and it has to belong to you. A socket answering as the system, as an administrator or as another user is refused as it opens, wherever it sits. Some notesy refuses outright, for every plugin, however they're named and whoever signed them: see plugin.toml.

#The event

Field What it is
kind An ipc::Event: Open, Data or Closed.
socket The connection it happened on, to send on or close.
bytes What arrived, for Data; empty otherwise.
why An ipc::Closed, for Closed: why it ended.
says The same in a word, for a log.

Open comes once, when the connection is through and everything about it passed. Data comes as bytes arrive, in the order they arrive: a socket is a stream, so what one send on the other end wrote may come in pieces, and two may come at once. Whatever framing the program uses is the plugin's to read.

Closed comes once, last, and nothing follows it.

ipc::Closed What happened
Gone The program on the other end hung up. Try again if it makes sense.
Refused It couldn't be opened, or what answered wasn't allowed to. Don't try again.
Stopped The user hung it up, or the plugin is being turned off.
Idle Nothing went either way for ten minutes.
TooMuch It went past what one plugin may push through a socket.

#Socket

Call What it does
socket.send(what) Sends bytes, or text as it's written. At most 1 MB at a time.
socket.close() Hangs up. Closed follows, with Stopped.
socket.name() The socket as its manifest names it.
socket.open() Whether it's still open.

Sending on a closed connection is an error, not a crash: check open(), or let Closed tell you.

#What it may not do

A plugin holds at most 10 connections at once, so it can try the ten Discord opens without holding them all. A message is at most 1 MB, and 8 MB a second may go each way; past that the connection closes with TooMuch. A connection with nothing going either way for ten minutes closes with Idle.

Turning a plugin off, or reloading it, hangs up everything it has open: a plugin that isn't running is talking to nothing.

A preview never gets a socket, notesy preview --trust or not: a preview is for looking at, and a socket reaches a program outside notesy altogether.

#What the user sees

The prompt says this first and on its own, above what the plugin would do to their notes, because notesy can't see what passes between a plugin and a program and so can't say where it ends up.

A plugin's page in Settings shows every connection it has open, where each one went, how much has gone each way and how long it's been open, with a way to hang one up. That works whether or not the plugin cooperates: the connection is notesy's to close.

#Where sockets live

run:name is where running programs keep theirs, and own:name is the plugin's own folder, which nothing else writes to. A name ending in * covers the names that start that way.

toml[ipc]
sockets = ["run:discord-ipc-*"]

On Windows these are named pipes under \\.\pipe\, and the same plugin works there unchanged: run:discord-ipc-0 finds the pipe of that name. A pipe already serving someone else is waited on briefly, since a program that hands out one connection at a time is busy for a moment rather than for a minute.

Who answered is checked there too, by a different road. Windows has nothing to ask a pipe who is on the other end, so notesy asks the pipe which process serves it and then asks that process whose it is. A pipe that won't say is refused, the same as one belonging to somebody else.

Every page