Implements tee://, a port scheme that allows sending a stream to two destinations at the same time.
This port scheme allows creating a "filter" port that works like a "tee"; that is, it lets data pass through it, but it also copies it into another port. This basically allows creating a "bifurcation", often called a "tee" (because of the shape of the letter T). This is very useful in combination with the chain:// scheme or the pipe function, as it allows creating complex filtering graphs for your data stream.
We define the tee:// scheme in the usual way; although this is not really a network protocol, REBOL does not like it if we don't use net-install. The specified port id does not matter (any random number will do).
Since we're writing a non pass-thru port, we only have to define read and write, and REBOL will map insert, copy, pick etc. to them internally; REBOL will also do any line termination conversion, however we recommend only using /binary ports for filtering. This scheme has not been tested without /binary.
〈Overview〉 ≡
net-utils/net-install 'tee make Root-Protocol [
init: func [port spec] [
〈Initialize the tee port port from spec〉
]
open: func [port] [
〈Open the tee port port〉
]
close: func [port] [
〈Close the tee port port〉
]
write: func [port data /local 〈write's locals〉] [
〈Write data into the buffer and to port/sub-port〉
]
read: func [port data /local 〈read's locals〉] [
〈Put the contents of the buffer into data and clear the buffer〉
]
update: func [port] [
〈Update port/sub-port〉
]
] 80
The following example will create a port that lets all data pass through unaltered, but also copies and inserts it into my-port.
〈Usage examples〉 ≡
tee: open/binary [scheme: 'tee sub-port: my-port]
If you just want to copy to a file, you can specify it directly or use a url! like in the example below and the file will be opened for writing and closed automatically for you.
〈Usage examples〉 +≡
tee: open/binary [scheme: 'tee sub-port: %/path/to/file]
tee: open/binary tee:/path/to/file
The implementation of the port scheme is relatively trivial.
The init function is used to initialize the port (called when you make it). The user will neet to pass a port to write to in the sub-port field, or specify a file. (See 〈Usage examples〉.)
〈Initialize the tee port port from spec〉 ≡
port/url: spec
if url? spec [
; assume that user wants to write to a file
spec: to file! skip spec 4
if find/match spec %// [remove spec]
port/sub-port: spec
]
if none? port/sub-port [
net-error "You must specify a sub port to write to"
]
The open function creates a buffer to store data into and, if a file! was provided, opens it in /binary/direct/write/new mode. We also force the /direct mode, since we are a "filter" kind of port.
〈Open the tee port port〉 ≡
port/locals: context [
buffer: make binary! 1024
close?: no
]
if file? port/sub-port [
port/locals/close?: yes
port/sub-port: system/words/open/binary/direct/write/new port/sub-port
]
port/state/flags: port/state/flags or system/standard/port-flags/direct
On close, we need to close the file port if we had opened it; we also set port/locals to none to allow REBOL to reclaim memory.
〈Close the tee port port〉 ≡
if port/locals/close? [
system/words/close port/sub-port
; allow reopening it easily
port/sub-port: join port/sub-port/path port/sub-port/target
]
port/locals: none
On write, we write the data into the sub port, and also store it into our buffer.
Note: we have a workaround here for a bug with checksum:// ports. We'll remove this code once the bug has been fixed.
〈Write data into the buffer and to port/sub-port〉 ≡
set/any 'len write-io port/sub-port data port/state/num
; checksum:// will return unset, so we always assume all data has been written
unless value? 'len [len: port/state/num]
insert/part tail port/locals/buffer data len
len
〈write's locals〉 ≡
len
On read, we copy data from our buffer into data. We remove the copied data from port/locals/buffer and return the actual number of bytes copied.
〈Put the contents of the buffer into data and clear the buffer〉 ≡
len: min length? port/locals/buffer port/state/num
insert/part tail data port/locals/buffer len
remove/part port/locals/buffer len
len
〈read's locals〉 ≡
len
On update, we just call update on the sub port (we're using attempt because file ports don't like update).
〈Update port/sub-port〉 ≡
attempt [system/words/update port/sub-port]