static bool read_script(struct key *key) { static struct buffer input_buffer; static const char *line = ""; enum status_code code; if (!line || !*line) { if (input_buffer.data && *input_buffer.data == ':') { line = "<Enter>"; memset(&input_buffer, 0, sizeof(input_buffer)); } else if (!io_get(&script_io, &input_buffer, '\n', true)) { io_done(&script_io); return false; } else { line = input_buffer.data; } } code = get_key_value(&line, key); if (code != SUCCESS) die("Error reading script: %s", get_status_message(code)); return true; }
bool io_complete(enum io_type type, const char **argv, const char *dir, int fd) { struct io io; return io_exec(&io, type, dir, NULL, argv, fd) && io_done(&io); }
static int io_load_file(struct io *io, const char *separators, size_t *lineno, io_read_fn read_property, void *data) { struct buffer buf; int state = OK; while (state == OK && io_get_line(io, &buf, '\n', lineno, TRUE)) { char *name; char *value; size_t namelen; size_t valuelen; name = chomp_string(buf.data); namelen = strcspn(name, separators); if (name[namelen]) { name[namelen] = 0; value = chomp_string(name + namelen + 1); valuelen = strlen(value); } else { value = ""; valuelen = 0; } state = read_property(name, namelen, value, valuelen, data); } if (state != ERR && io_error(io)) state = ERR; io_done(io); return state; }
/** Testuje wczytywanie drzewa. @param state Środowisko testowe. */ static void trie_load_test(void** state) { Trie *trie = NULL; IO *io = io_new(stdin, stdout, stderr); push_word_to_io_mock(L""); trie = trie_load(io); assert_non_null(trie); trie_done(trie); // Poprawny zapis push_word_to_io_mock(L"ciupagą*^^^^^^^\n"); trie = trie_load(io); pop_remaining_chars(io); assert_true(trie_has_word(trie, L"ciupagą")); assert_false(trie_has_word(trie, L"ciupaga")); assert_false(trie_has_word(trie, L"ciupag")); assert_false(trie_has_word(trie, L"ciupagąą")); trie_done(trie); // Próba dojścia wyżej niż korzeń push_word_to_io_mock(L"a*^^\n"); trie = trie_load(io); pop_remaining_chars(io); assert_null(trie); // Znaki spoza alfabetu push_word_to_io_mock(L"&*^\n"); trie = trie_load(io); pop_remaining_chars(); assert_null(trie); io_done(io); }
bool io_read_buf(struct io *io, char buf[], size_t bufsize) { struct buffer result = {0}; if (io_get(io, &result, '\n', TRUE)) { result.data = chomp_string(result.data); string_ncopy_do(buf, bufsize, result.data, strlen(result.data)); } return io_done(io) && result.data; }
bool index_diff(struct index_diff *diff, bool untracked, bool count_all) { const char *untracked_arg = !untracked ? "--untracked-files=no" : count_all ? "--untracked-files=all" : "--untracked-files=normal"; const char *status_argv[] = { "git", "status", "--porcelain", "-z", untracked_arg, NULL }; struct io io; struct buffer buf; bool ok = true; memset(diff, 0, sizeof(*diff)); if (!io_run(&io, IO_RD, repo.cdup, NULL, status_argv)) return false; while (io_get(&io, &buf, 0, true) && (ok = buf.size > 3)) { if (buf.data[0] == '?') diff->untracked++; /* Ignore staged but unmerged entries. */ else if (buf.data[0] != ' ' && buf.data[0] != 'U') diff->staged++; if (buf.data[1] != ' ') diff->unstaged++; if (!count_all && diff->staged && diff->unstaged && (!untracked || diff->untracked)) break; /* Skip source filename in rename */ if (buf.data[0] == 'R') { io_get(&io, &buf, 0, true); } } if (io_error(&io)) ok = false; io_done(&io); return ok; }
/** Testuje zapisywanie drzewa. @param state Środowisko testowe. */ static void trie_save_test(void** state) { Trie *trie = trie_new(); FILE *stream; wchar_t *buf = NULL; size_t len; stream = open_wmemstream(&buf, &len); if (stream == NULL) { fprintf(stderr, "Failed to open memory stream\n"); exit(EXIT_FAILURE); } IO *io = io_new(stdin, stream, stderr); assert_true(trie_save(trie, io) == 0); fflush(stream); assert_true(wcscmp(L"\n", buf) == 0); fseek(stream, 0, SEEK_SET); trie_insert_word(trie, L"ciupaga"); assert_true(trie_save(trie, io) == 0); fflush(stream); assert_true(wcscmp(L"ciupaga*^^^^^^^\n", buf) == 0); fseek(stream, 0, SEEK_SET); trie_delete_word(trie, L"ciupaga"); assert_true(trie_save(trie, io) == 0); fflush(stream); assert_true(wcscmp(L"\n", buf) == 0); fclose(stream); io_done(io); # undef free free(buf); # define free(ptr) _test_free(ptr, __FILE__, __LINE__) trie_done(trie); }
static void setup_blame_parent_line(struct view *view, struct blame *blame) { char from[SIZEOF_REF + SIZEOF_STR]; char to[SIZEOF_REF + SIZEOF_STR]; const char *diff_tree_argv[] = { "git", "diff", encoding_arg, "--no-textconv", "--no-extdiff", "--no-color", "-U0", from, to, "--", NULL }; struct io io; int parent_lineno = -1; int blamed_lineno = -1; char *line; if (!string_format(from, "%s:%s", view->env->ref, view->env->file) || !string_format(to, "%s:%s", blame->commit->id, blame->commit->filename) || !io_run(&io, IO_RD, NULL, opt_env, diff_tree_argv)) return; while ((line = io_get(&io, '\n', TRUE))) { if (*line == '@') { char *pos = strchr(line, '+'); parent_lineno = atoi(line + 4); if (pos) blamed_lineno = atoi(pos + 1); } else if (*line == '+' && parent_lineno != -1) { if (blame->lineno == blamed_lineno - 1 && !strcmp(blame->text, line + 1)) { view->pos.lineno = parent_lineno ? parent_lineno - 1 : 0; break; } blamed_lineno++; } } io_done(&io); }
bool index_diff(struct index_diff *diff, bool untracked, bool count_all) { const char *untracked_arg = !untracked ? "--untracked-files=no" : count_all ? "--untracked-files=all" : "--untracked-files=normal"; const char *status_argv[] = { "git", "status", "--porcelain", "-z", untracked_arg, NULL }; struct io io; struct buffer buf; bool ok = TRUE; memset(diff, 0, sizeof(*diff)); if (!io_run(&io, IO_RD, repo.cdup, NULL, status_argv)) return FALSE; while (io_get(&io, &buf, 0, TRUE) && (ok = buf.size > 3)) { if (buf.data[0] == '?') diff->untracked++; else if (buf.data[0] != ' ') diff->staged++; if (buf.data[1] != ' ') diff->unstaged++; if (!count_all && diff->staged && diff->unstaged && (!untracked || diff->untracked)) break; } if (io_error(&io)) ok = FALSE; io_done(&io); return ok; }
static int state_scrub_process(struct snapraid_state* state, struct snapraid_parity_handle* parity_handle, block_off_t blockstart, block_off_t blockmax, struct snapraid_plan* plan, time_t now) { struct snapraid_io io; struct snapraid_handle* handle; void* rehandle_alloc; struct snapraid_rehash* rehandle; unsigned diskmax; block_off_t blockcur; unsigned j; unsigned buffermax; data_off_t countsize; block_off_t countpos; block_off_t countmax; block_off_t autosavedone; block_off_t autosavelimit; block_off_t autosavemissing; int ret; unsigned error; unsigned silent_error; unsigned io_error; unsigned l; unsigned* waiting_map; unsigned waiting_mac; char esc_buffer[ESC_MAX]; /* maps the disks to handles */ handle = handle_mapping(state, &diskmax); /* rehash buffers */ rehandle = malloc_nofail_align(diskmax * sizeof(struct snapraid_rehash), &rehandle_alloc); /* we need 1 * data + 2 * parity */ buffermax = diskmax + 2 * state->level; /* initialize the io threads */ io_init(&io, state, state->opt.io_cache, buffermax, scrub_data_reader, handle, diskmax, scrub_parity_reader, 0, parity_handle, state->level); /* possibly waiting disks */ waiting_mac = diskmax > RAID_PARITY_MAX ? diskmax : RAID_PARITY_MAX; waiting_map = malloc_nofail(waiting_mac * sizeof(unsigned)); error = 0; silent_error = 0; io_error = 0; /* first count the number of blocks to process */ countmax = 0; plan->countlast = 0; for (blockcur = blockstart; blockcur < blockmax; ++blockcur) { if (!block_is_enabled(plan, blockcur)) continue; ++countmax; } /* compute the autosave size for all disk, even if not read */ /* this makes sense because the speed should be almost the same */ /* if the disks are read in parallel */ autosavelimit = state->autosave / (diskmax * state->block_size); autosavemissing = countmax; /* blocks to do */ autosavedone = 0; /* blocks done */ /* drop until now */ state_usage_waste(state); countsize = 0; countpos = 0; plan->countlast = 0; /* start all the worker threads */ io_start(&io, blockstart, blockmax, &block_is_enabled, plan); state_progress_begin(state, blockstart, blockmax, countmax); while (1) { unsigned char* buffer_recov[LEV_MAX]; snapraid_info info; int error_on_this_block; int silent_error_on_this_block; int io_error_on_this_block; int block_is_unsynced; int rehash; void** buffer; /* go to the next block */ blockcur = io_read_next(&io, &buffer); if (blockcur >= blockmax) break; /* until now is scheduling */ state_usage_sched(state); /* one more block processed for autosave */ ++autosavedone; --autosavemissing; /* by default process the block, and skip it if something goes wrong */ error_on_this_block = 0; silent_error_on_this_block = 0; io_error_on_this_block = 0; /* if all the blocks at this address are synced */ /* if not, parity is not even checked */ block_is_unsynced = 0; /* get block specific info */ info = info_get(&state->infoarr, blockcur); /* if we have to use the old hash */ rehash = info_get_rehash(info); /* for each disk, process the block */ for (j = 0; j < diskmax; ++j) { struct snapraid_task* task; int read_size; unsigned char hash[HASH_SIZE]; struct snapraid_block* block; int file_is_unsynced; struct snapraid_disk* disk; struct snapraid_file* file; block_off_t file_pos; unsigned diskcur; /* if the file on this disk is synced */ /* if not, silent errors are assumed as expected error */ file_is_unsynced = 0; /* until now is misc */ state_usage_misc(state); /* get the next task */ task = io_data_read(&io, &diskcur, waiting_map, &waiting_mac); /* until now is disk */ state_usage_disk(state, handle, waiting_map, waiting_mac); /* get the task results */ disk = task->disk; block = task->block; file = task->file; file_pos = task->file_pos; read_size = task->read_size; /* by default no rehash in case of "continue" */ rehandle[diskcur].block = 0; /* if the disk position is not used */ if (!disk) continue; /* if the block is unsynced, errors are expected */ if (block_has_invalid_parity(block)) { /* report that the block and the file are not synced */ block_is_unsynced = 1; file_is_unsynced = 1; /* follow */ } /* if the block is not used */ if (!block_has_file(block)) continue; /* if the block is unsynced, errors are expected */ if (task->is_timestamp_different) { /* report that the block and the file are not synced */ block_is_unsynced = 1; file_is_unsynced = 1; /* follow */ } /* handle error conditions */ if (task->state == TASK_STATE_IOERROR) { ++io_error; goto bail; } if (task->state == TASK_STATE_ERROR) { ++error; goto bail; } if (task->state == TASK_STATE_ERROR_CONTINUE) { ++error; error_on_this_block = 1; continue; } if (task->state == TASK_STATE_IOERROR_CONTINUE) { ++io_error; if (io_error >= state->opt.io_error_limit) { /* LCOV_EXCL_START */ log_fatal("DANGER! Too many input/output read error in a data disk, it isn't possible to scrub.\n"); log_fatal("Ensure that disk '%s' is sane and that file '%s' can be accessed.\n", disk->dir, task->path); log_fatal("Stopping at block %u\n", blockcur); goto bail; /* LCOV_EXCL_STOP */ } /* otherwise continue */ io_error_on_this_block = 1; continue; } if (task->state != TASK_STATE_DONE) { /* LCOV_EXCL_START */ log_fatal("Internal inconsistency in task state\n"); os_abort(); /* LCOV_EXCL_STOP */ } countsize += read_size; /* now compute the hash */ if (rehash) { memhash(state->prevhash, state->prevhashseed, hash, buffer[diskcur], read_size); /* compute the new hash, and store it */ rehandle[diskcur].block = block; memhash(state->hash, state->hashseed, rehandle[diskcur].hash, buffer[diskcur], read_size); } else { memhash(state->hash, state->hashseed, hash, buffer[diskcur], read_size); } /* until now is hash */ state_usage_hash(state); if (block_has_updated_hash(block)) { /* compare the hash */ if (memcmp(hash, block->hash, HASH_SIZE) != 0) { unsigned diff = memdiff(hash, block->hash, HASH_SIZE); log_tag("error:%u:%s:%s: Data error at position %u, diff bits %u\n", blockcur, disk->name, esc(file->sub, esc_buffer), file_pos, diff); /* it's a silent error only if we are dealing with synced files */ if (file_is_unsynced) { ++error; error_on_this_block = 1; } else { log_error("Data error in file '%s' at position '%u', diff bits %u\n", task->path, file_pos, diff); ++silent_error; silent_error_on_this_block = 1; } continue; } } } /* buffers for parity read and not computed */ for (l = 0; l < state->level; ++l) buffer_recov[l] = buffer[diskmax + state->level + l]; for (; l < LEV_MAX; ++l) buffer_recov[l] = 0; /* until now is misc */ state_usage_misc(state); /* read the parity */ for (l = 0; l < state->level; ++l) { struct snapraid_task* task; unsigned levcur; task = io_parity_read(&io, &levcur, waiting_map, &waiting_mac); /* until now is parity */ state_usage_parity(state, waiting_map, waiting_mac); /* handle error conditions */ if (task->state == TASK_STATE_IOERROR) { ++io_error; goto bail; } if (task->state == TASK_STATE_ERROR) { ++error; goto bail; } if (task->state == TASK_STATE_ERROR_CONTINUE) { ++error; error_on_this_block = 1; /* if continuing on error, clear the missing buffer */ buffer_recov[levcur] = 0; continue; } if (task->state == TASK_STATE_IOERROR_CONTINUE) { ++io_error; if (io_error >= state->opt.io_error_limit) { /* LCOV_EXCL_START */ log_fatal("DANGER! Too many input/output read error in the %s disk, it isn't possible to scrub.\n", lev_name(levcur)); log_fatal("Ensure that disk '%s' is sane and can be read.\n", lev_config_name(levcur)); log_fatal("Stopping at block %u\n", blockcur); goto bail; /* LCOV_EXCL_STOP */ } /* otherwise continue */ io_error_on_this_block = 1; /* if continuing on error, clear the missing buffer */ buffer_recov[levcur] = 0; continue; } if (task->state != TASK_STATE_DONE) { /* LCOV_EXCL_START */ log_fatal("Internal inconsistency in task state\n"); os_abort(); /* LCOV_EXCL_STOP */ } } /* if we have read all the data required and it's correct, proceed with the parity check */ if (!error_on_this_block && !silent_error_on_this_block && !io_error_on_this_block) { /* compute the parity */ raid_gen(diskmax, state->level, state->block_size, buffer); /* compare the parity */ for (l = 0; l < state->level; ++l) { if (buffer_recov[l] && memcmp(buffer[diskmax + l], buffer_recov[l], state->block_size) != 0) { unsigned diff = memdiff(buffer[diskmax + l], buffer_recov[l], state->block_size); log_tag("parity_error:%u:%s: Data error, diff bits %u\n", blockcur, lev_config_name(l), diff); /* it's a silent error only if we are dealing with synced blocks */ if (block_is_unsynced) { ++error; error_on_this_block = 1; } else { log_fatal("Data error in parity '%s' at position '%u', diff bits %u\n", lev_config_name(l), blockcur, diff); ++silent_error; silent_error_on_this_block = 1; } } } /* until now is raid */ state_usage_raid(state); } if (silent_error_on_this_block || io_error_on_this_block) { /* set the error status keeping other info */ info_set(&state->infoarr, blockcur, info_set_bad(info)); } else if (error_on_this_block) { /* do nothing, as this is a generic error */ /* likely caused by a not synced array */ } else { /* if rehash is needed */ if (rehash) { /* store all the new hash already computed */ for (j = 0; j < diskmax; ++j) { if (rehandle[j].block) memcpy(rehandle[j].block->hash, rehandle[j].hash, HASH_SIZE); } } /* update the time info of the block */ /* and clear any other flag */ info_set(&state->infoarr, blockcur, info_make(now, 0, 0, 0)); } /* mark the state as needing write */ state->need_write = 1; /* count the number of processed block */ ++countpos; /* progress */ if (state_progress(state, &io, blockcur, countpos, countmax, countsize)) { /* LCOV_EXCL_START */ break; /* LCOV_EXCL_STOP */ } /* autosave */ if (state->autosave != 0 && autosavedone >= autosavelimit /* if we have reached the limit */ && autosavemissing >= autosavelimit /* if we have at least a full step to do */ ) { autosavedone = 0; /* restart the counter */ /* until now is misc */ state_usage_misc(state); state_progress_stop(state); msg_progress("Autosaving...\n"); state_write(state); state_progress_restart(state); /* drop until now */ state_usage_waste(state); } } state_progress_end(state, countpos, countmax, countsize); state_usage_print(state); if (error || silent_error || io_error) { msg_status("\n"); msg_status("%8u file errors\n", error); msg_status("%8u io errors\n", io_error); msg_status("%8u data errors\n", silent_error); } else { /* print the result only if processed something */ if (countpos != 0) msg_status("Everything OK\n"); } if (error) log_fatal("WARNING! Unexpected file errors!\n"); if (io_error) log_fatal("DANGER! Unexpected input/output errors! The failing blocks are now marked as bad!\n"); if (silent_error) log_fatal("DANGER! Unexpected data errors! The failing blocks are now marked as bad!\n"); if (io_error || silent_error) { log_fatal("Use 'snapraid status' to list the bad blocks.\n"); log_fatal("Use 'snapraid -e fix' to recover.\n"); } log_tag("summary:error_file:%u\n", error); log_tag("summary:error_io:%u\n", io_error); log_tag("summary:error_data:%u\n", silent_error); if (error + silent_error + io_error == 0) log_tag("summary:exit:ok\n"); else log_tag("summary:exit:error\n"); log_flush(); bail: /* stop all the worker threads */ io_stop(&io); for (j = 0; j < diskmax; ++j) { struct snapraid_file* file = handle[j].file; struct snapraid_disk* disk = handle[j].disk; ret = handle_close(&handle[j]); if (ret == -1) { /* LCOV_EXCL_START */ log_tag("error:%u:%s:%s: Close error. %s\n", blockcur, disk->name, esc(file->sub, esc_buffer), strerror(errno)); log_fatal("DANGER! Unexpected close error in a data disk.\n"); ++error; /* continue, as we are already exiting */ /* LCOV_EXCL_STOP */ } } free(handle); free(rehandle_alloc); free(waiting_map); io_done(&io); if (state->opt.expect_recoverable) { if (error + silent_error + io_error == 0) return -1; } else { if (error + silent_error + io_error != 0) return -1; } return 0; }
static int state_dry_process(struct snapraid_state* state, struct snapraid_parity_handle* parity_handle, block_off_t blockstart, block_off_t blockmax) { struct snapraid_io io; struct snapraid_handle* handle; unsigned diskmax; block_off_t blockcur; unsigned j; unsigned buffermax; int ret; data_off_t countsize; block_off_t countpos; block_off_t countmax; unsigned error; unsigned io_error; unsigned l; unsigned* waiting_map; unsigned waiting_mac; char esc_buffer[ESC_MAX]; handle = handle_mapping(state, &diskmax); /* we need 1 * data + 2 * parity */ buffermax = diskmax + 2 * state->level; /* initialize the io threads */ io_init(&io, state, state->opt.io_cache, buffermax, dry_data_reader, handle, diskmax, dry_parity_reader, 0, parity_handle, state->level); /* possibly waiting disks */ waiting_mac = diskmax > RAID_PARITY_MAX ? diskmax : RAID_PARITY_MAX; waiting_map = malloc_nofail(waiting_mac * sizeof(unsigned)); error = 0; io_error = 0; /* drop until now */ state_usage_waste(state); countmax = blockmax - blockstart; countsize = 0; countpos = 0; /* start all the worker threads */ io_start(&io, blockstart, blockmax, &block_is_enabled, 0); state_progress_begin(state, blockstart, blockmax, countmax); while (1) { void** buffer; /* go to the next block */ blockcur = io_read_next(&io, &buffer); if (blockcur >= blockmax) break; /* until now is scheduling */ state_usage_sched(state); /* for each disk, process the block */ for (j = 0; j < diskmax; ++j) { struct snapraid_task* task; int read_size; struct snapraid_block* block; struct snapraid_disk* disk; unsigned diskcur; /* until now is misc */ state_usage_misc(state); /* get the next task */ task = io_data_read(&io, &diskcur, waiting_map, &waiting_mac); /* until now is disk */ state_usage_disk(state, handle, waiting_map, waiting_mac); /* get the task results */ disk = task->disk; block = task->block; read_size = task->read_size; /* if the disk position is not used */ if (!disk) continue; /* if the block is not used */ if (!block_has_file(block)) continue; /* handle error conditions */ if (task->state == TASK_STATE_IOERROR) { ++io_error; goto bail; } if (task->state == TASK_STATE_ERROR) { ++error; goto bail; } if (task->state == TASK_STATE_ERROR_CONTINUE) { ++error; continue; } if (task->state == TASK_STATE_IOERROR_CONTINUE) { ++io_error; if (io_error >= state->opt.io_error_limit) { /* LCOV_EXCL_START */ log_fatal("DANGER! Too many input/output read error in a data disk, it isn't possible to scrub.\n"); log_fatal("Ensure that disk '%s' is sane and that file '%s' can be accessed.\n", disk->dir, task->path); log_fatal("Stopping at block %u\n", blockcur); goto bail; /* LCOV_EXCL_STOP */ } /* otherwise continue */ continue; } if (task->state != TASK_STATE_DONE) { /* LCOV_EXCL_START */ log_fatal("Internal inconsistency in task state\n"); os_abort(); /* LCOV_EXCL_STOP */ } countsize += read_size; } /* until now is misc */ state_usage_misc(state); /* read the parity */ for (l = 0; l < state->level; ++l) { struct snapraid_task* task; unsigned levcur; task = io_parity_read(&io, &levcur, waiting_map, &waiting_mac); /* until now is parity */ state_usage_parity(state, waiting_map, waiting_mac); /* handle error conditions */ if (task->state == TASK_STATE_IOERROR) { ++io_error; goto bail; } if (task->state == TASK_STATE_ERROR) { ++error; goto bail; } if (task->state == TASK_STATE_ERROR_CONTINUE) { ++error; continue; } if (task->state == TASK_STATE_IOERROR_CONTINUE) { ++io_error; if (io_error >= state->opt.io_error_limit) { /* LCOV_EXCL_START */ log_fatal("DANGER! Too many input/output read error in the %s disk, it isn't possible to scrub.\n", lev_name(levcur)); log_fatal("Ensure that disk '%s' is sane and can be read.\n", lev_config_name(levcur)); log_fatal("Stopping at block %u\n", blockcur); goto bail; /* LCOV_EXCL_STOP */ } continue; } if (task->state != TASK_STATE_DONE) { /* LCOV_EXCL_START */ log_fatal("Internal inconsistency in task state\n"); os_abort(); /* LCOV_EXCL_STOP */ } } /* count the number of processed block */ ++countpos; /* progress */ if (state_progress(state, &io, blockcur, countpos, countmax, countsize)) { /* LCOV_EXCL_START */ break; /* LCOV_EXCL_STOP */ } } state_progress_end(state, countpos, countmax, countsize); state_usage_print(state); bail: /* stop all the worker threads */ io_stop(&io); for (j = 0; j < diskmax; ++j) { struct snapraid_file* file = handle[j].file; struct snapraid_disk* disk = handle[j].disk; ret = handle_close(&handle[j]); if (ret == -1) { /* LCOV_EXCL_START */ log_tag("error:%u:%s:%s: Close error. %s\n", blockmax, disk->name, esc(file->sub, esc_buffer), strerror(errno)); log_fatal("DANGER! Unexpected close error in a data disk.\n"); ++error; /* continue, as we are already exiting */ /* LCOV_EXCL_STOP */ } } if (error || io_error) { msg_status("\n"); msg_status("%8u file errors\n", error); msg_status("%8u io errors\n", io_error); } else { msg_status("Everything OK\n"); } if (error) log_fatal("DANGER! Unexpected errors!\n"); if (io_error) log_fatal("DANGER! Unexpected input/output errors!\n"); free(handle); free(waiting_map); io_done(&io); if (error + io_error != 0) return -1; return 0; }