aboutsummaryrefslogtreecommitdiffstats
path: root/erts/etc/common/run_erl_common.c
diff options
context:
space:
mode:
Diffstat (limited to 'erts/etc/common/run_erl_common.c')
-rw-r--r--erts/etc/common/run_erl_common.c686
1 files changed, 686 insertions, 0 deletions
diff --git a/erts/etc/common/run_erl_common.c b/erts/etc/common/run_erl_common.c
new file mode 100644
index 0000000000..dc55c2bea4
--- /dev/null
+++ b/erts/etc/common/run_erl_common.c
@@ -0,0 +1,686 @@
+/*
+ * %CopyrightBegin%
+ *
+ * Copyright Ericsson AB 2014. All Rights Reserved.
+ *
+ * The contents of this file are subject to the Erlang Public License,
+ * Version 1.1, (the "License"); you may not use this file except in
+ * compliance with the License. You should have received a copy of the
+ * Erlang Public License along with this software. If not, it can be
+ * retrieved online at http://www.erlang.org/.
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
+ * the License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * %CopyrightEnd%
+ */
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifdef HAVE_SYSLOG_H
+# include <syslog.h>
+#endif
+
+#ifdef __OSE__
+# include "ramlog.h"
+#endif
+
+#include "run_erl_common.h"
+#include "safe_string.h"
+
+#define DEFAULT_LOG_GENERATIONS 5
+#define LOG_MAX_GENERATIONS 1000 /* No more than 1000 log files */
+#define LOG_MIN_GENERATIONS 2 /* At least two to switch between */
+#define DEFAULT_LOG_MAXSIZE 100000
+#define LOG_MIN_MAXSIZE 1000 /* Smallast value for changing log file */
+#define LOG_STUBNAME "erlang.log."
+#define LOG_PERM 0664
+#define DEFAULT_LOG_ACTIVITY_MINUTES 5
+#define DEFAULT_LOG_ALIVE_MINUTES 15
+#define DEFAULT_LOG_ALIVE_FORMAT "%a %b %e %T %Z %Y"
+#define ALIVE_BUFFSIZ 1024
+
+#define STATUSFILENAME "/run_erl.log"
+
+#define PIPE_STUBNAME "erlang.pipe"
+#define PIPE_STUBLEN strlen(PIPE_STUBNAME)
+#define PERM (S_IWUSR | S_IRUSR | S_IWOTH | S_IROTH | S_IWGRP | S_IRGRP)
+
+/* OSE has defined O_SYNC but it is not recognized by open */
+#if !defined(O_SYNC) || defined(__OSE__)
+#undef O_SYNC
+#define O_SYNC 0
+#define USE_FSYNC 1
+#endif
+
+/* Global variable definitions
+ * We need this complex way of handling global variables because of how
+ * OSE works here. We want to make it possible to run the shell command
+ * run_erl multiple times with different global variables without them
+ * effecting eachother.
+ */
+typedef struct run_erl_ run_erl;
+
+#ifdef __OSE__
+static OSPPDKEY run_erl_pp_key;
+#define RE_DATA (*(run_erl**)ose_get_ppdata(run_erl_pp_key))
+#else
+static run_erl re;
+#define RE_DATA (&re)
+#endif
+
+#define STATUSFILE (RE_DATA->statusfile)
+#define LOG_DIR (RE_DATA->log_dir)
+#define STDSTATUS (RE_DATA->stdstatus)
+#define LOG_GENERATIONS (RE_DATA->log_generations)
+#define LOG_MAXSIZE (RE_DATA->log_maxsize)
+#define LOG_ACTIVITY_MINUTES (RE_DATA->log_activity_minutes)
+#define LOG_ALIVE_IN_GMT (RE_DATA->log_alive_in_gmt)
+#define LOG_ALIVE_FORMAT (RE_DATA->log_alive_format)
+#define RUN_DAEMON (RE_DATA->run_daemon)
+#define LOG_ALIVE_MINUTES (RE_DATA->log_alive_minutes)
+#define LOG_NUM (RE_DATA->log_num)
+#define LFD (RE_DATA->lfd)
+#define PROTOCOL_VER (RE_DATA->protocol_ver)
+
+struct run_erl_ {
+ /* constant config data */
+ char statusfile[FILENAME_BUFSIZ];
+ char log_dir[FILENAME_BUFSIZ];
+ FILE *stdstatus;
+ int log_generations;
+ int log_maxsize;
+ int log_activity_minutes;
+ int log_alive_in_gmt;
+ char log_alive_format[ALIVE_BUFFSIZ+1];
+ int run_daemon;
+ int log_alive_minutes;
+ /* Current log number and log fd */
+ int log_num;
+ int lfd;
+ unsigned protocol_ver;
+};
+
+/* prototypes */
+
+static int next_log(int log_num);
+static int prev_log(int log_num);
+static int find_next_log_num(void);
+static int open_log(int log_num, int flags);
+
+/*
+ * getenv_int:
+ */
+static char *getenv_int(const char *name) {
+#ifdef __OSE__
+ return get_env(get_bid(current_process()),name);
+#else
+ return getenv(name);
+#endif
+}
+
+/*
+ * next_log:
+ * Returns the index number that follows the given index number.
+ * (Wrapping after log_generations)
+ */
+static int next_log(int log_num) {
+ return log_num>=LOG_GENERATIONS?1:log_num+1;
+}
+
+/*
+ * prev_log:
+ * Returns the index number that precedes the given index number.
+ * (Wrapping after log_generations)
+ */
+static int prev_log(int log_num) {
+ return log_num<=1?LOG_GENERATIONS:log_num-1;
+}
+
+/*
+ * find_next_log_num()
+ * Searches through the log directory to check which logs that already
+ * exist. It finds the "hole" in the sequence, and returns the index
+ * number for the last log in the log sequence. If there is no hole, index
+ * 1 is returned.
+ */
+static int find_next_log_num(void) {
+ int i, next_gen, log_gen;
+ DIR *dirp;
+ struct dirent *direntp;
+ int log_exists[LOG_MAX_GENERATIONS+1];
+ int stub_len = strlen(LOG_STUBNAME);
+
+ /* Initialize exiting log table */
+
+ for(i=LOG_GENERATIONS; i>=0; i--)
+ log_exists[i] = 0;
+ dirp = opendir(LOG_DIR);
+ if(!dirp) {
+ ERRNO_ERR1(LOG_ERR,"Can't access log directory '%s'", LOG_DIR);
+ exit(1);
+ }
+
+ /* Check the directory for existing logs */
+
+ while((direntp=readdir(dirp)) != NULL) {
+ if(strncmp(direntp->d_name,LOG_STUBNAME,stub_len)==0) {
+ int num = atoi(direntp->d_name+stub_len);
+ if(num < 1 || num > LOG_GENERATIONS)
+ continue;
+ log_exists[num] = 1;
+ }
+ }
+ closedir(dirp);
+
+ /* Find out the next available log file number */
+
+ next_gen = 0;
+ for(i=LOG_GENERATIONS; i>=0; i--) {
+ if(log_exists[i])
+ if(next_gen)
+ break;
+ else
+ ;
+ else
+ next_gen = i;
+ }
+
+ /* Find out the current log file number */
+
+ if(next_gen)
+ log_gen = prev_log(next_gen);
+ else
+ log_gen = 1;
+
+ return log_gen;
+} /* find_next_log_num() */
+
+static int open_log(int log_num, int flags)
+{
+ char buf[FILENAME_MAX];
+ time_t now;
+ struct tm *tmptr;
+ char log_buffer[ALIVE_BUFFSIZ+1];
+
+ /* Remove the next log (to keep a "hole" in the log sequence) */
+ sn_printf(buf, sizeof(buf), "%s/%s%d",
+ LOG_DIR, LOG_STUBNAME, next_log(log_num));
+ unlink(buf);
+
+ /* Create or continue on the current log file */
+ sn_printf(buf, sizeof(buf), "%s/%s%d", LOG_DIR, LOG_STUBNAME, log_num);
+
+ LFD = sf_open(buf, flags, LOG_PERM);
+
+ if(LFD <0){
+ ERRNO_ERR1(LOG_ERR,"Can't open log file '%s'.", buf);
+ exit(1);
+ }
+
+ /* Write a LOGGING STARTED and time stamp into the log file */
+ time(&now);
+ if (LOG_ALIVE_IN_GMT) {
+ tmptr = gmtime(&now);
+ } else {
+ tmptr = localtime(&now);
+ }
+ if (!strftime(log_buffer, ALIVE_BUFFSIZ, LOG_ALIVE_FORMAT,
+ tmptr)) {
+ strn_cpy(log_buffer, sizeof(log_buffer),
+ "(could not format time in 256 positions "
+ "with current format string.)");
+ }
+ log_buffer[ALIVE_BUFFSIZ] = '\0';
+
+ sn_printf(buf, sizeof(buf), "\n=====\n===== LOGGING STARTED %s\n=====\n",
+ log_buffer);
+ if (erts_run_erl_write_all(LFD, buf, strlen(buf)) < 0)
+ erts_run_erl_log_status("Error in writing to log.\n");
+
+#if USE_FSYNC
+ fsync(LFD);
+#endif
+
+ return LFD;
+}
+
+/* Instead of making sure basename exists, we do our own */
+char *simple_basename(char *path)
+{
+ char *ptr;
+ for (ptr = path; *ptr != '\0'; ++ptr) {
+ if (*ptr == '/') {
+ path = ptr + 1;
+ }
+ }
+ return path;
+}
+
+ssize_t sf_read(int fd, void *buffer, size_t len) {
+ ssize_t n = 0;
+
+ do { n = read(fd, buffer, len); } while (n < 0 && errno == EINTR);
+
+ return n;
+}
+
+ssize_t sf_write(int fd, const void *buffer, size_t len) {
+ ssize_t n = 0;
+
+ do { n = write(fd, buffer, len); } while (n < 0 && errno == EINTR);
+
+ return n;
+}
+
+int sf_open(const char *path, int type, mode_t mode) {
+ int fd = 0;
+
+ do { fd = open(path, type, mode); } while(fd < 0 && errno == EINTR);
+
+ return fd;
+}
+
+int sf_close(int fd) {
+ int res = 0;
+
+ do { res = close(fd); } while(res < 0 && errno == EINTR);
+
+ return res;
+}
+
+/* Call write() until entire buffer has been written or error.
+ * Return len or -1.
+ */
+int erts_run_erl_write_all(int fd, const char* buf, int len)
+{
+ int left = len;
+ int written;
+ for (;;) {
+ do {
+ written = write(fd,buf,left);
+ } while (written < 0 && errno == EINTR);
+ if (written == left) {
+ return len;
+ }
+ if (written < 0) {
+ return -1;
+ }
+ left -= written;
+ buf += written;
+ }
+ return written;
+}
+
+/* erts_run_erl_log_status()
+ * Prints the arguments to a status file
+ * Works like printf (see vfrpintf)
+ */
+void erts_run_erl_log_status(const char *format,...)
+{
+ va_list args;
+ time_t now;
+
+ if (STDSTATUS == NULL)
+ STDSTATUS = fopen(STATUSFILE, "w");
+ if (STDSTATUS == NULL)
+ return;
+ now = time(NULL);
+ fprintf(STDSTATUS, "run_erl [%d] %s",
+#ifdef __OSE__
+ (int)current_process(),
+#else
+ (int)getpid(),
+#endif
+ ctime(&now));
+ va_start(args, format);
+ vfprintf(STDSTATUS, format, args);
+ va_end(args);
+ fflush(STDSTATUS);
+ return;
+}
+
+/* Fetch the current log alive minutes */
+int erts_run_erl_log_alive_minutes() {
+ return LOG_ALIVE_MINUTES;
+}
+
+/* error_logf()
+ * Prints the arguments to stderr or syslog
+ * Works like printf (see vfprintf)
+ */
+void erts_run_erl_log_error(int priority, int line, const char *format, ...)
+{
+ va_list args;
+ va_start(args, format);
+
+#ifdef HAVE_SYSLOG_H
+ if (RUN_DAEMON) {
+ vsyslog(priority,format,args);
+ }
+ else
+#endif
+#ifdef __OSE__
+ if (RUN_DAEMON) {
+ char *buff = malloc(sizeof(char)*1024);
+ vsnprintf(buff,1024,format, args);
+ ramlog_printf(buff);
+ }
+ else
+#endif
+ {
+ time_t now = time(NULL);
+ fprintf(stderr, "run_erl:%d [%d] %s", line,
+#ifdef __OSE__
+ (int)current_process(),
+#else
+ (int)getpid(),
+#endif
+ ctime(&now));
+ vfprintf(stderr, format, args);
+ }
+ va_end(args);
+}
+
+/* erts_run_erl_log_write()
+ * Writes a message to lfd. If the current log file is full,
+ * a new log file is opened.
+ */
+int erts_run_erl_log_write(char* buf, size_t len)
+{
+ int size;
+ ssize_t res;
+ /* Decide if new logfile needed, and open if so */
+
+ size = lseek(LFD,0,SEEK_END);
+ if(size+len > LOG_MAXSIZE) {
+ int res;
+ do {
+ res = close(LFD);
+ } while (res < 0 && errno == EINTR);
+ LOG_NUM = next_log(LOG_NUM);
+ LFD = open_log(LOG_NUM, O_RDWR|O_CREAT|O_TRUNC|O_SYNC);
+ }
+
+ /* Write to log file */
+
+ if ((res = erts_run_erl_write_all(LFD, buf, len)) < 0) {
+ erts_run_erl_log_status("Error in writing to log.\n");
+ }
+
+#if USE_FSYNC
+ fsync(LFD);
+#endif
+ return res;
+}
+
+int erts_run_erl_log_activity(int timeout,time_t now,time_t last_activity) {
+ char log_alive_buffer[ALIVE_BUFFSIZ+1];
+ char buf[BUFSIZ];
+
+ if (timeout || now - last_activity > LOG_ACTIVITY_MINUTES*60) {
+ /* Either a time out: 15 minutes without action, */
+ /* or something is coming in right now, but it's a long time */
+ /* since last time, so let's write a time stamp this message */
+ struct tm *tmptr;
+ if (LOG_ALIVE_IN_GMT) {
+ tmptr = gmtime(&now);
+ } else {
+ tmptr = localtime(&now);
+ }
+ if (!strftime(log_alive_buffer, ALIVE_BUFFSIZ, LOG_ALIVE_FORMAT,
+ tmptr)) {
+ strn_cpy(log_alive_buffer, sizeof(log_alive_buffer),
+ "(could not format time in 256 positions "
+ "with current format string.)");
+ }
+ log_alive_buffer[ALIVE_BUFFSIZ] = '\0';
+
+ sn_printf(buf, sizeof(buf), "\n===== %s%s\n",
+ timeout?"ALIVE ":"", log_alive_buffer);
+ return erts_run_erl_log_write(buf, strlen(buf));
+ }
+ return 0;
+}
+
+int erts_run_erl_log_open() {
+
+ LOG_NUM = find_next_log_num();
+ LFD = open_log(LOG_NUM, O_RDWR|O_APPEND|O_CREAT|O_SYNC);
+ return 0;
+}
+
+int erts_run_erl_log_init(int daemon, char* logdir) {
+ char *p;
+
+#ifdef __OSE__
+ run_erl **re_pp;
+ if (!run_erl_pp_key)
+ ose_create_ppdata("run_erl_ppdata",&run_erl_pp_key);
+ re_pp = (run_erl **)ose_get_ppdata(run_erl_pp_key);
+ *re_pp = malloc(sizeof(run_erl));
+#endif
+
+ STDSTATUS = NULL;
+ LOG_GENERATIONS = DEFAULT_LOG_GENERATIONS;
+ LOG_MAXSIZE = DEFAULT_LOG_MAXSIZE;
+ LOG_ACTIVITY_MINUTES = DEFAULT_LOG_ACTIVITY_MINUTES;
+ LOG_ALIVE_IN_GMT = 0;
+ RUN_DAEMON = 0;
+ LOG_ALIVE_MINUTES = DEFAULT_LOG_ALIVE_MINUTES;
+ LFD = 0;
+ PROTOCOL_VER = RUN_ERL_LO_VER; /* assume lowest to begin with */
+
+ /* Get values for LOG file handling from the environment */
+ if ((p = getenv_int("RUN_ERL_LOG_ALIVE_MINUTES"))) {
+ LOG_ALIVE_MINUTES = atoi(p);
+ if (!LOG_ALIVE_MINUTES) {
+ ERROR1(LOG_ERR,"Minimum value for RUN_ERL_LOG_ALIVE_MINUTES is 1 "
+ "(current value is %s)",p);
+ }
+ LOG_ACTIVITY_MINUTES = LOG_ALIVE_MINUTES / 3;
+ if (!LOG_ACTIVITY_MINUTES) {
+ ++LOG_ACTIVITY_MINUTES;
+ }
+ }
+ if ((p = getenv_int(
+ "RUN_ERL_LOG_ACTIVITY_MINUTES"))) {
+ LOG_ACTIVITY_MINUTES = atoi(p);
+ if (!LOG_ACTIVITY_MINUTES) {
+ ERROR1(LOG_ERR,"Minimum value for RUN_ERL_LOG_ACTIVITY_MINUTES is 1 "
+ "(current value is %s)",p);
+ }
+ }
+ if ((p = getenv_int("RUN_ERL_LOG_ALIVE_FORMAT"))) {
+ if (strlen(p) > ALIVE_BUFFSIZ) {
+ ERROR1(LOG_ERR, "RUN_ERL_LOG_ALIVE_FORMAT can contain a maximum of "
+ "%d characters", ALIVE_BUFFSIZ);
+ }
+ strn_cpy(LOG_ALIVE_FORMAT, sizeof(LOG_ALIVE_FORMAT), p);
+ } else {
+ strn_cpy(LOG_ALIVE_FORMAT, sizeof(LOG_ALIVE_FORMAT),
+ DEFAULT_LOG_ALIVE_FORMAT);
+ }
+ if ((p = getenv_int("RUN_ERL_LOG_ALIVE_IN_UTC"))
+ && strcmp(p,"0")) {
+ ++LOG_ALIVE_IN_GMT;
+ }
+ if ((p = getenv_int("RUN_ERL_LOG_GENERATIONS"))) {
+ LOG_GENERATIONS = atoi(p);
+ if (LOG_GENERATIONS < LOG_MIN_GENERATIONS)
+ ERROR1(LOG_ERR,"Minimum RUN_ERL_LOG_GENERATIONS is %d",
+ LOG_MIN_GENERATIONS);
+ if (LOG_GENERATIONS > LOG_MAX_GENERATIONS)
+ ERROR1(LOG_ERR,"Maximum RUN_ERL_LOG_GENERATIONS is %d",
+ LOG_MAX_GENERATIONS);
+ }
+
+ if ((p = getenv_int("RUN_ERL_LOG_MAXSIZE"))) {
+ LOG_MAXSIZE = atoi(p);
+ if (LOG_MAXSIZE < LOG_MIN_MAXSIZE)
+ ERROR1(LOG_ERR,"Minimum RUN_ERL_LOG_MAXSIZE is %d", LOG_MIN_MAXSIZE);
+ }
+
+ RUN_DAEMON = daemon;
+
+ strn_cpy(LOG_DIR, sizeof(LOG_DIR), logdir);
+ strn_cpy(STATUSFILE, sizeof(STATUSFILE), LOG_DIR);
+ strn_cat(STATUSFILE, sizeof(STATUSFILE), STATUSFILENAME);
+
+ return 0;
+}
+
+/* create_fifo()
+ * Creates a new fifo with the given name and permission.
+ */
+static int create_fifo(char *name, int perm)
+{
+ if ((mkfifo(name, perm) < 0) && (errno != EEXIST))
+ return -1;
+ return 0;
+}
+
+/*
+ * w- and r_pipename have to be pre-allocated of atleast FILENAME_MAX size
+ */
+int erts_run_erl_open_fifo(char *pipename,char *w_pipename,char *r_pipename) {
+ int calculated_pipename = 0;
+ int highest_pipe_num = 0;
+ int fd;
+
+ /*
+ * Create FIFOs and open them
+ */
+
+ if(*pipename && pipename[strlen(pipename)-1] == '/') {
+ /* The user wishes us to find a unique pipe name in the specified */
+ /* directory */
+ DIR *dirp;
+ struct dirent *direntp;
+
+ calculated_pipename = 1;
+ dirp = opendir(pipename);
+ if(!dirp) {
+ ERRNO_ERR1(LOG_ERR,"Can't access pipe directory '%s'.", pipename);
+ return 1;
+ }
+
+ /* Check the directory for existing pipes */
+
+ while((direntp=readdir(dirp)) != NULL) {
+ if(strncmp(direntp->d_name,PIPE_STUBNAME,PIPE_STUBLEN)==0) {
+ int num = atoi(direntp->d_name+PIPE_STUBLEN+1);
+ if(num > highest_pipe_num)
+ highest_pipe_num = num;
+ }
+ }
+ closedir(dirp);
+ strn_catf(pipename, BUFSIZ, "%s.%d",
+ PIPE_STUBNAME, highest_pipe_num+1);
+ } /* if */
+
+ for(;;) {
+ /* write FIFO - is read FIFO for `to_erl' program */
+ strn_cpy(w_pipename, BUFSIZ, pipename);
+ strn_cat(w_pipename, BUFSIZ, ".r");
+ if (create_fifo(w_pipename, PERM) < 0) {
+ ERRNO_ERR1(LOG_ERR,"Cannot create FIFO %s for writing.",
+ w_pipename);
+ return 1;
+ }
+
+ /* read FIFO - is write FIFO for `to_erl' program */
+ strn_cpy(r_pipename, BUFSIZ, pipename);
+ strn_cat(r_pipename, BUFSIZ, ".w");
+
+ /* Check that nobody is running run_erl already */
+ if ((fd = sf_open(r_pipename, O_WRONLY|DONT_BLOCK_PLEASE, 0)) >= 0) {
+ /* Open as client succeeded -- run_erl is already running! */
+ sf_close(fd);
+ if (calculated_pipename) {
+ ++highest_pipe_num;
+ strn_catf(pipename, BUFSIZ, "%s.%d",
+ PIPE_STUBNAME, highest_pipe_num+1);
+ continue;
+ }
+ ERROR1(LOG_ERR, "Erlang already running on pipe %s.\n", pipename);
+ unlink(w_pipename);
+ return 1;
+ }
+ if (create_fifo(r_pipename, PERM) < 0) {
+ unlink(w_pipename);
+ ERRNO_ERR1(LOG_ERR,"Cannot create FIFO %s for reading.",
+ r_pipename);
+ return 1;
+ }
+ break;
+ }
+ return 0;
+}
+
+/* Extract any control sequences that are ment only for run_erl
+ * and should not be forwarded to the pty.
+ */
+int erts_run_erl_extract_ctrl_seq(char* buf, int len)
+{
+ static const char prefix[] = "\033_";
+ static const char suffix[] = "\033\\";
+ char* bufend = buf + len;
+ char* start = buf;
+ char* command;
+ char* end;
+
+ for (;;) {
+ start = find_str(start, bufend-start, prefix);
+ if (!start) break;
+
+ command = start + strlen(prefix);
+ end = find_str(command, bufend-command, suffix);
+ if (end) {
+ unsigned col, row;
+ if (sscanf(command,"version=%u", &PROTOCOL_VER)==1) {
+ /*fprintf(stderr,"to_erl v%u\n", protocol_ver);*/
+ }
+ else if (sscanf(command,"winsize=%u,%u", &col, &row)==2) {
+#ifdef TIOCSWINSZ
+ struct winsize ws;
+ ws.ws_col = col;
+ ws.ws_row = row;
+ if (ioctl(MFD, TIOCSWINSZ, &ws) < 0) {
+ ERRNO_ERR0(LOG_ERR,"Failed to set window size");
+ }
+#endif
+ }
+ else {
+ ERROR2(LOG_ERR, "Ignoring unknown ctrl command '%.*s'\n",
+ (int)(end-command), command);
+ }
+
+ /* Remove ctrl sequence from buf */
+ end += strlen(suffix);
+ memmove(start, end, bufend-end);
+ bufend -= end - start;
+ }
+ else {
+ ERROR2(LOG_ERR, "Missing suffix in ctrl sequence '%.*s'\n",
+ (int)(bufend-start), start);
+ break;
+ }
+ }
+ return bufend - buf;
+}