Seems to be working great

This commit is contained in:
2026-10-07 01:03:18 -06:00
parent c39c226885
commit 739347c4da
26 changed files with 6290 additions and 7007 deletions
+5 -6
View File
@@ -1,16 +1,15 @@
CC ?= cc
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2 -pthread
CFLAGS = -std=c11 -Wall -Wextra -Werror -O2
.PHONY: test clean
test: tests/driver tests/test_bridge tests/bridge_driver
./tests/test_bridge
python3 tests/test_tcp.py
python3 tests/test_tcp.py bridge_driver
python3 tests/test_transport.py
python3 tests/test_worker.py
tests/bridge_driver: tests/bridge_driver.c src/atom_bridge.c src/atom_bridge.h src/atom_worker.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
$(CC) $(CFLAGS) -Isrc tests/bridge_driver.c src/atom_bridge.c src/atom_worker.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
tests/test_bridge: tests/test_bridge.c src/atom_bridge.c src/atom_bridge.h src/atom_worker.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
$(CC) $(CFLAGS) -Isrc tests/test_bridge.c src/atom_bridge.c src/atom_worker.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
tests/bridge_driver: tests/bridge_driver.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
$(CC) $(CFLAGS) -Isrc tests/bridge_driver.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
tests/test_bridge: tests/test_bridge.c src/atom_bridge.c src/atom_bridge.h src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
$(CC) $(CFLAGS) -Isrc tests/test_bridge.c src/atom_bridge.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
tests/driver: tests/driver.c src/atom_mb.c src/atom_mb.h src/atom_rtu.c src/atom_rtu.h src/atom_server.c src/atom_server.h
$(CC) $(CFLAGS) -Isrc tests/driver.c src/atom_mb.c src/atom_rtu.c src/atom_server.c -o $@
clean:
@@ -1,4 +1,3 @@
#define _GNU_SOURCE
#define _DEFAULT_SOURCE
#include "../src/atom_bridge.h"
#include "CmpStd.h"
@@ -32,5 +31,3 @@ void CDECL CDECL_EXT atmbcall_cext(atmbcall_cext_struct *p)
#include "../src/atom_rtu.c"
#include "../src/atom_server.c"
#include "../src/atom_bridge.c"
#include "../src/atom_worker.c"
+2 -4
View File
@@ -1,8 +1,8 @@
# Generated from supplied ExtensionSDK; build with target Linux toolchain.
SDKDIR ?= ../../ExtensionSDK
MAJORVER=0
MINORVER=7
CMPVERSION=0x00040000
MINORVER=4
CMPVERSION=0x00050000
# Local development ID; check no other component uses this ID.
CMPID=0x2001
DOS2UNIX=dos2unix
@@ -10,6 +10,4 @@ M4=m4
CC ?= gcc
# The SDK otherwise includes CmpFrame in its component identity wildcard.
override CSRC := CmpAtomModbus.c
CFLAGS += -pthread
LDLIBS += -pthread
include $(SDKDIR)/makefile
+15 -19
View File
@@ -4,7 +4,7 @@
#include "atom_server.h"
#include <stdio.h>
#include <string.h>
typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired, suspended; uint64_t heartbeat; } slot;
typedef struct { am_client c; ar_transport rtu; int serial; uint32_t generation; int used, retired; } slot;
static slot slots[AM_POOL_SIZE];
static uint32_t token(unsigned i) { return (slots[i].generation<<8)|(i+1u); }
static slot *lookup(uint32_t h) {
@@ -47,7 +47,7 @@ uint32_t ab_dispatch(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,
text[q]=0;
if(!ar_init(&s->c,&s->rtu,text,ip,unit,f,(uint8_t)addr,ct))return AM_ARGUMENT;
} else if(!am_init(&s->c,text,port)) return AM_ARGUMENT;
s->generation++; s->used=1; s->suspended=0; s->heartbeat=now; h=token(i);
s->generation++; s->used=1; h=token(i);
snapshot(&s->c,h,status); return 0;
}
s=lookup(h);
@@ -58,12 +58,9 @@ uint32_t ab_dispatch(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,
/* Never recycle a generation after wrap: stale handles stay invalid. */
if(s->generation==0xffffffu) s->retired=1;
snapshot(&s->c,0,status); return 0;
case AB_ENABLE: s->heartbeat=now; s->suspended=0; am_enable(&s->c,1,now); break;
case AB_DISABLE: s->suspended=0; if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break;
case AB_TICK:
s->heartbeat=now;
if(s->suspended) { s->suspended=0; am_enable(&s->c,1,now); }
break;
case AB_ENABLE: am_enable(&s->c,1,now); break;
case AB_DISABLE: if(s->serial)ar_close(&s->c);else am_enable(&s->c,0,now); break;
case AB_TICK: if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now); break;
case AB_RECONNECT: if(s->serial)ar_reconnect(&s->c,&s->rtu,now);else am_reconnect(&s->c,now); break;
case AB_CONFIGURE:
if(s->c.enabled) result=AM_BUSY;
@@ -106,15 +103,14 @@ void ab_close_all(void) {
}
}
/* Called only by the worker while holding the shared engine mutex. */
void ab_pump(uint64_t now,uint32_t watchdog_ms) {
for(unsigned i=0;i<AM_POOL_SIZE;i++) if(slots[i].used) {
slot *s=&slots[i];
if(s->c.enabled && now-s->heartbeat >= watchdog_ms) {
if(s->serial)ar_close(&s->c);else am_close(&s->c);
s->suspended=1;
}
if(s->serial)ar_tick(&s->c,&s->rtu,now);else am_tick(&s->c,now);
}
as_pump(now,watchdog_ms);
/* Synchronous ABI entry point. All transport progress occurs on the caller's
* PLC task; no native worker thread or retained IEC pointer is used. */
uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t unit,uint8_t f,
uint16_t addr,uint16_t q,uint16_t *data,uint32_t cap,uint32_t *status,uint32_t sc,
uint64_t now,uint32_t ct,uint32_t rt,uint32_t retry) {
return ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,now,ct,rt,retry);
}
void ab_shutdown(void) { ab_close_all(); }
/* Called only by the worker while holding the shared engine mutex. */
+2 -2
View File
@@ -1,14 +1,14 @@
#ifndef ATOM_BRIDGE_H
#define ATOM_BRIDGE_H
#include <stdint.h>
/* Fixed pool; serialized calls and worker I/O. IEC instances still belong to one task.
/* Fixed pool; transport progress is synchronous in the owning PLC task. IEC instances still belong to one task.
* Only synchronous pointer access; generation-checked handles, no heap. */
#define AM_STATUS_WORDS 24
#define AM_POOL_SIZE 16
enum { AB_OPEN=1, AB_CLOSE, AB_ENABLE, AB_DISABLE, AB_TICK, AB_SUBMIT,
AB_COPY_READ, AB_RECONNECT, AB_CONFIGURE, AB_ACK, AB_REQUEST_TIMEOUT };
enum { AB_OPEN_RTU=20 };
enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND, AB_WORKER_ERROR };
enum { AB_BAD_HANDLE=256, AB_POOL_FULL, AB_BAD_CAPACITY, AB_BAD_COMMAND };
uint32_t ab_call(uint32_t command,uint32_t handle,uint32_t ip,uint16_t port,
uint8_t unit,uint8_t function,uint16_t address,uint16_t quantity,
uint16_t *data,uint32_t capacity,uint32_t *status,uint32_t status_capacity,
+5 -9
View File
@@ -114,7 +114,11 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table
case AS_CLOSE:as_stop(s);s->used=0;as_snapshot(s,0,out);return 0;
case AS_ENABLE:s->enabled=1;s->suspended=0;s->heartbeat=now;break;
case AS_DISABLE:s->suspended=0;s->enabled=0;as_stop(s);break;
case AS_TICK:s->heartbeat=now;if(s->suspended){s->suspended=0;s->enabled=1;s->retry_at=now;}break;
case AS_TICK:
s->heartbeat=now;
if(s->suspended){s->suspended=0;s->enabled=1;s->retry_at=now;}
as_tick(s,now);
break;
case AS_PUSH:case AS_PULL:
if(table>3 || !q || q>AS_ITEMS || (uint32_t)addr+q>65536u)result=AM_ARGUMENT;
else if(!data || cap<q)result=258;
@@ -133,11 +137,3 @@ uint32_t as_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t table
as_snapshot(s,h,out);return result;
}
void as_shutdown(void){for(unsigned i=0;i<AS_POOL;i++)if(as_slots[i].used){as_stop(&as_slots[i]);as_slots[i].used=0;}}
void as_pump(uint64_t now,uint32_t watchdog_ms){
for(unsigned i=0;i<AS_POOL;i++)if(as_slots[i].used){
as_server *s=&as_slots[i];
if(s->enabled && now-s->heartbeat>=watchdog_ms){as_stop(s);s->enabled=0;s->suspended=1;}
as_tick(s,now);
}
}
-2
View File
@@ -5,5 +5,3 @@ enum { AS_OPEN=30,AS_CLOSE,AS_ENABLE,AS_DISABLE,AS_TICK,AS_PUSH,AS_PULL };
uint32_t as_call(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint16_t,uint16_t,uint16_t *,uint32_t,uint32_t *,uint64_t,uint32_t);
void as_shutdown(void);
#endif
void as_pump(uint64_t now,uint32_t watchdog_ms);
-115
View File
@@ -1,115 +0,0 @@
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include "atom_bridge.h"
#include "atom_mb.h"
#include <pthread.h>
#include <sched.h>
#include <errno.h>
#include <time.h>
#include <string.h>
/* All engine access is serialized. The worker never holds an IEC pointer.
* Network I/O is nonblocking; no mutex is held across sleep or thread join.
* Serial open/ioctl latency is driver dependent: this is not a hard RT API. */
uint32_t ab_dispatch(uint32_t,uint32_t,uint32_t,uint16_t,uint8_t,uint8_t,uint16_t,uint16_t,
uint16_t *,uint32_t,uint32_t *,uint32_t,uint64_t,uint32_t,uint32_t,uint32_t);
void ab_pump(uint64_t,uint32_t);
void ab_close_all(void);
static pthread_mutex_t aw_lifecycle=PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t aw_engine;
static pthread_once_t aw_once=PTHREAD_ONCE_INIT;
static int aw_init_error;
static void aw_init_mutex(void){
pthread_mutexattr_t a;
int e=pthread_mutexattr_init(&a);
if(e){aw_init_error=e;return;}
/* Priority inheritance limits inversion if the PLC task has RT priority. */
e=pthread_mutexattr_setprotocol(&a,PTHREAD_PRIO_INHERIT);
if(e){pthread_mutexattr_destroy(&a);aw_init_error=e;return;}
aw_init_error=pthread_mutex_init(&aw_engine,&a);
pthread_mutexattr_destroy(&a);
}
static pthread_t aw_thread;
static int aw_running,aw_stop,aw_error,aw_cpu=-1;
static uint32_t aw_interval=1,aw_watchdog=2000;
static uint64_t aw_cycles,aw_heartbeat;
static cpu_set_t aw_original_mask;
static uint64_t aw_now(void){
struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);
return (uint64_t)t.tv_sec*1000u+(uint64_t)t.tv_nsec/1000000u;
}
static void *aw_main(void *unused){
(void)unused;
for(;;){
pthread_mutex_lock(&aw_engine);
if(aw_stop){pthread_mutex_unlock(&aw_engine);break;}
aw_heartbeat=aw_now();ab_pump(aw_heartbeat,aw_watchdog);aw_cycles++;
uint32_t delay=aw_interval;
pthread_mutex_unlock(&aw_engine);
struct timespec t={delay/1000u,(long)(delay%1000u)*1000000L};
while(nanosleep(&t,&t)<0 && errno==EINTR){}
}
return NULL;
}
static int aw_start(void){
if(aw_running)return 0;
int e=pthread_getaffinity_np(pthread_self(),sizeof(aw_original_mask),&aw_original_mask);
if(e)return e;
pthread_attr_t attr;struct sched_param priority={0};e=pthread_attr_init(&attr);
if(e)return e;
/* Do not inherit a high-priority real-time PLC task's scheduling policy. */
e=pthread_attr_setinheritsched(&attr,PTHREAD_EXPLICIT_SCHED);
if(!e)e=pthread_attr_setschedpolicy(&attr,SCHED_OTHER);
if(!e)e=pthread_attr_setschedparam(&attr,&priority);
aw_stop=0;
if(!e)e=pthread_create(&aw_thread,&attr,aw_main,NULL);
pthread_attr_destroy(&attr);
if(!e){aw_running=1;aw_cpu=-1;}
return e;
}
static void aw_snapshot(uint32_t *o){
memset(o,0,24*sizeof(*o));o[0]=0x00040000u;o[1]=(uint32_t)aw_running;
o[2]=(uint32_t)aw_error;o[3]=(uint32_t)(aw_cpu+1);o[4]=aw_interval;o[5]=aw_watchdog;
o[6]=(uint32_t)aw_cycles;o[7]=(uint32_t)(aw_cycles>>32);
o[8]=(uint32_t)aw_heartbeat;o[9]=(uint32_t)(aw_heartbeat>>32);
}
uint32_t ab_call(uint32_t cmd,uint32_t h,uint32_t ip,uint16_t port,uint8_t unit,uint8_t f,
uint16_t addr,uint16_t q,uint16_t *data,uint32_t cap,uint32_t *status,uint32_t sc,
uint64_t ignored_now,uint32_t ct,uint32_t rt,uint32_t retry){
(void)ignored_now;
if(!status || sc<AM_STATUS_WORDS)return AB_BAD_CAPACITY;
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_init_error;return AB_WORKER_ERROR;}
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
uint32_t result=0;
if(cmd==1 || cmd==20 || cmd==30 || cmd==40){
aw_error=aw_start();
if(aw_error){memset(status,0,24*sizeof(*status));status[2]=status[7]=(uint32_t)aw_error;result=AB_WORKER_ERROR;goto done;}
}
if(cmd==40){
/* addr=CPU+1 (0 restores inherited mask), ct=pump interval, rt=scan watchdog. */
if(addr>CPU_SETSIZE || ct<1 || ct>20 || rt<100 || rt>60000){result=AM_ARGUMENT;goto worker_status;}
cpu_set_t mask=aw_original_mask;
if(addr){CPU_ZERO(&mask);CPU_SET(addr-1,&mask);}
aw_error=pthread_setaffinity_np(aw_thread,sizeof(mask),&mask);
if(aw_error){result=AB_WORKER_ERROR;goto worker_status;}
aw_cpu=(int)addr-1;aw_interval=ct;aw_watchdog=rt;
worker_status:
aw_snapshot(status);
}else if(cmd==41){aw_snapshot(status);}
else result=ab_dispatch(cmd,h,ip,port,unit,f,addr,q,data,cap,status,sc,aw_now(),ct,rt,retry);
done:
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
return result;
}
void ab_shutdown(void){
pthread_once(&aw_once,aw_init_mutex);
if(aw_init_error)return;
pthread_mutex_lock(&aw_lifecycle);pthread_mutex_lock(&aw_engine);
int join=aw_running;aw_stop=1;pthread_mutex_unlock(&aw_engine);
if(join)pthread_join(aw_thread,NULL);
pthread_mutex_lock(&aw_engine);ab_close_all();aw_running=0;aw_cpu=-1;
aw_error=0;aw_interval=1;aw_watchdog=2000;aw_cycles=0;aw_heartbeat=0;
pthread_mutex_unlock(&aw_engine);pthread_mutex_unlock(&aw_lifecycle);
}
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as tmp:
so=Path(tmp)/'test-transport.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True)
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-I'+str(root/'src'),*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c']],'-o',str(so)],check=True)
lib=C.CDLL(str(so));fn=lib.ab_call
U=C.c_uint32;W=C.c_uint16;B=C.c_uint8
fn.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];fn.restype=U
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env python3
"""Exercise production worker with real sockets/PTYS and deliberately sparse PLC calls."""
import ctypes as C
import os,pty,select,socket,struct,subprocess,tempfile,time,threading
from pathlib import Path
root=Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory() as td:
so=Path(td)/'worker.so'
subprocess.run(['cc','-shared','-fPIC','-std=c11','-Wall','-Wextra','-Werror','-O2','-pthread',
*[str(root/'src'/f) for f in ['atom_mb.c','atom_rtu.c','atom_server.c','atom_bridge.c','atom_worker.c']],'-o',str(so)],check=True)
lib=C.CDLL(str(so));U=C.c_uint32;W=C.c_uint16;B=C.c_uint8
lib.ab_call.argtypes=[U,U,U,W,B,B,W,W,C.POINTER(W),U,C.POINTER(U),U,C.c_uint64,U,U,U];lib.ab_call.restype=U
def call(cmd,h=0,ip=0,port=0,f=3,addr=0,q=1,values=(),ct=100,rt=100,retry=10,unit=1):
data=(W*2000)(*values);status=(U*24)()
result=lib.ab_call(cmd,h,ip,port,unit,f,addr,q,data,2000,status,24,0,ct,rt,retry)
return result,list(status),list(data)
def until(fn,seconds=1):
end=time.monotonic()+seconds
while time.monotonic()<end:
v=fn()
if v:return v
time.sleep(.002)
raise AssertionError('condition timed out')
def configure(watchdog=2000,cpu=-1):
r,s,_=call(40,addr=cpu+1,ct=1,rt=watchdog);assert r==0,(r,s)
def client(port):
r,s,_=call(1,ip=0x7f000001,port=port);assert not r;h=s[0]
assert not call(9,h,rt=100)[0];assert not call(3,h)[0]
until(lambda:call(5,h)[1][1]==3)
return h
def recv_exact(peer,count):
result=b''
while len(result)<count:
v=peer.recv(count-len(result));assert v;result+=v
return result
def server():
r,s,_=call(30,ip=0x7f000001,port=port);assert not r;h=s[0]
for t in range(4):assert not call(35,h,f=t,q=8,values=[0]*8)[0]
assert not call(32,h)[0]
until(lambda:call(34,h)[1][1])
return h
probe=socket.socket();probe.bind(('127.0.0.1',0));port=probe.getsockname()[1];probe.close()
try:
configure();sh=server();ch=client(port)
assert not call(6,ch,q=2)[0]
# The reply deadline is 100 ms; the PLC deliberately does nothing for 250 ms.
time.sleep(.25)
r,s,d=call(7,ch);assert not r and s[3] and not s[4] and d[:2]==[0,0],s
assert call(6,ch)[0]==2 # Completion cannot be overwritten before acknowledgment.
r,m,_=call(12,ch);assert not r and m[21]<100 and m[22]>=100,m
assert call(10,ch)[0]==0
print('PASS worker completes TCP before timeout with PLC absent for 250 ms; result retained and timed')
# Remote write must survive a stale PLC push before the PLC pulls its result.
peer=socket.create_connection(('127.0.0.1',port));peer.settimeout(1)
pdu=struct.pack('>BHH',6,0,4321);peer.sendall(struct.pack('>HHHB',1,0,len(pdu)+1,1)+pdu)
assert recv_exact(peer,12)[7:]==pdu
assert call(35,sh,f=2,q=8,values=[0]*8)[0]==0
assert call(36,sh,f=2,q=8)[2][0]==4321
# After acknowledgment a deliberate application override is permitted.
call(35,sh,f=2,q=8,values=[42]*8);assert call(36,sh,f=2,q=8)[2][0]==42
assert call(35,sh,f=2,addr=1,q=8,values=[0]*8)[0]==1 # explicit recreate for remap
peer.close();call(2,ch);call(31,sh)
print('PASS autonomous TCP slave and lossless remote-write handoff')
# Real serial PTY, again no PLC ticks after submission.
master,slave=pty.openpty();path=os.ttyname(slave);os.close(slave)
r,s,_=call(20,ip=9600,unit=0,f=8,addr=1,q=len(path),ct=0,values=list(path.encode()));assert not r;h=s[0]
call(9,h,rt=100);call(3,h);until(lambda:call(5,h)[1][1]==3)
call(6,h,unit=1,f=3,q=1)
assert select.select([master],[],[],1)[0];request=os.read(master,256);assert len(request)==8
def crc(p):
c=65535
for b in p:
c^=b
for _ in range(8):c=(c>>1)^(0xa001 if c&1 else 0)
return c
p=bytes([1,3,2,0,77]);os.write(master,p+struct.pack('<H',crc(p)))
time.sleep(.25);r,s,d=call(7,h);assert not r and s[3] and d[0]==77,s
call(2,h);os.close(master)
print('PASS RTU completion while PLC does not scan for 250 ms')
# Watchdog cancels an in-flight write without replaying it on resume.
configure(100)
listener=socket.socket();listener.bind(('127.0.0.1',0));listener.listen();listener.settimeout(1)
h=client(listener.getsockname()[1]);peer,_=listener.accept();peer.settimeout(1)
call(11,h,rt=1000);assert not call(6,h,f=16,q=1,values=[9])[0]
packet=recv_exact(peer,15);assert packet[7]==16
time.sleep(.16)
# command 12 is read-only and does not renew the scan lease.
r,s,_=call(12,h);assert not r and s[1]==0 and s[5]==10 and s[9]==1,s
assert peer.recv(1)==b''
call(5,h);peer2,_=listener.accept();peer2.settimeout(.03)
try:assert not peer2.recv(1), 'write replayed'
except socket.timeout:pass
call(2,h);peer.close();peer2.close();listener.close()
print('PASS scan watchdog closes transport, preserves uncertain write, resumes without replay')
# Server closes its listener when scanning ceases and recovers on refresh.
configure(100);sh=server();time.sleep(.16)
probe=socket.socket();probe.settimeout(.1);assert probe.connect_ex(('127.0.0.1',port))!=0;probe.close()
call(34,sh);until(lambda:call(34,sh)[1][1]);call(31,sh)
print('PASS server watchdog and restart')
# CPU selection, failure reporting, and restoring inherited mask.
configure(cpu=min(os.sched_getaffinity(0)))
r,s,_=call(41);assert not r and s[3]==min(os.sched_getaffinity(0))+1
assert call(40,addr=65535,ct=1,rt=2000)[0]==1
configure()
failures=[]
def reader():
for _ in range(200):
if call(41)[0]:failures.append('snapshot failure')
threads=[threading.Thread(target=reader) for _ in range(4)]
for t in threads:t.start()
for t in threads:t.join()
assert not failures
print('PASS configurable affinity, invalid arguments, concurrent snapshot access')
for _ in range(20):
lib.ab_shutdown();assert call(41)[1][1]==0;configure();assert call(41)[1][1]==1
print('PASS repeated joined shutdown/restart')
finally:lib.ab_shutdown()