/*************************************************************************/
/* Copyright (c) 2014 Amir Plivatsky */
/* All rights reserved */
/* */
/* Use of the link grammar parsing system is subject to the terms of the */
/* license set forth in the LICENSE file included with this software. */
/* This license allows free redistribution and use in source and binary */
/* forms, with or without modification, subject to certain conditions. */
/* */
/*************************************************************************/
#include <stdlib.h>
#ifdef USE_WORDGRAPH_DISPLAY
#include <stdio.h>
#include <errno.h>
#include <stdint.h>
#ifdef HAVE_FORK
#include <unistd.h> /* fork() and execl() */
#include <sys/wait.h> /* waitpid() */
#endif
#ifdef HAVE_PRCTL
#include <sys/prctl.h> /* prctl() */
#endif
#include <signal.h> /* SIG* */
#include "print/print-util.h" /* for append_string */
#include "utilities.h" /* for dyn_str functions and UNREACHABLE */
#endif /* USE_WORDGRAPH_DISPLAY */
#include "api-structures.h"
#include "error.h"
#include "string-set.h"
#include "tok-structures.h"
#include "wordgraph.h"
#ifdef __APPLE__
#define POPEN_DOT
#endif /* __APPLE__ */
#if USE_WORDGRAPH_DISPLAY || defined(DEBUG)
GNUC_UNUSED const char *gword_morpheme(Sentence sent, const Gword *w)
{
const char *mt;
char buff[64];
switch (w->morpheme_type)
{
case MT_NOT_SET:
mt = "MT_NOT_SET";
break;
case MT_WORD:
mt = "MT_WORD";
break;
case MT_FEATURE:
mt = "MT_FEATURE";
break;
case MT_INFRASTRUCTURE:
mt = "MT_I-S";
break;
case MT_WALL:
mt = "MT_WALL";
break;
case MT_EMPTY:
mt = "MT_EMPTY";
break;
case MT_UNKNOWN:
mt = "MT_UNKNOWN";
break;
case MT_TEMPLATE:
mt = "MT_TEMPLATE";
break;
case MT_ROOT:
mt = "MT_ROOT";
break;
case MT_CONTR:
mt = "MT_CONTR";
break;
case MT_PUNC:
mt = "MT_PUNC";
break;
case MT_STEM:
mt = "MT_STEM";
break;
case MT_PREFIX:
mt = "MT_PREFIX";
break;
case MT_MIDDLE:
mt = "MT_MIDDLE";
break;
case MT_SUFFIX:
mt = "MT_SUFFIX";
break;
default:
/* No truncation is expected. */
snprintf(buff, sizeof(buff), "MT_%d", (int)w->morpheme_type);
mt = string_set_add(buff, sent->string_set);
}
return mt;
}
#endif /* USE_WORDGRAPH_DISPLAY || defined(DEBUG) */
#if USE_WORDGRAPH_DISPLAY
/* === Wordgraph graphical representation === */
static void wordgraph_legend(dyn_str *wgd, unsigned int mode)
{
size_t i;
static char const *wst[] = {
"RE", "Matched a regex",
"SP", "Result of spell guess",
"RU", "Separated run-on word",
"HA", "Has an alternative",
"UNS", "Also unsplit_word",
"IN", "In the dict file",
"FI", "First char is uppercase"
};
append_string(wgd,
"subgraph cluster_legend {\n"
"label=Legend;\n"
"%s"
"legend [label=\"subword\\n(status-flags)\\nmorpheme-type\"];\n"
"legend [xlabel=\"ordinal-number\\ndebug-label\"];\n"
"%s"
"legend_width [width=4.5 height=0 shape=none label=<\n"
"<table border='0' cellborder='1' cellspacing='0'>\n"
"<tr><td colspan='2'>status-flags</td></tr>\n",
(mode & WGR_SUB) ? "subgraph cluster_unsplit_word {\n"
"label=\"ordinal-number unsplit-word\";\n" : "",
(mode & WGR_SUB) ? "}\n" : ""
);
for (i = 0; i < sizeof(wst)/sizeof(wst[0]); i += 2)
{
append_string(wgd,
"<tr><td align='left'>%s</td><td align='left'>%s</td></tr>\n",
wst[i], wst[i+1]);
}
append_string(wgd,
"</table>>];"
"}\n"
"subgraph cluster_legend_top_space {\n"
"style=invis legend_dummy [style=invis height=0 shape=box]\n"
"};\n"
);
}
/**
* Graph node name: Add "Sentence:" for the main node.
* Also escape " and \ with a \.
*/
static const char *wlabel(Sentence sent, const Gword *w)
{
const char *s;
const char sentence_label[] = "Sentence:\\n";
dyn_str *l = dyn_str_new();
char c0[] = "\0\0";
assert((NULL != w) && (NULL != w->subword), "Word must exist");
if ('\0' == *w->subword)
return string_set_add("(nothing)", sent->string_set);
if (w == sent->wordgraph) dyn_strcat(l, sentence_label);
for (s = w->subword; *s; s++)
{
switch (*s)
{
case '\"':
dyn_strcat(l, "\\\"");
break;
case '\\':
dyn_strcat(l, "\\");
break;
default:
*c0 = *s;
dyn_strcat(l, c0);
}
}
char *label_str = dyn_str_take(l);
s = string_set_add(label_str, sent->string_set);
free(label_str);
return s;
}
/**
* Generate the wordgraph in dot(1) format, for debug.
*/
static dyn_str *wordgraph2dot(Sentence sent, unsigned int mode, const char *modestr)
{
const Gword *w;
Gword **wp;
dyn_str *wgd = dyn_str_new(); /* the wordgraph in dot representation */
char nn[2*sizeof(char *) + 2 + 2 + 1]; /* \"%p\" node name: "0x..."+NUL*/
/* This function is called only if we have a wordgraph, in which case
* chain_next is non-NULL. So stop static analyzers to complain that
* it can be possibly NULL. */
UNREACHABLE(NULL == sent->wordgraph->chain_next);
append_string(wgd, "# Mode: %s\n", modestr);
dyn_strcat(wgd, "digraph G {\nsize =\"30,20\";\nrankdir=LR;\n");
if ((mode & (WGR_SUB)) && !(mode & WGR_COMPACT))
dyn_strcat(wgd, "newrank=true;\n");
if (mode & WGR_LEGEND) wordgraph_legend(wgd, mode);
append_string(wgd, "\"%p\" [shape=box,style=filled,color=\".7 .3 1.0\"];\n",
sent->wordgraph);
for (w = sent->wordgraph; w; w = w->chain_next)
{
bool show_node;
if (!(mode & WGR_UNSPLIT) && (MT_INFRASTRUCTURE != w->morpheme_type))
{
Gword *wu;
show_node = false;
/* In this mode nodes that are only unsplit_word are not shown. */
for (wu = sent->wordgraph; wu; wu = wu->chain_next)
{
if (NULL != wu->next)
{
for (wp = wu->next; *wp; wp++)
{
if (w == *wp)
{
show_node = true;
break;
}
}
}
}
if (!show_node) continue;
}
snprintf(nn, sizeof(nn), "\"%p\"", w);
/* Subword node format:
* +------------------+
* + +
* + w->subword +
* + (w->flags) +
* + w->morpheme_type +
* + +
* +------------------+
* w->node_num } <- external node label
* w->label }
*
* The flags and morpheme type are printed symbolically.
* The node_num field is the ordinal number of word creation.
* The label shows the code positions that created the subword.
* The external node label may appear at other positions near the node.
*
* FIXME: Use HTML labels.
*/
append_string(wgd, "%s [label=\"%s\\n(%s)\\n%s\"];\n", nn,
wlabel(sent, w), gword_status(sent, w), gword_morpheme(sent, w));
if (!(mode & WGR_DBGLABEL))
{
append_string(wgd, "%s [xlabel=\"%zu",
nn, w->node_num);
}
else
{
append_string(wgd, "%s [xlabel=\"%zu\\n%s",
nn, w->node_num, w->label);
}
/* For debugging this function: display also hex node names. */
if (mode & WGR_DOTDEBUG)
append_string(wgd, "\\n%p-%s", w, wlabel(sent, w));
dyn_strcat(wgd, "\"];\n");
if (NULL != w->next)
{
for (wp = w->next; *wp; wp++)
{
append_string(wgd, "%s->\"%p\" [label=next color=red];\n",
nn, *wp);
}
}
if (mode & WGR_PREV)
{
if (NULL != w->prev)
{
for (wp = w->prev; *wp; wp++)
{
append_string(wgd, "%s->\"%p\" [label=prev color=blue];\n",
nn, *wp);
}
}
}
if (mode & WGR_UNSPLIT)
{
if (!(mode & WGR_SUB) && (NULL != w->unsplit_word))
{
append_string(wgd, "%s->\"%p\" [label=unsplit];\n",
nn, w->unsplit_word);
}
}
}
if (mode & WGR_SUB)
{
const Gword *old_unsplit = NULL;
for (w = sent->wordgraph; w; w = w->chain_next)
{
if (NULL != w->unsplit_word)
{
if (w->unsplit_word != old_unsplit)
{
if (NULL != old_unsplit) dyn_strcat(wgd, "}\n");
append_string(wgd, "subgraph \"cluster-%p\" {", w->unsplit_word);
append_string(wgd, "label=\"%zu %s\"; \n",
w->unsplit_word->node_num, wlabel(sent, w->unsplit_word));
old_unsplit = w->unsplit_word;
}
snprintf(nn, sizeof(nn), "\"%p\"", w);
if (strstr(dyn_str_value(wgd), nn))
append_string(wgd, "\"%p\"; ", w);
}
}
dyn_strcat(wgd, "}\n");
}
else
{
#ifdef WGR_SHOW_TERMINATOR_AT_LHS /* not defined - not useful */
const Gword *terminating_node = NULL;
#endif
dyn_strcat(wgd, "{rank=same; ");
for (w = sent->wordgraph->chain_next; w; w = w->chain_next)
{
snprintf(nn, sizeof(nn), "\"%p\"", w);
if (IS_SENTENCE_WORD(sent, w) &&
((mode & WGR_UNSPLIT) || strstr(dyn_str_value(wgd), nn)))
{
append_string(wgd, "%s; ", nn);
}
#ifdef WGR_SHOW_TERMINATOR_AT_LHS
if (NULL == w->next) terminating_node = w;
#endif
}
dyn_strcat(wgd, "}\n");
#ifdef WGR_SHOW_TERMINATOR_AT_LHS
if (terminating_node)
append_string(wgd, "{rank=sink; \"%p\"}\n", terminating_node);
#endif
}
dyn_strcat(wgd, "\n}\n");
return wgd;
}
#if defined(HAVE_FORK) && !defined(POPEN_DOT)
static pid_t pid; /* XXX not reentrant */
#ifndef HAVE_PRCTL
/**
* Cancel the wordgraph viewers, to be used if there is fork() but no prctl().
*/
static void wordgraph_show_cancel(void)
{
kill(pid, SIGTERM);
}
#endif /* HAVE_FORK */
#endif /* HAVE_PRCTL */
#ifndef DOT_COMMNAD
#define DOT_COMMAND "dot"
#endif
#ifndef DOT_DRIVER
#define DOT_DRIVER "-Txlib"
#endif
/* In case files are used, their names are fixed. So more than one thread
* (or program) cannot use the word-graph display at the same time. This
* can be corrected, even though there is no much point to do that
* (displaying the word-graph is for debug). */
#define DOT_FILENAME "lg-wg.vg"
#define POPEN_DOT_CMD DOT_COMMAND" "DOT_DRIVER
#ifndef POPEN_DOT_CMD_NATIVE
# ifdef _WIN32
# ifndef IMAGE_VIEWER
# define IMAGE_VIEWER "rundll32 PhotoViewer,ImageView_Fullscreen"
# endif
# define WGJPG "%TEMP%\\lg-wg.jpg"
# define POPEN_DOT_CMD_NATIVE \
DOT_COMMAND" -Tjpg>"WGJPG"&"IMAGE_VIEWER" "WGJPG"&del "WGJPG
# elif __APPLE__
# ifndef IMAGE_VIEWER
# define IMAGE_VIEWER "open -W"
# endif
# define WGJPG "$TMPDIR/lg-wg.jpg"
# define POPEN_DOT_CMD_NATIVE \
DOT_COMMAND" -Tjpg>"WGJPG";"IMAGE_VIEWER" "WGJPG";rm "WGJPG
# else
# define POPEN_DOT_CMD_NATIVE POPEN_DOT_CMD
# endif
#endif
#if !defined HAVE_FORK || defined POPEN_DOT
#ifdef _MSC_VER
#define popen _popen
#define pclose _pclose
#endif
/**
* popen a command with the given input.
* If the system doesn't have fork(), popen() is used to launch "dot".
* This is an inferior implementation than the one below that uses
* fork(), in which the window remains open and is updated automatically
* when new sentences are entered. With popen(), the program blocks at
* pclose() and the user needs to close the window after each sentence.
*/
static bool x_popen(const char *cmd, const char *wgds)
{
lgdebug(+3, "Invoking: %s\n", cmd);
FILE *const cmdf = popen(cmd, "w");
bool rc = true;
if (NULL == cmdf)
{
prt_error("Error: popen of '%s' failed: %s\n", cmd, strerror(errno));
rc = false;
}
else
{
if (fputs(wgds, cmdf) == EOF) /* see default_error_handler() */
{
prt_error("Error: x_popen(): fputs() error: %s\n", strerror(errno));
rc = false;
}
if (pclose(cmdf) == -1)
{
prt_error("Error: x_popen(): pclose() error: %s\n", strerror(errno));
rc = false;
}
}
return rc;
}
#else
static bool x_forkexec(const char *const argv[], pid_t *vpid, const char err[])
{
/* Fork/exec a graph viewer, and leave it in the background until we exit.
* On exit, send SIGHUP. If prctl() is not available and the program
* crashes, then it is left to the user to exit the viewer. */
if (0 < *vpid)
{
pid_t rpid = waitpid(*vpid, NULL, WNOHANG);
if (0 == rpid) return true; /* viewer still active */
if (-1 == rpid)
{
prt_error("Error: waitpid(%d): %s\n", *vpid, strerror(errno));
*vpid = 0;
return false;
}
}
*vpid = fork();
switch (*vpid)
{
case -1:
prt_error("Error: fork(): %s\n", strerror(errno));
return false;
case 0:
#ifdef HAVE_PRCTL
if (-1 == prctl(PR_SET_PDEATHSIG, SIGHUP))
{
prt_error("Error: prctl: %s\n", strerror(errno));
/* Non-fatal error - continue. */
}
#endif
/* Not closing fd 0/1/2, to allow interaction with the program */
execvp(argv[0], (char **)argv);
prt_error("Error: execlp of %s: %s%s\n",
argv[0], strerror(errno), (ENOENT == errno) ? err : "");
_exit(1);
default:
#ifndef HAVE_PRCTL
if (0 != atexit(wordgraph_show_cancel))
{
prt_error("Warning: atexit(wordgraph_show_cancel) failed.\n");
/* Non-fatal error - continue. */
}
#endif
break;
}
return true;
}
#endif /* !defined HAVE_FORK || defined POPEN_DOT */
#ifdef _WIN32
#define TMPDIR (getenv("TEMP") ? getenv("TEMP") : ".")
#else
#define TMPDIR (getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp")
#endif
#define concatfn(fn, fn1, fn2) \
(fn=alloca(strlen(fn1)+strlen(fn2)+2),\
strcpy(fn, fn1), strcat(fn, "/"), strcat(fn, fn2))
static void wordgraph_unlink_xtmpfile(void)
{
char *fn;
if (!test_enabled("gvfile"))
{
concatfn(fn, TMPDIR, DOT_FILENAME);
if (unlink(fn) == -1)
prt_error("Warning: Cannot unlink %s: %s\n", fn, strerror(errno));
}
}
/**
* Display the word-graph in the indicated mode.
* This is for debug and inspiration. It is not reentrant due to the
* static pid and the possibly created fixed filenames.
* When Using X11, a "dot -Txlib" program is launched on the graph
* description file. The xlib driver refreshes the graph when the file is
* changed, displaying additional sentences in the same window. The viewer
* program exits on program end (see the comments in the code). When
* compiled with MSVC or MINGW, the system PhotoViewer is used by default,
* unless !wg:x is used (for using X11 when available).
*
* The "dot" and the "PhotoViewer" programs must be in the PATH.
*
* FIXME? "dot" may get a SEGV due to memory corruptions in it (a known
* problem - exists even in 2.38). This can be worked-around by trying it
* again until it succeeds (but the window size, if changed by the user,
* will not be preserved).
*
* modestr: a graph display mode as defined in wordgraph.h (default "ldu").
*/
bool sentence_display_wordgraph(Sentence sent, const char *modestr)
{
dyn_str *wgd;
char *gvf_name = NULL;
bool generate_gvfile = test_enabled("gvfile"); /* keep it for debug */
char *wgds;
bool gvfile = false;
uint32_t mode = 0;
const char *mp;
bool rc = true;
for (mp = modestr; '\0' != *mp && ',' != *mp; mp++)
{
if ((*mp >= 'a') && (*mp <= 'z')) mode |= 1<<(*mp-'a');
}
if ((0 == mode) || (WGR_X11 == mode))
mode |= WGR_LEGEND|WGR_DBGLABEL|WGR_UNSPLIT;
wgd = wordgraph2dot(sent, mode, modestr);
wgds = dyn_str_take(wgd);
#if defined(HAVE_FORK) && !defined(POPEN_DOT)
gvfile = true;
#endif
if (gvfile || generate_gvfile)
{
FILE *gvf;
bool gvf_error = false;
static bool wordgraph_unlink_xtmpfile_needed = true;
concatfn(gvf_name, TMPDIR, DOT_FILENAME);
gvf = fopen(gvf_name, "w");
if (NULL == gvf)
{
prt_error("Error: %s(): fopen() of %s failed: %s\n",
__func__, gvf_name, strerror(errno));
gvf_error = true;
}
else
{
if (fputs(wgds, gvf) == EOF)
{
gvf_error = true;
prt_error("Error: %s(): fputs() to %s failed: %s\n",
__func__, gvf_name, strerror(errno));
}
if (fclose(gvf) == EOF)
{
gvf_error = true;
prt_error("Error: %s(): fclose() of %s failed: %s\n",
__func__, gvf_name, strerror(errno));
}
}
if (gvf_error && gvfile) /* we need it - cannot continue */
{
rc = false;
goto finish;
}
if (wordgraph_unlink_xtmpfile_needed)
{
/* The filename is fixed - removal needed only once. */
wordgraph_unlink_xtmpfile_needed = false;
atexit(wordgraph_unlink_xtmpfile);
}
}
#ifdef _WIN32
#define EXITKEY "ALT-F4"
#elif __APPLE__
#define EXITKEY "⌘-Q"
#endif
#ifdef EXITKEY
prt_error("Press "EXITKEY" in the graphical display window to continue\n");
#endif
#if !defined HAVE_FORK || defined POPEN_DOT
rc = x_popen((mode & WGR_X11)? POPEN_DOT_CMD : POPEN_DOT_CMD_NATIVE, wgds);
#else
{
assert(NULL != gvf_name, "DOT filename not initialized (#define mess?)");
const char *const args[] = { DOT_COMMAND, DOT_DRIVER, gvf_name, NULL };
const char notfound[] =
" (command not in PATH; \"graphviz\" package not installed?).";
rc = x_forkexec(args, &pid, notfound);
}
#endif
finish:
free(wgds);
return rc;
}
#else
bool sentence_display_wordgraph(Sentence sent, const char *modestr)
{
prt_error("Error: Library not configured with wordgraph-display\n");
return false;
}
#endif /* USE_WORDGRAPH_DISPLAY */