@canboat/canboatjs 3.16.4-beta.1 → 3.17.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  #include <napi.h>
2
+ #include <uv.h>
2
3
  #include <sys/socket.h>
3
4
  #include <sys/ioctl.h>
4
5
  #include <net/if.h>
@@ -9,33 +10,25 @@
9
10
  #include <cstring>
10
11
  #include <cerrno>
11
12
 
12
- static Napi::Value OpenCanSocketImpl(const Napi::CallbackInfo& info, bool nonblock) {
13
- Napi::Env env = info.Env();
14
-
15
- if (info.Length() < 1 || !info[0].IsString()) {
16
- Napi::TypeError::New(env, "Interface name required")
17
- .ThrowAsJavaScriptException();
18
- return env.Undefined();
19
- }
20
-
21
- std::string ifname = info[0].As<Napi::String>().Utf8Value();
22
-
13
+ // Open a PF_CAN raw socket bound to the given interface in non-blocking mode.
14
+ // Both reads and writes use non-blocking I/O so the libuv threadpool is never
15
+ // occupied by a blocking syscall — this allows process.exit() to terminate
16
+ // cleanly even when no CAN traffic is flowing.
17
+ static int OpenAndBindCanSocket(const std::string& ifname, std::string& err) {
23
18
  int fd = socket(PF_CAN, SOCK_RAW, CAN_RAW);
24
19
  if (fd < 0) {
25
- Napi::Error::New(env, std::string("socket(PF_CAN): ") + strerror(errno))
26
- .ThrowAsJavaScriptException();
27
- return env.Undefined();
20
+ err = std::string("socket(PF_CAN): ") + strerror(errno);
21
+ return -1;
28
22
  }
29
23
 
30
24
  struct ifreq ifr;
31
25
  std::memset(&ifr, 0, sizeof(ifr));
32
26
  std::strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ - 1);
33
27
  if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
34
- std::string err = std::string("ioctl(SIOCGIFINDEX) for '") + ifname +
35
- "': " + strerror(errno);
28
+ err = std::string("ioctl(SIOCGIFINDEX) for '") + ifname +
29
+ "': " + strerror(errno);
36
30
  close(fd);
37
- Napi::Error::New(env, err).ThrowAsJavaScriptException();
38
- return env.Undefined();
31
+ return -1;
39
32
  }
40
33
 
41
34
  struct sockaddr_can addr;
@@ -43,94 +36,223 @@ static Napi::Value OpenCanSocketImpl(const Napi::CallbackInfo& info, bool nonblo
43
36
  addr.can_family = AF_CAN;
44
37
  addr.can_ifindex = ifr.ifr_ifindex;
45
38
  if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
46
- std::string err =
47
- std::string("bind() to '") + ifname + "': " + strerror(errno);
39
+ err = std::string("bind() to '") + ifname + "': " + strerror(errno);
48
40
  close(fd);
49
- Napi::Error::New(env, err).ThrowAsJavaScriptException();
50
- return env.Undefined();
41
+ return -1;
51
42
  }
52
43
 
53
- if (nonblock) {
54
- // Disable reception this socket is write-only. An empty filter array
55
- // tells the kernel not to deliver any incoming frames, avoiding wasted
56
- // copies into a receive buffer nobody reads.
57
- if (setsockopt(fd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0) < 0) {
58
- std::string err =
59
- std::string("setsockopt(CAN_RAW_FILTER) for '") + ifname + "': " + strerror(errno);
60
- close(fd);
61
- Napi::Error::New(env, err).ThrowAsJavaScriptException();
62
- return env.Undefined();
63
- }
64
-
65
- // NOTE: CAN_RAW_LOOPBACK is left at its default (enabled). On virtual CAN
66
- // (vcan) interfaces the kernel uses loopback to distribute frames between
67
- // sockets bound to the same interface — disabling it here would cause
68
- // write() to succeed silently while no other socket (including candump or
69
- // other processes on the bus) ever sees the frame. The empty CAN_RAW_FILTER
70
- // above already prevents unwanted frames from reaching this socket's
71
- // receive queue, so loopback has no effect on read behavior.
72
-
73
- if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
74
- std::string err =
75
- std::string("fcntl(O_NONBLOCK) for '") + ifname + "': " + strerror(errno);
76
- close(fd);
77
- Napi::Error::New(env, err).ThrowAsJavaScriptException();
78
- return env.Undefined();
79
- }
44
+ if (fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
45
+ err = std::string("fcntl(O_NONBLOCK) for '") + ifname +
46
+ "': " + strerror(errno);
47
+ close(fd);
48
+ return -1;
80
49
  }
81
50
 
82
- return Napi::Number::New(env, fd);
51
+ return fd;
83
52
  }
84
53
 
