From bc7e06c49411a891fe5d0f6c6f33209b123c6030 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sat, 2 Apr 2022 16:18:08 +0100 Subject: [PATCH] Windows tools: assorted '-demo' options. Using a new screenshot-taking module I just added in windows/utils, these new options allow me to start up one of the tools with demonstration window contents and automatically save a .BMP screenshot to disk. This will allow me to keep essentially the same set of demo images and update them easily to keep pace with the current appearance of the real tools as PuTTY - and Windows itself - both evolve. --- cmake/cmake.h.in | 1 + cmake/platforms/windows.cmake | 2 + settings.c | 18 +++-- windows/CMakeLists.txt | 1 + windows/dialog.c | 18 +++++ windows/platform.h | 3 + windows/pterm.c | 4 ++ windows/putty.c | 76 ++++++++++++++++++-- windows/puttygen.c | 40 +++++++++++ windows/utils/screenshot.c | 126 ++++++++++++++++++++++++++++++++++ windows/window.c | 2 + 11 files changed, 279 insertions(+), 12 deletions(-) create mode 100644 windows/utils/screenshot.c diff --git a/cmake/cmake.h.in b/cmake/cmake.h.in index 9de1386b..06f26176 100644 --- a/cmake/cmake.h.in +++ b/cmake/cmake.h.in @@ -13,6 +13,7 @@ #cmakedefine01 HAVE_GETNAMEDPIPECLIENTPROCESSID #cmakedefine01 HAVE_SETDEFAULTDLLDIRECTORIES #cmakedefine01 HAVE_STRTOUMAX +#cmakedefine01 HAVE_DWMAPI_H #cmakedefine NOT_X_WINDOWS diff --git a/cmake/platforms/windows.cmake b/cmake/platforms/windows.cmake index ef3f7825..e1da07dc 100644 --- a/cmake/platforms/windows.cmake +++ b/cmake/platforms/windows.cmake @@ -49,6 +49,8 @@ check_symbol_exists(GetNamedPipeClientProcessId "windows.h" HAVE_GETNAMEDPIPECLIENTPROCESSID) check_symbol_exists(CreatePseudoConsole "windows.h" HAVE_CONPTY) +check_include_files("windows.h;dwmapi.h" HAVE_DWMAPI_H) + check_c_source_compiles(" #include GCP_RESULTSW gcpw; diff --git a/settings.c b/settings.c index ff2bb6c4..98313d17 100644 --- a/settings.c +++ b/settings.c @@ -1307,6 +1307,8 @@ static int sessioncmp(const void *av, const void *bv) return strcmp(a, b); /* otherwise, compare normally */ } +bool sesslist_demo_mode = false; + void get_sesslist(struct sesslist *list, bool allocate) { int i; @@ -1316,12 +1318,18 @@ void get_sesslist(struct sesslist *list, bool allocate) if (allocate) { strbuf *sb = strbuf_new(); - if ((handle = enum_settings_start()) != NULL) { - while (enum_settings_next(handle, sb)) - put_byte(sb, '\0'); - enum_settings_finish(handle); + if (sesslist_demo_mode) { + put_asciz(sb, "demo-server"); + put_asciz(sb, "demo-server-2"); + } else { + if ((handle = enum_settings_start()) != NULL) { + while (enum_settings_next(handle, sb)) + put_byte(sb, '\0'); + enum_settings_finish(handle); + } + put_byte(sb, '\0'); } - put_byte(sb, '\0'); + list->buffer = strbuf_to_str(sb); /* diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index aad2f5af..b988792a 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -28,6 +28,7 @@ add_sources_from_current_dir(utils utils/platform_get_x_display.c utils/registry_get_string.c utils/request_file.c + utils/screenshot.c utils/security.c utils/split_into_argv.c utils/version.c diff --git a/windows/dialog.c b/windows/dialog.c index 31bf19e6..38af0f9b 100644 --- a/windows/dialog.c +++ b/windows/dialog.c @@ -388,6 +388,8 @@ static void create_controls(HWND hwnd, char *path) } } +const char *dialog_box_demo_screenshot_filename = NULL; + /* * This function is the configuration box. * (Being a dialog procedure, in general it returns 0 if the default @@ -396,6 +398,7 @@ static void create_controls(HWND hwnd, char *path) static INT_PTR CALLBACK GenericMainDlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + const int DEMO_SCREENSHOT_TIMER_ID = 1230; HWND hw, treeview; struct treeview_faff tvfaff; int ret; @@ -565,6 +568,21 @@ static INT_PTR CALLBACK GenericMainDlgProc(HWND hwnd, UINT msg, * spurious firing during the above setup procedure. */ SetWindowLongPtr(hwnd, GWLP_USERDATA, 1); + + if (dialog_box_demo_screenshot_filename) + SetTimer(hwnd, DEMO_SCREENSHOT_TIMER_ID, TICKSPERSEC, NULL); + return 0; + case WM_TIMER: + if (dialog_box_demo_screenshot_filename && + (UINT_PTR)wParam == DEMO_SCREENSHOT_TIMER_ID) { + KillTimer(hwnd, DEMO_SCREENSHOT_TIMER_ID); + const char *err = save_screenshot( + hwnd, dialog_box_demo_screenshot_filename); + if (err) + MessageBox(hwnd, err, "Demo screenshot failure", + MB_OK | MB_ICONERROR); + SaneEndDialog(hwnd, 0); + } return 0; case WM_LBUTTONUP: /* diff --git a/windows/platform.h b/windows/platform.h index 5264e8f0..eff5de61 100644 --- a/windows/platform.h +++ b/windows/platform.h @@ -757,4 +757,7 @@ bool aux_match_arg(AuxMatchOpt *amo, char **val); bool aux_match_opt(AuxMatchOpt *amo, char **val, const char *optname, ...); bool aux_match_done(AuxMatchOpt *amo); +char *save_screenshot(HWND hwnd, const char *outfile); +void gui_terminal_ready(HWND hwnd, Seat *seat, Backend *backend); + #endif /* PUTTY_WINDOWS_PLATFORM_H */ diff --git a/windows/pterm.c b/windows/pterm.c index 2cdef30c..0df849f9 100644 --- a/windows/pterm.c +++ b/windows/pterm.c @@ -45,3 +45,7 @@ const wchar_t *get_app_user_model_id(void) { return L"SimonTatham.Pterm"; } + +void gui_terminal_ready(HWND hwnd, Seat *seat, Backend *backend) +{ +} diff --git a/windows/putty.c b/windows/putty.c index b17ad7dc..83594d61 100644 --- a/windows/putty.c +++ b/windows/putty.c @@ -1,10 +1,16 @@ #include "putty.h" #include "storage.h" +extern bool sesslist_demo_mode; +extern const char *dialog_box_demo_screenshot_filename; +static strbuf *demo_terminal_data = NULL; +static const char *terminal_demo_screenshot_filename; + void gui_term_process_cmdline(Conf *conf, char *cmdline) { char *p; bool special_launchable_argument = false; + bool demo_config_box = false; settings_set_default_protocol(be_default_protocol); /* Find the appropriate default port. */ @@ -81,6 +87,29 @@ void gui_term_process_cmdline(Conf *conf, char *cmdline) } else if (!strcmp(p, "-pgpfp")) { pgp_fingerprints_msgbox(NULL); exit(1); + } else if (!strcmp(p, "-demo-config-box")) { + if (i+1 >= argc) { + cmdline_error("%s expects an output filename", p); + } else { + demo_config_box = true; + dialog_box_demo_screenshot_filename = argv[++i]; + } + } else if (!strcmp(p, "-demo-terminal")) { + if (i+2 >= argc) { + cmdline_error("%s expects input and output filenames", p); + } else { + const char *infile = argv[++i]; + terminal_demo_screenshot_filename = argv[++i]; + FILE *fp = fopen(infile, "rb"); + if (!fp) + cmdline_error("can't open input file '%s'", infile); + demo_terminal_data = strbuf_new(); + char buf[4096]; + int retd; + while ((retd = fread(buf, 1, sizeof(buf), fp)) > 0) + put_data(demo_terminal_data, buf, retd); + fclose(fp); + } } else if (*p != '-') { cmdline_error("unexpected argument \"%s\"", p); } else { @@ -91,13 +120,26 @@ void gui_term_process_cmdline(Conf *conf, char *cmdline) cmdline_run_saved(conf); - /* - * Bring up the config dialog if the command line hasn't - * (explicitly) specified a launchable configuration. - */ - if (!(special_launchable_argument || cmdline_host_ok(conf))) { - if (!do_config(conf)) - cleanup_exit(0); + if (demo_config_box) { + sesslist_demo_mode = true; + load_open_settings(NULL, conf); + conf_set_str(conf, CONF_host, "demo-server.example.com"); + do_config(conf); + cleanup_exit(0); + } else if (demo_terminal_data) { + /* Ensure conf will cause an immediate session launch */ + load_open_settings(NULL, conf); + conf_set_str(conf, CONF_host, "demo-server.example.com"); + conf_set_int(conf, CONF_close_on_exit, FORCE_OFF); + } else { + /* + * Bring up the config dialog if the command line hasn't + * (explicitly) specified a launchable configuration. + */ + if (!(special_launchable_argument || cmdline_host_ok(conf))) { + if (!do_config(conf)) + cleanup_exit(0); + } } prepare_session(conf); @@ -105,6 +147,10 @@ void gui_term_process_cmdline(Conf *conf, char *cmdline) const struct BackendVtable *backend_vt_from_conf(Conf *conf) { + if (demo_terminal_data) { + return &null_backend; + } + /* * Select protocol. This is farmed out into a table in a * separate file to enable an ssh-free variant. @@ -125,3 +171,19 @@ const wchar_t *get_app_user_model_id(void) { return L"SimonTatham.PuTTY"; } + +static void demo_terminal_screenshot(void *ctx, unsigned long now) +{ + HWND hwnd = (HWND)ctx; + save_screenshot(hwnd, terminal_demo_screenshot_filename); + cleanup_exit(0); +} + +void gui_terminal_ready(HWND hwnd, Seat *seat, Backend *backend) +{ + if (demo_terminal_data) { + ptrlen data = ptrlen_from_strbuf(demo_terminal_data); + seat_stdout(seat, data.ptr, data.len); + schedule_timer(TICKSPERSEC, demo_terminal_screenshot, (void *)hwnd); + } +} diff --git a/windows/puttygen.c b/windows/puttygen.c index 957169b7..281d17c0 100644 --- a/windows/puttygen.c +++ b/windows/puttygen.c @@ -27,6 +27,8 @@ #define DEFAULT_EDCURVE_INDEX 0 static char *cmdline_keyfile = NULL; +static ptrlen cmdline_demo_keystr; +static const char *demo_screenshot_filename = NULL; /* * Print a modal (Really Bad) message box and perform a fatal exit. @@ -1206,6 +1208,7 @@ static void start_generating_key(HWND hwnd, struct MainDlgState *state) static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + const int DEMO_SCREENSHOT_TIMER_ID = 1230; static const char entropy_msg[] = "Please generate some randomness by moving the mouse over the blank area."; struct MainDlgState *state; @@ -1429,9 +1432,30 @@ static INT_PTR CALLBACK MainDlgProc(HWND hwnd, UINT msg, Filename *fn = filename_from_str(cmdline_keyfile); load_key_file(hwnd, state, fn, false); filename_free(fn); + } else if (cmdline_demo_keystr.ptr) { + BinarySource src[1]; + BinarySource_BARE_INIT_PL(src, cmdline_demo_keystr); + const char *errmsg; + ssh2_userkey *k = ppk_load_s(src, NULL, &errmsg); + assert(!errmsg); + + update_ui_after_load(hwnd, state, "demo passphrase", + SSH_KEYTYPE_SSH2, NULL, k); + + SetTimer(hwnd, DEMO_SCREENSHOT_TIMER_ID, TICKSPERSEC, NULL); } return 1; + case WM_TIMER: + if ((UINT_PTR)wParam == DEMO_SCREENSHOT_TIMER_ID) { + KillTimer(hwnd, DEMO_SCREENSHOT_TIMER_ID); + const char *err = save_screenshot(hwnd, demo_screenshot_filename); + if (err) + MessageBox(hwnd, err, "Demo screenshot failure", + MB_OK | MB_ICONERROR); + EndDialog(hwnd, 0); + } + return 0; case WM_MOUSEMOVE: state = (struct MainDlgState *) GetWindowLongPtr(hwnd, GWLP_USERDATA); if (state->entropy && state->entropy_got < state->entropy_required) { @@ -2176,6 +2200,22 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) opt_error("unrecognised PPK parameter '%s'\n", val); } } + } else if (match_optval("-demo-screenshot")) { + demo_screenshot_filename = val; + cmdline_demo_keystr = PTRLEN_LITERAL( + "PuTTY-User-Key-File-3: ssh-ed25519\n" + "Encryption: none\n" + "Comment: ed25519-key-20220402\n" + "Public-Lines: 2\n" + "AAAAC3NzaC1lZDI1NTE5AAAAILzuIFwZ" + "8ZhgOlilcSb+9zPuCf/DmKJiloVlmWGy\n" + "xa/F\n" + "Private-Lines: 1\n" + "AAAAIPca6vLwtB2NJhZUpABQISR0gcQH8jjQLta19VyzA3wc\n" + "Private-MAC: 1159e9628259b35933b397379bbe8a14" + "a1f1d97fe91e446e45a9581a3408b70e\n"); + params->keybutton = IDC_KEYSSH2EDDSA; + argbits = 255; } else { opt_error("unrecognised option '%s'\n", amo.argv[amo.index]); } diff --git a/windows/utils/screenshot.c b/windows/utils/screenshot.c new file mode 100644 index 00000000..777520fd --- /dev/null +++ b/windows/utils/screenshot.c @@ -0,0 +1,126 @@ +#include "putty.h" + +#if HAVE_DWMAPI_H + +#include + +char *save_screenshot(HWND hwnd, const char *outfile) +{ + HDC dcWindow = NULL, dcSave = NULL; + HBITMAP bmSave = NULL; + uint8_t *buffer = NULL; + char *err = NULL; + + static HMODULE dwmapi_module; + DECL_WINDOWS_FUNCTION(static, HRESULT, DwmGetWindowAttribute, + (HWND, DWORD, PVOID, DWORD)); + + if (!dwmapi_module) { + dwmapi_module = load_system32_dll("dwmapi.dll"); + GET_WINDOWS_FUNCTION(dwmapi_module, DwmGetWindowAttribute); + } + + dcWindow = GetDC(NULL); + if (!dcWindow) { + err = dupprintf("GetDC(window): %s", win_strerror(GetLastError())); + goto out; + } + + int x, y, w, h; + RECT wr; + + /* Use DwmGetWindowAttribute in place of GetWindowRect to exclude + * drop shadow, otherwise we get a load of unwanted desktop + * background under the shadow */ + if (p_DwmGetWindowAttribute && + 0 <= p_DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, + &wr, sizeof(wr))) { + x = wr.left; + y = wr.top; + w = wr.right - wr.left; + h = wr.bottom - wr.top; + } else { + BITMAP bmhdr; + memset(&bmhdr, 0, sizeof(bmhdr)); + GetObject(GetCurrentObject(dcWindow, OBJ_BITMAP), + sizeof(bmhdr), &bmhdr); + x = y = 0; + w = bmhdr.bmWidth; + h = bmhdr.bmHeight; + } + + dcSave = CreateCompatibleDC(dcWindow); + if (!dcSave) { + err = dupprintf("CreateCompatibleDC(desktop window dc): %s", + win_strerror(GetLastError())); + goto out; + } + + bmSave = CreateCompatibleBitmap(dcWindow, w, h); + if (!bmSave) { + err = dupprintf("CreateCompatibleBitmap: %s", + win_strerror(GetLastError())); + goto out; + } + + if (!SelectObject(dcSave, bmSave)) { + err = dupprintf("SelectObject: %s", win_strerror(GetLastError())); + goto out; + } + + if (!BitBlt(dcSave, 0, 0, w, h, dcWindow, x, y, SRCCOPY)) { + err = dupprintf("BitBlt: %s", win_strerror(GetLastError())); + goto out; + } + + BITMAPINFO bmInfo; + memset(&bmInfo, 0, sizeof(bmInfo)); + bmInfo.bmiHeader.biSize = sizeof(bmInfo.bmiHeader); + bmInfo.bmiHeader.biWidth = w; + bmInfo.bmiHeader.biHeight = h; + bmInfo.bmiHeader.biPlanes = 1; + bmInfo.bmiHeader.biBitCount = 32; + bmInfo.bmiHeader.biCompression = BI_RGB; + + size_t bmPixels = (size_t)w*h, bmBytes = bmPixels * 4; + buffer = snewn(bmBytes, uint8_t); + + if (!GetDIBits(dcWindow, bmSave, 0, h, buffer, &bmInfo, DIB_RGB_COLORS)) + err = dupprintf("GetDIBits (get data): %s", + win_strerror(GetLastError())); + + FILE *fp = fopen(outfile, "wb"); + if (!fp) { + err = dupprintf("'%s': unable to open file", outfile); + goto out; + } + + BITMAPFILEHEADER bmFileHdr; + bmFileHdr.bfType = 'B' | ('M' << 8); + bmFileHdr.bfSize = sizeof(bmFileHdr) + sizeof(bmInfo.bmiHeader) + bmBytes; + bmFileHdr.bfOffBits = sizeof(bmFileHdr) + sizeof(bmInfo.bmiHeader); + fwrite((void *)&bmFileHdr, 1, sizeof(bmFileHdr), fp); + fwrite((void *)&bmInfo.bmiHeader, 1, sizeof(bmInfo.bmiHeader), fp); + fwrite((void *)buffer, 1, bmBytes, fp); + fclose(fp); + + out: + if (dcWindow) + ReleaseDC(NULL, dcWindow); + if (bmSave) + DeleteObject(bmSave); + if (dcSave) + DeleteObject(dcSave); + sfree(buffer); + return err; +} + +#else /* HAVE_DWMAPI_H */ + +/* Without we can't get the right window rectangle */ +char *save_screenshot(HWND hwnd, const char *outfile) +{ + return dupstr("Demo screenshots not compiled in to this build"); +} + +#endif /* HAVE_DWMAPI_H */ diff --git a/windows/window.c b/windows/window.c index cb9d5b1c..399829b6 100644 --- a/windows/window.c +++ b/windows/window.c @@ -816,6 +816,8 @@ int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) term_set_focus(term, GetForegroundWindow() == wgs.term_hwnd); UpdateWindow(wgs.term_hwnd); + gui_terminal_ready(wgs.term_hwnd, &wgs.seat, backend); + while (1) { int n; DWORD timeout;