From 11869cc8175f80927cd049297582971061bbe3ff Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 16:56:27 +0200 Subject: [PATCH 1/6] Windows embedded target: kiosk build, Shell Launcher deploy, hardware windows.embedded=true builds the native Windows exe as a kiosk for Windows 10/11 IoT Enterprise: a borderless topmost window on a chosen monitor, rotation/DPI/cursor defaults from board profiles (generic-x64, generic-x64-mouse, generic-arm64, imx8, qualcomm-iot) overridable through CN1_* variables, system shortcuts blocked by a low-level keyboard hook, the shared on-screen keyboard (EmbeddedTextEditor, moved to core from the Linux port) and stdout/stderr redirected to CN1_LOG_FILE. cn1:deploy-device recognises a PE and installs it over OpenSSH with a PowerShell script: Shell Launcher v2 as the shell (Explorer kept for Administrators), or a logon task, optional Keyboard Filter, machine environment variables. device-logs tails the log file. com.codename1.hardware on Windows: serial (COM ports), GPIO/I2C/SPI/PWM through Windows.Devices, backlight through DDC/CI with a WMI fallback. UWP builds stub the natives. Co-Authored-By: Claude Opus 5.5 --- .../codename1/impl/EmbeddedTextEditor.java | 23 +- .../impl/linux/LinuxImplementation.java | 2 +- Ports/WindowsPort/nativeSources/cn1_windows.h | 14 + .../nativeSources/cn1_windows_embedded.cpp | 256 ++++ .../nativeSources/cn1_windows_hardware.cpp | 1238 +++++++++++++++++ .../nativeSources/cn1_windows_window.cpp | 31 +- .../impl/windows/WindowsHardware.java | 195 +++ .../impl/windows/WindowsImplementation.java | 33 + .../codename1/impl/windows/WindowsNative.java | 30 + .../developer-guide/embedded-windows.sh | 17 + .../developer-guide/Embedded-Windows.asciidoc | 130 ++ .../Working-With-Windows.asciidoc | 4 + docs/developer-guide/developer-guide.asciidoc | 2 + .../builders/WindowsEmbeddedProfile.java | 86 ++ .../builders/WindowsNativeBuilder.java | 53 +- .../java/com/codename1/maven/AppBuilder.java | 11 + .../com/codename1/maven/DeviceDeployer.java | 285 +++- .../builders/WindowsEmbeddedTest.java | 79 ++ .../maven/WindowsDeviceDeployerTest.java | 157 +++ .../build/shared/BuildHintsDesktop.java | 54 + .../gradle/tasks/DeployDeviceTask.java | 7 +- .../gradle/tasks/DeviceLogsTask.java | 3 +- .../com/codename1/maven/DeployDeviceMojo.java | 24 +- .../tools/translator/ByteCodeTranslator.java | 14 +- 24 files changed, 2721 insertions(+), 27 deletions(-) rename Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java => CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java (92%) create mode 100644 Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp create mode 100644 Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp create mode 100644 Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java create mode 100644 docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh create mode 100644 docs/developer-guide/Embedded-Windows.asciidoc create mode 100644 maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java create mode 100644 maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java create mode 100644 maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java similarity index 92% rename from Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java rename to CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java index 1bdf0073f68..3d77abf24cf 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxEmbeddedEditor.java +++ b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java @@ -20,7 +20,7 @@ * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ -package com.codename1.impl.linux; +package com.codename1.impl; import com.codename1.ui.Button; import com.codename1.ui.Component; @@ -35,13 +35,14 @@ import com.codename1.ui.layouts.BoxLayout; import com.codename1.ui.layouts.GridLayout; -/// Text editing on the embedded Linux target, where there is no native text -/// widget and usually no physical keyboard. A field being edited opens a +/// Text editing on the embedded targets (Linux on DRM/fbdev, Windows in kiosk +/// mode), where there is no native text widget or no system touch keyboard to +/// rely on, and usually no physical keyboard. A field being edited opens a /// dialog holding a lightweight copy of it and an on-screen keyboard; a /// physical keyboard types into the same field, so a panel with one works the /// same way. Done delivers the text through `Display.onEditingComplete`, /// Cancel restores the original. -final class LinuxEmbeddedEditor { +public final class EmbeddedTextEditor { private static final String[][] LETTERS = { {"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, {"a", "s", "d", "f", "g", "h", "j", "k", "l", "'"}, @@ -53,10 +54,12 @@ final class LinuxEmbeddedEditor { {"!", "?", ";", "\"", "+", "=", "_", "<", ">", "~"} }; - private LinuxEmbeddedEditor() { + private EmbeddedTextEditor() { } - static void edit(final Component cmp, int maxSize, int constraint, final String text) { + /// Opens the editor for `cmp`; the arguments are those of + /// `CodenameOneImplementation.editString`. + public static void edit(final Component cmp, int maxSize, int constraint, final String text) { final boolean multiLine = cmp instanceof TextArea && !((TextArea) cmp).isSingleLineTextArea(); final TextField field = new TextField(text == null ? "" : text); field.setConstraint(constraint); @@ -78,10 +81,10 @@ static void edit(final Component cmp, int maxSize, int constraint, final String public void run() { keys.removeAll(); String[][] rows = symbols[0] ? SYMBOLS : LETTERS; - for (int r = 0; r < rows.length; r++) { - Container row = new Container(new GridLayout(1, rows[r].length)); - for (int k = 0; k < rows[r].length; k++) { - String label = rows[r][k]; + for (String[] keyRow : rows) { + Container row = new Container(new GridLayout(1, keyRow.length)); + for (String key : keyRow) { + String label = key; if (shift[0] && !symbols[0]) { label = upper(label); } diff --git a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java index 6872792c0b9..75266aec055 100644 --- a/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java +++ b/Ports/LinuxPort/src/com/codename1/impl/linux/LinuxImplementation.java @@ -2532,7 +2532,7 @@ public boolean isAsyncEditMode() { @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { if (Embedded.VALUE) { - LinuxEmbeddedEditor.edit(cmp, maxSize, constraint, text); + com.codename1.impl.EmbeddedTextEditor.edit(cmp, maxSize, constraint, text); return; } lowLevelTextInput.stopCurrent(); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows.h b/Ports/WindowsPort/nativeSources/cn1_windows.h index 828c9d1c815..55e34feb39a 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows.h +++ b/Ports/WindowsPort/nativeSources/cn1_windows.h @@ -355,6 +355,20 @@ extern CN1WindowsContext cn1Win; void cn1WindowsLog(const char* message); void cn1WinApplyPendingResize(void); int cn1WinCreateWindow(const char* utf8Title, int width, int height); + +/* The embedded (kiosk) target, cn1_windows_embedded.cpp. Each is a no-op in a + * build without CN1_WINDOWS_EMBEDDED. */ +int cn1WinEmbeddedActive(void); +/* Rotates the target monitor if asked, then answers the window style and the + * screen rectangle a kiosk window covers. */ +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc); +/* After the window is shown: topmost, the cursor policy, the keyboard filter. */ +void cn1WinEmbeddedWindowShown(HWND hwnd); +/* A forced density in DPI, or 0 for the monitor's own. */ +int cn1WinEmbeddedDpi(void); +/* Points stdout and stderr at CN1_LOG_FILE, which cn1:deploy-device sets: a + * shell has no console, and the device's log is what cn1:device-logs reads. */ +void cn1WinEmbeddedRedirectLog(void); void cn1WinPushEvent(CN1EventType type, int x, int y, int keyCode); /* Same, but tagged with the desktop window the event came from. */ void cn1WinPushWindowEvent(int windowId, CN1EventType type, int x, int y, int keyCode); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp new file mode 100644 index 00000000000..bbaf5aad7ee --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * The embedded (kiosk) target: `windows.embedded=true`, compiled with + * CN1_WINDOWS_EMBEDDED. The application is the only thing on the screen of a + * Windows IoT Enterprise device -- usually as its shell, through Shell + * Launcher, which `cn1:deploy-device` configures: + * + * - one borderless, topmost window covering a whole monitor, no title bar; + * - the monitor rotated when the panel is mounted that way; + * - the Windows key, Alt+Tab, Alt+Esc, Ctrl+Esc and Alt+F4 filtered while the + * application has the foreground (Ctrl+Alt+Del is the Secure Attention + * Sequence and only Keyboard Filter can suppress it; deploy-device turns that + * on); + * - the cursor hidden on a touch panel; + * - text edited through the shared on-screen keyboard (EmbeddedTextEditor). + * + * The board profile's defaults arrive as CN1_DEFAULT_* defines, and each can be + * overridden on the device by the CN1_* environment variable of the same name, + * the convention of the embedded Linux target (cn1_linux_embedded.c). + */ +#include "cn1_windows.h" + +#ifdef _WIN32 + +#include +#include + +#ifndef CN1_DEFAULT_ROTATION +#define CN1_DEFAULT_ROTATION 0 +#endif +#ifndef CN1_DEFAULT_DPI +#define CN1_DEFAULT_DPI 0 +#endif +#ifndef CN1_DEFAULT_MONITOR +#define CN1_DEFAULT_MONITOR 0 +#endif +/* 1 shows the cursor (a panel with a mouse), 0 hides it (touch only). */ +#ifndef CN1_DEFAULT_CURSOR +#define CN1_DEFAULT_CURSOR 0 +#endif + +extern "C" { + +JAVA_BOOLEAN com_codename1_impl_windows_WindowsNative_isEmbeddedDisplay___R_boolean(CODENAME_ONE_THREAD_STATE) { +#if defined(CN1_WINDOWS_EMBEDDED) && !defined(CN1_WINDOWS_UWP) + return JAVA_TRUE; +#else + return JAVA_FALSE; +#endif +} + +#if defined(CN1_WINDOWS_EMBEDDED) && !defined(CN1_WINDOWS_UWP) + +static int cn1EmbEnvInt(const char* name, int def) { + char buf[32]; + DWORD n = GetEnvironmentVariableA(name, buf, sizeof(buf)); + if (n == 0 || n >= sizeof(buf)) { + return def; + } + return atoi(buf); +} + +typedef struct { + int wanted; + int seen; + HMONITOR found; +} CN1EmbMonitorSearch; + +static BOOL CALLBACK cn1EmbMonitorProc(HMONITOR mon, HDC, LPRECT, LPARAM data) { + CN1EmbMonitorSearch* search = (CN1EmbMonitorSearch*) data; + if (search->seen == search->wanted) { + search->found = mon; + return FALSE; + } + search->seen++; + return TRUE; +} + +/* Monitor N in enumeration order; the primary when there is no such monitor. */ +static HMONITOR cn1EmbMonitor() { + CN1EmbMonitorSearch search; + search.wanted = cn1EmbEnvInt("CN1_MONITOR", CN1_DEFAULT_MONITOR); + search.seen = 0; + search.found = NULL; + if (search.wanted > 0) { + EnumDisplayMonitors(NULL, NULL, cn1EmbMonitorProc, (LPARAM) &search); + } + if (search.found == NULL) { + POINT origin = {0, 0}; + search.found = MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY); + } + return search.found; +} + +/* Rotates the monitor for this session. CDS_FULLSCREEN makes the change + * temporary: Windows restores the panel's own orientation when the process + * exits, so a crash never leaves the device sideways for whatever runs next. */ +static void cn1EmbRotate(HMONITOR mon, int degrees) { + DWORD orientation; + switch (degrees) { + case 90: orientation = DMDO_90; break; + case 180: orientation = DMDO_180; break; + case 270: orientation = DMDO_270; break; + default: return; + } + MONITORINFOEXW info; + ZeroMemory(&info, sizeof(info)); + info.cbSize = sizeof(info); + if (!GetMonitorInfoW(mon, (MONITORINFO*) &info)) { + return; + } + DEVMODEW dm; + ZeroMemory(&dm, sizeof(dm)); + dm.dmSize = sizeof(dm); + if (!EnumDisplaySettingsW(info.szDevice, ENUM_CURRENT_SETTINGS, &dm)) { + return; + } + if (dm.dmDisplayOrientation == orientation) { + return; + } + /* A quarter turn from where the panel is swaps its width and height. */ + if (((dm.dmDisplayOrientation + orientation) & 1) != 0) { + DWORD w = dm.dmPelsWidth; + dm.dmPelsWidth = dm.dmPelsHeight; + dm.dmPelsHeight = w; + } + dm.dmDisplayOrientation = orientation; + dm.dmFields = DM_DISPLAYORIENTATION | DM_PELSWIDTH | DM_PELSHEIGHT; + LONG r = ChangeDisplaySettingsExW(info.szDevice, &dm, NULL, CDS_FULLSCREEN, NULL); + if (r != DISP_CHANGE_SUCCESSFUL) { + cn1WindowsLog("embedded: the display driver refused the rotation"); + } +} + +static HWND cn1EmbWindow; +static HHOOK cn1EmbKeyboardHook; + +/* Swallows the shell's own shortcuts while the kiosk window has the + * foreground. A low-level hook sees them before the shell does, which is the + * only place the Windows key can be stopped from user mode. */ +static LRESULT CALLBACK cn1EmbKeyboardProc(int code, WPARAM wParam, LPARAM lParam) { + if (code == HC_ACTION && GetForegroundWindow() == cn1EmbWindow) { + const KBDLLHOOKSTRUCT* k = (const KBDLLHOOKSTRUCT*) lParam; + BOOL alt = (k->flags & LLKHF_ALTDOWN) != 0; + BOOL ctrl = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + if (k->vkCode == VK_LWIN || k->vkCode == VK_RWIN + || (alt && (k->vkCode == VK_TAB || k->vkCode == VK_ESCAPE || k->vkCode == VK_F4)) + || (ctrl && k->vkCode == VK_ESCAPE)) { + return 1; + } + } + return CallNextHookEx(cn1EmbKeyboardHook, code, wParam, lParam); +} + +int cn1WinEmbeddedActive(void) { + return 1; +} + +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc) { + HMONITOR mon = cn1EmbMonitor(); + cn1EmbRotate(mon, cn1EmbEnvInt("CN1_ROTATION", CN1_DEFAULT_ROTATION)); + MONITORINFO info; + ZeroMemory(&info, sizeof(info)); + info.cbSize = sizeof(info); + GetMonitorInfoW(mon, &info); + /* The whole monitor, not its work area: under Shell Launcher there is no + * taskbar, and under any other shell the window covers it. */ + *rc = info.rcMonitor; + *style = WS_POPUP | WS_CLIPCHILDREN; + *exStyle = WS_EX_TOPMOST; +} + +void cn1WinEmbeddedWindowShown(HWND hwnd) { + cn1EmbWindow = hwnd; + SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetForegroundWindow(hwnd); + if (cn1EmbEnvInt("CN1_CURSOR", CN1_DEFAULT_CURSOR) == 0) { + /* ShowCursor is a per-thread display counter; one decrement on the + * window's own thread hides it over the window. */ + ShowCursor(FALSE); + } + if (cn1EmbKeyboardHook == NULL) { + cn1EmbKeyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, cn1EmbKeyboardProc, GetModuleHandleW(NULL), 0); + if (cn1EmbKeyboardHook == NULL) { + cn1WindowsLog("embedded: keyboard filter not installed"); + } + } +} + +int cn1WinEmbeddedDpi(void) { + int dpi = cn1EmbEnvInt("CN1_DPI", CN1_DEFAULT_DPI); + return dpi > 0 ? dpi : 0; +} + +void cn1WinEmbeddedRedirectLog(void) { + char path[MAX_PATH]; + DWORD n = GetEnvironmentVariableA("CN1_LOG_FILE", path, sizeof(path)); + if (n == 0 || n >= sizeof(path)) { + return; + } + FILE* f = NULL; + freopen_s(&f, path, "a", stdout); + f = NULL; + freopen_s(&f, path, "a", stderr); + /* Line buffered, so cn1:device-logs follows the application as it runs + * rather than a buffer at a time. */ + setvbuf(stdout, NULL, _IOLBF, 1024); + setvbuf(stderr, NULL, _IONBF, 0); +} + +#else /* a desktop or UWP build */ + +int cn1WinEmbeddedActive(void) { + return 0; +} + +void cn1WinEmbeddedPlaceWindow(DWORD* style, DWORD* exStyle, RECT* rc) { +} + +void cn1WinEmbeddedWindowShown(HWND hwnd) { +} + +int cn1WinEmbeddedDpi(void) { + return 0; +} + +void cn1WinEmbeddedRedirectLog(void) { +} + +#endif + +} /* extern "C" */ + +#endif /* _WIN32 */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp new file mode 100644 index 00000000000..3a7d766cfa1 --- /dev/null +++ b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp @@ -0,0 +1,1238 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ + +/* + * com.codename1.hardware on Windows (WindowsHardware.java). + * + * The Java side is the Linux one (KernelHardware): every primitive answers a + * handle or a result >= 0, or a negative *Linux* errno, so the error mapping + * and the edge/timeout protocol are shared rather than reinvented here. + * + * - Serial: Win32 COM ports (CreateFileW + DCB), on every Windows. + * - GPIO, I2C, SPI, PWM: the Windows.Devices.* WinRT APIs. They answer on + * Windows IoT Enterprise boards whose BSP ships the GpioClx/SpbCx drivers + * (NXP i.MX 8M, Qualcomm IoT, and x64 boards with a vendor driver) and on + * nothing else; an ordinary PC reports no controller, which reaches Java as + * ENODEV. + * - Backlight: DDC/CI (dxva2) for an external monitor, else WMI's + * WmiMonitorBrightness for an integrated panel -- the usual panel PC case. + * - CAN and the watchdog have no Windows standard: both are vendor SDKs, so + * they answer ENOTSUP and WindowsHardware does not advertise them. + * + * A UWP build (CN1_WINDOWS_UWP) gets the ENOTSUP half only: the app container + * has neither the COM-port nor the WMI path, and an Xbox has no buses. + */ +#include "cn1_windows.h" + +#ifdef _WIN32 + +#include +#include + +/* Linux errno values: KernelHardware formats and compares these. */ +#define CN1HW_EIO 5 +#define CN1HW_EBADF 9 +#define CN1HW_EBUSY 16 +#define CN1HW_ENODEV 19 +#define CN1HW_EINVAL 22 +#define CN1HW_EMFILE 24 +#define CN1HW_ENOTSUP 95 +#define CN1HW_ETIMEDOUT 110 + +#if !defined(CN1_WINDOWS_UWP) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::Foundation::Collections; +using namespace ABI::Windows::Devices::Enumeration; +using namespace ABI::Windows::Devices::Gpio; +using namespace ABI::Windows::Devices::I2c; +using namespace ABI::Windows::Devices::Spi; +using namespace ABI::Windows::Devices::Pwm; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; + +/* ------------------------------------------------------------ handle table + * + * One slot per open device. Slots are claimed and released under one SRW lock; + * the device calls themselves run outside it, on a reference the caller holds + * (AddRef'd interface, or the duplicated serial HANDLE), so a close racing a + * transfer cannot free what the transfer is using. */ + +enum { HW_FREE = 0, HW_GPIO, HW_I2C, HW_SPI, HW_SERIAL, HW_PWM }; + +#define CN1HW_SLOTS 128 +#define CN1HW_EDGE_RING 32 + +typedef struct CN1WinHwSlot { + int kind; + IUnknown* device; /* IGpioPin / II2cDevice / ISpiDevice / IPwmPin */ + IUnknown* owner; /* IPwmController a PWM pin needs kept alive */ + HANDLE serial; + EventRegistrationToken edgeToken; + HANDLE edgeEvent; /* auto-reset; signalled per queued edge */ + int edgeHead; + int edgeCount; + int edgeRising[CN1HW_EDGE_RING]; + LONGLONG edgeNanos[CN1HW_EDGE_RING]; +} CN1WinHwSlot; + +static CN1WinHwSlot cn1WinHwSlots[CN1HW_SLOTS]; +static SRWLOCK cn1WinHwLock = SRWLOCK_INIT; + +static int cn1WinHwAlloc(int kind) { + int h = -CN1HW_EMFILE; + AcquireSRWLockExclusive(&cn1WinHwLock); + for (int i = 0; i < CN1HW_SLOTS; i++) { + if (cn1WinHwSlots[i].kind == HW_FREE) { + ZeroMemory(&cn1WinHwSlots[i], sizeof(CN1WinHwSlot)); + cn1WinHwSlots[i].kind = kind; + cn1WinHwSlots[i].serial = INVALID_HANDLE_VALUE; + h = i; + break; + } + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +/* A referenced copy of the slot's device interface, or NULL for a stale or + * wrong-kind handle. The caller releases it. */ +static IUnknown* cn1WinHwRef(int h, int kind) { + IUnknown* d = NULL; + if (h < 0 || h >= CN1HW_SLOTS) { + return NULL; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == kind && cn1WinHwSlots[h].device != NULL) { + d = cn1WinHwSlots[h].device; + d->AddRef(); + } + ReleaseSRWLockShared(&cn1WinHwLock); + return d; +} + +/* A duplicate of a serial slot's HANDLE (closed by the caller), so the port + * stays open for a read that is still running when another thread closes it. */ +static HANDLE cn1WinHwSerialRef(int h) { + HANDLE out = INVALID_HANDLE_VALUE; + if (h < 0 || h >= CN1HW_SLOTS) { + return out; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == HW_SERIAL) { + if (!DuplicateHandle(GetCurrentProcess(), cn1WinHwSlots[h].serial, GetCurrentProcess(), &out, + 0, FALSE, DUPLICATE_SAME_ACCESS)) { + out = INVALID_HANDLE_VALUE; + } + } + ReleaseSRWLockShared(&cn1WinHwLock); + return out; +} + +static void cn1WinHwInit() { + /* Refcounted by the OS; the worker threads re-enter it cheaply. The paired + * RoUninitialize is skipped for the reason cn1_windows_winrt.cpp gives. */ + RoInitialize(RO_INIT_MULTITHREADED); +} + +static int cn1WinHwFromHr(HRESULT hr) { + if (hr == HRESULT_FROM_WIN32(ERROR_ACCESS_DENIED) || hr == E_ACCESSDENIED + || hr == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)) { + return -CN1HW_EBUSY; + } + if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) + || hr == REGDB_E_CLASSNOTREG) { + return -CN1HW_ENODEV; + } + if (hr == E_INVALIDARG || hr == E_BOUNDS) { + return -CN1HW_EINVAL; + } + return -CN1HW_EIO; +} + +static int cn1WinHwFromWin32(DWORD err) { + switch (err) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return -CN1HW_ENODEV; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + return -CN1HW_EBUSY; + case ERROR_INVALID_PARAMETER: + return -CN1HW_EINVAL; + default: + return -CN1HW_EIO; + } +} + +/* Blocks the calling worker thread until a WinRT operation completes. The same + * helper as cn1_windows_winrt.cpp's, kept file-local as that one is. */ +template +static HRESULT cn1WinHwAwait(IAsyncOperation* op, TResultPtr result) { + Event done(CreateEventExW(nullptr, nullptr, 0, EVENT_ALL_ACCESS)); + if (!done.IsValid()) { + return E_FAIL; + } + HRESULT hr = op->put_Completed(Callback>( + [&done](IAsyncOperation*, AsyncStatus) { + SetEvent(done.Get()); + return S_OK; + }).Get()); + if (FAILED(hr)) { + return hr; + } + WaitForSingleObject(done.Get(), 30000); + return op->GetResults(result); +} + +/* Monotonic nanoseconds, the clock the Linux port's edge timestamps use. */ +static LONGLONG cn1WinHwNanos() { + static LARGE_INTEGER freq; + LARGE_INTEGER now; + if (freq.QuadPart == 0) { + QueryPerformanceFrequency(&freq); + } + QueryPerformanceCounter(&now); + return (LONGLONG) ((double) now.QuadPart * 1000000000.0 / (double) freq.QuadPart); +} + +/* The device ids an AQS selector matches, in the order Windows enumerates them: + * bus N in the Java API is the Nth. Answers the count and fills ids[] (each a + * new HSTRING the caller deletes) up to max. */ +static int cn1WinHwFindDevices(HSTRING selector, HSTRING* ids, int max) { + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Enumeration_DeviceInformation).Get(), + IID_PPV_ARGS(&statics)))) { + return 0; + } + ComPtr> op; + if (FAILED(statics->FindAllAsyncAqsFilter(selector, &op))) { + return 0; + } + ComPtr> list; + if (FAILED(cn1WinHwAwait(op.Get(), list.GetAddressOf())) || list == nullptr) { + return 0; + } + unsigned size = 0; + list->get_Size(&size); + for (unsigned i = 0; i < size && (int) i < max; i++) { + ComPtr info; + ids[i] = NULL; + if (SUCCEEDED(list->GetAt(i, &info))) { + info->get_Id(&ids[i]); + } + } + return (int) size; +} + +#define CN1HW_MAX_BUSES 16 + +/* The device id of bus `index` for a selector, or NULL. */ +static HSTRING cn1WinHwBusId(HSTRING selector, int index) { + HSTRING ids[CN1HW_MAX_BUSES]; + int n = cn1WinHwFindDevices(selector, ids, CN1HW_MAX_BUSES); + HSTRING out = NULL; + for (int i = 0; i < n && i < CN1HW_MAX_BUSES; i++) { + if (i == index) { + out = ids[i]; + } else if (ids[i] != NULL) { + WindowsDeleteString(ids[i]); + } + } + return out; +} + +extern "C" { + +/* ------------------------------------------------------------------ GPIO */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, + JAVA_INT bias, JAVA_INT edges) { + /* Windows exposes one GPIO controller to applications (GpioController's + * default, which a BSP may back with several banks numbered contiguously), + * so the chip name is not a selector here: WindowsHardware lists it as + * "gpio0", and any name opens it. */ + (void) chip; + if (line < 0) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Gpio_GpioController).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + ComPtr controller; + if (FAILED(statics->GetDefault(&controller)) || controller == nullptr) { + return -CN1HW_ENODEV; + } + ComPtr pin; + GpioOpenStatus status = GpioOpenStatus_PinUnavailable; + boolean opened = false; + HRESULT hr = controller->TryOpenPin(line, GpioSharingMode_Exclusive, &pin, &status, &opened); + if (FAILED(hr) || !opened || pin == nullptr) { + return status == GpioOpenStatus_SharingViolation ? -CN1HW_EBUSY : -CN1HW_ENODEV; + } + GpioPinDriveMode mode = GpioPinDriveMode_Input; + if (output) { + mode = GpioPinDriveMode_Output; + /* Latch the level first so the pin never glitches the other way when + * it turns into an output. */ + pin->Write(high ? GpioPinValue_High : GpioPinValue_Low); + } else if (bias == 2) { + mode = GpioPinDriveMode_InputPullUp; + } else if (bias == 3) { + mode = GpioPinDriveMode_InputPullDown; + } + boolean supported = false; + if (FAILED(pin->IsDriveModeSupported(mode, &supported)) || !supported) { + return -CN1HW_ENOTSUP; + } + if (FAILED(pin->SetDriveMode(mode))) { + return -CN1HW_EIO; + } + int h = cn1WinHwAlloc(HW_GPIO); + if (h < 0) { + return h; + } + if (!output && edges != 0) { + HANDLE ev = CreateEventW(NULL, FALSE, FALSE, NULL); + int slot = h; + EventRegistrationToken token; + hr = pin->add_ValueChanged(Callback>( + [slot, edges](IGpioPin*, IGpioPinValueChangedEventArgs* args) -> HRESULT { + GpioPinEdge edge = GpioPinEdge_FallingEdge; + args->get_Edge(&edge); + int rising = edge == GpioPinEdge_RisingEdge ? 1 : 0; + if ((rising && (edges & 1) == 0) || (!rising && (edges & 2) == 0)) { + return S_OK; + } + LONGLONG ns = cn1WinHwNanos(); + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot* s = &cn1WinHwSlots[slot]; + if (s->kind == HW_GPIO && s->edgeEvent != NULL) { + /* A full ring drops the oldest edge, as the kernel's + * line-event buffer does. */ + int tail = (s->edgeHead + s->edgeCount) % CN1HW_EDGE_RING; + if (s->edgeCount == CN1HW_EDGE_RING) { + s->edgeHead = (s->edgeHead + 1) % CN1HW_EDGE_RING; + } else { + s->edgeCount++; + } + s->edgeRising[tail] = rising; + s->edgeNanos[tail] = ns; + SetEvent(s->edgeEvent); + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + return S_OK; + }).Get(), &token); + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].edgeEvent = ev; + cn1WinHwSlots[h].edgeToken = token; + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (FAILED(hr)) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].kind = HW_FREE; + cn1WinHwSlots[h].edgeEvent = NULL; + ReleaseSRWLockExclusive(&cn1WinHwLock); + CloseHandle(ev); + return -CN1HW_ENOTSUP; + } + } + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = pin.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + IUnknown* d = cn1WinHwRef(h, HW_GPIO); + if (d == NULL) { + return -CN1HW_EBADF; + } + GpioPinValue v = GpioPinValue_Low; + HRESULT hr = ((IGpioPin*) d)->Read(&v); + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : (v == GpioPinValue_High ? 1 : 0); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN high) { + IUnknown* d = cn1WinHwRef(h, HW_GPIO); + if (d == NULL) { + return -CN1HW_EBADF; + } + HRESULT hr = ((IGpioPin*) d)->Write(high ? GpioPinValue_High : GpioPinValue_Low); + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + HANDLE ev = NULL; + if (h < 0 || h >= CN1HW_SLOTS) { + return -CN1HW_EBADF; + } + AcquireSRWLockShared(&cn1WinHwLock); + if (cn1WinHwSlots[h].kind == HW_GPIO && cn1WinHwSlots[h].edgeEvent != NULL) { + DuplicateHandle(GetCurrentProcess(), cn1WinHwSlots[h].edgeEvent, GetCurrentProcess(), &ev, 0, FALSE, + DUPLICATE_SAME_ACCESS); + } + ReleaseSRWLockShared(&cn1WinHwLock); + if (ev == NULL) { + return -CN1HW_EBADF; + } + int r = -CN1HW_ETIMEDOUT; + LONGLONG ns = 0; + DWORD deadline = GetTickCount() + (DWORD) (timeoutMs < 0 ? 0 : timeoutMs); + for (;;) { + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot* s = &cn1WinHwSlots[h]; + if (s->kind != HW_GPIO) { + r = -CN1HW_EBADF; + } else if (s->edgeCount > 0) { + r = s->edgeRising[s->edgeHead]; + ns = s->edgeNanos[s->edgeHead]; + s->edgeHead = (s->edgeHead + 1) % CN1HW_EDGE_RING; + s->edgeCount--; + } + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (r != -CN1HW_ETIMEDOUT) { + break; + } + DWORD wait = INFINITE; + if (timeoutMs >= 0) { + DWORD now = GetTickCount(); + if ((LONG) (deadline - now) <= 0) { + break; + } + wait = deadline - now; + } + CN1_YIELD_THREAD; + DWORD w = WaitForSingleObject(ev, wait); + CN1_RESUME_THREAD; + if (w != WAIT_OBJECT_0 && timeoutMs >= 0) { + break; + } + } + CloseHandle(ev); + if (r >= 0 && tsOut != JAVA_NULL && CN1_ARRAY_LENGTH(tsOut) > 0) { + ((JAVA_LONG*) CN1_ARRAY_DATA(tsOut))[0] = (JAVA_LONG) ns; + } + return r; +} + +/* ------------------------------------------------------------------- I2C */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, + JAVA_INT address) { + if (bus < 0 || address < 0 || address > 0x3ff) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cDevice).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), bus); + if (id == NULL) { + return -CN1HW_ENODEV; + } + ComPtr settingsFactory; + ComPtr settings; + HRESULT hr = RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cConnectionSettings).Get(), + IID_PPV_ARGS(&settingsFactory)); + if (SUCCEEDED(hr)) { + hr = settingsFactory->Create(address, &settings); + } + if (SUCCEEDED(hr) && address > 0x7f) { + hr = settings->put_AddressingMode(I2cAddressingMode_TenBit); + } + ComPtr> op; + if (SUCCEEDED(hr)) { + hr = statics->FromIdAsync(id, settings.Get(), &op); + } + WindowsDeleteString(id); + ComPtr device; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), device.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr)) { + return cn1WinHwFromHr(hr); + } + if (device == nullptr) { + /* FromIdAsync answers null for a bus another process holds. */ + return -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_I2C); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = device.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + if (wl < 0 || rl < 0 || (wl > 0 && (w == JAVA_NULL || wl > CN1_ARRAY_LENGTH(w))) + || (rl > 0 && (rd == JAVA_NULL || rl > CN1_ARRAY_LENGTH(rd))) || (wl == 0 && rl == 0)) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_I2C); + if (d == NULL) { + return -CN1HW_EBADF; + } + II2cDevice* dev = (II2cDevice*) d; + BYTE* wp = wl > 0 ? (BYTE*) CN1_ARRAY_DATA(w) : NULL; + BYTE* rp = rl > 0 ? (BYTE*) CN1_ARRAY_DATA(rd) : NULL; + HRESULT hr; + CN1_YIELD_THREAD; + if (wl > 0 && rl > 0) { + hr = dev->WriteRead((UINT32) wl, wp, (UINT32) rl, rp); + } else if (wl > 0) { + hr = dev->Write((UINT32) wl, wp); + } else { + hr = dev->Read((UINT32) rl, rp); + } + CN1_RESUME_THREAD; + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* ------------------------------------------------------------------- SPI */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + if (bus < 0 || cs < 0 || mode < 0 || mode > 3 || speed <= 0 || bits < 1 || bits > 32) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiDevice).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), bus); + if (id == NULL) { + return -CN1HW_ENODEV; + } + ComPtr settingsFactory; + ComPtr settings; + HRESULT hr = RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiConnectionSettings).Get(), + IID_PPV_ARGS(&settingsFactory)); + if (SUCCEEDED(hr)) { + hr = settingsFactory->Create(cs, &settings); + } + if (SUCCEEDED(hr)) { + settings->put_Mode((SpiMode) mode); + settings->put_ClockFrequency(speed); + settings->put_DataBitLength(bits); + } + ComPtr> op; + if (SUCCEEDED(hr)) { + hr = statics->FromIdAsync(id, settings.Get(), &op); + } + WindowsDeleteString(id); + ComPtr device; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), device.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr)) { + return cn1WinHwFromHr(hr); + } + if (device == nullptr) { + return -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_SPI); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = device.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + if (len <= 0 || (tx != JAVA_NULL && len > CN1_ARRAY_LENGTH(tx)) || (rx != JAVA_NULL && len > CN1_ARRAY_LENGTH(rx)) + || (tx == JAVA_NULL && rx == JAVA_NULL)) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_SPI); + if (d == NULL) { + return -CN1HW_EBADF; + } + ISpiDevice* dev = (ISpiDevice*) d; + HRESULT hr; + CN1_YIELD_THREAD; + if (tx != JAVA_NULL && rx != JAVA_NULL) { + hr = dev->TransferFullDuplex((UINT32) len, (BYTE*) CN1_ARRAY_DATA(tx), (UINT32) len, (BYTE*) CN1_ARRAY_DATA(rx)); + } else if (tx != JAVA_NULL) { + hr = dev->Write((UINT32) len, (BYTE*) CN1_ARRAY_DATA(tx)); + } else { + hr = dev->Read((UINT32) len, (BYTE*) CN1_ARRAY_DATA(rx)); + } + CN1_RESUME_THREAD; + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* ---------------------------------------------------------------- serial */ + +/* "COM3" or "\\.\COM3", and nothing else: the port opens a COM device, never a + * file or another DOS device an application names. Answers the number. */ +static int cn1WinHwComNumber(const char* name) { + if (name == NULL) { + return -1; + } + if (strncmp(name, "\\\\.\\", 4) == 0) { + name += 4; + } + if ((name[0] != 'C' && name[0] != 'c') || (name[1] != 'O' && name[1] != 'o') || (name[2] != 'M' && name[2] != 'm')) { + return -1; + } + int n = 0; + const char* p = name + 3; + if (*p == 0) { + return -1; + } + for (; *p; p++) { + if (*p < '0' || *p > '9' || n > 1000) { + return -1; + } + n = n * 10 + (*p - '0'); + } + return n > 0 ? n : -1; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialOpen___java_lang_String_int_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + int com = cn1WinHwComNumber(dev == JAVA_NULL ? NULL : stringToUTF8(threadStateData, dev)); + if (com < 0 || baud <= 0 || dataBits < 5 || dataBits > 8 || stopBits < 1 || stopBits > 2 || parity < 0 || parity > 2) { + return -CN1HW_EINVAL; + } + WCHAR path[32]; + wsprintfW(path, L"\\\\.\\COM%d", com); + HANDLE port = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); + if (port == INVALID_HANDLE_VALUE) { + return cn1WinHwFromWin32(GetLastError()); + } + DCB dcb; + ZeroMemory(&dcb, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(port, &dcb)) { + CloseHandle(port); + return -CN1HW_EIO; + } + /* Raw, like cfmakeraw on Linux: binary, no flow control, no character + * substitution. */ + dcb.BaudRate = (DWORD) baud; + dcb.ByteSize = (BYTE) dataBits; + dcb.Parity = parity == 1 ? ODDPARITY : parity == 2 ? EVENPARITY : NOPARITY; + dcb.StopBits = stopBits == 2 ? TWOSTOPBITS : ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fParity = parity != 0; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = DTR_CONTROL_ENABLE; + dcb.fDsrSensitivity = FALSE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fErrorChar = FALSE; + dcb.fNull = FALSE; + dcb.fRtsControl = RTS_CONTROL_ENABLE; + dcb.fAbortOnError = FALSE; + if (!SetCommState(port, &dcb)) { + CloseHandle(port); + return -CN1HW_EINVAL; + } + PurgeComm(port, PURGE_RXCLEAR | PURGE_TXCLEAR); + int h = cn1WinHwAlloc(HW_SERIAL); + if (h < 0) { + CloseHandle(port); + return h; + } + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].serial = port; + ReleaseSRWLockExclusive(&cn1WinHwLock); + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialRead___int_byte_1ARRAY_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -CN1HW_EINVAL; + } + HANDLE port = cn1WinHwSerialRef(h); + if (port == INVALID_HANDLE_VALUE) { + return -CN1HW_EBADF; + } + if (len == 0) { + CloseHandle(port); + return 0; + } + /* Return as soon as any byte is there, or after timeoutMs with nothing: the + * documented MAXDWORD/MAXDWORD/constant combination. A negative timeout + * waits indefinitely, which the constant cannot say, so it is a long one. */ + COMMTIMEOUTS t; + ZeroMemory(&t, sizeof(t)); + t.ReadIntervalTimeout = MAXDWORD; + t.ReadTotalTimeoutMultiplier = MAXDWORD; + t.ReadTotalTimeoutConstant = timeoutMs < 0 ? (MAXDWORD - 1) : (timeoutMs == 0 ? 1 : (DWORD) timeoutMs); + t.WriteTotalTimeoutConstant = 0; + SetCommTimeouts(port, &t); + DWORD n = 0; + CN1_YIELD_THREAD; + BOOL ok = ReadFile(port, (char*) CN1_ARRAY_DATA(buf) + off, (DWORD) len, &n, NULL); + CN1_RESUME_THREAD; + DWORD err = ok ? 0 : GetLastError(); + CloseHandle(port); + return ok ? (JAVA_INT) n : cn1WinHwFromWin32(err); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialWrite___int_byte_1ARRAY_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + if (buf == JAVA_NULL || off < 0 || len < 0 || off > CN1_ARRAY_LENGTH(buf) - len) { + return -CN1HW_EINVAL; + } + HANDLE port = cn1WinHwSerialRef(h); + if (port == INVALID_HANDLE_VALUE) { + return -CN1HW_EBADF; + } + int written = 0; + int r = 0; + CN1_YIELD_THREAD; + while (written < len) { + DWORD n = 0; + if (!WriteFile(port, (char*) CN1_ARRAY_DATA(buf) + off + written, (DWORD) (len - written), &n, NULL)) { + r = cn1WinHwFromWin32(GetLastError()); + break; + } + written += (int) n; + } + CN1_RESUME_THREAD; + CloseHandle(port); + return r < 0 ? r : written; +} + +/* ------------------------------------------------------------------- PWM */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, + JAVA_INT channel) { + if (chip < 0 || channel < 0) { + return -CN1HW_EINVAL; + } + cn1WinHwInit(); + ComPtr statics; + if (FAILED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Pwm_PwmController).Get(), + IID_PPV_ARGS(&statics)))) { + return -CN1HW_ENODEV; + } + HString selector; + if (FAILED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + return -CN1HW_ENODEV; + } + HSTRING id = cn1WinHwBusId(selector.Get(), chip); + if (id == NULL) { + return -CN1HW_ENODEV; + } + ComPtr> op; + HRESULT hr = statics->FromIdAsync(id, &op); + WindowsDeleteString(id); + ComPtr controller; + if (SUCCEEDED(hr)) { + CN1_YIELD_THREAD; + hr = cn1WinHwAwait(op.Get(), controller.GetAddressOf()); + CN1_RESUME_THREAD; + } + if (FAILED(hr) || controller == nullptr) { + return FAILED(hr) ? cn1WinHwFromHr(hr) : -CN1HW_EBUSY; + } + INT32 count = 0; + controller->get_PinCount(&count); + if (channel >= count) { + return -CN1HW_ENODEV; + } + ComPtr pin; + hr = controller->OpenPin(channel, &pin); + if (FAILED(hr) || pin == nullptr) { + return FAILED(hr) ? cn1WinHwFromHr(hr) : -CN1HW_EBUSY; + } + int h = cn1WinHwAlloc(HW_PWM); + if (h >= 0) { + AcquireSRWLockExclusive(&cn1WinHwLock); + cn1WinHwSlots[h].device = pin.Detach(); + cn1WinHwSlots[h].owner = controller.Detach(); + ReleaseSRWLockExclusive(&cn1WinHwLock); + } + return h; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + if (period <= 0 || duty < 0 || duty > period) { + return -CN1HW_EINVAL; + } + IUnknown* d = cn1WinHwRef(h, HW_PWM); + if (d == NULL) { + return -CN1HW_EBADF; + } + IPwmPin* pin = (IPwmPin*) d; + ComPtr controller; + HRESULT hr = pin->get_Controller(&controller); + if (SUCCEEDED(hr)) { + /* Windows sets the frequency per controller, not per channel: two + * channels of one controller share it, which the documentation of + * the Windows target states. */ + double actual = 0; + hr = controller->SetDesiredFrequency(1000000000.0 / (double) period, &actual); + } + if (SUCCEEDED(hr)) { + hr = pin->SetActiveDutyCyclePercentage((double) duty / (double) period); + } + if (SUCCEEDED(hr)) { + boolean started = false; + pin->get_IsStarted(&started); + if (enabled && !started) { + hr = pin->Start(); + } else if (!enabled && started) { + hr = pin->Stop(); + } + } + d->Release(); + return FAILED(hr) ? cn1WinHwFromHr(hr) : 0; +} + +/* -------------------------------------------------- CAN and the watchdog */ + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_OBJECT iface) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanSend___int_int_boolean_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT timeoutSeconds) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +/* ----------------------------------------------------------------- close */ + +JAVA_VOID com_codename1_impl_windows_WindowsNative_hwClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN disarm) { + (void) disarm; /* only a watchdog disarms, and there is none here */ + if (h < 0 || h >= CN1HW_SLOTS) { + return; + } + AcquireSRWLockExclusive(&cn1WinHwLock); + CN1WinHwSlot s = cn1WinHwSlots[h]; + cn1WinHwSlots[h].kind = HW_FREE; + cn1WinHwSlots[h].device = NULL; + cn1WinHwSlots[h].owner = NULL; + cn1WinHwSlots[h].serial = INVALID_HANDLE_VALUE; + cn1WinHwSlots[h].edgeEvent = NULL; + ReleaseSRWLockExclusive(&cn1WinHwLock); + if (s.kind == HW_FREE) { + return; + } + if (s.kind == HW_GPIO && s.device != NULL && s.edgeEvent != NULL) { + ((IGpioPin*) s.device)->remove_ValueChanged(s.edgeToken); + } + if (s.kind == HW_PWM && s.device != NULL) { + ((IPwmPin*) s.device)->Stop(); + } + if (s.device != NULL) { + /* Close releases the pin or bus for other processes now, not when the + * last reference a transfer may still hold goes away. */ + ComPtr closable; + if (SUCCEEDED(s.device->QueryInterface(IID_PPV_ARGS(&closable)))) { + closable->Close(); + } + s.device->Release(); + } + if (s.owner != NULL) { + s.owner->Release(); + } + if (s.serial != INVALID_HANDLE_VALUE) { + CloseHandle(s.serial); + } + if (s.edgeEvent != NULL) { + /* Wakes a waiter, which then finds the slot gone and answers EBADF. */ + SetEvent(s.edgeEvent); + CloseHandle(s.edgeEvent); + } +} + +/* ----------------------------------------------------------------- lists */ + +static void cn1WinHwAppend(char* out, size_t cap, const char* line) { + size_t used = strlen(out); + size_t n = strlen(line); + if (used + n + 2 < cap) { + if (used > 0) { + out[used++] = '\n'; + } + memcpy(out + used, line, n + 1); + } +} + +static void cn1WinHwListBuses(HSTRING selector, const char* prefix, char* out, size_t cap) { + HSTRING ids[CN1HW_MAX_BUSES]; + int n = cn1WinHwFindDevices(selector, ids, CN1HW_MAX_BUSES); + for (int i = 0; i < n && i < CN1HW_MAX_BUSES; i++) { + char line[32]; + wsprintfA(line, "%s%d", prefix, i); + cn1WinHwAppend(out, cap, line); + if (ids[i] != NULL) { + WindowsDeleteString(ids[i]); + } + } +} + +/* kind: 0 GPIO controllers, 1 I2C buses, 2 SPI buses, 3 serial ports, 4 CAN. + * Newline separated, in the shape WindowsHardware parses: "gpio0", "i2c-N", + * "spi-N", "COMn". */ +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_hwList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, + JAVA_INT kind) { + char out[4096]; + out[0] = 0; + cn1WinHwInit(); + if (kind == 0) { + ComPtr statics; + ComPtr controller; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Gpio_GpioController).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDefault(&controller)) && controller != nullptr) { + cn1WinHwAppend(out, sizeof(out), "gpio0"); + } + } else if (kind == 1) { + ComPtr statics; + HString selector; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cDevice).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + cn1WinHwListBuses(selector.Get(), "i2c-", out, sizeof(out)); + } + } else if (kind == 2) { + ComPtr statics; + HString selector; + if (SUCCEEDED(RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_Spi_SpiDevice).Get(), + IID_PPV_ARGS(&statics))) && SUCCEEDED(statics->GetDeviceSelector(selector.GetAddressOf()))) { + cn1WinHwListBuses(selector.Get(), "spi-", out, sizeof(out)); + } + } else if (kind == 3) { + /* Every DOS device name, filtered to COMn: no SetupAPI needed. */ + DWORD cap = 65536; + WCHAR* names = (WCHAR*) malloc(cap * sizeof(WCHAR)); + if (names != NULL && QueryDosDeviceW(NULL, names, cap) != 0) { + for (WCHAR* p = names; *p; p += wcslen(p) + 1) { + char narrow[64]; + if (WideCharToMultiByte(CP_UTF8, 0, p, -1, narrow, sizeof(narrow), NULL, NULL) > 0 + && cn1WinHwComNumber(narrow) > 0) { + cn1WinHwAppend(out, sizeof(out), narrow); + } + } + } + free(names); + } + return newStringFromCString(threadStateData, out); +} + +/* ------------------------------------------------------------- backlight + * + * op: 0 current, 1 maximum, 2 set. DDC/CI first (an external monitor that + * answers it), then WMI's WmiMonitorBrightness, which is what an integrated + * panel -- a panel PC, a tablet -- reports through. WMI's scale is 0..100. */ + +static int cn1WinHwDdc(int op, int value) { + HMONITOR mon = MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY); + DWORD count = 0; + if (!GetNumberOfPhysicalMonitorsFromHMONITOR(mon, &count) || count == 0) { + return -CN1HW_ENODEV; + } + PHYSICAL_MONITOR* monitors = (PHYSICAL_MONITOR*) calloc(count, sizeof(PHYSICAL_MONITOR)); + if (monitors == NULL || !GetPhysicalMonitorsFromHMONITOR(mon, count, monitors)) { + free(monitors); + return -CN1HW_ENODEV; + } + DWORD lo = 0, cur = 0, hi = 0; + int r = -CN1HW_ENODEV; + if (GetMonitorBrightness(monitors[0].hPhysicalMonitor, &lo, &cur, &hi)) { + if (op == 0) { + r = (int) cur; + } else if (op == 1) { + r = (int) hi; + } else { + DWORD v = value < (int) lo ? lo : (value > (int) hi ? hi : (DWORD) value); + r = SetMonitorBrightness(monitors[0].hPhysicalMonitor, v) ? 0 : -CN1HW_EIO; + } + } + DestroyPhysicalMonitors(count, monitors); + free(monitors); + return r; +} + +static int cn1WinHwWmi(int op, int value) { + if (op == 1) { + return 100; + } + CoInitializeEx(NULL, COINIT_MULTITHREADED); + ComPtr locator; + if (FAILED(CoCreateInstance(__uuidof(WbemLocator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&locator)))) { + return -CN1HW_ENODEV; + } + ComPtr services; + BSTR ns = SysAllocString(L"ROOT\\WMI"); + HRESULT hr = locator->ConnectServer(ns, NULL, NULL, NULL, 0, NULL, NULL, &services); + SysFreeString(ns); + if (FAILED(hr)) { + return -CN1HW_ENODEV; + } + CoSetProxyBlanket(services.Get(), RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, RPC_C_AUTHN_LEVEL_CALL, + RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); + BSTR lang = SysAllocString(L"WQL"); + BSTR query = SysAllocString(op == 0 ? L"SELECT CurrentBrightness FROM WmiMonitorBrightness" + : L"SELECT * FROM WmiMonitorBrightnessMethods"); + ComPtr results; + hr = services->ExecQuery(lang, query, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &results); + SysFreeString(lang); + SysFreeString(query); + if (FAILED(hr)) { + return -CN1HW_ENODEV; + } + ComPtr obj; + ULONG got = 0; + if (FAILED(results->Next(WBEM_INFINITE, 1, &obj, &got)) || got == 0) { + return -CN1HW_ENODEV; + } + VARIANT v; + VariantInit(&v); + if (op == 0) { + int r = -CN1HW_EIO; + if (SUCCEEDED(obj->Get(L"CurrentBrightness", 0, &v, NULL, NULL))) { + r = v.vt == VT_UI1 ? (int) v.bVal : (v.vt == VT_I4 ? (int) v.lVal : -CN1HW_EIO); + } + VariantClear(&v); + return r; + } + /* WmiSetBrightness(Timeout uint32, Brightness uint8) on the instance. */ + if (FAILED(obj->Get(L"__PATH", 0, &v, NULL, NULL)) || v.vt != VT_BSTR) { + VariantClear(&v); + return -CN1HW_EIO; + } + BSTR path = SysAllocString(v.bstrVal); + VariantClear(&v); + ComPtr cls; + ComPtr inDef; + ComPtr in; + BSTR clsName = SysAllocString(L"WmiMonitorBrightnessMethods"); + BSTR method = SysAllocString(L"WmiSetBrightness"); + hr = services->GetObject(clsName, 0, NULL, &cls, NULL); + if (SUCCEEDED(hr)) { + hr = cls->GetMethod(method, 0, &inDef, NULL); + } + if (SUCCEEDED(hr)) { + hr = inDef->SpawnInstance(0, &in); + } + if (SUCCEEDED(hr)) { + VARIANT a; + VariantInit(&a); + a.vt = VT_I4; + a.lVal = 0; + in->Put(L"Timeout", 0, &a, 0); + a.vt = VT_UI1; + a.bVal = (BYTE) (value < 0 ? 0 : (value > 100 ? 100 : value)); + in->Put(L"Brightness", 0, &a, 0); + hr = services->ExecMethod(path, method, 0, NULL, in.Get(), NULL, NULL); + } + SysFreeString(clsName); + SysFreeString(method); + SysFreeString(path); + return FAILED(hr) ? -CN1HW_EIO : 0; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, + JAVA_INT value) { + int r = cn1WinHwDdc(op, value); + if (r == -CN1HW_ENODEV) { + r = cn1WinHwWmi(op, value); + } + return r; +} + +} /* extern "C" */ + +#else /* CN1_WINDOWS_UWP: no COM ports, no WMI, no buses on an Xbox. */ + +extern "C" { + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_int_boolean_boolean_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT chip, JAVA_INT line, JAVA_BOOLEAN output, JAVA_BOOLEAN high, + JAVA_INT bias, JAVA_INT edges) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioRead___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWrite___int_boolean_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN high) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT bus, + JAVA_INT address) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cTransfer___int_byte_1ARRAY_int_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT w, JAVA_INT wl, JAVA_OBJECT rd, JAVA_INT rl) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiOpen___int_int_int_int_int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT bus, JAVA_INT cs, JAVA_INT mode, JAVA_INT speed, JAVA_INT bits) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSpiTransfer___int_byte_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT tx, JAVA_OBJECT rx, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialOpen___java_lang_String_int_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_OBJECT dev, JAVA_INT baud, JAVA_INT dataBits, JAVA_INT parity, JAVA_INT stopBits) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialRead___int_byte_1ARRAY_int_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialWrite___int_byte_1ARRAY_int_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT buf, JAVA_INT off, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmOpen___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT chip, + JAVA_INT channel) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwPwmConfigure___int_long_long_boolean_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT h, JAVA_LONG period, JAVA_LONG duty, JAVA_BOOLEAN enabled) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanOpen___java_lang_String_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_OBJECT iface) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanSend___int_int_boolean_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT id, JAVA_BOOLEAN ext, JAVA_OBJECT data, JAVA_INT len) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwCanReceive___int_int_1ARRAY_byte_1ARRAY_int_R_int( + CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_OBJECT idOut, JAVA_OBJECT data, JAVA_INT timeoutMs) { + return -CN1HW_EBADF; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogOpen___int_R_int(CODENAME_ONE_THREAD_STATE, + JAVA_INT timeoutSeconds) { + return -CN1HW_ENOTSUP; +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwWatchdogKick___int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h) { + return -CN1HW_EBADF; +} + +JAVA_VOID com_codename1_impl_windows_WindowsNative_hwClose___int_boolean(CODENAME_ONE_THREAD_STATE, JAVA_INT h, + JAVA_BOOLEAN disarm) { +} + +JAVA_OBJECT com_codename1_impl_windows_WindowsNative_hwList___int_R_java_lang_String(CODENAME_ONE_THREAD_STATE, + JAVA_INT kind) { + return newStringFromCString(threadStateData, ""); +} + +JAVA_INT com_codename1_impl_windows_WindowsNative_hwBacklight___int_int_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT op, + JAVA_INT value) { + return -CN1HW_ENODEV; +} + +} /* extern "C" */ + +#endif /* CN1_WINDOWS_UWP */ + +#endif /* _WIN32 */ diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp index 53cc731b0a5..0e5473843dc 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_window.cpp @@ -972,8 +972,22 @@ int cn1WinCreateWindow(const char* utf8Title, int width, int height) { /* WS_CLIPCHILDREN so the Direct2D present does not paint over native child * controls overlaid on the form (the WebView2 browser peer and the EDIT * control used for native text editing). */ - cn1Win.hwnd = CreateWindowExW(0, L"CodenameOneWindow", wTitle, - WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, width, height, + DWORD style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + DWORD exStyle = 0; + int x = CW_USEDEFAULT; + int y = CW_USEDEFAULT; + /* The kiosk target covers a whole monitor with a borderless window + * (cn1_windows_embedded.cpp); the requested size is the desktop's. */ + if (cn1WinEmbeddedActive() && !cn1Win.offscreenCapture) { + RECT rc; + cn1WinEmbeddedPlaceWindow(&style, &exStyle, &rc); + x = rc.left; + y = rc.top; + width = rc.right - rc.left; + height = rc.bottom - rc.top; + } + cn1Win.hwnd = CreateWindowExW(exStyle, L"CodenameOneWindow", wTitle, + style, x, y, width, height, NULL, NULL, hInstance, NULL); free(wTitle); return cn1Win.hwnd != NULL ? 1 : 0; @@ -1120,6 +1134,10 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin } + if (cn1WinEmbeddedActive()) { + cn1WinEmbeddedRedirectLog(); + } + /* Headless screenshot mode: no window; render into an offscreen WIC bitmap * that headlessTick later encodes to PNG. The EDT paints through the same * getWindowGraphics + flushGraphics path as on-screen. */ @@ -1205,6 +1223,9 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_initDisplay___java_lang_Strin if (!cn1Win.offscreenCapture) { ShowWindow(cn1Win.hwnd, SW_SHOW); UpdateWindow(cn1Win.hwnd); + if (cn1WinEmbeddedActive()) { + cn1WinEmbeddedWindowShown(cn1Win.hwnd); + } cn1WindowsLog("initDisplay: window shown"); } else { cn1WindowsLog("initDisplay: offscreen capture -- window kept hidden"); @@ -1219,6 +1240,12 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_getDisplayWidth___R_int(CODENA * mm-based theme metrics correctly instead of using the mobile density * heuristic, which over-scales everything. */ JAVA_INT com_codename1_impl_windows_WindowsNative_screenDpi___R_int(CODENAME_ONE_THREAD_STATE) { + /* A kiosk panel that misreports its size gets its density from the board + * profile or CN1_DPI instead (cn1_windows_embedded.cpp). */ + int forced = cn1WinEmbeddedDpi(); + if (forced > 0) { + return (JAVA_INT) forced; + } // LOGPIXELSX reports system DPI even for a per-monitor-aware window. typedef UINT (WINAPI *GetDpiFn)(HWND); GetDpiFn getDpi = (GetDpiFn)GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"); diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java new file mode 100644 index 00000000000..f0f5466ef12 --- /dev/null +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsHardware.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.impl.windows; + +import com.codename1.impl.KernelHardware; + +/// The Windows port's [com.codename1.hardware.Hardware]. It reuses +/// [KernelHardware]'s error and edge protocol -- the natives in +/// `cn1_windows_hardware.cpp` answer negative Linux errno values -- and +/// replaces the parts that name Linux device nodes: +/// +/// - Serial ports are `COM1`, `COM2`, ... on every Windows. +/// - GPIO, I2C, SPI and PWM go through `Windows.Devices.*`, which answer on a +/// Windows IoT Enterprise board whose BSP ships the bus drivers; an ordinary +/// PC lists none. There is one GPIO controller, listed as `gpio0`. +/// - The backlight is DDC/CI or the integrated panel's WMI brightness, on a +/// 0..100 scale for the latter. +/// - CAN and the watchdog are vendor SDKs on Windows and are not offered. +class WindowsHardware extends KernelHardware { + + @Override + protected int getSupportedFeatures() { + return FEATURE_GPIO | FEATURE_I2C | FEATURE_SPI | FEATURE_SERIAL | FEATURE_PWM | FEATURE_BACKLIGHT; + } + + /// Listeners run on the event dispatch thread. + @Override + protected void deliver(Runnable callback) { + com.codename1.ui.Display.getInstance().callSerially(callback); + } + + @Override + public String[] listSerialPorts() { + return lines(rawList(3)); + } + + @Override + public String[] listSpiDevices() { + // "spi-N" per bus; the chip select is chosen when a device is opened. + return lines(rawList(2)); + } + + @Override + public String[] listCanInterfaces() { + return new String[0]; + } + + private static String[] lines(String s) { + if (s == null || s.length() == 0) { + return new String[0]; + } + java.util.ArrayList out = new java.util.ArrayList(); + int start = 0; + int len = s.length(); + for (int i = 0; i <= len; i++) { + if (i == len || s.charAt(i) == '\n') { + if (i > start) { + out.add(s.substring(start, i)); + } + start = i + 1; + } + } + return out.toArray(new String[out.size()]); + } + + @Override + protected String rawList(int kind) { + return WindowsNative.hwList(kind); + } + + @Override + protected int rawBacklight(int op, int value) { + return WindowsNative.hwBacklight(op, value); + } + + @Override + protected int rawGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges) { + return WindowsNative.hwGpioOpen(chip, line, output, high, bias, edges); + } + + @Override + protected int rawGpioRead(int handle) { + return WindowsNative.hwGpioRead(handle); + } + + @Override + protected int rawGpioWrite(int handle, boolean high) { + return WindowsNative.hwGpioWrite(handle, high); + } + + @Override + protected int rawGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut) { + return WindowsNative.hwGpioWaitEdge(handle, timeoutMs, timestampOut); + } + + @Override + protected int rawI2cOpen(int bus, int address) { + return WindowsNative.hwI2cOpen(bus, address); + } + + @Override + protected int rawI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen) { + return WindowsNative.hwI2cTransfer(handle, write, writeLen, read, readLen); + } + + @Override + protected int rawSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord) { + return WindowsNative.hwSpiOpen(bus, chipSelect, mode, speedHz, bitsPerWord); + } + + @Override + protected int rawSpiTransfer(int handle, byte[] tx, byte[] rx, int len) { + return WindowsNative.hwSpiTransfer(handle, tx, rx, len); + } + + @Override + protected int rawSerialOpen(String device, int baud, int dataBits, int parity, int stopBits) { + // C reads the name up to its first NUL; a name holding one would open + // a device other than the one named. -22 is EINVAL. + if (device == null || device.indexOf('\0') >= 0) { + return -22; + } + return WindowsNative.hwSerialOpen(device, baud, dataBits, parity, stopBits); + } + + @Override + protected int rawSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs) { + return WindowsNative.hwSerialRead(handle, buf, off, len, timeoutMs); + } + + @Override + protected int rawSerialWrite(int handle, byte[] buf, int off, int len) { + return WindowsNative.hwSerialWrite(handle, buf, off, len); + } + + @Override + protected int rawPwmOpen(int chip, int channel) { + return WindowsNative.hwPwmOpen(chip, channel); + } + + @Override + protected int rawPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled) { + return WindowsNative.hwPwmConfigure(handle, periodNanos, dutyNanos, enabled); + } + + @Override + protected int rawCanOpen(String iface) { + return WindowsNative.hwCanOpen(iface); + } + + @Override + protected int rawCanSend(int handle, int id, boolean extended, byte[] data, int len) { + return WindowsNative.hwCanSend(handle, id, extended, data, len); + } + + @Override + protected int rawCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs) { + return WindowsNative.hwCanReceive(handle, idOut, data, timeoutMs); + } + + @Override + protected int rawWatchdogOpen(int timeoutSeconds) { + return WindowsNative.hwWatchdogOpen(timeoutSeconds); + } + + @Override + protected int rawWatchdogKick(int handle) { + return WindowsNative.hwWatchdogKick(handle); + } + + @Override + protected void rawClose(int handle, boolean disarm) { + WindowsNative.hwClose(handle, disarm); + } +} diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java index 9c08f5c0d13..30f19edbf99 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsImplementation.java @@ -2245,6 +2245,13 @@ public boolean isAsyncEditMode() { */ @Override public void editString(final Component cmp, int maxSize, int constraint, String text, int initiatingKeycode) { + // The kiosk target edits through the shared on-screen keyboard: a panel + // usually has no physical keyboard, and the system touch keyboard is not + // dependable once Shell Launcher has replaced explorer. + if (Embedded.VALUE) { + com.codename1.impl.EmbeddedTextEditor.edit(cmp, maxSize, constraint, text); + return; + } // Tapping straight from one field into another: finish the first cleanly // before overlaying the new one, so two controls never float at once. if (editPeer != 0) { @@ -4083,4 +4090,30 @@ public String getDatabasePath(String databaseName) { } return resolveDatabasePath(databaseName); } + + /// True on the embedded (kiosk) target. Fixed at build time; the port's JVM + /// unit tests run with no native library, so a missing native reads as "not + /// embedded". Caught as LinkageError: the device class library has no + /// UnsatisfiedLinkError. + private static final class Embedded { + static final boolean VALUE = query(); + + private static boolean query() { + try { + return WindowsNative.isEmbeddedDisplay(); + } catch (LinkageError notLinked) { + return false; + } + } + } + + private WindowsHardware hardware; + + @Override + public com.codename1.hardware.Hardware getHardware() { + if (hardware == null) { + hardware = new WindowsHardware(); + } + return hardware; + } } diff --git a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java index 8bda5d7f460..625db141379 100644 --- a/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java +++ b/Ports/WindowsPort/src/com/codename1/impl/windows/WindowsNative.java @@ -1187,4 +1187,34 @@ public static native void gl3dDrawArrays(long contextPeer, long pipelinePeer, lo public static native byte[] sqlColBlob(long stmtPeer, int col) throws java.io.IOException; public static native double sqlColDouble(long stmtPeer, int col) throws java.io.IOException; public static native long sqlColLong(long stmtPeer, int col) throws java.io.IOException; + + /// True when the exe was built for the embedded (kiosk) target + /// (`windows.embedded=true`, cn1_windows_embedded.cpp): a constant of the + /// build, never a runtime switch. + public static native boolean isEmbeddedDisplay(); + + // ---- com.codename1.hardware (cn1_windows_hardware.cpp). Each returns a value + // >= 0 or a negative Linux errno, the contract KernelHardware reads; + // -ETIMEDOUT reports a timeout. + public static native int hwGpioOpen(String chip, int line, boolean output, boolean high, int bias, int edges); + public static native int hwGpioRead(int handle); + public static native int hwGpioWrite(int handle, boolean high); + public static native int hwGpioWaitEdge(int handle, int timeoutMs, long[] timestampOut); + public static native int hwI2cOpen(int bus, int address); + public static native int hwI2cTransfer(int handle, byte[] write, int writeLen, byte[] read, int readLen); + public static native int hwSpiOpen(int bus, int chipSelect, int mode, int speedHz, int bitsPerWord); + public static native int hwSpiTransfer(int handle, byte[] tx, byte[] rx, int len); + public static native int hwSerialOpen(String device, int baud, int dataBits, int parity, int stopBits); + public static native int hwSerialRead(int handle, byte[] buf, int off, int len, int timeoutMs); + public static native int hwSerialWrite(int handle, byte[] buf, int off, int len); + public static native int hwPwmOpen(int chip, int channel); + public static native int hwPwmConfigure(int handle, long periodNanos, long dutyNanos, boolean enabled); + public static native int hwCanOpen(String iface); + public static native int hwCanSend(int handle, int id, boolean extended, byte[] data, int len); + public static native int hwCanReceive(int handle, int[] idOut, byte[] data, int timeoutMs); + public static native int hwWatchdogOpen(int timeoutSeconds); + public static native int hwWatchdogKick(int handle); + public static native void hwClose(int handle, boolean disarm); + public static native String hwList(int kind); + public static native int hwBacklight(int op, int value); } diff --git a/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh b/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh new file mode 100644 index 00000000000..bb6b79a5046 --- /dev/null +++ b/docs/demos/common/src/main/snippets/developer-guide/embedded-windows.sh @@ -0,0 +1,17 @@ +// Generated from docs/developer-guide/Embedded-Windows.asciidoc source blocks. Edit the guide snippets here, not inline. + +// tag::embedded-windows-properties-001[] +codename1.arg.windows.embedded=true +codename1.arg.windows.embedded.board=imx8 +codename1.arg.windows.embedded.rotation=90 +// end::embedded-windows-properties-001[] + +// tag::embedded-windows-bash-001[] +mvn cn1:deploy-device -Dcn1.device.host=kiosk.local -Dcn1.device.user=Administrator +gradle cn1DeployDevice -Pcn1.device.host=kiosk.local -Pcn1.device.user=Administrator +// end::embedded-windows-bash-001[] + +// tag::embedded-windows-bash-002[] +mvn cn1:deploy-device -Dcn1.device.host=kiosk.local -Dcn1.device.user=Administrator \ + -Dcn1.device.kioskUser=kiosk -Dcn1.device.keyboardFilter=true +// end::embedded-windows-bash-002[] diff --git a/docs/developer-guide/Embedded-Windows.asciidoc b/docs/developer-guide/Embedded-Windows.asciidoc new file mode 100644 index 00000000000..262211f8b25 --- /dev/null +++ b/docs/developer-guide/Embedded-Windows.asciidoc @@ -0,0 +1,130 @@ +== Embedded Windows devices + +Panel PCs, point-of-sale terminals, self-service kiosks and arm64 IoT boards +running *Windows 10/11 IoT Enterprise* run Codename One as the same native `.exe` +the <<_working_with_the_native_windows_port,native Windows port>> builds. No JVM +and no runtime installer go onto the device. Windows still owns the display, the +input and the boot, so the embedded build doesn't change how the application draws. +It changes what surrounds it: the window fills a screen and keeps it, the shell is +the application, and the boards' buses are reachable from Java. + +Windows 10 IoT Core isn't a target. It runs UWP applications only and is no +longer serviced. + +=== Building for a device + +Two build hints turn a native Windows build into an embedded one: + +[source,properties] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-properties-001,indent=0] +---- + +.Embedded Windows build hints +|=== +|Name |Description + +|windows.embedded +|`true`/`false` (default `false`). Builds the kiosk variant described here. Can't +be combined with `windows.xbox`. + +|windows.embedded.board +|The device profile: `generic-x64` (default, a touch panel PC), `generic-x64-mouse`, +`generic-arm64`, `imx8` or `qualcomm-iot`. The profile sets the defaults below and +the CPU architecture, so an arm64 board needs no `windows.arch`. + +|windows.embedded.rotation +|`0`, `90`, `180` or `270`. The display is rotated when the application starts. + +|windows.embedded.dpi +|The density the UI is laid out for. `0` (default) asks Windows. + +|windows.embedded.monitor +|Which monitor the application takes, counting from `0`. + +|windows.embedded.cursor +|`true` shows the mouse pointer. Touch profiles hide it. +|=== + +Each value can be changed on the device without a rebuild, through the matching +environment variable: `CN1_ROTATION`, `CN1_DPI`, `CN1_MONITOR` and `CN1_CURSOR` +(`1` shows the pointer, `0` hides it). + +=== What the embedded build does + +* The window has no frame, covers the chosen monitor and stays above every other + window. +* The Windows key, Alt+Tab, Alt+Esc, Alt+F4 and Ctrl+Esc never reach the system + while the application has the screen. Ctrl+Alt+Del is handled by Windows before + any application sees it; the Keyboard Filter below blocks it. +* Text is entered through Codename One's own on-screen keyboard, the one the + <<_embedded_linux_devices,embedded Linux>> build uses, rather than the Windows + touch keyboard, which is unreliable when the application is the shell. +* Standard output and standard error go to the file `CN1_LOG_FILE` names, so a + device with no console still keeps a log. + +=== Deploying to a device + +`deploy-device` installs the application over SSH. Windows ships OpenSSH Server as +an optional feature; enable it on the device and log in once as an administrator +so its key is accepted: + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-bash-001,indent=0] +---- + +The goal recognises a Windows executable, checks that its architecture matches +the device and copies it to `C:\ProgramData\CodenameOne`. It then makes the +application the device's shell with *Shell Launcher v2*: it enables the feature +if it's missing, restarts the application whenever it exits, and keeps Explorer +as the shell for administrators, so the device can still be serviced. Enabling +the feature takes a restart, which the goal performs. + +Options, as `cn1.device.*` properties: + +* `kioskUser` limits the kiosk shell to one account, `kiosk` for instance; other + users keep Explorer. +* `keyboardFilter=true` enables the Keyboard Filter and blocks Ctrl+Alt+Del, + Ctrl+Shift+Esc, Win+L and the Windows key. +* `kioskMode=autostart` replaces no shell and starts the application at logon + instead. Use it on a development device, or on an edition without Shell + Launcher. +* `env` sets machine environment variables, `CN1_ROTATION=90` for instance. Windows + reads them at logon, so they apply from the next one. + +[source,bash] +---- +include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-bash-002,indent=0] +---- + +`cn1:device-logs` follows the application's log file over the same connection. +As on Linux, the application runs with `CN1_PROFILE=device`, so a backend module's +`@Profile("device")` beans are active on the device only. + +Assigned Access, the single-app kiosk of the Pro edition, isn't offered: it runs +only packaged applications, under a restricted account that can't reach the +hardware below. + +=== Hardware access + +`com.codename1.hardware` works on Windows through the device APIs Windows provides: + +|=== +|API |On Windows + +|Serial ports +|Every `COM` port. + +|GPIO, I2C, SPI, PWM +|The `Windows.Devices` APIs. They need a board whose vendor ships the bus drivers, +which IoT boards do and most x64 panel PCs don't. Without one, the feature reports +that it isn't available. I2C and SPI bus `N` is the board's `N`th controller. + +|Backlight +|The monitor's brightness over DDC/CI, or the built-in panel's through WMI. + +|CAN, watchdog +|Not available: Windows has no standard interface for either. Shell Launcher's +restart takes the place of a supervisor. +|=== diff --git a/docs/developer-guide/Working-With-Windows.asciidoc b/docs/developer-guide/Working-With-Windows.asciidoc index 10dcc8e3436..8d47f6573b9 100644 --- a/docs/developer-guide/Working-With-Windows.asciidoc +++ b/docs/developer-guide/Working-With-Windows.asciidoc @@ -113,6 +113,10 @@ and folding identical functions (`/OPT:ICF`). Set `true` to keep debug symbols ( `.pdb` next to the exe) so a native crash address can be symbolized while you are developing. Optimizations stay on in either case. +|windows.embedded +|`true`/`false` (default `false`). Builds a kiosk `.exe` for a Windows IoT Enterprise +device. See <<_embedded_windows_devices>>. + |windows.xbox |Experimental. `true`/`false` (default `false`). Builds a UWP app for an Xbox in Developer Mode instead of a desktop `.exe`, packaged as an `.msix`. See diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 15b2cfed12d..52c8722c22b 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -304,6 +304,8 @@ include::Working-With-Linux.asciidoc[] include::Embedded-Linux.asciidoc[] +include::Embedded-Windows.asciidoc[] + include::Realtime.asciidoc[] include::Desktop-Integration.asciidoc[] diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java new file mode 100644 index 00000000000..3203f94c66e --- /dev/null +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import java.util.LinkedHashMap; +import java.util.Map; + +/// What the embedded Windows (kiosk) target needs to know about a device, +/// keyed by the `windows.embedded.board` build hint. Like the Linux +/// [EmbeddedBoardProfile] a profile only sets defaults: each value has its own +/// hint, and at run time the matching `CN1_*` environment variable overrides +/// it (see `cn1_windows_embedded.cpp`). +/// +/// Windows owns the display driver, so unlike Linux there is no display +/// backend or renderer to choose: the profile is the architecture, the panel +/// density for panels that misreport it, and whether a cursor is shown. +final class WindowsEmbeddedProfile { + final String name; + final String arch; + final int rotation; + final int dpi; + /// True for devices normally driven by a mouse; touch panels hide it. + final boolean cursor; + final String description; + + private static final Map PROFILES = + new LinkedHashMap(); + + static { + add("generic-x64", WindowsNativeBuilder.ARCH_X64, 0, 0, false, + "x86-64 panel PC or kiosk on Windows IoT Enterprise (touch)"); + add("generic-x64-mouse", WindowsNativeBuilder.ARCH_X64, 0, 0, true, + "x86-64 kiosk driven by a mouse or trackball"); + add("generic-arm64", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "Any 64-bit ARM board on Windows IoT Enterprise"); + add("imx8", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "NXP i.MX 8M family with NXP's Windows IoT Enterprise BSP (GPIO, I2C, SPI, PWM)"); + add("qualcomm-iot", WindowsNativeBuilder.ARCH_ARM64, 0, 0, false, + "Qualcomm IoT boards (QCS6490 and kin) on Windows IoT Enterprise"); + } + + private WindowsEmbeddedProfile(String name, String arch, int rotation, int dpi, boolean cursor, + String description) { + this.name = name; + this.arch = arch; + this.rotation = rotation; + this.dpi = dpi; + this.cursor = cursor; + this.description = description; + } + + private static void add(String name, String arch, int rotation, int dpi, boolean cursor, String description) { + PROFILES.put(name, new WindowsEmbeddedProfile(name, arch, rotation, dpi, cursor, description)); + } + + /// The profile for a board name, or `generic-x64` for an unknown or absent + /// one: most Windows kiosks are x86-64 panel PCs. + static WindowsEmbeddedProfile forBoard(String board) { + WindowsEmbeddedProfile p = board == null ? null : PROFILES.get(board.trim()); + return p != null ? p : PROFILES.get("generic-x64"); + } + + static Map all() { + return PROFILES; + } +} diff --git a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java index eb463935cfc..5f535740010 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java @@ -175,9 +175,50 @@ protected String hardeningPlatform(BuildRequest request) { return "win"; } + /// True for `windows.embedded=true`: the kiosk target, one borderless window + /// over a whole monitor of a Windows IoT Enterprise device, normally run as + /// its shell (see `cn1_windows_embedded.cpp` and `cn1:deploy-device`). + static boolean isEmbeddedRequested(BuildRequest request) { + return "true".equalsIgnoreCase(request.getArg("windows.embedded", "false")); + } + + /// The `CN1_DEFAULT_*` defines the kiosk unit reads when the device sets no + /// `CN1_*` environment variable: the board profile's, overridden by the + /// individual hints. Semicolon separated, as CMake lists are. + static String embeddedDefines(BuildRequest request, WindowsEmbeddedProfile profile) { + int rotation = parseIntOr(request.getArg("windows.embedded.rotation", null), profile.rotation); + if (rotation != 90 && rotation != 180 && rotation != 270) { + rotation = 0; + } + int dpi = Math.max(0, parseIntOr(request.getArg("windows.embedded.dpi", null), profile.dpi)); + int monitor = Math.max(0, parseIntOr(request.getArg("windows.embedded.monitor", null), 0)); + String cursorHint = request.getArg("windows.embedded.cursor", null); + boolean cursor = cursorHint == null || cursorHint.trim().isEmpty() + ? profile.cursor : "true".equalsIgnoreCase(cursorHint.trim()); + return "CN1_DEFAULT_ROTATION=" + rotation + ";CN1_DEFAULT_DPI=" + dpi + + ";CN1_DEFAULT_MONITOR=" + monitor + ";CN1_DEFAULT_CURSOR=" + (cursor ? 1 : 0); + } + + private static int parseIntOr(String value, int def) { + if (value == null || value.trim().isEmpty()) { + return def; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ex) { + return def; + } + } + @Override public boolean build(File sourceZip, BuildRequest request) throws BuildException { - String arch = normalizeArch(request.getArg("windows.arch", ARCH_X64)); + boolean embedded = isEmbeddedRequested(request); + WindowsEmbeddedProfile board = WindowsEmbeddedProfile.forBoard(request.getArg("windows.embedded.board", null)); + // A board implies its architecture; windows.arch still wins when given. + String arch = normalizeArch(request.getArg("windows.arch", embedded ? board.arch : ARCH_X64)); + if (embedded) { + log("Embedded Windows (kiosk) target, board profile " + board.name + " (" + board.description + ")"); + } log("Building native Windows app for arch=" + arch + " (triple " + targetTriple(arch) + ")"); File tmpFile = getBuildDirectory(); @@ -451,6 +492,12 @@ public void usesClassMethod(String cls, String method) { + "desktop package with a Widgets Board provider. Build them separately."); } + if (xbox && embedded) { + throw new BuildException("windows.xbox=true and windows.embedded=true cannot be combined: " + + "the kiosk target is a full-trust Win32 shell for Windows IoT Enterprise, the Xbox " + + "package a UWP app container."); + } + List configure = new ArrayList(); configure.add("cmake"); configure.add("-S"); @@ -463,6 +510,10 @@ public void usesClassMethod(String cls, String method) { configure.add("-DCMAKE_C_COMPILER=" + clangClExecutable()); configure.add("-DCMAKE_CXX_COMPILER=" + clangClExecutable()); configure.add("-DCN1_WINDOWS_UWP=" + (xbox ? "ON" : "OFF")); + configure.add("-DCN1_WINDOWS_EMBEDDED=" + (embedded ? "ON" : "OFF")); + if (embedded) { + configure.add("-DCN1_WINDOWS_EMBEDDED_DEFINES=" + embeddedDefines(request, board)); + } if (cross) { addCrossCompileConfigure(configure, triple, arch, xwinSysroot, widgetBoardCompileFlags, msix ? widgetBoardLinkFlags(winAppSdk, arch) : ""); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java index 91f4f45a743..a038f156261 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/AppBuilder.java @@ -2949,6 +2949,17 @@ private void doWindowsNativeLocalBuild(File tmpProjectDir, Properties props, Fil } if (e.getWindowsExecutable() != null) { getLog().info("Built native Windows executable: " + e.getWindowsExecutable().getAbsolutePath()); + // Also kept where deploy-device looks for it, as the Linux build is: + // a windows.embedded build goes to a kiosk the same way. + File out = new File(host.buildDirectory(), "windows-native"); + out.mkdirs(); + try { + java.nio.file.Files.copy(e.getWindowsExecutable().toPath(), + new File(out, e.getWindowsExecutable().getName()).toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } catch (java.io.IOException ioe) { + throw new BuildExecutionException("Could not copy the Windows executable to " + out, ioe); + } } } catch (com.codename1.builders.BuildException hardeningEx) { throw new BuildExecutionException(hardeningEx.getMessage(), hardeningEx); diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index 87d627c9265..5b44863010c 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -44,17 +44,36 @@ /// restarted, so a redeploy is one command. The unit starts the application on /// boot, restarts it if it exits, and runs it on the console's virtual /// terminal, which is what the embedded display needs to take the screen. +/// +/// A Windows executable (`windows.embedded=true`) goes to a Windows IoT +/// Enterprise device the same way, over the OpenSSH server Windows ships: +/// copied with `scp`, then installed by a PowerShell script run as the deploy +/// user, who must be an administrator. By default it becomes the device's shell +/// through Shell Launcher v2 -- the application replaces explorer for every +/// account but the Administrators group, which keeps explorer so the device can +/// still be serviced, and Shell Launcher restarts the application when it +/// exits. [#kioskMode(String)] `autostart` instead starts it at logon from a +/// scheduled task, with no lockdown. Assigned Access is not offered: it hosts +/// packaged applications, and its Win32 path is a multi-app kiosk profile that +/// keeps the shell. public final class DeviceDeployer { + /// Where a Windows device keeps the application when no directory is given. + static final String WINDOWS_DIR = "C:\\ProgramData\\CodenameOne"; + private static final String LINUX_DIR = "/opt/codenameone"; private final Log log; private File binary; private String host; private String user; private int port = 22; - private String remoteDir = "/opt/codenameone"; + private String remoteDir = LINUX_DIR; private String service; private String environment = ""; private String profile = "device"; private boolean followLogs; + private String kioskMode = "shell"; + private String kioskUser; + private boolean keyboardFilter; + private Boolean windowsTarget; public DeviceDeployer(Log log) { this.log = log; @@ -108,6 +127,37 @@ public DeviceDeployer profile(String profile) { return this; } + /// Windows only: `shell` (the default) installs the application as the + /// device's shell through Shell Launcher; `autostart` starts it at logon + /// and changes nothing else. + public DeviceDeployer kioskMode(String kioskMode) { + this.kioskMode = kioskMode == null || kioskMode.trim().isEmpty() ? "shell" : kioskMode.trim(); + return this; + } + + /// Windows only: the one account whose shell becomes the application. By + /// default it is every account but the Administrators group. + public DeviceDeployer kioskUser(String kioskUser) { + this.kioskUser = kioskUser; + return this; + } + + /// Windows only: also turns on Keyboard Filter and blocks the shortcuts the + /// application cannot filter itself, Ctrl+Alt+Del among them. Off by + /// default: with it on, only Keyboard Filter's breakout key (the left + /// Windows key pressed five times on the welcome screen) leaves the kiosk. + public DeviceDeployer keyboardFilter(boolean keyboardFilter) { + this.keyboardFilter = keyboardFilter; + return this; + } + + /// Whether the device runs Windows, for [#tailLogs()] when there is no + /// binary to tell; null decides from the binary. + public DeviceDeployer windowsTarget(Boolean windowsTarget) { + this.windowsTarget = windowsTarget; + return this; + } + /// Streams the service's journal after the restart, until interrupted. public DeviceDeployer followLogs(boolean followLogs) { this.followLogs = followLogs; @@ -123,10 +173,23 @@ public void tailLogs() throws BuildException { throw new BuildException("Name the service: set cn1.device.service (or build the app, whose" + " executable names it)"); } + if (isWindowsTarget()) { + name = stripExe(name); + String target = checkedWindowsTarget(name); + run(ssh(target, true, windowsLogCommand(name, 100))); + return; + } String target = checkedTarget(name); run(ssh(target, true, "sudo journalctl -f -n 100 -u " + name + ".service")); } + private boolean isWindowsTarget() { + if (windowsTarget != null) { + return windowsTarget.booleanValue(); + } + return binary != null && binary.isFile() && isPe(binary); + } + public void deploy() throws BuildException { if (host == null || host.trim().isEmpty()) { throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); @@ -135,8 +198,12 @@ public void deploy() throws BuildException { throw new BuildException("No native Linux binary to deploy" + (binary == null ? "" : " at " + binary) + ": build the linux-native target locally or set cn1.device.binary"); } + if (isPe(binary)) { + deployWindows(); + return; + } if (!isElf(binary)) { - throw new BuildException(binary + " is not a Linux executable (no ELF header)"); + throw new BuildException(binary + " is neither a Linux executable (no ELF header) nor a Windows one"); } String name = service != null && !service.trim().isEmpty() ? service.trim() : binary.getName(); String target = checkedTarget(name); @@ -238,6 +305,183 @@ String installScript(String name, String remoteBinary, String machines) { + "sudo systemctl restart -- " + name + ".service\n"; } + private static String stripExe(String name) { + return name.regionMatches(true, Math.max(0, name.length() - 4), ".exe", 0, 4) + ? name.substring(0, name.length() - 4) : name; + } + + private String windowsDir() { + return LINUX_DIR.equals(remoteDir) ? WINDOWS_DIR : remoteDir; + } + + /// As [#checkedTarget(String)], for a Windows device: the directory is a + /// drive-letter path. + private String checkedWindowsTarget(String name) throws BuildException { + String dir = windowsDir(); + if (!dir.matches("[A-Za-z]:\\\\[A-Za-z0-9_. \\\\-]*") || dir.contains("..")) { + throw new BuildException("On Windows cn1.device.dir is a drive-letter path of letters, digits, spaces" + + " and _.- (" + dir + ")"); + } + String saved = remoteDir; + remoteDir = LINUX_DIR; + try { + return checkedTarget(name); + } finally { + remoteDir = saved; + } + } + + private void deployWindows() throws BuildException { + String name = stripExe(service != null && !service.trim().isEmpty() ? service.trim() : binary.getName()); + String target = checkedWindowsTarget(name); + if (!"shell".equals(kioskMode) && !"autostart".equals(kioskMode)) { + throw new BuildException("cn1.device.kioskMode is shell or autostart, not " + kioskMode); + } + String u = kioskUser == null ? "" : kioskUser.trim(); + if (!u.matches("([A-Za-z0-9_][A-Za-z0-9_. -]*(\\\\[A-Za-z0-9_][A-Za-z0-9_. -]*)?)?")) { + throw new BuildException("cn1.device.kioskUser is an account name, optionally DOMAIN\\user (" + u + ")"); + } + File script; + try { + script = File.createTempFile("cn1-deploy-", ".ps1"); + java.nio.file.Files.write(script.toPath(), + windowsInstallScript(name, peMachine(binary)).getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } catch (IOException ex) { + throw new BuildException("Could not write the install script", ex); + } + try { + String staged = "cn1-deploy-" + name; + log.info("Copying " + binary.getName() + " (" + (binary.length() / 1024) + " KB) to " + target + + " and installing it " + ("shell".equals(kioskMode) ? "as the device's shell" : "to start at logon")); + List scp = new ArrayList(Arrays.asList("scp", "-q", "-P", String.valueOf(port), "--", + binary.getAbsolutePath(), target + ":" + staged + ".exe")); + run(scp); + scp.set(scp.size() - 2, script.getAbsolutePath()); + scp.set(scp.size() - 1, target + ":" + staged + ".ps1"); + run(scp); + // The default OpenSSH shell on Windows is cmd; the staged files are in + // the deploy user's profile, which is where both commands start. + run(ssh(target, false, "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " + + staged + ".ps1")); + } finally { + if (!script.delete()) { + script.deleteOnExit(); + } + } + log.info("Deployed " + windowsDir() + "\\" + name + ".exe"); + if (followLogs) { + run(ssh(target, true, windowsLogCommand(name, 50))); + } + } + + private String windowsLogCommand(String name, int lines) { + return "powershell -NoProfile -Command \"Get-Content -Wait -Tail " + lines + " -LiteralPath '" + + windowsDir() + "\\" + name + ".log'\""; + } + + private static String psQuote(String s) { + return "'" + s.replace("'", "''") + "'"; + } + + /// The PowerShell run on a Windows device, from the deploy user's profile + /// where `scp` left the executable beside it. It refuses an executable the + /// device cannot run, stops the running copy, installs the new one, writes + /// the environment and the log path as machine variables (a shell has no + /// other environment to inherit), and then configures the kiosk. Turning a + /// Windows feature on needs a restart, which the script does itself, once. + String windowsInstallScript(String name, String machine) { + String dir = windowsDir(); + StringBuilder ps = new StringBuilder(); + ps.append("$ErrorActionPreference = 'Stop'\n") + .append("$name = ").append(psQuote(name)).append("\n") + .append("$dir = ").append(psQuote(dir)).append("\n") + .append("$exe = Join-Path $dir \"$name.exe\"\n") + .append("$staged = Join-Path $PSScriptRoot \"cn1-deploy-$name.exe\"\n") + .append("$reboot = $false\n"); + if (machine != null) { + // An x64 build also runs on Windows on ARM, under emulation. + ps.append("$arch = $env:PROCESSOR_ARCHITECTURE\n") + .append("if (-not (").append("ARM64".equals(machine) + ? "$arch -eq 'ARM64'" : "$arch -eq 'AMD64' -or $arch -eq 'ARM64'").append(")) {\n") + .append(" Write-Error \"$name was built for ").append(machine) + .append(", but this device is $arch: set windows.arch or windows.embedded.board\"\n") + .append("}\n"); + } + ps.append("New-Item -ItemType Directory -Force -Path $dir | Out-Null\n") + // The kiosk account writes its log here; it is not an administrator. + .append("icacls $dir /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null\n") + .append("Get-Process -Name $name -ErrorAction SilentlyContinue | Stop-Process -Force\n") + .append("Start-Sleep -Milliseconds 300\n") + .append("Move-Item -Force -LiteralPath $staged -Destination $exe\n") + .append("Remove-Item -Force -LiteralPath $PSCommandPath\n") + .append("[Environment]::SetEnvironmentVariable('CN1_LOG_FILE', (Join-Path $dir \"$name.log\"), 'Machine')\n"); + String pairs = environment; + if (!profile.isEmpty() && !(" " + environment.replace(',', ' ')).contains(" CN1_PROFILE=")) { + pairs = environment + " CN1_PROFILE=" + profile; + } + for (String pair : pairs.split("[,\\s]+")) { + int eq = pair.indexOf('='); + if (eq > 0 && pair.substring(0, eq).matches("[A-Za-z_][A-Za-z0-9_]*")) { + ps.append("[Environment]::SetEnvironmentVariable(").append(psQuote(pair.substring(0, eq))) + .append(", ").append(psQuote(pair.substring(eq + 1))).append(", 'Machine')\n"); + } + } + if ("shell".equals(kioskMode)) { + ps.append("if ((Get-WindowsOptionalFeature -Online -FeatureName Client-EmbeddedShellLauncher).State -ne 'Enabled') {\n") + .append(" Enable-WindowsOptionalFeature -Online -All -NoRestart -FeatureName Client-EmbeddedShellLauncher | Out-Null\n") + .append(" $reboot = $true\n") + .append("}\n") + .append("$sl = [wmiclass]'\\\\localhost\\root\\standardcimv2\\embedded:WESL_UserSetting'\n") + // Restart actions: 0 restarts the shell, 3 does nothing. + .append("$sl.SetEnabled($true) | Out-Null\n") + .append("$sl.SetCustomShell('S-1-5-32-544', 'explorer.exe', $null, $null, 3) | Out-Null\n"); + if (kioskUser != null && !kioskUser.trim().isEmpty()) { + ps.append("$sid = (New-Object System.Security.Principal.NTAccount(").append(psQuote(kioskUser.trim())) + .append(")).Translate([System.Security.Principal.SecurityIdentifier]).Value\n") + .append("$sl.SetCustomShell($sid, $exe, $null, $null, 0) | Out-Null\n"); + } else { + ps.append("$sl.SetDefaultShell($exe, 0) | Out-Null\n"); + } + ps.append("Unregister-ScheduledTask -TaskName \"Codename One $name\" -Confirm:$false -ErrorAction SilentlyContinue\n"); + } else { + ps.append("$action = New-ScheduledTaskAction -Execute $exe -WorkingDirectory $dir\n") + .append("$trigger = New-ScheduledTaskTrigger -AtLogOn\n") + .append("$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited\n") + .append("$settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)" + + " -ExecutionTimeLimit 0 -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries\n") + .append("Register-ScheduledTask -Force -TaskName \"Codename One $name\" -Action $action -Trigger $trigger" + + " -Principal $principal -Settings $settings | Out-Null\n"); + } + if (keyboardFilter) { + ps.append("if ((Get-WindowsOptionalFeature -Online -FeatureName Client-KeyboardFilter).State -ne 'Enabled') {\n") + .append(" Enable-WindowsOptionalFeature -Online -All -NoRestart -FeatureName Client-KeyboardFilter | Out-Null\n") + .append(" $reboot = $true\n") + .append("} else {\n") + .append(" foreach ($k in 'Ctrl+Alt+Del', 'Ctrl+Shift+Esc', 'Win+L', 'Alt+Tab', 'Ctrl+Esc', 'Alt+F4', 'Windows') {\n") + .append(" $p = Get-CimInstance -Namespace root\\standardcimv2\\embedded -ClassName WEKF_PredefinedKey" + + " | Where-Object { $_.Id -eq $k }\n") + .append(" if ($p) { Set-CimInstance -InputObject $p -Property @{ Enabled = $true } }\n") + .append(" }\n") + .append("}\n"); + } + ps.append("if ($reboot) {\n") + .append(" Write-Output 'A Windows feature was turned on; restarting the device to finish. Deploy again" + + " after it is back.'\n") + .append(" Restart-Computer -Force\n") + .append(" exit 0\n") + .append("}\n"); + if ("shell".equals(kioskMode)) { + // Shell Launcher started the old copy and restarts its shell when it + // exits, so stopping it above relaunches the new one. A device on which + // nobody is logged on yet starts it at the next logon. + ps.append("Write-Output \"$name is the shell; it restarts when it exits\"\n"); + } else { + ps.append("Start-ScheduledTask -TaskName \"Codename One $name\" -ErrorAction SilentlyContinue\n") + .append("Write-Output \"$name starts at logon\"\n"); + } + return ps.toString(); + } + private List ssh(String target, boolean tty, String script) { List cmd = new ArrayList(Arrays.asList("ssh", "-p", String.valueOf(port))); if (tty) { @@ -314,8 +558,37 @@ static boolean isElf(File f) { } } - /// The newest ELF executable directly under one of `dirs`, or `null`: what a - /// local linux-native build left behind. + static boolean isPe(File f) { + byte[] magic = new byte[2]; + try (InputStream in = new FileInputStream(f)) { + return in.read(magic) == 2 && magic[0] == 'M' && magic[1] == 'Z'; + } catch (IOException ex) { + return false; + } + } + + /// `X64` or `ARM64` from a PE header's machine field, else null. + static String peMachine(File f) { + byte[] h = new byte[4096]; + int n; + try (InputStream in = new FileInputStream(f)) { + n = in.read(h); + } catch (IOException ex) { + return null; + } + if (n < 0x40) { + return null; + } + int pe = (h[0x3c] & 0xff) | ((h[0x3d] & 0xff) << 8) | ((h[0x3e] & 0xff) << 16) | ((h[0x3f] & 0xff) << 24); + if (pe < 0 || pe + 6 > n || h[pe] != 'P' || h[pe + 1] != 'E') { + return null; + } + int m = (h[pe + 4] & 0xff) | ((h[pe + 5] & 0xff) << 8); + return m == 0x8664 ? "X64" : m == 0xAA64 ? "ARM64" : null; + } + + /// The newest executable directly under one of `dirs` -- an ELF binary, or a + /// Windows `.exe` -- or `null`: what a local native build left behind. public static File findBinary(File... dirs) { File best = null; for (File dir : dirs) { @@ -324,7 +597,9 @@ public static File findBinary(File... dirs) { continue; } for (File f : files) { - if (f.isFile() && f.canExecute() && isElf(f) && (best == null || f.lastModified() > best.lastModified())) { + boolean exe = f.isFile() && ((f.canExecute() && isElf(f)) + || (f.getName().toLowerCase(java.util.Locale.ROOT).endsWith(".exe") && isPe(f))); + if (exe && (best == null || f.lastModified() > best.lastModified())) { best = f; } } diff --git a/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java b/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java new file mode 100644 index 00000000000..cb8d4d932b3 --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/builders/WindowsEmbeddedTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.builders; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The `windows.embedded` hints reach the kiosk unit as `CN1_DEFAULT_*` defines. +class WindowsEmbeddedTest { + + private static BuildRequest request(String board) { + BuildRequest r = new BuildRequest(); + r.putArgument("windows.embedded", "true"); + if (board != null) { + r.putArgument("windows.embedded.board", board); + } + return r; + } + + @Test + void aTouchPanelHidesTheCursor() { + BuildRequest r = request(null); + assertTrue(WindowsNativeBuilder.isEmbeddedRequested(r)); + WindowsEmbeddedProfile p = WindowsEmbeddedProfile.forBoard(null); + assertEquals("generic-x64", p.name); + assertEquals("CN1_DEFAULT_ROTATION=0;CN1_DEFAULT_DPI=0;CN1_DEFAULT_MONITOR=0;CN1_DEFAULT_CURSOR=0", + WindowsNativeBuilder.embeddedDefines(r, p)); + } + + @Test + void anArmBoardImpliesArm64() { + assertEquals(WindowsNativeBuilder.ARCH_ARM64, WindowsEmbeddedProfile.forBoard("imx8").arch); + assertEquals(WindowsNativeBuilder.ARCH_ARM64, WindowsEmbeddedProfile.forBoard("qualcomm-iot").arch); + } + + @Test + void theHintsOverrideTheProfileAndBadValuesFallBack() { + BuildRequest r = request("generic-x64-mouse"); + r.putArgument("windows.embedded.rotation", "90"); + r.putArgument("windows.embedded.dpi", "160"); + r.putArgument("windows.embedded.monitor", "1"); + WindowsEmbeddedProfile p = WindowsEmbeddedProfile.forBoard("generic-x64-mouse"); + assertEquals("CN1_DEFAULT_ROTATION=90;CN1_DEFAULT_DPI=160;CN1_DEFAULT_MONITOR=1;CN1_DEFAULT_CURSOR=1", + WindowsNativeBuilder.embeddedDefines(r, p)); + r.putArgument("windows.embedded.rotation", "45"); + r.putArgument("windows.embedded.cursor", "false"); + r.putArgument("windows.embedded.dpi", "lots"); + assertEquals("CN1_DEFAULT_ROTATION=0;CN1_DEFAULT_DPI=0;CN1_DEFAULT_MONITOR=1;CN1_DEFAULT_CURSOR=0", + WindowsNativeBuilder.embeddedDefines(r, p)); + } + + @Test + void notRequestedByDefault() { + assertFalse(WindowsNativeBuilder.isEmbeddedRequested(new BuildRequest())); + } +} diff --git a/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java new file mode 100644 index 00000000000..97c6427514c --- /dev/null +++ b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package com.codename1.maven; + +import com.codename1.builders.BuildException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/// The Windows half of [DeviceDeployer]: a kiosk executable installed as a +/// Windows IoT Enterprise device's shell. +class WindowsDeviceDeployerTest { + + @TempDir + File dir; + + /// A minimal PE image: "MZ", e_lfanew at 0x3c, then "PE\0\0" and the machine. + private File pe(String name, int machine) throws IOException { + byte[] b = new byte[0x100]; + b[0] = 'M'; + b[1] = 'Z'; + b[0x3c] = (byte) 0x80; + b[0x80] = 'P'; + b[0x81] = 'E'; + b[0x84] = (byte) (machine & 0xff); + b[0x85] = (byte) (machine >> 8); + File f = new File(dir, name); + try (OutputStream out = new FileOutputStream(f)) { + out.write(b); + } + return f; + } + + @Test + void readsTheArchitectureOfAWindowsExecutable() throws IOException { + assertEquals("X64", DeviceDeployer.peMachine(pe("a.exe", 0x8664))); + assertEquals("ARM64", DeviceDeployer.peMachine(pe("b.exe", 0xAA64))); + assertNull(DeviceDeployer.peMachine(pe("c.exe", 0x14c))); + assertTrue(DeviceDeployer.isPe(pe("d.exe", 0x8664))); + } + + @Test + void findsAWindowsExecutableToo() throws IOException { + File exe = pe("Kiosk.exe", 0x8664); + assertEquals(exe, DeviceDeployer.findBinary(dir)); + File notExe = pe("Kiosk.dll", 0x8664); + assertTrue(notExe.setLastModified(exe.lastModified() + 5000)); + assertEquals(exe, DeviceDeployer.findBinary(dir), "a DLL is not an application"); + } + + @Test + void theShellModeKeepsExplorerForAdministrators() { + String ps = new DeviceDeployer(new NullLog()).environment("CN1_ROTATION=90") + .windowsInstallScript("Kiosk", "ARM64"); + assertTrue(ps.contains("Client-EmbeddedShellLauncher"), ps); + assertTrue(ps.contains("SetCustomShell('S-1-5-32-544', 'explorer.exe'"), ps); + assertTrue(ps.contains("SetDefaultShell($exe, 0)"), ps); + assertTrue(ps.contains("$dir = 'C:\\ProgramData\\CodenameOne'"), ps); + assertTrue(ps.contains("SetEnvironmentVariable('CN1_ROTATION', '90', 'Machine')"), ps); + assertTrue(ps.contains("SetEnvironmentVariable('CN1_PROFILE', 'device', 'Machine')"), ps); + assertTrue(ps.contains("$arch -eq 'ARM64'"), ps); + assertFalse(ps.contains("Client-KeyboardFilter"), ps); + // The architecture is checked before anything is replaced. + assertTrue(ps.indexOf("PROCESSOR_ARCHITECTURE") < ps.indexOf("Move-Item"), ps); + } + + @Test + void anX64BuildMayGoToWindowsOnArm() { + String ps = new DeviceDeployer(new NullLog()).windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("$arch -eq 'AMD64' -or $arch -eq 'ARM64'"), ps); + } + + @Test + void oneKioskAccountAndTheKeyboardFilter() { + String ps = new DeviceDeployer(new NullLog()).kioskUser("kiosk").keyboardFilter(true) + .windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("NTAccount('kiosk')"), ps); + assertTrue(ps.contains("SetCustomShell($sid, $exe"), ps); + assertFalse(ps.contains("SetDefaultShell"), ps); + assertTrue(ps.contains("'Ctrl+Alt+Del'"), ps); + } + + @Test + void autostartRegistersALogonTaskAndNoShell() { + String ps = new DeviceDeployer(new NullLog()).kioskMode("autostart").windowsInstallScript("Kiosk", "X64"); + assertTrue(ps.contains("New-ScheduledTaskTrigger -AtLogOn"), ps); + assertFalse(ps.contains("WESL_UserSetting"), ps); + } + + @Test + void valuesCannotBreakOutOfTheirQuotes() { + String ps = new DeviceDeployer(new NullLog()).environment("GREETING=it's") + .windowsInstallScript("Kiosk", null); + assertTrue(ps.contains("'GREETING', 'it''s'"), ps); + } + + @Test + void refusesWhatItCannotInstall() throws IOException { + File exe = pe("Kiosk.exe", 0x8664); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .kioskMode("assigned-access")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .kioskUser("a'; Remove-Item C:\\")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("kiosk").binary(exe) + .remoteDir("C:\\..\\Windows")::deploy); + assertThrows(BuildException.class, new DeviceDeployer(new NullLog()).host("-oProxyCommand=x").binary(exe)::deploy); + } + + private static final class NullLog implements com.codename1.build.Log { + public boolean isDebugEnabled() { return false; } + public void debug(CharSequence c) { } + public void debug(CharSequence c, Throwable t) { } + public void debug(Throwable t) { } + public boolean isInfoEnabled() { return false; } + public void info(CharSequence c) { } + public void info(CharSequence c, Throwable t) { } + public void info(Throwable t) { } + public boolean isWarnEnabled() { return false; } + public void warn(CharSequence c) { } + public void warn(CharSequence c, Throwable t) { } + public void warn(Throwable t) { } + public boolean isErrorEnabled() { return false; } + public void error(CharSequence c) { } + public void error(CharSequence c, Throwable t) { } + public void error(Throwable t) { } + } +} diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index fcd6b4bc0f3..7e674790db7 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -437,6 +437,60 @@ static void register(List h) { .type(HintType.STRING) .platform("windows")); + h.add(new Hint("windows.embedded") + .group(HintGroup.WINDOWS) + .type(HintType.BOOLEAN) + .def("false") + .platform("windows") + .doc("Native Windows port only. Builds the kiosk target for a Windows IoT Enterprise " + + "device: one borderless, topmost window covering a whole monitor, the Windows key " + + "and the shell's task switching shortcuts filtered, and text edited through an " + + "on-screen keyboard. `cn1:deploy-device` installs it as the device's shell through " + + "Shell Launcher. Cannot be combined with `windows.xbox`.")); + + h.add(new Hint("windows.embedded.board") + .group(HintGroup.WINDOWS) + .type(HintType.STRING) + .def("generic-x64") + .platform("windows") + .doc("Board profile for `windows.embedded`, which sets the architecture and the " + + "cursor and density defaults: `generic-x64`, `generic-x64-mouse`, " + + "`generic-arm64`, `imx8` or `qualcomm-iot`.")); + + h.add(new Hint("windows.embedded.rotation") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Clockwise rotation `windows.embedded` applies to its monitor while it runs: " + + "0, 90, 180 or 270. Windows restores the panel's own orientation when the " + + "application exits. `CN1_ROTATION` on the device overrides it.")); + + h.add(new Hint("windows.embedded.dpi") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Panel density for `windows.embedded` when the panel misreports its size; 0 " + + "uses what Windows reports. `CN1_DPI` on the device overrides it.")); + + h.add(new Hint("windows.embedded.monitor") + .group(HintGroup.WINDOWS) + .type(HintType.INT) + .def("0") + .platform("windows") + .doc("Which monitor `windows.embedded` covers, in Windows' enumeration order; 0, or a " + + "monitor that does not exist, is the primary. `CN1_MONITOR` on the device " + + "overrides it.")); + + h.add(new Hint("windows.embedded.cursor") + .group(HintGroup.WINDOWS) + .type(HintType.BOOLEAN) + .platform("windows") + .doc("Whether `windows.embedded` shows the mouse cursor. Defaults to the board " + + "profile's choice: hidden on touch panels. `CN1_CURSOR` on the device " + + "overrides it.")); + h.add(new Hint("windows.xbox") .group(HintGroup.WINDOWS) .type(HintType.BOOLEAN) diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java index e7a55993492..4ecf2622343 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeployDeviceTask.java @@ -59,7 +59,8 @@ public void deploy() { File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); String binary = deviceProperty("binary"); File bin = binary != null ? getProject().file(binary) - : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native"), + new File(buildDir, "windows-native")); String port = deviceProperty("port"); String dir = deviceProperty("dir"); try { @@ -69,7 +70,9 @@ public void deploy() { .environment(deviceProperty("env")) .profile(deviceProperty("profile") == null ? "device" : deviceProperty("profile")) .followLogs(deviceProperty("logs") == null ? followLogsByDefault - : "true".equals(deviceProperty("logs"))).deploy(); + : "true".equals(deviceProperty("logs"))) + .kioskMode(deviceProperty("kioskMode")).kioskUser(deviceProperty("kioskUser")) + .keyboardFilter("true".equals(deviceProperty("keyboardFilter"))).deploy(); } catch (BuildException ex) { throw new GradleException(ex.getMessage(), ex); } catch (NumberFormatException ex) { diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java index cb2a4801535..5ad3a0953fc 100644 --- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java +++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/tasks/DeviceLogsTask.java @@ -44,7 +44,8 @@ public void tail() { File buildDir = getProject().getLayout().getBuildDirectory().get().getAsFile(); String binary = deviceProperty("binary"); File bin = binary != null ? getProject().file(binary) - : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native")); + : DeviceDeployer.findBinary(buildDir, new File(buildDir, "linux-native"), + new File(buildDir, "windows-native")); String port = deviceProperty("port"); try { new DeviceDeployer(new GradleLog(getLogger())).host(deviceProperty("host")).user(deviceProperty("user")) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java index 34dde3c6ba5..379a0573351 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/DeployDeviceMojo.java @@ -78,15 +78,30 @@ public class DeployDeviceMojo extends AbstractMojo { @Parameter(property = "cn1.device.logs", defaultValue = "false") private boolean logs; + /** Windows devices: {@code shell} (Shell Launcher, the default) or {@code autostart}. */ + @Parameter(property = "cn1.device.kioskMode", defaultValue = "shell") + private String kioskMode; + + /** Windows devices: the one account whose shell becomes the application; every non-administrator by default. */ + @Parameter(property = "cn1.device.kioskUser") + private String kioskUser; + + /** Windows devices: turns on Keyboard Filter and blocks Ctrl+Alt+Del and the other shell shortcuts. */ + @Parameter(property = "cn1.device.keyboardFilter", defaultValue = "false") + private boolean keyboardFilter; + @Parameter(defaultValue = "${project.build.directory}", readonly = true) private File buildDirectory; - /// The newest executable a local Linux build left: `target/linux-native` of this - /// module, or of the `linux` module beside it when the goal runs at the root. + /// The newest executable a local native build left: `target/linux-native` or + /// `target/windows-native` of this module, or of the `linux` or `win` module + /// beside it when the goal runs at the root. static File findBuiltBinary(File buildDirectory) { File root = buildDirectory.getParentFile(); return DeviceDeployer.findBinary(buildDirectory, new File(buildDirectory, "linux-native"), - root == null ? null : new File(root, "linux" + File.separator + "target" + File.separator + "linux-native")); + new File(buildDirectory, "windows-native"), + root == null ? null : new File(root, "linux" + File.separator + "target" + File.separator + "linux-native"), + root == null ? null : new File(root, "win" + File.separator + "target" + File.separator + "windows-native")); } @Override @@ -97,7 +112,8 @@ public void execute() throws MojoExecutionException { } try { new DeviceDeployer(MavenLog.of(getLog())).host(host).user(user).port(port).binary(bin) - .remoteDir(remoteDir).service(service).environment(environment).profile(profile).followLogs(logs).deploy(); + .remoteDir(remoteDir).service(service).environment(environment).profile(profile).followLogs(logs) + .kioskMode(kioskMode).kioskUser(kioskUser).keyboardFilter(keyboardFilter).deploy(); } catch (BuildException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } diff --git a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java index 038788d960b..2de2020c937 100644 --- a/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java +++ b/vm/ByteCodeTranslator/src/com/codename1/tools/translator/ByteCodeTranslator.java @@ -1476,7 +1476,19 @@ private static void writeCmakeProject(File projectRoot, File srcRoot, String app writer.append(" string(REPLACE \"/MDd\" \"/MTd\" ${CN1_FLAG_VAR} \"${${CN1_FLAG_VAR}}\")\n"); writer.append(" endforeach()\n"); writer.append(" else()\n"); - writer.append(" target_link_libraries(${PROJECT_NAME} d2d1 dwrite dxgi windowscodecs winhttp ws2_32 user32 gdi32 ole32 oleaut32 uuid mf mfplat mfreadwrite mfuuid shell32 comdlg32 crypt32 bcrypt ncrypt winmm runtimeobject dbghelp advapi32 dwmapi)\n"); + writer.append(" target_link_libraries(${PROJECT_NAME} d2d1 dwrite dxgi windowscodecs winhttp ws2_32 user32 gdi32 ole32 oleaut32 uuid mf mfplat mfreadwrite mfuuid shell32 comdlg32 crypt32 bcrypt ncrypt winmm runtimeobject dbghelp advapi32 dwmapi dxva2)\n"); + writer.append(" endif()\n"); + // CN1_WINDOWS_EMBEDDED (WindowsNativeBuilder passes it for + // windows.embedded=true) is the kiosk target: a borderless window + // over a whole monitor, the shell shortcuts filtered, the shared + // on-screen keyboard (cn1_windows_embedded.cpp). The board profile's + // defaults ride in CN1_WINDOWS_EMBEDDED_DEFINES as CN1_DEFAULT_*. + // dxva2, above, is the DDC/CI backlight of cn1_windows_hardware.cpp, + // which a desktop build answers through as well. + writer.append(" option(CN1_WINDOWS_EMBEDDED \"Build the embedded (kiosk) target\" OFF)\n"); + writer.append(" set(CN1_WINDOWS_EMBEDDED_DEFINES \"\" CACHE STRING \"CN1_DEFAULT_* defaults of the kiosk target, semicolon separated\")\n"); + writer.append(" if(CN1_WINDOWS_EMBEDDED AND NOT CN1_WINDOWS_UWP)\n"); + writer.append(" target_compile_definitions(${PROJECT_NAME} PRIVATE CN1_WINDOWS_EMBEDDED=1 ${CN1_WINDOWS_EMBEDDED_DEFINES})\n"); writer.append(" endif()\n"); // BrowserComponent is backed by WebView2 (cn1_windows_browser.cpp), // gated on the SDK being present: when WEBVIEW2_SDK_DIR points at a From 7d46d434148283f5a59d5fe51e9399adb36da963 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:19:12 +0200 Subject: [PATCH 2/6] Windows embedded: cross-build the kiosk exe for x64 and arm64, launch it in CI The xwin cross-compile test now also builds the windows.embedded variant for x64 and arm64 (the arm64 sysroot is splatted in CI), which caught a 10-bit I2C mode Windows.Devices.I2c does not have: such an address now answers ENOTSUP. A new kiosk-run job launches the x64 kiosk exe on windows-latest and checks the window is a frameless topmost popup covering the primary monitor, survives Alt+F4 and Alt+Tab, and writes CN1_LOG_FILE. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/windows-cross-compile.yml | 95 ++++++++++++++++++- .../nativeSources/cn1_windows_hardware.cpp | 8 +- .../CleanTargetIntegrationTest.java | 41 +++++++- 3 files changed, 135 insertions(+), 9 deletions(-) diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index 791fe526de3..5954d7e4bc8 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -17,6 +17,7 @@ on: - 'maven/windows/**' - 'scripts/gen-windows-uwp-stubs.py' - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java' - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' - '!vm/**/README.md' @@ -31,6 +32,7 @@ on: - 'maven/windows/**' - 'scripts/gen-windows-uwp-stubs.py' - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsNativeBuilder.java' + - 'maven/build-engine/src/main/java/com/codename1/builders/WindowsEmbeddedProfile.java' - 'maven/build-engine/src/test/java/com/codename1/builders/WindowsNativeBuilderArchTest.java' - 'maven/build-engine/src/test/resources/com/codename1/builders/xbox-smoke-AppxManifest.xml' @@ -94,7 +96,7 @@ jobs: uses: actions/setup-java@v5 with: { distribution: 'temurin', java-version: '8' } - - name: Install xwin + lay out the Windows SDK (x64) + - name: Install xwin + lay out the Windows SDK (x64 + arm64) env: # Harden against transient crates.io / download flakes ("curl [16] # HTTP2 framing layer", "[55] OpenSSL SSL_read unexpected eof"): @@ -106,7 +108,7 @@ jobs: n=0 until [ "$n" -ge 3 ]; do cargo install xwin --version 0.9.0 --locked \ - && xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ + && xwin --accept-license --arch x86_64,aarch64 splat --output "$RUNNER_TEMP/xwin-sdk" \ && break n=$((n+1)) echo "xwin install/splat attempt $n failed; retrying after backoff..." @@ -178,12 +180,14 @@ jobs: CN1_CLANG_CL: clang-cl CN1_LLVM_RC: llvm-rc CN1_UWP_EXE_OUT: ${{ github.workspace }}/uwp-smoke/app.exe + CN1_KIOSK_EXE_OUT: ${{ github.workspace }}/kiosk-smoke/app.exe run: | cd vm JAVA_HOME="$JDK_8_HOME" mvn -B -pl tests -am test \ '-Dtest=CleanTargetIntegrationTest#crossCompilesWindowsExeWithXwin' \ '-Dsurefire.failIfNoSpecifiedTests=false' test -f "$CN1_UWP_EXE_OUT" + test -f "$CN1_KIOSK_EXE_OUT" # The development certificate windows.xbox builds create, made by the # builder's own openssl commands; the Windows job signs with it, so a @@ -209,6 +213,14 @@ jobs: # The manifest is pinned to WindowsNativeBuilder.buildXboxAppxManifest by # WindowsNativeBuilderArchTest, so the run below installs what windows.xbox # builds ship. + - name: Upload the kiosk smoke app + uses: actions/upload-artifact@v4 + with: + name: kiosk-smoke + path: kiosk-smoke/app.exe + if-no-files-found: error + retention-days: 3 + - name: Upload the UWP smoke app uses: actions/upload-artifact@v4 with: @@ -316,3 +328,82 @@ jobs: path: uwp-run/ if-no-files-found: ignore retention-days: 7 + + # The windows.embedded=true build on a real desktop: the window must cover the + # primary monitor with no frame, stay topmost, and survive the shortcuts the + # kiosk hook blocks. Shell Launcher itself needs IoT Enterprise, which no + # runner has; the deploy script is unit-tested in build-engine instead. + kiosk-run: + name: launch the kiosk build (Windows x64) + needs: cross-compile + runs-on: windows-latest + timeout-minutes: 15 + steps: + - name: Download the kiosk smoke app + uses: actions/download-artifact@v4 + with: + name: kiosk-smoke + path: kiosk-smoke + + - name: Launch, check the window, send the blocked shortcuts + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + Add-Type -AssemblyName System.Drawing, System.Windows.Forms + Add-Type @' + using System; + using System.Runtime.InteropServices; + public static class K { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } + [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra); + public static void Chord(byte mod, byte key) { + keybd_event(mod, 0, 0, UIntPtr.Zero); keybd_event(key, 0, 0, UIntPtr.Zero); + keybd_event(key, 0, 2, UIntPtr.Zero); keybd_event(mod, 0, 2, UIntPtr.Zero); + } + } + '@ + New-Item -ItemType Directory -Force kiosk-run | Out-Null + $log = Join-Path (Resolve-Path kiosk-run) 'app.log' + $env:CN1_LOG_FILE = $log + $p = Start-Process -FilePath (Resolve-Path kiosk-smoke/app.exe) -PassThru + $h = [IntPtr]::Zero + for ($i = 0; $i -lt 60 -and $h -eq [IntPtr]::Zero; $i++) { + Start-Sleep -Seconds 1 + if ($p.HasExited) { throw "the kiosk app exited with $($p.ExitCode) before showing a window" } + $p.Refresh(); $h = $p.MainWindowHandle + } + if ($h -eq [IntPtr]::Zero) { throw "the kiosk app showed no window in 60s" } + Start-Sleep -Seconds 3 + $style = [K]::GetWindowLong($h, -16); $ex = [K]::GetWindowLong($h, -20) + $r = New-Object K+RECT; [void][K]::GetWindowRect($h, [ref]$r) + $b = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds + Write-Host ("style=0x{0:X8} ex=0x{1:X8} rect={2},{3},{4},{5} screen={6}" -f $style, $ex, $r.L, $r.T, $r.R, $r.B, $b) + $bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height + $g = [System.Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size); $g.Dispose() + $bmp.Save('kiosk-run/screen.png', [System.Drawing.Imaging.ImageFormat]::Png) + if (($style -band 0x80000000) -eq 0) { throw "the kiosk window is not WS_POPUP" } + if (($style -band 0x00C00000) -ne 0) { throw "the kiosk window has a caption" } + if (($ex -band 0x8) -eq 0) { throw "the kiosk window is not topmost" } + if ($r.L -ne $b.Left -or $r.T -ne $b.Top -or $r.R -ne $b.Right -or $r.B -ne $b.Bottom) { + throw "the kiosk window does not cover the primary monitor" + } + # Alt+F4 would close an ordinary window; the kiosk hook swallows it. + [void][K]::SetForegroundWindow($h) + [K]::Chord(0x12, 0x73) + [K]::Chord(0x12, 0x09) + Start-Sleep -Seconds 3 + if ($p.HasExited) { throw "Alt+F4 reached the kiosk window and closed it" } + if (-not (Test-Path $log)) { throw "CN1_LOG_FILE was not created" } + Stop-Process -Id $p.Id -Force + + - name: Upload the kiosk capture + if: always() + uses: actions/upload-artifact@v4 + with: + name: kiosk-run-capture + path: kiosk-run/ + if-no-files-found: ignore + retention-days: 7 diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp index 3a7d766cfa1..67657c69fc8 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp @@ -480,6 +480,11 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODE if (id == NULL) { return -CN1HW_ENODEV; } + // Windows.Devices.I2c addresses 7-bit devices only; there is no 10-bit mode. + if (address > 0x7f) { + WindowsDeleteString(id); + return -CN1HW_ENOTSUP; + } ComPtr settingsFactory; ComPtr settings; HRESULT hr = RoGetActivationFactory(HStringReference(RuntimeClass_Windows_Devices_I2c_I2cConnectionSettings).Get(), @@ -487,9 +492,6 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwI2cOpen___int_int_R_int(CODE if (SUCCEEDED(hr)) { hr = settingsFactory->Create(address, &settings); } - if (SUCCEEDED(hr) && address > 0x7f) { - hr = settings->put_AddressingMode(I2cAddressingMode_TenBit); - } ComPtr> op; if (SUCCEEDED(hr)) { hr = statics->FromIdAsync(id, settings.Get(), &op); diff --git a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java index b9bfc151dd6..941b85f1a9c 100644 --- a/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java +++ b/vm/tests/src/test/java/com/codename1/tools/translator/CleanTargetIntegrationTest.java @@ -769,6 +769,29 @@ void crossCompilesWindowsExeWithXwin() throws Exception { Path uwpExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", true); System.out.println("CN1_XWIN_UWP_EXE=" + uwpExe.toAbsolutePath() + " (" + (Files.size(uwpExe) / 1024) + "KB)"); + // --- the kiosk build (windows.embedded=true) for an x64 panel PC and an + // arm64 IoT board: the embedded window code and the WinRT bus natives + // compile only in this variant. The x64 one is what the Windows CI job + // launches to check the kiosk window (CN1_KIOSK_EXE_OUT). --- + Path kioskExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", false, true, false); + System.out.println("CN1_XWIN_KIOSK_EXE=" + kioskExe.toAbsolutePath()); + String kioskOut = System.getenv("CN1_KIOSK_EXE_OUT"); + boolean ci = kioskOut != null && !kioskOut.trim().isEmpty(); + // CI splats aarch64 too, so a missing arm64 build there is a failure. + if (ci || Files.isDirectory(sys.resolve("crt/lib/aarch64"))) { + Path armExe = crossBuildDist(cmakeRoot, sys, clangCl, "WinFormApp", false, true, true); + System.out.println("CN1_XWIN_KIOSK_ARM64_EXE=" + armExe.toAbsolutePath()); + } else { + System.out.println("No aarch64 libraries in the sysroot; the arm64 kiosk build was skipped"); + } + if (ci) { + Path dest = Paths.get(kioskOut.trim()); + if (dest.getParent() != null) { + Files.createDirectories(dest.getParent()); + } + Files.copy(kioskExe, dest, StandardCopyOption.REPLACE_EXISTING); + } + // --- the UWP smoke app the Windows CI job installs and launches as a real // UWP activation (windows-cross-compile.yml). It writes a marker into its // package LocalState once its first form has painted, which is the only @@ -880,8 +903,16 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN /// which compiles the app API partition and links WindowsApp.lib alone. static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName, boolean uwp) throws Exception { - String target = "x86_64-pc-windows-msvc"; - String libArch = "x86_64"; + return crossBuildDist(cmakeRoot, sys, clangCl, exeName, uwp, false, false); + } + + /// `embedded` builds the kiosk variant `windows.embedded=true` produces + /// (`-DCN1_WINDOWS_EMBEDDED=ON`); `arm64` targets Windows on Arm, which the + /// arm64 IoT boards run. The sysroot must have been splatted for aarch64 too. + static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeName, boolean uwp, + boolean embedded, boolean arm64) throws Exception { + String target = arm64 ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"; + String libArch = arm64 ? "aarch64" : "x86_64"; String inc = String.join(" ", "--target=" + target, imsvc(sys.resolve("crt/include")), @@ -903,13 +934,15 @@ static Path crossBuildDist(Path cmakeRoot, Path sys, String clangCl, String exeN "-I", sys.resolve("crt/include").toString(), "-I", sys.resolve("sdk/include/ucrt").toString()); String llvmRc = System.getenv().getOrDefault("CN1_LLVM_RC", "llvm-rc"); - Path buildDir = cmakeRoot.resolve(uwp ? "xbuild-uwp" : "xbuild"); + Path buildDir = cmakeRoot.resolve((uwp ? "xbuild-uwp" : embedded ? "xbuild-embedded" : "xbuild") + + (arm64 ? "-arm64" : "")); Files.createDirectories(buildDir); runCommand(Arrays.asList("cmake", "-S", cmakeRoot.toString(), "-B", buildDir.toString(), "-DCN1_WINDOWS_UWP=" + (uwp ? "ON" : "OFF"), + "-DCN1_WINDOWS_EMBEDDED=" + (embedded ? "ON" : "OFF"), "-G", "Ninja", "-DCMAKE_SYSTEM_NAME=Windows", - "-DCMAKE_SYSTEM_PROCESSOR=AMD64", + "-DCMAKE_SYSTEM_PROCESSOR=" + (arm64 ? "ARM64" : "AMD64"), // STATIC_LIBRARY so CMake's compiler-detection try_compile does not // need a full link before the toolchain flags are fully applied. "-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY", From 1a84e89402abda4545dfb56ba1b0a7f90876ec1f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:41:04 +0200 Subject: [PATCH 3/6] Windows embedded: red-team round 1 fixes WinRT awaits keep their completion event in the handler object and cancel on timeout (ETIMEDOUT) instead of reading a dead stack event; enumeration parks the thread for GC; GPIO edge waiters detect a reused slot by generation, are woken on close and stop on WAIT_FAILED. The kiosk exe directory is admin-only (Users read/execute, no inheritance) so no user can swap the shell or plant a DLL beside it; the log moves to logs\. The CI kiosk job proves the window is foreground before sending Alt+F4 and closes it with WM_CLOSE as a control. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/windows-cross-compile.yml | 19 ++++- .../nativeSources/cn1_windows_hardware.cpp | 69 ++++++++++++++++--- .../com/codename1/maven/DeviceDeployer.java | 19 +++-- .../maven/WindowsDeviceDeployerTest.java | 7 ++ 4 files changed, 97 insertions(+), 17 deletions(-) diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index 5954d7e4bc8..3cb445ebfc5 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -358,6 +358,8 @@ jobs: [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r); [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); + [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra); public static void Chord(byte mod, byte key) { keybd_event(mod, 0, 0, UIntPtr.Zero); keybd_event(key, 0, 0, UIntPtr.Zero); @@ -391,13 +393,26 @@ jobs: throw "the kiosk window does not cover the primary monitor" } # Alt+F4 would close an ordinary window; the kiosk hook swallows it. - [void][K]::SetForegroundWindow($h) + # The hook filters only while the kiosk window is in front, so the + # check means nothing unless it is: a refused SetForegroundWindow + # would send the chord to another window and pass vacuously. A lone + # Alt tap releases the foreground lock for this process. + for ($i = 0; $i -lt 10 -and [K]::GetForegroundWindow() -ne $h; $i++) { + [K]::keybd_event(0x12, 0, 0, [UIntPtr]::Zero); [K]::keybd_event(0x12, 0, 2, [UIntPtr]::Zero) + [void][K]::SetForegroundWindow($h) + Start-Sleep -Milliseconds 300 + } + if ([K]::GetForegroundWindow() -ne $h) { throw "the kiosk window never became the foreground window" } [K]::Chord(0x12, 0x73) [K]::Chord(0x12, 0x09) Start-Sleep -Seconds 3 if ($p.HasExited) { throw "Alt+F4 reached the kiosk window and closed it" } if (-not (Test-Path $log)) { throw "CN1_LOG_FILE was not created" } - Stop-Process -Id $p.Id -Force + if ([K]::GetForegroundWindow() -ne $h) { throw "Alt+Tab moved the foreground away from the kiosk window" } + # Control: the same window does close when asked directly, so its + # surviving Alt+F4 above was the hook, not an app that ignores closing. + [void][K]::PostMessage($h, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero) + if (-not $p.WaitForExit(15000)) { Stop-Process -Id $p.Id -Force; throw "WM_CLOSE did not close the kiosk app" } - name: Upload the kiosk capture if: always() diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp index 67657c69fc8..2e6bd457c57 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp @@ -101,6 +101,7 @@ enum { HW_FREE = 0, HW_GPIO, HW_I2C, HW_SPI, HW_SERIAL, HW_PWM }; typedef struct CN1WinHwSlot { int kind; + unsigned gen; /* bumped on every open: a waiter's handle is stale once it moves */ IUnknown* device; /* IGpioPin / II2cDevice / ISpiDevice / IPwmPin */ IUnknown* owner; /* IPwmController a PWM pin needs kept alive */ HANDLE serial; @@ -120,7 +121,9 @@ static int cn1WinHwAlloc(int kind) { AcquireSRWLockExclusive(&cn1WinHwLock); for (int i = 0; i < CN1HW_SLOTS; i++) { if (cn1WinHwSlots[i].kind == HW_FREE) { + unsigned gen = cn1WinHwSlots[i].gen + 1; ZeroMemory(&cn1WinHwSlots[i], sizeof(CN1WinHwSlot)); + cn1WinHwSlots[i].gen = gen; cn1WinHwSlots[i].kind = kind; cn1WinHwSlots[i].serial = INVALID_HANDLE_VALUE; h = i; @@ -176,6 +179,9 @@ static int cn1WinHwFromHr(HRESULT hr) { || hr == HRESULT_FROM_WIN32(ERROR_SHARING_VIOLATION)) { return -CN1HW_EBUSY; } + if (hr == HRESULT_FROM_WIN32(ERROR_TIMEOUT)) { + return -CN1HW_ETIMEDOUT; + } if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) || hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND) || hr == REGDB_E_CLASSNOTREG) { return -CN1HW_ENODEV; @@ -203,21 +209,43 @@ static int cn1WinHwFromWin32(DWORD err) { /* Blocks the calling worker thread until a WinRT operation completes. The same * helper as cn1_windows_winrt.cpp's, kept file-local as that one is. */ +/* The completion event belongs to the handler object, which the operation + * holds a reference to: an operation that completes after this function gave up + * on it signals an event that still exists, never a closed (and possibly + * reused) handle. */ +template +class CN1WinHwCompleted : public RuntimeClass, IAsyncOperationCompletedHandler> { +public: + HANDLE done; + CN1WinHwCompleted() : done(CreateEventExW(nullptr, nullptr, 0, EVENT_ALL_ACCESS)) {} + ~CN1WinHwCompleted() { + if (done != NULL) { + CloseHandle(done); + } + } + HRESULT STDMETHODCALLTYPE Invoke(IAsyncOperation*, AsyncStatus) override { + SetEvent(done); + return S_OK; + } +}; + template static HRESULT cn1WinHwAwait(IAsyncOperation* op, TResultPtr result) { - Event done(CreateEventExW(nullptr, nullptr, 0, EVENT_ALL_ACCESS)); - if (!done.IsValid()) { + ComPtr> handler = Make>(); + if (handler == nullptr || handler->done == NULL) { return E_FAIL; } - HRESULT hr = op->put_Completed(Callback>( - [&done](IAsyncOperation*, AsyncStatus) { - SetEvent(done.Get()); - return S_OK; - }).Get()); + HRESULT hr = op->put_Completed(handler.Get()); if (FAILED(hr)) { return hr; } - WaitForSingleObject(done.Get(), 30000); + if (WaitForSingleObject(handler->done, 30000) != WAIT_OBJECT_0) { + ComPtr info; + if (SUCCEEDED(op->QueryInterface(IID_PPV_ARGS(&info)))) { + info->Cancel(); + } + return HRESULT_FROM_WIN32(ERROR_TIMEOUT); + } return op->GetResults(result); } @@ -246,7 +274,11 @@ static int cn1WinHwFindDevices(HSTRING selector, HSTRING* ids, int max) { return 0; } ComPtr> list; - if (FAILED(cn1WinHwAwait(op.Get(), list.GetAddressOf())) || list == nullptr) { + /* Enumeration can take seconds; no Java object is touched while parked. */ + CN1_YIELD_THREAD; + HRESULT hr = cn1WinHwAwait(op.Get(), list.GetAddressOf()); + CN1_RESUME_THREAD; + if (FAILED(hr) || list == nullptr) { return 0; } unsigned size = 0; @@ -407,11 +439,13 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWrite___int_boolean_R_in JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_1ARRAY_R_int(CODENAME_ONE_THREAD_STATE, JAVA_INT h, JAVA_INT timeoutMs, JAVA_OBJECT tsOut) { HANDLE ev = NULL; + unsigned gen = 0; if (h < 0 || h >= CN1HW_SLOTS) { return -CN1HW_EBADF; } AcquireSRWLockShared(&cn1WinHwLock); if (cn1WinHwSlots[h].kind == HW_GPIO && cn1WinHwSlots[h].edgeEvent != NULL) { + gen = cn1WinHwSlots[h].gen; DuplicateHandle(GetCurrentProcess(), cn1WinHwSlots[h].edgeEvent, GetCurrentProcess(), &ev, 0, FALSE, DUPLICATE_SAME_ACCESS); } @@ -425,7 +459,8 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_ for (;;) { AcquireSRWLockExclusive(&cn1WinHwLock); CN1WinHwSlot* s = &cn1WinHwSlots[h]; - if (s->kind != HW_GPIO) { + if (s->kind != HW_GPIO || s->gen != gen) { + /* Closed, or closed and reopened as another pin while we waited. */ r = -CN1HW_EBADF; } else if (s->edgeCount > 0) { r = s->edgeRising[s->edgeHead]; @@ -448,6 +483,10 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioWaitEdge___int_int_long_ CN1_YIELD_THREAD; DWORD w = WaitForSingleObject(ev, wait); CN1_RESUME_THREAD; + if (w == WAIT_FAILED) { + r = -CN1HW_EIO; + break; + } if (w != WAIT_OBJECT_0 && timeoutMs >= 0) { break; } @@ -724,7 +763,10 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwSerialRead___int_byte_1ARRAY CloseHandle(port); return 0; } - /* Return as soon as any byte is there, or after timeoutMs with nothing: the + /* A timeout with nothing read answers 0, not -ETIMEDOUT: the Linux port's + * hwSerialRead (cn1_hardware_impl.h) does the same, and KernelHardware reads + * 0 as "nothing yet" on both. + * Return as soon as any byte is there, or after timeoutMs with nothing: the * documented MAXDWORD/MAXDWORD/constant combination. A negative timeout * waits indefinitely, which the constant cannot say, so it is a long one. */ COMMTIMEOUTS t; @@ -904,6 +946,11 @@ JAVA_VOID com_codename1_impl_windows_WindowsNative_hwClose___int_boolean(CODENAM if (s.kind == HW_GPIO && s.device != NULL && s.edgeEvent != NULL) { ((IGpioPin*) s.device)->remove_ValueChanged(s.edgeToken); } + if (s.edgeEvent != NULL) { + /* Wake a hwGpioWaitEdge parked on its duplicate of this event; it then + * sees the slot freed and answers EBADF instead of waiting forever. */ + SetEvent(s.edgeEvent); + } if (s.kind == HW_PWM && s.device != NULL) { ((IPwmPin*) s.device)->Stop(); } diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index 5b44863010c..b65a3a39885 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -376,7 +376,7 @@ private void deployWindows() throws BuildException { private String windowsLogCommand(String name, int lines) { return "powershell -NoProfile -Command \"Get-Content -Wait -Tail " + lines + " -LiteralPath '" - + windowsDir() + "\\" + name + ".log'\""; + + windowsDir() + "\\logs\\" + name + ".log'\""; } private static String psQuote(String s) { @@ -408,13 +408,24 @@ String windowsInstallScript(String name, String machine) { .append("}\n"); } ps.append("New-Item -ItemType Directory -Force -Path $dir | Out-Null\n") - // The kiosk account writes its log here; it is not an administrator. - .append("icacls $dir /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null\n") + // The exe is the shell of every non-administrator: only + // Administrators and SYSTEM may change anything beside it. With + // write access a user could replace it, or plant a DLL the exe + // would load from its own directory first, and run code as the + // next user to log on. ProgramData's inherited ACL lets Users + // create files, so inheritance is cut and the grants replaced. + .append("icacls $dir /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F'" + + " '*S-1-5-32-545:(OI)(CI)RX' | Out-Null\n") + // The kiosk account is not an administrator and writes its log + // here, the one place it can write. + .append("$logs = Join-Path $dir 'logs'\n") + .append("New-Item -ItemType Directory -Force -Path $logs | Out-Null\n") + .append("icacls $logs /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null\n") .append("Get-Process -Name $name -ErrorAction SilentlyContinue | Stop-Process -Force\n") .append("Start-Sleep -Milliseconds 300\n") .append("Move-Item -Force -LiteralPath $staged -Destination $exe\n") .append("Remove-Item -Force -LiteralPath $PSCommandPath\n") - .append("[Environment]::SetEnvironmentVariable('CN1_LOG_FILE', (Join-Path $dir \"$name.log\"), 'Machine')\n"); + .append("[Environment]::SetEnvironmentVariable('CN1_LOG_FILE', (Join-Path $logs \"$name.log\"), 'Machine')\n"); String pairs = environment; if (!profile.isEmpty() && !(" " + environment.replace(',', ' ')).contains(" CN1_PROFILE=")) { pairs = environment + " CN1_PROFILE=" + profile; diff --git a/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java index 97c6427514c..4a328f2f903 100644 --- a/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java +++ b/maven/build-engine/src/test/java/com/codename1/maven/WindowsDeviceDeployerTest.java @@ -90,6 +90,13 @@ void theShellModeKeepsExplorerForAdministrators() { assertTrue(ps.contains("SetEnvironmentVariable('CN1_PROFILE', 'device', 'Machine')"), ps); assertTrue(ps.contains("$arch -eq 'ARM64'"), ps); assertFalse(ps.contains("Client-KeyboardFilter"), ps); + // Users may run the shell but never write beside it (exe swap, DLL plant); + // only the logs folder is theirs. + assertTrue(ps.contains("icacls $dir /inheritance:r /grant:r"), ps); + assertTrue(ps.contains("'*S-1-5-32-545:(OI)(CI)RX'"), ps); + assertFalse(ps.contains("icacls $dir /grant '*S-1-5-32-545:(OI)(CI)M'"), ps); + assertTrue(ps.contains("icacls $logs /grant '*S-1-5-32-545:(OI)(CI)M'"), ps); + assertTrue(ps.contains("(Join-Path $logs \"$name.log\")"), ps); // The architecture is checked before anything is replaced. assertTrue(ps.indexOf("PROCESSOR_ARCHITECTURE") < ps.indexOf("Move-Item"), ps); } From 870dcd2cc1e6a49840547a28b1d16f337dd3ad01 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:46:08 +0200 Subject: [PATCH 4/6] Windows embedded: red-team round 2 fixes GPIO edge callbacks check the slot generation so a late edge of a closed pin never reaches the pin that reopened the slot; the kiosk log is unbuffered (the MS CRT treats _IOLBF as full buffering); CN1_* booleans accept true/false; icacls failures stop the install; docs list every key the Keyboard Filter blocks and say autostart covers administrators. Co-Authored-By: Claude Opus 5.5 --- .../nativeSources/cn1_windows_embedded.cpp | 14 +++++++++++--- .../nativeSources/cn1_windows_hardware.cpp | 15 +++++++++++++-- docs/developer-guide/Embedded-Windows.asciidoc | 10 +++++----- .../java/com/codename1/maven/DeviceDeployer.java | 5 ++++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp index bbaf5aad7ee..0a8a4d5a4cb 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp @@ -79,6 +79,13 @@ static int cn1EmbEnvInt(const char* name, int def) { if (n == 0 || n >= sizeof(buf)) { return def; } + /* The hints are booleans for the cursor; accept the words as well as 1/0. */ + if (lstrcmpiA(buf, "true") == 0) { + return 1; + } + if (lstrcmpiA(buf, "false") == 0) { + return 0; + } return atoi(buf); } @@ -224,9 +231,10 @@ void cn1WinEmbeddedRedirectLog(void) { freopen_s(&f, path, "a", stdout); f = NULL; freopen_s(&f, path, "a", stderr); - /* Line buffered, so cn1:device-logs follows the application as it runs - * rather than a buffer at a time. */ - setvbuf(stdout, NULL, _IOLBF, 1024); + /* Unbuffered: the Microsoft CRT treats _IOLBF as full buffering, so a line + * buffer would reach cn1:device-logs a kilobyte at a time and lose the last + * output before a crash. */ + setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); } diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp index 2e6bd457c57..2367608734f 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_hardware.cpp @@ -134,6 +134,13 @@ static int cn1WinHwAlloc(int kind) { return h; } +static unsigned cn1WinHwSlotGen(int h) { + AcquireSRWLockShared(&cn1WinHwLock); + unsigned gen = cn1WinHwSlots[h].gen; + ReleaseSRWLockShared(&cn1WinHwLock); + return gen; +} + /* A referenced copy of the slot's device interface, or NULL for a stale or * wrong-kind handle. The caller releases it. */ static IUnknown* cn1WinHwRef(int h, int kind) { @@ -367,9 +374,10 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_ if (!output && edges != 0) { HANDLE ev = CreateEventW(NULL, FALSE, FALSE, NULL); int slot = h; + unsigned gen = cn1WinHwSlotGen(h); EventRegistrationToken token; hr = pin->add_ValueChanged(Callback>( - [slot, edges](IGpioPin*, IGpioPinValueChangedEventArgs* args) -> HRESULT { + [slot, gen, edges](IGpioPin*, IGpioPinValueChangedEventArgs* args) -> HRESULT { GpioPinEdge edge = GpioPinEdge_FallingEdge; args->get_Edge(&edge); int rising = edge == GpioPinEdge_RisingEdge ? 1 : 0; @@ -379,7 +387,10 @@ JAVA_INT com_codename1_impl_windows_WindowsNative_hwGpioOpen___java_lang_String_ LONGLONG ns = cn1WinHwNanos(); AcquireSRWLockExclusive(&cn1WinHwLock); CN1WinHwSlot* s = &cn1WinHwSlots[slot]; - if (s->kind == HW_GPIO && s->edgeEvent != NULL) { + /* remove_ValueChanged does not wait for a callback already + * running: a late edge of a closed pin must not land in the + * pin that reopened this slot. */ + if (s->kind == HW_GPIO && s->gen == gen && s->edgeEvent != NULL) { /* A full ring drops the oldest edge, as the kernel's * line-event buffer does. */ int tail = (s->edgeHead + s->edgeCount) % CN1HW_EDGE_RING; diff --git a/docs/developer-guide/Embedded-Windows.asciidoc b/docs/developer-guide/Embedded-Windows.asciidoc index 262211f8b25..1bd2e3feb25 100644 --- a/docs/developer-guide/Embedded-Windows.asciidoc +++ b/docs/developer-guide/Embedded-Windows.asciidoc @@ -48,7 +48,7 @@ the CPU architecture, so an arm64 board needs no `windows.arch`. Each value can be changed on the device without a rebuild, through the matching environment variable: `CN1_ROTATION`, `CN1_DPI`, `CN1_MONITOR` and `CN1_CURSOR` -(`1` shows the pointer, `0` hides it). +(`true` or `1` shows the pointer, `false` or `0` hides it). === What the embedded build does @@ -76,7 +76,7 @@ include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[t The goal recognises a Windows executable, checks that its architecture matches the device and copies it to `C:\ProgramData\CodenameOne`. It then makes the -application the device's shell with *Shell Launcher v2*: it enables the feature +application the device's shell with *Shell Launcher*: it enables the feature if it's missing, restarts the application whenever it exits, and keeps Explorer as the shell for administrators, so the device can still be serviced. Enabling the feature takes a restart, which the goal performs. @@ -86,10 +86,10 @@ Options, as `cn1.device.*` properties: * `kioskUser` limits the kiosk shell to one account, `kiosk` for instance; other users keep Explorer. * `keyboardFilter=true` enables the Keyboard Filter and blocks Ctrl+Alt+Del, - Ctrl+Shift+Esc, Win+L and the Windows key. + Ctrl+Shift+Esc, Win+L, the Windows key, Alt+Tab, Ctrl+Esc and Alt+F4. * `kioskMode=autostart` replaces no shell and starts the application at logon - instead. Use it on a development device, or on an edition without Shell - Launcher. + instead, for every account, administrators included. Use it on a development + device, or on an edition without Shell Launcher. * `env` sets machine environment variables, `CN1_ROTATION=90` for instance. Windows reads them at logon, so they apply from the next one. diff --git a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java index b65a3a39885..0dee74056ce 100644 --- a/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java +++ b/maven/build-engine/src/main/java/com/codename1/maven/DeviceDeployer.java @@ -195,7 +195,7 @@ public void deploy() throws BuildException { throw new BuildException("No device host: set cn1.device.host (an address or an ~/.ssh/config alias)"); } if (binary == null || !binary.isFile()) { - throw new BuildException("No native Linux binary to deploy" + (binary == null ? "" : " at " + binary) + throw new BuildException("No native Linux or Windows binary to deploy" + (binary == null ? "" : " at " + binary) + ": build the linux-native target locally or set cn1.device.binary"); } if (isPe(binary)) { @@ -416,11 +416,14 @@ String windowsInstallScript(String name, String machine) { // create files, so inheritance is cut and the grants replaced. .append("icacls $dir /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F'" + " '*S-1-5-32-545:(OI)(CI)RX' | Out-Null\n") + // icacls is a native command: Stop does not see its failure. + .append("if ($LASTEXITCODE -ne 0) { throw \"icacls could not lock down $dir\" }\n") // The kiosk account is not an administrator and writes its log // here, the one place it can write. .append("$logs = Join-Path $dir 'logs'\n") .append("New-Item -ItemType Directory -Force -Path $logs | Out-Null\n") .append("icacls $logs /grant '*S-1-5-32-545:(OI)(CI)M' | Out-Null\n") + .append("if ($LASTEXITCODE -ne 0) { throw \"icacls could not open $logs to Users\" }\n") .append("Get-Process -Name $name -ErrorAction SilentlyContinue | Stop-Process -Force\n") .append("Start-Sleep -Milliseconds 300\n") .append("Move-Item -Force -LiteralPath $staged -Destination $exe\n") From fcac09f6dd561effe2d82d7221cc6b05f3043c09 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 21:18:31 +0200 Subject: [PATCH 5/6] Pin xwin to manifest 16 (SDK 22621); contract two hint descriptions for Vale Co-Authored-By: Claude Opus 5.5 --- .github/workflows/windows-cross-build-run.yml | 6 +++++- .github/workflows/windows-cross-compile.yml | 6 +++++- .../java/com/codename1/build/shared/BuildHintsDesktop.java | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/windows-cross-build-run.yml b/.github/workflows/windows-cross-build-run.yml index ca002958f13..e4da122c1da 100644 --- a/.github/workflows/windows-cross-build-run.yml +++ b/.github/workflows/windows-cross-build-run.yml @@ -171,6 +171,10 @@ jobs: # HTTP/1.1, which avoids the framing errors) and let cargo retry. CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" + # --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest + # lists Win11SDK_10.0.26100 without the per-arch header packages xwin + # 0.9.0 looks for, so the default splat fails with "unable to find + # x86_64 headers". Every target here builds against 22621. run: | set -e # Retry the whole install/splat a few times for non-cargo download @@ -178,7 +182,7 @@ jobs: n=0 until [ "$n" -ge 3 ]; do cargo install xwin --version 0.9.0 --locked \ - && xwin --accept-license --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ + && xwin --accept-license --manifest-version 16 --arch x86_64 splat --output "$RUNNER_TEMP/xwin-sdk" \ && break n=$((n+1)) echo "xwin install/splat attempt $n failed; retrying after backoff..." diff --git a/.github/workflows/windows-cross-compile.yml b/.github/workflows/windows-cross-compile.yml index 3cb445ebfc5..15c9aaf7c88 100644 --- a/.github/workflows/windows-cross-compile.yml +++ b/.github/workflows/windows-cross-compile.yml @@ -103,12 +103,16 @@ jobs: # force HTTP/1.1 and let cargo retry. CARGO_NET_RETRY: "10" CARGO_HTTP_MULTIPLEXING: "false" + # --manifest-version 16 (SDK 10.0.22621): the current VS 17 manifest + # lists Win11SDK_10.0.26100 without the per-arch header packages xwin + # 0.9.0 looks for, so the default splat fails with "unable to find + # x86_64 headers". Every target here builds against 22621. run: | set -e n=0 until [ "$n" -ge 3 ]; do cargo install xwin --version 0.9.0 --locked \ - && xwin --accept-license --arch x86_64,aarch64 splat --output "$RUNNER_TEMP/xwin-sdk" \ + && xwin --accept-license --manifest-version 16 --arch x86_64,aarch64 splat --output "$RUNNER_TEMP/xwin-sdk" \ && break n=$((n+1)) echo "xwin install/splat attempt $n failed; retrying after backoff..." diff --git a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java index 61b64b56625..5ff833def20 100644 --- a/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java +++ b/maven/build-hint-catalog/src/main/java/com/codename1/build/shared/BuildHintsDesktop.java @@ -503,7 +503,7 @@ static void register(List h) { + "device: one borderless, topmost window covering a whole monitor, the Windows key " + "and the shell's task switching shortcuts filtered, and text edited through an " + "on-screen keyboard. `cn1:deploy-device` installs it as the device's shell through " - + "Shell Launcher. Cannot be combined with `windows.xbox`.")); + + "Shell Launcher. Can't be combined with `windows.xbox`.")); h.add(new Hint("windows.embedded.board") .group(HintGroup.WINDOWS) @@ -537,7 +537,7 @@ static void register(List h) { .def("0") .platform("windows") .doc("Which monitor `windows.embedded` covers, in Windows' enumeration order; 0, or a " - + "monitor that does not exist, is the primary. `CN1_MONITOR` on the device " + + "monitor that doesn't exist, is the primary. `CN1_MONITOR` on the device " + "overrides it.")); h.add(new Hint("windows.embedded.cursor") From a0a216270d85ac5294840ba1f5ef3f06d55a8c43 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 10 Nov 2026 22:31:45 +0200 Subject: [PATCH 6/6] Windows kiosk: fix startup fail-fast in log redirect; classify the embedded editor dialog freopen_s opens without write sharing, so reopening stderr on the log path failed and setvbuf on the closed stream aborted with 0xC0000409. Co-Authored-By: Claude Opus 5.5 --- .../codename1/impl/EmbeddedTextEditor.java | 3 ++ .../nativeSources/cn1_windows_embedded.cpp | 34 ++++++++++++++----- .../developer-guide/Embedded-Windows.asciidoc | 2 +- .../ui/FrameworkChromeNeverWindowsTest.java | 1 + 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java index 3d77abf24cf..d5b0b5517e7 100644 --- a/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java +++ b/CodenameOne/src/com/codename1/impl/EmbeddedTextEditor.java @@ -153,6 +153,9 @@ public void actionPerformed(ActionEvent evt) { } } }); + // Docked to the bottom edge by showPacked below: framework chrome, never an + // operating system window of its own, whatever the theme defaults to. + dlg.setNativeWindowMode(false); dlg.setDisposeWhenPointerOutOfBounds(false); dlg.setScrollable(false); dlg.getContentPane().setPreferredH(Display.getInstance().getDisplayHeight() / 2); diff --git a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp index 0a8a4d5a4cb..0337611d58e 100644 --- a/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp +++ b/Ports/WindowsPort/nativeSources/cn1_windows_embedded.cpp @@ -227,15 +227,31 @@ void cn1WinEmbeddedRedirectLog(void) { if (n == 0 || n >= sizeof(path)) { return; } - FILE* f = NULL; - freopen_s(&f, path, "a", stdout); - f = NULL; - freopen_s(&f, path, "a", stderr); - /* Unbuffered: the Microsoft CRT treats _IOLBF as full buffering, so a line - * buffer would reach cn1:device-logs a kilobyte at a time and lose the last - * output before a crash. */ - setvbuf(stdout, NULL, _IONBF, 0); - setvbuf(stderr, NULL, _IONBF, 0); + /* Plain freopen, not freopen_s: the _s variant opens the file without write + * sharing, so the second reopen of the same path failed, left stderr closed, + * and the setvbuf below then took the CRT's invalid-parameter fail-fast + * (0xC0000409) before the window existed. setvbuf only touches a stream + * that reopened. Unbuffered: the Microsoft CRT treats + * _IOLBF as full buffering, so a line buffer would reach cn1:device-logs a + * kilobyte at a time and lose the last output before a crash. */ +#pragma warning(suppress: 4996) + if (freopen(path, "a", stdout) != NULL) { + setvbuf(stdout, NULL, _IONBF, 0); + } else { + /* freopen closed the stream before failing; a later write to a closed + * stream is the same fail-fast, so park it on NUL instead. */ +#pragma warning(suppress: 4996) + freopen("NUL", "w", stdout); + } +#pragma warning(suppress: 4996) + if (freopen(path, "a", stderr) != NULL) { + setvbuf(stderr, NULL, _IONBF, 0); + } else { + /* freopen closed the stream before failing; a later write to a closed + * stream is the same fail-fast, so park it on NUL instead. */ +#pragma warning(suppress: 4996) + freopen("NUL", "w", stderr); + } } #else /* a desktop or UWP build */ diff --git a/docs/developer-guide/Embedded-Windows.asciidoc b/docs/developer-guide/Embedded-Windows.asciidoc index 1bd2e3feb25..5503e28a738 100644 --- a/docs/developer-guide/Embedded-Windows.asciidoc +++ b/docs/developer-guide/Embedded-Windows.asciidoc @@ -74,7 +74,7 @@ so its key is accepted: include::../demos/common/src/main/snippets/developer-guide/embedded-windows.sh[tag=embedded-windows-bash-001,indent=0] ---- -The goal recognises a Windows executable, checks that its architecture matches +The goal recognizes a Windows executable, checks that its architecture matches the device and copies it to `C:\ProgramData\CodenameOne`. It then makes the application the device's shell with *Shell Launcher*: it enables the feature if it's missing, restarts the application whenever it exits, and keeps Explorer diff --git a/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java b/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java index f002cae6be3..01e95ae5bd8 100644 --- a/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java +++ b/maven/core-unittests/src/test/java/com/codename1/ui/FrameworkChromeNeverWindowsTest.java @@ -87,6 +87,7 @@ public class FrameworkChromeNeverWindowsTest extends UITestBase { "com/codename1/ui/validation/Validator.java", "com/codename1/components/FloatingActionButton.java", "com/codename1/components/InfiniteProgress.java", + "com/codename1/impl/EmbeddedTextEditor.java", }; /** Every construction of a dialog that would take the default if left alone. */