85
- Napi::Value OpenCanSocket(const Napi::CallbackInfo& info) {
86
- return OpenCanSocketImpl(info, false);
54
+ Napi::Value OpenCanReadSocket(const Napi::CallbackInfo& info) {
55
+ Napi::Env env = info.Env();
56
+ if (info.Length() < 1 || !info[0].IsString()) {
57
+ Napi::TypeError::New(env, "Interface name required")
58
+ .ThrowAsJavaScriptException();
59
+ return env.Undefined();
60
+ }
61
+ std::string ifname = info[0].As<Napi::String>().Utf8Value();
62
+ std::string err;
63
+ int fd = OpenAndBindCanSocket(ifname, err);
64
+ if (fd < 0) {
65
+ Napi::Error::New(env, err).ThrowAsJavaScriptException();
66
+ return env.Undefined();
67
+ }
68
+ return Napi::Number::New(env, fd);
87
69
  }
88
70
 
89
- Napi::Value OpenCanSocketNonBlock(const Napi::CallbackInfo& info) {
90
- return OpenCanSocketImpl(info, true);
71
+ Napi::Value OpenCanWriteSocket(const Napi::CallbackInfo& info) {
72
+ Napi::Env env = info.Env();
73
+ if (info.Length() < 1 || !info[0].IsString()) {
74
+ Napi::TypeError::New(env, "Interface name required")
75
+ .ThrowAsJavaScriptException();
76
+ return env.Undefined();
77
+ }
78
+ std::string ifname = info[0].As<Napi::String>().Utf8Value();
79
+ std::string err;
80
+ int fd = OpenAndBindCanSocket(ifname, err);
81
+ if (fd < 0) {
82
+ Napi::Error::New(env, err).ThrowAsJavaScriptException();
83
+ return env.Undefined();
84
+ }
85
+
86
+ // Disable reception on the write-only socket — empty filter array tells
87
+ // the kernel not to deliver incoming frames to this socket's receive
88
+ // buffer, avoiding wasted copies.
89
+ if (setsockopt(fd, SOL_CAN_RAW, CAN_RAW_FILTER, NULL, 0) < 0) {
90
+ std::string msg = std::string("setsockopt(CAN_RAW_FILTER) for '") +
91
+ ifname + "': " + strerror(errno);
92
+ close(fd);
93
+ Napi::Error::New(env, msg).ThrowAsJavaScriptException();
94
+ return env.Undefined();
95
+ }
96
+
97
+ // NOTE: CAN_RAW_LOOPBACK is left at its default (enabled). On virtual CAN
98
+ // (vcan) interfaces the kernel uses loopback to distribute frames between
99
+ // sockets bound to the same interface — disabling it would cause write()
100
+ // to succeed silently while no other socket (including candump or peers
101
+ // on the bus) ever sees the frame.
102
+
103
+ return Napi::Number::New(env, fd);
91
104
  }
92
105
 
93
106
  Napi::Value WriteCanFrame(const Napi::CallbackInfo& info) {
94
107
  Napi::Env env = info.Env();
95
-
96
108
  if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsBuffer()) {
97
109
  Napi::TypeError::New(env, "writeCanFrame(fd, buffer) expected")
98
110
  .ThrowAsJavaScriptException();
99
111
  return env.Undefined();
100
112
  }
101
-
102
113
  int fd = info[0].As<Napi::Number>().Int32Value();
103
114
  Napi::Buffer<uint8_t> buf = info[1].As<Napi::Buffer<uint8_t>>();
104
-
105
115
  ssize_t written = write(fd, buf.Data(), buf.Length());
106
-
107
116
  if (written < 0) {
108
117
  return Napi::Number::New(env, -errno);
109
118
  }
110
-
111
119
  return Napi::Number::New(env, static_cast<int>(written));
112
120
  }
113
121
 
114
- Napi::Value ShutdownCanSocket(const Napi::CallbackInfo& info) {
122
+ // Drain all currently-readable frames from the non-blocking fd into an
123
+ // array of Buffers, returned to JS. Returns an empty array if no frames
124
+ // are available (EAGAIN). This is called from the JS-side poll callback
125
+ // when the fd becomes readable.
126
+ Napi::Value ReadCanFrames(const Napi::CallbackInfo& info) {
115
127
  Napi::Env env = info.Env();
116
-
117
128
  if (info.Length() < 1 || !info[0].IsNumber()) {
118
- Napi::TypeError::New(env, "shutdownCanSocket(fd) expected")
129
+ Napi::TypeError::New(env, "readCanFrames(fd) expected")
119
130
  .ThrowAsJavaScriptException();
120
131
  return env.Undefined();
121
132
  }
122
-
123
133
  int fd = info[0].As<Napi::Number>().Int32Value();
124
- int rc = shutdown(fd, SHUT_RDWR);
125
134
 
126
- return Napi::Number::New(env, rc < 0 ? -errno : 0);
135
+ Napi::Array out = Napi::Array::New(env);
136
+ uint32_t idx = 0;
137
+ uint8_t buf[sizeof(struct can_frame)];
138
+
139
+ while (true) {
140
+ ssize_t n = read(fd, buf, sizeof(buf));
141
+ if (n < 0) {
142
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
143
+ break;
144
+ }
145
+ Napi::Error::New(env, std::string("read: ") + strerror(errno))
146
+ .ThrowAsJavaScriptException();
147
+ return env.Undefined();
148
+ }
149
+ if (n == 0) {
150
+ break;
151
+ }
152
+ out.Set(idx++, Napi::Buffer<uint8_t>::Copy(env, buf, n));
153
+ }
154
+
155
+ return out;
127
156
  }
128
157
 
158
+ // CanPoller: wraps a uv_poll_t on a CAN read fd, calls a JS callback when
159
+ // frames are readable. Closing the poller is synchronous and prevents any
160
+ // further callbacks; the underlying fd is the caller's responsibility.
161
+ class CanPoller : public Napi::ObjectWrap<CanPoller> {
162
+ public:
163
+ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
164
+ Napi::Function func = DefineClass(
165
+ env, "CanPoller",
166
+ {InstanceMethod("close", &CanPoller::Close)});
167
+ exports.Set("CanPoller", func);
168
+ return exports;
169
+ }
170
+
171
+ CanPoller(const Napi::CallbackInfo& info) : Napi::ObjectWrap<CanPoller>(info) {
172
+ Napi::Env env = info.Env();
173
+ if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsFunction()) {
174
+ Napi::TypeError::New(env, "new CanPoller(fd, callback)")
175
+ .ThrowAsJavaScriptException();
176
+ return;
177
+ }
178
+ fd_ = info[0].As<Napi::Number>().Int32Value();
179
+ callback_.Reset(info[1].As<Napi::Function>(), 1);
180
+ closed_ = false;
181
+
182
+ uv_loop_t* loop = nullptr;
183
+ napi_get_uv_event_loop(env, &loop);
184
+ poll_ = new uv_poll_t;
185
+ poll_->data = this;
186
+ int rc = uv_poll_init(loop, poll_, fd_);
187
+ if (rc < 0) {
188
+ delete poll_;
189
+ poll_ = nullptr;
190
+ Napi::Error::New(env,
191
+ std::string("uv_poll_init: ") + uv_strerror(rc))
192
+ .ThrowAsJavaScriptException();
193
+ return;
194
+ }
195
+ rc = uv_poll_start(poll_, UV_READABLE, &CanPoller::OnPoll);
196
+ if (rc < 0) {
197
+ uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
198
+ poll_ = nullptr;
199
+ Napi::Error::New(env,
200
+ std::string("uv_poll_start: ") + uv_strerror(rc))
201
+ .ThrowAsJavaScriptException();
202
+ return;
203
+ }
204
+ }
205
+
206
+ ~CanPoller() {
207
+ // poll_ should already be null after Close(); if not, the object was
208
+ // GC'd without explicit close — schedule a libuv close to clean up.
209
+ if (poll_ != nullptr && !closed_) {
210
+ closed_ = true;
211
+ uv_poll_stop(poll_);
212
+ uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
213
+ poll_ = nullptr;
214
+ }
215
+ }
216
+
217
+ private:
218
+ static void OnPoll(uv_poll_t* handle, int status, int events) {
219
+ CanPoller* self = static_cast<CanPoller*>(handle->data);
220
+ if (self->closed_ || status < 0 || !(events & UV_READABLE)) {
221
+ return;
222
+ }
223
+ Napi::Env env = self->callback_.Env();
224
+ Napi::HandleScope scope(env);
225
+ self->callback_.Call({});
226
+ }
227
+
228
+ static void OnClose(uv_handle_t* handle) {
229
+ delete reinterpret_cast<uv_poll_t*>(handle);
230
+ }
231
+
232
+ Napi::Value Close(const Napi::CallbackInfo& info) {
233
+ if (!closed_ && poll_ != nullptr) {
234
+ closed_ = true;
235
+ uv_poll_stop(poll_);
236
+ uv_close(reinterpret_cast<uv_handle_t*>(poll_), &CanPoller::OnClose);
237
+ poll_ = nullptr;
238
+ callback_.Reset();
239
+ }
240
+ return info.Env().Undefined();
241
+ }
242
+
243
+ int fd_ = -1;
244
+ uv_poll_t* poll_ = nullptr;
245
+ bool closed_ = true;
246
+ Napi::FunctionReference callback_;
247
+ };
248
+
129
249
  Napi::Object Init(Napi::Env env, Napi::Object exports) {
130
- exports.Set("openCanSocket", Napi::Function::New(env, OpenCanSocket));
131
- exports.Set("openCanSocketNonBlock", Napi::Function::New(env, OpenCanSocketNonBlock));
250
+ exports.Set("openCanReadSocket", Napi::Function::New(env, OpenCanReadSocket));
251
+ exports.Set("openCanWriteSocket",
252
+ Napi::Function::New(env, OpenCanWriteSocket));
132
253
  exports.Set("writeCanFrame", Napi::Function::New(env, WriteCanFrame));
133
- exports.Set("shutdownCanSocket", Napi::Function::New(env, ShutdownCanSocket));
254
+ exports.Set("readCanFrames", Napi::Function::New(env, ReadCanFrames));
255
+ CanPoller::Init(env, exports);
134
256
  return exports;
135
257
  }
136
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canboat/canboatjs",
3
- "version": "3.16.4-beta.1",
3
+ "version": "3.17.0-beta.1",
4
4
  "description": "Native javascript version of canboat",
5
5
  "main": "dist/index.js",
6
6
  "browser": "dist/browser.js",