1 /*
2 * vim: filetype=c:tabstop=4:ai:expandtab
3 * SPDX-License-Identifier: ICU
4 * scspell-id: 35c9cf5c-ebbf-11ed-8d34-80ee73e9b8e7
5 *
6 * ---------------------------------------------------------------------------
7 *
8 * Copyright (c) 2007-2013 Michael Mondy
9 * Copyright (c) 2015-2018 Charles Anthony
10 * Copyright (c) 2023 Björn Victor
11 * Copyright (c) 2026 Eric Swenson
12 * Copyright (c) 2026 Jeffrey H. Johnson
13 * Copyright (c) 2021-2026 The DPS8M Development Team
14 *
15 * This software is made available under the terms of the ICU License.
16 * See the LICENSE.md file at the top-level directory of this distribution.
17 *
18 * ---------------------------------------------------------------------------
19 */
20
21 // This is a thin shim that passes MGP packets between the IOM (Multics)
22 // and an external NCP process (multics_ncp) via a Unix domain socket.
23 //
24 // The NCP process handles all Chaosnet protocol logic: connection management,
25 // flow control, retransmission, and endpoint ID mapping.
26 //
27 // Communication protocol with the NCP:
28 // - Single bidirectional Unix domain socket (default: /tmp/mgp_ncp)
29 // - Length-prefixed framing: [2-byte BE length][MGP packet in 8-bit format]
30 // - The shim converts between Multics 36-bit/9-bit IOM words and 8-bit bytes
31 //
32 // Write (Multics -> NCP): Read 36-bit words from IOM, convert to 8-bit,
33 // send length-prefixed to NCP, return IOM_CMD_DISCONNECT (terminate).
34 //
35 // Read (NCP -> Multics): Set want_to_read flag, return IOM_CMD_PENDING.
36 // In mgp_process_event(), poll NCP socket non-blocking; if data available,
37 // read length-prefixed packet, convert 8-bit to 36-bit, write to IOM,
38 // send marker interrupt.
39
40 #include <stdio.h>
41 #include <stdarg.h>
42 #include <ctype.h>
43 #include <unistd.h>
44 #include <stdint.h>
45 #include <errno.h>
46 #include <fcntl.h>
47
48 #include <sys/types.h>
49 #include <sys/un.h>
50 #include <sys/select.h>
51 #include <time.h>
52 #include <sys/time.h>
53 #include <sys/socket.h>
54
55 #include "dps8.h"
56 #include "dps8_sir.h"
57 #include "dps8_iom.h"
58 #include "dps8_mgp.h"
59 #include "dps8_sys.h"
60 #include "dps8_cable.h"
61 #include "dps8_cpu.h"
62 #include "dps8_faults.h"
63 #include "dps8_scu.h"
64 #include "dps8_utils.h"
65
66 #if defined(NO_LOCALE)
67 # define xstrerror_l strerror
68 #endif
69
70 #if defined(FREE)
71 # undef FREE
72 #endif /* if defined(FREE) */
73 #define FREE(p) do \
74 { \
75 free((p)); \
76 (p) = NULL; \
77 } while(0)
78
79 #if defined(WITH_MGP_DEV)
80
81 # define DBG_CTR 1
82
83 // Path to NCP process Unix socket (configurable via SET MGP SOCKET=<path>)
84 # define NCP_SOCKET_PATH_DEFAULT "/tmp/mgp_ncp"
85 # define NCP_SOCKET_PATH_MAX 108
86
87 static char ncp_socket_path[NCP_SOCKET_PATH_MAX] = NCP_SOCKET_PATH_DEFAULT;
88
89 // Number of words in an MGP packet header (4 words x 36 bits = 16 x 9-bit bytes)
90 # define MGP_PACKET_HEADER_SIZE 4
91
92 // Maximum data bytes in an MGP packet
93 # define MGP_MAX_DATA 488
94
95 // Frame header size for NCP communication (2-byte big-endian length prefix)
96 # define NCP_FRAME_HEADER_SIZE 2
97
98 // Maximum 8-bit packet size (16 header + 488 data)
99 # define MAX_PKT_BYTES (16 + MGP_MAX_DATA)
100
101 /* Maximum plausible DDCW_TALLY for any MGP channel buffer, in 36-bit words.
102 * The read channel uses data_size=129 and the write channel uses data_size=128
103 * (set by initialize_workspace in mgp_read_dcm_.pl1 / mgp_write_dcm_.pl1).
104 * MAX_PKT_BYTES/4 = 126 words for the payload plus ~4 header words ~= 130 max.
105 * We use 256 as a generous upper bound so this check remains valid even if the
106 * Multics buffer size is changed, as long as it stays within one IOM page
107 * (the ioi_ workspace is 2000 octal = 1024 words = one page).
108 * TALLY=0 (IOM convention for 4096) and TALLY > MGP_MAX_TALLY both indicate
109 * DCW corruption; see the validation checks in mgp_cmd cases 001 and 011. */
110 # define MGP_MAX_TALLY 256
111
112 /* Backoff after a PTW failure: do not reconnect for this many seconds.
113 * When Multics's memory manager pages out the MGP daemon's IOM DMA buffer
114 * pages after overnight idle, the PTW check fails. Without a backoff the
115 * terminate-interrupt -> mgp_cmd(001) -> ncp_connect cycle repeats every
116 * ~60 ms (too fast for the memory manager to page the data back in).
117 * The backoff check lives in ncp_connect() so it catches ALL reconnect
118 * paths, including ncp_send_packet() (Multics WRITE cmd) which previously
119 * bypassed the per-event check and caused ~91 ms reconnect cycles. */
120 # define PTW_BACKOFF_SECS 15
121
122 /* Timeout before declaring the channel "masked+stuck" and forcing recovery.
123 * See masked_since in mgp_dev_state and the masked-channel check in
124 * mgp_process_event for details. */
125 # define MASKED_STUCK_TIMEOUT_SECS 30
126
127 /* Timeout before releasing an IOM channel that has been waiting for the NCP
128 * to connect. Keeps the channel from staying in IOM_CMD_PENDING indefinitely
129 * when multics_ncp is not running. */
130 # define NCP_ABSENT_TIMEOUT_SECS 30
131
132 /* Timeout before declaring "no forward progress" and forcing a hard socket
133 * reset, independent of the masked/want_to_read state.
134 *
135 * MASKED_STUCK_TIMEOUT_SECS (above) only accumulates while the channel is
136 * continuously seen masked across successive mgp_process_event() calls.
137 * ioi_masked$timer's mask_channel (Multics's own channel-timeout handler,
138 * ioi_masked.pl1) both masks the channel AND immediately issues a dummy
139 * "unmask connect" (reset-status IDCW) in the same call when the device is
140 * multiplexed. That reconnect can flip want_to_read back to 1 for one
141 * mgp_process_event() tick, which resets masked_since to 0 (see the
142 * "channel is not masked" branch below) before MASKED_STUCK_TIMEOUT_SECS is
143 * ever reached -> so a channel that is genuinely wedged in a repeating
144 * mask/unmask timeout loop (observed at ~4s intervals against Multics's
145 * short mgpr device read timeout) can defeat that safety net indefinitely.
146 *
147 * This second, independent watchdog tracks wall-clock time since the last
148 * *successfully delivered* packet (last_progress_time), updated only when a
149 * packet is actually written into Multics's workspace. It is immune to the
150 * masked/unmask flicker because it does not depend on observing !masked in
151 * between: it fires purely on elapsed time without any real delivery. */
152 # define NO_PROGRESS_TIMEOUT_SECS 60
153
154 /* Minimum valid DDCW_ADDR for the MGP read channel workspace.
155 *
156 * The read channel workspace layout (mgp_read_dcm_.pl1, buffer_size=6,
157 * data_size=129):
158 * offsets 0-11: DCW list (6 x IDCW + 6 x DDCW)
159 * offset 12: TDCW (circular wrap; DATA_ADDRESS=0)
160 * offset 13: reset_idcw
161 * offsets 14-21: status_queue (4 x istat = 8 words)
162 * offset 22+ : buffer(0..5) data areas (129 words each)
163 *
164 * Any DDCW_ADDR < 22 is invalid (it points into the DCW list or control
165 * structures, not into a data buffer). DDCW_ADDR=0 in particular is set
166 * by iom_list_service when it processes the TDCW at offset 12 via
167 * unpack_DCW, which stores the TDCW's DATA_ADDRESS field (=0, all zero
168 * bits) into p->DDCW_ADDR.
169 */
170 # define MGP_FIRST_BUFFER_OFFSET 22
171
172 static void mgp_init_dev_state(void);
173
174 # if defined(TESTING)
175 static void dumppkt(char *hdr, word36 *buf, uint words);
176 # endif
177
178 struct mgp_dev_state
179 {
180 int ncp_socket; /* connected socket to NCP process */
181 u_char want_to_read; /* flag: Multics has a pending read */
182 uint read_unit_idx; /* saved IOM unit index for pending read */
183 uint read_unit_chan; /* saved IOM channel for pending read */
184 u_char delivery_succeeded; /* set by mgp_cmd(READ) when IOM accepts pkt */
185 time_t want_to_read_since; /* wall-clock time when want_to_read was set */
186 time_t ptw_failed_at; /* wall-clock time of last PTW check failure */
187 time_t masked_since; /* wall-clock time channel first seen masked while connected */
188 time_t last_progress_time; /* wall-clock time of last successful packet delivery to Multics;
189 0 means "not yet established" (lazily initialized) */
190 } mgp_dev_state;
191
192 static struct mgp_state
193 {
194 char device_name[MAX_DEV_NAME_LEN];
195 } mgp_state[N_MGP_UNITS_MAX];
196
197 # define N_MGP_UNITS 2 // default
198
199 # define UNIT_FLAGS \
200 ( UNIT_FIX | UNIT_ATTABLE | UNIT_ROABLE | UNIT_DISABLE | UNIT_IDLE )
201
202 UNIT mgp_unit[N_MGP_UNITS_MAX] = {
203 {
204 UDATA(NULL, UNIT_FLAGS, 0),
205 0, 0, 0, 0, 0,
206 NULL, NULL, NULL, NULL
207 }
208 };
209
210 # define MGP_UNIT_IDX(uptr) (( uptr ) - mgp_unit )
211
212 static DEBTAB mgp_dt[] = {
213 { "NOTIFY", DBG_NOTIFY, NULL },
214 { "INFO", DBG_INFO, NULL },
215 { "ERR", DBG_ERR, NULL },
216 { "WARN", DBG_WARN, NULL },
217 { "DEBUG", DBG_DEBUG, NULL },
218 { "ALL", DBG_ALL, NULL }, // Don't move as it messes up DBG message
219 { NULL, 0, NULL }
220 };
221
222 static t_stat
223 mgp_show_nunits(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
/* ![[previous]](../icons/n_left.png)
![[next]](../icons/right.png)
![[first]](../icons/n_first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
224 UNUSED const void *desc)
225 {
226 sim_printf("Number of MGP units in system is %d\r\n", mgp_dev.numunits);
227
228 return SCPE_OK;
229 }
230
231 static t_stat
232 mgp_set_nunits(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
233 UNUSED void *desc)
234 {
235 if (!cptr)
236 {
237 return SCPE_ARG;
238 }
239
240 int n = atoi(cptr);
241 if (n < 1 || n > N_MGP_UNITS_MAX)
242 {
243 return SCPE_ARG;
244 }
245
246 mgp_dev.numunits = (uint32)n;
247
248 return SCPE_OK;
249 }
250
251 static t_stat
252 mgp_show_device_name(UNUSED FILE *st, UNIT *uptr, UNUSED int val,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
253 UNUSED const void *desc)
254 {
255 int n = (int)MGP_UNIT_IDX(uptr);
256
257 if (n < 0 || n >= N_MGP_UNITS_MAX)
258 {
259 return SCPE_ARG;
260 }
261
262 if (mgp_state[n].device_name[1] != 0)
263 {
264 sim_printf("name : %s", mgp_state[n].device_name);
265 }
266 else
267 {
268 sim_printf("name : MGP%d", n);
269 }
270
271 return SCPE_OK;
272 }
273
274 static t_stat
275 mgp_set_device_name(UNIT *uptr, UNUSED int32 value, const char *cptr,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
276 UNUSED void *desc)
277 {
278 int n = (int)MGP_UNIT_IDX(uptr);
279
280 if (n < 0 || n >= N_MGP_UNITS_MAX)
281 {
282 return SCPE_ARG;
283 }
284
285 if (cptr)
286 {
287 strncpy(mgp_state[n].device_name, cptr, MAX_DEV_NAME_LEN - 1);
288 mgp_state[n].device_name[MAX_DEV_NAME_LEN - 1] = 0;
289 }
290 else
291 {
292 mgp_state[n].device_name[0] = 0;
293 }
294
295 return SCPE_OK;
296 }
297
298 static t_stat
299 mgp_show_socket_path(UNUSED FILE *st, UNUSED UNIT *uptr, UNUSED int val,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
300 UNUSED const void *desc)
301 {
302 sim_printf("NCP socket path: %s\r\n", ncp_socket_path);
303 return SCPE_OK;
304 }
305
306 static t_stat
307 mgp_set_socket_path(UNUSED UNIT *uptr, UNUSED int32 value, const char *cptr,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
308 UNUSED void *desc)
309 {
310 if (!cptr || strlen(cptr) == 0)
311 return SCPE_ARG;
312 if (strlen(cptr) >= NCP_SOCKET_PATH_MAX)
313 {
314 sim_printf("NCP socket path too long (max %d chars)\r\n",
315 NCP_SOCKET_PATH_MAX - 1);
316 return SCPE_ARG;
317 }
318 strncpy(ncp_socket_path, cptr, NCP_SOCKET_PATH_MAX - 1);
319 ncp_socket_path[NCP_SOCKET_PATH_MAX - 1] = '\0';
320 sim_printf("MGP NCP socket path set to: %s\r\n", ncp_socket_path);
321 return SCPE_OK;
322 }
323
324 # define UNIT_WATCH UNIT_V_UF
325
326 static MTAB mgp_mod[] = {
327 # if !defined(SPEED)
328 { UNIT_WATCH, 1, "WATCH", "WATCH", 0, 0, NULL, NULL },
329 { UNIT_WATCH, 0, "NOWATCH", "NOWATCH", 0, 0, NULL, NULL },
330 # endif /* if !defined(SPEED) */
331 {
332 MTAB_XTD | MTAB_VDV | MTAB_NMO | MTAB_VALR, /* Mask */
333 0, /* Match */
334 "NUNITS", /* Print string */
335 "NUNITS", /* Match string */
336 mgp_set_nunits, /* Validation routine */
337 mgp_show_nunits, /* Display routine */
338 "Number of MGP units in the system", /* Value descriptor */
339 NULL /* Help */
340 },
341 {
342 MTAB_XTD | MTAB_VUN | MTAB_VALR | MTAB_NC, /* Mask */
343 0, /* Match */
344 "NAME", /* Print string */
345 "NAME", /* Match string */
346 mgp_set_device_name, /* Validation routine */
347 mgp_show_device_name, /* Display routine */
348 "Set the device name", /* Value descriptor */
349 NULL /* Help */
350 },
351 {
352 MTAB_XTD | MTAB_VDV | MTAB_VALR | MTAB_NC, /* Mask */
353 0, /* Match */
354 "SOCKET", /* Print string */
355 "SOCKET", /* Match string */
356 mgp_set_socket_path, /* Validation routine */
357 mgp_show_socket_path, /* Display routine */
358 "Unix socket path for NCP connection", /* Value descriptor */
359 NULL /* Help */
360 },
361 MTAB_eol
362 };
363
364 static t_stat
365 mgp_reset(UNUSED DEVICE *dptr)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
366 {
367 return SCPE_OK;
368 }
369
370 static t_stat
371 mgpAttach(UNIT *uptr, const char *cptr)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
372 {
373 if (!cptr)
374 {
375 return SCPE_ARG;
376 }
377
378 // If we're already attached, then detach ...
379 if (( uptr->flags & UNIT_ATT ) != 0)
380 {
381 detach_unit(uptr);
382 }
383
384 uptr->flags |= UNIT_ATT;
385
386 return SCPE_OK;
387 }
388
389 // Detach (connect) ...
390 static t_stat
391 mgpDetach(UNIT *uptr)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
392 {
393 if (( uptr->flags & UNIT_ATT ) == 0)
394 {
395 return SCPE_OK;
396 }
397
398 uptr->flags &= ~(unsigned int)UNIT_ATT;
399
400 return SCPE_OK;
401 }
402
403 DEVICE mgp_dev = {
404 "MGP", /* Name */
405 mgp_unit, /* Units */
406 NULL, /* Registers */
407 mgp_mod, /* Modifiers */
408 N_MGP_UNITS, /* #units */
409 10, /* Address radix */
410 24, /* Address width */
411 1, /* Address increment */
412 8, /* Data radix */
413 36, /* Data width */
414 NULL, /* Examine */
415 NULL, /* Deposit */
416 mgp_reset, /* Reset */
417 NULL, /* Boot */
418 mgpAttach, /* Attach */
419 mgpDetach, /* Detach */
420 NULL, /* Context */
421 DEV_DEBUG, /* Flags */
422 0, /* Debug control flags */
423 mgp_dt, /* Debug flag names */
424 NULL, /* Memory size change */
425 NULL, /* Logical name */
426 NULL, /* Help */
427 NULL, /* Attach help */
428 NULL, /* Attach context */
429 NULL, /* Description */
430 NULL /* End */
431 };
432
433 /*
434 * mgp_init()
435 */
436
437 // Once-only initialization
438
439 void
440 mgp_init(void)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
441 {
442 (void)memset(mgp_state, 0, sizeof ( mgp_state ));
443 mgp_init_dev_state();
444 }
445
446 static void
447 mgp_init_dev_state(void)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
448 {
449 (void)memset(&mgp_dev_state, 0, sizeof ( mgp_dev_state ));
450 mgp_dev_state.ncp_socket = -1;
451 }
452
453 /*
454 * Connect to the NCP process via Unix domain socket.
455 * Returns 0 on success, -1 on failure.
456 */
457 static int
458 ncp_connect(void)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
459 {
460 if (mgp_dev_state.ncp_socket >= 0)
461 {
462 return 0; /* already connected */
463 }
464
465 /* Refuse to reconnect while in PTW-failure backoff.
466 * This check must live here (not only in mgp_process_event) because
467 * ncp_send_packet() and ncp_recv_packet() both call us directly and
468 * would otherwise bypass the backoff, causing ~91 ms reconnect cycles
469 * even while the 15-second timer is counting down. */
470 if (mgp_dev_state.ptw_failed_at > 0)
471 {
472 if (time(NULL) - mgp_dev_state.ptw_failed_at < PTW_BACKOFF_SECS)
473 return -1; /* still in backoff - give memory manager time to work */
474 mgp_dev_state.ptw_failed_at = 0; /* backoff expired */
475 }
476
477 int sock = socket(AF_UNIX, SOCK_STREAM, 0);
478 if (sock < 0)
479 {
480 sim_printf("%s: socket(AF_UNIX) error: %s (%d)\r\n",
481 __func__, xstrerror_l(errno), errno);
482 return -1;
483 }
484
485 struct sockaddr_un server;
486 server.sun_family = AF_UNIX;
487 snprintf(server.sun_path, sizeof(server.sun_path), "%.*s",
488 (int)(sizeof(server.sun_path) - 1), ncp_socket_path);
489
490 socklen_t slen = (socklen_t)(
491 offsetof(struct sockaddr_un, sun_path) + strlen(server.sun_path) + 1);
492
493 if (connect(sock, (struct sockaddr *)&server, slen) < 0)
494 {
495 /* Rate-limit error messages to avoid flooding the operator console */
496 static time_t last_err_time = 0;
497 time_t now = time(NULL);
498 if (now - last_err_time >= 30)
499 {
500 size_t path_len = strlen(ncp_socket_path);
501 if (path_len > sizeof(server.sun_path) - 1) //-V547
502 {
503 sim_printf("%s: unix socket address was %zu characters; use a PATH at most %zu characters long.\r\n",
504 __func__, path_len, sizeof(server.sun_path) - 1);
505 }
506 sim_printf("%s: connect(%s) error: %s (%d)\r\n",
507 __func__, server.sun_path, xstrerror_l(errno), errno);
508 last_err_time = now;
509 }
510 close(sock);
511 return -1;
512 }
513
514 /* Set non-blocking for polling */
515 int flags = fcntl(sock, F_GETFL, 0);
516 if (flags >= 0)
517 {
518 fcntl(sock, F_SETFL, flags | O_NONBLOCK);
519 }
520
521 mgp_dev_state.ncp_socket = sock;
522 sim_printf("%s: connected to NCP at %s (fd %d)\r\n",
523 __func__, ncp_socket_path, sock);
524
525 return 0;
526 }
527
528 /*
529 * Send a length-prefixed 8-bit MGP packet to the NCP process.
530 * The packet has already been converted from 36-bit words to 8-bit bytes.
531 * Returns 0 on success, -1 on failure.
532 */
533 static int
534 ncp_send_packet(u_char *pkt8, int pktlen)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
535 {
536 if (mgp_dev_state.ncp_socket < 0)
537 {
538 if (ncp_connect() < 0)
539 {
540 return -1;
541 }
542 }
543
544 /* Temporarily set blocking for the send */
545 int flags = fcntl(mgp_dev_state.ncp_socket, F_GETFL, 0);
546 if (flags >= 0)
547 {
548 fcntl(mgp_dev_state.ncp_socket, F_SETFL, flags & ~O_NONBLOCK);
549 }
550
551 u_char hdr[NCP_FRAME_HEADER_SIZE];
552 hdr[0] = (pktlen >> 8) & 0xFF;
553 hdr[1] = pktlen & 0xFF;
554
555 int rc = 0;
556 ssize_t w = write(mgp_dev_state.ncp_socket, hdr, NCP_FRAME_HEADER_SIZE);
557 if (w != NCP_FRAME_HEADER_SIZE)
558 {
559 sim_printf("%s: write header failed: %s (%d)\r\n",
560 __func__, xstrerror_l(errno), errno);
561 close(mgp_dev_state.ncp_socket);
562 mgp_dev_state.ncp_socket = -1;
563 rc = -1;
564 }
565 else
566 {
567 w = write(mgp_dev_state.ncp_socket, pkt8, pktlen);
568 if (w != pktlen)
569 {
570 sim_printf("%s: write body failed: wrote %zd of %d: %s (%d)\r\n",
571 __func__, w, pktlen, xstrerror_l(errno), errno);
572 close(mgp_dev_state.ncp_socket);
573 mgp_dev_state.ncp_socket = -1;
574 rc = -1;
575 }
576 }
577
578 /* Restore non-blocking */
579 if (mgp_dev_state.ncp_socket >= 0)
580 {
581 flags = fcntl(mgp_dev_state.ncp_socket, F_GETFL, 0);
582 if (flags >= 0)
583 {
584 fcntl(mgp_dev_state.ncp_socket, F_SETFL, flags | O_NONBLOCK);
585 }
586 }
587
588 return rc;
589 }
590
591 /*
592 * Try to receive a length-prefixed 8-bit MGP packet from the NCP process
593 * (non-blocking). Returns the number of bytes received (>0), 0 if nothing
594 * available, or -1 on error.
595 */
596
597 /*
598 * read() on a stream (including a local Unix-domain SOCK_STREAM) socket is
599 * not guaranteed by POSIX to return all requested bytes in a single call,
600 * even when the socket is in blocking mode -- it may return as soon as at
601 * least one byte is available. Loop until the full count is read or the
602 * connection errors/closes.
603 */
604 static ssize_t
605 read_full(int fd, u_char *buf, size_t count)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
606 {
607 size_t got = 0;
608 while (got < count)
609 {
610 ssize_t r = read(fd, buf + got, count - got);
611 if (r == 0)
612 {
613 /* Peer closed */
614 return (ssize_t)got;
615 }
616 if (r < 0)
617 {
618 if (errno == EINTR)
619 {
620 continue;
621 }
622 return -1;
623 }
624 got += (size_t)r;
625 }
626 return (ssize_t)got;
627 }
628
629 /*
630 * XXX(ejs)
631 * TEMPDIAG3 (temporary -- "Fast-Typing Character Loss" investigation,
632 * fine-grained ncp_recv_packet() framing check). Writes directly to its own
633 * file via plain fopen/fprintf, completely independent of SIMH's console,
634 * sim_warn/sim_debug, and SET DEBUG -- so it never touches the interactive
635 * BCE/Multics console.
636 *
637 * Disabled by default (no MGP_TEMPDIAG3 define): the function is a no-op
638 * that never opens or writes /tmp/mgp_recv_trace.<pid>.log. This was left
639 * unconditionally active during development, which is not acceptable for a
640 * shipped simulator (opens/writes a file on every MGP packet with no way to
641 * turn it off). Build with -DMGP_TEMPDIAG3 to re-enable if this
642 * investigation needs to be revisited; call sites are left in place either
643 * way. Remove entirely once the investigation is closed for good.
644 */
645 # ifdef MGP_TEMPDIAG3
646 static FILE *tempdiag3_fp = NULL;
647 static unsigned long tempdiag3_seq = 0;
648 # endif
649
650 static void
651 tempdiag3_log(const char *fmt, ...)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
652 {
653 # ifndef MGP_TEMPDIAG3
654 (void)fmt;
655 return;
656 # else
657 if (tempdiag3_fp == NULL)
658 {
659 /* PID-qualified filename: two dps8 instances (one per Multics system)
660 * run the same binary on the same host, and would otherwise both
661 * append to one shared file with no way to tell which system a
662 * given line came from. */
663 char path[64];
664 snprintf(path, sizeof(path), "/tmp/mgp_recv_trace.%d.log", (int)getpid());
665 tempdiag3_fp = fopen(path, "a");
666 if (tempdiag3_fp == NULL)
667 {
668 return;
669 }
670 setvbuf(tempdiag3_fp, NULL, _IOLBF, 0); /* line-buffered */
671 fprintf(tempdiag3_fp, "=== opened by pid %d ===\n", (int)getpid());
672 }
673
674 struct timespec ts;
675 clock_gettime(CLOCK_REALTIME, &ts);
676 struct tm tmv;
677 localtime_r(&ts.tv_sec, &tmv);
678 char tbuf[32];
679 strftime(tbuf, sizeof(tbuf), "%H:%M:%S", &tmv);
680
681 tempdiag3_seq++;
682 fprintf(tempdiag3_fp, "%s.%06ld #%lu ", tbuf, ts.tv_nsec / 1000, tempdiag3_seq);
683
684 va_list ap;
685 va_start(ap, fmt);
686 vfprintf(tempdiag3_fp, fmt, ap);
687 va_end(ap);
688
689 fprintf(tempdiag3_fp, "\n");
690 # endif
691 }
692
693 static int
694 ncp_recv_packet(u_char *pkt8, int maxlen)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
695 {
696 if (mgp_dev_state.ncp_socket < 0)
697 {
698 if (ncp_connect() < 0)
699 {
700 return 0; /* not connected, nothing to read */
701 }
702 }
703
704 /* Non-blocking read of the 2-byte length header */
705 u_char hdr[NCP_FRAME_HEADER_SIZE];
706 ssize_t r = recv(mgp_dev_state.ncp_socket, hdr, NCP_FRAME_HEADER_SIZE, MSG_PEEK);
707
708 if (r == 0)
709 {
710 /* Connection closed */
711 tempdiag3_log("peek: connection closed");
712 sim_printf("%s: NCP connection closed\r\n", __func__);
713 close(mgp_dev_state.ncp_socket);
714 mgp_dev_state.ncp_socket = -1;
715 return -1;
716 }
717
718 if (r < 0)
719 {
720 if (errno == EAGAIN || errno == EWOULDBLOCK)
721 {
722 return 0; /* nothing available */
723 }
724
725 tempdiag3_log("peek: recv error errno=%d", errno);
726 sim_printf("%s: recv error: %s (%d)\r\n",
727 __func__, xstrerror_l(errno), errno);
728 close(mgp_dev_state.ncp_socket);
729 mgp_dev_state.ncp_socket = -1;
730 return -1;
731 }
732
733 if (r < NCP_FRAME_HEADER_SIZE)
734 {
735 tempdiag3_log("peek: incomplete header, only %zd byte(s) available", r);
736 return 0; /* incomplete header, wait for more */
737 }
738
739 tempdiag3_log("peek: header available hdr=%02x,%02x", hdr[0], hdr[1]);
740
741 /* We have the full header, switch to blocking to read the rest */
742 int flags = fcntl(mgp_dev_state.ncp_socket, F_GETFL, 0);
743 if (flags >= 0)
744 {
745 fcntl(mgp_dev_state.ncp_socket, F_SETFL, flags & ~O_NONBLOCK);
746 }
747
748 /* Actually consume the header */
749 r = read_full(mgp_dev_state.ncp_socket, hdr, NCP_FRAME_HEADER_SIZE);
750 if (r != NCP_FRAME_HEADER_SIZE)
751 {
752 tempdiag3_log("read header failed: got %zd of %d hdr=%02x,%02x",
753 r, NCP_FRAME_HEADER_SIZE, hdr[0], hdr[1]);
754 sim_printf("%s: read header failed: got %zd of %d\r\n",
755 __func__, r, NCP_FRAME_HEADER_SIZE);
756 close(mgp_dev_state.ncp_socket);
757 mgp_dev_state.ncp_socket = -1;
758 return -1;
759 }
760
761 int pktlen = (hdr[0] << 8) | hdr[1];
762 tempdiag3_log("header consumed: hdr=%02x,%02x pktlen=%d", hdr[0], hdr[1], pktlen);
763
764 if (pktlen <= 0 || pktlen > maxlen)
765 {
766 /*
767 * The 2-byte header has already been consumed at this point (via
768 * read_full above), but we don't know how many bytes to skip to
769 * resynchronize with the next real packet -- the stream is now
770 * desynchronized. The old code returned -1 here WITHOUT closing the
771 * socket, silently leaving every subsequent ncp_recv_packet() call to
772 * misparse whatever bytes follow as a bogus header, repeating this
773 * same failure indefinitely with no recovery. Close and force a
774 * clean reconnect/resync instead, matching every other error path
775 * in this function.
776 */
777 tempdiag3_log("bad packet length %d -- closing socket to force resync", pktlen);
778 sim_printf("%s: bad packet length %d -- closing socket to force resync\r\n",
779 __func__, pktlen);
780 close(mgp_dev_state.ncp_socket);
781 mgp_dev_state.ncp_socket = -1;
782 return -1;
783 }
784
785 /* Read the packet body */
786 r = read_full(mgp_dev_state.ncp_socket, pkt8, (size_t)pktlen);
787
788 /* Restore non-blocking */
789 flags = fcntl(mgp_dev_state.ncp_socket, F_GETFL, 0);
790 if (flags >= 0)
791 {
792 fcntl(mgp_dev_state.ncp_socket, F_SETFL, flags | O_NONBLOCK);
793 }
794
795 if (r != pktlen)
796 {
797 /*
798 * A short read here (peer closed or errored mid-body) leaves the
799 * stream desynchronized -- the remaining bytes of this packet (if
800 * any were queued behind it) are no longer aligned with the next
801 * 2-byte length header. Close and force a reconnect/resync rather
802 * than silently continuing to parse a misaligned stream.
803 */
804 tempdiag3_log("read body failed: got %zd of %d hdr=%02x,%02x pktlen=%d",
805 r, pktlen, hdr[0], hdr[1], pktlen);
806 sim_printf("%s: read body failed: got %zd of %d\r\n",
807 __func__, r, pktlen);
808 close(mgp_dev_state.ncp_socket);
809 mgp_dev_state.ncp_socket = -1;
810 return -1;
811 }
812
813 tempdiag3_log("body consumed OK: pktlen=%d type=%d frame=%d", pktlen,
814 pktlen > 2 ? pkt8[2] : -1, pktlen > 4 ? pkt8[4] : -1);
815
816 return (int)r;
817 }
818
819 /*
820 * Convert an 8-bit MGP packet to 36-bit IOM words.
821 * The 16-byte header occupies 4 words (4 x 9-bit bytes per word).
822 * Data bytes follow in 9-bit format (upper bit zero).
823 */
824 static void
825 pkt8_to_word36(u_char *pkt8, int pktlen, word36 *buf, uint maxwords)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
826 {
827 uint j;
828 (void)memset(buf, 0, maxwords * sizeof(word36));
829
830 for (j = 0; j < (uint)pktlen && j / 4 < maxwords; j++)
831 {
832 putbits36_9(&buf[j / 4], (j % 4) * 9, pkt8[j]);
833 }
834 }
835
836 /*
837 * Convert 36-bit IOM words to an 8-bit MGP packet.
838 * Returns the total packet length in bytes.
839 */
840 static int
841 word36_to_pkt8(word36 *buf, uint words, u_char *pkt8, int maxlen)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
842 {
843 int j;
844 int total = (int)(words * 4);
845 if (total > maxlen)
846 {
847 total = maxlen;
848 }
849
850 for (j = 0; j < total; j++)
851 {
852 pkt8[j] = getbits36_9(buf[j / 4], (j % 4) * 9);
853 }
854
855 /* Determine actual packet length from byte_count in header */
856 if (total >= 16)
857 {
858 int byte_count = (pkt8[8] & 0xFF) | ((pkt8[9] & 0xFF) << 8);
859 int real_len = 16 + byte_count;
860 if (real_len < total)
861 {
862 total = real_len;
863 }
864 }
865
866 return total;
867 }
868
869 static iom_cmd_rc_t
870 get_ddcw(iom_chan_data_t *p, uint iom_unit_idx, uint chan, bool *ptro,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
871 uint expected_tally, uint *tally)
872 {
873 # if defined(TESTING)
874 cpu_state_t * cpup = _cpup;
875 # endif
876 bool send, uff;
877 int rc = iom_list_service(iom_unit_idx, chan, ptro, &send, &uff);
878
879 if (rc < 0)
880 {
881 p->stati = 05001;
882 sim_warn("%s list service failed\r\n", __func__);
883
884 return IOM_CMD_ERROR;
885 }
886
887 if (uff)
888 {
889 sim_warn("%s ignoring uff\r\n", __func__);
890 }
891
892 if (!send)
893 {
894 sim_warn("%s nothing to send\r\n", __func__);
895 p->stati = 05001;
896
897 return IOM_CMD_ERROR;
898 }
899
900 if (IS_IDCW(p) || IS_TDCW(p))
901 {
902 sim_warn("%s expected DDCW\r\n", __func__);
903 p->stati = 05001;
904
905 return IOM_CMD_ERROR;
906 }
907
908 *tally = p->DDCW_TALLY;
909
910 if (*tally == 0)
911 {
912 sim_debug(DBG_DEBUG, &mgp_dev,
913 "%s: Tally of zero interpreted as 010000(4096)\r\n", __func__);
914 *tally = 4096;
915 }
916
917 sim_debug(DBG_DEBUG, &mgp_dev,
918 "%s: Tally %d (%o)\r\n", __func__, *tally, *tally);
919
920 if (expected_tally && *tally != expected_tally)
921 {
922 sim_warn("mgp_dev call expected tally of %d; got %d\r\n",
923 expected_tally, *tally);
924 p->stati = 05001;
925
926 return IOM_CMD_ERROR;
927 }
928
929 return IOM_CMD_PROCEED;
930 }
931
932 static char *
933 cmd_name(int code)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
934 {
935 switch (code)
936 {
937 case 000:
938 return "Request status";
939
940 case 001:
941 return "Read";
942
943 case 011:
944 return "Write";
945
946 case 020:
947 return "Host switch down";
948
949 case 040:
950 return "Reset status";
951
952 case 042:
953 return "Disable Bus Back";
954
955 case 043:
956 return "Enable Bus Back";
957
958 case 060:
959 return "Host switch up";
960
961 default:
962 return "Unknown";
963 }
964 }
965
966 # if defined(TESTING)
967 /*
968 * dumppkt: Debug dump of a 36-bit MGP packet.
969 * Kept from the original for debugging purposes.
970 */
971 static void
972 dumppkt(char *hdr, word36 *buf, uint words)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
973 {
974 int i;
975 if (words < MGP_PACKET_HEADER_SIZE)
976 {
977 sim_printf("%s: packet too small (%d words)\r\n", hdr, words);
978 return;
979 }
980
981 int checksum = getbits36_9(buf[0], 0);
982 int id = getbits36_9(buf[0], 9);
983 int pktype = getbits36_9(buf[0], 18);
984 int flags = getbits36_9(buf[0], 27);
985 int framenr = getbits36_9(buf[1], 0);
986 int rcpt = getbits36_9(buf[1], 9);
987 int pknr = getbits36_9(buf[1], 18);
988 int acknr = getbits36_9(buf[1], 27);
989 int bytecount = (getbits36_9(buf[2], 0) & 0xff)
990 | ((getbits36_9(buf[2], 9) & 0xff) << 8);
991 int srcprc = (getbits36_9(buf[2], 18) & 0xff)
992 | ((getbits36_9(buf[2], 27) & 0xff) << 8);
993 int dstprc = (getbits36_9(buf[3], 0) & 0xff)
994 | ((getbits36_9(buf[3], 9) & 0xff) << 8);
995 int chopcode = getbits36_9(buf[3], 18);
996
997 sim_printf("%s packet (%d words)\r\n", hdr, words);
998 sim_printf("cks %#x, id %#x, type %#x, flags %#x\r\n"
999 "frame %#x, rcpt %#x, pknr %#x, acknr %#x\r\n"
1000 "bytecount %d, src %#x, dst %#x, chopcode %#o\r\n",
1001 checksum, id, pktype, flags,
1002 framenr, rcpt, pknr, acknr,
1003 bytecount, srcprc, dstprc, chopcode);
1004
1005 int pklen = MGP_PACKET_HEADER_SIZE + (bytecount / 4)
1006 + (bytecount % 4 ? 1 : 0);
1007 if (pklen > (int)words)
1008 {
1009 pklen = (int)words;
1010 }
1011
1012 for (i = 0; i < pklen; i++)
1013 {
1014 int lh = getbits36_18(buf[i], 0);
1015 int rh = getbits36_18(buf[i], 18);
1016 int b0 = getbits36_9 (buf[i], 0);
1017 int b1 = getbits36_9 (buf[i], 9);
1018 int b2 = getbits36_9 (buf[i], 18);
1019 int b3 = getbits36_9 (buf[i], 27);
1020 sim_printf(" %d: %06o,,%06o = 0x%02x %02x %02x %02x\r\n",
1021 i, lh, rh, b0, b1, b2, b3);
1022 }
1023
1024 sim_printf("EOP\r\n");
1025 }
1026 # endif
1027
1028 /* Forward declarations - defined later in this file */
1029 static int mgp_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words);
1030 static void mgp_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
1031 const char *caller);
1032
1033 /*
1034 * mgp_validate_dcw_state() - Diagnostic: validate DCW list state vs Multics memory.
1035 *
1036 * Called when an anomalous DDCW_TALLY or DDCW_ADDR is detected. Logs:
1037 * 1. Current iom_chan_data fields (cached values the IOM is using).
1038 * 2. The raw DCW list entries from Multics memory (workspace page 0),
1039 * so we can see whether the corruption is in the cache or in memory.
1040 *
1041 * The workspace structure (from mgp_read_dcm_.pl1 declare read_workspace):
1042 * offset 0-11: dcw_list[0..5] - 6 x (idcw + ddcw) = 12 words
1043 * offset 12: tdcw - 1 word (transfer/wrap-around DCW)
1044 * offset 13: reset_idcw - 1 word
1045 * offset 14-xx: status_queue[0..3] - 4 x istat (size varies)
1046 * offset ~22: buffer[0..5] - 6 x data_size words (129 read, 128 write)
1047 *
1048 * Expected DDCW_ADDR values (for the 6 buffer slots) should be within
1049 * [0, 01777] octal (the workspace fits in one IOM page = 1024 words).
1050 * Expected DDCW_TALLY is 129 (read channel) or 128 (write channel).
1051 *
1052 * If DDCW_ADDR or DDCW_TALLY is outside those bounds, DCW corruption has
1053 * occurred - most likely from a prior PTW-failure DMA write to address ~= 0
1054 * that overwrote the IOM mailbox and caused iom_list_service to follow a
1055 * bad LPW pointer into arbitrary memory.
1056 */
1057 static void
1058 mgp_validate_dcw_state(uint iom_unit_idx, uint chan, int expected_tally,
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
1059 const char *caller)
1060 {
1061 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1062
1063 /* 1. Dump the cached iom_chan_data state */
1064 sim_warn("DCW_VALIDATE [%s] chan=%d:\r\n"
1065 " cached: DDCW_ADDR=0%o DDCW_TALLY=%d (expected ~%d)"
1066 " DDCW_22_23_TYPE=%d\r\n"
1067 " LPW_DCW_PTR=0%o LPW_TALLY=%d\r\n"
1068 " PCW_PAGE_TABLE_PTR=0%o PCW_63_PTP=%d PCW_64_PGE=%d SEG=%d\r\n"
1069 " in_use=%d masked=%d\r\n",
1070 caller, chan,
1071 p->DDCW_ADDR, (int)p->DDCW_TALLY, expected_tally,
1072 (int)p->DDCW_22_23_TYPE,
1073 p->LPW_DCW_PTR, (int)p->LPW_TALLY,
1074 p->PCW_PAGE_TABLE_PTR,
1075 (int)p->PCW_63_PTP, (int)p->PCW_64_PGE, (int)p->SEG,
1076 (int)p->in_use, (int)p->masked);
1077
1078 /* 2. Sanity-check DDCW_ADDR range (workspace = one IOM page = 1024 words) */
1079 if (p->DDCW_ADDR > 01777)
1080 {
1081 sim_warn("DCW_VALIDATE: DDCW_ADDR=0%o is OUTSIDE workspace range"
1082 " [0, 01777] - LPW likely corrupted\r\n", p->DDCW_ADDR);
1083 }
1084
1085 /* 3. Read DCW list from Multics memory in paged mode */
1086 if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1087 {
1088 sim_warn("DCW_VALIDATE: not in paged mode"
1089 " (PTP=%d PGE=%d) - skipping memory read\r\n",
1090 (int)p->PCW_63_PTP, (int)p->PCW_64_PGE);
1091 return;
1092 }
1093
1094 /* Look up the workspace page 0 PTW */
1095 word24 pgte0 = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1096 + (((word24)(p->SEG & 1)) << 8)
1097 + 0u; /* page 0 */
1098
1099 word36 ptw0 = 0;
1100 iom_core_read(iom_unit_idx, pgte0, &ptw0, __func__);
1101
1102 int ptw0_valid = ((ptw0 & 0740000777747llu) == 04llu);
1103 sim_warn("DCW_VALIDATE: workspace page 0 PTW at pgte=0%o:"
1104 " 0%012llo (%s)\r\n",
1105 pgte0, (unsigned long long)ptw0,
1106 ptw0_valid ? "valid" : "INVALID");
1107
1108 if (!ptw0_valid)
1109 {
1110 sim_warn("DCW_VALIDATE: page 0 PTW invalid -"
1111 " cannot read DCW list from Multics memory\r\n");
1112 return;
1113 }
1114
1115 /* Physical base address of workspace (bits 4-17 of PTW, shifted left 10) */
1116 word24 phys_base = ((word24)((ptw0 >> 18) & MASK14)) << 10;
1117 sim_warn("DCW_VALIDATE: workspace physical base = 0%o\r\n", phys_base);
1118
1119 /* 4. Read and validate each of the 6 IDCW+DDCW pairs.
1120 * The dcw_list occupies offsets 0-11 in the workspace:
1121 * slot i: IDCW at offset 2*i, DDCW at offset 2*i+1 */
1122 sim_warn("DCW_VALIDATE: dcw_list entries from memory"
1123 " (expected TALLY=%d, ADDR in [0,01777]):\r\n", expected_tally);
1124 for (int i = 0; i < 6; i++)
1125 {
1126 word24 idcw_phys = phys_base + (word24)(2 * i);
1127 word24 ddcw_phys = phys_base + (word24)(2 * i + 1);
1128
1129 word36 idcw_word = 0, ddcw_word = 0;
1130 iom_core_read(iom_unit_idx, idcw_phys, &idcw_word, __func__);
1131 iom_core_read(iom_unit_idx, ddcw_phys, &ddcw_word, __func__);
1132
1133 /* DDCW layout: bits 0-17 = address, bits 24-35 = tally */
1134 uint ddcw_addr = (uint)((ddcw_word >> 18) & MASK18);
1135 uint ddcw_tally = (uint)(ddcw_word & 0xFFFu);
1136
1137 int anomalous = (ddcw_tally == 0
1138 || (int)ddcw_tally > (expected_tally + 2)
1139 || ddcw_addr > 01777u);
1140
1141 sim_warn(" slot[%d]: IDCW=0%012llo DDCW=0%012llo"
1142 " addr=0%o tally=%d%s\r\n",
1143 i,
1144 (unsigned long long)idcw_word,
1145 (unsigned long long)ddcw_word,
1146 ddcw_addr, ddcw_tally,
1147 anomalous ? " *** ANOMALOUS" : "");
1148 }
1149
1150 /* 5. Read the word at LPW_DCW_PTR to see what iom_list_service last fetched */
1151 word18 lpw_ptr = p->LPW_DCW_PTR;
1152 if (lpw_ptr <= 01777u)
1153 {
1154 word36 lpw_word = 0;
1155 iom_core_read(iom_unit_idx, phys_base + (word24)lpw_ptr, &lpw_word, __func__);
1156 sim_warn("DCW_VALIDATE: mem[LPW_DCW_PTR=0%o] = 0%012llo\r\n",
1157 lpw_ptr, (unsigned long long)lpw_word);
1158 }
1159 else
1160 {
1161 sim_warn("DCW_VALIDATE: LPW_DCW_PTR=0%o is OUTSIDE workspace"
1162 " [0, 01777] - LPW pointer corrupted\r\n", lpw_ptr);
1163 }
1164 }
1165
1166 static iom_cmd_rc_t
1167 mgp_cmd(uint iom_unit_idx, uint chan)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
1168 {
1169 # if defined(TESTING)
1170 cpu_state_t * cpup = _cpup;
1171 # endif
1172 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1173
1174 sim_debug(DBG_TRACE, &mgp_dev,
1175 "mgp_cmd CHAN_CMD %o DEV_CODE %o DEV_CMD %o COUNT %o\r\n",
1176 p->IDCW_CHAN_CMD, p->IDCW_DEV_CODE, p->IDCW_DEV_CMD, p->IDCW_COUNT);
1177
1178 // Not IDCW?
1179 if (IS_NOT_IDCW(p))
1180 {
1181 sim_warn("%s: Unexpected IOTx\r\n", __func__);
1182
1183 return IOM_CMD_ERROR;
1184 }
1185
1186 bool ptro;
1187
1188 sim_debug(DBG_DEBUG, &mgp_dev, "mgp_cmd %#o (%s)\r\n",
1189 p->IDCW_DEV_CMD, cmd_name(p->IDCW_DEV_CMD));
1190
1191 switch (p->IDCW_DEV_CMD)
1192 {
1193 case 000: // CMD 00 Request status
1194 {
1195 p->stati = 04000;
1196 sim_debug(DBG_DEBUG, &mgp_dev, "mgp request status\r\n");
1197 }
1198 break;
1199
1200 case 001: // CMD 01 Read
1201 {
1202 sim_debug(DBG_DEBUG, &mgp_dev, "%s: mgp_dev_$read\r\n", __func__);
1203
1204 const uint expected_tally = 0;
1205 uint tally;
1206 iom_cmd_rc_t rc
1207 = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1208 if (rc)
1209 {
1210 return rc;
1211 }
1212
1213 /* Validate the DMA PTW before any IOM buffer access.
1214 * If Multics's memory manager has paged out the IOM buffer (e.g. after
1215 * extended idle), iom_indirect_data_service() would compute physical
1216 * address ~= 0 from the zero PTW and either read garbage or corrupt the
1217 * IOM mailbox area (addresses 0-0777). Return IOM_CMD_DISCONNECT so the
1218 * IOM sends a terminate interrupt; Multics can then re-establish the
1219 * channel with properly pinned memory.
1220 *
1221 * max_words=256: NCP packets are at most MAX_PKT_BYTES=504 bytes =
1222 * ~126 36-bit words, which fits within the first IOM page (1024 words).
1223 * Using max_words=0 (full DDCW_TALLY=4096 words = 4 pages) would cause
1224 * false positives when Multics's memory manager pages out unused pages
1225 * 1-3 of the large DCW buffer overnight - those pages are never touched
1226 * by the DMA since the actual payload is only ~126 words. */
1227 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1228 {
1229 sim_warn("%s: CMD 001 invalid DMA PTW on chan %d - "
1230 "skipping buffer access, sending terminate interrupt\r\n",
1231 __func__, chan);
1232 mgp_dev_state.ptw_failed_at = time(NULL);
1233 p->stati = 04000;
1234 return IOM_CMD_DISCONNECT;
1235 }
1236
1237 /* Sanity-check DDCW_TALLY.
1238 *
1239 * Legitimate MGP read DDCWs have TALLY set by initialize_workspace
1240 * in mgp_read_dcm_.pl1: data_size=129 words. TALLY=0 (IOM convention
1241 * for 4096) or an implausibly large value means the DCW entry was
1242 * corrupted. The primary corruption path (TDCW wrap -> DDCW_ADDR=0 ->
1243 * IDS overwrites DCW list) is now blocked by the DDCW_ADDR range check
1244 * in mgp_process_event; this check is retained as defense-in-depth. */
1245 if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1246 {
1247 sim_warn("%s: CMD 001 implausible DDCW_TALLY=%d on chan %d"
1248 " (expected ~129, max %d - DCW corruption?)"
1249 " sending terminate interrupt\r\n",
1250 __func__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1251 mgp_validate_dcw_state(iom_unit_idx, chan, 129, "CMD001-TALLY");
1252 p->stati = 04000;
1253 return IOM_CMD_DISCONNECT;
1254 }
1255
1256 /* Read current buffer and complete DDCW processing */
1257 word36 buffer[256];
1258 uint words_processed;
1259 iom_indirect_data_service(
1260 iom_unit_idx, chan, buffer, &words_processed, false);
1261
1262 sim_debug(DBG_DEBUG, &mgp_dev,
1263 "%s: Read unit %#x chan %#x (%d), %d words\r\n",
1264 __func__, iom_unit_idx, chan, chan, words_processed);
1265
1266 /*
1267 * Mark that Multics wants to read. The actual data delivery happens
1268 * in mgp_process_event() when the NCP sends us a packet.
1269 */
1270 mgp_dev_state.want_to_read = 1;
1271 mgp_dev_state.want_to_read_since = time(NULL);
1272 mgp_dev_state.read_unit_idx = iom_unit_idx;
1273 mgp_dev_state.read_unit_chan = chan;
1274
1275 /* Write back to IOM to complete the DDCW processing.
1276 * This is required before returning IOM_CMD_PENDING -
1277 * without it the IOM channel state is inconsistent and
1278 * Multics will timeout and mask the channel.
1279 * (cf. new_dps8m_chaos_code/dps8_mgp.c.new lines 525-526)
1280 */
1281 iom_indirect_data_service(
1282 iom_unit_idx, chan, buffer, &words_processed, true);
1283
1284 p->stati = 04000;
1285 /* Signal mgp_process_event that this delivery attempt succeeded
1286 * (iom_continue_channel successfully called mgp_cmd). */
1287 mgp_dev_state.delivery_succeeded = 1;
1288 return IOM_CMD_PENDING;
1289 }
1290 /*NOTREACHED*/ /* unreachable */
1291 break;
1292
1293 case 011: // CMD 11 Write
1294 {
1295 sim_debug(DBG_DEBUG, &mgp_dev, "%s: mgp_dev_$write\r\n", __func__);
1296
1297 const uint expected_tally = 0;
1298 uint tally;
1299 iom_cmd_rc_t rc
1300 = get_ddcw(p, iom_unit_idx, chan, &ptro, expected_tally, &tally);
1301
1302 /* Check get_ddcw result (same as CMD 001 - missing this check was a
1303 * bug: a failed get_ddcw left p->DDCW_ADDR stale, causing the PTW
1304 * check below to validate the wrong page).
1305 *
1306 * NOTE: get_ddcw() returns IOM_CMD_PROCEED (0) on success or
1307 * IOM_CMD_ERROR (-1) on failure - never IOM_CMD_PENDING.
1308 * Returning IOM_CMD_ERROR here sends a TERMINATE interrupt (not marker)
1309 * via the rc<0 path in doPayloadChannel/iom_continue_channel. Multics
1310 * sees the terminate and re-issues the write; the packet content is
1311 * lost (silent frame drop -> "Unordered frame" in NCP log).
1312 * Logged as WARNING so we can correlate with NCP-side drops. */
1313 if (rc)
1314 {
1315 sim_warn("%s: CMD 011 get_ddcw failed rc=%d on chan %d"
1316 " - packet will be silently dropped (frame loss)\r\n",
1317 __func__, rc, chan);
1318 return rc;
1319 }
1320
1321 /* Validate PTW before DMA access (prevents mailbox corruption if
1322 * Multics has paged out the WRITE channel buffer).
1323 * max_words=256: same rationale as CMD 001 - the write payload is at
1324 * most ~126 36-bit words (MAX_PKT_BYTES / 4), which fits within the
1325 * current IOM page. Checking the full DDCW_TALLY (4096 words = 4
1326 * pages) would cause false positives when pages beyond the payload
1327 * are paged out, leading to tight IOM_CMD_DISCONNECT loops. */
1328 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1329 {
1330 sim_warn("%s: CMD 011 invalid DMA PTW on chan %d - "
1331 "skipping buffer access, sending terminate interrupt\r\n",
1332 __func__, chan);
1333 mgp_dev_state.ptw_failed_at = time(NULL);
1334 close(mgp_dev_state.ncp_socket);
1335 mgp_dev_state.ncp_socket = -1; /* discard queued NOOPs */
1336 p->stati = 04000;
1337 return IOM_CMD_DISCONNECT;
1338 }
1339
1340 /* Sanity-check DDCW_TALLY (same rationale as CMD 001).
1341 * The write channel uses data_size=128 (mgp_write_dcm_.pl1).
1342 * iom_indirect_data_service's READ path (first call below, reading
1343 * the outgoing packet from Multics memory) ignores cnt and walks
1344 * all p->DDCW_TALLY words; TALLY=0 -> 4096-word walk -> same console-
1345 * flooding cascade as CMD 001. */
1346 if (p->DDCW_TALLY == 0 || p->DDCW_TALLY > MGP_MAX_TALLY)
1347 {
1348 sim_warn("%s: CMD 011 implausible DDCW_TALLY=%d on chan %d"
1349 " (expected ~128, max %d - DCW corruption?)"
1350 " sending terminate interrupt\r\n",
1351 __func__, p->DDCW_TALLY, chan, MGP_MAX_TALLY);
1352 mgp_validate_dcw_state(iom_unit_idx, chan, 128, "CMD011-TALLY");
1353 mgp_dev_state.ptw_failed_at = time(NULL);
1354 close(mgp_dev_state.ncp_socket);
1355 mgp_dev_state.ncp_socket = -1;
1356 p->stati = 04000;
1357 return IOM_CMD_DISCONNECT;
1358 }
1359
1360 word36 buffer[256];
1361 uint words_processed;
1362 iom_indirect_data_service(
1363 iom_unit_idx, chan, buffer, &words_processed, false);
1364
1365 sim_debug(DBG_DEBUG, &mgp_dev,
1366 "%s: Write unit %#x chan %#x (%d), %d words\r\n",
1367 __func__, iom_unit_idx, chan, chan, words_processed);
1368 # if defined(TESTING)
1369 if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
1370 {
1371 dumppkt("Write", buffer, words_processed);
1372 }
1373 # endif
1374 /* Convert 36-bit words to 8-bit bytes */
1375 u_char pkt8[MAX_PKT_BYTES];
1376 int pktlen = word36_to_pkt8(buffer, words_processed, pkt8, MAX_PKT_BYTES);
1377
1378 /* Send to NCP */
1379 int v = ncp_send_packet(pkt8, pktlen);
1380 if (v < 0)
1381 {
1382 sim_warn("%s: ncp_send_packet failed\r\n", __func__);
1383 }
1384
1385 /* Return value depends on IDCW control field:
1386 *
1387 * mgp_write_dcm_ uses a double-buffer (half_buffer_size=3 slots per
1388 * half). When fill>=2, update_workspace_and_start_io patches the first
1389 * IDCW to CHAN_CTRL_PROCEED (=2, "no-terminate") and sets the last to
1390 * CHAN_CTRL_TERMINATE (=0). start_io calls ioi_$connect and resets
1391 * fill=0. doPayloadChannel's do-while loop then processes each IDCW:
1392 *
1393 * - IOM_CMD_PROCEED (0): loop continues -> iom_list_service advances
1394 * to the next IDCW -> mgp_cmd(011) called again for the next packet.
1395 * - IOM_CMD_DISCONNECT (2): sets terminate=true -> loop exits after
1396 * this iteration -> terminate interrupt sent.
1397 *
1398 * Without this check, we always returned IOM_CMD_DISCONNECT for every
1399 * IDCW, causing the loop to exit after the FIRST packet in a batch.
1400 * The second packet was orphaned (start_io had already reset fill=0, so
1401 * check_status_queue on the next write call skipped start_io entirely).
1402 * Result: frame N sent, frame N+1 silently dropped -> "Unordered frame"
1403 * in NCP log and truncated file transfer. */
1404 rc = (p->IDCW_CHAN_CTRL == CHAN_CTRL_TERMINATE)
1405 ? IOM_CMD_DISCONNECT /* terminate IDCW: send terminate interrupt */
1406 : IOM_CMD_PROCEED; /* no-terminate IDCW: continue DCW list loop */
1407 p->stati = 04000;
1408
1409 /* Write-back: re-validates PTW before writing back to the Multics
1410 * write DMA buffer. ncp_connect() inside ncp_send_packet() can
1411 * block briefly (Unix connect syscall), during which CPU A (Multics)
1412 * may page out the DMA buffer. If the page is now gone, skip the
1413 * write-back (avoids fetch_IDSPTW warnings and address-0 corruption),
1414 * close the socket to flush queued NOOPs, and let the terminate
1415 * interrupt trigger Multics channel recovery.
1416 * max_words=256: same rationale as initial PTW check above - check
1417 * only the payload area, not unused pages beyond it. */
1418 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1419 {
1420 sim_warn("%s: CMD 011 PTW invalid after ncp_send_packet on chan %d"
1421 " - skipping write-back\r\n", __func__, chan);
1422 mgp_dev_state.ptw_failed_at = time(NULL);
1423 close(mgp_dev_state.ncp_socket);
1424 mgp_dev_state.ncp_socket = -1;
1425 return rc; /* IOM_CMD_DISCONNECT - sends terminate interrupt */
1426 }
1427
1428 iom_indirect_data_service(
1429 iom_unit_idx, chan, buffer, &words_processed, true);
1430
1431 return rc;
1432 }
1433 /*NOTREACHED*/ /* unreachable */
1434 break;
1435
1436 case 006: // CMD 06 (seen during channel restart; acknowledge gracefully)
1437 {
1438 p->stati = 04000;
1439 sim_debug(DBG_DEBUG, &mgp_dev, "mgp cmd 006 (handled)\r\n");
1440 }
1441 break;
1442
1443 case 020: // CMD 20 Host switch down
1444 {
1445 p->stati = 04000;
1446 sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch down\r\n");
1447 }
1448 break;
1449
1450 case 040: // CMD 40 Reset status
1451 {
1452 p->stati = 04000;
1453 }
1454 break;
1455
1456 case 042: // CMD 42 Disable Bus Back
1457 {
1458 p->stati = 04000;
1459 sim_debug(DBG_DEBUG, &mgp_dev, "mgp disable bus back\r\n");
1460 }
1461 break;
1462
1463 case 043: // CMD 43 Enable Bus Back
1464 {
1465 p->stati = 04000;
1466 sim_debug(DBG_DEBUG, &mgp_dev, "mgp enable bus back\r\n");
1467 }
1468 break;
1469
1470 case 060: // CMD 60 Host switch up
1471 {
1472 p->stati = 04000;
1473 sim_debug(DBG_DEBUG, &mgp_dev, "mgp host switch up\r\n");
1474 }
1475 break;
1476
1477 default:
1478 {
1479 if (p->IDCW_DEV_CMD != 051) // ignore bootload console probe
1480 {
1481 sim_warn("%s: MGP unrecognized device command %02o\r\n",
1482 __func__, p->IDCW_DEV_CMD);
1483 }
1484
1485 p->stati = 04501; // cmd reject, invalid opcode
1486 p->chanStatus = chanStatIncorrectDCW;
1487 }
1488 return IOM_CMD_ERROR;
1489 }
1490
1491 if (p->IDCW_CHAN_CMD == 0)
1492 {
1493 return IOM_CMD_DISCONNECT; // don't do DCW list
1494 }
1495
1496 return IOM_CMD_PROCEED;
1497 }
1498
1499 iom_cmd_rc_t
1500 mgp_iom_cmd(uint iom_unit_idx, uint chan)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
1501 {
1502 iom_chan_data_t *p = &iom_chan_data[iom_unit_idx][chan];
1503
1504 // Is it an IDCW?
1505 if (IS_IDCW(p))
1506 {
1507 return mgp_cmd(iom_unit_idx, chan);
1508 }
1509
1510 sim_printf("%s expected IDCW\r\n", __func__);
1511
1512 return IOM_CMD_ERROR;
1513 }
1514
1515 /*
1516 * mgp_check_dma_ptw() - Validate that ALL pages of the IOM DMA buffer for
1517 * channel `chan` have valid page table words (PTWs) before calling
1518 * iom_indirect_data_service().
1519 *
1520 * ioi_$workspace pins all workspace pages (DCW list and data buffers) in
1521 * physical memory for the duration of active I/O (while in_use=true).
1522 * Page eviction cannot occur. However, if the DCW list becomes corrupted
1523 * (e.g., circular-buffer wrap during an RCP force-detach/reattach cycle),
1524 * DDCW_ADDR may point outside the wired workspace, where no PTW exists.
1525 * Calling iom_indirect_data_service() with PTW=0 computes physical address
1526 * ~= 0, overwriting the IOM mailbox area (addresses 0-0777). That corruption
1527 * cascades: Multics's IOM interrupt handler reads garbage vectors ->
1528 * fault/interrupt storm -> 100% CPU and system hang.
1529 *
1530 * The buffer spans from DDCW_ADDR through DDCW_ADDR+tally-1, potentially
1531 * crossing page boundaries. Only checking the first page misses unmapped
1532 * pages later in the buffer (symptom: fetch_IDSPTW warnings at addr 0o02000+
1533 * after a force-detach/reattach cycle). This function validates ALL pages
1534 * in the range so that any zero PTW is caught before the DMA starts.
1535 *
1536 * This function replicates the PTW lookup from fetch_IDSPTW /
1537 * build_IDSPTW_address (both static in dps8_iom.c) so dps8_mgp.c can
1538 * pre-check validity without touching the IOM code.
1539 *
1540 * Returns: 1 if all PTWs in the buffer range are valid (safe to proceed),
1541 * 0 if any PTW is zero or otherwise invalid (skip the DMA).
1542 */
1543 static int
1544 mgp_check_dma_ptw(uint iom_unit_idx, uint chan, uint max_words)
/* ![[previous]](../icons/left.png)
![[next]](../icons/right.png)
![[first]](../icons/first.png)
![[last]](../icons/last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
1545 {
1546 iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1547
1548 /* If the channel is not in paged mode, no PTW to validate. */
1549 if (!p->PCW_63_PTP || !p->PCW_64_PGE)
1550 return 1;
1551
1552 /* Determine the range of IOM pages the buffer spans.
1553 * tally=0 is interpreted as 4096 by get_ddcw / iom_indirect_data_service.
1554 * page numbers are 8-bit (IOM page table has at most 256 entries).
1555 *
1556 * max_words: when non-zero, caps the effective tally used for page range
1557 * calculation. Use this when the caller knows it will only write a small
1558 * payload (e.g. mgp_process_event delivers NCP packets of at most ~128
1559 * 36-bit words) so that pages beyond the actual payload are not validated
1560 * unnecessarily. Pass 0 to use the full DDCW_TALLY (or 4096). */
1561 uint raw_tally = p->DDCW_TALLY ? p->DDCW_TALLY : 4096;
1562 uint tally = (max_words && max_words < raw_tally) ? max_words : raw_tally;
1563 word18 start_page = (p->DDCW_ADDR >> 10) & MASK8;
1564 word18 end_page = ((p->DDCW_ADDR + tally - 1) >> 10) & MASK8;
1565
1566 /* Replicate build_IDSPTW_address() from dps8_iom.c for each page:
1567 * pgte = ((PCW_PAGE_TABLE_PTR & MASK18) << 6)
1568 * + ((SEG & 1) << 8)
1569 * + (pageNumber & MASK8) */
1570 for (word18 page = start_page; page <= end_page; page++)
1571 {
1572 word24 pgte = (((word24)(p->PCW_PAGE_TABLE_PTR & MASK18)) << 6)
1573 + (((word24)(p->SEG & 1)) << 8)
1574 + (page & MASK8);
1575
1576 word36 ptw;
1577 iom_core_read(iom_unit_idx, pgte, &ptw, __func__);
1578
1579 /* Valid PTW has specific bits set; zero PTW means page not present. */
1580 if ((ptw & 0740000777747llu) != 04llu)
1581 {
1582 sim_warn ("%s: chan %d DDCW_ADDR 0%o page %u/%u: invalid PTW"
1583 " 0%012llo at pgte 0%o"
1584 " (PCW_PAGE_TABLE_PTR=0%o SEG=%d tally=%u)\r\n",
1585 __func__, chan, p->DDCW_ADDR,
1586 (unsigned)(page - start_page + 1),
1587 (unsigned)(end_page - start_page + 1),
1588 (unsigned long long)ptw, pgte,
1589 p->PCW_PAGE_TABLE_PTR, (int)p->SEG, tally);
1590 return 0;
1591 }
1592 }
1593 return 1;
1594 }
1595
1596 /*
1597 * mgp_process_event() - Called periodically from the emulator's event loop.
1598 *
1599 * If Multics has a pending read (want_to_read), poll the NCP socket for
1600 * incoming data. If data is available, read it, convert from 8-bit to
1601 * 36-bit words, write to the current IOM workspace buffer[N] via
1602 * iom_indirect_data_service, then call iom_continue_channel() to:
1603 *
1604 * 1. Advance DDCW_ADDR to workspace buffer[N+1] (via one loop iteration
1605 * of doPayloadChannel: fetch IDCW[N+1] -> call mgp_cmd(read) ->
1606 * get_ddcw() sets DDCW_ADDR = buffer[N+1]).
1607 *
1608 * 2. Send a marker interrupt so mgp_read_dcm_'s wakeup handler fires.
1609 * The interrupt's stat.offset causes stop_buffer_number = N+1, which
1610 * is one ahead of the DCM's buffer_number (N), triggering the
1611 * processing loop to consume buffer[N].
1612 *
1613 * Because the interrupt is a MARKER (not terminate), start_io in the DCM
1614 * sees running=true and does NOT call ioi_$connect. The channel remains
1615 * pending with DDCW_ADDR pointing to buffer[N+1], ready for the next
1616 * incoming packet. want_to_read stays set (mgp_cmd restores it inside
1617 * iom_continue_channel).
1618 *
1619 * TDCW wrap-around is handled transparently by iom_list_service inside
1620 * iom_continue_channel.
1621 */
1622 void
1623 mgp_process_event(void)
/* ![[previous]](../icons/left.png)
![[next]](../icons/n_right.png)
![[first]](../icons/first.png)
![[last]](../icons/n_last.png)
![[top]](../icons/top.png)
![[bottom]](../icons/bottom.png)
![[index]](../icons/index.png)
*/
1624 {
1625 /* Unconditional no-progress watchdog. Runs regardless of want_to_read so
1626 * it cannot be starved by the masked/unmask flicker described above.
1627 * Lazily initializes last_progress_time on first observation so a fresh
1628 * connection (or one that just recovered) gets a full timeout window
1629 * before this can fire. Gated on want_to_read_since > 0: read_unit_idx/
1630 * read_unit_chan are only meaningfully set once Multics has issued at
1631 * least one real read (mgp_cmd CMD 001); before that they are still
1632 * zero-initialized, and firing the watchdog would send a terminate
1633 * interrupt to the wrong (unit 0, chan 0) channel. */
1634 # if defined(TESTING)
1635 cpu_state_t * cpup = _cpup;
1636 # endif
1637 if (mgp_dev_state.ncp_socket >= 0 && mgp_dev_state.want_to_read_since > 0)
1638 {
1639 time_t now_np = time(NULL);
1640 if (mgp_dev_state.last_progress_time == 0)
1641 {
1642 mgp_dev_state.last_progress_time = now_np;
1643 }
1644 else if (now_np - mgp_dev_state.last_progress_time >= NO_PROGRESS_TIMEOUT_SECS)
1645 {
1646 sim_warn("%s: no packet delivered to Multics in %d+ seconds despite "
1647 "NCP connection; forcing socket reset and terminate "
1648 "interrupt to break a possible masked/unmask timeout loop\r\n",
1649 __func__, NO_PROGRESS_TIMEOUT_SECS);
1650 close(mgp_dev_state.ncp_socket);
1651 mgp_dev_state.ncp_socket = -1;
1652 mgp_dev_state.last_progress_time = 0;
1653 mgp_dev_state.masked_since = 0;
1654 mgp_dev_state.want_to_read = 0;
1655 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1656 mgp_dev_state.read_unit_chan);
1657 return;
1658 }
1659 }
1660
1661 if (!mgp_dev_state.want_to_read)
1662 {
1663 return;
1664 }
1665
1666 uint iom_unit_idx = mgp_dev_state.read_unit_idx;
1667 uint chan = mgp_dev_state.read_unit_chan;
1668 iom_chan_data_t * p = & iom_chan_data[iom_unit_idx][chan];
1669
1670 /* If the channel has been masked (Multics sent a PCW with MSK=1), stop
1671 * trying to deliver packets until Multics re-enables it with a fresh
1672 * Connect PCW (MSK=0). want_to_read is restored by mgp_cmd() when
1673 * Multics issues the new read command inside doPayloadChannel.
1674 *
1675 * NOTE: We do NOT check !in_use here. The IOM sets in_use=false after
1676 * a terminate interrupt (e.g. DCW fault), but iom_continue_channel()
1677 * needs to run in that case so it can advance the DCW list and generate
1678 * the terminate interrupt that tells Multics to re-issue the read command.
1679 * Blocking on !in_use (without masked) prevents that signalling and
1680 * causes Multics to stall for ~30 seconds until its d102 timer fires. */
1681 {
1682 if (p->masked)
1683 {
1684 mgp_dev_state.want_to_read = 0;
1685
1686 /* If the NCP socket is connected, track how long the channel has been
1687 * stuck masked. After MASKED_STUCK_TIMEOUT_SECS, close the socket and
1688 * send a terminate interrupt. This breaks the deadlock:
1689 * - masked channel -> no delivery -> NCP inflight=30 -> no more NOOPs
1690 * - ioi_masked$timer fires but "masked while in use" prevents recovery
1691 * The terminate interrupt tells Multics the I/O failed so it re-issues
1692 * READ with a fresh channel state. The NCP-absent path then handles
1693 * reconnection cleanly. */
1694 if (mgp_dev_state.ncp_socket >= 0)
1695 {
1696 time_t now = time(NULL);
1697 if (mgp_dev_state.masked_since == 0)
1698 {
1699 mgp_dev_state.masked_since = now;
1700 sim_debug(DBG_DEBUG, &mgp_dev,
1701 "%s: channel %d masked while NCP connected; "
1702 "starting stuck timer\r\n", __func__, chan);
1703 }
1704 else if (now - mgp_dev_state.masked_since >= MASKED_STUCK_TIMEOUT_SECS)
1705 {
1706 sim_warn("%s: channel %d masked+stuck for %d+ seconds; "
1707 "closing NCP socket and sending terminate interrupt "
1708 "to force recovery\r\n",
1709 __func__, chan, MASKED_STUCK_TIMEOUT_SECS);
1710 close(mgp_dev_state.ncp_socket);
1711 mgp_dev_state.ncp_socket = -1;
1712 mgp_dev_state.masked_since = 0;
1713 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1714 mgp_dev_state.read_unit_chan);
1715 }
1716 }
1717 else
1718 {
1719 /* NCP not connected. Reset the masked-stuck timer (it's only
1720 * meaningful when the NCP is connected), but also run the
1721 * NCP-absent timeout so the IOM channel's in_use=true state is
1722 * eventually released.
1723 *
1724 * Without this: masked=true causes us to return here every 10ms,
1725 * bypassing the NCP-absent timeout check below. If the channel
1726 * is masked AND the NCP is absent, in_use never clears, and
1727 * ioi_masked$timer fires every ~4 minutes finding "chan N masked
1728 * while in use" - a permanent stuck deadlock.
1729 *
1730 * With this: after NCP_ABSENT_TIMEOUT_SECS (30 s) we send a
1731 * terminate interrupt. in_use becomes false. The next
1732 * ioi_masked$timer invocation successfully reconnects the masked
1733 * channel (no longer masked+in_use), and normal operation
1734 * resumes once the NCP connects again. */
1735 mgp_dev_state.masked_since = 0;
1736 time_t now_ma = time(NULL);
1737 if (mgp_dev_state.want_to_read_since > 0 &&
1738 now_ma - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1739 {
1740 sim_debug(DBG_DEBUG, &mgp_dev,
1741 "%s: NCP absent + channel %d masked for %d+ s; "
1742 "sending terminate interrupt to release in_use\r\n",
1743 __func__, chan, NCP_ABSENT_TIMEOUT_SECS);
1744 mgp_dev_state.want_to_read = 0;
1745 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1746 mgp_dev_state.read_unit_chan);
1747 }
1748 }
1749 return;
1750 }
1751
1752 /* Channel is not masked - clear the stuck timer */
1753 mgp_dev_state.masked_since = 0;
1754 }
1755
1756 /* Periodic diagnostic: log socket state every 60 seconds at debug level */
1757 {
1758 static time_t last_diag = 0;
1759 time_t now = time(NULL);
1760 if (now - last_diag >= 60)
1761 {
1762 sim_debug(DBG_DEBUG, &mgp_dev,
1763 "MGP diag: want_to_read=%d ncp_socket=%d unit=%d chan=%d\r\n",
1764 mgp_dev_state.want_to_read,
1765 mgp_dev_state.ncp_socket,
1766 mgp_dev_state.read_unit_idx,
1767 mgp_dev_state.read_unit_chan);
1768 last_diag = now;
1769 }
1770 }
1771
1772 /* If the NCP is not connected, try to connect first. If still not
1773 * connected after NCP_ABSENT_TIMEOUT_SECS, release the IOM channel via
1774 * a terminate interrupt. Without this release, the channel stays in
1775 * IOM_CMD_PENDING indefinitely; Multics's ioi_masked$timer eventually
1776 * fires and sends a mask PCW while the channel is still "in use",
1777 * producing spurious "doConnectChan: chan N masked while in use" and
1778 * "ioi_masked$timer: Timeout on channel" console messages.
1779 * After the terminate interrupt, Multics re-issues the READ command and
1780 * want_to_read_since is reset, so the cycle repeats quietly every
1781 * NCP_ABSENT_TIMEOUT_SECS seconds until the NCP connects. */
1782 if (mgp_dev_state.ncp_socket < 0)
1783 {
1784 /* ncp_connect() handles PTW backoff and rate-limiting internally. */
1785 ncp_connect();
1786 if (mgp_dev_state.ncp_socket < 0)
1787 {
1788 /* Still not connected. Check whether we have been waiting too long. */
1789 time_t now2 = time(NULL);
1790 if (now2 - mgp_dev_state.want_to_read_since >= NCP_ABSENT_TIMEOUT_SECS)
1791 {
1792 sim_debug(DBG_DEBUG, &mgp_dev,
1793 "%s: NCP absent for %d+ seconds, releasing IOM channel %d "
1794 "via terminate interrupt\r\n",
1795 __func__, NCP_ABSENT_TIMEOUT_SECS,
1796 mgp_dev_state.read_unit_chan);
1797 mgp_dev_state.want_to_read = 0;
1798 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1799 mgp_dev_state.read_unit_chan);
1800 }
1801 return;
1802 }
1803 /* NCP just connected. Reset the timestamp so we don't immediately
1804 * time out on the next invocation. */
1805 mgp_dev_state.want_to_read_since = time(NULL);
1806 }
1807
1808 /* REVERTED (2026-07-24) -- the advance_retry_pending mechanism that used to
1809 * live here (added 2026-07-23 alongside this same commit's read_full()/
1810 * resync-on-error improvements below, which are kept) turned out to cause
1811 * a worse bug than the one it fixed. On failure it retried by calling
1812 * iom_continue_channel() again on the next tick -- but iom_continue_channel()
1813 * unconditionally calls iom_list_service() as its first step, which fetches
1814 * the next DCW and advances LPW_DCW_PTR regardless of what happens
1815 * afterward. Each retry therefore advanced the DCW list an extra step with
1816 * no corresponding fresh packet write, silently racing the channel's
1817 * reported progress (stat.offset / stop_buffer_number on the Multics side)
1818 * ahead of how many buffer slots actually held fresh data. Confirmed via
1819 * mgp_daemon.log tracing on system-a/system-b: this caused Multics's
1820 * mgp_read_dcm_ catch-up loop to sweep past slots last written up to
1821 * buffer_size (16) delivery cycles ago and silently reprocess the stale
1822 * packet still sitting there, producing a steady-state "NAK from Multics"
1823 * storm recurring every ~3 seconds indefinitely, including at idle, and a
1824 * failed CFTP binary transfer. Reverting this mechanism (this branch) fixed
1825 * both directions of CFTP with no NAK storm, and reverified that fast-typing
1826 * character loss over chtn (the bug this was meant to fix) does NOT
1827 * resurface -- that fix already landed independently the day before
1828 * (2026-07-22) in mgp_queue_manager_.pl1's find_packet_connection and
1829 * mgp_read_dcm_.pl1's do-while loop, so this mechanism was redundant as well
1830 * as buggy. A defense-in-depth fix was also added on the Multics side
1831 * (mgp_read_dcm_.pl1 zeroes buffer_data(1) after a successful read) to
1832 * harmlessly skip any future stale-slot revisit regardless of cause, and is
1833 * kept independent of this revert.
1834 */
1835
1836 /* Try to receive a packet from the NCP */
1837 u_char pkt8[MAX_PKT_BYTES];
1838 int pktlen = ncp_recv_packet(pkt8, MAX_PKT_BYTES);
1839
1840 if (pktlen <= 0)
1841 {
1842 return; /* nothing available or error */
1843 }
1844
1845 /* pkt8 is the 8-bit wire format documented in multics_ncp/src/mgp.rs:
1846 * byte 2 = packet_type, byte 4 = frame_number, byte 16 = first data byte. */
1847 sim_debug (DBG_DEBUG, &mgp_dev,
1848 "%s: recv pktlen=%d type=%d frame=%d data0=0x%02x('%c')\r\n",
1849 __func__, pktlen,
1850 pktlen > 2 ? pkt8[2] : -1,
1851 pktlen > 4 ? pkt8[4] : -1,
1852 pktlen > 16 ? pkt8[16] : 0,
1853 (pktlen > 16 && pkt8[16] >= 32 && pkt8[16] < 127) ? pkt8[16] : '.');
1854
1855 sim_debug(DBG_DEBUG, &mgp_dev,
1856 "%s: received %d bytes from NCP for unit %d chan %d\r\n",
1857 __func__, pktlen, iom_unit_idx, chan);
1858
1859 /* Guard: only deliver if the IOM channel has an active, pinned I/O
1860 * operation. ioi_$workspace wires all workspace pages (DCW list, IDCWs,
1861 * DDCWs, and data buffers) while in_use=true. When in_use=false the
1862 * channel has no active I/O: DDCW_ADDR and DDCW_TALLY may be stale or
1863 * corrupted (e.g., left over from the previous iom_continue_channel call),
1864 * and the workspace pin is not guaranteed. Attempting delivery in this
1865 * state risks writing to a bad address.
1866 *
1867 * want_to_read=1 with in_use=false is the pathological state that produces
1868 * the overnight "fetch_IDSPTW: addr 07766 ptw 000000000000" cascade: mgp_cmd
1869 * was called via iom_continue_channel and stored a stale DDCW_ADDR, then
1870 * send_terminate_interrupt set in_use=false without clearing want_to_read.
1871 * The fix: if in_use is false, discard the packet, clear want_to_read, and
1872 * wait for Multics to re-issue ioi_$connect (which will set in_use=true and
1873 * establish a fresh, valid DDCW for us). */
1874
1875 if (! p->in_use)
1876 {
1877 sim_warn("%s: chan %d not in active I/O (in_use=false) but want_to_read=1"
1878 " - discarding packet, clearing want_to_read\r\n",
1879 __func__, chan);
1880 mgp_validate_dcw_state(iom_unit_idx, chan, 129, "process_event-in_use=0");
1881 mgp_dev_state.want_to_read = 0;
1882 return;
1883 }
1884
1885 /* Convert 8-bit packet to 36-bit words */
1886 word36 buffer[128]; /* 4 header + up to 122 data words */
1887 uint words_processed = 128;
1888 (void)memset(buffer, 0, sizeof(buffer));
1889
1890 pkt8_to_word36(pkt8, pktlen, buffer, 128);
1891
1892 sim_debug(DBG_DEBUG, &mgp_dev,
1893 "%s: received %d bytes from NCP for unit %d chan %d\r\n",
1894 __func__, pktlen, iom_unit_idx, chan);
1895
1896 # if defined(TESTING)
1897 if (sim_deb && (mgp_dev.dctrl & DBG_DEBUG))
1898 {
1899 dumppkt("NCP-Read", buffer, words_processed);
1900 }
1901 # endif
1902
1903 /* Validate the IOM DMA target PTW before writing packet data.
1904 *
1905 * This is a belt-and-suspenders check that runs AFTER the in_use guard
1906 * above. If in_use=true, the workspace IS pinned and DDCW_ADDR should
1907 * be valid - but if the DCW list became corrupted (e.g., circular-buffer
1908 * wrap producing a bad DDCW), DDCW_ADDR might point outside the workspace.
1909 * The workspace has only 1 IOM page (820 words for buffer_size=6,
1910 * data_size=129), so any DDCW_ADDR >= 01400 octal (page 1+) has no PTW
1911 * entry and PTW=0. The stale value "07766" seen in overnight cascades is
1912 * page 3 - far beyond the workspace - and is caught here.
1913 *
1914 * Calling iom_indirect_data_service() with PTW=0 would compute physical
1915 * address ~= 0 and overwrite the IOM mailbox area (addresses 0-0777),
1916 * causing an IOM interrupt storm -> Multics CPU at 100%.
1917 *
1918 * If invalid: close the NCP socket (discarding all queued NOOPs from the
1919 * kernel receive buffer), send one clean terminate interrupt so Multics
1920 * can re-establish the channel, and return without doing the DMA.
1921 *
1922 * max_words=256: cap the PTW range check at 256 words (generous upper
1923 * bound for any NCP packet: header 16 bytes + max data 488 bytes = 504
1924 * bytes = 126 36-bit words, rounded up). This prevents false positives
1925 * when DDCW_TALLY=0 (IOM interprets as 4096 words, spanning pages 0-3)
1926 * which can occur if iom_continue_channel left the channel in a
1927 * transitional "uff or nothing to send" state. With max_words=256 and
1928 * DDCW_ADDR=0: end_page=(0+255)/1024=0 -> only page 0 checked -> valid.
1929 * Real corruption (DDCW_ADDR=07766 = page 3) is still caught because
1930 * start_page=3 and that page has no PTW entry. */
1931 if (!mgp_check_dma_ptw(iom_unit_idx, chan, 256))
1932 {
1933 sim_warn("%s: invalid DMA PTW on chan %d - closing NCP socket and "
1934 "sending terminate interrupt to allow Multics channel recovery\r\n",
1935 __func__, chan);
1936 mgp_dev_state.want_to_read = 0;
1937 mgp_dev_state.ptw_failed_at = time(NULL); /* start reconnect backoff */
1938 /* Close the socket. From the RECEIVER side, close() discards all
1939 * unread data in the kernel receive buffer. This eliminates the
1940 * remaining queued NCP packets (typically 30 keepalive NOOPs) that
1941 * would otherwise continue triggering failed delivery attempts and
1942 * rapid terminate-interrupt storms after Multics re-issues the READ.
1943 * The NCP detects the closed connection and reconnects; NAK recovery
1944 * then re-synchronizes the MGP frame counters. */
1945 if (mgp_dev_state.ncp_socket >= 0)
1946 {
1947 close(mgp_dev_state.ncp_socket);
1948 mgp_dev_state.ncp_socket = -1;
1949 }
1950 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
1951 mgp_dev_state.read_unit_chan);
1952 return;
1953 }
1954
1955 /* Validate DDCW_ADDR is within the read channel's data buffer area.
1956 *
1957 * ROOT CAUSE FIX for the DDCW_ADDR=0 / DCW-list corruption bug:
1958 *
1959 * After all 6 buffers are delivered, iom_continue_channel reaches the TDCW
1960 * at DCW list offset 12. iom_list_service calls unpack_DCW for the TDCW
1961 * word; since the TDCW has DATA_ADDRESS=0 (all zero bits, as initialized
1962 * by mgp_read_dcm_.pl1 initialize_workspace), unpack_DCW stores
1963 * p->DDCW_ADDR = 0. LPW_TALLY then decrements to 0, setting uff=true.
1964 * iom_continue_channel sees uff=true, logs "uff or nothing to send", and
1965 * returns WITHOUT calling mgp_cmd(001) and WITHOUT sending a terminate
1966 * interrupt. want_to_read=1 persists with p->DDCW_ADDR=0.
1967 *
1968 * The PTW check above passes for DDCW_ADDR=0 with max_words=256 because
1969 * workspace page 0 IS a valid mapped page - the workspace is only 1 IOM
1970 * page. Without this range check, iom_indirect_data_service(write=true)
1971 * would write 128 words of NCP packet data to workspace offset 0,
1972 * overwriting the entire DCW list (IDCW/DDCW pairs at offsets 0-11).
1973 * The corrupted DCW list then causes downstream DDCW_TALLY=0 readings
1974 * in iom_list_service, triggering the 4096-word IDS walk -> thousands of
1975 * fetch_IDSPTW sim_warn calls -> console buffer full -> emulator blocked.
1976 *
1977 * Fix: reject any DDCW_ADDR below the first valid data buffer offset.
1978 * The minimum valid address is MGP_FIRST_BUFFER_OFFSET (22), which is the
1979 * start of buffer(0) in the workspace. Send a terminate interrupt so
1980 * Multics re-issues ioi_$connect; the fresh mgp_cmd(001) / get_ddcw() call
1981 * will advance through the TDCW wrap back to IDCW[0]/DDCW[0] and set
1982 * DDCW_ADDR = 22 as expected. */
1983
1984 if ((int)p->DDCW_ADDR < MGP_FIRST_BUFFER_OFFSET)
1985 {
1986 sim_warn("%s: DDCW_ADDR=%d on chan %d is below first buffer offset %d"
1987 " (stale DDCW_ADDR; TAL fix prevents TDCW wrap case)"
1988 " - sending terminate interrupt; NCP socket stays connected\r\n",
1989 __func__, p->DDCW_ADDR, chan,
1990 MGP_FIRST_BUFFER_OFFSET);
1991 sim_warn("%s: DCW=%llo, IS_IDCW=%d, IS_TDCW=%d, IS_IOTD=%d, IS_IONTP=%d.\r\n",
1992 __func__, p->DCW, IS_IDCW(p), IS_TDCW(p), IS_IOTD(p), IS_IONTP(p));
1993
1994 /* Do NOT close the NCP socket. The TDCW wrap is a normal, periodic
1995 * IOM event (happens after every 6th buffer delivery). The socket
1996 * is healthy - only the IOM channel state needs resetting. Closing
1997 * the socket would cause unnecessary NCP reconnect cycles (~every 6s)
1998 * which accumulate Multics error counts and eventually trigger channel
1999 * masking. A terminate interrupt is sufficient: Multics re-issues
2000 * ioi_$connect, the fresh mgp_cmd(001)/get_ddcw() sets a valid
2001 * DDCW_ADDR (>= 22), and delivery resumes on the next NCP packet. */
2002 mgp_dev_state.want_to_read = 0;
2003 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2004 mgp_dev_state.read_unit_chan);
2005 return;
2006 }
2007
2008 /* Write the packet to the current IOM workspace buffer[N]. */
2009 iom_indirect_data_service(
2010 iom_unit_idx, chan, buffer, &words_processed, true);
2011
2012 /* Real forward progress: feed the no-progress watchdog above. */
2013 mgp_dev_state.last_progress_time = time(NULL);
2014
2015 sim_debug(DBG_DEBUG, &mgp_dev,
2016 "%s: wrote %d words to IOM buffer, advancing channel and sending marker interrupt\r\n",
2017 __func__, words_processed);
2018
2019 /*
2020 * Advance the channel to workspace buffer[N+1] and send a marker
2021 * interrupt. iom_continue_channel() does the following in order:
2022 *
2023 * 1. Calls iom_list_service() to fetch IDCW[N+1] from the DCW list
2024 * (LPW_DCW_PTR advances from IDCW[N+1] to DDCW[N+1]).
2025 * 2. Calls d->iom_cmd() for IDCW[N+1]:
2026 * mgp_cmd(read) -> get_ddcw() -> iom_list_service() reads DDCW[N+1]
2027 * -> DDCW_ADDR = buffer[N+1], LPW_DCW_PTR = IDCW[N+2].
2028 * mgp_cmd sets want_to_read=1 and returns IOM_CMD_PENDING.
2029 * 3. Calls send_marker_interrupt():
2030 * stat.offset = LPW_offset(IDCW[N+2]) - 1 = 2*(N+2) - 1
2031 * stop_buffer = divide(stat.offset, 2) = N+1
2032 * mgp_read_dcm_'s loop fires (buffer_number=N != stop_buffer=N+1),
2033 * calls mgp_read_dim_(N), then increments buffer_number to N+1.
2034 *
2035 * start_io sees running=true (marker) and does NOT reconnect; the
2036 * channel stays pending with DDCW_ADDR pointing to buffer[N+1].
2037 * want_to_read remains 1 (set inside mgp_cmd via iom_continue_channel).
2038 *
2039 * On failure (e.g. DCW list corrupt, "expected IDCW"): iom_continue_channel
2040 * just returns without calling mgp_cmd and without sending any interrupt.
2041 * We detect this via delivery_succeeded: mgp_cmd(READ) sets it to 1 on
2042 * success; we clear it just before calling iom_continue_channel. After
2043 * 3 consecutive failures we reset want_to_read, close the NCP socket,
2044 * and send a terminate interrupt so Multics can re-establish the channel.
2045 */
2046 mgp_dev_state.delivery_succeeded = 0;
2047 int rc = iom_continue_channel(iom_unit_idx, chan);
2048
2049 sim_debug (DBG_DEBUG, &mgp_dev,
2050 "%s: iom_continue_channel rc=%d delivery_succeeded=%d\r\n",
2051 __func__, rc, mgp_dev_state.delivery_succeeded);
2052
2053 /* Detect and break "expected IDCW" cascade. */
2054 {
2055 static int consecutive_iom_failures = 0;
2056 if (mgp_dev_state.delivery_succeeded)
2057 {
2058 consecutive_iom_failures = 0;
2059 }
2060 else
2061 {
2062 if (++consecutive_iom_failures >= 3)
2063 {
2064 sim_warn("%s: %d consecutive IOM delivery failures on chan %d; "
2065 "resetting want_to_read, closing NCP socket, and "
2066 "sending terminate interrupt\r\n",
2067 __func__, consecutive_iom_failures,
2068 mgp_dev_state.read_unit_chan);
2069 consecutive_iom_failures = 0;
2070 mgp_dev_state.want_to_read = 0;
2071 /* Close socket to discard remaining queued NCP packets;
2072 * see the comment in the PTW-check block above. */
2073 if (mgp_dev_state.ncp_socket >= 0)
2074 {
2075 close(mgp_dev_state.ncp_socket);
2076 mgp_dev_state.ncp_socket = -1;
2077 }
2078 if (rc == 0) /* we handle the rc != 0 case below */
2079 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2080 mgp_dev_state.read_unit_chan);
2081 }
2082 }
2083 }
2084
2085 /* if iom_continue_channel returned a fatal error, terminate the I/O and let the DCM
2086 restart it. */
2087 if (rc != 0)
2088 {
2089 send_terminate_interrupt(mgp_dev_state.read_unit_idx,
2090 mgp_dev_state.read_unit_chan);
2091 }
2092 }
2093
2094 #endif /* if defined(WITH_MGP_DEV) */