Commit 25813b22 authored by Oleg Dzhimiev's avatar Oleg Dzhimiev

1. Built in glibc 2.28 and gcc 8.2.0:

  mkfifo/fopen/fread/fwrite kind of worked with glibc 2.26 and gcc 7.2.0.
  With 2.28 named-pipe reading with fread would read the pipe only once for
  some reason - probably because it stopped being a 'blocking' call
  To fix - switched to open/read/poll combination - so none of glibc functions
  is used.

2. camogm_fifo_reader - test program - use with *_writer or just 'echo'
3. camogm_fifo_writer - test program
parent 64eeb889
GUIDIR = camogmgui GUIDIR = camogmgui
PROGS = camogm PROGS = camogm
TEST_PROG = camogm_test TEST_PROG = camogm_test
TEST_PROG1 = camogm_fifo_writer
TEST_PROG2 = camogm_fifo_reader
PHPSCRIPTS = camogmstate.php $(GUIDIR)/camogmgui.php $(GUIDIR)/camogmgui.css $(GUIDIR)/camogmgui.js $(GUIDIR)/camogm_interface.php \ PHPSCRIPTS = camogmstate.php $(GUIDIR)/camogmgui.php $(GUIDIR)/camogmgui.css $(GUIDIR)/camogmgui.js $(GUIDIR)/camogm_interface.php \
$(GUIDIR)/SpryTabbedPanels.css $(GUIDIR)/SpryTabbedPanels.js $(GUIDIR)/xml_simple.php $(GUIDIR)/SpryCollapsiblePanel.css \ $(GUIDIR)/SpryTabbedPanels.css $(GUIDIR)/SpryTabbedPanels.js $(GUIDIR)/xml_simple.php $(GUIDIR)/SpryCollapsiblePanel.css \
$(GUIDIR)/SpryCollapsiblePanel.js $(GUIDIR)/SpryCollapsiblePanel.js
...@@ -12,7 +15,10 @@ IMAGES = $(GUIDIR)/images/filebrowser-01.gif $(GUIDIR)/images/filebrowser-bo ...@@ -12,7 +15,10 @@ IMAGES = $(GUIDIR)/images/filebrowser-01.gif $(GUIDIR)/images/filebrowser-bo
SRCS = camogm.c camogm_ogm.c camogm_jpeg.c camogm_mov.c camogm_kml.c camogm_read.c index_list.c camogm_align.c SRCS = camogm.c camogm_ogm.c camogm_jpeg.c camogm_mov.c camogm_kml.c camogm_read.c index_list.c camogm_align.c
TEST_SRC = camogm_test.c TEST_SRC = camogm_test.c
TEST_SRC1 = camogm_fifo_writer.c
TEST_SRC2 = camogm_fifo_reader.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
CFLAGS += -Wall -I$(STAGING_DIR_HOST)/usr/include-uapi CFLAGS += -Wall -I$(STAGING_DIR_HOST)/usr/include-uapi
...@@ -28,17 +34,21 @@ BINDIR = /usr/bin/ ...@@ -28,17 +34,21 @@ BINDIR = /usr/bin/
WWW_PAGES = /www/pages WWW_PAGES = /www/pages
IMAGEDIR = $(WWW_PAGES)/images IMAGEDIR = $(WWW_PAGES)/images
all: $(PROGS) $(TEST_PROG) all: $(PROGS) $(TEST_PROG) $(TEST_PROG1) $(TEST_PROG2)
$(PROGS): $(OBJS) $(PROGS): $(OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@ $(CC) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(TEST_PROG): $(TEST_SRC:.c=.o) $(TEST_PROG): $(TEST_SRC:.c=.o)
$(TEST_PROG1): $(TEST_SRC1:.c=.o)
$(TEST_PROG2): $(TEST_SRC2:.c=.o)
install: $(PROGS) $(PHPSCRIPTS) $(CONFIGS) install: $(PROGS) $(PHPSCRIPTS) $(CONFIGS)
$(INSTALL) $(OWN) -d $(DESTDIR)$(BINDIR) $(INSTALL) $(OWN) -d $(DESTDIR)$(BINDIR)
$(INSTALL) $(OWN) -m $(INSTMODE) $(PROGS) $(DESTDIR)$(BINDIR) $(INSTALL) $(OWN) -m $(INSTMODE) $(PROGS) $(DESTDIR)$(BINDIR)
$(INSTALL) $(OWN) -m $(INSTMODE) $(TEST_PROG) $(DESTDIR)$(BINDIR) $(INSTALL) $(OWN) -m $(INSTMODE) $(TEST_PROG) $(DESTDIR)$(BINDIR)
$(INSTALL) $(OWN) -m $(INSTMODE) $(TEST_PROG1) $(DESTDIR)$(BINDIR)
$(INSTALL) $(OWN) -m $(INSTMODE) $(TEST_PROG2) $(DESTDIR)$(BINDIR)
$(INSTALL) $(OWN) -d $(DESTDIR)$(SYSCONFDIR) $(INSTALL) $(OWN) -d $(DESTDIR)$(SYSCONFDIR)
$(INSTALL) $(OWN) -m $(INSTDOCS) $(CONFIGS) $(DESTDIR)$(SYSCONFDIR) $(INSTALL) $(OWN) -m $(INSTDOCS) $(CONFIGS) $(DESTDIR)$(SYSCONFDIR)
$(INSTALL) $(OWN) -d $(DESTDIR)$(WWW_PAGES) $(INSTALL) $(OWN) -d $(DESTDIR)$(WWW_PAGES)
......
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
#include <ctype.h> #include <ctype.h>
#include <elphel/ahci_cmd.h> #include <elphel/ahci_cmd.h>
#include <poll.h>
#include "camogm_ogm.h" #include "camogm_ogm.h"
#include "camogm_jpeg.h" #include "camogm_jpeg.h"
#include "camogm_mov.h" #include "camogm_mov.h"
...@@ -38,6 +40,8 @@ ...@@ -38,6 +40,8 @@
/** @brief Default debug level */ /** @brief Default debug level */
#define DEFAULT_DEBUG_LVL 6 #define DEFAULT_DEBUG_LVL 6
/** @brief Default poll() timeout in ms*/
#define DEFAULT_POLL_TIMEOUT 1000
/** @brief JPEG trailer size in bytes */ /** @brief JPEG trailer size in bytes */
#define TRAILER_SIZE 0x02 #define TRAILER_SIZE 0x02
/** @brief Default segment duration in seconds */ /** @brief Default segment duration in seconds */
...@@ -1301,7 +1305,9 @@ char * getLineFromPipe(FILE* npipe) ...@@ -1301,7 +1305,9 @@ char * getLineFromPipe(FILE* npipe)
if (!cmdbufp) cmdbuf[cmdbufp] = 0; //null-terminate first access (probably not needed for the static buffer if (!cmdbufp) cmdbuf[cmdbufp] = 0; //null-terminate first access (probably not needed for the static buffer
nlp = strpbrk(cmdbuf, ";\n"); nlp = strpbrk(cmdbuf, ";\n");
if (!nlp) { //no complete string, try to read more if (!nlp) { //no complete string, try to read more
fl = fread(&cmdbuf[cmdbufp], 1, sizeof(cmdbuf) - cmdbufp - 1, npipe); // 2019/01/16: this change is related to switching to poll()
//fl = fread(&cmdbuf[cmdbufp], 1, sizeof(cmdbuf) - cmdbufp - 1, npipe);
fl = read(npipe, &cmdbuf[cmdbufp], sizeof(cmdbuf) - cmdbufp - 1);
cmdbuf[cmdbufp + fl] = 0; cmdbuf[cmdbufp + fl] = 0;
// is there any complete string in a buffer after reading? // is there any complete string in a buffer after reading?
nlp = strpbrk(&cmdbuf[cmdbufp], ";\n"); // there were no new lines before cmdbufp nlp = strpbrk(&cmdbuf[cmdbufp], ";\n"); // there were no new lines before cmdbufp
...@@ -1548,6 +1554,8 @@ int listener_loop(camogm_state *state) ...@@ -1548,6 +1554,8 @@ int listener_loop(camogm_state *state)
int curr_port = 0; int curr_port = 0;
const char *pipe_name = state->pipe_name; const char *pipe_name = state->pipe_name;
struct pollfd pfd;
// create a named pipe // create a named pipe
// always delete the pipe if it existed, start a fresh one // always delete the pipe if it existed, start a fresh one
f_ok = access(pipe_name, F_OK); f_ok = access(pipe_name, F_OK);
...@@ -1567,6 +1575,8 @@ int listener_loop(camogm_state *state) ...@@ -1567,6 +1575,8 @@ int listener_loop(camogm_state *state)
} }
} }
/* old */
/*
// now open the pipe - will block until something will be written (or just open for writing, // now open the pipe - will block until something will be written (or just open for writing,
// reads themselves will not block) // reads themselves will not block)
if (!((cmd_file = fopen(pipe_name, "r")))) { if (!((cmd_file = fopen(pipe_name, "r")))) {
...@@ -1574,6 +1584,27 @@ int listener_loop(camogm_state *state) ...@@ -1574,6 +1584,27 @@ int listener_loop(camogm_state *state)
clean_up(state); clean_up(state);
return -5; return -5;
} }
*/
/* new: 2019/01/16 */
// Adding poll() because supposedly read calls (read, fread)
// stopped being blocking at all
// Why O_RDWR?
// https://stackoverflow.com/questions/22021253/poll-on-named-pipe-returns-with-pollhup-constantly-and-immediately
if (!((cmd_file = open(pipe_name, O_RDWR|O_NONBLOCK)))) {
D0(fprintf(debug_file, "Can not open command file %s\n", pipe_name));
clean_up(state);
return -5;
}
/* force(?) disable O_NONBLOCK */
fcntl(cmd_file, F_SETFL, 0);
// ready to read
pfd.events = POLLIN;
pfd.fd = cmd_file;
D0(fprintf(debug_file, "Pipe %s open for reading\n", pipe_name)); // to make sure something is sent out D0(fprintf(debug_file, "Pipe %s open for reading\n", pipe_name)); // to make sure something is sent out
// enter main processing loop // enter main processing loop
...@@ -1581,99 +1612,108 @@ int listener_loop(camogm_state *state) ...@@ -1581,99 +1612,108 @@ int listener_loop(camogm_state *state)
curr_port = select_port(state); curr_port = select_port(state);
state->port_num = curr_port; state->port_num = curr_port;
// look at command queue first // look at command queue first
cmd = parse_cmd(state, cmd_file);
if (cmd) { ret = poll(&pfd,1,DEFAULT_POLL_TIMEOUT);
if (cmd < 0) D0(fprintf(debug_file, "Unrecognized command\n"));
} else if (state->prog_state == STATE_RUNNING) { // no commands in queue, started if (ret==0){
switch ((rslt = -sendImageFrame(state))) { D6(fprintf(debug_file, "Waiting for commands...\n"));
case 0: }
break; // frame sent OK, nothing to do (TODO: check file length/duration)
case CAMOGM_FRAME_NOT_READY: // just wait for the frame to appear at the current pointer if (pfd.revents & POLLIN){
// we'll wait for a frame, not to waste resources. But if the compressor is stopped this program will not respond to any commands cmd = parse_cmd(state, cmd_file);
// TODO - add another wait with (short) timeout? if (cmd) {
fp0 = lseek(state->fd_circ[curr_port], 0, SEEK_CUR); if (cmd < 0) D0(fprintf(debug_file, "Unrecognized command\n"));
if (fp0 < 0) { } else if (state->prog_state == STATE_RUNNING) { // no commands in queue, started
D0(fprintf(debug_file, "%s:line %d got broken frame (%d) before waiting for ready\n", __FILE__, __LINE__, fp0)); switch ((rslt = -sendImageFrame(state))) {
rslt = CAMOGM_FRAME_BROKEN; case 0:
} else { break; // frame sent OK, nothing to do (TODO: check file length/duration)
fp1 = lseek(state->fd_circ[curr_port], LSEEK_CIRC_WAIT, SEEK_END); case CAMOGM_FRAME_NOT_READY: // just wait for the frame to appear at the current pointer
if (fp1 < 0) { // we'll wait for a frame, not to waste resources. But if the compressor is stopped this program will not respond to any commands
D0(fprintf(debug_file, "%s:line %d got broken frame (%d) while waiting for ready. Before that fp0=0x%x\n", __FILE__, __LINE__, fp1, fp0)); // TODO - add another wait with (short) timeout?
fp0 = lseek(state->fd_circ[curr_port], 0, SEEK_CUR);
if (fp0 < 0) {
D0(fprintf(debug_file, "%s:line %d got broken frame (%d) before waiting for ready\n", __FILE__, __LINE__, fp0));
rslt = CAMOGM_FRAME_BROKEN; rslt = CAMOGM_FRAME_BROKEN;
} else { } else {
break; fp1 = lseek(state->fd_circ[curr_port], LSEEK_CIRC_WAIT, SEEK_END);
if (fp1 < 0) {
D0(fprintf(debug_file, "%s:line %d got broken frame (%d) while waiting for ready. Before that fp0=0x%x\n", __FILE__, __LINE__, fp1, fp0));
rslt = CAMOGM_FRAME_BROKEN;
} else {
break;
}
} }
} // no break
// no break case CAMOGM_FRAME_CHANGED: // frame parameters have changed
case CAMOGM_FRAME_CHANGED: // frame parameters have changed case CAMOGM_FRAME_NEXTFILE: // next file needed (need to switch to a new file (time/size exceeded limit)
case CAMOGM_FRAME_NEXTFILE: // next file needed (need to switch to a new file (time/size exceeded limit) case CAMOGM_FRAME_INVALID: // invalid frame pointer
case CAMOGM_FRAME_INVALID: // invalid frame pointer case CAMOGM_FRAME_BROKEN: // frame broken (buffer overrun)
case CAMOGM_FRAME_BROKEN: // frame broken (buffer overrun) // restart the file
// restart the file D3(fprintf(debug_file,"%s:line %d - sendImageFrame() returned -%d\n", __FILE__, __LINE__, rslt));
D3(fprintf(debug_file,"%s:line %d - sendImageFrame() returned -%d\n", __FILE__, __LINE__, rslt)); camogm_stop(state);
camogm_stop(state); state->prog_state = STATE_RESTARTING;
state->prog_state = STATE_RESTARTING; camogm_start(state);
camogm_start(state); break;
break; case CAMOGM_FRAME_FILE_ERR: // error with file I/O
case CAMOGM_FRAME_FILE_ERR: // error with file I/O case CAMOGM_FRAME_OTHER: // other errors
case CAMOGM_FRAME_OTHER: // other errors D0(fprintf(debug_file, "%s:line %d - error=%d\n", __FILE__, __LINE__, rslt));
D0(fprintf(debug_file, "%s:line %d - error=%d\n", __FILE__, __LINE__, rslt)); break;
break; default:
default: D0(fprintf(debug_file, "%s:line %d - should not get here (rslt=%d)\n", __FILE__, __LINE__, rslt));
D0(fprintf(debug_file, "%s:line %d - should not get here (rslt=%d)\n", __FILE__, __LINE__, rslt)); clean_up(state);
clean_up(state); exit(-1);
exit(-1); } // switch sendImageFrame()
} // switch sendImageFrame()
// collect error statistics
// collect error statistics if (rslt > 0 && rslt < CAMOGM_ERRNUM)
if (rslt > 0 && rslt < CAMOGM_ERRNUM) state->error_stat[curr_port][rslt]++;
state->error_stat[curr_port][rslt]++;
if ((rslt != 0) && (rslt != CAMOGM_FRAME_NOT_READY) && (rslt != CAMOGM_FRAME_CHANGED))
if ((rslt != 0) && (rslt != CAMOGM_FRAME_NOT_READY) && (rslt != CAMOGM_FRAME_CHANGED)) // add port number to error code to facilitate debugging
// add port number to error code to facilitate debugging state->last_error_code = rslt + 100 * state->port_num;
state->last_error_code = rslt + 100 * state->port_num; } else if (state->prog_state == STATE_STARTING) { // no commands in queue,starting (but not started yet)
} else if (state->prog_state == STATE_STARTING) { // no commands in queue,starting (but not started yet)
// retry starting
// retry starting switch ((rslt = -camogm_start(state))) {
switch ((rslt = -camogm_start(state))) { case 0:
case 0: break; // file started OK, nothing to do
break; // file started OK, nothing to do case CAMOGM_TOO_EARLY:
case CAMOGM_TOO_EARLY: lseek(state->fd_circ[curr_port], LSEEK_CIRC_TOWP, SEEK_END); // set pointer to the frame to wait for
lseek(state->fd_circ[curr_port], LSEEK_CIRC_TOWP, SEEK_END); // set pointer to the frame to wait for lseek(state->fd_circ[curr_port], LSEEK_CIRC_WAIT, SEEK_END); // It already passed CAMOGM_FRAME_NOT_READY, so compressor may be running already
lseek(state->fd_circ[curr_port], LSEEK_CIRC_WAIT, SEEK_END); // It already passed CAMOGM_FRAME_NOT_READY, so compressor may be running already break; // no need to wait extra
break; // no need to wait extra case CAMOGM_FRAME_NOT_READY: // just wait for the frame to appear at the current pointer
case CAMOGM_FRAME_NOT_READY: // just wait for the frame to appear at the current pointer // we'll wait for a frame, not to waste resources. But if the compressor is stopped this program will not respond to any commands
// we'll wait for a frame, not to waste resources. But if the compressor is stopped this program will not respond to any commands // TODO - add another wait with (short) timeout?
// TODO - add another wait with (short) timeout? case CAMOGM_FRAME_CHANGED: // frame parameters have changed
case CAMOGM_FRAME_CHANGED: // frame parameters have changed case CAMOGM_FRAME_NEXTFILE:
case CAMOGM_FRAME_NEXTFILE: case CAMOGM_FRAME_INVALID: // invalid frame pointer
case CAMOGM_FRAME_INVALID: // invalid frame pointer case CAMOGM_FRAME_BROKEN: // frame broken (buffer overrun)
case CAMOGM_FRAME_BROKEN: // frame broken (buffer overrun) usleep(COMMAND_LOOP_DELAY); // it should be not too long so empty buffer will not be overrun
usleep(COMMAND_LOOP_DELAY); // it should be not too long so empty buffer will not be overrun break;
break; case CAMOGM_FRAME_FILE_ERR: // error with file I/O
case CAMOGM_FRAME_FILE_ERR: // error with file I/O case CAMOGM_FRAME_OTHER: // other errors
case CAMOGM_FRAME_OTHER: // other errors D0(fprintf(debug_file, "%s:line %d - error=%d\n", __FILE__, __LINE__, rslt));
D0(fprintf(debug_file, "%s:line %d - error=%d\n", __FILE__, __LINE__, rslt)); break;
break; default:
default: D0(fprintf(debug_file, "%s:line %d - should not get here (rslt=%d)\n", __FILE__, __LINE__, rslt));
D0(fprintf(debug_file, "%s:line %d - should not get here (rslt=%d)\n", __FILE__, __LINE__, rslt)); clean_up(state);
clean_up(state); exit(-1);
exit(-1); } // switch camogm_start()
} // switch camogm_start()
// collect error statistics
// collect error statistics if (rslt > 0 && rslt < CAMOGM_ERRNUM)
if (rslt > 0 && rslt < CAMOGM_ERRNUM) state->error_stat[curr_port][rslt]++;
state->error_stat[curr_port][rslt]++;
if ((rslt != 0) && (rslt != CAMOGM_TOO_EARLY) && (rslt != CAMOGM_FRAME_NOT_READY) && (rslt != CAMOGM_FRAME_CHANGED) )
if ((rslt != 0) && (rslt != CAMOGM_TOO_EARLY) && (rslt != CAMOGM_FRAME_NOT_READY) && (rslt != CAMOGM_FRAME_CHANGED) ) // add port number to error code to facilitate debugging
// add port number to error code to facilitate debugging state->last_error_code = rslt + 100 * state->port_num;
state->last_error_code = rslt + 100 * state->port_num; } else if (state->prog_state == STATE_READING) {
} else if (state->prog_state == STATE_READING) { usleep(COMMAND_LOOP_DELAY);
usleep(COMMAND_LOOP_DELAY); } else { // not running, not starting
} else { // not running, not starting state->rawdev.thread_state = STATE_RUNNING;
state->rawdev.thread_state = STATE_RUNNING; usleep(COMMAND_LOOP_DELAY); // make it longer but interruptible by signals?
usleep(COMMAND_LOOP_DELAY); // make it longer but interruptible by signals? }
} } // if pfd.revents & POLLIN
} // while (process) } // while (process)
// normally, we should not be here // normally, we should not be here
......
/** @brief This define is needed to use lseek64 and should be set before includes */
#define _LARGEFILE64_SOURCE
/** Needed for O_DIRECT */
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/fs.h>
#include <ctype.h>
#include <sys/uio.h>
#include <poll.h>
// set to 1 second
#define CAMOGM_TIMEOUT 1000
int main(int argc, char *argv[]){
int f_ok;
int ret;
int fer, feo;
long int ftl;
int i=0;
const char pipe_name[] = "/tmp/fifo_test";
int flags;
char cmdbuf[1024];
int fl;
int somecounter = 0;
int writecounter = 0;
FILE *cmd_file;
struct pollfd pfd;
pfd.events = POLLIN;
printf("This is reader. It creates FIFO: %s\n",pipe_name);
// always delete pipe if it exists
f_ok = access(pipe_name, F_OK);
ret = unlink(pipe_name);
if (ret && f_ok == 0) {
printf("Some error\n");
}
ret = mkfifo(pipe_name, 0666); //EEXIST
if (ret) {
if (errno==EEXIST){
printf("Pipe exists\n");
}
}
cmd_file = open(pipe_name, O_RDWR|O_NONBLOCK);
pfd.fd = cmd_file;
fcntl(cmd_file, F_SETFL, 0); /* disable O_NONBLOCK */
printf("Pipe is now open for reading\n");
while(true){
ret = poll(&pfd, 1, CAMOGM_TIMEOUT); /* poll to avoid reading EOF */
somecounter +=1;
if (ret==0) {
printf("TIMEOUT %d\n",somecounter);
}
if (pfd.revents & POLLIN){
printf("PostPoll %d %d, revents = %d, errno = %d\n",somecounter,ret,pfd.revents,errno);
fl = read(cmd_file, cmdbuf, sizeof(cmdbuf));
//clearerr(cmd_file);
if (fl>0) {
writecounter +=1;
}
if (fl<0) {
printf("Error?\n");
break;
}
//fl = read(cmd_file,cmdbuf,sizeof(cmdbuf));
if ((fl>10)||(somecounter>10000000)) {
break;
}
}
}
printf("EXIT! errno=%d writes=%d wdc=%d\n",errno,writecounter,somecounter);
return 0;
}
/** @brief This define is needed to use lseek64 and should be set before includes */
#define _LARGEFILE64_SOURCE
/** Needed for O_DIRECT */
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <stdbool.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <linux/fs.h>
#include <ctype.h>
#include <sys/uio.h>
int main(int argc, char *argv[]){
FILE *cmd_file;
const char pipe_name[] = "/tmp/fifo_test";
const char message[] = "test";
int ret;
printf("This is writer\n");
cmd_file = fopen(pipe_name, "w");
while(true){
ret = fwrite(message,sizeof(char),sizeof(message),cmd_file);
printf("Wrote %d bytes, fwrite returned %d\n",sizeof(message),ret);
}
return 0;
}
...@@ -1017,8 +1017,8 @@ void *reader(void *arg) ...@@ -1017,8 +1017,8 @@ void *reader(void *arg)
.sockfd_const = &sockfd, .sockfd_const = &sockfd,
.sockfd_temp = &fd .sockfd_temp = &fd
}; };
memset(&index_dir, 0, sizeof(struct disk_index)); memset(&index_dir, 0, sizeof(struct disk_idir));
memset(&index_sparse, 0, sizeof(struct disk_index)); memset(&index_sparse, 0, sizeof(struct disk_idir));
prep_socket(&sockfd, state->sock_port); prep_socket(&sockfd, state->sock_port);
pthread_cleanup_push(exit_thread, &exit_state); pthread_cleanup_push(exit_thread, &exit_state);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment