Rewrite export screen
This commit is contained in:
parent
1c2a99a865
commit
8f4fd1958d
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ bin/
|
||||
compile_commands.json
|
||||
.cache/
|
||||
config.conf
|
||||
.clangd
|
||||
|
6
Makefile
6
Makefile
@ -9,7 +9,8 @@ SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\
|
||||
src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c\
|
||||
src/ui/statrange.c src/config.c src/ui/blankscreen.c src/drive.c\
|
||||
src/ui/exportscreen.c src/ui/setdatescreen.c src/ui/viewdatescreen.c\
|
||||
src/ui/powerscreen.c src/ui/settzscreen.c src/ui/cleardatascreen.c
|
||||
src/ui/powerscreen.c src/ui/settzscreen.c src/ui/cleardatascreen.c\
|
||||
src/async.c
|
||||
PROG=rpi4b-temp-humidity
|
||||
|
||||
all: config.conf bin/${PROG}
|
||||
@ -24,7 +25,7 @@ bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o: src/util.h
|
||||
bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/util.h
|
||||
bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/util.h
|
||||
bin/ui/viewdatescreen.o bin/ui/setdatescreen.o bin/ui/powerscreen.o: src/util.h
|
||||
bin/ui/settzscreen.o bin/ui/cleardatascreen.o: src/util.h
|
||||
bin/ui/settzscreen.o bin/ui/cleardatascreen.o bin/async.o: src/util.h
|
||||
|
||||
bin/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h
|
||||
bin/ui/statsby.o bin/ui/timesel.o bin/ui/statrange.o: src/lcd.h
|
||||
@ -59,6 +60,7 @@ bin/main.o bin/ui/powerscreen.o: src/ui/powerscreen.h
|
||||
bin/main.o bin/ui/settzscreen.o: src/ui/settzscreen.h
|
||||
bin/main.o bin/ui/cleardatascreen.o: src/ui/cleardatascreen.h
|
||||
bin/main.o bin/drive.o bin/ui/exportscreen.o: src/drive.h
|
||||
bin/main.o bin/drive.o bin/ui/exportscreen.o: src/async.h
|
||||
|
||||
.if "${DEFAULT_TEMP_UNIT}" != "F" && "${DEFAULT_TEMP_UNIT}" != "C" &&\
|
||||
"${DEFAULT_TEMP_UNIT}" != "c" && "${DEFAULT_TEMP_UNIT}" != "c"
|
||||
|
111
src/async.c
Normal file
111
src/async.c
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
* async.c - Utilities for async operations
|
||||
* Copyright (C) 2024 Alexander Rosenberg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version. See the LICENSE file for more information.
|
||||
*/
|
||||
#include "async.h"
|
||||
#include "signal.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <sys/wait.h>
|
||||
#include <sys/signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <err.h>
|
||||
|
||||
|
||||
AsyncOperation *async_operation_start(AsyncOperationPollFunc poll,
|
||||
AsyncOperationCleanupFunc cleanup,
|
||||
void *data1, void *data2,
|
||||
const char *path, char * const *args) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
warn("fork(2)");
|
||||
return NULL;
|
||||
} else if (pid == 0) {
|
||||
// child
|
||||
if (!GLOBAL_OPTS.verbose) {
|
||||
freopen("/dev/null", "w", stdout);
|
||||
}
|
||||
execvp(path, args);
|
||||
err(1, "execvp(2) for \"%s\"", path);
|
||||
} else {
|
||||
// parent
|
||||
AsyncOperation *op = malloc_checked(sizeof(AsyncOperation));
|
||||
op->pid = pid;
|
||||
op->data1 = data1;
|
||||
op->data2 = data2;
|
||||
op->poll_func = poll;
|
||||
op->cleanup_func = cleanup;
|
||||
op->done = false;
|
||||
return op;
|
||||
}
|
||||
}
|
||||
|
||||
AsyncOperation *async_operation_take(AsyncOperationPollFunc poll,
|
||||
AsyncOperationCleanupFunc cleanup,
|
||||
void *data1, void *data2, pid_t pid) {
|
||||
AsyncOperation *op = malloc_checked(sizeof(AsyncOperation));
|
||||
op->pid = pid;
|
||||
op->data1 = data1;
|
||||
op->data2 = data2;
|
||||
op->poll_func = poll;
|
||||
op->cleanup_func = cleanup;
|
||||
op->done = false;
|
||||
return op;
|
||||
}
|
||||
|
||||
AsyncOperationStatus async_operation_poll(AsyncOperation *op, bool hang) {
|
||||
if (op->done) {
|
||||
// polling a done process is a no-op
|
||||
return ASYNC_OPERATION_DONE;
|
||||
}
|
||||
pid_t proc = waitpid(op->pid, &op->status, hang ? 0 : WNOHANG);
|
||||
if (proc < 0) {
|
||||
warn("waitpid(2)");
|
||||
async_operation_cancel(op, false);
|
||||
return ASYNC_OPERATION_FAILED;
|
||||
} else if (proc > 0) {
|
||||
op->done = true;
|
||||
}
|
||||
if (op->poll_func && !op->poll_func(op)) {
|
||||
async_operation_cancel(op, false);
|
||||
return ASYNC_OPERATION_FAILED;
|
||||
}
|
||||
return op->done ? ASYNC_OPERATION_DONE : ASYNC_OPERATION_WORKING;
|
||||
}
|
||||
|
||||
void async_operation_cancel(AsyncOperation *op, bool force) {
|
||||
if (op->done) {
|
||||
// cancelling a done process is a no-op
|
||||
return;
|
||||
}
|
||||
kill(op->pid, force ? SIGKILL : SIGTERM);
|
||||
if (!force) {
|
||||
int status;
|
||||
if (!timed_wait(op->pid, &status, 1000 /* 1ms */)) {
|
||||
warn("waitpid(2)");
|
||||
}
|
||||
if (!WIFEXITED(status) && !WIFSIGNALED(status)) {
|
||||
// force kill it if it refuses to exit itself
|
||||
async_operation_cancel(op, true);
|
||||
}
|
||||
}
|
||||
op->done = true;
|
||||
}
|
||||
|
||||
void async_operation_free(AsyncOperation *op) {
|
||||
if (op) {
|
||||
if (!op->done) {
|
||||
async_operation_cancel(op, false);
|
||||
}
|
||||
if (op->cleanup_func) {
|
||||
op->cleanup_func(op);
|
||||
}
|
||||
free(op);
|
||||
}
|
||||
}
|
75
src/async.h
Normal file
75
src/async.h
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* async.h - Utilities for async operations
|
||||
* Copyright (C) 2024 Alexander Rosenberg
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version. See the LICENSE file for more information.
|
||||
*/
|
||||
#ifndef INCLUDED_ASYNC_H
|
||||
#define INCLUDED_ASYNC_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
typedef enum {
|
||||
ASYNC_OPERATION_DONE,
|
||||
ASYNC_OPERATION_FAILED,
|
||||
ASYNC_OPERATION_WORKING
|
||||
} AsyncOperationStatus;
|
||||
|
||||
typedef struct _AsyncOperation AsyncOperation;
|
||||
|
||||
// true on success, false otherwise
|
||||
typedef bool(*AsyncOperationPollFunc)(AsyncOperation *op);
|
||||
typedef void(*AsyncOperationCleanupFunc)(AsyncOperation *op);
|
||||
|
||||
struct _AsyncOperation {
|
||||
AsyncOperationPollFunc poll_func;
|
||||
AsyncOperationCleanupFunc cleanup_func;
|
||||
int status;
|
||||
pid_t pid;
|
||||
bool done;
|
||||
void *data1;
|
||||
void *data2;
|
||||
};
|
||||
#define async_operation_failed(o) (WEXITSTATUS((o)->status) != 0)
|
||||
|
||||
/*
|
||||
* Start a new async operation using the executable PATH and the arguments
|
||||
* ARGS. The first element of ARGS should be the name of the new process, by
|
||||
* convention. DATA1 and DATA2 are arbitrary pointers for storing data needed by
|
||||
* CLEANUP or POLL.
|
||||
* Return: NULL on error, the operation otherwise
|
||||
*/
|
||||
AsyncOperation *async_operation_start(AsyncOperationPollFunc poll,
|
||||
AsyncOperationCleanupFunc cleanup,
|
||||
void *data1, void *data2,
|
||||
const char *path, char * const *args);
|
||||
|
||||
/*
|
||||
* Create a new async operation from an already running process with PID.
|
||||
*/
|
||||
AsyncOperation *async_operation_take(AsyncOperationPollFunc poll,
|
||||
AsyncOperationCleanupFunc cleanup,
|
||||
void *data1, void *data2, pid_t pid);
|
||||
|
||||
/*
|
||||
* Poll and return the status of OP. If HANG, wait until OP is done.
|
||||
*/
|
||||
AsyncOperationStatus async_operation_poll(AsyncOperation *op, bool hang);
|
||||
|
||||
/*
|
||||
* Kill OP, by FORCE if necessary. This does NOT free the process, you need to
|
||||
* do that yourself.
|
||||
*/
|
||||
void async_operation_cancel(AsyncOperation *op, bool force);
|
||||
|
||||
/*
|
||||
* Free OP by calling its cleanup function. This will cancel it if it's still
|
||||
* running!
|
||||
*/
|
||||
void async_operation_free(AsyncOperation *op);
|
||||
|
||||
#endif
|
196
src/drive.c
196
src/drive.c
@ -19,7 +19,6 @@
|
||||
#include <dirent.h>
|
||||
#include <sys/param.h>
|
||||
#include <sys/mount.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static int compare_drives(const Drive *d1, const Drive *d2) {
|
||||
return strcmp(d1->name, d2->name);
|
||||
@ -62,29 +61,16 @@ void drive_free_drives(Drive *drives, size_t count) {
|
||||
free(drives);
|
||||
}
|
||||
|
||||
static void close_pipe(void *ptr) {
|
||||
int *pipe = ptr;
|
||||
if (pipe[0] >= 0) {
|
||||
close(pipe[0]);
|
||||
}
|
||||
if (pipe[1] >= 0) {
|
||||
close(pipe[1]);
|
||||
}
|
||||
}
|
||||
|
||||
char *drive_guess_fs(const char *dev) {
|
||||
char *result_fs = NULL;
|
||||
int ctop[2] = {0, 0};
|
||||
pthread_cleanup_push(close_pipe, ctop);
|
||||
pthread_cleanup_push(free, result_fs);
|
||||
if (pipe(ctop) < 0) {
|
||||
warn("failed to create pipe");
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
warn("fork(2) failed");
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
} else if (pid == 0) {
|
||||
// child
|
||||
close(ctop[0]); // close read end
|
||||
@ -104,125 +90,119 @@ char *drive_guess_fs(const char *dev) {
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0) {
|
||||
warn("waiting for child failed");
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
}
|
||||
if (WEXITSTATUS(status) != 0) {
|
||||
// no error, we probably just don't know the FS type
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
}
|
||||
char read_buf[32];
|
||||
ssize_t read_count = read(ctop[0], read_buf, 32);
|
||||
if (read_count < 0) {
|
||||
warn("reading from pipe failed");
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
}
|
||||
read_buf[read_count - 1] = '\0';
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
result_fs = strdup_checked(read_buf);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
cleanup:
|
||||
pthread_cleanup_pop(false); // dont free result
|
||||
pthread_cleanup_pop(true);
|
||||
return result_fs;
|
||||
return strdup_checked(read_buf);
|
||||
}
|
||||
|
||||
bool drive_newfs_msdos(Drive *drive) {
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
warn("fork(2) failed");
|
||||
return false;
|
||||
} else if (pid == 0) {
|
||||
// child
|
||||
if (!GLOBAL_OPTS.verbose) {
|
||||
freopen("/dev/null", "w", stdout);
|
||||
}
|
||||
execl("/sbin/newfs_msdos", "/sbin/newfs_msdos", "-F32", drive->path, NULL);
|
||||
warn("execl(2) failed");
|
||||
exit(1);
|
||||
} else {
|
||||
// parent
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0) {
|
||||
warn("waiting for child failed");
|
||||
return false;
|
||||
}
|
||||
if (WEXITSTATUS(status) != 0) {
|
||||
warnx("creating FAT32 FS failed");
|
||||
return false;
|
||||
}
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
static bool newfs_operation_poll(AsyncOperation *op) {
|
||||
if (op->done && !async_operation_failed(op)) {
|
||||
Drive *drive = op->data1;
|
||||
free(drive->fs);
|
||||
drive->fs = strdup_checked("msdosfs");
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
AsyncOperation *drive_newfs_msdos(Drive *drive) {
|
||||
if (drive->mnt) {
|
||||
warnx("\"%s\" is mounted", drive->path);
|
||||
return NULL;
|
||||
}
|
||||
char * const args[] = {"newfs_msdos", "-F32", drive->path, NULL};
|
||||
AsyncOperation * op= async_operation_start(newfs_operation_poll, NULL,
|
||||
drive, NULL, args[0], args);
|
||||
if (!op) {
|
||||
warnx("%s: failed to start newfs_msdos process", drive->path);
|
||||
return NULL;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
||||
bool drive_mount(Drive *drive) {
|
||||
bool status = true;
|
||||
char *name = NULL;
|
||||
pthread_cleanup_push(free, name);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
name = strdup_checked("/tmp/mntXXXXXX");
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
static bool mount_operation_poll(AsyncOperation *op) {
|
||||
Drive *drive = op->data1;
|
||||
if (op->done && !async_operation_failed(op)) {
|
||||
// steal the mount point string
|
||||
drive->mnt = op->data2;
|
||||
op->data2 = NULL;
|
||||
LOG_VERBOSE("Mounted \"%s\" at \"%s\"\n", drive->path, drive->mnt);
|
||||
} else if (op->done) {
|
||||
warnx("mount of \"%s\" failed with status %d", drive->path,
|
||||
WEXITSTATUS(op->status));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mount_operation_cleanup(AsyncOperation *op) {
|
||||
free(op->data2);
|
||||
}
|
||||
|
||||
AsyncOperation *drive_mount(Drive *drive) {
|
||||
if (drive->mnt) {
|
||||
warnx("\"%s\" is already mounted", drive->path);
|
||||
return NULL;
|
||||
}
|
||||
char *name = strdup_checked("/tmp/mntXXXXXX");
|
||||
if (!mkdtemp(name)) {
|
||||
warn("failed to create temp mount point");
|
||||
status = false;
|
||||
goto cleanup;
|
||||
return NULL;
|
||||
}
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
warn("fork(2) failed");
|
||||
status = false;
|
||||
goto cleanup;
|
||||
} else if (pid == 0) {
|
||||
// child
|
||||
if (!GLOBAL_OPTS.verbose) {
|
||||
freopen("/dev/null", "w", stdout);
|
||||
char * const args[] = { "mount", "-t", drive->fs, drive->path, name, NULL };
|
||||
AsyncOperation *op = async_operation_start(mount_operation_poll,
|
||||
mount_operation_cleanup,
|
||||
drive, name, args[0], args);
|
||||
if (!op) {
|
||||
warnx("%s: failed to start mount process", drive->path);
|
||||
free(name);
|
||||
return NULL;
|
||||
}
|
||||
execl("/sbin/mount", "/sbin/mount", "-t", drive->fs, drive->path, name, NULL);
|
||||
warn("execl(2) failed");
|
||||
abort();
|
||||
}
|
||||
// parent
|
||||
int exit_code;
|
||||
if (waitpid(pid, &exit_code, 0) < 0) {
|
||||
warn("waiting for child failed");
|
||||
rmdir(name);
|
||||
status = false;
|
||||
goto cleanup;
|
||||
}
|
||||
if (WEXITSTATUS(exit_code) != 0) {
|
||||
warnx("mount(8) failed: \"%s\"", drive->path);
|
||||
rmdir(name);
|
||||
status = false;
|
||||
goto cleanup;
|
||||
}
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
drive->mnt = strdup_checked(name);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
LOG_VERBOSE("Mounted \"%s\" with fs %s to \"%s\"\n", drive->path, drive->fs,
|
||||
drive->mnt);
|
||||
cleanup:
|
||||
pthread_cleanup_pop(true);
|
||||
return status;
|
||||
return op;
|
||||
}
|
||||
|
||||
bool drive_unmount(Drive *drive, bool force) {
|
||||
if (!drive->mnt) {
|
||||
warnx("\"%s\" is not mounted", drive->path);
|
||||
return false;
|
||||
}
|
||||
int flags = force ? MNT_FORCE : 0;
|
||||
if (unmount(drive->mnt, flags) < 0) {
|
||||
warn("unmount(2) failed for \"%s\"", drive->path);
|
||||
return false;
|
||||
}
|
||||
bool unmount_operation_poll(AsyncOperation *op) {
|
||||
if (op->done) {
|
||||
Drive *drive = op->data1;
|
||||
rmdir(drive->mnt);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
free(drive->mnt);
|
||||
drive->mnt = NULL;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
LOG_VERBOSE("Unmounted \"%s\"\n", drive->path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
AsyncOperation *drive_unmount(Drive *drive, bool force) {
|
||||
if (!drive->mnt) {
|
||||
warnx("\"%s\" is not mounted", drive->path);
|
||||
return NULL;
|
||||
}
|
||||
const char *args[4];
|
||||
if (force) {
|
||||
args[0] = "umount";
|
||||
args[1] = "-f";
|
||||
args[2] = drive->mnt;
|
||||
args[3] = NULL;
|
||||
} else {
|
||||
args[0] = "umount";
|
||||
args[1] = drive->mnt;
|
||||
args[2] = NULL;
|
||||
}
|
||||
AsyncOperation *op = async_operation_start(unmount_operation_poll, NULL,
|
||||
drive, NULL, args[0],
|
||||
(char *const *) args);
|
||||
if (!op) {
|
||||
warnx("%s: failed to unmount drive", drive->mnt);
|
||||
return NULL;
|
||||
}
|
||||
return op;
|
||||
}
|
||||
|
23
src/drive.h
23
src/drive.h
@ -10,8 +10,10 @@
|
||||
#ifndef INCLUDED_DRIVE_H
|
||||
#define INCLUDED_DRIVE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include "async.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
typedef struct {
|
||||
char *name;
|
||||
@ -23,7 +25,8 @@ typedef struct {
|
||||
/*
|
||||
* Attempt to list all connected USB drives and their file systems. The list is
|
||||
* sorted alphabetically.
|
||||
* Return: all connected USB (actually any serial drive) and their file systems.
|
||||
* Return: all connected USB (actually any serial drive, so SATA too) and their
|
||||
* file systems.
|
||||
*/
|
||||
Drive *drive_list_usb_drives(size_t *count);
|
||||
|
||||
@ -40,21 +43,21 @@ void drive_free_drives(Drive *drives, size_t count);
|
||||
char *drive_guess_fs(const char *dev);
|
||||
|
||||
/*
|
||||
* Create an MSDOS (FAT32) FS on DRIVE. DRIVE MUST NOT BE MOUNTED!
|
||||
* Return: true on success, false otherwise
|
||||
* Create an MSDOS (FAT32) FS on DRIVE.
|
||||
* Return: an operation on success, NULL otherwise
|
||||
*/
|
||||
bool drive_newfs_msdos(Drive *drive);
|
||||
AsyncOperation *drive_newfs_msdos(Drive *drive);
|
||||
|
||||
/*
|
||||
* Mount DRIVE onto a new temp. dir in /tmp. DRIVE MUST NOT BE MOUNTED!
|
||||
* Return: true on success, false otherwise
|
||||
* Mount DRIVE onto a new temp. dir in /tmp.
|
||||
* Return: an operation on success, NULL otherwise
|
||||
*/
|
||||
bool drive_mount(Drive *drive);
|
||||
AsyncOperation *drive_mount(Drive *drive);
|
||||
|
||||
/*
|
||||
* Unmount DRIVE. Force the operation if FORCE. Also rmdir(2) the mount point.
|
||||
* Return: true of success, false on failure.
|
||||
* Return: an operation on success, NULL otherwise
|
||||
*/
|
||||
bool drive_unmount(Drive *drive, bool force);
|
||||
AsyncOperation *drive_unmount(Drive *drive, bool force);
|
||||
|
||||
#endif
|
||||
|
@ -92,7 +92,8 @@ int main(int argc, char *const *argv) {
|
||||
setenv("TZ", GLOBAL_OPTS.timezone, true);
|
||||
NEED_CLEAR_TZ = true;
|
||||
} else {
|
||||
LOG_VERBOSE("Config timezone option shadowed by local environment variable\n");
|
||||
LOG_VERBOSE("Config timezone option shadowed by local environment "
|
||||
"variable.\n");
|
||||
}
|
||||
}
|
||||
tzset();
|
||||
|
@ -10,14 +10,11 @@
|
||||
#include "exportscreen.h"
|
||||
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <sys/types.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
#include <pthread.h>
|
||||
#include <pthread_np.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <signal.h>
|
||||
|
||||
static void export_screen_reset(ExportScreen *screen,
|
||||
SensorState *state) {
|
||||
@ -36,20 +33,25 @@ static void export_screen_reset(ExportScreen *screen,
|
||||
* Retrun: true if task is done, false otherwise
|
||||
*/
|
||||
static bool export_check_bg_task(ExportScreen *screen) {
|
||||
void *status;
|
||||
if (pthread_peekjoin_np(screen->backgroud_oper, &status) != EBUSY) {
|
||||
// done
|
||||
screen->bg_inited = false;
|
||||
if ((intptr_t) status == EXPORT_SCREEN_FAILED) {
|
||||
// error happened
|
||||
warnx("background export operation failed");
|
||||
switch (async_operation_poll(screen->current_op, false)) {
|
||||
case ASYNC_OPERATION_DONE:
|
||||
if (async_operation_failed(screen->current_op)) {
|
||||
goto failed;
|
||||
}
|
||||
screen->stage = (intptr_t) status;
|
||||
screen->need_redraw = true;
|
||||
screen->current_op = NULL;
|
||||
return true;
|
||||
}
|
||||
case ASYNC_OPERATION_FAILED:
|
||||
goto failed;
|
||||
case ASYNC_OPERATION_WORKING:
|
||||
return false;
|
||||
}
|
||||
failed:
|
||||
screen->stage = EXPORT_SCREEN_FAILED;
|
||||
screen->need_redraw = true;
|
||||
screen->current_op = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *FORMAT_STRINGS[] = {
|
||||
"SQLITE",
|
||||
@ -151,7 +153,7 @@ static enum {
|
||||
SensorState *state,
|
||||
const char *fmt, ...) {
|
||||
int done_status = EXPORT_CONFIRM_CONTINUE;
|
||||
if (screen->bg_inited && export_check_bg_task(screen)) {
|
||||
if (screen->current_op && export_check_bg_task(screen)) {
|
||||
// background task done
|
||||
done_status = EXPORT_CONFIRM_CONTINUE;
|
||||
goto confirm_done;
|
||||
@ -203,6 +205,8 @@ static bool export_show_message(ExportScreen *screen,
|
||||
return true;
|
||||
}
|
||||
if (screen->need_redraw) {
|
||||
lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF,
|
||||
LCD_DISPLAY_ON);
|
||||
lcd_clear(state->lcd);
|
||||
lcd_move_to(state->lcd, 0, 0);
|
||||
lcd_write_string(state->lcd, message);
|
||||
@ -211,97 +215,32 @@ static bool export_show_message(ExportScreen *screen,
|
||||
return false;
|
||||
}
|
||||
|
||||
static intptr_t export_newfs_action(ExportScreen *screen) {
|
||||
if (drive_newfs_msdos(&screen->drives[screen->cur_drive])) {
|
||||
return EXPORT_SCREEN_MOUNTING;
|
||||
} else {
|
||||
return EXPORT_SCREEN_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
static intptr_t export_mount_action(ExportScreen *screen) {
|
||||
Drive *drive = &screen->drives[screen->cur_drive];
|
||||
if (drive_mount(drive)) {
|
||||
struct stat statbuf;
|
||||
char *path = NULL;
|
||||
int exist;
|
||||
pthread_cleanup_push(free, path);
|
||||
path = export_build_cur_path(screen);
|
||||
exist = stat(path, &statbuf);
|
||||
screen->need_overwrite = exist == 0;
|
||||
pthread_cleanup_pop(true); // free(path)
|
||||
return screen->need_overwrite ? EXPORT_SCREEN_CONFIRM_OVERWRITE :
|
||||
EXPORT_SCREEN_WRITING;
|
||||
} else {
|
||||
return EXPORT_SCREEN_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
static intptr_t export_write_action(ExportScreen *screen) {
|
||||
char *path = NULL;
|
||||
intptr_t next_target = EXPORT_SCREEN_DONE;
|
||||
pthread_cleanup_push(free, path);
|
||||
path = export_build_cur_path(screen);
|
||||
if (screen->need_overwrite && !recursive_rm(path)) {
|
||||
next_target = EXPORT_SCREEN_FAILED;
|
||||
goto cleanup;
|
||||
}
|
||||
LOG_VERBOSE("Exporting to \"%s\"\n", path);
|
||||
switch (screen->format) {
|
||||
case EXPORT_FORMAT_SQLITE:
|
||||
if (!export_database(screen->db_cache, path)) {
|
||||
warnx("export to sqlite3 db failed: \"%s\"", path);
|
||||
next_target = EXPORT_SCREEN_FAILED;
|
||||
}
|
||||
break;
|
||||
case EXPORT_FORMAT_CSV:
|
||||
if (!export_database_as_csv(screen->db_cache, path)) {
|
||||
warnx("export to CSV failed: \"%s\"", path);
|
||||
next_target = EXPORT_SCREEN_FAILED;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
warnx("unknown export format");
|
||||
next_target = EXPORT_SCREEN_FAILED;
|
||||
break;
|
||||
}
|
||||
LOG_VERBOSE("Export done\n");
|
||||
cleanup:
|
||||
pthread_cleanup_pop(true); // free(path)
|
||||
drive_unmount(&screen->drives[screen->cur_drive], true);
|
||||
return next_target;
|
||||
}
|
||||
|
||||
static const char BACKGROUND_CHARS[] = {
|
||||
'|',
|
||||
'-'
|
||||
};
|
||||
#define NBACKGROUND_CHAR sizeof(BACKGROUND_CHARS)
|
||||
|
||||
static void export_do_background_action(ExportScreen *screen,
|
||||
SensorState *state,
|
||||
intptr_t(*action)(ExportScreen *screen)) {
|
||||
if (!screen->bg_inited) {
|
||||
if (pthread_create(&screen->backgroud_oper, NULL,
|
||||
(void*(*)(void *)) action, screen)) {
|
||||
warnx("could not start background export operation");
|
||||
screen->stage = EXPORT_SCREEN_FAILED;
|
||||
}
|
||||
screen->bg_inited = true;
|
||||
screen->working_char = 0;
|
||||
}
|
||||
/*
|
||||
* True if done, false otherwise
|
||||
*/
|
||||
static bool export_do_background_action(ExportScreen *screen, SensorState *state) {
|
||||
if (state->down_down || state->up_down || state->back_down ||
|
||||
state->sel_down) {
|
||||
screen->last_cancel_stage = screen->stage;
|
||||
screen->stage = EXPORT_SCREEN_CONFIRM_CANCEL;
|
||||
screen->need_redraw = true;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!screen->current_op) {
|
||||
screen->stage = EXPORT_SCREEN_FAILED;
|
||||
screen->need_redraw = true;
|
||||
return false;
|
||||
}
|
||||
if (export_check_bg_task(screen)) {
|
||||
return true;
|
||||
}
|
||||
time_t now = time(NULL);
|
||||
if (export_check_bg_task(screen)) {
|
||||
// bg task done
|
||||
return;
|
||||
}
|
||||
if (screen->need_redraw || now != screen->last_char_change) {
|
||||
lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF,
|
||||
LCD_DISPLAY_ON);
|
||||
@ -312,6 +251,60 @@ static void export_do_background_action(ExportScreen *screen,
|
||||
lcd_write_string(state->lcd, "Working");
|
||||
lcd_write_char(state->lcd, BACKGROUND_CHARS[screen->working_char++ % NBACKGROUND_CHAR]);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void export_async_write_action(ExportScreen *screen) {
|
||||
signal(SIGINT, SIG_DFL);
|
||||
signal(SIGTERM, SIG_DFL);
|
||||
char *path = export_build_cur_path(screen);
|
||||
if (screen->need_overwrite && !recursive_rm(path)) {
|
||||
exit(1);
|
||||
}
|
||||
LOG_VERBOSE("Exporting to \"%s\"\n", path);
|
||||
switch (screen->format) {
|
||||
case EXPORT_FORMAT_SQLITE:
|
||||
if (!export_database(screen->db_cache, path)) {
|
||||
errx(1, "export to sqlite3 db failed: \"%s\"", path);
|
||||
}
|
||||
break;
|
||||
case EXPORT_FORMAT_CSV:
|
||||
if (!export_database_as_csv(screen->db_cache, path)) {
|
||||
errx(1, "export to CSV failed: \"%s\"", path);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
errx(1, "unknown export format");
|
||||
break;
|
||||
}
|
||||
free(path);
|
||||
LOG_VERBOSE("Export done\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
static void export_handle_write(ExportScreen *screen, SensorState *state) {
|
||||
if (screen->current_op) {
|
||||
if (export_do_background_action(screen, state)) {
|
||||
screen->need_redraw = true;
|
||||
screen->stage = EXPORT_SCREEN_UNMOUNT;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// if we have no current op, start a new one
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
warn("failed to start write background task");
|
||||
screen->need_redraw = true;
|
||||
screen->stage = EXPORT_SCREEN_FAILED;
|
||||
return;
|
||||
} else if (pid == 0) {
|
||||
// child
|
||||
export_async_write_action(screen);
|
||||
abort();
|
||||
} else {
|
||||
// parent
|
||||
screen->current_op = async_operation_take(NULL, NULL, NULL, NULL, pid);
|
||||
}
|
||||
}
|
||||
|
||||
static bool export_screen_dispatch(ExportScreen *screen,
|
||||
@ -349,10 +342,29 @@ static bool export_screen_dispatch(ExportScreen *screen,
|
||||
}
|
||||
break;
|
||||
case EXPORT_SCREEN_WRITE_NEWFS:
|
||||
export_do_background_action(screen, state, export_newfs_action);
|
||||
if (!screen->current_op) {
|
||||
screen->current_op = drive_newfs_msdos(cur_drive);
|
||||
}
|
||||
if (export_do_background_action(screen, state)) {
|
||||
screen->stage = EXPORT_SCREEN_MOUNTING;
|
||||
screen->need_redraw = true;
|
||||
}
|
||||
break;
|
||||
case EXPORT_SCREEN_MOUNTING:
|
||||
export_do_background_action(screen, state, export_mount_action);
|
||||
if (!screen->current_op) {
|
||||
screen->current_op = drive_mount(cur_drive);
|
||||
}
|
||||
if (export_do_background_action(screen, state)) {
|
||||
char *path = export_build_cur_path(screen);
|
||||
struct stat statbuf;
|
||||
if (stat(path, &statbuf) == 0) {
|
||||
screen->stage = EXPORT_SCREEN_CONFIRM_OVERWRITE;
|
||||
} else {
|
||||
screen->stage = EXPORT_SCREEN_WRITING;
|
||||
}
|
||||
screen->need_redraw = true;
|
||||
free(path);
|
||||
}
|
||||
break;
|
||||
case EXPORT_SCREEN_CONFIRM_OVERWRITE:
|
||||
confirm_status = export_confirm_action(screen, state, "Overwrite?");
|
||||
@ -364,20 +376,31 @@ static bool export_screen_dispatch(ExportScreen *screen,
|
||||
screen->need_redraw = true;
|
||||
// on the main thread so force unmount
|
||||
// also, nothing should be writing, so kill it if it is
|
||||
drive_unmount(cur_drive, true);
|
||||
AsyncOperation *op = drive_unmount(cur_drive, true);
|
||||
async_operation_poll(op, true);
|
||||
async_operation_free(op);
|
||||
}
|
||||
break;
|
||||
case EXPORT_SCREEN_WRITING:
|
||||
export_do_background_action(screen, state, export_write_action);
|
||||
export_handle_write(screen, state);
|
||||
break;
|
||||
case EXPORT_SCREEN_UNMOUNT:
|
||||
if (!screen->current_op) {
|
||||
screen->current_op = drive_unmount(cur_drive, false);
|
||||
}
|
||||
if (export_do_background_action(screen, state)) {
|
||||
screen->stage = EXPORT_SCREEN_DONE;
|
||||
screen->need_redraw = true;
|
||||
}
|
||||
break;
|
||||
case EXPORT_SCREEN_CONFIRM_CANCEL:
|
||||
confirm_status = export_confirm_action(screen, state, "Really cancel?");
|
||||
if (confirm_status == EXPORT_CONFIRM_YES) {
|
||||
kill(0, SIGUSR1);
|
||||
pthread_cancel(screen->backgroud_oper);
|
||||
pthread_detach(screen->backgroud_oper);
|
||||
drive_unmount(cur_drive, true);
|
||||
screen->bg_inited = false;
|
||||
async_operation_free(screen->current_op);
|
||||
screen->current_op = NULL;
|
||||
AsyncOperation *op = drive_unmount(cur_drive, true);
|
||||
async_operation_poll(op, true);
|
||||
async_operation_free(op);
|
||||
screen->stage = EXPORT_SCREEN_CANCELED;
|
||||
screen->need_redraw = true;
|
||||
} else if (confirm_status == EXPORT_CONFIRM_NO) {
|
||||
@ -398,10 +421,7 @@ static bool export_screen_dispatch(ExportScreen *screen,
|
||||
}
|
||||
|
||||
static void export_screen_cleanup(ExportScreen *screen) {
|
||||
if (screen->bg_inited) {
|
||||
pthread_cancel(screen->backgroud_oper);
|
||||
pthread_detach(screen->backgroud_oper);
|
||||
}
|
||||
async_operation_free(screen->current_op);
|
||||
}
|
||||
|
||||
ExportScreen *export_screen_new() {
|
||||
@ -412,5 +432,6 @@ ExportScreen *export_screen_new() {
|
||||
s->read_drives = true;
|
||||
s->stage = EXPORT_SCREEN_FORMAT;
|
||||
s->format = EXPORT_FORMAT_SQLITE;
|
||||
s->current_op = NULL;
|
||||
return s;
|
||||
}
|
||||
|
@ -11,11 +11,11 @@
|
||||
#define INCLUDED_EXPORTSCREEN_H
|
||||
|
||||
#include "screen.h"
|
||||
#include "../async.h"
|
||||
#include "../drive.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
typedef struct {
|
||||
@ -28,6 +28,7 @@ typedef struct {
|
||||
EXPORT_SCREEN_MOUNTING,
|
||||
EXPORT_SCREEN_CONFIRM_OVERWRITE,
|
||||
EXPORT_SCREEN_WRITING,
|
||||
EXPORT_SCREEN_UNMOUNT,
|
||||
EXPORT_SCREEN_DONE,
|
||||
EXPORT_SCREEN_CONFIRM_CANCEL,
|
||||
EXPORT_SCREEN_CANCELED,
|
||||
@ -47,13 +48,12 @@ typedef struct {
|
||||
size_t ndrive;
|
||||
size_t cur_drive;
|
||||
int confirm_state;
|
||||
bool bg_inited;
|
||||
pthread_t backgroud_oper;
|
||||
int working_char;
|
||||
time_t last_char_change;
|
||||
bool need_overwrite;
|
||||
sqlite3 *db_cache;
|
||||
int last_cancel_stage;
|
||||
AsyncOperation *current_op;
|
||||
} ExportScreen;
|
||||
|
||||
ExportScreen *export_screen_new(void);
|
||||
|
98
src/util.c
98
src/util.c
@ -17,9 +17,8 @@
|
||||
#include <dirent.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <errno.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/signal.h>
|
||||
|
||||
const char *PERIOD_LABELS[] = {
|
||||
"HOUR",
|
||||
@ -414,12 +413,6 @@ char *pad_humid_str(int humid, char *buf, size_t buf_size) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
static void close_dir_if_set(void *dir) {
|
||||
if (dir) {
|
||||
closedir(dir);
|
||||
}
|
||||
}
|
||||
|
||||
bool recursive_rm_at(int dfd, const char *path) {
|
||||
if (dfd != AT_FDCWD) {
|
||||
LOG_VERBOSE("Recursive delete: \"%s\"\n", path);
|
||||
@ -432,18 +425,12 @@ bool recursive_rm_at(int dfd, const char *path) {
|
||||
int flag = 0;
|
||||
if (S_ISDIR(statbuf.st_mode)) {
|
||||
flag = AT_REMOVEDIR;
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
|
||||
int fd = openat(dfd, path, O_DIRECTORY | O_RDONLY);
|
||||
if (fd < 0) {
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
warn("open \"%s\"", path);
|
||||
return false;
|
||||
}
|
||||
DIR *dir = NULL;
|
||||
bool error = false;
|
||||
pthread_cleanup_push(close_dir_if_set, dir);
|
||||
dir = fdopendir(fd);
|
||||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||||
DIR *dir = fdopendir(fd);
|
||||
if (!dir) {
|
||||
close(fd);
|
||||
warn("fdopendir \"%s\"", path);
|
||||
@ -453,15 +440,11 @@ bool recursive_rm_at(int dfd, const char *path) {
|
||||
while ((sd = readdir(dir))) {
|
||||
if (strcmp(sd->d_name, ".") != 0 && strcmp(sd->d_name, "..") != 0 &&
|
||||
!recursive_rm_at(dirfd(dir), sd->d_name)) {
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pthread_cleanup_pop(true);
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
if (unlinkat(dfd, path, flag)< 0) {
|
||||
warn("remove \"%s\"", path);
|
||||
return false;
|
||||
@ -474,48 +457,31 @@ bool recursive_rm(const char *path) {
|
||||
}
|
||||
|
||||
bool export_database(sqlite3 *db, const char *dest) {
|
||||
bool status = true;
|
||||
sqlite3 *new_db = NULL;
|
||||
sqlite3_backup *oper = NULL;
|
||||
pthread_cleanup_push((void(*)(void*))sqlite3_close, new_db);
|
||||
pthread_cleanup_push((void(*)(void*))sqlite3_backup_finish, oper);
|
||||
int res = sqlite3_open(dest, &new_db);
|
||||
if (res != SQLITE_OK) {
|
||||
warnx("could not open new db: \"%s\": %s", dest,
|
||||
sqlite3_errstr(res));
|
||||
status = false;
|
||||
goto cleanup;
|
||||
return false;
|
||||
}
|
||||
oper = sqlite3_backup_init(new_db, "main", db, "main");
|
||||
if (!oper) {
|
||||
warnx("could not start backup: \"%s\": %s", dest,
|
||||
sqlite3_errstr(res));
|
||||
status = false;
|
||||
goto cleanup;
|
||||
return false;
|
||||
}
|
||||
sqlite3_backup_step(oper, -1);
|
||||
cleanup:
|
||||
pthread_cleanup_pop(true); // cleanup oper
|
||||
pthread_cleanup_pop(true); // cleanup new_db
|
||||
return status;
|
||||
}
|
||||
|
||||
static void cleanup_file_obj(void *obj) {
|
||||
if (obj) {
|
||||
fclose(obj);
|
||||
}
|
||||
sqlite3_backup_finish(oper);
|
||||
sqlite3_close(new_db);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool export_database_as_csv(sqlite3 *db, const char *dest) {
|
||||
bool status = true;
|
||||
FILE *outfile = NULL;
|
||||
pthread_cleanup_push((void(*)(void *))sqlite3_reset, EXPORT_CSV_QUERY);
|
||||
pthread_cleanup_push(cleanup_file_obj, outfile);
|
||||
outfile = fopen(dest, "w");
|
||||
FILE *outfile = fopen(dest, "w");
|
||||
if (!outfile) {
|
||||
warn("fopen \"%s\"", dest);
|
||||
status = false;
|
||||
goto cleanup;
|
||||
return false;
|
||||
}
|
||||
int res;
|
||||
while ((res = sqlite3_step(EXPORT_CSV_QUERY)) == SQLITE_ROW) {
|
||||
@ -526,14 +492,11 @@ bool export_database_as_csv(sqlite3 *db, const char *dest) {
|
||||
if (res != SQLITE_DONE) {
|
||||
warnx("CSV export to \"%s\" faield: %s\n", dest,
|
||||
sqlite3_errstr(res));
|
||||
status = false;
|
||||
goto cleanup; // readability
|
||||
return false;
|
||||
}
|
||||
fflush(outfile);
|
||||
cleanup:
|
||||
pthread_cleanup_pop(true); // reset query
|
||||
pthread_cleanup_pop(true); // close file
|
||||
return status;
|
||||
sqlite3_reset(EXPORT_CSV_QUERY);
|
||||
fclose(outfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ensure_dir_exists(const char *path, mode_t mode) {
|
||||
@ -556,7 +519,7 @@ static bool ensure_dir_exists(const char *path, mode_t mode) {
|
||||
}
|
||||
|
||||
bool mkdirs(const char *path, mode_t mode) {
|
||||
LOG_VERBOSE("Creating directory: \"%s\"\n", path)
|
||||
LOG_VERBOSE("Creating directory: \"%s\"\n", path);
|
||||
char *copy = strdup_checked(path);
|
||||
char *work_str = copy, *token;
|
||||
while ((token = strsep(&work_str, "/"))) {
|
||||
@ -576,3 +539,32 @@ void request_restart() {
|
||||
RUNNING = false;
|
||||
SHOULD_RESTART = true;
|
||||
}
|
||||
|
||||
bool timed_wait(pid_t pid, int *out_status, useconds_t micro) {
|
||||
struct timespec waittime = {
|
||||
.tv_sec = micro / 1000000,
|
||||
.tv_nsec = micro % 1000000,
|
||||
};
|
||||
int status;
|
||||
struct timespec start, now, tmp;
|
||||
clock_gettime(CLOCK_UPTIME, &start);
|
||||
while (true) {
|
||||
if (waitpid(pid, &status, WNOHANG) < 0) {
|
||||
return false;
|
||||
}
|
||||
if (WIFEXITED(status) || WIFSIGNALED(status)) {
|
||||
if (out_status) {
|
||||
*out_status = status;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
clock_gettime(CLOCK_UPTIME, &now);
|
||||
timespecsub(&now, &start, &tmp);
|
||||
if (timespeccmp(&tmp, &waittime, >=)) {
|
||||
if (out_status) {
|
||||
*out_status = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
#include <sys/types.h>
|
||||
#include <libgpio.h>
|
||||
#include <sqlite3.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#define _STRINGIFY_LIT(s) (#s)
|
||||
#define STRINGIFY(v) _STRINGIFY_LIT(v)
|
||||
@ -254,5 +255,12 @@ bool mkdirs(const char *path, mode_t mode);
|
||||
* re-initialize configuration values.
|
||||
*/
|
||||
void request_restart(void);
|
||||
/*
|
||||
* waitpid(2) for PID for MICRO microseconds. If STATUS is not NULL, store the
|
||||
* exit status there. Use the various WIF* macros to test the status. This
|
||||
* function sets errno to the errno value of waitpid(2) on error.
|
||||
* Return: true if successful, false otherwise and the error is in errno
|
||||
*/
|
||||
bool timed_wait(pid_t pid, int *out_status, useconds_t micro);
|
||||
|
||||
#endif
|
||||
|
Loading…
Reference in New Issue
Block a user