diff --git a/Buildscr b/Buildscr index c143c9df..a8252c31 100644 --- a/Buildscr +++ b/Buildscr @@ -257,11 +257,11 @@ in putty/windows do mkdir deliver in putty/windows do for subdir in build32 abuild32 build64 abuild64 buildold; do mkdir deliver/$$subdir; done in putty/windows do while read x; do mv $$x deliver/$$x; mv $$x.map deliver/$$x.map; done < to-sign.txt -in putty/windows/deliver/buildold do zip -k -j putty.zip `ls *.exe | grep -v puttytel` ../../../docbuild/putty.chm -in putty/windows/deliver/build32 do zip -k -j putty.zip `ls *.exe | grep -v puttytel` ../../../docbuild/putty.chm -in putty/windows/deliver/build64 do zip -k -j putty.zip `ls *.exe | grep -v puttytel` ../../../docbuild/putty.chm -in putty/windows/deliver/abuild32 do zip -k -j putty.zip `ls *.exe | grep -v puttytel` ../../../docbuild/putty.chm -in putty/windows/deliver/abuild64 do zip -k -j putty.zip `ls *.exe | grep -v puttytel` ../../../docbuild/putty.chm +in putty/windows/deliver/buildold do zip -k -j putty.zip `ls *.exe | grep -vxE '^(puttytel|pterm).exe'` ../../../docbuild/putty.chm +in putty/windows/deliver/build32 do zip -k -j putty.zip `ls *.exe | grep -vxE '^(puttytel|pterm).exe'` ../../../docbuild/putty.chm +in putty/windows/deliver/build64 do zip -k -j putty.zip `ls *.exe | grep -vxE '^(puttytel|pterm).exe'` ../../../docbuild/putty.chm +in putty/windows/deliver/abuild32 do zip -k -j putty.zip `ls *.exe | grep -vxE '^(puttytel|pterm).exe'` ../../../docbuild/putty.chm +in putty/windows/deliver/abuild64 do zip -k -j putty.zip `ls *.exe | grep -vxE '^(puttytel|pterm).exe'` ../../../docbuild/putty.chm in docbuild/html do zip puttydoc.zip *.html # Deliver the actual PuTTY release directory into a subdir `putty'. diff --git a/cmake/platforms/windows.cmake b/cmake/platforms/windows.cmake index 931812f2..fb245003 100644 --- a/cmake/platforms/windows.cmake +++ b/cmake/platforms/windows.cmake @@ -47,6 +47,7 @@ check_symbol_exists(SetDefaultDllDirectories "windows.h" HAVE_SETDEFAULTDLLDIRECTORIES) check_symbol_exists(GetNamedPipeClientProcessId "windows.h" HAVE_GETNAMEDPIPECLIENTPROCESSID) +check_symbol_exists(CreatePseudoConsole "windows.h" HAVE_CONPTY) check_c_source_compiles(" #include diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 6165df27..a46c9a4b 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -134,6 +134,28 @@ set_target_properties(puttygen PROPERTIES LINK_FLAGS "${LFLAG_MANIFEST_NO}") installed_program(puttygen) +if(HAVE_CONPTY) + add_executable(pterm + window.c + pterm.c + help.c + conpty.c + be_conpty.c + ${CMAKE_SOURCE_DIR}/nogss.c + ${CMAKE_SOURCE_DIR}/norand.c + pterm.rc) + add_dependencies(pterm generated_licence_h) + target_link_libraries(pterm + guiterminal guimisc eventloop settings network utils + ${platform_libraries}) + set_target_properties(pterm PROPERTIES + WIN32_EXECUTABLE ON + LINK_FLAGS "${LFLAG_MANIFEST_NO}") + installed_program(pterm) +else() + message("ConPTY not available; cannot build Windows pterm") +endif() + add_executable(test_split_into_argv utils/split_into_argv.c) target_compile_definitions(test_split_into_argv PRIVATE TEST) diff --git a/windows/be_conpty.c b/windows/be_conpty.c new file mode 100644 index 00000000..d8f000b1 --- /dev/null +++ b/windows/be_conpty.c @@ -0,0 +1,13 @@ +#include +#include "putty.h" + +const char *const appname = "pterm"; + +const int be_default_protocol = -1; + +const struct BackendVtable *const backends[] = { + &conpty_backend, + NULL +}; + +const size_t n_ui_backends = 1; diff --git a/windows/conpty.c b/windows/conpty.c new file mode 100644 index 00000000..4f6f130f --- /dev/null +++ b/windows/conpty.c @@ -0,0 +1,389 @@ +/* + * Backend to run a Windows console session using ConPTY. + */ + +#include +#include +#include + +#include "putty.h" + +#include +#include + +typedef struct ConPTY ConPTY; +struct ConPTY { + HPCON pseudoconsole; + HANDLE outpipe, inpipe, hprocess; + struct handle *out, *in, *subprocess; + bool exited; + DWORD exitstatus; + Seat *seat; + LogContext *logctx; + int bufsize; + Backend backend; +}; + +static void conpty_terminate(ConPTY *conpty) +{ + if (conpty->out) { + handle_free(conpty->out); + conpty->out = NULL; + } + if (conpty->outpipe != INVALID_HANDLE_VALUE) { + CloseHandle(conpty->outpipe); + conpty->outpipe = INVALID_HANDLE_VALUE; + } + if (conpty->in) { + handle_free(conpty->in); + conpty->in = NULL; + } + if (conpty->inpipe != INVALID_HANDLE_VALUE) { + CloseHandle(conpty->inpipe); + conpty->inpipe = INVALID_HANDLE_VALUE; + } + if (conpty->subprocess) { + handle_free(conpty->subprocess); + conpty->subprocess = NULL; + conpty->hprocess = INVALID_HANDLE_VALUE; + } + if (conpty->pseudoconsole != INVALID_HANDLE_VALUE) { + ClosePseudoConsole(conpty->pseudoconsole); + conpty->pseudoconsole = INVALID_HANDLE_VALUE; + } +} + +static void conpty_process_wait_callback(void *vctx) +{ + ConPTY *conpty = (ConPTY *)vctx; + + if (!GetExitCodeProcess(conpty->hprocess, &conpty->exitstatus)) + return; + conpty->exited = true; + + /* + * We can stop waiting for the process now. + */ + if (conpty->subprocess) { + handle_free(conpty->subprocess); + conpty->subprocess = NULL; + conpty->hprocess = INVALID_HANDLE_VALUE; + } + + /* + * Once the contained process exits, close the pseudo-console as + * well. But don't close the pipes yet, since apparently + * ClosePseudoConsole can trigger a final bout of terminal output + * as things clean themselves up. + */ + if (conpty->pseudoconsole != INVALID_HANDLE_VALUE) { + ClosePseudoConsole(conpty->pseudoconsole); + conpty->pseudoconsole = INVALID_HANDLE_VALUE; + } +} + +static size_t conpty_gotdata( + struct handle *h, const void *data, size_t len, int err) +{ + ConPTY *conpty = (ConPTY *)handle_get_privdata(h); + if (err || len == 0) { + char *error_msg; + + conpty_terminate(conpty); + + seat_notify_remote_exit(conpty->seat); + + if (!err && conpty->exited) { + /* + * The clean-exit case: our subprocess terminated, we + * deleted the PseudoConsole ourself, and now we got the + * expected EOF on the pipe. + */ + return 0; + } + + if (err) + error_msg = dupprintf("Error reading from console pty: %s", + win_strerror(err)); + else + error_msg = dupprintf( + "Unexpected end of file reading from console pty"); + + logevent(conpty->logctx, error_msg); + seat_connection_fatal(conpty->seat, "%s", error_msg); + sfree(error_msg); + + return 0; + } else { + return seat_stdout(conpty->seat, data, len); + } +} + +static void conpty_sentdata(struct handle *h, size_t new_backlog, int err) +{ + ConPTY *conpty = (ConPTY *)handle_get_privdata(h); + if (err) { + const char *error_msg = "Error writing to conpty device"; + + conpty_terminate(conpty); + + seat_notify_remote_exit(conpty->seat); + + logevent(conpty->logctx, error_msg); + + seat_connection_fatal(conpty->seat, "%s", error_msg); + } else { + conpty->bufsize = new_backlog; + } +} + +static char *conpty_init(const BackendVtable *vt, Seat *seat, + Backend **backend_handle, LogContext *logctx, + Conf *conf, const char *host, int port, + char **realhost, bool nodelay, bool keepalive) +{ + ConPTY *conpty; + char *err = NULL; + + HANDLE in_r = INVALID_HANDLE_VALUE; + HANDLE in_w = INVALID_HANDLE_VALUE; + HANDLE out_r = INVALID_HANDLE_VALUE; + HANDLE out_w = INVALID_HANDLE_VALUE; + + HPCON pcon; + bool pcon_needs_cleanup = false; + + STARTUPINFOEX si; + memset(&si, 0, sizeof(si)); + + if (!CreatePipe(&in_r, &in_w, NULL, 0)) { + err = dupprintf("CreatePipe: %s", win_strerror(GetLastError())); + goto out; + } + if (!CreatePipe(&out_r, &out_w, NULL, 0)) { + err = dupprintf("CreatePipe: %s", win_strerror(GetLastError())); + goto out; + } + + COORD size; + size.X = conf_get_int(conf, CONF_width); + size.Y = conf_get_int(conf, CONF_height); + + HRESULT result = CreatePseudoConsole(size, in_r, out_w, 0, &pcon); + if (FAILED(result)) { + if (HRESULT_FACILITY(result) == FACILITY_WIN32) + err = dupprintf("CreatePseudoConsole: %s", + win_strerror(HRESULT_CODE(result))); + else + err = dupprintf("CreatePseudoConsole failed: HRESULT=0x%08x", + (unsigned)result); + goto out; + } + pcon_needs_cleanup = true; + + CloseHandle(in_r); + in_r = INVALID_HANDLE_VALUE; + CloseHandle(out_w); + out_w = INVALID_HANDLE_VALUE; + + si.StartupInfo.cb = sizeof(si); + + size_t attrsize = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &attrsize); + si.lpAttributeList = smalloc(attrsize); + if (!InitializeProcThreadAttributeList( + si.lpAttributeList, 1, 0, &attrsize)) { + err = dupprintf("InitializeProcThreadAttributeList: %s", + win_strerror(GetLastError())); + goto out; + } + if (!UpdateProcThreadAttribute( + si.lpAttributeList, + 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, + pcon, sizeof(pcon), NULL, NULL)) { + err = dupprintf("UpdateProcThreadAttribute: %s", + win_strerror(GetLastError())); + goto out; + } + + PROCESS_INFORMATION pi; + memset(&pi, 0, sizeof(pi)); + + char *command; + const char *conf_cmd = conf_get_str(conf, CONF_remote_cmd); + if (*conf_cmd) { + command = dupstr(conf_cmd); + } else { + command = dupcat(get_system_dir(), "\\cmd.exe"); + } + bool created_ok = CreateProcess(NULL, command, NULL, NULL, + false, EXTENDED_STARTUPINFO_PRESENT, + NULL, NULL, &si.StartupInfo, &pi); + sfree(command); + if (!created_ok) { + err = dupprintf("CreateProcess: %s", + win_strerror(GetLastError())); + goto out; + } + + /* No local authentication phase in this protocol */ + seat_set_trust_status(seat, false); + + conpty = snew(ConPTY); + conpty->pseudoconsole = pcon; + pcon_needs_cleanup = false; + conpty->outpipe = in_w; + conpty->out = handle_output_new(in_w, conpty_sentdata, conpty, 0); + in_w = INVALID_HANDLE_VALUE; + conpty->inpipe = out_r; + conpty->in = handle_input_new(out_r, conpty_gotdata, conpty, 0); + out_r = INVALID_HANDLE_VALUE; + conpty->subprocess = handle_add_foreign_event( + pi.hProcess, conpty_process_wait_callback, conpty); + conpty->hprocess = pi.hProcess; + CloseHandle(pi.hThread); + conpty->exited = false; + conpty->exitstatus = 0; + conpty->bufsize = 0; + conpty->backend.vt = vt; + *backend_handle = &conpty->backend; + + conpty->seat = seat; + conpty->logctx = logctx; + + *realhost = dupstr(""); + + /* + * Specials are always available. + */ + seat_update_specials_menu(conpty->seat); + + out: + if (in_r != INVALID_HANDLE_VALUE) + CloseHandle(in_r); + if (in_w != INVALID_HANDLE_VALUE) + CloseHandle(in_w); + if (out_r != INVALID_HANDLE_VALUE) + CloseHandle(out_r); + if (out_w != INVALID_HANDLE_VALUE) + CloseHandle(out_w); + if (pcon_needs_cleanup) + ClosePseudoConsole(pcon); + sfree(si.lpAttributeList); + return err; +} + +static void conpty_free(Backend *be) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + + conpty_terminate(conpty); + expire_timer_context(conpty); + sfree(conpty); +} + +static void conpty_reconfig(Backend *be, Conf *conf) +{ +} + +static size_t conpty_send(Backend *be, const char *buf, size_t len) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + + if (conpty->out == NULL) + return 0; + + conpty->bufsize = handle_write(conpty->out, buf, len); + return conpty->bufsize; +} + +static size_t conpty_sendbuffer(Backend *be) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + return conpty->bufsize; +} + +static void conpty_size(Backend *be, int width, int height) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + COORD size; + size.X = width; + size.Y = height; + ResizePseudoConsole(conpty->pseudoconsole, size); +} + +static void conpty_special(Backend *be, SessionSpecialCode code, int arg) +{ +} + +static const SessionSpecial *conpty_get_specials(Backend *be) +{ + static const SessionSpecial specials[] = { + {NULL, SS_EXITMENU} + }; + return specials; +} + +static bool conpty_connected(Backend *be) +{ + return true; /* always connected */ +} + +static bool conpty_sendok(Backend *be) +{ + return true; +} + +static void conpty_unthrottle(Backend *be, size_t backlog) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + if (conpty->in) + handle_unthrottle(conpty->in, backlog); +} + +static bool conpty_ldisc(Backend *be, int option) +{ + return false; +} + +static void conpty_provide_ldisc(Backend *be, Ldisc *ldisc) +{ +} + +static int conpty_exitcode(Backend *be) +{ + ConPTY *conpty = container_of(be, ConPTY, backend); + + if (conpty->exited && + 0 <= conpty->exitstatus && + conpty->exitstatus <= INT_MAX) + return conpty->exitstatus; + else + return -1; +} + +static int conpty_cfg_info(Backend *be) +{ + return 0; +} + +const BackendVtable conpty_backend = { + .init = conpty_init, + .free = conpty_free, + .reconfig = conpty_reconfig, + .send = conpty_send, + .sendbuffer = conpty_sendbuffer, + .size = conpty_size, + .special = conpty_special, + .get_specials = conpty_get_specials, + .connected = conpty_connected, + .exitcode = conpty_exitcode, + .sendok = conpty_sendok, + .ldisc_option_state = conpty_ldisc, + .provide_ldisc = conpty_provide_ldisc, + .unthrottle = conpty_unthrottle, + .cfg_info = conpty_cfg_info, + .id = "conpty", + .displayname = "ConPTY", + .protocol = -1, +}; diff --git a/windows/platform.h b/windows/platform.h index b746c76d..a03d9d63 100644 --- a/windows/platform.h +++ b/windows/platform.h @@ -705,7 +705,9 @@ void cli_main_loop(cliloop_pre_t pre, cliloop_post_t post, void *ctx); bool cliloop_null_pre(void *vctx, const HANDLE **, size_t *); bool cliloop_null_post(void *vctx, size_t); -/* Functions that parametrise window.c */ +extern const struct BackendVtable conpty_backend; + +/* Functions that parametrise window.c between PuTTY and pterm */ void gui_term_process_cmdline(Conf *conf, char *cmdline); const struct BackendVtable *backend_vt_from_conf(Conf *conf); const wchar_t *get_app_user_model_id(void); diff --git a/windows/pterm.c b/windows/pterm.c new file mode 100644 index 00000000..57463449 --- /dev/null +++ b/windows/pterm.c @@ -0,0 +1,45 @@ +#include "putty.h" +#include "storage.h" + +void gui_term_process_cmdline(Conf *conf, char *cmdline) +{ + do_defaults(NULL, conf); + conf_set_str(conf, CONF_remote_cmd, ""); + + cmdline = handle_restrict_acl_cmdline_prefix(cmdline); + if (handle_special_sessionname_cmdline(cmdline, conf) || + handle_special_filemapping_cmdline(cmdline, conf)) + return; + + int argc; + char **argv, **argstart; + split_into_argv(cmdline, &argc, &argv, &argstart); + + for (int i = 0; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "-e")) { + if (i+1 < argc) { + /* The command to execute is taken to be the unparsed + * version of the whole remainder of the command line. */ + conf_set_str(conf, CONF_remote_cmd, argstart[i+1]); + return; + } else { + cmdline_error("option \"%s\" requires an argument", arg); + } + } else if (arg[0] == '-') { + cmdline_error("unrecognised option \"%s\"", arg); + } else { + cmdline_error("unexpected non-option argument \"%s\"", arg); + } + } +} + +const struct BackendVtable *backend_vt_from_conf(Conf *conf) +{ + return &conpty_backend; +} + +const wchar_t *get_app_user_model_id(void) +{ + return L"SimonTatham.Pterm"; +} diff --git a/windows/pterm.rc b/windows/pterm.rc new file mode 100644 index 00000000..8bd3a043 --- /dev/null +++ b/windows/pterm.rc @@ -0,0 +1,15 @@ +#include "rcstuff.h" +#include "putty-rc.h" + +#define APPNAME "pterm" +#define APPDESC "PuTTY-style wrapper for Windows command prompts" + +IDI_MAINICON ICON "pterm.ico" +IDI_CFGICON ICON "ptermcfg.ico" + +#include "help.rc2" +#include "putty-common.rc2" + +#ifndef NO_MANIFESTS +1 RT_MANIFEST "putty.mft" +#endif /* NO_MANIFESTS */