`, or `` elements), _notifications messages_, and option _label_ strings, etc.
+
+We **do not** translate the console log messages, CSS class names, theme names, nor the various _value_ strings (e.g. for value/label pair options), nor URL routes.
+
+The localizations are stored in _.json_ files in the `ui/localizations/messages` directory, with one language-per-file using the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (e.g. en for English, de for German, etc.)
+
+#### m-function-matcher
+
+The translations are extracted into language files (e.g. _en.json_ for English) and then paraglide-js compiles them into helpers for use with the [m-function-matcher](https://inlang.com/m/632iow21/plugin-inlang-mFunctionMatcher). An example:
+
+```tsx
+
+```
+
+#### shakespere plug-in
+
+If you enable the [Sherlock](https://inlang.com/m/r7kp499g/app-inlang-ideExtension) plug-in, the localized text "tooltip" is shown in the VSCode editor after any localized text in the language you've selected for preview. In this image, it's the blue text at the end of the line :
+
+
+
+#### Process
+
+##### Localizing a UI
+
+1. Locate a string that is visible to the end user on the client/browser
+2. Assign that string a "key" that reflects the logical meaning of the string in snake-case (look at existing localizations for examples), for example if there's a string `This is a test` on the _thing edit page_ it would be "thing_edit_this_is_a_test"
+
+ ```json
+ "thing_edit_this_is_a_test": "This is a test",
+ ```
+
+3. Add the key and string to the _en.json_ like this:
+
+ - **Note** if the string has replacement parameters (line a user-entered name), the syntax for the localized string has `{ }` around the replacement token (e.g. _This is your name: {name}_). An complex example:
+
+ ```react
+ {m.mount_button_showing_results({
+ from: indexOfFirstFile + 1,
+ to: Math.min(indexOfLastFile, onStorageFiles.length),
+ total: onStorageFiles.length
+ })}
+ ```
+
+4. Save the _en.json_ file and execute `npm run i18n` to resort the language files, validate the translations, and create the m-functions
+5. Edit the _.tsx_ file and replace the string with the calls to the new m-function which will be the key-string you chose in snake-case. For example `This is a test` in _thing edit page_ turns into `m.thing_edit_this_is_a_test()`
+ - **Note** if the string has a replacement token, supply that to the m-function, for example for the literal `I will call you {name}`, use `m.profile_i_will_call_you({ name: edit.value })`
+6. When all your strings are extracted, run `npm run i18n:machine-translate` to get a first-stab at the translations for the other supported languages. Make sure you use an LLM (you can use [aifiesta](https://chat.aifiesta.ai/chat/) to use multiple LLMs) or a [translator](https://translate.google.com) of some form to back-translate each **new** machine-generation in each _language_ to ensure those terms translate reasonably.
+
+### Adding a new language
+
+1. Get the [ISO 3166-1 alpha-2 country code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (for example AT for Austria)
+2. Create a new file in the _ui/localization/messages_ directory (example _at.json_)
+3. Add the new country code to the _ui/localizations/settings.json_ file in both the `"locales"` and the `"languageTags"` section (inlang and Sherlock aren't exactly current to each other, so we need it in both places).
+4. That file also declares the baseLocale/sourceLanguageTag which is `"en"` because this project started out in English. Do NOT change that.
+5. Run `npm run i18n:machine-translate` to do an initial pass at localizing all existing messages to the new language.
+ - **Note** you will get an error _DB has been closed_, ignore that message, we're not using a database.
+ - **Note** you likely will get errors while running this command due to rate limits and such (it uses anonymous Google Translate). Just keep running the command over and over... it'll translate a bunch each time until it says _Machine translate complete_
+
+### Other notes
+
+- Run `npm run i18n:validate` to ensure that language files and settings are well-formed.
+- Run `npm run i18n:find-excess` to look for extra keys in other language files that have been deleted from the master-list in _en.json_.
+- Run `npm run i18n:find-dupes` to look for multiple keys in _en.json_ that have the same translated value (this is normal)
+- Run `npm run i18n:find-unused` to look for keys in _en.json_ that are not referenced in the UI anywhere.
+ - **Note** there are a few that are not currently used, only concern yourself with ones you obsoleted.
+- Run `npm run i18n:audit` to do all the above checks.
+- Using [inlang CLI](https://inlang.com/m/2qj2w8pu/app-inlang-cli) to support the npm commands.
+- You can install the [Sherlock VS Code extension](https://marketplace.visualstudio.com/items?itemName=inlang.vs-code-extension) in your devcontainer.
---
diff --git a/config.go b/config.go
index 36df92da..5a3e7dc8 100644
--- a/config.go
+++ b/config.go
@@ -106,6 +106,7 @@ type Config struct {
NetworkConfig *types.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
+ VideoQualityFactor float64 `json:"video_quality_factor"`
}
func (c *Config) GetDisplayRotation() uint16 {
@@ -176,7 +177,8 @@ func getDefaultConfig() Config {
_ = confparser.SetDefaultsAndValidate(c)
return c
}(),
- DefaultLogLevel: "INFO",
+ DefaultLogLevel: "INFO",
+ VideoQualityFactor: 1.0,
}
}
diff --git a/display.go b/display.go
index 042bf122..68723b59 100644
--- a/display.go
+++ b/display.go
@@ -305,11 +305,11 @@ func wakeDisplay(force bool, reason string) {
displayLogger.Warn().Err(err).Msg("failed to wake display")
}
- if config.DisplayDimAfterSec != 0 {
+ if config.DisplayDimAfterSec != 0 && dimTicker != nil {
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
}
- if config.DisplayOffAfterSec != 0 {
+ if config.DisplayOffAfterSec != 0 && offTicker != nil {
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
}
backlightState = 0
diff --git a/hw.go b/hw.go
index 20d88ebf..7797adc1 100644
--- a/hw.go
+++ b/hw.go
@@ -3,6 +3,7 @@ package kvm
import (
"fmt"
"os"
+ "os/exec"
"regexp"
"strings"
"sync"
@@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil
}
+func hwReboot(force bool, postRebootAction *PostRebootAction, delay time.Duration) error {
+ logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay)
+
+ writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
+ time.Sleep(1 * time.Second) // Wait for the JSONRPCEvent to be sent
+
+ nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
+ time.Sleep(delay - (1 * time.Second)) // wait requested extra settle time
+
+ args := []string{}
+ if force {
+ args = append(args, "-f")
+ }
+
+ cmd := exec.Command("reboot", args...)
+ err := cmd.Start()
+ if err != nil {
+ logger.Error().Err(err).Msg("failed to reboot")
+ switchToMainScreen()
+ return fmt.Errorf("failed to reboot: %w", err)
+ }
+
+ // If the reboot command is successful, exit the program after 5 seconds
+ go func() {
+ time.Sleep(5 * time.Second)
+ os.Exit(0)
+ }()
+
+ return nil
+}
+
var deviceID string
var deviceIDOnce sync.Once
diff --git a/internal/native/cgo/ctrl.c b/internal/native/cgo/ctrl.c
index dd285859..547d5694 100644
--- a/internal/native/cgo/ctrl.c
+++ b/internal/native/cgo/ctrl.c
@@ -306,7 +306,7 @@ int jetkvm_ui_add_flag(const char *obj_name, const char *flag_name) {
if (obj == NULL) {
return -1;
}
-
+
lv_obj_flag_t flag_val = str_to_lv_obj_flag(flag_name);
if (flag_val == 0)
{
@@ -368,7 +368,7 @@ void jetkvm_video_stop() {
}
int jetkvm_video_set_quality_factor(float quality_factor) {
- if (quality_factor < 0 || quality_factor > 1) {
+ if (quality_factor <= 0 || quality_factor > 1) {
return -1;
}
video_set_quality_factor(quality_factor);
@@ -405,8 +405,8 @@ char *jetkvm_video_log_status() {
return (char *)videoc_log_status();
}
-int jetkvm_video_init() {
- return video_init();
+int jetkvm_video_init(float factor) {
+ return video_init(factor);
}
void jetkvm_video_shutdown() {
@@ -417,4 +417,4 @@ void jetkvm_crash() {
// let's call a function that will crash the program
int* p = 0;
*p = 0;
-}
\ No newline at end of file
+}
diff --git a/internal/native/cgo/ctrl.h b/internal/native/cgo/ctrl.h
index 430e4c21..774ee147 100644
--- a/internal/native/cgo/ctrl.h
+++ b/internal/native/cgo/ctrl.h
@@ -52,7 +52,7 @@ const char *jetkvm_ui_get_lvgl_version();
const char *jetkvm_ui_event_code_to_name(int code);
-int jetkvm_video_init();
+int jetkvm_video_init(float quality_factor);
void jetkvm_video_shutdown();
void jetkvm_video_start();
void jetkvm_video_stop();
diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c
index 22fa378b..857acbbb 100644
--- a/internal/native/cgo/video.c
+++ b/internal/native/cgo/video.c
@@ -29,6 +29,7 @@
#define VIDEO_DEV "/dev/video0"
#define SUB_DEV "/dev/v4l-subdev2"
+#define SLEEP_MODE_FILE "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
@@ -39,6 +40,7 @@ int sub_dev_fd = -1;
#define VENC_CHANNEL 0
MB_POOL memPool = MB_INVALID_POOLID;
+bool sleep_mode_available = false;
bool should_exit = false;
float quality_factor = 1.0f;
@@ -51,6 +53,45 @@ RK_U64 get_us()
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
}
+static void ensure_sleep_mode_disabled()
+{
+ if (!sleep_mode_available)
+ {
+ return;
+ }
+
+ int fd = open(SLEEP_MODE_FILE, O_RDWR);
+ if (fd < 0)
+ {
+ log_error("Failed to open sleep mode file: %s", strerror(errno));
+ return;
+ }
+ lseek(fd, 0, SEEK_SET);
+ char buffer[1];
+ read(fd, buffer, 1);
+ if (buffer[0] == '0') {
+ close(fd);
+ return;
+ }
+ log_warn("HDMI sleep mode is not disabled, disabling it");
+ lseek(fd, 0, SEEK_SET);
+ write(fd, "0", 1);
+ close(fd);
+
+ usleep(1000); // give some time to the system to disable the sleep mode
+ return;
+}
+
+static void detect_sleep_mode()
+{
+ if (access(SLEEP_MODE_FILE, F_OK) != 0) {
+ sleep_mode_available = false;
+ return;
+ }
+ sleep_mode_available = true;
+ ensure_sleep_mode_disabled();
+}
+
double calculate_bitrate(float bitrate_factor, int width, int height)
{
const int32_t base_bitrate_high = 2000;
@@ -190,8 +231,15 @@ static int32_t buf_init()
pthread_t *format_thread = NULL;
-int video_init()
+int video_init(float factor)
{
+ detect_sleep_mode();
+
+ if (factor <= 0 || factor > 1) {
+ factor = 1.0f;
+ }
+ quality_factor = factor;
+
if (RK_MPI_SYS_Init() != RK_SUCCESS)
{
log_error("RK_MPI_SYS_Init failed");
@@ -301,11 +349,29 @@ static void *venc_read_stream(void *arg)
}
uint32_t detected_width, detected_height;
-bool detected_signal = false, streaming_flag = false;
+bool detected_signal = false, streaming_flag = false, streaming_stopped = true;
pthread_t *streaming_thread = NULL;
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
+bool get_streaming_flag()
+{
+ log_info("getting streaming flag");
+ pthread_mutex_lock(&streaming_mutex);
+ bool flag = streaming_flag;
+ pthread_mutex_unlock(&streaming_mutex);
+ return flag;
+}
+
+void set_streaming_flag(bool flag)
+{
+ log_info("setting streaming flag to %d", flag);
+
+ pthread_mutex_lock(&streaming_mutex);
+ streaming_flag = flag;
+ pthread_mutex_unlock(&streaming_mutex);
+}
+
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
{
FILE *file = fopen(filename, "wb");
@@ -319,6 +385,8 @@ void *run_video_stream(void *arg)
log_info("running video stream");
+ streaming_stopped = false;
+
while (streaming_flag)
{
if (detected_signal == false)
@@ -401,7 +469,7 @@ void *run_video_stream(void *arg)
{
log_error("get mb blk failed!");
close(video_dev_fd);
- return ;
+ return (void *)errno;
}
log_info("Got memory block for buffer %d", i);
@@ -538,6 +606,18 @@ void *run_video_stream(void *arg)
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
}
+ // Explicitly free V4L2 buffer queue
+ struct v4l2_requestbuffers req_free;
+ memset(&req_free, 0, sizeof(req_free));
+ req_free.count = 0;
+ req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
+ req_free.memory = V4L2_MEMORY_DMABUF;
+
+ if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
+ {
+ log_error("Failed to free V4L2 buffers: %s", strerror(errno));
+ }
+
venc_stop();
for (int i = 0; i < input_buffer_count; i++)
@@ -553,6 +633,9 @@ void *run_video_stream(void *arg)
}
log_info("video stream thread exiting");
+
+ streaming_stopped = true;
+
return NULL;
}
@@ -577,61 +660,80 @@ void video_shutdown()
RK_MPI_MB_DestroyPool(memPool);
}
log_info("Destroyed memory pool");
-
+
pthread_mutex_destroy(&streaming_mutex);
log_info("Destroyed streaming mutex");
}
-
void video_start_streaming()
{
- pthread_mutex_lock(&streaming_mutex);
+ log_info("starting video streaming");
if (streaming_thread != NULL)
{
+ if (streaming_stopped == true) {
+ log_error("video streaming already stopped but streaming_thread is not NULL");
+ assert(streaming_stopped == true);
+ }
log_warn("video streaming already started");
- goto cleanup;
+ return;
}
-
+
pthread_t *new_thread = malloc(sizeof(pthread_t));
if (new_thread == NULL)
{
log_error("Failed to allocate memory for streaming thread");
- goto cleanup;
+ return;
}
-
- streaming_flag = true;
+
+ set_streaming_flag(true);
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
if (result != 0)
{
log_error("Failed to create streaming thread: %s", strerror(result));
- streaming_flag = false;
+ set_streaming_flag(false);
free(new_thread);
- goto cleanup;
+ return;
}
-
- // Only set streaming_thread after successful creation, and before unlocking the mutex
+
+ // Only set streaming_thread after successful creation
streaming_thread = new_thread;
-cleanup:
- pthread_mutex_unlock(&streaming_mutex);
- return;
}
void video_stop_streaming()
{
- pthread_mutex_lock(&streaming_mutex);
- if (streaming_thread != NULL)
- {
- streaming_flag = false;
- log_info("stopping video streaming");
- // wait 100ms for the thread to exit
- usleep(1000000);
- log_info("waiting for video streaming thread to exit");
- pthread_join(*streaming_thread, NULL);
- free(streaming_thread);
- streaming_thread = NULL;
- log_info("video streaming stopped");
+ if (streaming_thread == NULL) {
+ log_info("video streaming already stopped");
+ return;
}
- pthread_mutex_unlock(&streaming_mutex);
+
+ log_info("stopping video streaming");
+ set_streaming_flag(false);
+
+ log_info("waiting for video streaming thread to exit");
+ int attempts = 0;
+ while (!streaming_stopped && attempts < 30) {
+ usleep(100000); // 100ms
+ attempts++;
+ }
+ if (!streaming_stopped) {
+ log_error("video streaming thread did not exit after 30s");
+ }
+
+ pthread_join(*streaming_thread, NULL);
+ free(streaming_thread);
+ streaming_thread = NULL;
+
+ log_info("video streaming stopped");
+}
+
+void video_restart_streaming()
+{
+ if (get_streaming_flag() == true)
+ {
+ log_info("restarting video streaming");
+ video_stop_streaming();
+ }
+ video_start_streaming();
}
void *run_detect_format(void *arg)
@@ -650,6 +752,8 @@ void *run_detect_format(void *arg)
while (!should_exit)
{
+ ensure_sleep_mode_disabled();
+
memset(&dv_timings, 0, sizeof(dv_timings));
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
{
@@ -689,21 +793,17 @@ void *run_detect_format(void *arg)
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
dv_timings.bt.hbackporch));
log_info("Frames per second: %.2f fps", frames_per_second);
+
+ bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal;
+
detected_width = dv_timings.bt.width;
detected_height = dv_timings.bt.height;
detected_signal = true;
video_report_format(true, NULL, detected_width, detected_height, frames_per_second);
- pthread_mutex_lock(&streaming_mutex);
- if (streaming_flag == true)
- {
- pthread_mutex_unlock(&streaming_mutex);
- log_info("restarting on going video streaming");
- video_stop_streaming();
- video_start_streaming();
- }
- else
- {
- pthread_mutex_unlock(&streaming_mutex);
+
+ if (should_restart) {
+ log_info("restarting video streaming due to format change");
+ video_restart_streaming();
}
}
@@ -731,21 +831,9 @@ void video_set_quality_factor(float factor)
quality_factor = factor;
// TODO: update venc bitrate without stopping streaming
-
- pthread_mutex_lock(&streaming_mutex);
- if (streaming_flag == true)
- {
- pthread_mutex_unlock(&streaming_mutex);
- log_info("restarting on going video streaming due to quality factor change");
- video_stop_streaming();
- video_start_streaming();
- }
- else
- {
- pthread_mutex_unlock(&streaming_mutex);
- }
+ video_restart_streaming();
}
float video_get_quality_factor() {
return quality_factor;
-}
\ No newline at end of file
+}
diff --git a/internal/native/cgo/video.h b/internal/native/cgo/video.h
index e9309be4..6fa00ca4 100644
--- a/internal/native/cgo/video.h
+++ b/internal/native/cgo/video.h
@@ -6,7 +6,7 @@
*
* @return int 0 on success, -1 on failure
*/
-int video_init();
+int video_init(float quality_factor);
/**
* @brief Shutdown the video subsystem
diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go
index 8cd6d489..850da0e8 100644
--- a/internal/native/cgo_linux.go
+++ b/internal/native/cgo_linux.go
@@ -129,11 +129,13 @@ func uiTick() {
C.jetkvm_ui_tick()
}
-func videoInit() error {
+func videoInit(factor float64) error {
cgoLock.Lock()
defer cgoLock.Unlock()
- ret := C.jetkvm_video_init()
+ factorC := C.float(factor)
+
+ ret := C.jetkvm_video_init(factorC)
if ret != 0 {
return fmt.Errorf("failed to initialize video: %d", ret)
}
diff --git a/internal/native/native.go b/internal/native/native.go
index b89b37a3..3b1cc0b4 100644
--- a/internal/native/native.go
+++ b/internal/native/native.go
@@ -15,6 +15,7 @@ type Native struct {
systemVersion *semver.Version
appVersion *semver.Version
displayRotation uint16
+ defaultQualityFactor float64
onVideoStateChange func(state VideoState)
onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string)
@@ -22,12 +23,14 @@ type Native struct {
sleepModeSupported bool
videoLock sync.Mutex
screenLock sync.Mutex
+ extraLock sync.Mutex
}
type NativeOptions struct {
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
+ DefaultQualityFactor float64
OnVideoStateChange func(state VideoState)
OnVideoFrameReceived func(frame []byte, duration time.Duration)
OnIndevEvent func(event string)
@@ -65,6 +68,11 @@ func NewNative(opts NativeOptions) *Native {
sleepModeSupported := isSleepModeSupported()
+ defaultQualityFactor := opts.DefaultQualityFactor
+ if defaultQualityFactor <= 0 || defaultQualityFactor > 1 {
+ defaultQualityFactor = 1.0
+ }
+
return &Native{
ready: make(chan struct{}),
l: nativeLogger,
@@ -72,6 +80,7 @@ func NewNative(opts NativeOptions) *Native {
systemVersion: opts.SystemVersion,
appVersion: opts.AppVersion,
displayRotation: opts.DisplayRotation,
+ defaultQualityFactor: defaultQualityFactor,
onVideoStateChange: onVideoStateChange,
onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent,
@@ -97,7 +106,7 @@ func (n *Native) Start() {
n.initUI()
go n.tickUI()
- if err := videoInit(); err != nil {
+ if err := videoInit(n.defaultQualityFactor); err != nil {
n.l.Error().Err(err).Msg("failed to initialize video")
}
diff --git a/internal/native/video.go b/internal/native/video.go
index d5008756..c556a938 100644
--- a/internal/native/video.go
+++ b/internal/native/video.go
@@ -1,11 +1,18 @@
package native
import (
+ "fmt"
"os"
+ "time"
)
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
+// DefaultEDID is the default EDID for the video stream.
+const DefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
+
+var extraLockTimeout = 5 * time.Second
+
// VideoState is the state of the video stream.
type VideoState struct {
Ready bool `json:"ready"`
@@ -66,12 +73,32 @@ func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported
}
+// useExtraLock uses the extra lock to execute a function.
+// if the lock is currently held by another goroutine, returns an error.
+//
+// it's used to ensure that only one change is made to the video stream at a time.
+// as the change usually requires to restart video streaming
+// TODO: check video streaming status instead of using a hardcoded timeout
+func (n *Native) useExtraLock(fn func() error) error {
+ if !n.extraLock.TryLock() {
+ return fmt.Errorf("the previous change hasn't been completed yet")
+ }
+ err := fn()
+ if err == nil {
+ time.Sleep(extraLockTimeout)
+ }
+ n.extraLock.Unlock()
+ return err
+}
+
// VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
- return videoSetStreamQualityFactor(factor)
+ return n.useExtraLock(func() error {
+ return videoSetStreamQualityFactor(factor)
+ })
}
// VideoGetQualityFactor gets the quality factor for the video stream.
@@ -87,7 +114,13 @@ func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
- return videoSetEDID(edid)
+ if edid == "" {
+ edid = DefaultEDID
+ }
+
+ return n.useExtraLock(func() error {
+ return videoSetEDID(edid)
+ })
}
// VideoGetEDID gets the EDID for the video stream.
diff --git a/jsonrpc.go b/jsonrpc.go
index 0492a8ac..d51dec3c 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -171,40 +171,12 @@ func rpcGetDeviceID() (string, error) {
}
func rpcReboot(force bool) error {
- logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
-
- writeJSONRPCEvent("willReboot", nil, currentSession)
-
- // Wait for the JSONRPCEvent to be sent
- time.Sleep(1 * time.Second)
- nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
-
- args := []string{}
- if force {
- args = append(args, "-f")
- }
-
- cmd := exec.Command("reboot", args...)
- err := cmd.Start()
- if err != nil {
- logger.Error().Err(err).Msg("failed to reboot")
- switchToMainScreen()
- return fmt.Errorf("failed to reboot: %w", err)
- }
-
- // If the reboot command is successful, exit the program after 5 seconds
- go func() {
- time.Sleep(5 * time.Second)
- os.Exit(0)
- }()
-
- return nil
+ logger.Info().Msg("Got reboot request via RPC")
+ return hwReboot(force, nil, 0)
}
-var streamFactor = 1.0
-
func rpcGetStreamQualityFactor() (float64, error) {
- return streamFactor, nil
+ return config.VideoQualityFactor, nil
}
func rpcSetStreamQualityFactor(factor float64) error {
@@ -214,7 +186,10 @@ func rpcSetStreamQualityFactor(factor float64) error {
return err
}
- streamFactor = factor
+ config.VideoQualityFactor = factor
+ if err := SaveConfig(); err != nil {
+ return fmt.Errorf("failed to save config: %w", err)
+ }
return nil
}
@@ -241,7 +216,6 @@ func rpcGetEDID() (string, error) {
func rpcSetEDID(edid string) error {
if edid == "" {
logger.Info().Msg("Restoring EDID to default")
- edid = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"
} else {
logger.Info().Str("edid", edid).Msg("Setting EDID")
}
diff --git a/main.go b/main.go
index 2648b68d..bcc2d73d 100644
--- a/main.go
+++ b/main.go
@@ -14,6 +14,7 @@ import (
var appCtx context.Context
func Main() {
+ logger.Log().Msg("JetKVM Starting Up")
LoadConfig()
var cancel context.CancelFunc
@@ -79,16 +80,16 @@ func Main() {
startVideoSleepModeTicker()
go func() {
+ // wait for 15 minutes before starting auto-update checks
+ // this is to avoid interfering with initial setup processes
+ // and to ensure the system is stable before checking for updates
time.Sleep(15 * time.Minute)
- for {
- logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
- if !config.AutoUpdateEnabled {
- return
- }
- if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
- logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
- time.Sleep(30 * time.Second)
+ for {
+ logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
+ if !config.AutoUpdateEnabled {
+ logger.Debug().Msg("auto-update disabled")
+ time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
continue
}
@@ -98,6 +99,12 @@ func Main() {
continue
}
+ if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
+ logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
+ time.Sleep(30 * time.Second)
+ continue
+ }
+
includePreRelease := config.IncludePreRelease
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
if err != nil {
@@ -107,6 +114,7 @@ func Main() {
time.Sleep(1 * time.Hour)
}
}()
+
//go RunFuseServer()
go RunWebServer()
@@ -123,7 +131,8 @@ func Main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
- logger.Info().Msg("JetKVM Shutting Down")
+
+ logger.Log().Msg("JetKVM Shutting Down")
//if fuseServer != nil {
// err := setMassStorageImage(" ")
// if err != nil {
diff --git a/native.go b/native.go
index 4268bf2c..4a523bce 100644
--- a/native.go
+++ b/native.go
@@ -17,9 +17,10 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{
- SystemVersion: systemVersion,
- AppVersion: appVersion,
- DisplayRotation: config.GetDisplayRotation(),
+ SystemVersion: systemVersion,
+ AppVersion: appVersion,
+ DisplayRotation: config.GetDisplayRotation(),
+ DefaultQualityFactor: config.VideoQualityFactor,
OnVideoStateChange: func(state native.VideoState) {
lastVideoState = state
triggerVideoStateUpdate()
@@ -36,14 +37,17 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeLogger.Trace().Str("event", event).Msg("rpc event received")
switch event {
case "resetConfig":
+ nativeLogger.Info().Msg("Reset configuration request via native rpc event")
err := rpcResetConfig()
if err != nil {
nativeLogger.Warn().Err(err).Msg("error resetting config")
}
_ = rpcReboot(true)
case "reboot":
+ nativeLogger.Info().Msg("Reboot request via native rpc event")
_ = rpcReboot(true)
case "toggleDHCPClient":
+ nativeLogger.Info().Msg("Toggle DHCP request via native rpc event")
_ = rpcToggleDHCPClient()
default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")
@@ -58,7 +62,13 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
}
},
})
+
nativeInstance.Start()
+ go func() {
+ if err := nativeInstance.VideoSetEDID(config.EdidString); err != nil {
+ nativeLogger.Warn().Err(err).Msg("error setting EDID")
+ }
+ }()
if os.Getenv("JETKVM_CRASH_TESTING") == "1" {
nativeInstance.DoNotUseThisIsForCrashTestingOnly()
diff --git a/network.go b/network.go
index ff071460..846f41f1 100644
--- a/network.go
+++ b/network.go
@@ -29,7 +29,7 @@ func (s *RpcNetworkSettings) ToNetworkConfig() *types.NetworkConfig {
type PostRebootAction struct {
HealthCheck string `json:"healthCheck"`
- RedirectUrl string `json:"redirectUrl"`
+ RedirectTo string `json:"redirectTo"`
}
func toRpcNetworkSettings(config *types.NetworkConfig) *RpcNetworkSettings {
@@ -193,6 +193,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String
+
// IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode {
rebootRequired = true
@@ -201,7 +202,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
if newIPv4Mode == "static" && oldIPv4Mode != "static" {
postRebootAction = &PostRebootAction{
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
- RedirectUrl: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
+ RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
}
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 mode changed to static, reboot required")
}
@@ -218,7 +219,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String {
postRebootAction = &PostRebootAction{
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
- RedirectUrl: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
+ RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
}
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 static config changed, reboot required")
@@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
}
if rebootRequired {
- if err := rpcReboot(false); err != nil {
+ l.Info().Msg("Rebooting due to network changes")
+ if err := hwReboot(true, postRebootAction, 0); err != nil {
return nil, err
}
}
diff --git a/ota.go b/ota.go
index 7063c7ff..5371e428 100644
--- a/ota.go
+++ b/ota.go
@@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
if nr > 0 {
nw, ew := file.Write(buf[0:nr])
if nw < nr {
- return fmt.Errorf("short write: %d < %d", nw, nr)
+ return fmt.Errorf("short file write: %d < %d", nw, nr)
}
written += int64(nw)
if ew != nil {
@@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
if nr > 0 {
nw, ew := hash.Write(buf[0:nr])
if nw < nr {
- return fmt.Errorf("short write: %d < %d", nw, nr)
+ return fmt.Errorf("short hash write: %d < %d", nw, nr)
}
verified += int64(nw)
if ew != nil {
@@ -260,11 +260,16 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
}
}
- hashSum := hash.Sum(nil)
- scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
+ // close the file so we can rename below
+ if err := fileToHash.Close(); err != nil {
+ return fmt.Errorf("error closing file: %w", err)
+ }
- if hex.EncodeToString(hashSum) != expectedHash {
- return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
+ hashSum := hex.EncodeToString(hash.Sum(nil))
+ scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of")
+
+ if hashSum != expectedHash {
+ return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash)
}
if err := os.Rename(unverifiedPath, path); err != nil {
@@ -313,7 +318,7 @@ func triggerOTAStateUpdate() {
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
scopedLogger := otaLogger.With().
Str("deviceId", deviceId).
- Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
+ Bool("includePreRelease", includePreRelease).
Logger()
scopedLogger.Info().Msg("Trying to update...")
@@ -362,8 +367,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading app update")
triggerOTAStateUpdate()
- return err
+ return fmt.Errorf("error downloading app update: %w", err)
}
+
downloadFinished := time.Now()
otaState.AppDownloadFinishedAt = &downloadFinished
otaState.AppDownloadProgress = 1
@@ -379,17 +385,21 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
triggerOTAStateUpdate()
- return err
+ return fmt.Errorf("error verifying app update: %w", err)
}
+
verifyFinished := time.Now()
otaState.AppVerifiedAt = &verifyFinished
otaState.AppVerificationProgress = 1
+ triggerOTAStateUpdate()
+
otaState.AppUpdatedAt = &verifyFinished
otaState.AppUpdateProgress = 1
triggerOTAStateUpdate()
scopedLogger.Info().Msg("App update downloaded")
rebootNeeded = true
+ triggerOTAStateUpdate()
} else {
scopedLogger.Info().Msg("App is up to date")
}
@@ -405,8 +415,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
scopedLogger.Error().Err(err).Msg("Error downloading system update")
triggerOTAStateUpdate()
- return err
+ return fmt.Errorf("error downloading system update: %w", err)
}
+
downloadFinished := time.Now()
otaState.SystemDownloadFinishedAt = &downloadFinished
otaState.SystemDownloadProgress = 1
@@ -422,8 +433,9 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
triggerOTAStateUpdate()
- return err
+ return fmt.Errorf("error verifying system update: %w", err)
}
+
scopedLogger.Info().Msg("System update downloaded")
verifyFinished := time.Now()
otaState.SystemVerifiedAt = &verifyFinished
@@ -439,8 +451,10 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if err != nil {
otaState.Error = fmt.Sprintf("Error starting rk_ota command: %v", err)
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
+ triggerOTAStateUpdate()
return fmt.Errorf("error starting rk_ota command: %w", err)
}
+
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -475,37 +489,42 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
Str("output", output).
Int("exitCode", cmd.ProcessState.ExitCode()).
Msg("Error executing rk_ota command")
+ triggerOTAStateUpdate()
return fmt.Errorf("error executing rk_ota command: %w\nOutput: %s", err, output)
}
+
scopedLogger.Info().Str("output", output).Msg("rk_ota success")
otaState.SystemUpdateProgress = 1
otaState.SystemUpdatedAt = &verifyFinished
- triggerOTAStateUpdate()
rebootNeeded = true
+ triggerOTAStateUpdate()
} else {
scopedLogger.Info().Msg("System is up to date")
}
if rebootNeeded {
- scopedLogger.Info().Msg("System Rebooting in 10s")
+ scopedLogger.Info().Msg("System Rebooting due to OTA update")
- // TODO: Future enhancement - send postRebootAction to redirect to release notes
- // Example:
- // postRebootAction := &PostRebootAction{
- // HealthCheck: "[..]/device/status",
- // RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
- // }
- // writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
+ // Build redirect URL with conditional query parameters
+ redirectTo := "/settings/general/update"
+ queryParams := url.Values{}
+ if systemUpdateAvailable {
+ queryParams.Set("systemVersion", remote.SystemVersion)
+ }
+ if appUpdateAvailable {
+ queryParams.Set("appVersion", remote.AppVersion)
+ }
+ if len(queryParams) > 0 {
+ redirectTo += "?" + queryParams.Encode()
+ }
- time.Sleep(10 * time.Second)
- cmd := exec.Command("reboot")
- err := cmd.Start()
- if err != nil {
- otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err)
- scopedLogger.Error().Err(err).Msg("Failed to start reboot")
- return fmt.Errorf("failed to start reboot: %w", err)
- } else {
- os.Exit(0)
+ postRebootAction := &PostRebootAction{
+ HealthCheck: "/device/status",
+ RedirectTo: redirectTo,
+ }
+
+ if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil {
+ return fmt.Errorf("error requesting reboot: %w", err)
}
}
diff --git a/pkg/nmlite/jetdhcpc/client.go b/pkg/nmlite/jetdhcpc/client.go
index 155ea249..102d3bee 100644
--- a/pkg/nmlite/jetdhcpc/client.go
+++ b/pkg/nmlite/jetdhcpc/client.go
@@ -111,6 +111,7 @@ type Client struct {
var (
defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second
+ defaultDHCPTimeout = 5 * time.Second // DHCP request timeout (not link up timeout)
maxRenewalAttemptDuration = 2 * time.Hour
)
@@ -125,11 +126,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
}
if cfg.Timeout == 0 {
- cfg.Timeout = defaultLinkUpTimeout
+ cfg.Timeout = defaultDHCPTimeout
}
if cfg.Retries == 0 {
- cfg.Retries = 3
+ cfg.Retries = 4
}
return &Client{
@@ -153,9 +154,15 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
}, nil
}
-func resetTimer(t *time.Timer, l *zerolog.Logger) {
- l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
- t.Reset(defaultTimerDuration)
+func resetTimer(t *time.Timer, attempt int, l *zerolog.Logger) {
+ // Exponential backoff: 1s, 2s, 4s, 8s, max 8s
+ backoffAttempt := attempt
+ if backoffAttempt > 3 {
+ backoffAttempt = 3
+ }
+ delay := time.Duration(1< /dev/null 2>&1; then
+ msg_err "Error: Cannot reach device at ${host}"
+ msg_err "Please verify the IP address and network connectivity"
+ exit 1
+ fi
+ msg_info "✓ Device is reachable"
+}
+
+# Function to check if SSH is accessible
+check_ssh() {
+ local user=$1
+ local host=$2
+ msg_info "▶ Checking SSH connectivity to ${user}@${host}..."
+ if ! ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ConnectTimeout=10 "${user}@${host}" "echo 'SSH connection successful'" > /dev/null 2>&1; then
+ msg_err "Error: Cannot establish SSH connection to ${user}@${host}"
+ msg_err "Please verify SSH access and credentials"
+ exit 1
+ fi
+ msg_info "✓ SSH connection successful"
+}
+
# Default values
SCRIPT_PATH=$(realpath "$(dirname $(realpath "${BASH_SOURCE[0]}"))")
REMOTE_USER="root"
@@ -113,6 +138,10 @@ if [ -z "$REMOTE_HOST" ]; then
exit 1
fi
+# Check device connectivity before proceeding
+check_ping "${REMOTE_HOST}"
+check_ssh "${REMOTE_USER}" "${REMOTE_HOST}"
+
# check if the current CPU architecture is x86_64
if [ "$(uname -m)" != "x86_64" ]; then
msg_warn "Warning: This script is only supported on x86_64 architecture"
@@ -131,7 +160,7 @@ if [[ "$SKIP_UI_BUILD" = true && ! -f "static/index.html" ]]; then
SKIP_UI_BUILD=false
fi
-if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
+if [[ "$SKIP_UI_BUILD" = false && "$JETKVM_INSIDE_DOCKER" != 1 ]]; then
msg_info "▶ Building frontend"
make frontend SKIP_UI_BUILD=0
SKIP_UI_BUILD_RELEASE=1
@@ -144,13 +173,13 @@ fi
if [ "$RUN_GO_TESTS" = true ]; then
msg_info "▶ Building go tests"
- make build_dev_test
+ make build_dev_test
msg_info "▶ Copying device-tests.tar.gz to remote host"
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /tmp/device-tests.tar.gz" < device-tests.tar.gz
msg_info "▶ Running go tests"
- ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << 'EOF'
set -e
TMP_DIR=$(mktemp -d)
cd ${TMP_DIR}
@@ -191,35 +220,35 @@ then
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
-
+
# Copy the binary to the remote host as if we were the OTA updater.
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
-
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
+
# Reboot the device, the new app will be deployed by the startup process.
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
else
msg_info "▶ Building development binary"
do_make build_dev \
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
-
+
# Kill any existing instances of the application
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
-
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"
+
# Copy the binary to the remote host
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
-
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "cat > ${REMOTE_PATH}/jetkvm_app_debug" < bin/jetkvm_app
+
if [ "$RESET_USB_HID_DEVICE" = true ]; then
msg_info "▶ Resetting USB HID device"
msg_warn "The option has been deprecated and will be removed in a future version, as JetKVM will now reset USB gadget configuration when needed"
# Remove the old USB gadget configuration
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
- ssh "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "rm -rf /sys/kernel/config/usb_gadget/jetkvm/configs/c.1/hid.usb*"
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" "ls /sys/class/udc > /sys/kernel/config/usb_gadget/jetkvm/UDC"
fi
-
+
# Deploy and run the application on the remote host
- ssh "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
+ ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "${REMOTE_USER}@${REMOTE_HOST}" ash << EOF
set -e
# Set the library path to include the directory where librockit.so is located
@@ -229,6 +258,17 @@ export LD_LIBRARY_PATH=/oem/usr/lib:\$LD_LIBRARY_PATH
killall jetkvm_app || true
killall jetkvm_app_debug || true
+# Wait until both binaries are killed, max 10 seconds
+i=1
+while [ \$i -le 10 ]; do
+ echo "Waiting for jetkvm_app and jetkvm_app_debug to be killed, \$i/10 ..."
+ if ! pgrep -f "jetkvm_app" > /dev/null && ! pgrep -f "jetkvm_app_debug" > /dev/null; then
+ break
+ fi
+ sleep 1
+ i=\$((i + 1))
+done
+
# Navigate to the directory where the binary will be stored
cd "${REMOTE_PATH}"
diff --git a/ui/eslint.config.cjs b/ui/eslint.config.cjs
index 6e972586..ad4338a3 100644
--- a/ui/eslint.config.cjs
+++ b/ui/eslint.config.cjs
@@ -9,8 +9,6 @@ const {
fixupConfigRules,
} = require("@eslint/compat");
-const tsParser = require("@typescript-eslint/parser");
-const reactRefresh = require("eslint-plugin-react-refresh");
const js = require("@eslint/js");
const {
@@ -23,6 +21,9 @@ const compat = new FlatCompat({
allConfig: js.configs.all
});
+const tsParser = require("@typescript-eslint/parser");
+const reactRefresh = require("eslint-plugin-react-refresh");
+
module.exports = defineConfig([{
languageOptions: {
globals: {
@@ -66,7 +67,7 @@ module.exports = defineConfig([{
groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always",
}],
-
+
"@typescript-eslint/no-unused-vars": ["warn", {
"argsIgnorePattern": "^_", "varsIgnorePattern": "^_"
}],
@@ -81,7 +82,10 @@ module.exports = defineConfig([{
map: [
["@components", "./src/components"],
["@routes", "./src/routes"],
+ ["@hooks", "./src/hooks"],
+ ["@providers", "./src/providers"],
["@assets", "./src/assets"],
+ ["@localizations", "./localization/paraglide"],
["@", "./src"],
],
diff --git a/ui/index.html b/ui/index.html
index 3c6c5606..77936233 100644
--- a/ui/index.html
+++ b/ui/index.html
@@ -45,31 +45,39 @@
-
+
-
+
-
+