LINUX.ORG.RU

Вывод многоуровневого сообщения об ошибке

 ,


0

4

Привет!

Имеется система:

входные данные -> преобразователь1 -> преобразователь2 -> ... -> выходные данные.

То есть цепочка преобразователей, что-то вроде трубы cat | grep | sed | wc. В зависимости от входных данных цепочки, пройдя через ряд преобразователей, ошибка может быть обнаружена где-то в середине цепочки, и, чтобы в сообщении об ошибке отразить полную информацию о причине проблемы, хочется выдать не просто «преобразователь3: некорректное поле Х в позиции У», а собрать всю цепочку входных данных для каждого предыдущего преобразователя. Примерно:

IN: 2 a b 1 c 4 d e f ->
proc1 -> (2 a b) (1 c) (4 d e f) ->
proc2 -> (a b) (c) ... FAIL: too few fields in (d e f), received 3, expected 4.

И вот вопрос, как правильно (или принято? или просто элегантные идеи?) собирать такие сообщения воедино? У меня две мысли.

1. Внутрь каждого преобразователя отправлять вместе с входными данными и историю входных данных предшественников, в случае ошибки сигнализировать об исключении, в общем, что-то вроде ведения лога. Плюс: одно место обработки ошибки; минус: засорение интерфейсов преобразователей и как следствие возня с накоплением истории и усложнение вызовов последователей. Возможно, минус этот можно избежать, но ничего кроме еще больших костылей вроде глобальных переменных, в голову не лезет.

2. Каждый преобразователь умеет сигнализировать об исключении в рамках своих входных данных, но в то же время ловит исключения последователя, к этому исключению аттачит свои входные данные, бросает исключение еще выше и т.д. Плюс: мне кажется логичным. Минус: лапша однотипных обработчиков ошибок. Ок, в лиспе можно спрятать в макру, но как быть с этим например в С++?

Какие еще есть идеи? Уверен, что проблема типичная, но сформулировать краткий и емкий вопрос в гугл не получается.

★★★★★
Ответ на: комментарий от theNamelessOne

Т.е. оберни входные аргументы некоторым типом, который может кроме них содержать данные об ошибке (лог). В теле функции лови ошибку, заворачивай ее в свой тип и передавай на вход следующему преобразователю. Если предыдущий преобразователь завершился с ошибкой (об этом можно узнать из входных данных), то тело функции не выполняется, а ошибка сразу распространяется дальше, с добавлением информации о текущем преобразователе.

Все это можно обернуть макросом над defun, мне кажется.

theNamelessOne ★★★★★
()

На плюсах при помощи макросов прекрасно делается вывод сообщений об ошибках. Например:

...
WERR( a, b, a+b*c )
...
выхлоп:
# test.cpp l123: a=1 b=false a+b*c=1
более лаконичного в использовании варианта я не видел. Правда делается это извращенно - WERR то макрос, но надо еще перегружать оператор запятую;-)

По дизайну, если есть стандартные преобразователи и стандартные интерфейсы (те же пайпы и греп) то тока на уровне шелла наверное - пишется свой шелл, который последовательно запускает преобразователи, хранит рез-таты между запусками и проверяет коды возврата. Если интерфейс имени себя и преобразователи тоже все свои, то я бы завел некую шнягу к-я накапливала бы стек вызовов, в простейшем случае это просто общий файл с логом - каждый преобразователь в этот файл че то пишет. Единственный вопрос - как сообщить преобразователям куда писать, в простейшем случае это файл с именем на основе пида родителя.

AIv ★★★★★
()
Ответ на: комментарий от AIv

На плюсах при помощи макросов прекрасно делается вывод сообщений об ошибках.

Господи, какой же ты все-таки тупой.

Спрашивают, как лучше получить предысторию возникновения ошибки, а ты рассказываешь про макросы для распечатывания.

Издали видно, что питонист и плюсист.

anonymous
()
Ответ на: комментарий от anonymous

А ты во первых хам, во вторых читать не умеешь... уж и не знаю, что хуже. Тех кто вынужден с тобой жить и общаться откровенно жалко...

И да, с чего ты решил что тут кого то волнует твое мнение?

