Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1591 lines (1321 sloc) 47.2 KB
/* See Copyright Notice in LICENSE.txt */
#define _BSD_SOURCE
#define _GNU_SOURCE
#include <strings.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <signal.h>
#include <assert.h>
#include <errno.h>
#include <time.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/inotify.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <GLFW/glfw3.h>
#include <IL/il.h>
#include <IL/ilu.h>
#include <libavformat/avformat.h>
#include <event.h>
#include <event2/dns.h>
#include <lualib.h>
#include <lauxlib.h>
#include "uthash.h"
#include "utlist.h"
#include "misc.h"
#include "image.h"
#include "video.h"
#include "font.h"
#include "shader.h"
#include "vnc.h"
#include "framebuffer.h"
#include "struct.h"
#include "kernel.h"
#include "userlib.h"
#include "module_json.h"
#if USE_LUAJIT
#include <luajit.h>
#define VERSION_STRING "Info Beamer " VERSION "+" LUA_VERSION "+" LUAJIT_VERSION
#else
#define VERSION_STRING "Info Beamer " VERSION "+" LUA_VERSION
#endif
#define INFO_URL "http://info-beamer.org/"
#define NODE_CODE_FILE "node.lua"
#define ENVIRONMENT_PREFIX "INFOBEAMER_ENV_"
#define ENVIRONMENT_PREFIX_SIZE (sizeof(ENVIRONMENT_PREFIX)-1)
#define MAX_MEM 2000000 // KB
#define MAX_GL_PUSH 20 // glPushMatrix depth
#define MAX_CHILD_RENDERS 20 // maximum childs rendered per node
#define MAX_SNAPSHOTS 5 // maximum number of snapshots per render
// Default host/port (both udp & tcp)
#define LISTEN_ADDR "0.0.0.0"
#define DEFAULT_PORT 4444
#ifdef DEBUG
#define MAX_RUNAWAY_TIME 10 // sec
#define MAX_PCALL_TIME 5000000 // usec
#else
#define MAX_RUNAWAY_TIME 1 // sec
#define MAX_PCALL_TIME 500000 // usec
#endif
#define NO_GL_PUSHPOP -1
#define NODE_INACTIVITY 2.0 // node considered idle after x seconds
#define NODE_CPU_BLACKLIST 60.0 // seconds a node is blacklisted if it exceeds cpu usage
static int win_w, win_h;
typedef enum { PROFILE_BOOT, PROFILE_UPDATE, PROFILE_EVENT } profiling_bins;
typedef struct node_s {
int wd; // inotify watch descriptor
char *name; // local node name
char *path; // full path (including node name)
char *alias; // alias path
lua_State *L;
UT_hash_handle by_wd; // global handle for search by watch descriptor
UT_hash_handle by_name; // childs by name
UT_hash_handle by_path; // node by path
UT_hash_handle by_alias; // node by alias
struct node_s *parent;
struct node_s *childs;
int width;
int height;
int gl_matrix_depth;
struct client_s *clients;
int child_render_quota;
int snapshot_quota;
double profiling[3];
double last_profile;
int num_frames;
int num_resource_inits;
int num_allocs;
double last_activity;
double blacklisted;
} node_t;
static node_t *nodes_by_wd = NULL;
static node_t *nodes_by_path = NULL;
static node_t *nodes_by_alias = NULL;
static node_t root = {0};
typedef struct client_s {
int fd;
node_t *node;
struct bufferevent *buf_ev;
struct client_s *next;
struct client_s *prev;
} client_t;
static int inotify_fd;
static double now;
static int running = 1;
static int listen_port;
GLuint default_tex; // white default texture
struct event_base *event_base;
struct evdns_base *dns_base;
/*=== Forward declarations =====*/
static void client_write(client_t *client, const char *data, size_t data_size);
static void client_close(client_t *client);
static void node_printf(node_t *node, const char *fmt, ...);
static void node_blacklist(node_t *node, double time);
static void node_remove_alias(node_t *node);
static void node_reset_quota(node_t *node);
static int node_render_to_image(lua_State *L, node_t *node);
static void node_init(node_t *node, node_t *parent, const char *path, const char *name);
static void node_free(node_t *node);
/*======= Lua Sandboxing =======*/
#ifndef USE_LUAJIT
static void *lua_alloc(void *ud, void *ptr, size_t osize, size_t nsize) {
node_t *node = ud;
node->num_allocs++;
(void)osize; /* not used */
if (nsize == 0) {
free(ptr);
return NULL;
} else {
return realloc(ptr, nsize);
}
}
#endif
/* execution time limiting for pcalls */
static node_t *global_node = NULL;
static int timers_expired = 0;
static void deadline_stop(lua_State *L, lua_Debug *ar) {
lua_sethook(L, NULL, 0, 0);
lua_pushliteral(L, "alarm");
lua_gettable(L, LUA_REGISTRYINDEX);
lua_call(L, 0, 0);
}
static void deadline_signal(int i) {
if (!global_node)
die("urg. timer expired and no global_node");
fprintf(stderr, RED("[%s]") " timeout\n", global_node->path);
if (timers_expired == 0) {
// timer expired once? Try to solve it inside of
// lua: set a hook that will execute deadline_stop.
lua_sethook(global_node->L, deadline_stop,
LUA_MASKCALL | LUA_MASKRET | LUA_MASKLINE | LUA_MASKCOUNT, 1);
node_blacklist(global_node, NODE_CPU_BLACKLIST);
} else {
// timer expired again without lua being stopped?
die("unstoppable runaway code in %s", global_node->path);
}
timers_expired++;
}
static int lua_timed_pcall(node_t *node, int in, int out,
int error_handler_pos)
{
node_t *old_global_node = global_node;
struct itimerval old_timer;
struct itimerval deadline;
deadline.it_interval.tv_sec = MAX_RUNAWAY_TIME;
deadline.it_interval.tv_usec = 0;
deadline.it_value.tv_sec = MAX_PCALL_TIME / 1000000;
deadline.it_value.tv_usec = MAX_PCALL_TIME % 1000000;
setitimer(ITIMER_VIRTUAL, &deadline, &old_timer);
global_node = node;
timers_expired = 0;
int ret = lua_pcall(node->L, in, out, error_handler_pos);
setitimer(ITIMER_VIRTUAL, &old_timer, NULL);
global_node = old_global_node;
return ret;
}
static int lua_panic(lua_State *L) {
die("node panic!");
return 0;
}
static const char *lua_safe_dedup_error_message(lua_State *L) {
const char *message = lua_tostring(L, -1);
if (!message)
die("<null> error message");
lua_pushliteral(L, "last_error");
lua_rawget(L, LUA_REGISTRYINDEX);
int same_as_last_time = lua_equal(L, -1, -2);
lua_pop(L, 1); // remove value of last_error
if (same_as_last_time) {
message = NULL;
} else {
lua_pushliteral(L, "last_error");
lua_pushvalue(L, -2);
lua_rawset(L, LUA_REGISTRYINDEX);
}
return message;
}
static void lua_node_enter(node_t *node, int args, profiling_bins bin) {
node_reset_quota(node);
lua_State *L = node->L;
lua_pushliteral(L, "execute"); // [args] "execute"
lua_rawget(L, LUA_REGISTRYINDEX); // [args] execute
lua_insert(L, -1 - args); // execute [args]
lua_pushliteral(L, "traceback"); // execute [args] "traceback"
lua_rawget(L, LUA_REGISTRYINDEX); // execute [args] traceback
const int error_handler_pos = lua_gettop(L) - 1 - args;
lua_insert(L, error_handler_pos); // traceback execute [args]
struct timeval before, after;
gettimeofday(&before, NULL);
int status = lua_timed_pcall(node, args, 0, error_handler_pos);
if (status == 0) {
// success // traceback
lua_remove(L, error_handler_pos); //
} else {
// error // traceback "error"
char *err = status == LUA_ERRRUN ? "runtime error" :
status == LUA_ERRMEM ? "memory error" :
status == LUA_ERRERR ? "error handling error" : NULL;
assert(err);
const char *message = lua_safe_dedup_error_message(L);
if (message)
node_printf(node, "%s: %s\n", err, message);
lua_pop(L, 2); //
}
gettimeofday(&after, NULL);
lua_gc(node->L, LUA_GCSTEP, 5);
node->profiling[bin] += time_delta(&before, &after);
node->last_activity = now;
}
/*======= Lua entry points =======*/
// reinit sandbox, load usercode and user code
static void node_boot(node_t *node) {
lua_pushliteral(node->L, "boot");
lua_node_enter(node, 1, PROFILE_BOOT);
}
// notify of child update
static void node_child_update(node_t *node, const char *name, int added) {
lua_pushliteral(node->L, "child_update");
lua_pushstring(node->L, name);
lua_pushboolean(node->L, added);
lua_node_enter(node, 3, PROFILE_UPDATE);
}
// notify of content update
static void node_content_update(node_t *node, const char *name, int added) {
fprintf(stderr, YELLOW("[%s]")" update %c%s\n", node->path, added ? '+' : '-', name);
lua_pushliteral(node->L, "content_update");
lua_pushstring(node->L, name);
lua_pushboolean(node->L, added);
if (!strcmp(name, NODE_CODE_FILE)) {
// reset blacklisted flag
node->blacklisted = 0;
// reset node dimensions
node->width = 0;
node->height = 0;
// remove existing node alias
node_remove_alias(node);
}
lua_node_enter(node, 3, PROFILE_UPDATE);
}
// event.<event_name>(args...)
static void node_event(node_t *node, const char *name, int args) {
lua_pushliteral(node->L, "event"); // [args] "event_name"
lua_pushstring(node->L, name); // [args] "event_name" name
lua_insert(node->L, -2 - args); // name [args] "event_name"
lua_insert(node->L, -2 - args); // "event_name" name [args]
lua_node_enter(node, 2 + args, PROFILE_EVENT);
}
// render node
static void node_render_self(node_t *node, int width, int height) {
lua_pushliteral(node->L, "render_self");
lua_pushnumber(node->L, width);
lua_pushnumber(node->L, height);
lua_node_enter(node, 3, PROFILE_EVENT);
}
/*===== node macros =======*/
#define node_setup_completed(node) ((node)->width != 0)
#define node_is_idle(node) (now > (node)->last_activity + NODE_INACTIVITY)
#define node_is_blacklisted(node) (now < (node)->blacklisted)
#define node_is_rendering(node) ((node)->gl_matrix_depth != NO_GL_PUSHPOP)
/*===== Lua bindings ======*/
static node_t *get_rendering_node(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
if (!node_is_rendering(node))
luaL_error(L, "only callable in node.render");
return node;
}
static int luaResetError(lua_State *L) {
lua_pushliteral(L, "last_error");
lua_pushnil(L);
lua_rawset(L, LUA_REGISTRYINDEX);
return 0;
}
static int luaRenderSelf(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
return node_render_to_image(L, node);
}
static int luaRenderChild(lua_State *L) {
node_t *node = get_rendering_node(L);
if (node->child_render_quota-- <= 0)
return luaL_error(L, "too many childs rendered");
const char *name = luaL_checkstring(L, 1);
node_t *child;
HASH_FIND(by_name, node->childs, name, strlen(name), child);
if (!child)
return luaL_error(L, "child %s not found", name);
return node_render_to_image(L, child);
}
static int luaSetup(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
if (node_is_rendering(node))
return luaL_error(L, "cannot change width or height while rendering");
int width = (int)luaL_checknumber(L, 1);
int height = (int)luaL_checknumber(L, 2);
if (width < 32 || width > 4096)
luaL_argerror(L, 1, "invalid width. must be within [32,4096]");
if (height < 32 || height > 4096)
luaL_argerror(L, 2, "invalid height. must be within [32,4096]");
node->width = width;
node->height = height;
return 0;
}
static int luaGlOrtho(lua_State *L) {
node_t *node = get_rendering_node(L);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, node->width,
node->height, 0,
-1000, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
node->gl_matrix_depth = 0;
return 0;
}
static int luaGlPerspective(lua_State *L) {
node_t *node = get_rendering_node(L);
double fov = luaL_checknumber(L, 1);
double eye_x = luaL_checknumber(L, 2);
double eye_y = luaL_checknumber(L, 3);
double eye_z = luaL_checknumber(L, 4);
double center_x = luaL_checknumber(L, 5);
double center_y = luaL_checknumber(L, 6);
double center_z = luaL_checknumber(L, 7);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(fov, (float)node->width / (float)node->height, 0.1, 10000);
gluLookAt(eye_x, eye_y, eye_z,
center_x, center_y, center_z,
0, -1, 0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
node->gl_matrix_depth = 0;
return 0;
}
static int luaSetAlias(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *alias = luaL_checkstring(L, 1);
// already exists?
node_t *existing_node;
HASH_FIND(by_alias, nodes_by_alias, alias, strlen(alias), existing_node);
if (existing_node) {
if (existing_node == node) {
return 0;
} else {
return luaL_error(L, "alias already taken by %s", existing_node->path);
}
}
// remove old alias
if (node->alias) {
HASH_DELETE(by_alias, nodes_by_alias, node);
free(node->alias);
}
// set new alias
node->alias = strdup(alias);
HASH_ADD_KEYPTR(by_alias, nodes_by_alias, node->alias, strlen(node->alias), node);
return 0;
}
static int luaLoadImage(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *name = luaL_checkstring(L, 1);
if (index(name, '/'))
luaL_argerror(L, 1, "invalid resource name");
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", node->path, name);
node->num_resource_inits++;
return image_load(L, path, name);
}
static int luaLoadVideo(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *name = luaL_checkstring(L, 1);
if (index(name, '/'))
luaL_argerror(L, 1, "invalid resource name");
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", node->path, name);
node->num_resource_inits++;
return video_load(L, path, name);
}
static int luaLoadFont(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *name = luaL_checkstring(L, 1);
if (index(name, '/'))
luaL_argerror(L, 1, "invalid resource name");
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", node->path, name);
node->num_resource_inits++;
return font_new(L, path, name);
}
static int luaLoadFile(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *name = luaL_checkstring(L, 1);
if (index(name, '/'))
luaL_argerror(L, 1, "invalid resource name");
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", node->path, name);
int fd = open(path, O_RDONLY);
if (fd == -1)
return luaL_error(L, "cannot open file '%s'", path);
luaL_Buffer b;
luaL_buffinit(L, &b);
while (1) {
char *data = luaL_prepbuffer(&b);
ssize_t data_size = read(fd, data, LUAL_BUFFERSIZE);
if (data_size < 0)
return luaL_error(L, "cannot read %s: %s", name, strerror(errno));
if (data_size == 0)
break;
luaL_addsize(&b, data_size);
}
close(fd);
luaL_pushresult(&b);
node->num_resource_inits++;
return 1;
}
static int luaCreateColoredTexture(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
GLfloat r = luaL_checknumber(L, 1);
GLfloat g = luaL_checknumber(L, 2);
GLfloat b = luaL_checknumber(L, 3);
GLfloat a = luaL_optnumber(L, 4, 1.0);
node->num_resource_inits++;
return image_from_color(L,
CLAMP(r, 0, 1),
CLAMP(g, 0, 1),
CLAMP(b, 0, 1),
CLAMP(a, 0, 1)
);
}
static int luaCreateSnapshot(lua_State *L) {
node_t *node = get_rendering_node(L);
if (node->snapshot_quota-- <= 0)
return luaL_error(L, "too many snapshots");
node->num_resource_inits++;
int mipmap = 0;
int x = 0;
int y = 0;
int width = node->width;
int height = node->height;
if (lua_gettop(L) <= 1) {
mipmap = lua_toboolean(L, 1);
} else if (lua_gettop(L) == 4) {
x = luaL_checknumber(L, 1);
y = luaL_checknumber(L, 2);
width = luaL_checknumber(L, 3);
height = luaL_checknumber(L, 4);
if (x < 0 || y < 0 || width < 0 || height < 0 ||
x + width > node->width || y + height > node->height) {
return luaL_error(L, "snapshot out of bounds");
}
} else {
return luaL_error(L, "invalid number of arguments");
}
return image_from_current_framebuffer(
L, x, node->height - y - height, width, height, mipmap
);
}
static int luaCreateShader(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *vertex = luaL_checkstring(L, 1);
const char *fragment = luaL_checkstring(L, 2);
node->num_resource_inits++;
return shader_new(L, vertex, fragment);
}
static int luaCreateVnc(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
const char *host = luaL_checkstring(L, 1);
int port = luaL_optnumber(L, 2, 5900);
node->num_resource_inits++;
return vnc_create(L, host, port);
}
static int luaPushFormattedArgs(lua_State *L) {
luaL_Buffer b;
luaL_buffinit(L, &b);
int n = lua_gettop(L);
lua_getglobal(L, "tostring");
for (int i = 1; i <= n; i++) {
lua_pushvalue(L, n + 1);
lua_pushvalue(L, i);
lua_call(L, 1, 1);
if (!lua_isstring(L, -1))
return luaL_error(L, "tostring must return a string to print");
if (i > 1)
luaL_addchar(&b, '\t');
luaL_addvalue(&b);
}
luaL_addchar(&b, '\n');
luaL_pushresult(&b);
return 1;
}
static int luaPrint(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
luaPushFormattedArgs(L);
node_printf(node, "%s", lua_tostring(L, -1));
return 0;
}
static int luaGetScreenInfo(lua_State *L) {
lua_pushnumber(L, win_w);
lua_pushnumber(L, win_h);
return 2;
}
static int luaClientWrite(lua_State *L) {
node_t *node = lua_touserdata(L, lua_upvalueindex(1));
client_t *client = lua_touserdata(L, 1);
lua_remove(L, 1);
luaPushFormattedArgs(L);
size_t string_len;
const char *string = lua_tolstring(L, -1, &string_len);
client_t *current_client;
DL_FOREACH(node->clients, current_client) {
if (current_client == client) {
client_write(current_client, string, string_len);
}
}
return 0;
}
static int luaGlClear(lua_State *L) {
get_rendering_node(L);
GLdouble r = luaL_checknumber(L, 1);
GLdouble g = luaL_checknumber(L, 2);
GLdouble b = luaL_checknumber(L, 3);
GLdouble a = luaL_checknumber(L, 4);
glClearColor(r, g, b, a);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(0);
return 0;
}
static int luaGlPushMatrix(lua_State *L) {
node_t *node = get_rendering_node(L);
if (node->gl_matrix_depth > MAX_GL_PUSH)
return luaL_error(L, "Too may pushes");
glPushMatrix();
node->gl_matrix_depth++;
return 0;
}
static int luaGlPopMatrix(lua_State *L) {
node_t *node = get_rendering_node(L);
if (node->gl_matrix_depth == 0)
return luaL_error(L, "Nothing to pop");
glPopMatrix();
node->gl_matrix_depth--;
return 0;
}
static int luaGlRotate(lua_State *L) {
get_rendering_node(L);
double angle = luaL_checknumber(L, 1);
double x = luaL_checknumber(L, 2);
double y = luaL_checknumber(L, 3);
double z = luaL_checknumber(L, 4);
glRotated(angle, x, y, z);
return 0;
}
static int luaGlTranslate(lua_State *L) {
get_rendering_node(L);
double x = luaL_checknumber(L, 1);
double y = luaL_checknumber(L, 2);
double z = luaL_optnumber(L, 3, 0.0);
glTranslated(x, y, z);
return 0;
}
static int luaGlScale(lua_State *L) {
get_rendering_node(L);
double x = luaL_checknumber(L, 1);
double y = luaL_checknumber(L, 2);
double z = luaL_optnumber(L, 3, 1.0);
glScaled(x, y, z);
return 0;
}
static int luaNow(lua_State *L) {
lua_pushnumber(L, now);
return 1;
}
/*==== Node functions =====*/
static int node_render_to_image(lua_State *L, node_t *node) {
// save current gl state
int prev_fbo, prev_prog;
GLdouble prev_projection[16];
GLdouble prev_modelview[16];
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
glGetIntegerv(GL_CURRENT_PROGRAM, &prev_prog);
glGetDoublev(GL_PROJECTION_MATRIX, prev_projection);
glGetDoublev(GL_MODELVIEW_MATRIX, prev_modelview);
glPushAttrib(GL_ALL_ATTRIB_BITS);
int width = 1, height = 1;
if (node_setup_completed(node))
width = node->width, height = node->height;
// get new framebuffer and associated texture from recycler
unsigned int fbo, tex;
make_framebuffer(width, height, &tex, &fbo);
// initialize gl state
glUseProgram(0);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glViewport(0, 0, width, height);
glOrtho(0, width,
height, 0,
-1000, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
if (!node_setup_completed(node)) {
node_printf(node, "node not initialized with gl.setup()\n");
glClearColor(0.5, 0.5, 0.5, 1);
glClear(GL_COLOR_BUFFER_BIT);
} else if (node_is_blacklisted(node)) {
node_printf(node, "node is blacklisted\n");
glClearColor(0.5, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
} else {
// clear with transparent color
glClearColor(1, 1, 1, 0);
glClear(GL_COLOR_BUFFER_BIT);
// render node
node->gl_matrix_depth = 0;
node->num_frames++;
node_event(node, "render", 0);
while (node->gl_matrix_depth-- > 0)
glPopMatrix();
node->gl_matrix_depth = NO_GL_PUSHPOP;
}
// rebind to framebuffer texture
glBindTexture(GL_TEXTURE_2D, tex);
glGenerateMipmap(GL_TEXTURE_2D);
// restore previous state
glPopAttrib();
glMatrixMode(GL_PROJECTION);
glLoadMatrixd(prev_projection);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixd(prev_modelview);
glUseProgram(prev_prog);
glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo);
return image_create(L, tex, fbo, width, height, 1);
}
static void node_printf(node_t *node, const char *fmt, ...) {
char buffer[16384];
va_list ap;
va_start(ap, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, ap);
va_end(ap);
fprintf(stderr, GREEN("[%s]")" %s", node->path, buffer);
}
static void node_blacklist(node_t *node, double time) {
node->blacklisted = now + time;
node_printf(node, "blacklisted for %.0f seconds\n", time);
}
static void node_remove_alias(node_t *node) {
if (node->alias) {
HASH_DELETE(by_alias, nodes_by_alias, node);
free(node->alias);
node->alias = NULL;
}
}
static void node_tree_gc(node_t *node) {
if (!node_is_idle(node))
lua_gc(node->L, LUA_GCSTEP, 30);
node_t *child, *tmp;
HASH_ITER(by_name, node->childs, child, tmp) {
node_tree_gc(child);
};
}
static node_t *node_add_child(node_t* node, const char *path, const char *name) {
fprintf(stderr, YELLOW("[%s]")" adding new child node %s\n", node->name, name);
node_t *child = xmalloc(sizeof(node_t));
node_init(child, node, path, name);
HASH_ADD_KEYPTR(by_name, node->childs, child->name, strlen(child->name), child);
return child;
}
static void node_remove_child(node_t* node, node_t* child) {
fprintf(stderr, YELLOW("[%s]")" removing child node %s\n", node->name, child->name);
node_child_update(node, child->name, 0);
HASH_DELETE(by_name, node->childs, child);
node_free(child);
free(child);
}
static void node_remove_child_by_name(node_t* node, const char *name) {
node_t *child;
HASH_FIND(by_name, node->childs, name, strlen(name), child);
if (!child)
die("child not found: %s", name);
node_remove_child(node, child);
}
static void node_reset_quota(node_t *node) {
node->child_render_quota = MAX_CHILD_RENDERS;
node->snapshot_quota = MAX_SNAPSHOTS;
}
static void node_reset_profiler(node_t *node) {
node->last_profile = now;
node->profiling[PROFILE_BOOT] = 0.0;
node->profiling[PROFILE_UPDATE] = 0.0;
node->profiling[PROFILE_EVENT] = 0.0;
node->num_frames = 0;
node->num_resource_inits = 0;
node->num_allocs = 0;
}
#define lua_register_node_func(node,name,func) \
(lua_pushliteral((node)->L, name), \
lua_pushlightuserdata((node)->L, node), \
lua_pushcclosure((node)->L, func, 1), \
lua_settable((node)->L, LUA_GLOBALSINDEX))
static void node_init(node_t *node, node_t *parent, const char *path, const char *name) {
// add directory watcher
node->wd = inotify_add_watch(inotify_fd, path,
IN_CLOSE_WRITE|IN_CREATE|IN_DELETE|IN_DELETE_SELF|
IN_MOVE);
if (node->wd == -1)
die("cannot start watching directory %s: %s", path, strerror(errno));
node->parent = parent;
node->path = strdup(path);
node->name = strdup(name);
node->alias = NULL;
node->width = 0;
node->height = 0;
node_reset_profiler(node);
node->last_activity = now;
node->gl_matrix_depth = NO_GL_PUSHPOP;
// link by watch descriptor & path
HASH_ADD(by_wd, nodes_by_wd, wd, sizeof(int), node);
HASH_ADD_KEYPTR(by_path, nodes_by_path, node->path, strlen(node->path), node);
// create lua state
#ifdef USE_LUAJIT
node->L = luaL_newstate();
#else
node->L = lua_newstate(lua_alloc, node);
#endif
if (!node->L)
die("cannot create lua");
lua_atpanic(node->L, lua_panic);
luaL_openlibs(node->L);
image_register(node->L);
video_register(node->L);
font_register(node->L);
shader_register(node->L);
vnc_register(node->L);
luaopen_struct(node->L);
lua_register_node_func(node, "reset_error", luaResetError);
lua_register_node_func(node, "setup", luaSetup);
lua_register_node_func(node, "print", luaPrint);
lua_register_node_func(node, "set_alias", luaSetAlias);
lua_register_node_func(node, "client_write", luaClientWrite);
lua_register_node_func(node, "get_screen_info", luaGetScreenInfo);
lua_register_node_func(node, "render_self", luaRenderSelf);
lua_register_node_func(node, "render_child", luaRenderChild);
lua_register_node_func(node, "load_image", luaLoadImage);
lua_register_node_func(node, "load_video", luaLoadVideo);
lua_register_node_func(node, "load_font", luaLoadFont);
lua_register_node_func(node, "load_file", luaLoadFile);
lua_register_node_func(node, "create_colored_texture", luaCreateColoredTexture);
lua_register_node_func(node, "create_snapshot", luaCreateSnapshot);
lua_register_node_func(node, "create_shader", luaCreateShader);
lua_register_node_func(node, "create_vnc", luaCreateVnc);
lua_register_node_func(node, "glClear", luaGlClear);
lua_register_node_func(node, "glPushMatrix", luaGlPushMatrix);
lua_register_node_func(node, "glPopMatrix", luaGlPopMatrix);
lua_register_node_func(node, "glRotate", luaGlRotate);
lua_register_node_func(node, "glTranslate", luaGlTranslate);
lua_register_node_func(node, "glScale", luaGlScale);
lua_register_node_func(node, "glOrtho", luaGlOrtho);
lua_register_node_func(node, "glPerspective", luaGlPerspective);
lua_register(node->L, "now", luaNow);
lua_pushliteral(node->L, VERSION);
lua_setglobal(node->L, "VERSION");
lua_pushstring(node->L, path);
lua_setglobal(node->L, "PATH");
lua_pushstring(node->L, name);
lua_setglobal(node->L, "NAME");
lua_pushlstring(node->L, userlib, userlib_size);
lua_setglobal(node->L, "USERLIB");
lua_pushlstring(node->L, module_json, module_json_size);
lua_setglobal(node->L, "MODULE_JSON");
lua_pushliteral(node->L, NODE_CODE_FILE);
lua_setglobal(node->L, "NODE_CODE_FILE");
// get variables from environment
lua_newtable(node->L);
for (const char **cur = (const char**)environ; *cur; cur++) {
if (strstr(*cur, ENVIRONMENT_PREFIX) != *cur)
continue;
const char *equals = strstr(*cur + ENVIRONMENT_PREFIX_SIZE, "=");
if (!equals)
continue;
const char *name = *cur + ENVIRONMENT_PREFIX_SIZE;
int name_len = equals - name;
const char *value = equals+1;
// printf("%d %.*s %s\n", name_len, name_len, name, value);
lua_pushlstring(node->L, name, name_len);
lua_pushstring(node->L, value);
lua_rawset(node->L, -3);
}
lua_setglobal(node->L, "NODE_ENVIRON");
if (luaL_loadbuffer(node->L, kernel, kernel_size, "=kernel.lua") != 0) {
const char *error = lua_tostring(node->L, -1);
// If kernel.lua was procompiled with an incompatible lua
// version, loading the embedded code fail here. Try to
// detect this...
die("cannot load kernel.lua: %s%s", error,
strstr(error, "bad header") ? " (See 'kernel load error' in the docs)" : ""
);
}
if (lua_pcall(node->L, 0, 0, 0) != 0)
die("kernel run %s", lua_tostring(node->L, -1));
}
static void node_free(node_t *node) {
node_t *child, *tmp;
HASH_ITER(by_name, node->childs, child, tmp) {
node_remove_child(node, child);
}
HASH_DELETE(by_wd, nodes_by_wd, node);
HASH_DELETE(by_path, nodes_by_path, node);
free(node->path);
free(node->name);
node_remove_alias(node);
client_t *client, *tmp_client;
DL_FOREACH_SAFE(node->clients, client, tmp_client) {
client_close(client);
}
assert(node->clients == NULL);
lua_close(node->L);
}
static void node_search_and_boot(node_t *node) {
DIR *dp = opendir(node->path);
if (!dp)
die("cannot open directory %s: %s", node->path, strerror(errno));
struct dirent *ep;
while ((ep = readdir(dp))) {
if (ep->d_name[0] == '.')
continue;
const char *child_name = ep->d_name;
char child_path[PATH_MAX];
snprintf(child_path, sizeof(child_path), "%s/%s", node->path, child_name);
enum { CHILD_FILE, CHILD_DIR, CHILD_UNKNOWN } type = CHILD_UNKNOWN;
if (ep->d_type == DT_UNKNOWN) {
struct stat sb;
if (stat(child_path, &sb) == -1)
die("cannot stat %s", child_path);
if (S_ISDIR(sb.st_mode)) {
type = CHILD_DIR;
} else if (S_ISREG(sb.st_mode)) {
type = CHILD_FILE;
}
} else if (ep->d_type == DT_DIR) {
type = CHILD_DIR;
} else if (ep->d_type == DT_REG) {
type = CHILD_FILE;
}
if (type == CHILD_DIR) {
node_t *child = node_add_child(node, child_path, child_name);
node_search_and_boot(child);
node_child_update(node, child->name, 1);
} else if (type == CHILD_FILE && strcmp(child_name, NODE_CODE_FILE)) {
node_content_update(node, child_name, 1);
}
}
closedir(dp);
node_boot(node);
}
static void node_init_root(node_t *root, const char *base_path) {
node_init(root, NULL, base_path, base_path);
node_search_and_boot(root);
}
static node_t *node_find_by_path_or_alias(const char *needle) {
size_t needle_size = strlen(needle);
node_t *node;
HASH_FIND(by_path, nodes_by_path, needle, needle_size, node);
if (node)
return node;
HASH_FIND(by_alias, nodes_by_alias, needle, needle_size, node);
return node;
}
static void node_print_profile(node_t *node, int depth) {
node_t *child, *tmp;
double delta = (now - node->last_profile) * 1000;
fprintf(stderr, "%c%4dkb %3.0f %5.1f %6.1f %5d %5d %5.1lf%% %5.1lf%% %5.1lf%% %*s '- %s (%s)\n",
node_is_blacklisted(node) ? 'X' : node_is_idle(node) ? ' ' : '*',
lua_gc(node->L, LUA_GCCOUNT, 0),
node->num_frames * 1000 / delta,
(double)node->num_resource_inits * 1000 / delta,
node->num_frames ? (double)node->num_allocs / node->num_frames : 0.0,
node->width, node->height,
100 / delta * node->profiling[PROFILE_BOOT],
100 / delta * node->profiling[PROFILE_UPDATE],
100 / delta * node->profiling[PROFILE_EVENT],
depth*3, "", node->name, node->alias ? node->alias : "-"
);
node_reset_profiler(node);
HASH_ITER(by_name, node->childs, child, tmp) {
node_print_profile(child, depth+1);
};
}
static void node_profiler() {
fprintf(stderr, " mem fps rps allocs width height boot update event name (alias)\n");
fprintf(stderr, "---------------------------------------------------------------------------\n");
node_print_profile(&root, 0);
fprintf(stderr, "---------------------------------------------------------------------------\n");
}
/*======= inotify ==========*/
static void check_inotify() {
static char inotify_buffer[sizeof(struct inotify_event) + PATH_MAX + 1];
while (1) {
size_t size = read(inotify_fd, &inotify_buffer, sizeof(inotify_buffer));
if (size == -1) {
if (errno == EAGAIN)
break;
die("error reading from inotify fd");
}
char *pos = inotify_buffer;
char *end = pos + size;
while (pos < end) {
struct inotify_event *event = (struct inotify_event*)pos;
pos += sizeof(struct inotify_event) + event->len;
// printf("%s %08x %d\n", event->name, event->mask, event->wd);
// ignore dot-files (including parent and current directory)
if (event->len && event->name[0] == '.')
continue; // ignore dot files
// notifies, that wd was removed from kernel.
// can be ignored (since it is handled in
// IN_DELETE_SELF).
if (event->mask & IN_IGNORED)
continue;
node_t *node;
HASH_FIND(by_wd, nodes_by_wd, &event->wd, sizeof(int), node);
if (!node)
die("node not found: %s", event->name);
char path[PATH_MAX];
snprintf(path, sizeof(path), "%s/%s", node->path, event->name);
// fprintf(stderr, "event for %s (%s), mask: %08x\n", path, event->name, event->mask);
if (event->mask & IN_CREATE) {
struct stat stat_buf;
if (stat(path, &stat_buf) == -1) {
// file/path can be gone (race between inotify and
// user actions)
fprintf(stderr, "cannot stat %s\n", path);
continue;
}
if (S_ISDIR(stat_buf.st_mode)) {
node_t *child = node_add_child(node, path, event->name);
node_search_and_boot(child);
node_child_update(node, child->name, 1);
} else if (S_ISREG(stat_buf.st_mode)) {
node_content_update(node, event->name, 1);
}
} else if (event->mask & IN_CLOSE_WRITE) {
node_content_update(node, event->name, 1);
} else if (event->mask & IN_DELETE_SELF) {
if (!node->parent)
die("root node deleted. cannot continue");
node_remove_child(node->parent, node);
} else if (event->mask & IN_DELETE && !(event->mask & IN_ISDIR)) {
node_content_update(node, event->name, 0);
} else if (event->mask & IN_MOVED_FROM) {
if (event->mask & IN_ISDIR) {
node_remove_child_by_name(node, event->name);
} else {
node_content_update(node, event->name, 0);
}
} else if (event->mask & IN_MOVED_TO) {
if (event->mask & IN_ISDIR) {
node_t *child = node_add_child(node, path, event->name);
node_search_and_boot(child);
node_child_update(node, child->name, 1);
} else {
node_content_update(node, event->name, 1);
}
}
}
}
}
/*============ GUI ===========*/
static GLFWwindow *window;
static void reshape(GLFWwindow* window, int width, int height) {
win_w = width;
win_h = height;
fprintf(stderr, INFO("resized to %dx%d\n"), width, height);
}
static void keypressed(GLFWwindow* window, int key, int scancode, int action, int mods) {
if (action != GLFW_PRESS)
return;
switch (key) {
case GLFW_KEY_SPACE:
node_profiler();
break;
case GLFW_KEY_ESCAPE:
running = 0;
break;
}
}
/*===== Util ========*/
static int create_socket(int type) {
int one = 1;
struct sockaddr_in sin;
int fd = socket(AF_INET, type, 0);
if (fd < 0)
die("socket failed: %s", strerror(errno));
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(int)) < 0)
die("setsockopt reuse failed: %s", strerror(errno));
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
const char *addr = getenv("INFOBEAMER_ADDR");
if (!addr) addr = LISTEN_ADDR;
if (!inet_aton(addr, &sin.sin_addr))
die("invalid address %s", addr);
sin.sin_port = htons(listen_port);
if (bind(fd, (struct sockaddr *)&sin, sizeof(struct sockaddr)) < 0)
die("binding to %s port %d failed: %s",
type == SOCK_DGRAM ? "udp" : "tcp",
listen_port,
strerror(errno)
);
return fd;
}
/*===== UDP (osc) Handling ========*/
static void udp_read(int fd, short event, void *arg) {
char buf[1500];
int len;
unsigned int size = sizeof(struct sockaddr);
struct sockaddr_in client_addr;
len = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&client_addr, &size);
if (len == -1)
die("recvfrom");
assert(len > 0);
// own format: <path>:<payload>
int is_osc = 0;
char payload_separator = ':';
int initial_offset = 0;
// If data starts with /, assume it's osc
// format: /<path>0x00<payload>
if (*buf == '/') {
is_osc = 1;
payload_separator = '\0';
initial_offset = 1;
};
char *sep = memchr(buf, payload_separator, len);
if (!sep) {
sendto(fd, LITERAL_AND_SIZE("fmt\n"), 0, (struct sockaddr *)&client_addr, size);
return;
}
// Terminate by NUL
*sep = '\0';
char *path = buf + initial_offset;
char *data = sep + 1;
if (is_osc) {
// round up to next multiple of 4
data += 3 - (data - buf - 1) % 4;
}
int data_len = buf + len - data;
if (data_len < 0) {
sendto(fd, LITERAL_AND_SIZE("wtf\n"), 0, (struct sockaddr *)&client_addr, size);
return;
}
// split a/b/c into first matching prefix:
// a/b -> suffix: c if node a/b exists
// fprintf(stderr, "udp event: %s: %*s\n", path, data_len, data);
char *suffix = sep;
node_t *node;
while (1) {
node = node_find_by_path_or_alias(path);
if (node)
break;
char *next_split = memrchr(path, '/', suffix - path);
if (!next_split) {
sendto(fd, LITERAL_AND_SIZE("404\n"), 0, (struct sockaddr *)&client_addr, size);
return;
}
if (suffix != sep)
*suffix = '/';
suffix = next_split;
*next_split = '\0';
}
if (suffix != sep)
suffix++;
lua_pushlstring(node->L, data, data_len);
lua_pushboolean(node->L, is_osc);
lua_pushstring(node->L, suffix);
node_event(node, "raw_data", 3);
}
static void open_udp(struct event *event) {
int fd = create_socket(SOCK_DGRAM);
event_set(event, fd, EV_READ | EV_PERSIST, &udp_read, NULL);
if (event_add(event, NULL) == -1)
die("event_add failed");
}
/*===== TCP Handler ========*/
static void client_write(client_t *client, const char *data, size_t data_size) {
bufferevent_write(client->buf_ev, data, data_size);
}
static void client_close(client_t *client) {
if (client->node) {
lua_pushlightuserdata(client->node->L, client);
node_event(client->node, "disconnect", 1);
// unlink client & node
DL_DELETE(client->node->clients, client);
client->node = NULL;
}
bufferevent_free(client->buf_ev);
close(client->fd);
free(client);
}
static void client_read(struct bufferevent *bev, void *arg) {
client_t *client = arg;
while (1) {
char *line = evbuffer_readln(bev->input, NULL, EVBUFFER_EOL_CRLF);
if (!line)
break;
if (client->node) {
lua_pushstring(client->node->L, line);
lua_pushlightuserdata(client->node->L, client);
node_event(client->node, "input", 2);
} else {
node_t *node = node_find_by_path_or_alias(line);
if (!node) {
client_write(client, LITERAL_AND_SIZE("404\n"));
} else {
// link client & node
DL_APPEND(node->clients, client);
client->node = node;
client_write(client, LITERAL_AND_SIZE("ok!\n"));
lua_pushlightuserdata(node->L, client);
node_event(client->node, "connect", 1);
}
}
free(line);
}
}
static void client_error(struct bufferevent *bev, short what, void *arg) {
client_t *client = arg;
client_close(client);
}
static void client_create(int fd) {
client_t *client = xmalloc(sizeof(client_t));
client->fd = fd;
client->buf_ev = bufferevent_new(
fd,
client_read,
NULL,
client_error,
client);
bufferevent_enable(client->buf_ev, EV_READ);
client_write(client, LITERAL_AND_SIZE(VERSION_STRING));
client_write(client, LITERAL_AND_SIZE(" ("));
client_write(client, LITERAL_AND_SIZE(INFO_URL));
client_write(client, LITERAL_AND_SIZE(") ["));
char status[64];
snprintf(status, sizeof(status), "pid %d/uptime %d", getpid(), (int)now);
client_write(client, status, strlen(status));
client_write(client, LITERAL_AND_SIZE("]. Select your channel!\n"));
}
static void accept_callback(int fd, short ev, void *arg) {
int client_fd;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
client_fd = accept(fd,
(struct sockaddr *)&client_addr,
&client_len);
if (client_fd < 0) {
fprintf(stderr, "accept() failed\n");
return;
}
evutil_make_socket_nonblocking(client_fd);
client_create(client_fd);
}
static void open_tcp(struct event *event) {
int fd = create_socket(SOCK_STREAM);
if (listen(fd, 5) < 0)
die("listen failed: %s", strerror(errno));
evutil_make_socket_nonblocking(fd);
event_set(event,
fd,
EV_READ | EV_PERSIST,
accept_callback,
NULL);
if (event_add(event, NULL) == -1)
die("event_add failed");
}
static void tick() {
now = glfwGetTime();
check_inotify();
event_loop(EVLOOP_NONBLOCK);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFuncSeparate(
GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
GL_ONE_MINUS_DST_ALPHA, GL_ONE
);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glViewport(0, 0, win_w, win_h);
glOrtho(0, win_w,
win_h, 0,
-1000, 1000);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glClearColor(0.05, 0.05, 0.05, 1);
glClear(GL_COLOR_BUFFER_BIT);
node_render_self(&root, win_w, win_h);
glfwSwapBuffers(window);
glfwPollEvents();
node_tree_gc(&root);
if (glfwWindowShouldClose(window))
running = 0;
}
static void init_default_texture() {
glGenTextures(1, &default_tex);
glBindTexture(GL_TEXTURE_2D, default_tex);
unsigned char white_pixel[] = {255, 255, 255, 255};
glTexImage2D(GL_TEXTURE_2D, 0, 4, 1, 1, 0,
GL_RGBA, GL_UNSIGNED_BYTE, white_pixel);
}
int main(int argc, char *argv[]) {
fprintf(stdout, VERSION_STRING " (" INFO_URL ")\n");
fprintf(stdout, "Copyright (c) 2015 Florian Wesch <fw@dividuum.de>\n\n");
if (argc != 2 || (argc == 2 && !strcmp(argv[1], "-h"))) {
fprintf(stderr,
"Usage: %s <path_to_root_node>\n"
"\n"
"Optional environment variables:\n"
"\n"
" INFOBEAMER_FULLSCREEN=1 # Fullscreen mode\n"
" INFOBEAMER_ADDR=<addr> # Bind to specified ip (default %s)\n"
" INFOBEAMER_PORT=<port> # Listen on alternative port (tcp & udp, default %d)\n"
" INFOBEAMER_PRECOMPILED=1 # Allow precompiled code\n"
" Warning: unsafe for untrusted code\n"
" INFOBEAMER_FULLSCALE=1 # Scale root node to full screen size\n"
" INFOBEAMER_WIDTH=<w> # Width (default 1024)\n"
" INFOBEAMER_HEIGHT=<h> # Height (default 768)\n"
"\n",
argv[0], LISTEN_ADDR, DEFAULT_PORT);
exit(1);
}
char *root_name = realpath(argv[1], NULL);
if (!root_name)
die("cannot canonicalize path: %s", strerror(errno));
char *split = rindex(root_name, '/');
if (!split)
die("no slashes in target path. cannot continue");
*split = '\0';
if (*root_name == '\0')
root_name = "/";
fprintf(stderr, INFO("chdir %s\n"), root_name);
if (chdir(root_name) == -1)
die("cannot chdir(%s): %s", root_name, strerror(errno));
root_name = split+1;
fprintf(stderr, INFO("root node is %s\n"), root_name);
inotify_fd = inotify_init1(IN_NONBLOCK);
if (inotify_fd == -1)
die("cannot open inotify: %s", strerror(errno));
av_register_all();
event_base = event_init();
dns_base = evdns_base_new(event_base, 1);
const char *port = getenv("INFOBEAMER_PORT");
listen_port = port ? atoi(port) : DEFAULT_PORT;
fprintf(stderr, INFO("tcp/udp port is %d\n"), listen_port);
struct event udp_event;
open_udp(&udp_event);
struct event tcp_event;
open_tcp(&tcp_event);
if (!glfwInit())
die("cannot initialize glfw");
int fullscreen = getenv("INFOBEAMER_FULLSCREEN") != NULL;
int width = 1024;
int height = 768;
const char *new_width = getenv("INFOBEAMER_WIDTH");
if (new_width)
width = atoi(new_width);
const char *new_height = getenv("INFOBEAMER_HEIGHT");
if (new_height)
height = atoi(new_height);
fprintf(stderr, INFO("initial size is %dx%d\n"), width, height);
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
if (!fullscreen)
monitor = NULL;
window = glfwCreateWindow(width, height, VERSION_STRING, monitor, NULL);
if (!window)
die("cannot open window");
glfwSetFramebufferSizeCallback(window, reshape);
glfwSetKeyCallback(window, keypressed);
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
glfwGetFramebufferSize(window, &win_w, &win_h);
GLenum err = glewInit();
if (err != GLEW_OK)
die("cannot initialize glew");
if (!glewIsSupported("GL_VERSION_3_0"))
die("need opengl 3.0 support\n");
if (fullscreen)
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
ilInit();
iluInit();
signal(SIGVTALRM, deadline_signal);
init_default_texture();
now = glfwGetTime();
node_init_root(&root, root_name);
fprintf(stderr, INFO("initialization completed\n"));
while (running) {
tick();
}
// no cleanup :-}
return 0;
}