mirror of https://github.com/fltk/fltk.git
FLTK - Fast Light Tool Kit - https://github.com/fltk/fltk - cross platform GUI development
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
576 lines
18 KiB
576 lines
18 KiB
// |
|
// External code editor management class for Unix |
|
// |
|
// Note: This entire file Unix only |
|
|
|
#include "ExternalCodeEditor_UNIX.h" |
|
|
|
#include "fluid.h" |
|
|
|
#include <FL/Fl.H> /* Fl_Timeout_Handler.. */ |
|
#include <FL/fl_ask.H> /* fl_alert() */ |
|
#include <FL/fl_string_functions.h> /* fl_strdup() */ |
|
|
|
#include <errno.h> /* errno */ |
|
#include <string.h> /* strerror() */ |
|
#include <sys/types.h> /* stat().. */ |
|
#include <sys/stat.h> |
|
#include <sys/wait.h> /* waitpid().. */ |
|
#include <fcntl.h> /* open().. */ |
|
#include <signal.h> /* kill().. */ |
|
#include <unistd.h> |
|
#include <stdlib.h> /* free().. */ |
|
#include <stdio.h> /* snprintf().. */ |
|
|
|
// Static local data |
|
static int L_editors_open = 0; // keep track of #editors open |
|
static Fl_Timeout_Handler L_update_timer_cb = 0; // app's update timer callback |
|
|
|
// [Static/Local] See if file exists |
|
static int is_file(const char *filename) { |
|
struct stat buf; |
|
if ( stat(filename, &buf) < 0 ) return(0); |
|
return(S_ISREG(buf.st_mode) ? 1 : 0); // regular file? |
|
} |
|
|
|
// [Static/Local] See if dir exists |
|
static int is_dir(const char *dirname) { |
|
struct stat buf; |
|
if ( stat(dirname, &buf) < 0 ) return(0); |
|
return(S_ISDIR(buf.st_mode) ? 1 : 0); // a dir? |
|
} |
|
|
|
// ---- ExternalCodeEditor implementation |
|
|
|
/** \class ExternalCodeEditor |
|
Support for an external C++ code editor for Fluid Code block. |
|
|
|
This class can launch and quit a user defined program for editing |
|
code outside of Fluid. |
|
It observes changes in the external file and updates the Fluid |
|
widget to stay synchronized. |
|
*/ |
|
|
|
/** |
|
Create the manager for external code editors. |
|
*/ |
|
ExternalCodeEditor::ExternalCodeEditor() { |
|
pid_ = -1; |
|
filename_ = 0; |
|
file_mtime_ = 0; |
|
file_size_ = 0; |
|
alert_pipe_[0] = alert_pipe_[1] = -1; |
|
alert_pipe_open_ = false; |
|
} |
|
|
|
/** |
|
Destroy the manager. |
|
This also closes the external editor. |
|
*/ |
|
ExternalCodeEditor::~ExternalCodeEditor() { |
|
if ( G_debug ) |
|
printf("ExternalCodeEditor() DTOR CALLED (this=%p, pid=%ld)\n", |
|
(void*)this, (long)pid_); |
|
close_editor(); // close editor, delete tmp file |
|
set_filename(0); // free()s filename |
|
|
|
if (alert_pipe_open_) { |
|
Fl::remove_fd(alert_pipe_[0]); |
|
if (alert_pipe_[0] != -1) ::close(alert_pipe_[0]); |
|
if (alert_pipe_[1] != -1) ::close(alert_pipe_[1]); |
|
} |
|
} |
|
|
|
/** |
|
Set the filename for the file we wish to edit. |
|
Handles memory allocation/free. |
|
If set to NULL, frees memory. |
|
\param[in] val new filename |
|
*/ |
|
void ExternalCodeEditor::set_filename(const char *val) { |
|
if ( filename_ ) free((void*)filename_); |
|
filename_ = val ? fl_strdup(val) : 0; |
|
} |
|
|
|
/** |
|
Is editor running? |
|
\return 1 if we are currently editing a file. |
|
*/ |
|
int ExternalCodeEditor::is_editing() { |
|
return( (pid_ != -1) ? 1 : 0 ); |
|
} |
|
|
|
/** |
|
Wait for editor to close |
|
*/ |
|
void ExternalCodeEditor::close_editor() { |
|
if ( G_debug ) printf("close_editor() called: pid=%ld\n", long(pid_)); |
|
// Wait until editor is closed + reaped |
|
while ( is_editing() ) { |
|
switch ( reap_editor() ) { |
|
case -2: // no editor running (unlikely to happen) |
|
return; |
|
case -1: // error |
|
fl_alert("Error reaping external editor\n" |
|
"pid=%ld file=%s", long(pid_), filename()); |
|
break; |
|
case 0: // process still running |
|
switch ( fl_choice("Please close external editor\npid=%ld file=%s", |
|
"Force Close", // button 0 |
|
"Closed", // button 1 |
|
0, // button 2 |
|
long(pid_), filename() ) ) { |
|
case 0: // Force Close |
|
kill_editor(); |
|
continue; |
|
case 1: // Closed? try to reap |
|
continue; |
|
} |
|
break; |
|
case 1: // process reaped |
|
return; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
Kill the running editor (if any). |
|
|
|
Kills the editor, reaps the process, and removes the tmp file. |
|
The dtor calls this to ensure no editors remain running when fluid exits. |
|
*/ |
|
void ExternalCodeEditor::kill_editor() { |
|
if ( G_debug ) printf("kill_editor() called: pid=%ld\n", (long)pid_); |
|
if ( !is_editing() ) return; // editor not running? return.. |
|
kill(pid_, SIGTERM); // kill editor |
|
int wcount = 0; |
|
while ( is_editing() ) { // and wait for editor to finish.. |
|
usleep(100000); // 1/10th sec delay gives editor time to close itself |
|
pid_t pid_reaped; |
|
switch ( reap_editor(&pid_reaped) ) { |
|
case -2: // editor not running (unlikely to happen) |
|
return; |
|
case -1: // error |
|
fl_alert("Can't seem to close editor of file: %s\n" |
|
"waitpid() returned: %s\n" |
|
"Please close editor and hit OK", |
|
filename(), strerror(errno)); |
|
continue; |
|
case 0: // process still running |
|
if ( ++wcount > 3 ) { // retry 3x with 1/10th delay before showing dialog |
|
fl_alert("Can't seem to close editor of file: %s\n" |
|
"Please close editor and hit OK", filename()); |
|
} |
|
continue; |
|
case 1: // process reaped (reap_editor() sets pid_ to -1) |
|
if ( G_debug ) |
|
printf("*** REAPED KILLED EXTERNAL EDITOR: PID %ld\n", (long)pid_reaped); |
|
break; |
|
} |
|
} |
|
return; |
|
} |
|
|
|
/** |
|
Handle if file changed since last check, and update records if so. |
|
|
|
Load new data into 'code', which caller must free(). |
|
If 'force' set, forces reload even if file size/time didn't change. |
|
|
|
\param[in] code |
|
\param[in] force |
|
\return 0 if file unchanged or not editing |
|
\return 1 if file changed, internal records updated, 'code' has new content |
|
\return -1 error getting file info (strerror() has reason) |
|
*/ |
|
int ExternalCodeEditor::handle_changes(const char **code, int force) { |
|
code[0] = 0; |
|
if ( !is_editing() ) return 0; |
|
// Get current time/size info, see if file changed |
|
int changed = 0; |
|
{ |
|
struct stat sbuf; |
|
if ( stat(filename(), &sbuf) < 0 ) return(-1); // TODO: show fl_alert(), do this in win32 too, adjust func call docs above |
|
time_t now_mtime = sbuf.st_mtime; |
|
size_t now_size = sbuf.st_size; |
|
// OK, now see if file changed; update records if so |
|
if ( now_mtime != file_mtime_ ) { changed = 1; file_mtime_ = now_mtime; } |
|
if ( now_size != file_size_ ) { changed = 1; file_size_ = now_size; } |
|
} |
|
// No changes? done |
|
if ( !changed && !force ) return 0; |
|
// Changes? Load file, and fallthru to close() |
|
int fd = open(filename(), O_RDONLY); |
|
if ( fd < 0 ) { |
|
fl_alert("ERROR: can't open '%s': %s", filename(), strerror(errno)); |
|
return -1; |
|
} |
|
int ret = 0; |
|
char *buf = (char*)malloc(file_size_ + 1); |
|
ssize_t count = read(fd, buf, file_size_); |
|
if ( count == -1 ) { |
|
fl_alert("ERROR: read() %s: %s", filename(), strerror(errno)); |
|
free((void*)buf); |
|
ret = -1; |
|
} else if ( (long)count != (long)file_size_ ) { |
|
fl_alert("ERROR: read() failed for %s:\n" |
|
"expected %ld bytes, only got %ld", |
|
filename(), long(file_size_), long(count)); |
|
ret = -1; |
|
} else { |
|
// Success -- file loaded OK |
|
buf[count] = '\0'; |
|
code[0] = buf; // return pointer to allocated buffer |
|
ret = 1; |
|
} |
|
close(fd); |
|
return ret; |
|
} |
|
|
|
/** |
|
Remove the tmp file (if it exists), and zero out filename/mtime/size. |
|
\return -1 on error (dialog is posted as to why) |
|
\return 0 no file to remove |
|
\return 1 -- file was removed |
|
*/ |
|
int ExternalCodeEditor::remove_tmpfile() { |
|
const char *tmpfile = filename(); |
|
if ( !tmpfile ) return 0; |
|
// Filename set? remove (if exists) and zero filename/mtime/size |
|
if ( is_file(tmpfile) ) { |
|
if ( G_debug ) printf("Removing tmpfile '%s'\n", tmpfile); |
|
if ( remove(tmpfile) < 0 ) { |
|
fl_alert("WARNING: Can't remove() '%s': %s", tmpfile, strerror(errno)); |
|
return -1; |
|
} |
|
} |
|
set_filename(0); |
|
file_mtime_ = 0; |
|
file_size_ = 0; |
|
return 1; |
|
} |
|
|
|
/** |
|
Return tmpdir name for this fluid instance. |
|
\return pointer to static memory. |
|
*/ |
|
const char* ExternalCodeEditor::tmpdir_name() { |
|
static char dirname[100]; |
|
snprintf(dirname, sizeof(dirname), "/tmp/.fluid-%ld", (long)getpid()); |
|
return dirname; |
|
} |
|
|
|
/** |
|
Clear the external editor's tempdir. |
|
Static so that the main program can call it on exit to clean up. |
|
*/ |
|
void ExternalCodeEditor::tmpdir_clear() { |
|
const char *tmpdir = tmpdir_name(); |
|
if ( is_dir(tmpdir) ) { |
|
if ( G_debug ) printf("Removing tmpdir '%s'\n", tmpdir); |
|
if ( rmdir(tmpdir) < 0 ) { |
|
fl_alert("WARNING: Can't rmdir() '%s': %s", tmpdir, strerror(errno)); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
Creates temp dir (if doesn't exist) and returns the dirname |
|
as a static string. |
|
\return NULL on error, dialog shows reason. |
|
*/ |
|
const char* ExternalCodeEditor::create_tmpdir() { |
|
const char *dirname = tmpdir_name(); |
|
if ( ! is_dir(dirname) ) { |
|
if ( mkdir(dirname, 0777) < 0 ) { |
|
fl_alert("can't create directory '%s': %s", |
|
dirname, strerror(errno)); |
|
return NULL; |
|
} |
|
} |
|
return dirname; |
|
} |
|
|
|
/** |
|
Returns temp filename in static buffer. |
|
\return NULL if can't, posts dialog explaining why. |
|
*/ |
|
const char* ExternalCodeEditor::tmp_filename() { |
|
static char path[FL_PATH_MAX+1]; |
|
const char *tmpdir = create_tmpdir(); |
|
if ( !tmpdir ) return 0; |
|
const char *ext = g_project.code_file_name.c_str(); // e.g. ".cxx" |
|
snprintf(path, FL_PATH_MAX, "%s/%p%s", tmpdir, (void*)this, ext); |
|
path[FL_PATH_MAX] = 0; |
|
return path; |
|
} |
|
|
|
/** |
|
Save string 'code' to 'filename', returning file's mtime/size. |
|
'code' can be NULL -- writes an empty file if so. |
|
\return 0 on success |
|
\return -1 on error (posts dialog with reason) |
|
*/ |
|
static int save_file(const char *filename, const char *code) { |
|
if ( code == 0 ) code = ""; // NULL? write an empty file |
|
int fd = open(filename, O_WRONLY|O_CREAT, 0666); |
|
if ( fd == -1 ) { |
|
fl_alert("ERROR: open() '%s': %s", filename, strerror(errno)); |
|
return -1; |
|
} |
|
ssize_t clen = strlen(code); |
|
ssize_t count = write(fd, code, clen); |
|
int ret = 0; |
|
if ( count == -1 ) { |
|
fl_alert("ERROR: write() '%s': %s", filename, strerror(errno)); |
|
ret = -1; // fallthru to close() |
|
} else if ( count != clen ) { |
|
fl_alert("ERROR: write() '%s': wrote only %lu bytes, expected %lu", |
|
filename, (unsigned long)count, (unsigned long)clen); |
|
ret = -1; // fallthru to close() |
|
} |
|
close(fd); |
|
return(ret); |
|
} |
|
|
|
/** |
|
Convert string 's' to array of argv[], useful for execve(). |
|
- 's' will be modified (words will be NULL separated) |
|
- argv[] will end up pointing to the words of 's' |
|
- Caller must free argv with: free(argv); |
|
\return -1 in case of memory allocation error |
|
\return number of arguments in argv (same value as in argc) |
|
*/ |
|
static int make_args(char *s, // string containing words (gets trashed!) |
|
int *aargc, // pointer to argc |
|
char ***aargv) { // pointer to argv |
|
char *ss, **argv; |
|
if ((argv=(char**)malloc(sizeof(char*) * (strlen(s)/2)))==NULL) { |
|
return -1; |
|
} |
|
int t; |
|
for(t=0; (t==0)?(ss=strtok(s," \t")):(ss=strtok(0," \t")); t++) { |
|
argv[t] = ss; |
|
} |
|
argv[t] = 0; |
|
aargv[0] = argv; |
|
aargc[0] = t; |
|
return(t); |
|
} |
|
|
|
/** |
|
If no alert pipe is open yet, try to create the pipe and hook it up the the fd callback. |
|
|
|
The alert pipe is used to communicate from the forked process to the main |
|
FLTK app in case launching the editor failed. |
|
*/ |
|
void ExternalCodeEditor::open_alert_pipe() { |
|
if (!alert_pipe_open_) { |
|
if (::pipe(alert_pipe_) == 0) { |
|
Fl::add_fd(alert_pipe_[0], FL_READ, alert_pipe_cb, this); |
|
alert_pipe_open_ = true; |
|
} else { |
|
alert_pipe_[0] = alert_pipe_[1] = -1; |
|
} |
|
} |
|
} |
|
|
|
/** |
|
Start editor in background (fork/exec) |
|
\return 0 on success, leaves editor child process running as 'pid_' |
|
\return -1 on error, posts dialog with reason (child exits) |
|
*/ |
|
int ExternalCodeEditor::start_editor(const char *editor_cmd, |
|
const char *filename) { |
|
if ( G_debug ) printf("start_editor() cmd='%s', filename='%s'\n", |
|
editor_cmd, filename); |
|
char cmd[1024]; |
|
snprintf(cmd, sizeof(cmd), "%s %s", editor_cmd, filename); |
|
command_line_ = editor_cmd; |
|
open_alert_pipe(); |
|
// Fork editor to background.. |
|
switch ( pid_ = fork() ) { |
|
case -1: // error |
|
fl_alert("couldn't fork(): %s", strerror(errno)); |
|
return -1; |
|
case 0: { // child |
|
// NOTE: OSX wants minimal code between fork/exec, see Apple TN2083 |
|
// NOTE: no FLTK calls after a fork. Use a pipe to tell the app if the |
|
// command can't launch |
|
int nargs; |
|
char **args = 0; |
|
if (make_args(cmd, &nargs, &args) > 0) { |
|
execvp(args[0], args); // run command - doesn't return if succeeds |
|
if (alert_pipe_open_) { |
|
int err = errno; |
|
if (::write(alert_pipe_[1], &err, sizeof(int)) != sizeof(int)) { |
|
// should not happen, but if it does, at least we tried |
|
} |
|
} |
|
exit(1); |
|
} |
|
exit(1); |
|
// break; |
|
} |
|
default: // parent |
|
if ( L_editors_open++ == 0 ) // first editor? start timers |
|
{ start_update_timer(); } |
|
if ( G_debug ) |
|
printf("--- EDITOR STARTED: pid_=%ld #open=%d\n", (long)pid_, L_editors_open); |
|
break; |
|
} |
|
return 0; |
|
} |
|
|
|
/** |
|
Try to reap external editor process. |
|
|
|
If 'pid_reaped' not NULL, returns PID of reaped editor. |
|
|
|
\return -2: editor not open |
|
\return -1: waitpid() failed (errno has reason) |
|
\return 0: process still running |
|
\return 1: process finished + reaped ('pid_reaped' has pid), pid_ set to -1. |
|
Handles removing tmpfile/zeroing file_mtime/file_size/filename |
|
\return If return value <=0, 'pid_reaped' is set to zero. |
|
*/ |
|
int ExternalCodeEditor::reap_editor(pid_t *pid_reaped) { |
|
if ( pid_reaped ) *pid_reaped = 0; |
|
if ( !is_editing() ) return -2; |
|
int status = 0; |
|
pid_t wpid; |
|
switch (wpid = waitpid(pid_, &status, WNOHANG)) { |
|
case -1: // waitpid() failed |
|
return -1; |
|
case 0: // process didn't reap, still running |
|
return 0; |
|
default: // process reaped |
|
if ( pid_reaped ) *pid_reaped = wpid; // return pid to caller |
|
remove_tmpfile(); // also zeroes mtime/size |
|
pid_ = -1; |
|
if ( --L_editors_open <= 0 ) |
|
{ stop_update_timer(); } |
|
break; |
|
} |
|
if ( G_debug ) |
|
printf("*** EDITOR REAPED: pid=%ld #open=%d\n", long(wpid), L_editors_open); |
|
return 1; |
|
} |
|
|
|
/** |
|
Open external editor using 'editor_cmd' to edit 'code'. |
|
|
|
'code' contains multiline code to be edited as a temp file. |
|
'code' can be NULL -- edits an empty file if so. |
|
|
|
\return 0 if succeeds |
|
\return -1 if can't open editor (already open, etc), |
|
errors were shown to user in a dialog |
|
*/ |
|
int ExternalCodeEditor::open_editor(const char *editor_cmd, |
|
const char *code) { |
|
// Make sure a temp filename exists |
|
if ( !filename() ) { |
|
set_filename(tmp_filename()); |
|
if ( !filename() ) return -1; |
|
} |
|
// See if tmpfile already exists or editor already open |
|
if ( is_file(filename()) ) { |
|
if ( is_editing() ) { |
|
// See if editor recently closed but not reaped; try to reap |
|
pid_t wpid; |
|
switch ( reap_editor(&wpid) ) { |
|
case -2: // no editor running? (unlikely if is_editing() true) |
|
break; |
|
case -1: // waitpid() failed |
|
fl_alert("ERROR: waitpid() failed: %s\nfile='%s', pid=%ld", |
|
strerror(errno), filename(), (long)pid_); |
|
return -1; |
|
case 0: // process still running |
|
fl_alert("Editor Already Open\n file='%s'\n pid=%ld", |
|
filename(), (long)pid_); |
|
return 0; |
|
case 1: // process reaped, wpid is pid reaped |
|
if ( G_debug ) |
|
printf("*** REAPED EXTERNAL EDITOR: PID %ld\n", (long)wpid); |
|
break; // fall thru to open new editor instance |
|
} |
|
// Reinstate tmp filename (reap_editor() clears it) |
|
set_filename(tmp_filename()); |
|
} |
|
} |
|
if ( save_file(filename(), code) < 0 ) { |
|
return -1; // errors were shown in dialog |
|
} |
|
// Update mtime/size from closed file |
|
struct stat sbuf; |
|
if ( stat(filename(), &sbuf) < 0 ) { |
|
fl_alert("ERROR: can't stat('%s'): %s", filename(), strerror(errno)); |
|
return -1; |
|
} |
|
file_mtime_ = sbuf.st_mtime; |
|
file_size_ = sbuf.st_size; |
|
if ( start_editor(editor_cmd, filename()) < 0 ) { // open file in external editor |
|
if ( G_debug ) printf("Editor failed to start\n"); |
|
return -1; // errors were shown in dialog |
|
} |
|
return 0; |
|
} |
|
|
|
/** |
|
Start update timer. |
|
*/ |
|
void ExternalCodeEditor::start_update_timer() { |
|
if ( !L_update_timer_cb ) return; |
|
if ( G_debug ) printf("--- TIMER: STARTING UPDATES\n"); |
|
Fl::add_timeout(2.0, L_update_timer_cb); |
|
} |
|
|
|
/** |
|
Stop update timer. |
|
*/ |
|
void ExternalCodeEditor::stop_update_timer() { |
|
if ( !L_update_timer_cb ) return; |
|
if ( G_debug ) printf("--- TIMER: STOPPING UPDATES\n"); |
|
Fl::remove_timeout(L_update_timer_cb); |
|
} |
|
|
|
/** |
|
Set app's external editor update timer callback. |
|
|
|
This is the app's callback callback we start while editors are open, |
|
and stop when all editors are closed. |
|
*/ |
|
void ExternalCodeEditor::set_update_timer_callback(Fl_Timeout_Handler cb) { |
|
L_update_timer_cb = cb; |
|
} |
|
|
|
/** |
|
See if any external editors are open. |
|
App's timer cb can see if any editors need checking.. |
|
*/ |
|
int ExternalCodeEditor::editors_open() { |
|
return L_editors_open; |
|
} |
|
|
|
/** |
|
It the forked process can't run the editor, it will send the errno through a pipe. |
|
*/ |
|
void ExternalCodeEditor::alert_pipe_cb(FL_SOCKET s, void* d) { |
|
ExternalCodeEditor* self = (ExternalCodeEditor*)d; |
|
self->last_error_ = 0; |
|
if (::read(s, &self->last_error_, sizeof(int)) != sizeof(int)) |
|
return; |
|
const char* cmd = self->command_line_.c_str(); |
|
if (cmd && *cmd) { |
|
if (cmd[0] == '/') { // is this an absolute filename? |
|
fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"", |
|
fl_filename_name(cmd), strerror(self->last_error_), cmd); |
|
} else { |
|
char pwd[FL_PATH_MAX+1]; |
|
fl_getcwd(pwd, FL_PATH_MAX); |
|
fl_alert("Can't launch external editor '%s':\n%s\n\ncmd: \"%s\"\npwd: \"%s\"", |
|
fl_filename_name(cmd), strerror(self->last_error_), cmd, pwd); |
|
} |
|
} |
|
}
|
|
|