AIv ★★★★★
()
Ответ на: комментарий от AIv

А ты во первых хам

Это лучше, чем тупой.

во вторых читать не умеешь

А это просто неправда.

И да, с чего ты решил что тут кого то волнует твое мнение?

Ну вот тебя явно взволновало.

anonymous
()
Ответ на: комментарий от AIv

Во-первых, у тебя баттхерт, а во вторых - если человеку плевать, он не пишет об этом гневные посты, ня.

anonymous
()
Ответ на: комментарий от AIv

По дизайну, если есть стандартные преобразователи и стандартные интерфейсы (те же пайпы и греп) то тока на уровне шелла наверное - пишется свой шелл, который последовательно запускает преобразователи, хранит рез-таты между запусками и проверяет коды возврата. Если интерфейс имени себя и преобразователи тоже все свои, то я бы завел некую шнягу к-я накапливала бы стек вызовов, в простейшем случае это просто общий файл с логом - каждый преобразователь в этот файл че то пишет. Единственный вопрос - как сообщить преобразователям куда писать, в простейшем случае это файл с именем на основе пида родителя.

Вопрос не про шелл и тем более межпроцессное взаимодействие. Преобразователи - это функции на лиспе или С++. Или питоне. Вызов цепочки в простейшем случае может выглядеть так:

std::string in = read_data();
std::string out = process3(process2(process1(in)));

processN - это и есть преобразователи, в приведенном примере может показаться, что на вход всех подается строка и на выходе тоже строка, но в общем случае типы данных произвольные с условием, что соседние преобразователи понимают друг друга. Каждый из преобразователей при обработке своих входных данных может сигнализировать об ошибке, чтобы понять которую, нужно как-то собрать всю историю предыдущих преобразований данных в цепочке.

staseg ★★★★★
() автор топика
Ответ на: комментарий от staseg

И, чтобы никого не путать, все функции написаны на одном языке.

staseg ★★★★★
() автор топика

Транзакционный лог?

Перед вызовом первого преобразователя загоняешь данные в лог (*). Каждый последующий преобразователь в начале своей работы записывает в лог, что он начал работу. Если где-то произошла ошибка, то она заносится в лог и транзакция коммитится. После этого она более не доступна. Если вся цепочка отработала без ошибок, то в конце транзакция просто прибивается. Итого: в случае ошибки ты получишь данные, подаваемые на вход преобразователя 1, и последовательность вызовов, в случае успеха — ничего, дабы не замусоривать лог. Если какому-то преобразователю сильно хочется, то он может и свои воходные данные логгировать, хотя это выглядит явно избыточным, поскольку может быть потом воспроизведено. Каждому участнику в цепочке понадобится всего одно число — ID текущей транзакции. Оно ставится известно после шага, помеченного (*). Если средства платформы и языка позволяют, то оно может даже не передаваться явно через интерфейс функций. Оно может храниться, скажем, в thread local storage.

fang
()
Ответ на: комментарий от staseg

в питоне вообще все просто - стек открыт, ковыряйся в нем скока влезет средствами ЯП. В плюсах чуть сложнее - я бы сделал список строк, в который каждый преобразователь че нить добавлял бы от себя, при исклбчении список выводился, при нормальном завершении цепочки чистился.

В простейшем случае список глобальный, тогда его все видят, чистить можно руками, можно по деструктору создавая перед вызовом какой нить объект. В многопоточном случае все хуже... Можно конечно изголиться, завернуть аргументы-результаты преобразователя в какой нить класс (в кортеж бустовый), туда же прикоцапить список, и пусть он из преобразователя в преобразователь гуляет. На шаблонах это можно сделать относительно изящно...

AIv ★★★★★
()
Ответ на: комментарий от one_more_hokum

это типа комплимент?;-) Макросы не так страшны, как их малюют.

AIv ★★★★★
()
Ответ на: комментарий от one_more_hokum

забавно, что для того, чтоб получить средства, доступные в лиспе из коробки, требуется городить адов треш и угар (см. boost::Wave).

anonymous
()

В си можно примерно так делать:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <limits.h>

char *error_message = NULL;

/* `goto error' on falsity */
#define check(e) do { if (!(e)) goto error; } while (0)

