Add receiver diagnostics and remote PicCap handover

This commit is contained in:
matze19999 committed 2026-10-06 18:35:56 +02:00
1 parent ae01879d45
commit 42aeaf56ea
19 files changed
+322 -50

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
#ifndef HYPERDRM_DIAGNOSTICS_H
#define HYPERDRM_DIAGNOSTICS_H
/* An independent nonblocking connection. Tests never send colors or acquire
* capture. Work per timer iteration is bounded to keep output responsive. */
typedef enum {TEST_IDLE,TEST_CONNECT,TEST_SEND,TEST_LOGIN,TEST_LAYOUT,TEST_DONE,TEST_FAILED} TestStage;
static struct {
int fd;TestStage stage,after_send;TargetConfig target;
uint64_t started,deadline,elapsed;unsigned leds;
char request[384],error[128];size_t sent,used;char *response;
} connection_test={.fd=-1};
static const char *test_stage_name(void){
static const char *names[]={"Not tested","Connecting","Sending request","Checking authentication","Reading LED layout","Passed","Failed"};
return names[connection_test.stage];
}
static bool test_busy(void){return connection_test.stage>=TEST_CONNECT && connection_test.stage<=TEST_LAYOUT;}
static void test_close(void){if(connection_test.fd>=0)close(connection_test.fd);connection_test.fd=-1;free(connection_test.response);connection_test.response=NULL;memset(connection_test.request,0,sizeof(connection_test.request));memset(connection_test.target.token,0,sizeof(connection_test.target.token));}
static void test_finish(const char *error,uint64_t now){
test_close();connection_test.elapsed=now-connection_test.started;
connection_test.stage=error?TEST_FAILED:TEST_DONE;
snprintf(connection_test.error,sizeof(connection_test.error),"%s",error?error:"");
}
static void test_request(bool login,uint64_t now){
if(login)snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"authorize\",\"subcommand\":\"login\",\"token\":\"%s\"}\n",connection_test.target.token);
else snprintf(connection_test.request,sizeof(connection_test.request),"{\"command\":\"serverinfo\"}\n");
connection_test.sent=connection_test.used=0;connection_test.after_send=login?TEST_LOGIN:TEST_LAYOUT;
connection_test.stage=TEST_SEND;connection_test.deadline=now+3000;
}
static bool test_begin(const TargetConfig *target,uint64_t now){
if(test_busy())return false;
test_close();memset(&connection_test,0,sizeof(connection_test));connection_test.fd=-1;
connection_test.target=*target;connection_test.started=now;
connection_test.response=malloc(2*1024*1024+1);
if(!connection_test.response){test_finish("Cannot allocate connection test buffer",now);return true;}
connection_test.fd=target_open(target);
if(connection_test.fd<0){test_finish("Cannot open receiver connection",now);return true;}
connection_test.stage=TEST_CONNECT;connection_test.deadline=now+3000;return true;
}
static void test_tick(PlaybackJson *json,uint64_t now){
if(!test_busy())return;
if(now>=connection_test.deadline){test_finish("Receiver timed out",now);return;}
int fd=connection_test.fd;
if(connection_test.stage==TEST_CONNECT){
struct pollfd p={fd,POLLOUT,0};int error=0;socklen_t size=sizeof(error);
if(poll(&p,1,0)<=0)return;
if(getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){test_finish("Receiver connection refused or unreachable",now);return;}
test_request(connection_test.target.token[0]!=0,now);return;
}
if(connection_test.stage==TEST_SEND){
size_t length=strlen(connection_test.request);
ssize_t n=send(fd,connection_test.request+connection_test.sent,length-connection_test.sent,MSG_NOSIGNAL);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
connection_test.sent+=(size_t)n;
if(connection_test.sent==length){connection_test.stage=connection_test.after_send;connection_test.deadline=now+3000;memset(connection_test.request,0,sizeof(connection_test.request));}
return;
}
size_t available=2*1024*1024-connection_test.used;if(available>65536)available=65536;
if(!available){test_finish("Receiver response exceeds 2 MiB",now);return;}
ssize_t n=recv(fd,connection_test.response+connection_test.used,available,0);
if(n<0 && (errno==EAGAIN || errno==EWOULDBLOCK || errno==EINTR))return;
if(n<=0){test_finish("Receiver closed the test connection",now);return;}
connection_test.used+=(size_t)n;connection_test.response[connection_test.used]=0;
char *end=strchr(connection_test.response,'\n');if(!end)return;*end=0;
void *root=json->parse(connection_test.response);bool success=playback_true(json,root,"success");if(root)json->put(root);
if(!success){test_finish(connection_test.stage==TEST_LOGIN?"Receiver authentication failed":"Receiver rejected serverinfo; check authorization",now);return;}
if(connection_test.stage==TEST_LOGIN){test_request(false,now);return;}
LedBlend *mapping=NULL;unsigned count=0;
if(!layout_from_json(connection_test.response,&mapping,&count)){test_finish("Receiver returned an invalid or empty LED layout",now);return;}
free(mapping);connection_test.leds=count;test_finish(NULL,now);
}
#endif
+20 -8
View File
@@ -17,6 +17,8 @@ static bool (*cancel_call)(void*,unsigned long,LSError*);
static unsigned long piccap_token,media_token;
static uint64_t piccap_deadline,media_deadline;
static unsigned clear_samples;
static TargetConfig piccap_target;
static char piccap_error[128];
static bool piccap_request(int command);
static inline bool piccap_installed(void){return access(PICCAP_BINARY,X_OK)==0;}
static bool handover_save(bool enabled){
@@ -39,7 +41,7 @@ static bool media_reply(void *h,void *m,void *ctx){
if(protection==PLAYBACK_CLEAR){if(clear_samples<2)clear_samples++;}else clear_samples=0;
return true;
}
/* Commands: 0 status, 1 start, 2 stop. Confirm stopped status before VT reuse. */
/* Commands: 0 status, 1 start, 2 stop, 3 getSettings, 4 setSettings. Confirm stopped status before VT reuse. */
static bool piccap_reply(void *h,void *m,void *ctx){
(void)h;int command=(int)(intptr_t)ctx;piccap_busy=false;piccap_token=0;
void *root=playback_json.parse(payload(m));bool ok=playback_true(&playback_json,root,"returnValue");
@@ -55,7 +57,16 @@ static bool piccap_reply(void *h,void *m,void *ctx){
if(!retries.paused)retries.due=milliseconds();
}
}else if(!piccap_installed()){piccap_owned=false;capture_source=SOURCE_HISTOGRAM;piccap_connected=false;}
}else if(command==3 && capture_source==SOURCE_PICCAP_STARTING){
void *port=playback_field(&playback_json,root,"port"),*socket=playback_field(&playback_json,root,"unix-socket");
bool same=ok && playback_is(&playback_json,root,"address",target_piccap_host(&piccap_target)) && port && playback_json.type(port)==3 && strtoul(playback_json.string(port),NULL,10)==piccap_target.piccap_port && socket && playback_json.type(socket)==1 && !playback_json.boolean(socket);
if(ok){if(!piccap_request(same?1:4)){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap endpoint request failed");}}
else {capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"Cannot read PicCap settings");}
}else if(command==4){
if(!ok){capture_source=SOURCE_PICCAP_STOPPING;snprintf(piccap_error,sizeof(piccap_error),"PicCap rejected receiver settings");}
/* setSettings restarts PicCap itself; status confirms ownership. */
}else if(command==1 && !ok){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=milliseconds()+10000;}
if(command>=3 && capture_source==SOURCE_PICCAP_STOPPING)handover_cooldown=milliseconds()+10000;
if(root)playback_json.put(root);
next_piccap_probe=milliseconds()+500;
if(command==2)piccap_request(0);
@@ -63,9 +74,10 @@ static bool piccap_reply(void *h,void *m,void *ctx){
}
static bool piccap_request(int command){
if(piccap_busy)return false;
const char *uri=command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
const char *uri=command==3?"luna://org.webosbrew.piccap.service/getSettings":command==4?"luna://org.webosbrew.piccap.service/setSettings":command==1?"luna://org.webosbrew.piccap.service/start":command==2?"luna://org.webosbrew.piccap.service/stop":"luna://org.webosbrew.piccap.service/status";
LSError e;error_init(&e);
piccap_busy=call_once(service_handle,uri,"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
char data[160];snprintf(data,sizeof(data),"{\"address\":\"%s\",\"port\":%u,\"unix-socket\":false}",target_piccap_host(&piccap_target),piccap_target.piccap_port);
piccap_busy=call_once(service_handle,uri,command==4?data:"{}",piccap_reply,(void*)(intptr_t)command,&piccap_token,&e);
piccap_deadline=milliseconds()+3000;
error_free(&e);if(command)piccap_action_at=milliseconds();return piccap_busy;
}
@@ -73,20 +85,20 @@ static bool piccap_request(int command){
static bool handover_tick(void){
uint64_t now=milliseconds();
if(media_busy && now>=media_deadline){LSError e;error_init(&e);cancel_call(service_handle,media_token,&e);error_free(&e);media_busy=false;media_token=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;}
if(piccap_busy && now>=piccap_deadline){LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(piccap_busy && now>=piccap_deadline){snprintf(piccap_error,sizeof(piccap_error),"PicCap request timed out");LSError e;error_init(&e);cancel_call(service_handle,piccap_token,&e);error_free(&e);piccap_busy=false;piccap_token=0;capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(handover_enabled && piccap_installed() && !retries.paused && playback_json.lib && !media_busy && now>=next_media_probe){
LSError e;error_init(&e);
media_busy=call_once(service_handle,"luna://com.webos.media/getActivePipelines","{}",media_reply,NULL,&media_token,&e);
media_deadline=now+3000;
error_free(&e);next_media_probe=now+1000;
}
bool clear=handover_enabled && target_piccap_local(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
bool clear=handover_enabled && target_piccap_compatible(&target) && piccap_installed() && !retries.paused && !retries.blocked &&
protection==PLAYBACK_CLEAR && clear_samples>=2 && now-protection_at<3000 && now>=handover_cooldown;
if(capture_source==SOURCE_HISTOGRAM && clear){
stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
if(!piccap_request(1)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
piccap_target=target;piccap_error[0]=0;stop_capture();retries.due=0;piccap_owned=true;capture_source=SOURCE_PICCAP_STARTING;
if(!piccap_request(3)){capture_source=SOURCE_PICCAP_STOPPING;handover_cooldown=now+10000;}
}
if(piccap_owned && !clear && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(piccap_owned && (!clear || strcmp(piccap_target.host,target.host) || piccap_target.piccap_port!=target.piccap_port) && capture_source!=SOURCE_PICCAP_STOPPING){capture_source=SOURCE_PICCAP_STOPPING;next_piccap_probe=0;}
if(piccap_owned && !piccap_busy && now>=next_piccap_probe){
if(capture_source==SOURCE_PICCAP_STOPPING)piccap_request(2);else piccap_request(0);
next_piccap_probe=now+1000;
+16 -6
View File
@@ -26,6 +26,7 @@
#include "retry.h"
#include "target.h"
#include "spatial_image.h"
#include "diagnostics.h"
#define SERVICE "org.webosbrew.histogramhyperion.service"
#define PRIORITY 149
typedef struct {int code;char *message;const char *file;int line;const char *func;void *padding;unsigned long magic;} LSError;
@@ -64,7 +65,7 @@ static LedBlend *led_blends;
static bool interpolation_enabled;
static RetryState retries;
static unsigned capture_recoveries;
static uint64_t recovery_started,last_recovery_ms;
static uint64_t recovery_started,last_recovery_ms,last_output_at;
static uint64_t milliseconds(void){struct timespec t;clock_gettime(CLOCK_MONOTONIC,&t);return (uint64_t)t.tv_sec*1000+t.tv_nsec/1000000;}
static char last_error[256];
static volatile sig_atomic_t revoked,quitting;
@@ -189,15 +190,23 @@ static bool settings(void *h,void *m,void *ctx){
if(!playback_json.lib || !target_parse(&playback_json,payload(m),&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Enter a valid IPv4/IPv6 address, integer port (1-65535), receiver and optional API token\"}");
if(!target_save(TARGET_PREF,&next))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Could not save receiver settings\"}");
bool active=capture || piccap_owned || retries.due;
stop_capture();target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
stop_capture();test_close();connection_test.stage=TEST_IDLE;target=next;last_error[0]=0;protection=PLAYBACK_UNKNOWN;clear_samples=0;
if(active && !retries.paused && !retries.blocked)retries.due=milliseconds();
return respond(h,m,"{\"returnValue\":true}");
}
static bool test_connection(void *h,void *m,void *ctx){
(void)ctx;
if(strcmp(payload(m),"{}") || !playback_json.lib)return respond(h,m,"{\"returnValue\":false,\"errorText\":\"Test requires an empty object and JSON-C\"}");
if(!test_begin(&target,milliseconds()))return respond(h,m,"{\"returnValue\":false,\"errorText\":\"A connection test is already running\"}");
return respond(h,m,"{\"returnValue\":true,\"testing\":true}");
}
static bool status(void *h,void *m,void *ctx){
(void)ctx;char json[4096],safe[512],endpoint[80];size_t n=0;
snprintf(endpoint,sizeof(endpoint),strchr(target.host,':')?"[%s]:%u":"%s:%u",target.host,target.port);
for(size_t i=0;last_error[i] && n+2<sizeof(safe);i++){char c=last_error[i];if(c=='"'||c=='\\')safe[n++]='\\';if((unsigned char)c>=32)safe[n++]=c;}safe[n]=0;
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"version\":\"0.8.1\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_local(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
uint64_t now=milliseconds();
int used=snprintf(json,sizeof(json),"{\"returnValue\":true,\"piccapPort\":%u,\"connectionTest\":{\"state\":\"%s\",\"ledCount\":%u,\"elapsedMs\":%llu,\"error\":\"%s\"},\"lastOutputAgeMs\":%lld,\"retryInMs\":%llu,\"captureState\":\"%s\",\"piccapError\":\"%s\",",target.piccap_port,test_stage_name(),connection_test.leds,(unsigned long long)connection_test.elapsed,connection_test.error,last_output_at?(long long)(now-last_output_at):-1,(unsigned long long)(retries.due>now?retries.due-now:0),capture_source==SOURCE_PICCAP_STARTING?"Starting PicCap":capture_source==SOURCE_PICCAP_STOPPING?"Stopping PicCap":piccap_owned?"PicCap":capture?"Histogram":"Stopped",piccap_error);
used+=snprintf(json+used,sizeof(json)-used,"\"version\":\"0.9.0\",\"running\":%s,\"colorValid\":%s,\"frames\":%lu,\"captureRecoveries\":%u,\"lastRecoveryMs\":%llu,\"endpoint\":\"%s\",\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"tokenSet\":%s,\"piccapTargetCompatible\":%s,\"priority\":%d,\"uid\":%u,\"drmVerified\":false,\"mappingVerified\":false,\"outputColor\":[%u,%u,%u],\"error\":\"%s\",\"regions\":[",(capture||capture_source==SOURCE_PICCAP)?"true":"false",color_valid?"true":"false",frames,capture_recoveries,(unsigned long long)last_recovery_ms,endpoint,target.host,target.port,target.receiver,target.token[0]?"true":"false",target_piccap_compatible(&target)?"true":"false",PRIORITY,(unsigned)geteuid(),sent_rgb[0],sent_rgb[1],sent_rgb[2],safe);
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s[%u,%u,%u]",i?",":"",rgb[i][0],rgb[i][1],rgb[i][2]);
used+=snprintf(json+used,sizeof(json)-used,"],\"ledCount\":%u,\"spatialOutput\":%s,\"blackPercent\":[",led_count,led_count?"true":"false");
for(int i=0;i<16;i++)used+=snprintf(json+used,sizeof(json)-used,"%s%u",i?",":"",black_percent[i]);
@@ -205,6 +214,7 @@ static bool status(void *h,void *m,void *ctx){
}
static int tick(void *unused){
(void)unused;
test_tick(&playback_json,milliseconds());
if(handover_tick())return 1;
if(!capture){
retry_update(&retries,boot_enabled() && geteuid()==0,false,milliseconds());
@@ -265,7 +275,7 @@ static int tick(void *unused){
while(used+1<sizeof(answer)){ssize_t len=recv(output,answer+used,sizeof(answer)-used-1,0);if(len<=0)break;used+=(size_t)len;answer[used]=0;if(strchr(answer,'\n')){complete=true;break;}}
if(!complete){set_error("Receiver did not acknowledge color data");stop_capture();return 1;}
if(!strstr(answer,"\"success\":true")&&!strstr(answer,"\"success\": true")){set_error("Receiver rejected the color command");stop_capture();return 1;}
color_valid=true;frames++;return 1;
color_valid=true;last_output_at=milliseconds();frames++;return 1;
}
static void *symbol(void *lib,const char *name){void *s=dlsym(lib,name);if(!s){fprintf(stderr,"Missing symbol %s\n",name);exit(2);}return s;}
int main(void){
@@ -287,13 +297,13 @@ int main(void){
unsigned (*timer)(unsigned,int(*)(void*),void*)=symbol(glib,"g_timeout_add");
void (*loop_unref)(void*)=symbol(glib,"g_main_loop_unref");
void *handle=NULL,*loop=loop_new(NULL,0);LSError e;error_init(&e);
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{NULL,NULL,0}};
Method methods[]={{"start",start,0},{"stop",stop,0},{"status",status,0},{"autostart",autostart,0},{"handover",handover,0},{"settings",settings,0},{"testConnection",test_connection,0},{NULL,NULL,0}};
if(!reg(SERVICE,&handle,&e)||!category(handle,"/",methods,NULL,NULL,&e)||!attach(handle,loop,&e)){fprintf(stderr,"Luna registration failed: %s\n",e.message?e.message:"unknown");error_free(&e);return 3;}
service_handle=handle;
if(!boot_enabled())retry_pause(&retries);
timer(100,tick,NULL);
while(!quitting){iterate(NULL,0);usleep(10000);}
stop_capture();
test_close();stop_capture();
if(piccap_owned){
handover_enabled=false;retry_pause(&retries);
uint64_t deadline=milliseconds()+4000;
+20 -7
View File
@@ -11,12 +11,15 @@
#include <unistd.h>
#include "playback.h"
#define TARGET_PREF "/media/developer/apps/usr/palm/services/org.webosbrew.histogramhyperion.service/target.json"
typedef struct {char host[46],receiver[16],token[257];unsigned port;} TargetConfig;
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444};
typedef struct {char host[46],receiver[16],token[257];unsigned port,piccap_port;} TargetConfig;
static TargetConfig target={.host="127.0.0.1",.receiver="Hyperion",.port=19444,.piccap_port=19400};
static inline bool target_piccap_local(const TargetConfig *t){return t->port==19444 && (!strcmp(t->host,"127.0.0.1") || !strcmp(t->host,"::1"));}
static inline bool target_piccap_compatible(const TargetConfig *t){unsigned char address[4];return !strcmp(t->host,"::1") || inet_pton(AF_INET,t->host,address)==1;}
static inline const char *target_piccap_host(const TargetConfig *t){return !strcmp(t->host,"::1")?"127.0.0.1":t->host;}
static bool target_valid(const TargetConfig *t){
unsigned char address[16];
if(inet_pton(AF_INET,t->host,address)!=1 && inet_pton(AF_INET6,t->host,address)!=1)return false;
if(!t->piccap_port || t->piccap_port>65535)return false;
if(!t->port || t->port>65535 || (strcmp(t->receiver,"Hyperion") && strcmp(t->receiver,"HyperHDR")))return false;
for(const unsigned char *p=(const unsigned char*)t->token;*p;p++)
if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='-'||*p=='_'||*p=='.'))return false;
@@ -37,6 +40,11 @@ static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
void *port=playback_field(j,root,"port");
if(!port || j->type(port)!=3)valid=false;
else {char *end=NULL;errno=0;long n=strtol(j->string(port),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.port=(unsigned)n;}
void *flat=NULL;
if(j->type(root)==4 && j->get(root,"piccapPort",&flat)){
if(j->type(flat)!=3)valid=false;
else {char *end=NULL;errno=0;long n=strtol(j->string(flat),&end,10);if(errno || !end || *end || n<1 || n>65535)valid=false;else next.piccap_port=(unsigned)n;}
}
void *clear=playback_field(j,root,"clearToken");
if(clear){if(j->type(clear)!=1)valid=false;else if(j->boolean(clear))next.token[0]=0;}
valid=valid && target_valid(&next);j->put(root);if(valid)*out=next;return valid;
@@ -44,7 +52,7 @@ static bool target_parse(PlaybackJson *j,const char *text,TargetConfig *out){
static bool target_save(const char *path,const TargetConfig *t){
if(!target_valid(t))return false;
char tmp[512],data[512];if(snprintf(tmp,sizeof(tmp),"%s.tmp",path)>=(int)sizeof(tmp))return false;
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->receiver,t->token);
int n=snprintf(data,sizeof(data),"{\"host\":\"%s\",\"port\":%u,\"piccapPort\":%u,\"receiver\":\"%s\",\"token\":\"%s\"}\n",t->host,t->port,t->piccap_port,t->receiver,t->token);
int fd=open(tmp,O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);if(fd<0)return false;
bool ok=write(fd,data,n)==n && fsync(fd)==0;if(close(fd)!=0)ok=false;
if(ok)ok=rename(tmp,path)==0;
@@ -56,7 +64,7 @@ static bool target_load(PlaybackJson *j,const char *path,TargetConfig *t){
char data[1024];ssize_t n=read(fd,data,sizeof(data)-1);close(fd);if(n<=0 || n==(ssize_t)sizeof(data)-1)return false;
data[n]=0;return target_parse(j,data,t);
}
static int target_connect(const TargetConfig *t){
static int target_open(const TargetConfig *t){
if(!target_valid(t))return -1;
struct sockaddr_storage storage={0};socklen_t length;int family;
struct sockaddr_in *v4=(void*)&storage;struct sockaddr_in6 *v6=(void*)&storage;
@@ -66,10 +74,15 @@ static int target_connect(const TargetConfig *t){
int flags=fcntl(fd,F_GETFL,0);if(flags<0 || fcntl(fd,F_SETFL,flags|O_NONBLOCK)<0){close(fd);return -1;}
if(connect(fd,(void*)&storage,length)!=0){
if(errno!=EINPROGRESS){close(fd);return -1;}
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
}
if(fcntl(fd,F_SETFL,flags)<0){close(fd);return -1;}
return fd;
}
static int target_connect(const TargetConfig *t){
int fd=target_open(t);if(fd<0)return -1;
struct pollfd p={.fd=fd,.events=POLLOUT};int error=0;socklen_t size=sizeof(error);
if(poll(&p,1,1000)<=0 || getsockopt(fd,SOL_SOCKET,SO_ERROR,&error,&size)!=0 || error){close(fd);return -1;}
int flags=fcntl(fd,F_GETFL,0);
if(flags<0 || fcntl(fd,F_SETFL,flags & ~O_NONBLOCK)<0){close(fd);return -1;}
struct timeval timeout={1,0};setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
return fd;
}
+42
View File
@@ -0,0 +1,42 @@
#define _DEFAULT_SOURCE
#include <assert.h>
#include <stdint.h>
#include <sys/wait.h>
#include "target.h"
#include "layout.h"
#include "diagnostics.h"
static void fixture(PlaybackJson *json,int kind){
int server=socket(AF_INET,SOCK_STREAM,0);assert(server>=0);
struct sockaddr_in address={.sin_family=AF_INET,.sin_addr={htonl(INADDR_LOOPBACK)}};
assert(bind(server,(void*)&address,sizeof(address))==0 && listen(server,1)==0);
socklen_t length=sizeof(address);assert(getsockname(server,(void*)&address,&length)==0);
TargetConfig config=target;config.port=ntohs(address.sin_port);if(kind==1)strcpy(config.token,"fixture-token");
pid_t child=fork();assert(child>=0);
if(!child){
int fd=accept(server,NULL,NULL);if(fd<0)_exit(2);char request[512]={0};size_t used=0;
while(used+1<sizeof(request)){ssize_t n=read(fd,request+used,sizeof(request)-used-1);if(n<=0)_exit(3);used+=(size_t)n;if(strchr(request,'\n'))break;}
if(!strstr(request,kind==1?"authorize":"serverinfo") || strstr(request,"color") || strstr(request,"image"))_exit(4);
const char *response=kind==1?"{\"success\":false}\n":kind==2?"{\"success\":true,\"info\":{\"leds\":[]}}\n":"{\"success\":true,\"info\":{\"leds\":[{\"hmin\":0,\"hmax\":1,\"vmin\":0,\"vmax\":0.1}]}}\n";
if(write(fd,response,7)!=7)_exit(5);
usleep(20000);
if(write(fd,response+7,strlen(response)-7)!=(ssize_t)strlen(response)-7)_exit(6);
close(fd);close(server);_exit(0);
}
assert(test_begin(&config,1000));assert(!test_begin(&config,1000));
for(unsigned i=0;test_busy() && i<400;i++){test_tick(json,1000+i*10);usleep(1000);}
assert(!test_busy());assert(connection_test.stage==(kind?TEST_FAILED:TEST_DONE));
if(!kind)assert(connection_test.leds==1 && !connection_test.error[0]);
if(kind==1)assert(strstr(connection_test.error,"authentication"));
if(kind==2)assert(strstr(connection_test.error,"layout"));
int status;assert(waitpid(child,&status,0)==child && WIFEXITED(status) && WEXITSTATUS(status)==0);
close(server);assert(test_begin(&config,10000));test_tick(json,10010);assert(connection_test.stage==TEST_FAILED);
}
int main(void){
(void)target_save;(void)target_load;(void)target_connect;(void)test_stage_name;
PlaybackJson json={0};assert(playback_json_init(&json));
fixture(&json,0);fixture(&json,1);fixture(&json,2);
int peers[2];assert(socketpair(AF_UNIX,SOCK_STREAM,0,peers)==0);
connection_test.fd=peers[0];connection_test.stage=TEST_LAYOUT;connection_test.started=20000;connection_test.deadline=23000;
test_tick(&json,23000);assert(connection_test.stage==TEST_FAILED && strstr(connection_test.error,"timed out"));close(peers[1]);
puts("PASS: read-only connection test, fragmented response, layout validation, rejected token, connection refusal and timeout");return 0;
}
+24 -9
View File
@@ -10,45 +10,60 @@ typedef bool (*Callback)(void*,void*,void*);
static RetryState retries;
static uint64_t clock_ms=10000;
static unsigned stopped,called,canceled;
static char last_uri[128];
static char last_uri[128],last_data[256];
static uint64_t milliseconds(void){return clock_ms;}
static void stop_capture(void){stopped++;}
static const char *payload(void *m){return m;}
static void error_init(LSError *e){(void)e;}
static void error_free(LSError *e){(void)e;}
#define PICCAP_BINARY "/bin/sh"
static int target;
static bool target_piccap_local(const int *t){(void)t;return true;}
#include "target.h"
#include "handover.h"
static bool mock_call(void *h,const char *uri,const char *data,Callback cb,void *ctx,unsigned long *token,LSError *e){
(void)h;(void)data;(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
(void)h;snprintf(last_data,sizeof(last_data),"%s",data);(void)cb;(void)ctx;(void)e;*token=++called;snprintf(last_uri,sizeof(last_uri),"%s",uri);return true;
}
static bool mock_cancel(void *h,unsigned long token,LSError *e){(void)h;(void)token;(void)e;canceled++;return true;}
static void configured(void){
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
assert(strstr(last_uri,"/start"));
}
static void answer(int cmd,bool running){
char json[128];snprintf(json,sizeof(json),"{\"returnValue\":true,\"isRunning\":%s,\"connected\":true}",running?"true":"false");
piccap_reply(NULL,json,(void*)(intptr_t)cmd);
}
int main(void){
(void)target_save;(void)target_load;(void)target_connect;
(void)handover_save;(void)handover_load;
(void)retry_failure;(void)retry_take;(void)retry_update;
assert(playback_json_init(&playback_json));call_once=mock_call;cancel_call=mock_cancel;
handover_enabled=true;protection=PLAYBACK_PROTECTED;protection_at=clock_ms;next_media_probe=UINT64_MAX;
assert(!handover_tick());assert(called==0 && stopped==0);
protection=PLAYBACK_CLEAR;clear_samples=1;assert(!handover_tick());assert(called==0);
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/start"));
answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
clear_samples=2;assert(handover_tick());assert(stopped==1 && strstr(last_uri,"/getSettings"));
configured();answer(1,false);clock_ms+=600;assert(handover_tick());assert(strstr(last_uri,"/status"));answer(0,true);
assert(capture_source==SOURCE_PICCAP && piccap_connected);
protection=PLAYBACK_PROTECTED;assert(handover_tick());assert(strstr(last_uri,"/stop"));
answer(2,true);assert(strstr(last_uri,"/status"));assert(piccap_owned);answer(0,false);
assert(!piccap_owned && capture_source==SOURCE_HISTOGRAM && retries.due==clock_ms);
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());answer(1,true);
protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;assert(handover_tick());configured();answer(1,true);
retry_pause(&retries);assert(handover_tick());assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
assert(!piccap_owned && retries.due==0);assert(!handover_tick());
retry_reset(&retries);assert(handover_tick());answer(1,true);handover_enabled=false;
retry_reset(&retries);assert(handover_tick());configured();answer(1,true);handover_enabled=false;
assert(handover_tick());answer(2,false);answer(0,false);assert(!piccap_owned);
handover_enabled=true;protection_at=clock_ms;assert(handover_tick());
clock_ms+=3100;assert(handover_tick());assert(canceled==1 && capture_source==SOURCE_PICCAP_STOPPING);
assert(strstr(last_uri,"/stop"));answer(2,false);answer(0,false);
assert(!piccap_owned);assert(!handover_tick());
puts("PicCap handover tests passed");return 0;
retries.paused=false;handover_cooldown=0;protection=PLAYBACK_CLEAR;protection_at=clock_ms;clear_samples=2;
strcpy(target.host,"10.1.0.100");target.piccap_port=19446;
assert(handover_tick());assert(strstr(last_uri,"/getSettings"));
piccap_reply(NULL,"{\"returnValue\":true,\"address\":\"127.0.0.1\",\"port\":19400,\"unix-socket\":false}",(void*)(intptr_t)3);
assert(strstr(last_uri,"/setSettings"));assert(!strcmp(last_data,"{\"address\":\"10.1.0.100\",\"port\":19446,\"unix-socket\":false}"));
answer(4,true);clock_ms+=600;handover_tick();answer(0,true);assert(capture_source==SOURCE_PICCAP);
target.piccap_port=19400;assert(handover_tick());assert(capture_source==SOURCE_PICCAP_STOPPING);answer(2,false);answer(0,false);
handover_cooldown=0;protection_at=clock_ms;assert(handover_tick());
piccap_reply(NULL,"{\"returnValue\":false}",(void*)(intptr_t)3);
assert(capture_source==SOURCE_PICCAP_STOPPING && piccap_error[0]);
handover_tick();answer(2,false);answer(0,false);assert(!piccap_owned);
puts("PicCap handover tests passed: source ownership, remote endpoint, target change and configuration failure");return 0;
}
+6 -1
View File
@@ -5,12 +5,17 @@
#include "target.h"
int main(void){
PlaybackJson j={0};assert(playback_json_init(&j));TargetConfig t=target;
assert(target_valid(&t));assert(target_piccap_local(&t));
assert(target_valid(&t));assert(t.piccap_port==19400);assert(target_piccap_local(&t));
assert(target_parse(&j,"{\"host\":\"10.1.0.91\",\"port\":19445,\"receiver\":\"HyperHDR\",\"token\":\"test-token\"}",&t));
assert(t.port==19445 && !strcmp(t.receiver,"HyperHDR") && !strcmp(t.token,"test-token"));assert(!target_piccap_local(&t));
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"Hyperion\"}",&t));assert(!strcmp(t.token,"test-token"));
const char *bad[]={"{}","[]","{\"host\":\"localhost\",\"port\":19444,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":0,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":65536,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444.5,\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":\"19444\",\"receiver\":\"HyperHDR\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"Other\"}","{\"host\":\"127.0.0.1\",\"port\":19444,\"receiver\":\"HyperHDR\",\"token\":\"bad\\\"token\"}"};
for(unsigned i=0;i<sizeof(bad)/sizeof(*bad);i++){TargetConfig before=t;assert(!target_parse(&j,bad[i],&t));assert(!memcmp(&t,&before,sizeof(t)));}
assert(target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":65535,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":0,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
assert(target_parse(&j,"{\"host\":\"::1\",\"port\":65535,\"receiver\":\"HyperHDR\"}",&t));assert(t.piccap_port==65535);
assert(!target_parse(&j,"{\"host\":\"127.0.0.1\",\"port\":19444,\"piccapPort\":null,\"receiver\":\"HyperHDR\"}",&t));
TargetConfig v6=t;strcpy(v6.host,"2001:db8::1");assert(!target_piccap_compatible(&v6));strcpy(v6.host,"::1");assert(target_piccap_compatible(&v6) && !strcmp(target_piccap_host(&v6),"127.0.0.1"));
const char *file="/tmp/hyperdrm-target-test.json";assert(access(file,F_OK)!=0);assert(access("/tmp/hyperdrm-target-test.json.tmp",F_OK)!=0);
assert(target_save(file,&t));struct stat st;assert(stat(file,&st)==0 && (st.st_mode&0777)==0600);
TargetConfig restored=target;assert(target_load(&j,file,&restored));assert(!strcmp(restored.host,"::1") && restored.port==65535 && !strcmp(restored.token,"test-token"));