blob: 29c1491586bf8e969effefbf5de83d57906222ea [file] [log] [blame]
James Kuszmaulcf324122023-01-14 14:07:17 -08001// Copyright (c) FIRST and other WPILib contributors.
2// Open Source Software; you can modify and/or share it under the terms of
3// the WPILib BSD license file in the root directory of this project.
4
5#ifdef __APPLE__
6#include <util.h>
7#elif !defined(_WIN32)
8#include <pty.h>
9#endif
10
11#include <cstdio>
12
13#include <fmt/format.h>
14#include <wpi/MathExtras.h>
15#include <wpi/SmallVector.h>
16#include <wpi/StringExtras.h>
James Kuszmaulb13e13f2023-11-22 20:44:04 -080017#include <wpi/bit.h>
James Kuszmaulcf324122023-01-14 14:07:17 -080018#include <wpi/timestamp.h>
19
20#include "wpinet/raw_uv_ostream.h"
21#include "wpinet/uv/Loop.h"
22#include "wpinet/uv/Pipe.h"
23#include "wpinet/uv/Process.h"
24#include "wpinet/uv/Signal.h"
25#include "wpinet/uv/Tcp.h"
26#include "wpinet/uv/Tty.h"
27#include "wpinet/uv/Udp.h"
28#include "wpinet/uv/util.h"
29
30namespace uv = wpi::uv;
31
32static uint64_t startTime = wpi::Now();
33
34static bool NewlineBuffer(std::string& rem, uv::Buffer& buf, size_t len,
35 wpi::SmallVectorImpl<uv::Buffer>& bufs, bool tcp,
36 uint16_t tcpSeq) {
37 // scan for last newline
38 std::string_view str(buf.base, len);
39 size_t idx = str.rfind('\n');
40 if (idx == std::string_view::npos) {
41 // no newline yet, just keep appending to remainder
42 rem += str;
43 return false;
44 }
45
46 // build output
47 wpi::raw_uv_ostream out(bufs, 4096);
48 std::string_view toCopy = wpi::slice(str, 0, idx + 1);
49 if (tcp) {
50 // Header is 2 byte len, 1 byte type, 4 byte timestamp, 2 byte sequence num
James Kuszmaulb13e13f2023-11-22 20:44:04 -080051 uint32_t ts =
52 wpi::bit_cast<uint32_t, float>((wpi::Now() - startTime) * 1.0e-6);
James Kuszmaulcf324122023-01-14 14:07:17 -080053 uint16_t len = rem.size() + toCopy.size() + 1 + 4 + 2;
54 const uint8_t header[] = {static_cast<uint8_t>((len >> 8) & 0xff),
55 static_cast<uint8_t>(len & 0xff),
56 12,
57 static_cast<uint8_t>((ts >> 24) & 0xff),
58 static_cast<uint8_t>((ts >> 16) & 0xff),
59 static_cast<uint8_t>((ts >> 8) & 0xff),
60 static_cast<uint8_t>(ts & 0xff),
61 static_cast<uint8_t>((tcpSeq >> 8) & 0xff),
62 static_cast<uint8_t>(tcpSeq & 0xff)};
63 out << std::span<const uint8_t>(header);
64 }
65 out << rem << toCopy;
66
67 // reset remainder
68 rem = wpi::slice(str, idx + 1, std::string_view::npos);
69 return true;
70}
71
James Kuszmaulb13e13f2023-11-22 20:44:04 -080072// FIXME: clang-tidy reports a false positive for leaking a captured shared_ptr
73// (clang-analyzer-cplusplus.NewDeleteLeaks)
74
75// NOLINTBEGIN
James Kuszmaulcf324122023-01-14 14:07:17 -080076static void CopyUdp(uv::Stream& in, std::shared_ptr<uv::Udp> out,
77 bool broadcast) {
78 sockaddr_in addr;
79 if (broadcast) {
80 out->SetBroadcast(true);
81 uv::NameToAddr("0.0.0.0", 6666, &addr);
82 } else {
83 uv::NameToAddr("127.0.0.1", 6666, &addr);
84 }
85
86 in.data.connect(
87 [rem = std::make_shared<std::string>(), outPtr = out.get(), addr](
88 uv::Buffer& buf, size_t len) {
89 // build buffers
90 wpi::SmallVector<uv::Buffer, 4> bufs;
91 if (!NewlineBuffer(*rem, buf, len, bufs, false, 0)) {
92 return;
93 }
94
95 // send output
96 outPtr->Send(addr, bufs, [](auto bufs2, uv::Error) {
97 for (auto buf : bufs2) {
98 buf.Deallocate();
99 }
100 });
101 },
102 out);
103}
104
105static void CopyTcp(uv::Stream& in, std::shared_ptr<uv::Stream> out) {
106 struct StreamData {
107 std::string rem;
108 uint16_t seq = 0;
109 };
110 in.data.connect(
111 [data = std::make_shared<StreamData>(), outPtr = out.get()](
112 uv::Buffer& buf, size_t len) {
113 // build buffers
114 wpi::SmallVector<uv::Buffer, 4> bufs;
115 if (!NewlineBuffer(data->rem, buf, len, bufs, true, data->seq++)) {
116 return;
117 }
118
119 // send output
120 outPtr->Write(bufs, [](auto bufs2, uv::Error) {
121 for (auto buf : bufs2) {
122 buf.Deallocate();
123 }
124 });
125 },
126 out);
127}
128
129static void CopyStream(uv::Stream& in, std::shared_ptr<uv::Stream> out) {
130 in.data.connect([out](uv::Buffer& buf, size_t len) {
131 uv::Buffer buf2 = buf.Dup();
132 buf2.len = len;
133 out->Write({buf2}, [](auto bufs, uv::Error) {
134 for (auto buf : bufs) {
135 buf.Deallocate();
136 }
137 });
138 });
139}
James Kuszmaulb13e13f2023-11-22 20:44:04 -0800140// NOLINTEND
James Kuszmaulcf324122023-01-14 14:07:17 -0800141
142int main(int argc, char* argv[]) {
143 // parse arguments
144 int programArgc = 1;
145 bool useUdp = false;
146 bool broadcastUdp = false;
147 bool err = false;
148
149 while (programArgc < argc && argv[programArgc][0] == '-') {
150 if (std::string_view(argv[programArgc]) == "-u") {
151 useUdp = true;
152 } else if (std::string_view(argv[programArgc]) == "-b") {
153 useUdp = true;
154 broadcastUdp = true;
155 } else {
156 fmt::print(stderr, "unrecognized command line option {}\n",
157 argv[programArgc]);
158 err = true;
159 }
160 ++programArgc;
161 }
162
163 if (err || (argc - programArgc) < 1) {
164 std::fputs(argv[0], stderr);
165 std::fputs(
166 " [-ub] program [arguments ...]\n"
167 " -u send udp to localhost port 6666 instead of using tcp\n"
168 " -b broadcast udp to port 6666 instead of using tcp\n",
169 stderr);
170 return EXIT_FAILURE;
171 }
172
173 uv::Process::DisableStdioInheritance();
174
175 auto loop = uv::Loop::Create();
176 loop->error.connect(
177 [](uv::Error err) { fmt::print(stderr, "uv ERROR: {}\n", err.str()); });
178
179 // create pipes to communicate with child
180 auto stdinPipe = uv::Pipe::Create(loop);
181 auto stdoutPipe = uv::Pipe::Create(loop);
182 auto stderrPipe = uv::Pipe::Create(loop);
183
184 // create tty to pass from our console to child's
185 auto stdinTty = uv::Tty::Create(loop, 0, true);
186 auto stdoutTty = uv::Tty::Create(loop, 1, false);
187 auto stderrTty = uv::Tty::Create(loop, 2, false);
188
189 // pass through our console to child's (bidirectional)
190 if (stdinTty) {
191 CopyStream(*stdinTty, stdinPipe);
192 }
193 if (stdoutTty) {
194 CopyStream(*stdoutPipe, stdoutTty);
195 }
196 if (stderrTty) {
197 CopyStream(*stderrPipe, stderrTty);
198 }
199
200 // when our stdin closes, also close child stdin
201 if (stdinTty) {
202 stdinTty->end.connect([stdinPipe] { stdinPipe->Close(); });
203 }
204
205 if (useUdp) {
206 auto udp = uv::Udp::Create(loop);
207 // tee stdout and stderr
208 CopyUdp(*stdoutPipe, udp, broadcastUdp);
209 CopyUdp(*stderrPipe, udp, broadcastUdp);
210 } else {
211 auto tcp = uv::Tcp::Create(loop);
212
213 // bind to listen address and port
214 tcp->Bind("", 1740);
215
216 // when we get a connection, accept it
217 tcp->connection.connect([srv = tcp.get(), stdoutPipe, stderrPipe] {
218 auto tcp = srv->Accept();
219 if (!tcp) {
220 return;
221 }
222
223 // close on error
224 tcp->error.connect([s = tcp.get()](wpi::uv::Error err) { s->Close(); });
225
226 // tee stdout and stderr
227 CopyTcp(*stdoutPipe, tcp);
228 CopyTcp(*stderrPipe, tcp);
229 });
230
231 // start listening for incoming connections
232 tcp->Listen();
233 }
234
235 // build process options
236 wpi::SmallVector<uv::Process::Option, 8> options;
237
238 // hook up pipes to child
239 options.emplace_back(
240 uv::Process::StdioCreatePipe(0, *stdinPipe, UV_READABLE_PIPE));
241#ifndef _WIN32
242 // create a PTY so the child does unbuffered output
243 int parentfd, childfd;
244 if (openpty(&parentfd, &childfd, nullptr, nullptr, nullptr) == 0) {
245 stdoutPipe->Open(parentfd);
246 options.emplace_back(uv::Process::StdioInherit(1, childfd));
247 } else {
248 options.emplace_back(
249 uv::Process::StdioCreatePipe(1, *stdoutPipe, UV_WRITABLE_PIPE));
250 }
251#else
252 options.emplace_back(
253 uv::Process::StdioCreatePipe(1, *stdoutPipe, UV_WRITABLE_PIPE));
254#endif
255 options.emplace_back(
256 uv::Process::StdioCreatePipe(2, *stderrPipe, UV_WRITABLE_PIPE));
257
258 // pass our args as the child args (argv[1] becomes child argv[0], etc)
259 for (int i = programArgc; i < argc; ++i) {
260 options.emplace_back(argv[i]);
261 }
262
263 auto proc = uv::Process::SpawnArray(loop, argv[programArgc], options);
264 if (!proc) {
265 std::fputs("could not start subprocess\n", stderr);
266 return EXIT_FAILURE;
267 }
268 proc->exited.connect([](int64_t status, int) { std::exit(status); });
269
270 // start reading
271 if (stdinTty) {
272 stdinTty->StartRead();
273 }
274 stdoutPipe->StartRead();
275 stderrPipe->StartRead();
276
277 // pass various signals to child
278 auto sigHandler = [proc](int signum) { proc->Kill(signum); };
279 for (int signum : {SIGINT, SIGHUP, SIGTERM}) {
280 auto sig = uv::Signal::Create(loop);
281 sig->Start(signum);
282 sig->signal.connect(sigHandler);
283 }
284
285 // run the loop!
286 loop->Run();
287}