#define check_with_message(e, m) \
    do { if (!(e)) { error_message = (m); goto error; }} while (0)

/* when `-1' on errors */
#define libc(e) check((e) != -1)
#define libc_with_message(e, m) check_with_message((e) != -1, (m))

/* when `NULL' on errors */
#define check_memory(e) check((e) != NULL)
#define check_memory_with_message(e, m) check_with_message((e) != NULL, (m))

#define log(M, ...)                                                             \
    do {                                                                        \
        fprintf(stderr, "(%s:%d: errno: %s) " M "\n", __FILE__, __LINE__,       \
                (errno == 0 ? "0" : strerror(errno)), ##__VA_ARGS__);           \
        if (error_message)                                                      \
            fprintf(stderr, "^ additional information: %s\n", error_message);   \
    } while (0)

#ifndef FAIL
/* as NULL == ((void*)0) or false == ((bool)0) */
#define fail do { error_message = NULL; return 0; } while (0)
#else
#define fail exit(EXIT_FAILURE)
#endif

/* as true == ((bool)1) */
#define ok return 1;

bool parse_long(long *res, const char *str)
{
    check_memory_with_message(res, "null `res' pointer");
    check_memory_with_message(str, "null `str' pointer");

    errno = 0;
    char *endptr;
    long val = strtol(str, &endptr, 10);

    check(!(errno == ERANGE && (val == LONG_MAX || val == LONG_MIN)));
    check(!(errno != 0 && val == 0));
    check(endptr != str);

    *res = val;
    ok;
error:
    log("problems in parse_long, bad number: %s", str);
    fail;
}

bool proc1(const char *prog, const char *file)
{
    check_memory_with_message(prog, "null `prog' pointer");
    check_memory_with_message(file, "null `file' pointer");

    libc_with_message(access(prog, X_OK), (char*)prog);
    libc_with_message(access(file, R_OK), (char*)file);
    ok;
error:
    log("problems in proc1");
    fail;
}

int *proc2(const char *prog, const char *file, size_t words)
{
    check_memory_with_message(prog, "null `prog' pointer");
    check_memory_with_message(file, "null `file' pointer");

    int *p = NULL;
    check_memory(p = calloc(words, sizeof(int)));
    // put `p' in a garbage pool here
    check(proc1(prog, file));

    printf("ok, prog = %s, file = %s, words = %zd, p = %p, p[%zd] = %d\n",
           prog, file, words, (void*)p, words - 1, p[words - 1]);

    return p;
error:
    log("problems in proc2");
    free(p); // we can free NULL;
    fail;
}

int main(int argc, char **argv)
{
    check_with_message(argc == 4, "bad number of arguments");

    char *prog = argv[1];
    char *file = argv[2];
    long words;

    check(parse_long(&words, argv[3]));
    check(proc2(prog, file, words));
    check(proc1(prog, file));

    // free the garbage pool here

    exit(EXIT_SUCCESS);
error:
    log("problems in main");
    exit(EXIT_FAILURE);
}
$ ./err
(err.c:113: errno: 0) problems in main
^ additional information: bad number of arguments
$ ./err ls 111 q
(err.c:60: errno: 0) problems in parse_long, bad number: q
(err.c:113: errno: 0) problems in main
$ ./err ls 111 5
(err.c:73: errno: No such file or directory) problems in proc1
^ additional information: ls
(err.c:92: errno: No such file or directory) problems in proc2
(err.c:113: errno: No such file or directory) problems in main
$ ./err /bin/ls 111 5
(err.c:73: errno: No such file or directory) problems in proc1
^ additional information: 111
(err.c:92: errno: No such file or directory) problems in proc2
(err.c:113: errno: No such file or directory) problems in main
$ ./err /bin/ls err.c 5
ok, prog = /bin/ls, file = err.c, words = 5, p = 0x12f8010, p[4] = 0
$ ./err err.c err.c 5
(err.c:73: errno: Permission denied) problems in proc1
^ additional information: err.c
(err.c:92: errno: Permission denied) problems in proc2
(err.c:113: errno: Permission denied) problems in main

В «хорошем» блоке функции, до метки error, предполагается возвращать только хорошие значения - положительные указатели или true, делать return для false или NULL _не_ предполагается. Функции имеют тип возвращаемого значения bool или T*, всё остальное возвращается через заполнение аргументов-указателей. Если control flow не дошёл в «хорошем» блоке до возвращения true или положительного указателя, то мы автоматически попадаем в «плохой блок» - пишем в лог, освобождаем ресурсы, падаем если нужно. Ну и проверяющие возвращаемые значения макросы могут насильно сделать goto error и перейти из хорошего блока в плохой, и если не падать, то при ошибке где-то глубоко происходит возвращение вверх по стеку с освобождением соответствующих ресурсов и печатью отладочной информации.

Неудобно тем, что «плохой» блок один - если нужно освобождать сразу много ресурсов, то приходится писать обвёртки которые игнорирую попытки освободить неинициализированные ресурсы (в ядре принято делать много меток для goto - в обратном порядке, но тогда макросы с автоматическим goto теряют смысл), ну и печатать что-то сложное тоже не очень удобно.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Или проще, как тут:

#define log(m, ...)                                              \
    fprintf(stderr, "(%s:%d) %s: " m "\n", __FILE__, __LINE__,   \
            (errno == 0 ? "" : strerror(errno)), ##__VA_ARGS__);

#define check(e, m, ...)                                                \
    do { if (!(e)) { log(m, ##__VA_ARGS__); errno = 0; goto error; }} while (0)

#define check_(e) do { if (!(e)) goto error; } while (0)

#define libc(e, m, ...) check((e) != -1, m, ##__VA_ARGS__)

#define memory(e, m, ...) check((e) != NULL, m, ##__VA_ARGS__)

#ifndef FAIL
#define fail return 0
#else
#define fail exit(EXIT_FAILURE)
#endif

#define ok return 1
$ ./err err.c err.c 5
(err.c:56) Permission denied: err.c
(err.c:60) : problems in proc1
(err.c:79) : problems in proc2
(err.c:100) : problems in main
quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

Неудобно тем, что «плохой» блок один

Неудобно тем, что немногопоточно by design

(в ядре принято делать много меток для goto - в обратном порядке, но тогда макросы с автоматическим goto теряют смысл

Передавать в макрос метку, на которую надо прыгнуть в случае ошибки?

fang
()
Ответ на: комментарий от fang

Неудобно тем, что немногопоточно by design

Хотя это решаемо. Тут я немного поторопился

fang
()
Ответ на: комментарий от anonymous

Плюсовые шаблоны если-б развивали как следует, и предоставили им возможность взаимодействия с самим языком, как, например, это в D сделано — то радость.

one_more_hokum ★★★
()
Ответ на: комментарий от fang

Неудобно тем, что немногопоточно by design

Если про error_message, то __thread, но лучше вообще без него, как в примере чуть ниже. Идея в том, чтобы следить за возвращаемыми значениями - возвращать либо плохие (NULL, false, -1), либо хорошие (указатель, true, положительное число) и никогда их не терять, то есть если функция g вызывает функцию f которая использует подход с возвращаемыми значениями, то функция g тоже должна его использовать. Ну это само собой, в libc / POSIX много таких функций. И в том, чтобы централизовать обработку ошибок в конце функции - хорошие значения (которые могут быть разными - числа, указатели) возвращать отовсюду в хорошем блоке, но освобождать ресурсы при сбое и возвращать плохое значение (которое всегда одно - NULL, -1, false) только в одном месте, ну и чуть что, сразу идти в плохой блок. Так можно откатиться при сбое от произвольной функции вверх до произвольной, освобождая ресурсы и печатая сообщения.

Передавать в макрос метку, на которую надо прыгнуть в случае ошибки?

Можно, это вопрос стиля, можно просто if и goto явно использовать. С макросами сокращение не очень большое, но требует вникать - что тут за макросы. Я их скорее для идиоматичности привёл - может быть много ok (retrun хорошего значения) в хорошем блоке и только один fail (return плохого) в плохом.

quasimoto ★★★★
()

«преобразователь» оформить в виде класса, который реализует интерфейс и «знает» вызывающего, метод для получения текста ошибки вызывает аналогичный метод для родителя (если он есть) + склеивает со своими данными

wota ★★
()
Ответ на: комментарий от quasimoto

Ещё хорошо, когда всё инициализируется дефолтными значениями - указатели NULL-ами, память - нулями (calloc). И когда освобождение дефолтного значения игнорируется (как free(NULL), но не как fclose(NULL), в последнем случае нужна fclose(FILE**) которая будет игнорировать NULL и выставлять FILE* f = NULL при успешном закрытии).

quasimoto ★★★★
()
Ответ на: комментарий от staseg

Продолжаю свою мысль, я имел в виду что-то такое:

(defclass args-wrapper ()
   ((args :initarg :args :initform nil :accessor args)
    (err :initarg :err :initform nil :accessor error-flag)
    (stack-trace :initarg :stack-trace :initform nil :accessor stack-trace)))
 
(defun wrap (&rest args)
   (make-instance 'args-wrapper :args args))

(defun unwrap (wrapped-args)
  (if (error-flag wrapped-args)
      (error (make-condition 'transformation-error 
                             :stack-trace (stack-trace wrapped-args)))
      (values-list (args wrapped-args))))

(define-condition transformation-error (error)
  ((stack-trace :initarg :stack-trace :reader stack-trace))
  (:report (lambda (condition stream)
             (format stream "Transformation error:~%~a:~%~{~4t~a~%~}"
                     (car (stack-trace condition))
                     (cdr (stack-trace condition)))))) 

(defmacro define-transformer (name (&rest params) &body body)
   `(defun ,name (wrapped-args)
      (if (error-flag wrapped-args)
          wrapped-args
          (progn
            (push (cons ',name (args wrapped-args)) (stack-trace wrapped-args))
            (let ((new-args
                   (multiple-value-list 
                    (handler-case (apply (lambda ,params ,@body) 
                                         (args wrapped-args))
                      (condition (c)
                        (progn
                          (push (stack-trace wrapped-args) c)
                          (setf (error-flag wrapped-args) t)
                          wrapped-args))))))
              (setf (args wrapped-args) new-args)
              wrapped-args)))))

(define-transformer sum (&rest args)
  (reduce #'+ args))

(define-transformer always-fail (&rest args)
  (declare (ignore args))
  (error "This transformation always fails"))

(define-transformer min-max (val1 val2)
  (cond ((= val1 val2)
         (error "MIN-MAX: expected two different numbers; given ~a ~a" val1 val2))
        ((< val1 val2)
         (values val1 val2))
        (t
         (values val2 val1))))

(define-transformer range (a b)
  (values-list (loop for i from a to b collecting i)))

Обрати внимание, что можно объявлять преобразователи от многих переменных.

Примеры:

CL-USER> (unwrap (sum (range (min-max (wrap 5 2)))))
14

CL-USER> (unwrap (sum (range (min-max (wrap 5 5)))))
Transformation error:
(MIN-MAX 5 5):

   [Condition of type TRANSFORMATION-ERROR]
CL-USER> (unwrap (always-fail (sum (range (min-max (wrap 1 5))))))
Transformation error:
(ALWAYS-FAIL 15):
    (SUM 1 2 3 4 5)
    (RANGE 1 5)
    (MIN-MAX 1 5)

   [Condition of type TRANSFORMATION-ERROR]

Только нужно сообщения об ошибках допилить, а то выводит не совсем то, что нужно.

theNamelessOne ★★★★★
()
Ответ на: комментарий от theNamelessOne

Спасибо. Тоже появилась одна задумка, как будет время, напишу, выложу.

staseg ★★★★★
() автор топика
Ответ на: комментарий от quasimoto

Очень «сишное» решение :) При использовании неудобно и много телодвижений.

staseg ★★★★★
() автор топика
Ответ на: комментарий от wota

Вот пожалуй пока самая хорошая и лаконичная в использовании идея для С++. Спасибо.

staseg ★★★★★
() автор топика

минус: засорение интерфейсов преобразователей

В CL есть метаданные? Если да, то можно цеплять их на результат преобразования на каждом этапе. В итоге интерфейсы не засоряются, про данные об ошибке знает только тот, кому нужно.

unlog1c ★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.