#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mongoose.h" #include "app_installer.h" #include "lang.h" /* PS5 system language API — same as SM uses */ #define SCE_SYSTEM_SERVICE_PARAM_ID_LANG 1 int sceSystemServiceParamGetInt(int paramId, int *value); /* Maps PS5 lang integers → our 2-letter codes (DE/FR/ES only; others → EN) */ static const char* ps5_lang_to_ui(int id) { /* 0=ja,1=en-US,2=fr,3=es,4=de,5=it,6=nl,7=pt-PT,8=ru,9=ko, 10=zh-TW,11=zh-CN,12=fi,13=sv,14=da,15=no,16=pl,17=pt-BR, 18=en-GB,19=tr,20=es-MX,21=ar,22=el,23=cs,24=hu,25=ro, 26=uk,27=id,28=th,29=vi */ if(id==4) return "de"; if(id==2) return "fr"; if(id==3||id==20) return "es"; return "en"; } #define SCE_NOTIFY_UID 0xFE int sceNotificationSend(int uid, int logged, const char *payload); /* simple debug notification like Payload Manager */ typedef struct { char padding[45]; char message[3075]; } notify_request_t; int sceKernelSendNotificationRequest(int device, notify_request_t *req, size_t size, int unused); static void notify_simple(const char *msg){ notify_request_t req; memset(&req,0,sizeof(req)); strncpy(req.message,msg,sizeof(req.message)-1); sceKernelSendNotificationRequest(0,&req,sizeof(req),0); } static void _notify_send(const char *title, const char *sub) { char id[24], buf[2048]; snprintf(id, sizeof(id), "%lu", (unsigned long)time(NULL)); if (sub && sub[0]) { snprintf(buf, sizeof(buf), "{\"rawData\":{\"viewTemplateType\":\"InteractiveToastTemplateB\"," "\"channelType\":\"Downloads\",\"useCaseId\":\"IDC\"," "\"toastOverwriteType\":\"No\",\"isImmediate\":true,\"priority\":100," "\"viewData\":{\"icon\":{\"type\":\"Url\",\"parameters\":{\"url\":\"/user/app/SMPL00001/sce_sys/icon0.png\"}}," "\"message\":{\"body\":\"%s\"},\"subMessage\":{\"body\":\"%s\"}}}," "\"createdDateTime\":\"2025-01-01T00:00:00.000Z\"," "\"localNotificationId\":\"%s\"}", title, sub, id); } else { snprintf(buf, sizeof(buf), "{\"rawData\":{\"viewTemplateType\":\"InteractiveToastTemplateB\"," "\"channelType\":\"Downloads\",\"useCaseId\":\"IDC\"," "\"toastOverwriteType\":\"No\",\"isImmediate\":true,\"priority\":100," "\"viewData\":{\"icon\":{\"type\":\"Predefined\",\"parameters\":{\"icon\":\"download\"}}," "\"message\":{\"body\":\"%s\"}}}," "\"createdDateTime\":\"2025-01-01T00:00:00.000Z\"," "\"localNotificationId\":\"%s\"}", title, id); } sceNotificationSend(SCE_NOTIFY_UID, 1, buf); } static void notify(const char *msg) { _notify_send(msg, NULL); } /* SMPlusGui own preferences (not SM config) */ #define PREFS_PATH "/data/SMPlusGui/prefs.ini" #define PREFS_MAX_EXTRA 8 typedef struct { int auto_start; int icon_always_front; int always_restart_sm; /* 0=only start if not running (default), 1=always restart */ char preferred_elf[512]; char extra_scan[PREFS_MAX_EXTRA][256]; int extra_scan_count; } SMPrefs; static void read_prefs(SMPrefs *p) { memset(p, 0, sizeof(*p)); FILE *f = fopen(PREFS_PATH, "r"); if(!f) return; char line[600]; while(fgets(line, sizeof(line), f)) { line[strcspn(line,"\r\n")]=0; if(strncmp(line,"auto_start=",11)==0) p->auto_start=atoi(line+11); else if(strncmp(line,"icon_always_front=",18)==0) p->icon_always_front=atoi(line+18); else if(strncmp(line,"always_restart_sm=",18)==0) p->always_restart_sm=atoi(line+18); else if(strncmp(line,"preferred_elf=",14)==0) strncpy(p->preferred_elf,line+14,511); else if(strncmp(line,"extra_scan=",11)==0&&p->extra_scan_countextra_scan[p->extra_scan_count++],line+11,255); } fclose(f); } static void write_prefs(const SMPrefs *p) { mkdir("/data/SMPlusGui",0755); FILE *f = fopen(PREFS_PATH,"w"); if(!f) return; fprintf(f,"auto_start=%d\n",p->auto_start); fprintf(f,"icon_always_front=%d\n",p->icon_always_front); fprintf(f,"always_restart_sm=%d\n",p->always_restart_sm); fprintf(f,"preferred_elf=%s\n",p->preferred_elf); for(int i=0;iextra_scan_count;i++) fprintf(f,"extra_scan=%s\n",p->extra_scan[i]); fclose(f); } static void get_local_ip(char *buf, size_t buflen) { struct ifaddrs *ifap, *ifa; buf[0] = 0; if (getifaddrs(&ifap) != 0) return; for (ifa = ifap; ifa; ifa = ifa->ifa_next) { if (!ifa->ifa_addr) continue; if (ifa->ifa_addr->sa_family != AF_INET) continue; if (ifa->ifa_flags & IFF_LOOPBACK) continue; struct sockaddr_in *sin = (struct sockaddr_in *)ifa->ifa_addr; inet_ntop(AF_INET, &sin->sin_addr, buf, buflen); break; } freeifaddrs(ifap); } /* ShadowMount Version auslesen - API + Log-Fallback */ static void get_sm_version(char *buf, size_t buflen) { strncpy(buf, "nicht aktiv", buflen-1); buf[buflen-1] = 0; /* Erst versuchen aus debug.log zu lesen */ FILE *lf = fopen("/data/shadowmount/debug.log", "r"); if (lf) { char line[256]; for (int i = 0; i < 20; i++) { if (!fgets(line, sizeof(line), lf)) break; /* Suche nach "ShadowMount" + Versionsnummer */ char *p = strstr(line, "ShadowMount"); if (!p) p = strstr(line, "shadowmount"); if (p) { /* Suche nach Zahl nach Leerzeichen oder 'v' */ char *v = strpbrk(p, "0123456789"); if (v && (v[-1]==' '||v[-1]=='v'||v[-1]=='\t')) { size_t len=0; while(v[len]&&!strchr(" \t\r\n\"",v[len])&&len0&&len<32){ strncpy(buf,v,len); buf[len]=0; fclose(lf); return; } } } } fclose(lf); } /* API anfragen */ int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return; struct timeval tv = {1, 0}; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(10101); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(sock); return; } strncpy(buf, "aktiv", buflen-1); /* Verschiedene Endpoints probieren */ const char *reqs[] = { "POST /api/v1/version HTTP/1.0\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}", "GET /version HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", "GET /api/version HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", "GET /status HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", "GET /api/status HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", "GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", NULL }; char resp[4096]; for (int i = 0; reqs[i]; i++) { memset(resp, 0, sizeof(resp)); send(sock, reqs[i], strlen(reqs[i]), 0); ssize_t n = recv(sock, resp, sizeof(resp)-1, 0); if (n <= 0) continue; resp[n] = 0; /* JSON Keys suchen */ const char *keys[] = { "\"shadowmount_version\":\"", "\"shadowmount_version\": \"", "\"version\":\"", "\"version\": \"", "\"sm_version\":\"", "\"sm_version\": \"", "\"app_version\":\"", "\"app_version\": \"", NULL }; for (int k = 0; keys[k]; k++) { char *p = strstr(resp, keys[k]); if (!p) continue; p += strlen(keys[k]); size_t len = 0; while(p[len] && p[len]!='"' && p[len]!=',' && p[len]!='}' && p[len]!='\n' && len 0 && len < 32) { strncpy(buf, p, len); buf[len]=0; close(sock); return; } } /* Freie Versionsnummer-Suche im Body */ char *body = strstr(resp, "\r\n\r\n"); if (body) { body += 4; char *v = strpbrk(body, "0123456789"); while (v) { if (v>body && (v[-1]==' '||v[-1]=='"'||v[-1]==':'||v[-1]=='v')) { size_t len=0; while(v[len]&&!strchr(" \t\r\n\",}",v[len])&&len=3&&len<32) { strncpy(buf,v,len); buf[len]=0; close(sock); return; } } v = strpbrk(v+1, "0123456789"); } } } close(sock); } /* Generic SM API proxy - returns malloc'd body or NULL */ static char* sm_api_req(const char *method, const char *path, const char *body) { int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) return NULL; struct timeval tv = {3, 0}; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(10101); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) != 0) { close(sock); return NULL; } char req[512]; int blen = body ? (int)strlen(body) : 0; if (blen > 0) snprintf(req, sizeof(req), "%s %s HTTP/1.0\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: %d\r\n\r\n", method, path, blen); else snprintf(req, sizeof(req), "%s %s HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", method, path); send(sock, req, strlen(req), 0); if (blen > 0) send(sock, body, blen, 0); size_t bufsz = 131072; char *buf = malloc(bufsz); if (!buf) { close(sock); return NULL; } size_t total = 0; ssize_t rn; while (total < bufsz-1 && (rn = recv(sock, buf+total, bufsz-1-total, 0)) > 0) total += rn; close(sock); buf[total] = 0; char *start = strstr(buf, "\r\n\r\n"); if (!start) { free(buf); return NULL; } char *result = strdup(start + 4); free(buf); return result; } #define KI_PID_OFFSET 72 #define KI_TDNAME_OFFSET 447 static pid_t find_pid(const char *name) { int mib[4]={1,14,8,0}; pid_t mypid=getpid(),found=-1; size_t sz=0; if(sysctl(mib,4,NULL,&sz,NULL,0)!=0) return -1; uint8_t *buf=malloc(sz); if(!buf) return -1; if(sysctl(mib,4,buf,&sz,NULL,0)==0) for(uint8_t *p=buf;p0){kill(pid,SIGKILL);sleep(1);} } #define CONFIG_PATH "/data/shadowmount/config.ini" #define BAK_DIR "/data/SMPlusGui/backups" #define ICO(n) "" #define HTTP_PORT "7070" #define MAX_PATHS 20 #define PATH_LEN 256 #define MAX_IMG 50 #define MAX_LIST 30 #define IMG_LEN 128 #define ID_LEN 32 #define MANUAL_LST "/data/shadowmount/manual.lst" #define MAX_MANUAL 50 typedef struct { /* Allgemein */ int debug; int quiet_mode; char language[32]; /* API */ char api_bind_address[64]; int api_port; /* Mounting */ int mount_read_only; int force_mount; int persistent_image_mounts; int app_install_all; /* Scan */ int scan_depth; int recursive_scan; int scan_interval_seconds; int stability_wait_seconds; /* Auto-Remove */ int auto_remove_missing_games; int auto_remove_games_with_dlc; int auto_remove_missing_delay_seconds; /* kstuff */ int kstuff_game_auto_toggle; int kstuff_crash_detection; int kstuff_pause_delay_image_seconds; int kstuff_pause_delay_direct_seconds; /* Fakelib */ int backport_fakelib; int global_fakelib; char global_fakelib_path[PATH_LEN]; char global_fakelib_priority[16]; /* Backend */ char exfat_backend[8]; char ufs_backend[8]; int lvd_exfat_sector_size; int lvd_ufs_sector_size; int lvd_pfs_sector_size; int md_exfat_sector_size; int md_ufs_sector_size; /* Scanpaths */ char scanpaths[MAX_PATHS][PATH_LEN]; int path_count; /* Per-Image Overrides */ char image_ro[MAX_IMG][IMG_LEN]; int image_ro_count; char image_rw[MAX_IMG][IMG_LEN]; int image_rw_count; char image_sector[MAX_IMG][IMG_LEN]; /* "datei.exfat:65536" */ int image_sector_count; /* kstuff Ausnahmen */ char kstuff_no_pause[MAX_LIST][ID_LEN]; int kstuff_no_pause_count; char kstuff_delay[MAX_LIST][ID_LEN+8]; /* "TITLEID:SEKUNDEN" */ int kstuff_delay_count; /* Fakelib Ausnahmen */ char global_fakelib_exclude[MAX_LIST][PATH_LEN]; int global_fakelib_exclude_count; /* 1.7alpha5 */ char fan_target_temperature[8]; /* 1.7alpha7 */ int legacy_mount_ufs; int legacy_mount_exfat; int legacy_mount_pfs; int legacy_mount_pfsc; int nested_pfs_index_cache; int update_emulators; char emulators_path[PATH_LEN]; int auto_update_ampr; char ampr_update_url[256]; } ShadowConfig; static void set_defaults(ShadowConfig *cfg) { memset(cfg, 0, sizeof(*cfg)); cfg->debug=0; cfg->quiet_mode=0; strncpy(cfg->language,"auto",31); strncpy(cfg->api_bind_address,"127.0.0.1",63); cfg->api_port=10101; cfg->mount_read_only=1; cfg->force_mount=0; cfg->persistent_image_mounts=0; cfg->app_install_all=0; cfg->scan_depth=1; cfg->recursive_scan=0; cfg->scan_interval_seconds=15; cfg->stability_wait_seconds=10; cfg->auto_remove_missing_games=0; cfg->auto_remove_games_with_dlc=0; cfg->auto_remove_missing_delay_seconds=300; cfg->kstuff_game_auto_toggle=1; cfg->kstuff_crash_detection=1; cfg->kstuff_pause_delay_image_seconds=25; cfg->kstuff_pause_delay_direct_seconds=15; cfg->backport_fakelib=1; cfg->global_fakelib=1; strncpy(cfg->global_fakelib_path,"/data/shadowmount/fakelib",PATH_LEN-1); strncpy(cfg->global_fakelib_priority,"game",15); strncpy(cfg->exfat_backend,"lvd",7); strncpy(cfg->ufs_backend,"lvd",7); cfg->lvd_exfat_sector_size=512; cfg->lvd_ufs_sector_size=4096; cfg->lvd_pfs_sector_size=32768; cfg->md_exfat_sector_size=512; cfg->md_ufs_sector_size=512; cfg->image_ro_count=0; cfg->image_rw_count=0; cfg->image_sector_count=0; cfg->kstuff_no_pause_count=0; cfg->kstuff_delay_count=0; cfg->global_fakelib_exclude_count=0; strncpy(cfg->fan_target_temperature,"system",7); cfg->legacy_mount_ufs=1; cfg->legacy_mount_exfat=1; cfg->legacy_mount_pfs=1; cfg->legacy_mount_pfsc=1; cfg->nested_pfs_index_cache=0; cfg->update_emulators=1; strncpy(cfg->emulators_path,"/data/shadowmount/emus",PATH_LEN-1); cfg->auto_update_ampr=0; strncpy(cfg->ampr_update_url,"https://github.com/drakmor/ampr_emu/releases/latest/download/libSceAmpr.sprx",255); } static int parse_bool(const char *v) { return !strcmp(v,"true")||!strcmp(v,"1")||!strcmp(v,"yes")||!strcmp(v,"on")||!strcmp(v,"ro"); } static void load_config(ShadowConfig *cfg) { set_defaults(cfg); FILE *f=fopen(CONFIG_PATH,"r"); if(!f) return; char line[512]; while(fgets(line,sizeof(line),f)){ line[strcspn(line,"\r\n")]=0; char *s=line; while(*s==' '||*s=='\t') s++; if(*s==';'||*s=='#'||*s=='['||!*s) continue; char *eq=strchr(s,'='); if(!eq) continue; *eq=0; char *key=s,*val=eq+1; int kl=strlen(key); while(kl>0&&(key[kl-1]==' '||key[kl-1]=='\t')) key[--kl]=0; while(*val==' '||*val=='\t') val++; #define B(k,f) if(!strcmp(key,k)){cfg->f=parse_bool(val);} #define I(k,f) else if(!strcmp(key,k)){cfg->f=atoi(val);} #define S(k,f,n) else if(!strcmp(key,k)){strncpy(cfg->f,val,n-1);} B("debug",debug) B("quiet_mode",quiet_mode) S("language",language,32) else if(!strcmp(key,"api_bind_address")){strncpy(cfg->api_bind_address,val,63);} I("api_port",api_port) B("mount_read_only",mount_read_only) B("force_mount",force_mount) B("persistent_image_mounts",persistent_image_mounts) B("app_install_all",app_install_all) I("scan_depth",scan_depth) B("recursive_scan",recursive_scan) I("scan_interval_seconds",scan_interval_seconds) I("stability_wait_seconds",stability_wait_seconds) B("auto_remove_missing_games",auto_remove_missing_games) B("auto_remove_games_with_dlc",auto_remove_games_with_dlc) I("auto_remove_missing_delay_seconds",auto_remove_missing_delay_seconds) B("kstuff_game_auto_toggle",kstuff_game_auto_toggle) B("kstuff_crash_detection",kstuff_crash_detection) S("fan_target_temperature",fan_target_temperature,8) I("kstuff_pause_delay_image_s",kstuff_pause_delay_image_seconds) I("kstuff_pause_delay_image_seconds",kstuff_pause_delay_image_seconds) I("kstuff_pause_delay_direct_s",kstuff_pause_delay_direct_seconds) I("kstuff_pause_delay_direct_seconds",kstuff_pause_delay_direct_seconds) B("backport_fakelib",backport_fakelib) B("global_fakelib",global_fakelib) S("global_fakelib_path",global_fakelib_path,PATH_LEN) S("global_fakelib_priority",global_fakelib_priority,16) S("exfat_backend",exfat_backend,8) S("ufs_backend",ufs_backend,8) B("legacy_mount_ufs",legacy_mount_ufs) B("legacy_mount_exfat",legacy_mount_exfat) B("legacy_mount_pfs",legacy_mount_pfs) B("legacy_mount_pfsc",legacy_mount_pfsc) B("nested_pfs_index_cache",nested_pfs_index_cache) B("legacy_gddr5_cache",nested_pfs_index_cache) /* old name alias */ B("update_emulators",update_emulators) S("emulators_path",emulators_path,PATH_LEN) B("auto_update_ampr",auto_update_ampr) S("ampr_update_url",ampr_update_url,256) I("lvd_exfat_sector_size",lvd_exfat_sector_size) I("lvd_ufs_sector_size",lvd_ufs_sector_size) I("lvd_pfs_sector_size",lvd_pfs_sector_size) I("md_exfat_sector_size",md_exfat_sector_size) I("md_ufs_sector_size",md_ufs_sector_size) else if(!strcmp(key,"scanpath")&&cfg->path_countscanpaths[cfg->path_count++],val,PATH_LEN-1); else if(!strcmp(key,"image_ro")&&cfg->image_ro_countimage_ro[cfg->image_ro_count++],val,IMG_LEN-1); else if(!strcmp(key,"image_rw")&&cfg->image_rw_countimage_rw[cfg->image_rw_count++],val,IMG_LEN-1); else if(!strcmp(key,"image_sector")&&cfg->image_sector_countimage_sector[cfg->image_sector_count++],val,IMG_LEN-1); else if(!strcmp(key,"kstuff_no_pause")&&cfg->kstuff_no_pause_countkstuff_no_pause[cfg->kstuff_no_pause_count++],val,ID_LEN-1); else if(!strcmp(key,"kstuff_delay")&&cfg->kstuff_delay_countkstuff_delay[cfg->kstuff_delay_count++],val,ID_LEN+7); else if(!strcmp(key,"global_fakelib_exclude")&&cfg->global_fakelib_exclude_countglobal_fakelib_exclude[cfg->global_fakelib_exclude_count++],val,PATH_LEN-1); #undef B #undef I #undef S } fclose(f); /* legacy: recursive_scan=1 forces scan_depth=2 */ if(cfg->recursive_scan) cfg->scan_depth=2; } static int save_config(ShadowConfig *cfg) { char lines[2048][512]; int lc=0; FILE *f=fopen(CONFIG_PATH,"r"); if(f){while(lc<2048&&fgets(lines[lc],512,f))lc++;fclose(f);} FILE *out=fopen(CONFIG_PATH,"w"); if(!out) return 0; int wd=0,wq=0,wlang=0,wapi=0,wport=0; int wro=0,wfm=0,wpim=0,waia=0; int wsd=0,wrs=0,wsi=0,wss=0; int warm=0,wargd=0,warmd=0; int wkgt=0,wkcd=0,wkpi=0,wkpd=0; int wlmu=0,wlme=0,wlmp=0,wlmpc=0,wlgc=0,wue=0,wep=0,waua=0,wauf=0; int wbf=0,wgf=0,wgfp=0,wgfpr=0; int web=0,wub=0,wles=0,wlus=0,wlps=0,wmes=0,wmus=0; int wscan=0,wfan=0; #define MATCH(key) (strncmp(pc, key "=", strlen(key)+1)==0) for(int i=0;idebug);wd=1;} else if(MATCH("quiet_mode")&&!wq){fprintf(out,"quiet_mode=%d\n",cfg->quiet_mode);wq=1;} else if(MATCH("language")&&!wlang){fprintf(out,"language=%s\n",cfg->language);wlang=1;} else if(MATCH("api_bind_address")&&!wapi){fprintf(out,"api_bind_address=%s\n",cfg->api_bind_address);wapi=1;} else if(MATCH("api")&&!wapi){fprintf(out,"api_bind_address=%s\n",cfg->api_bind_address);wapi=1;} /* migrate combined key */ else if(MATCH("api_port")&&!wport){fprintf(out,"api_port=%d\n",cfg->api_port);wport=1;} else if(MATCH("mount_read_only")&&!wro){fprintf(out,"mount_read_only=%d\n",cfg->mount_read_only);wro=1;} else if(MATCH("force_mount")&&!wfm){fprintf(out,"force_mount=%d\n",cfg->force_mount);wfm=1;} else if(MATCH("persistent_image_mounts")&&!wpim){fprintf(out,"persistent_image_mounts=%d\n",cfg->persistent_image_mounts);wpim=1;} else if(MATCH("app_install_all")&&!waia){fprintf(out,"app_install_all=%d\n",cfg->app_install_all);waia=1;} else if(MATCH("scan_depth")&&!wsd){fprintf(out,"scan_depth=%d\n",cfg->scan_depth);wsd=1;} else if(MATCH("recursive_scan")&&!wrs){fprintf(out,"recursive_scan=%d\n",cfg->recursive_scan);wrs=1;} else if(MATCH("scan_interval_seconds")&&!wsi){fprintf(out,"scan_interval_seconds=%d\n",cfg->scan_interval_seconds);wsi=1;} else if(MATCH("scan_interval_s")&&!wsi){fprintf(out,"scan_interval_seconds=%d\n",cfg->scan_interval_seconds);wsi=1;} /* migrate */ else if(MATCH("stability_wait_seconds")&&!wss){fprintf(out,"stability_wait_seconds=%d\n",cfg->stability_wait_seconds);wss=1;} else if(MATCH("stability_wait_s")&&!wss){fprintf(out,"stability_wait_seconds=%d\n",cfg->stability_wait_seconds);wss=1;} /* migrate */ else if(MATCH("auto_remove_missing_games")&&!warm){fprintf(out,"auto_remove_missing_games=%d\n",cfg->auto_remove_missing_games);warm=1;} else if(MATCH("auto_remove_games_with_dlc")&&!wargd){fprintf(out,"auto_remove_games_with_dlc=%d\n",cfg->auto_remove_games_with_dlc);wargd=1;} else if(MATCH("auto_remove_missing_delay_seconds")&&!warmd){fprintf(out,"auto_remove_missing_delay_seconds=%d\n",cfg->auto_remove_missing_delay_seconds);warmd=1;} else if(MATCH("auto_remove_missing_delay_s")&&!warmd){fprintf(out,"auto_remove_missing_delay_seconds=%d\n",cfg->auto_remove_missing_delay_seconds);warmd=1;} /* migrate */ else if(MATCH("kstuff_game_auto_toggle")&&!wkgt){fprintf(out,"kstuff_game_auto_toggle=%d\n",cfg->kstuff_game_auto_toggle);wkgt=1;} else if(MATCH("kstuff_crash_detection")&&!wkcd){fprintf(out,"kstuff_crash_detection=%d\n",cfg->kstuff_crash_detection);wkcd=1;} else if((MATCH("kstuff_pause_delay_image_s")||MATCH("kstuff_pause_delay_image_seconds"))&&!wkpi){fprintf(out,"kstuff_pause_delay_image_seconds=%d\n",cfg->kstuff_pause_delay_image_seconds);wkpi=1;} else if((MATCH("kstuff_pause_delay_direct_s")||MATCH("kstuff_pause_delay_direct_seconds"))&&!wkpd){fprintf(out,"kstuff_pause_delay_direct_seconds=%d\n",cfg->kstuff_pause_delay_direct_seconds);wkpd=1;} else if(MATCH("fan_target_temperature")&&!wfan){fprintf(out,"fan_target_temperature=%s\n",cfg->fan_target_temperature);wfan=1;} else if(MATCH("backport_fakelib")&&!wbf){fprintf(out,"backport_fakelib=%d\n",cfg->backport_fakelib);wbf=1;} else if(MATCH("global_fakelib")&&!wgf){fprintf(out,"global_fakelib=%d\n",cfg->global_fakelib);wgf=1;} else if(MATCH("global_fakelib_path")&&!wgfp){fprintf(out,"global_fakelib_path=%s\n",cfg->global_fakelib_path);wgfp=1;} else if(MATCH("global_fakelib_priority")&&!wgfpr){fprintf(out,"global_fakelib_priority=%s\n",cfg->global_fakelib_priority);wgfpr=1;} else if(MATCH("exfat_backend")&&!web){fprintf(out,"exfat_backend=%s\n",cfg->exfat_backend);web=1;} else if(MATCH("ufs_backend")&&!wub){fprintf(out,"ufs_backend=%s\n",cfg->ufs_backend);wub=1;} else if(MATCH("legacy_mount_ufs")&&!wlmu){fprintf(out,"legacy_mount_ufs=%d\n",cfg->legacy_mount_ufs);wlmu=1;} else if(MATCH("legacy_mount_exfat")&&!wlme){fprintf(out,"legacy_mount_exfat=%d\n",cfg->legacy_mount_exfat);wlme=1;} else if(MATCH("legacy_mount_pfs")&&!wlmp){fprintf(out,"legacy_mount_pfs=%d\n",cfg->legacy_mount_pfs);wlmp=1;} else if(MATCH("legacy_mount_pfsc")&&!wlmpc){fprintf(out,"legacy_mount_pfsc=%d\n",cfg->legacy_mount_pfsc);wlmpc=1;} else if(MATCH("nested_pfs_index_cache")&&!wlgc){fprintf(out,"nested_pfs_index_cache=%d\n",cfg->nested_pfs_index_cache);wlgc=1;} else if(MATCH("legacy_gddr5_cache")&&!wlgc){fprintf(out,"nested_pfs_index_cache=%d\n",cfg->nested_pfs_index_cache);wlgc=1;} else if(MATCH("update_emulators")&&!wue){fprintf(out,"update_emulators=%d\n",cfg->update_emulators);wue=1;} else if(MATCH("emulators_path")&&!wep){fprintf(out,"emulators_path=%s\n",cfg->emulators_path);wep=1;} else if(MATCH("auto_update_ampr")&&!waua){fprintf(out,"auto_update_ampr=%d\n",cfg->auto_update_ampr);waua=1;} else if(MATCH("ampr_update_url")&&!wauf){fprintf(out,"ampr_update_url=%s\n",cfg->ampr_update_url);wauf=1;} else if(MATCH("lvd_exfat_sector_size")&&!wles){fprintf(out,"lvd_exfat_sector_size=%d\n",cfg->lvd_exfat_sector_size);wles=1;} else if(MATCH("lvd_ufs_sector_size")&&!wlus){fprintf(out,"lvd_ufs_sector_size=%d\n",cfg->lvd_ufs_sector_size);wlus=1;} else if(MATCH("lvd_pfs_sector_size")&&!wlps){fprintf(out,"lvd_pfs_sector_size=%d\n",cfg->lvd_pfs_sector_size);wlps=1;} else if(MATCH("md_exfat_sector_size")&&!wmes){fprintf(out,"md_exfat_sector_size=%d\n",cfg->md_exfat_sector_size);wmes=1;} else if(MATCH("md_ufs_sector_size")&&!wmus){fprintf(out,"md_ufs_sector_size=%d\n",cfg->md_ufs_sector_size);wmus=1;} else if(MATCH("scanpath")){ if(!wscan){ for(int j=0;jpath_count;j++) if(cfg->scanpaths[j][0]) fprintf(out,"scanpath=%s\n",cfg->scanpaths[j]); wscan=1; } } else if(MATCH("image_ro")||MATCH("image_rw")||MATCH("image_sector")){ /* Per-Image Overrides werden gesammelt und später geschrieben */ } else if(MATCH("kstuff_no_pause")||MATCH("kstuff_delay")){ /* kstuff Ausnahmen werden gesammelt und später geschrieben */ } else if(MATCH("global_fakelib_exclude")){ /* Fakelib Ausnahmen werden gesammelt und später geschrieben */ } else { /* skip duplicate active key lines — only write if not a known key */ if(*p=='#'||!strchr(p,'=')) fputs(lines[i],out); else if(!( MATCH("debug")||MATCH("quiet_mode")||MATCH("language")|| MATCH("api_bind_address")||MATCH("api_port")||MATCH("api")|| MATCH("mount_read_only")||MATCH("force_mount")|| MATCH("persistent_image_mounts")||MATCH("app_install_all")|| MATCH("scan_depth")||MATCH("recursive_scan")|| MATCH("scan_interval_seconds")||MATCH("scan_interval_s")|| MATCH("stability_wait_seconds")||MATCH("stability_wait_s")|| MATCH("auto_remove_missing_games")||MATCH("auto_remove_games_with_dlc")|| MATCH("auto_remove_missing_delay_seconds")||MATCH("auto_remove_missing_delay_s")|| MATCH("kstuff_game_auto_toggle")||MATCH("kstuff_crash_detection")|| MATCH("kstuff_pause_delay_image_s")||MATCH("kstuff_pause_delay_image_seconds")|| MATCH("kstuff_pause_delay_direct_s")||MATCH("kstuff_pause_delay_direct_seconds")|| MATCH("fan_target_temperature")|| MATCH("backport_fakelib")||MATCH("global_fakelib")|| MATCH("global_fakelib_path")||MATCH("global_fakelib_priority")|| MATCH("exfat_backend")||MATCH("ufs_backend")|| MATCH("legacy_mount_ufs")||MATCH("legacy_mount_exfat")|| MATCH("legacy_mount_pfs")||MATCH("legacy_mount_pfsc")|| MATCH("nested_pfs_index_cache")||MATCH("legacy_gddr5_cache")||MATCH("update_emulators")|| MATCH("emulators_path")||MATCH("auto_update_ampr")||MATCH("ampr_update_url")|| MATCH("lvd_exfat_sector_size")||MATCH("lvd_ufs_sector_size")|| MATCH("lvd_pfs_sector_size")||MATCH("md_exfat_sector_size")|| MATCH("md_ufs_sector_size") )) fputs(lines[i],out); } } #undef MATCH if(!wd) fprintf(out,"debug=%d\n",cfg->debug); if(!wq) fprintf(out,"quiet_mode=%d\n",cfg->quiet_mode); if(!wlang) fprintf(out,"language=%s\n",cfg->language); if(!wapi) fprintf(out,"api_bind_address=%s\n",cfg->api_bind_address); if(!wport) fprintf(out,"api_port=%d\n",cfg->api_port); if(!wro) fprintf(out,"mount_read_only=%d\n",cfg->mount_read_only); if(!wfm) fprintf(out,"force_mount=%d\n",cfg->force_mount); if(!wpim) fprintf(out,"persistent_image_mounts=%d\n",cfg->persistent_image_mounts); if(!waia) fprintf(out,"app_install_all=%d\n",cfg->app_install_all); if(!wsd) fprintf(out,"scan_depth=%d\n",cfg->scan_depth); if(!wrs) fprintf(out,"recursive_scan=%d\n",cfg->recursive_scan); if(!wsi) fprintf(out,"scan_interval_seconds=%d\n",cfg->scan_interval_seconds); if(!wss) fprintf(out,"stability_wait_seconds=%d\n",cfg->stability_wait_seconds); if(!warm) fprintf(out,"auto_remove_missing_games=%d\n",cfg->auto_remove_missing_games); if(!wargd) fprintf(out,"auto_remove_games_with_dlc=%d\n",cfg->auto_remove_games_with_dlc); if(!warmd) fprintf(out,"auto_remove_missing_delay_seconds=%d\n",cfg->auto_remove_missing_delay_seconds); if(!wkgt) fprintf(out,"kstuff_game_auto_toggle=%d\n",cfg->kstuff_game_auto_toggle); if(!wkcd) fprintf(out,"kstuff_crash_detection=%d\n",cfg->kstuff_crash_detection); if(!wkpi) fprintf(out,"kstuff_pause_delay_image_seconds=%d\n",cfg->kstuff_pause_delay_image_seconds); if(!wkpd) fprintf(out,"kstuff_pause_delay_direct_seconds=%d\n",cfg->kstuff_pause_delay_direct_seconds); if(!wfan) fprintf(out,"fan_target_temperature=%s\n",cfg->fan_target_temperature); if(!wbf) fprintf(out,"backport_fakelib=%d\n",cfg->backport_fakelib); if(!wgf) fprintf(out,"global_fakelib=%d\n",cfg->global_fakelib); if(!wgfp) fprintf(out,"global_fakelib_path=%s\n",cfg->global_fakelib_path); if(!wgfpr) fprintf(out,"global_fakelib_priority=%s\n",cfg->global_fakelib_priority); if(!web) fprintf(out,"exfat_backend=%s\n",cfg->exfat_backend); if(!wub) fprintf(out,"ufs_backend=%s\n",cfg->ufs_backend); if(!wlmu) fprintf(out,"legacy_mount_ufs=%d\n",cfg->legacy_mount_ufs); if(!wlme) fprintf(out,"legacy_mount_exfat=%d\n",cfg->legacy_mount_exfat); if(!wlmp) fprintf(out,"legacy_mount_pfs=%d\n",cfg->legacy_mount_pfs); if(!wlmpc) fprintf(out,"legacy_mount_pfsc=%d\n",cfg->legacy_mount_pfsc); if(!wlgc) fprintf(out,"nested_pfs_index_cache=%d\n",cfg->nested_pfs_index_cache); if(!wue) fprintf(out,"update_emulators=%d\n",cfg->update_emulators); if(!wep) fprintf(out,"emulators_path=%s\n",cfg->emulators_path); if(!waua) fprintf(out,"auto_update_ampr=%d\n",cfg->auto_update_ampr); if(!wauf) fprintf(out,"ampr_update_url=%s\n",cfg->ampr_update_url); if(!wles) fprintf(out,"lvd_exfat_sector_size=%d\n",cfg->lvd_exfat_sector_size); if(!wlus) fprintf(out,"lvd_ufs_sector_size=%d\n",cfg->lvd_ufs_sector_size); if(!wlps) fprintf(out,"lvd_pfs_sector_size=%d\n",cfg->lvd_pfs_sector_size); if(!wmes) fprintf(out,"md_exfat_sector_size=%d\n",cfg->md_exfat_sector_size); if(!wmus) fprintf(out,"md_ufs_sector_size=%d\n",cfg->md_ufs_sector_size); if(!wscan) for(int j=0;jpath_count;j++) if(cfg->scanpaths[j][0]) fprintf(out,"scanpath=%s\n",cfg->scanpaths[j]); for(int j=0;jimage_ro_count;j++) if(cfg->image_ro[j][0]) fprintf(out,"image_ro=%s\n",cfg->image_ro[j]); for(int j=0;jimage_rw_count;j++) if(cfg->image_rw[j][0]) fprintf(out,"image_rw=%s\n",cfg->image_rw[j]); for(int j=0;jimage_sector_count;j++) if(cfg->image_sector[j][0]) fprintf(out,"image_sector=%s\n",cfg->image_sector[j]); for(int j=0;jkstuff_no_pause_count;j++) if(cfg->kstuff_no_pause[j][0]) fprintf(out,"kstuff_no_pause=%s\n",cfg->kstuff_no_pause[j]); for(int j=0;jkstuff_delay_count;j++) if(cfg->kstuff_delay[j][0]) fprintf(out,"kstuff_delay=%s\n",cfg->kstuff_delay[j]); for(int j=0;jglobal_fakelib_exclude_count;j++) if(cfg->global_fakelib_exclude[j][0]) fprintf(out,"global_fakelib_exclude=%s\n",cfg->global_fakelib_exclude[j]); fclose(out); return 1; } static size_t next_kv_param(struct mg_str body, size_t ofs, struct mg_str *key, struct mg_str *val) { if(ofs>=body.len) return 0; size_t eq=ofs,amp; while(eq=body.len) return 0; amp=eq+1; while(ampbuf=body.buf+ofs; key->len=eq-ofs; val->buf=body.buf+eq+1; val->len=amp-(eq+1); return amp= geforderte Version ist. * Beispiele: "1.6beta16", "1.7alpha3", "1.6test13" * Reihenfolge: alpha < beta < test innerhalb gleicher major.minor */ static int sm_version_at_least(const char *ver, int maj, int min, const char *suf, int num) { if(!ver||!*ver) return 0; int vmaj=0,vmin=0,vnum=0; char vsuf[8]={0}; /* Major.Minor parsen */ if(sscanf(ver,"%d.%d",&vmaj,&vmin)<2) return 0; /* Suffix und Nummer finden */ const char *p=ver; while(*p&&*p!='.') p++; if(*p) p++; while(*p&&*p>='0'&&*p<='9') p++; /* p zeigt jetzt auf suffix wie "beta16" */ int si=0; while(*p&&(*p<'0'||*p>'9')&&si<7) vsuf[si++]=*p++; vnum=atoi(p); /* Major.Minor vergleichen */ if(vmaj>maj) return 1; if(vmajmin) return 1; if(vminmp) return 1; if(vp=num; } #define SM_MAX_ELFS 20 #define SM_EPATH 256 /* Scan known dirs for SM ELF files, returns count */ static int sm_find_elfs(char paths[SM_MAX_ELFS][SM_EPATH]) { int count=0; /* Helper: true if filename ends with .elf */ #define IS_ELF(s) (strlen(s)>4&&strcmp((s)+strlen(s)-4,".elf")==0) /* pldmgr payloads: direct ELFs + scan subdirs with "shadow" in dir name */ { DIR *d=opendir("/data/pldmgr/payloads"); if(d){ struct dirent *e; while((e=readdir(d))&&countd_name[0]=='.') continue; char nl[64]={0}; int j=0; while(e->d_name[j]&&j<63){nl[j]=(char)tolower((unsigned char)e->d_name[j]);j++;} char fp[SM_EPATH]; snprintf(fp,SM_EPATH,"/data/pldmgr/payloads/%s",e->d_name); /* Direct .elf file ending with .elf and shadowmount in name */ if(IS_ELF(nl)&&strstr(nl,"shadowmount")){ snprintf(paths[count++],SM_EPATH,"%s",fp); continue; } /* Subdirectory with "shadow" in dir name: grab all *.elf inside */ if(strstr(nl,"shadow")){ DIR *sub=opendir(fp); if(sub){ struct dirent *se; while((se=readdir(sub))&&countd_name[k]&&k<63){snl[k]=(char)tolower((unsigned char)se->d_name[k]);k++;} if(IS_ELF(snl)&&strstr(snl,"shadowmount")) snprintf(paths[count++],SM_EPATH,"%s/%s",fp,se->d_name); } closedir(sub); } } } closedir(d); } } /* /data/shadowmount: direct scan, only *.elf ending files */ { DIR *d=opendir("/data/shadowmount"); if(d){ struct dirent *e; while((e=readdir(d))&&countd_name[0]=='.') continue; char nl[64]={0}; int j=0; while(e->d_name[j]&&j<63){nl[j]=(char)tolower((unsigned char)e->d_name[j]);j++;} if(IS_ELF(nl)&&strstr(nl,"shadowmount")) snprintf(paths[count++],SM_EPATH,"/data/shadowmount/%s",e->d_name); } closedir(d); } } /* USB roots /mnt/usb0 - /mnt/usb7: direct scan, only *.elf ending files */ for(int ui=0;ui<8&&countd_name[0]=='.') continue; char nl[64]={0}; int j=0; while(e->d_name[j]&&j<63){nl[j]=(char)tolower((unsigned char)e->d_name[j]);j++;} if(IS_ELF(nl)&&strstr(nl,"shadowmount")) snprintf(paths[count++],SM_EPATH,"%s/%s",usb,e->d_name); } closedir(d); } #undef IS_ELF /* extra scan directories from prefs */ #define IS_ELF2(s) (strlen(s)>4&&strcmp((s)+strlen(s)-4,".elf")==0) { SMPrefs prefs; read_prefs(&prefs); for(int pi=0;pid_name[0]=='.') continue; char nl[64]={0}; int j=0; while(e->d_name[j]&&j<63){nl[j]=(char)tolower((unsigned char)e->d_name[j]);j++;} if(IS_ELF2(nl)&&strstr(nl,"shadowmount")) snprintf(paths[count++],SM_EPATH,"%s/%s",prefs.extra_scan[pi],e->d_name); } closedir(d); } } #undef IS_ELF2 return count; } /* Returns 1 if buf looks like a valid ShadowMount INI config */ static int is_valid_sm_cfg(const char *buf, size_t len){ if(!buf||len<10||len>65536) return 0; for(size_t i=0;i" \ "" \ "", \ id,lstr,id,nm,(chk)?"checked":"",id) #define NF(nm,lstr,mn,mx,val) H( \ "
" \ "
", \ lstr,mn,mx,nm,mn,mx,val) #define TF(nm,lstr,val,ph) H( \ "
" \ "
", \ lstr,nm,val,ph) #define SEC(dl,el) H("
" \ "▶%s",T(dl,el)) /* CSS */ H("" "" "SMPlusGui by Karner" "" "" "SM" "\">" "
"); /* Lucide SVG icon sprite — defined once, reused via */ H("" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""); /* Topbar + Status in einer Zeile */ /* ShadowMount Prozess-Check via find_pid */ const char *sm_proc_names[] = { "shadowmount.elf","ShadowMount.elf", "shadowmountplus.elf","ShadowMountPlus.elf", "shadowmount","ShadowMount", NULL }; int sm_running = 0; for(int pi=0; sm_proc_names[pi]; pi++){ if(find_pid(sm_proc_names[pi])>0){ sm_running=1; break; } } /* Fallback: API erreichbar? */ if(!sm_running){ int ts=socket(AF_INET,SOCK_STREAM,0); if(ts>=0){ struct timeval tv={1,0}; setsockopt(ts,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); setsockopt(ts,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); struct sockaddr_in ta; memset(&ta,0,sizeof(ta)); ta.sin_family=AF_INET; ta.sin_port=htons(10101); ta.sin_addr.s_addr=inet_addr("127.0.0.1"); if(connect(ts,(struct sockaddr*)&ta,sizeof(ta))==0) sm_running=1; close(ts); } } H("
" "
" "" "

SMPlusGui v" SMPLUS_VERSION "

" "
PORT " HTTP_PORT "
" "
" "" "ShadowMount %s
", sm_running ? "#10b981" : "#f87171", sm_running ? "#10b981" : "#f87171", sm_running ? "animation:pulse 2s infinite;" : "", sm_running ? L(LS_RUNNING) : L(LS_NOT_RUNNING)); if(sm_running && strcmp(sm_ver,"nicht aktiv")!=0) H("
v %s
", sm_ver); else H(""); H("", sm_running ? "stop" : "start", sm_running ? "stop" : "start", sm_running ? "Stop" : "Start"); H("
"); /* close left flex */ H("
" "AUTO" "DE" "EN" "FR" "ES" "
" "
", is_auto?"active":"", !is_auto&&strcmp(lang,"de")==0?"active":"", !is_auto&&strcmp(lang,"en")==0?"active":"", !is_auto&&strcmp(lang,"fr")==0?"active":"", !is_auto&&strcmp(lang,"es")==0?"active":""); /* Warnung bei veralteter Version (< 1.6beta16) */ if(sm_running && strcmp(sm_ver,"nicht aktiv")!=0 && strcmp(sm_ver,"aktiv")!=0){ if(!sm_version_at_least(sm_ver,1,6,"beta",16)){ H("
" "⚠ %s %s — %s
", L(LS_WARNING_SM), sm_ver, L(LS_MIN_VER)); } } /* Pre-compute version checks before sidebar */ int has_lang = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",4); int has_pim = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",4); int has_autoremove= !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",3); int has_api = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",3); int has_fan = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",5); int has_legacy_mp = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",7); int has_emus = !sm_running || sm_version_at_least(sm_ver,1,7,"alpha",7); H("
"); H("
"); /* Panel: General / Notifications */ H("
"); H("
%s
",L(LS_NOTIFS)); SW("qm","quiet_mode",L(LS_QUIET_MODE),cfg.quiet_mode); H("
",has_lang?"":"opacity:.35;pointer-events:none;"); H("", L(LS_NOTIF_LANG),has_lang?"display:none;":""); { struct{const char*c;const char*n;}ll[]={ {"auto","auto"}, {"ar-SA","العربية (ar-SA)"},{"cs-CZ","Čeština (cs-CZ)"}, {"da-DK","Dansk (da-DK)"},{"de-DE","Deutsch (de-DE)"},{"el-GR","Ελληνικά (el-GR)"}, {"en-GB","English UK (en-GB)"},{"en-US","English US (en-US)"}, {"es-ES","Español España (es-ES)"},{"es-MX","Español México (es-MX)"}, {"fi-FI","Suomi (fi-FI)"}, {"fr-CA","Français Canada (fr-CA)"},{"fr-FR","Français France (fr-FR)"}, {"hu-HU","Magyar (hu-HU)"},{"id-ID","Indonesia (id-ID)"}, {"it-IT","Italiano (it-IT)"},{"ja-JP","日本語 (ja-JP)"},{"ko-KR","한국어 (ko-KR)"}, {"nl-NL","Nederlands (nl-NL)"},{"no-NO","Norsk (no-NO)"},{"pl-PL","Polski (pl-PL)"}, {"pt-BR","Português Brasil (pt-BR)"},{"pt-PT","Português Portugal (pt-PT)"}, {"ro-RO","Română (ro-RO)"},{"ru-RU","Русский (ru-RU)"},{"sv-SE","Svenska (sv-SE)"}, {"th-TH","ภาษาไทย (th-TH)"},{"tr-TR","Türkçe (tr-TR)"}, {"uk-UA","Українська (uk-UA)"},{"vi-VN","Tiếng Việt (vi-VN)"}, {"zh-CN","中文简体 (zh-CN)"},{"zh-TW","中文繁體 (zh-TW)"}, {NULL,NULL}}; H("
"); } H("
"); /* Panel: Mounting */ /* ── Autostart panel ── */ { SMPrefs prefs; read_prefs(&prefs); const char *elfname=L(LS_SELECT_ELF); if(prefs.preferred_elf[0]){const char *sl=strrchr(prefs.preferred_elf,'/');elfname=sl?sl+1:prefs.preferred_elf;} H("
"); H("
Autostart
"); H("
" "" "
", L(LS_AUTOSTART), prefs.auto_start?" checked":""); H("
" "
", L(LS_PREF_ELF), elfname); H("
%s
",L(LS_EXTRA_SCAN)); H("

ⓘ %s

",L(LS_SCAN_DEF_HINT)); H("
"); for(int i=0;i" "
",prefs.extra_scan[i]); H("
",L(LS_ADD_PATH)); /* icon option */ H("
%s
",L(LS_ICON_FRONT)); H("
" "" "
", L(LS_ICON_FRONT_LABEL), prefs.icon_always_front?" checked":""); /* always-restart option */ H("
" "" "
", L(LS_ALWAYS_RESTART_SM), prefs.always_restart_sm?" checked":""); H("
"); } /* close autostart section + panel-auto */ /* Panel: Auto-Remove */ H("
",has_autoremove?"1":"0.35",has_autoremove?"auto":"none"); H("
Auto-Remove
"); H("
" "" "
", L(LS_RM_MISSING),has_autoremove?"display:none;":"",cfg.auto_remove_missing_games?"checked":""); H("
" "" "
", L(LS_RM_DLC),has_autoremove?"display:none;":"",cfg.auto_remove_games_with_dlc?"checked":""); H("
" "
", L(LS_DELAY_SEC),has_autoremove?"display:none;":"",cfg.auto_remove_missing_delay_seconds); H("
"); /* close section + ar-wrap + panel-arm */ /* Panel: Mounting */ H("
"); H("
Mounting
"); SW("ro","mount_read_only",L(LS_READ_ONLY),cfg.mount_read_only); SW("fm","force_mount",L(LS_MOUNT_DAMAGED),cfg.force_mount); H("

⚠ %s

",L(LS_FORCE_HINT)); { H("
",has_pim?"":"opacity:.35;pointer-events:none;"); H("", L(LS_KEEP_IMGS),has_pim?"display:none":""); H("" "
",cfg.persistent_image_mounts?"checked":""); } SW("aia","app_install_all",L(LS_BATCH_REG),cfg.app_install_all); H("
"); /* Panel: Scan */ H("
"); H("
Scan
"); H("
" "
", L(LS_SCAN_DEPTH), cfg.scan_depth==1?" selected":"", L(LS_DEPTH_1), cfg.scan_depth==2?" selected":"", L(LS_DEPTH_2)); NF("scan_interval_seconds",L(LS_SCAN_INTV),"1","3600",cfg.scan_interval_seconds); NF("stability_wait_seconds",L(LS_STAB_WAIT),"0","3600",cfg.stability_wait_seconds); H("
%s
",L(LS_SCAN_PATHS)); H("

ⓘ %s

",L(LS_SCANPATH_HINT)); H("
"); for(int i=0;i
",cfg.scanpaths[i]); H("
",L(LS_ADD_PATH)); H("
"); /* Panel: kstuff */ H("
"); H("
kstuff
"); SW("kcd","kstuff_crash_detection",L(LS_CRASH_TUNE),cfg.kstuff_crash_detection); H("

ⓘ %s

",L(LS_KSTUFF_TOG)); NF("kstuff_pause_delay_image_seconds",L(LS_PAUSE_IMG),"1","3600",cfg.kstuff_pause_delay_image_seconds); NF("kstuff_pause_delay_direct_seconds",L(LS_PAUSE_DIR),"1","3600",cfg.kstuff_pause_delay_direct_seconds); H("

ⓘ %s

",L(LS_KSTUFF_DLY)); H("
kstuff_no_pause
"); H("

📄 Beispiel:  PPSA12345  →  %s

",L(LS_KSTUFF_NOPAUSE)); H("
"); for(int i=0;i
",cfg.kstuff_no_pause[i]); H("
"); H("
kstuff_delay
"); H("

📄 Beispiel:  PPSA12345:30  (%s)

",L(LS_KSTUFF_FMT)); H("
"); for(int i=0;i
",cfg.kstuff_delay[i]); H("
"); H("
"); /* close kstuff section + panel-kst */ /* Panel: Fakelib */ H("
"); H("
Fakelib
"); SW("gf","global_fakelib",L(LS_EN_GLOBAL_FL),cfg.global_fakelib); H("

ⓘ %s

",L(LS_FAKELIB_HINT)); TF("global_fakelib_path",L(LS_FL_PATH),cfg.global_fakelib_path,"/data/shadowmount/fakelib"); H("
" "
", L(LS_PRIORITY), strcmp(cfg.global_fakelib_priority,"game")==0?" selected":"", L(LS_PRIO_GAME), strcmp(cfg.global_fakelib_priority,"global")==0?" selected":"", L(LS_PRIO_GLOBAL)); H("
global_fakelib_exclude
"); H("

📄 Beispiel:  PPSA12345  →  %s

",L(LS_FAKELIB_EXCL)); H("
"); for(int i=0;i
",cfg.global_fakelib_exclude[i]); H("
"); /* Emulator settings (1.7alpha7+) */ H("
%s
", has_emus?"":"opacity:.35;pointer-events:none",L(LS_EMUS)); H("
" "" "
", L(LS_UPDATE_EMUS),has_emus?"display:none;":"",cfg.update_emulators?"checked":""); H("
" "
", L(LS_EMUS_PATH),has_emus?"display:none;":"",cfg.emulators_path); H("
" "" "
", has_emus?"display:none;":"",cfg.auto_update_ampr?"checked":""); H("
" "
", has_emus?"display:none;":"",cfg.ampr_update_url); H("
"); H("
"); /* close fakelib section + panel-fkl */ /* Panel: Backend */ H("
"); H("
Backend
"); H("

ⓘ %s

",L(LS_SECTOR_HINT)); H("
" "
", L(LS_EXFAT_BE), strcmp(cfg.exfat_backend,"lvd")==0?" selected":"", strcmp(cfg.exfat_backend,"md")==0?" selected":""); H("
" "
", L(LS_UFS_BE), strcmp(cfg.ufs_backend,"lvd")==0?" selected":"", strcmp(cfg.ufs_backend,"md")==0?" selected":""); NF("lvd_exfat_sector_size",L(LS_LVD_EXFAT_S),"512","65536",cfg.lvd_exfat_sector_size); NF("lvd_ufs_sector_size",L(LS_LVD_UFS_S),"512","65536",cfg.lvd_ufs_sector_size); NF("lvd_pfs_sector_size",L(LS_LVD_PFS_S),"512","65536",cfg.lvd_pfs_sector_size); NF("md_exfat_sector_size",L(LS_MD_EXFAT_S),"512","65536",cfg.md_exfat_sector_size); NF("md_ufs_sector_size",L(LS_MD_UFS_S),"512","65536",cfg.md_ufs_sector_size); /* fan_target_temperature belongs here as SM hardware setting */ H("
fan_target_temperature
"); H("

ⓘ %s

",L(LS_FAN_HINT)); H("
",has_fan?"":"opacity:.35;pointer-events:none"); H("
", has_fan?"display:none;":""); H("
"); /* Legacy mount profiles (1.7alpha7+) */ H("
%s
", has_legacy_mp?"":"opacity:.35;pointer-events:none",L(LS_LEGACY_MOUNT)); H("
" "" "
", has_legacy_mp?"display:none;":"",cfg.legacy_mount_ufs?"checked":""); H("
" "" "
", has_legacy_mp?"display:none;":"",cfg.legacy_mount_exfat?"checked":""); H("
" "" "
", has_legacy_mp?"display:none;":"",cfg.legacy_mount_pfs?"checked":""); H("
" "" "
", has_legacy_mp?"display:none;":"",cfg.legacy_mount_pfsc?"checked":""); H("
" "" "
", has_legacy_mp?"display:none;":"",cfg.nested_pfs_index_cache?"checked":""); H("
"); H("
"); /* close backend section + panel-bkd */ /* Panel: API */ H("
", has_api?"1":"0.35",has_api?"auto":"none"); H("
API
"); H("
" "
", L(LS_BIND_ADDR),has_api?"display:none;":"", strcmp(cfg.api_bind_address,"127.0.0.1")==0?" selected":"", L(LS_LOCAL_ONLY), strcmp(cfg.api_bind_address,"0.0.0.0")==0?" selected":"", L(LS_ALL_IFACES)); H("
" "
", L(LS_PORT),has_api?"display:none;":"",cfg.api_port); H("
"); /* close section + api-wrap + panel-api */ /* Panel: Config/Backup */ { const char *bdir=BAK_DIR; mkdir("/data/SMPlusGui",0755); mkdir(bdir,0755); /* collect and sort local backups */ char lnames[100][64]; int ln=0; { DIR *dp=opendir(bdir); if(dp){ struct dirent *de; while((de=readdir(dp))&&ln<100){ char *nm=de->d_name; size_t nl=strlen(nm); if(nm[0]=='.'||nl<8||strcmp(nm+nl-4,".ini")!=0||strncmp(nm,"cfg_",4)!=0) continue; strncpy(lnames[ln++],nm,63); } closedir(dp); } } for(int i=0;i2){strncpy(usb_path,p,31);usb_ok=1;} } } char usb_dir[64]={0}; char unames[100][64]; int un=0; if(usb_ok){ snprintf(usb_dir,sizeof(usb_dir),"%s/SMPlusGui",usb_path); DIR *udp=opendir(usb_dir); if(udp){ struct dirent *ude; while((ude=readdir(udp))&&un<100){ char *nm=ude->d_name; size_t nl=strlen(nm); if(nm[0]=='.'||nl<8||strcmp(nm+nl-4,".ini")!=0||strncmp(nm,"cfg_",4)!=0) continue; strncpy(unames[un++],nm,63); } closedir(udp); } for(int i=0;i
"); /* ═══ Aktuelle Config ═══ */ H("
%s
",L(LS_CURR_CONFIG)); H("

ⓘ %s

",CONFIG_PATH); H("
"); H("",L(LS_DL_CURRENT)); H("",L(LS_USB_EXPORT)); H("",L(LS_USB_IMPORT)); H("",L(LS_IMPORT_PC)); H(""); H("
"); /* ═══ Interne Backups — komplett eigenständig ═══ */ H("
%s
",L(LS_INTERN_BAKS)); H("

ⓘ %s

",bdir); H("
"); H("Name:"); H("",L(LS_BAK_NAME_PH)); H("",L(LS_BAK_CREATE)); H("",L(LS_BAK_IMPORT_PC)); H("
"); if(ln==0) H("

%s

",L(LS_BAK_NO_BAKS)); else{ H("
"); for(int i=0;i=22){ snprintf(disp,sizeof(disp),"%.2s.%.2s.%.4s %.2s:%.2s",nm+10,nm+8,nm+4,nm+13,nm+15); if(nlen>23){ char cn[32]={0}; strncpy(cn,nm+20,31); char *dot=strrchr(cn,'.'); if(dot)*dot=0; char *p2=cn; while(*p2){if(*p2=='_')*p2=' ';p2++;} strncat(disp," ",sizeof(disp)-strlen(disp)-1); strncat(disp,cn,sizeof(disp)-strlen(disp)-1); } } else strncpy(disp,nm,31); H("
",nm); H("%s",disp); H("",L(LS_BAK_RESTORE),nm); H("",L(LS_BAK_DL),nm); H("",L(LS_USB_EXPORT),nm); H("",L(LS_MOVE_TO_USB),nm); H("",L(LS_BAK_DELETE),nm); H("
"); } H("
"); } /* Intern-Fußzeile */ H("
"); H("",L(LS_BAK_DL_ALL)); if(usb_ok&&ln>0){ H("",L(LS_BAK_USB_ALL)); H("",L(LS_MOVE_ALL_USB)); } if(ln>0) H("",L(LS_BAK_DEL_ALL)); H("
"); /* ═══ USB-Backups — identische Struktur, gespiegelt ═══ */ H("
%s
",L(LS_USB_BAKS)); if(!usb_ok){ H("

✗ %s

",L(LS_USB_NO_DRIVE)); } else { H("

ⓘ %s

",usb_dir); H("
"); H("Name:"); H("",L(LS_BAK_NAME_PH)); H("",L(LS_BAK_CREATE)); H("",L(LS_BAK_IMPORT_PC)); H("
"); if(un==0) H("

%s

",L(LS_USB_NO_BAKS)); else{ H("
"); for(int i=0;i=22){ snprintf(disp,sizeof(disp),"%.2s.%.2s.%.4s %.2s:%.2s",nm+10,nm+8,nm+4,nm+13,nm+15); if(nlen>23){ char cn[32]={0}; strncpy(cn,nm+20,31); char *dot=strrchr(cn,'.'); if(dot)*dot=0; char *p2=cn; while(*p2){if(*p2=='_')*p2=' ';p2++;} strncat(disp," ",sizeof(disp)-strlen(disp)-1); strncat(disp,cn,sizeof(disp)-strlen(disp)-1); } } else strncpy(disp,nm,31); H("
"); H("%s",disp); H("",L(LS_BAK_RESTORE),nm); H("",L(LS_BAK_DL),nm); H("",L(LS_COPY_TO_LOCAL),nm); H("",L(LS_MOVE_FROM_USB),nm); H("",L(LS_BAK_DELETE),nm); H("
"); } H("
"); } /* USB-Fußzeile — gleiche Labels wie Intern */ H("
"); H("",L(LS_BAK_DL_ALL)); if(un>0){ H("",L(LS_COPY_ALL_LOCAL)); H("",L(LS_MOVE_ALL_LOCAL)); } if(un>0) H("",L(LS_BAK_DEL_ALL)); H("
"); } #undef _BW6 #undef _BW4 #undef _BA4 #undef _BR4 #undef _BGN #undef _BRD H("
"); } /* Panel: Image Overrides */ H("
"); H("
%s
",L(LS_IMG_OVERRIDES)); H("

📄 Beispiel:  PPSA12345.exfat  %s  PPSA12345.ffpfs

",L(LS_OR)); H("
"); for(int i=0;i
",cfg.image_ro[i]); H("
"); H("
image_rw
"); H("

📄 Beispiel:  PPSA12345.exfat  →  %s

",L(LS_MOUNTED_RW)); H("
"); for(int i=0;i
",cfg.image_rw[i]); H("
"); H("
image_sector
"); H("

📄 Beispiel:  PPSA12345.exfat:65536  (%s)

",L(LS_SECTOR_FMT)); H("
"); for(int i=0;i
",cfg.image_sector[i]); H(""); H(""); /* Panel: Manual List */ H("
"); H("
%s
",L(LS_MANUAL)); H("

ⓘ %s

", L(LS_MANUAL_HINT)); H("

📄 Beispiel:  /mnt/usb0/PPSA12345  %s  /mnt/usb0/PPSA12345.ffpkg

",L(LS_OR)); H("
"); for(int i=0;i
",manual_entries[i]); H("
",L(LS_ADD_ENTRY)); H("
"); /* Panel: Spiele & Images (kombiniert) */ H("
"); H("
%s
",L(LS_GAMES)); if(!has_api) H("

ab 1.7alpha3

"); H("
" "" "
"); H(""); H("

%s

",L(LS_LOADING)); H("
"); /* Panel: Debug Log */ H("
"); H("
Log
"); SW("dbg","debug",L(LS_DEBUG_LOG),cfg.debug); H("
" "/data/shadowmount/debug.log" "" "",L(LS_CLEAR_LOG)); H("
"); { const char *cats[]={"All","NOTIFY","SCAN","DB","GAME","KSTUFF","ERR",NULL}; const char *keys[]={ "","NOTIFY","SCAN","DB","GAME","KSTUFF","ERR",NULL}; H("
"); for(int ci=0;cats[ci];ci++) H("", keys[ci],keys[ci],ci==0?"var(--accent)":"var(--dim)",cats[ci]); H("
"); } H("
%s
", L(LS_LOADING)); H("
"); /* Panel: Raw config.ini editor */ H("
"); /* Toolbar: all buttons same dim-border style, no inline msg (toast is fixed position) */ H("
"); H("
" ICO("pen") " config.ini
"); H("", "background:transparent;border:1px solid var(--dim);border-radius:6px;color:var(--dim);padding:5px 10px;cursor:pointer;font-size:.76rem;white-space:nowrap;"); H("", "background:var(--accent);border:1px solid var(--accent);border-radius:6px;color:#021a18;font-weight:600;padding:5px 14px;cursor:pointer;font-size:.76rem;white-space:nowrap;"); H("", "background:transparent;border:1px solid var(--accent);border-radius:6px;color:var(--accent);padding:5px 10px;cursor:pointer;font-size:.76rem;white-space:nowrap;"); H("", "background:transparent;border:1px solid var(--dim);border-radius:6px;color:var(--dim);padding:5px 10px;cursor:pointer;font-size:.76rem;white-space:nowrap;"); H("
"); /* Search bar: integrated box with icon */ H("
"); H("" ICO("search") ""); /* input wrapper: relative so × button can sit inside the field */ H("
"); H(""); H(""); H("
"); H(""); H(""); H("
"); H(""); H("

ⓘ %s

",CONFIG_PATH); /* textarea: PS5 keyboard handled via visualViewport listener in JS */ H(""); H("
"); H("
"); /* close content + layout divs */ H(""); H("
"); H("", L(LS_SAVE)); H("", L(LS_RESTORE_DEF)); H(""); H("
"); H("
"); H("", L(LS_ELF_NOT_FOUND), L(LS_SELECT_SM), L(LS_CANCEL), L(LS_SAVED), L(LS_ERROR_STR), L(LS_PS5_NO_DL), L(LS_PS5_NO_UL)); H("", L(LS_USB_NO_DRIVE),L(LS_USB_EXPORT_OK),L(LS_USB_IMPORT_OK),L(LS_IMPORT_OK),L(LS_BAD_CONFIG), L(LS_CONFIRM_RESTORE),L(LS_CONFIRM_DELETE),L(LS_CONFIRM_DEL_ALL), L(LS_NO_USB_CONFIG),L(LS_USB_NO_BAKS),L(LS_BAK_SKIPPED), L(LS_CONFIRM_DEL_USB),L(LS_CONFIRM_MOVE)); /* JS */ H("", L(LS_RESET_CONFIRM)); #undef H #undef SW #undef NF #undef TF #undef SEC mg_http_reply(c,200,"Content-Type: text/html; charset=utf-8\r\n","%s",html); free(html); } else if(mg_match(hm->uri,mg_str("/api/status"),NULL)){ char sv[64]; get_sm_version(sv,sizeof(sv)); /* Prozess-Check */ const char *names[]={"shadowmount.elf","ShadowMount.elf", "shadowmountplus.elf","ShadowMountPlus.elf","shadowmount","ShadowMount",NULL}; int run=0; for(int pi=0;names[pi];pi++) if(find_pid(names[pi])>0){run=1;break;} if(!run){ int ts=socket(AF_INET,SOCK_STREAM,0); if(ts>=0){ struct timeval tv={1,0}; setsockopt(ts,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); setsockopt(ts,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); struct sockaddr_in ta; memset(&ta,0,sizeof(ta)); ta.sin_family=AF_INET; ta.sin_port=htons(10101); ta.sin_addr.s_addr=inet_addr("127.0.0.1"); if(connect(ts,(struct sockaddr*)&ta,sizeof(ta))==0) run=1; close(ts); } } char resp[256]; snprintf(resp,sizeof(resp), "{\"running\":%s,\"version\":\"%s\",\"status\":\"%s\"}", run?"true":"false", sv, run ? L(LS_RUNNING) : L(LS_NOT_RUNNING)); mg_http_reply(c,200, "Content-Type: application/json\r\n" "Cache-Control: no-cache\r\n","%s",resp); } else if(mg_match(hm->uri,mg_str("/save"),NULL)&&mg_strcmp(hm->method,mg_str("POST"))==0){ ShadowConfig nc; set_defaults(&nc); char buf[PATH_LEN]; #define GB(name,field) if(mg_http_get_var(&hm->body,name,buf,sizeof(buf))>0) nc.field=1; #define GI(name,field) if(mg_http_get_var(&hm->body,name,buf,sizeof(buf))>0) nc.field=atoi(buf); #define GS(name,field) if(mg_http_get_var(&hm->body,name,buf,sizeof(buf))>0) strncpy(nc.field,buf,sizeof(nc.field)-1); GB("debug",debug) GB("quiet_mode",quiet_mode) GS("language",language) GS("api_bind_address",api_bind_address) GI("api_port",api_port) GS("fan_target_temperature",fan_target_temperature) GB("mount_read_only",mount_read_only) GB("force_mount",force_mount) GB("persistent_image_mounts",persistent_image_mounts) GB("app_install_all",app_install_all) GB("recursive_scan",recursive_scan) GI("scan_depth",scan_depth) GI("scan_interval_seconds",scan_interval_seconds) GI("stability_wait_seconds",stability_wait_seconds) GB("auto_remove_missing_games",auto_remove_missing_games) GB("auto_remove_games_with_dlc",auto_remove_games_with_dlc) GI("auto_remove_missing_delay_seconds",auto_remove_missing_delay_seconds) GB("kstuff_game_auto_toggle",kstuff_game_auto_toggle) GB("kstuff_crash_detection",kstuff_crash_detection) GI("kstuff_pause_delay_image_seconds",kstuff_pause_delay_image_seconds) GI("kstuff_pause_delay_direct_seconds",kstuff_pause_delay_direct_seconds) GB("backport_fakelib",backport_fakelib) GB("global_fakelib",global_fakelib) GS("global_fakelib_path",global_fakelib_path) GS("global_fakelib_priority",global_fakelib_priority) GS("exfat_backend",exfat_backend) GS("ufs_backend",ufs_backend) GB("legacy_mount_ufs",legacy_mount_ufs) GB("legacy_mount_exfat",legacy_mount_exfat) GB("legacy_mount_pfs",legacy_mount_pfs) GB("legacy_mount_pfsc",legacy_mount_pfsc) GB("nested_pfs_index_cache",nested_pfs_index_cache) GB("update_emulators",update_emulators) GS("emulators_path",emulators_path) GB("auto_update_ampr",auto_update_ampr) GS("ampr_update_url",ampr_update_url) GI("lvd_exfat_sector_size",lvd_exfat_sector_size) GI("lvd_ufs_sector_size",lvd_ufs_sector_size) GI("lvd_pfs_sector_size",lvd_pfs_sector_size) GI("md_exfat_sector_size",md_exfat_sector_size) GI("md_ufs_sector_size",md_ufs_sector_size) #undef GB #undef GI #undef GS char manual_save[MAX_MANUAL][PATH_LEN]; int manual_save_count=0; struct mg_str key,val; size_t ofs=0; while((ofs=next_kv_param(hm->body,ofs,&key,&val))>0){ char dec[PATH_LEN]; dec[0]=0; if(val.len>0) mg_url_decode(val.buf,val.len,dec,sizeof(dec),0); #define LISTPUSH(kname,arr,cnt,max,len) \ if(mg_strcmp(key,mg_str(kname))==0&&strlen(dec)>0&&cnt0&&nc.path_count0) strncpy(nc.scanpaths[nc.path_count++],dec,PATH_LEN-1); } LISTPUSH("image_ro[]", nc.image_ro, nc.image_ro_count, MAX_IMG, IMG_LEN) LISTPUSH("image_rw[]", nc.image_rw, nc.image_rw_count, MAX_IMG, IMG_LEN) LISTPUSH("image_sector[]", nc.image_sector, nc.image_sector_count, MAX_IMG, IMG_LEN) LISTPUSH("kstuff_no_pause[]", nc.kstuff_no_pause, nc.kstuff_no_pause_count, MAX_LIST, ID_LEN) LISTPUSH("kstuff_delay[]", nc.kstuff_delay, nc.kstuff_delay_count, MAX_LIST, ID_LEN+8) LISTPUSH("global_fakelib_exclude[]", nc.global_fakelib_exclude, nc.global_fakelib_exclude_count, MAX_LIST, PATH_LEN) LISTPUSH("manual[]", manual_save, manual_save_count, MAX_MANUAL, PATH_LEN) #undef LISTPUSH } /* Write manual.lst (user-managed; SM only removes entries on game uninstall) */ { FILE *mf=fopen(MANUAL_LST,"w"); if(mf){ for(int j=0;juri,mg_str("/api/sm/scan"),NULL)){ char elfs[SM_MAX_ELFS][SM_EPATH]; int n=sm_find_elfs(elfs); char json[4096]; int jp=0; jp+=snprintf(json+jp,sizeof(json)-jp,"{\"count\":%d,\"paths\":[",n); for(int i=0;iuri,mg_str("/api/sm/stop"),NULL)){ const char *names[]={"shadowmount.elf","ShadowMount.elf", "shadowmountplus.elf","ShadowMountPlus.elf","shadowmount","ShadowMount",NULL}; int killed=0; for(int pi=0;names[pi];pi++){ pid_t p=find_pid(names[pi]); if(p>0){kill(p,SIGTERM);killed=1;} } mg_http_reply(c,200,"Content-Type: application/json\r\nCache-Control: no-cache\r\n", "{\"ok\":%s}",killed?"true":"false"); } else if(mg_match(hm->uri,mg_str("/api/sm/start"),NULL)){ char lang[8]; read_lang(lang,sizeof(lang)); int en=(strcmp(lang,"en")==0||strcmp(lang,"fr")==0||strcmp(lang,"es")==0); char elf_path[SM_EPATH]={0}; mg_http_get_var(&hm->query,"path",elf_path,sizeof(elf_path)); if(!elf_path[0]){ char elfs[SM_MAX_ELFS][SM_EPATH]; if(sm_find_elfs(elfs)>0) strncpy(elf_path,elfs[0],SM_EPATH-1); } if(!elf_path[0]){ notify(en?"SM ELF not found":"SM ELF nicht gefunden"); mg_http_reply(c,200,"Content-Type: application/json\r\nCache-Control: no-cache\r\n","{\"ok\":false,\"err\":\"elf_not_found\"}"); return; } /* respond immediately, send ELF on next poll to avoid browser timeout */ strncpy(smg_pending_elf, elf_path, SM_EPATH-1); /* do NOT save preferred ELF here — managed by auto-start panel only */ mg_http_reply(c,200,"Content-Type: application/json\r\nCache-Control: no-cache\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/backup"),NULL)){ const char *bdir=BAK_DIR; mkdir("/data/SMPlusGui",0755); mkdir(bdir,0755); time_t now=time(NULL); struct tm *lt=localtime(&now); char cname[32]={0}; mg_http_get_var(&hm->query,"name",cname,sizeof(cname)); /* sanitize custom name */ for(char *p=cname;*p;p++) if(!isalnum(*p)&&*p!='_'&&*p!='-')*p='_'; char fname[128]; if(cname[0]) snprintf(fname,sizeof(fname),"%s/cfg_%04d%02d%02d_%02d%02d%02d_%s.ini", bdir,lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday, lt->tm_hour,lt->tm_min,lt->tm_sec,cname); else snprintf(fname,sizeof(fname),"%s/cfg_%04d%02d%02d_%02d%02d%02d.ini", bdir,lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday, lt->tm_hour,lt->tm_min,lt->tm_sec); FILE *src=fopen(CONFIG_PATH,"r"); if(!src){mg_http_reply(c,404,"","no config");return;} FILE *dst=fopen(fname,"w"); if(!dst){fclose(src);mg_http_reply(c,500,"","write err");return;} char buf[512]; size_t n; while((n=fread(buf,1,sizeof(buf),src))>0) fwrite(buf,1,n,dst); fclose(src); fclose(dst); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/restore"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")){{mg_http_reply(c,400,"","bad param");return;}} const char *bdir=BAK_DIR; char src_path[128]; snprintf(src_path,sizeof(src_path),"%s/%s",bdir,f); FILE *src=fopen(src_path,"r"); if(!src){mg_http_reply(c,404,"","not found");return;} FILE *dst=fopen(CONFIG_PATH,"w"); if(!dst){fclose(src);mg_http_reply(c,500,"","write err");return;} char buf[512]; size_t n; while((n=fread(buf,1,sizeof(buf),src))>0) fwrite(buf,1,n,dst); fclose(src); fclose(dst); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/upload-direct"),NULL)){ if(hm->body.len==0||hm->body.len>65536){mg_http_reply(c,400,"","bad body");return;} if(!is_valid_sm_cfg(hm->body.buf,hm->body.len)){ mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} FILE *fp=fopen(CONFIG_PATH,"w"); if(!fp){mg_http_reply(c,500,"","write err");return;} fwrite(hm->body.buf,1,hm->body.len,fp); fclose(fp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/upload-bak-usb"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")){{mg_http_reply(c,400,"","bad param");return;}} if(hm->body.len>0&&!is_valid_sm_cfg(hm->body.buf,hm->body.len)){ mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char udir[64]; snprintf(udir,sizeof(udir),"%s/SMPlusGui",usb); mkdir(udir,0755); char fname[128]; if(strncmp(f,"cfg_",4)!=0){ time_t now=time(NULL); struct tm *lt=localtime(&now); char base[64]; strncpy(base,f,63); char *dot=strrchr(base,'.');if(dot)*dot=0; snprintf(fname,sizeof(fname),"%s/cfg_%04d%02d%02d_%02d%02d%02d_%s.ini", udir,lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday,lt->tm_hour,lt->tm_min,lt->tm_sec,base); } else snprintf(fname,sizeof(fname),"%s/%s",udir,f); FILE *fp=fopen(fname,"w"); if(!fp){mg_http_reply(c,500,"","write err");return;} if(hm->body.len>0) fwrite(hm->body.buf,1,hm->body.len,fp); fclose(fp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/upload-bak"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")){{mg_http_reply(c,400,"","bad param");return;}} /* add timestamp prefix if file doesn't already start with cfg_ */ char fname[128]; if(strncmp(f,"cfg_",4)!=0){ time_t now=time(NULL); struct tm *lt=localtime(&now); char base[64]; strncpy(base,f,63); char *dot=strrchr(base,'.');if(dot)*dot=0; snprintf(fname,sizeof(fname),BAK_DIR "/cfg_%04d%02d%02d_%02d%02d%02d_%s.ini", lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday,lt->tm_hour,lt->tm_min,lt->tm_sec,base); } else snprintf(fname,sizeof(fname),BAK_DIR "/%s",f); FILE *fp=fopen(fname,"w"); if(!fp){mg_http_reply(c,500,"","write err");return;} if(hm->body.len>0&&!is_valid_sm_cfg(hm->body.buf,hm->body.len)){ fclose(fp); unlink(fname); mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} if(hm->body.len>0) fwrite(hm->body.buf,1,hm->body.len,fp); fclose(fp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/usb-export"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} /* source: specific backup file or current config */ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); char fsrc[128]; if(f[0]&&!strstr(f,"..")) snprintf(fsrc,sizeof(fsrc),BAK_DIR "/%s",f); else strncpy(fsrc,CONFIG_PATH,sizeof(fsrc)-1); char dir[64]; snprintf(dir,sizeof(dir),"%s/SMPlusGui",usb); mkdir(dir,0755); char dst[128]; snprintf(dst,sizeof(dst),"%s/%s",dir,f[0]?f:"config.ini"); FILE *src=fopen(fsrc,"r"); if(!src){mg_http_reply(c,404,"","{\"ok\":false}");return;} FILE *out=fopen(dst,"w"); if(!out){fclose(src);mg_http_reply(c,500,"","{\"ok\":false}");return;} char buf[512]; size_t n; while((n=fread(buf,1,sizeof(buf),src))>0) fwrite(buf,1,n,out); fclose(src); fclose(out); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true,\"path\":\"%s\"}",dst); } else if(mg_match(hm->uri,mg_str("/api/config/usb-export-all"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char dir[64]; snprintf(dir,sizeof(dir),"%s/SMPlusGui",usb); mkdir(dir,0755); const char *bdir2=BAK_DIR; DIR *dp=opendir(bdir2); int cnt=0; if(dp){ struct dirent *de; while((de=readdir(dp))){ if(de->d_name[0]=='.'||!strstr(de->d_name,".ini")) continue; char src[128],dst[128]; char buf[512]; size_t n; snprintf(src,sizeof(src),"%s/%s",bdir2,de->d_name); snprintf(dst,sizeof(dst),"%s/%s",dir,de->d_name); FILE *fi=fopen(src,"r"); if(!fi) continue; FILE *fo=fopen(dst,"w"); if(!fo){fclose(fi);continue;} while((n=fread(buf,1,sizeof(buf),fi))>0) fwrite(buf,1,n,fo); fclose(fi); fclose(fo); cnt++; } closedir(dp); } mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true,\"count\":%d}",cnt); } else if(mg_match(hm->uri,mg_str("/api/config/delete-usb-bak"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")||strncmp(f,"cfg_",4)!=0){mg_http_reply(c,400,"","bad param");return;} char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char path[128]; snprintf(path,sizeof(path),"%s/SMPlusGui/%s",usb,f); int r=unlink(path); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":%s}",r==0?"true":"false"); } else if(mg_match(hm->uri,mg_str("/api/config/usb-copy-to-local"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")||strncmp(f,"cfg_",4)!=0){mg_http_reply(c,400,"","bad param");return;} char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char src[128]; snprintf(src,sizeof(src),"%s/SMPlusGui/%s",usb,f); FILE *fi=fopen(src,"r"); if(!fi){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false}");return;} char vbuf[65537]; size_t vn=fread(vbuf,1,65536,fi); fclose(fi); if(!is_valid_sm_cfg(vbuf,vn)){mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} mkdir("/data/SMPlusGui",0755); mkdir(BAK_DIR,0755); char dst[128]; snprintf(dst,sizeof(dst),BAK_DIR "/%s",f); FILE *fo=fopen(dst,"w"); if(!fo){mg_http_reply(c,500,"Content-Type: application/json\r\n","{\"ok\":false}");return;} fwrite(vbuf,1,vn,fo); fclose(fo); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/backup-to-usb"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char ud[64]; snprintf(ud,sizeof(ud),"%s/SMPlusGui",usb); mkdir(ud,0755); time_t now=time(NULL); struct tm *lt=localtime(&now); char cname[32]={0}; mg_http_get_var(&hm->query,"name",cname,sizeof(cname)); for(char *p=cname;*p;p++) if(!isalnum(*p)&&*p!='_'&&*p!='-')*p='_'; char fname[128]; if(cname[0]) snprintf(fname,sizeof(fname),"%s/cfg_%04d%02d%02d_%02d%02d%02d_%s.ini", ud,lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday,lt->tm_hour,lt->tm_min,lt->tm_sec,cname); else snprintf(fname,sizeof(fname),"%s/cfg_%04d%02d%02d_%02d%02d%02d.ini", ud,lt->tm_year+1900,lt->tm_mon+1,lt->tm_mday,lt->tm_hour,lt->tm_min,lt->tm_sec); FILE *src=fopen(CONFIG_PATH,"r"); if(!src){mg_http_reply(c,404,"","no config");return;} FILE *dst=fopen(fname,"w"); if(!dst){fclose(src);mg_http_reply(c,500,"","write err");return;} char buf2[512]; size_t n2; while((n2=fread(buf2,1,sizeof(buf2),src))>0) fwrite(buf2,1,n2,dst); fclose(src); fclose(dst); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/delete-all-usb-bak"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}");return;} char ud[64]; snprintf(ud,sizeof(ud),"%s/SMPlusGui",usb); DIR *dp=opendir(ud); if(dp){ struct dirent *de; char pp[128]; while((de=readdir(dp))) if(de->d_name[0]!='.'&&strncmp(de->d_name,"cfg_",4)==0&&strstr(de->d_name,".ini")){ snprintf(pp,sizeof(pp),"%s/%s",ud,de->d_name); unlink(pp);} closedir(dp); } mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/usb-status"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } mg_http_reply(c,200,"Content-Type: application/json\r\n", "{\"present\":%s,\"path\":\"%s\"}",found?"true":"false",found?usb:""); } else if(mg_match(hm->uri,mg_str("/api/config/usb-bak-list"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char usb_dir[64]; snprintf(usb_dir,sizeof(usb_dir),"%s/SMPlusGui",usb); char jbuf[2048]={0}; int jlen=0; jlen+=snprintf(jbuf+jlen,sizeof(jbuf)-jlen,"{\"ok\":true,\"files\":["); DIR *dp=opendir(usb_dir); int first=1; if(dp){ struct dirent *de; while((de=readdir(dp))&&jlen<(int)sizeof(jbuf)-80){ char *nm=de->d_name; size_t nlen=strlen(nm); if(strncmp(nm,"cfg_",4)!=0||nlen<8||strcmp(nm+nlen-4,".ini")!=0||strstr(nm,"..")) continue; if(!first) jlen+=snprintf(jbuf+jlen,sizeof(jbuf)-jlen,","); jlen+=snprintf(jbuf+jlen,sizeof(jbuf)-jlen,"\"%s\"",nm); first=0; } closedir(dp); } snprintf(jbuf+jlen,sizeof(jbuf)-jlen,"]}"); mg_http_reply(c,200,"Content-Type: application/json\r\n","%s",jbuf); } else if(mg_match(hm->uri,mg_str("/api/config/usb-restore-bak"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")||strncmp(f,"cfg_",4)!=0){ mg_http_reply(c,400,"","bad param");return;} char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char src[128]; snprintf(src,sizeof(src),"%s/SMPlusGui/%s",usb,f); FILE *fi=fopen(src,"r"); if(!fi){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_file\"}");return;} char vbuf[65537]; size_t vn=fread(vbuf,1,65536,fi); fclose(fi); if(!is_valid_sm_cfg(vbuf,vn)){ mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} FILE *fo=fopen(CONFIG_PATH,"w"); if(!fo){mg_http_reply(c,500,"","{\"ok\":false}");return;} fwrite(vbuf,1,vn,fo); fclose(fo); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/usb-import-baks"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char usb_dir[64]; snprintf(usb_dir,sizeof(usb_dir),"%s/SMPlusGui",usb); mkdir("/data/SMPlusGui",0755); mkdir(BAK_DIR,0755); DIR *dp=opendir(usb_dir); int cnt=0,skip=0; if(!dp){mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true,\"count\":0,\"skipped\":0}");return;} struct dirent *de2; while((de2=readdir(dp))){ char *nm=de2->d_name; size_t nlen=strlen(nm); if(strncmp(nm,"cfg_",4)!=0||nlen<8||strcmp(nm+nlen-4,".ini")!=0||strstr(nm,"..")) continue; char src[128],dst[128]; snprintf(src,sizeof(src),"%s/%s",usb_dir,nm); snprintf(dst,sizeof(dst),BAK_DIR "/%s",nm); FILE *fi=fopen(src,"r"); if(!fi){skip++;continue;} char vbuf[65537]; size_t vn=fread(vbuf,1,65536,fi); fclose(fi); if(!is_valid_sm_cfg(vbuf,vn)){skip++;continue;} FILE *fo=fopen(dst,"w"); if(!fo){skip++;continue;} fwrite(vbuf,1,vn,fo); fclose(fo); cnt++; } closedir(dp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true,\"count\":%d,\"skipped\":%d}",cnt,skip); } else if(mg_match(hm->uri,mg_str("/api/config/usb-import"),NULL)){ char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_usb\"}");return;} char src_path[128]; snprintf(src_path,sizeof(src_path),"%s/SMPlusGui/config.ini",usb); FILE *src=fopen(src_path,"r"); if(!src){mg_http_reply(c,404,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"no_file\"}");return;} char buf[65537]; size_t n=fread(buf,1,65536,src); fclose(src); if(!is_valid_sm_cfg(buf,n)){ mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} FILE *out=fopen(CONFIG_PATH,"w"); if(!out){mg_http_reply(c,500,"","{\"ok\":false}");return;} fwrite(buf,1,n,out); fclose(out); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/delete-all-bak"),NULL)){ DIR *dp=opendir(BAK_DIR); if(!dp){mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}");return;} struct dirent *de; char path[128]; while((de=readdir(dp))) if(de->d_name[0]!='.'&&strstr(de->d_name,".ini")){ snprintf(path,sizeof(path),BAK_DIR "/%s",de->d_name); unlink(path); } closedir(dp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/delete-bak"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")){{mg_http_reply(c,400,"","bad param");return;}} char path[128]; snprintf(path,sizeof(path),BAK_DIR "/%s",f); int r=unlink(path); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":%s}",r==0?"true":"false"); } else if(mg_match(hm->uri,mg_str("/api/config/download"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); char src_path[128]; if(f[0]&&!strstr(f,"..")) snprintf(src_path,sizeof(src_path),BAK_DIR "/%s",f); else strncpy(src_path,CONFIG_PATH,sizeof(src_path)-1); FILE *cf=fopen(src_path,"r"); if(!cf){mg_http_reply(c,404,"","not found"); return;} fseek(cf,0,SEEK_END); long sz=ftell(cf); fseek(cf,0,SEEK_SET); char *buf=malloc(sz+1); if(!buf){fclose(cf);mg_http_reply(c,500,"","OOM");return;} fread(buf,1,sz,cf); fclose(cf); buf[sz]=0; const char *dlname=f[0]?f:"config.ini"; char hdr[256]; snprintf(hdr,sizeof(hdr), "Content-Type: text/plain; charset=utf-8\r\n" "Content-Disposition: attachment; filename=\"%s\"\r\n",dlname); mg_http_reply(c,200,hdr,"%s",buf); free(buf); } else if(mg_match(hm->uri,mg_str("/api/config/raw"),NULL)&&mg_strcmp(hm->method,mg_str("GET"))==0){ FILE *f=fopen(CONFIG_PATH,"r"); if(!f){mg_http_reply(c,404,"Content-Type: text/plain\r\n","# config not found");return;} fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); char *buf=malloc(sz+1); if(!buf){fclose(f);mg_http_reply(c,500,"","OOM");return;} fread(buf,1,sz,f); fclose(f); buf[sz]=0; mg_http_reply(c,200,"Content-Type: text/plain; charset=utf-8\r\nCache-Control: no-cache\r\n","%s",buf); free(buf); } else if(mg_match(hm->uri,mg_str("/api/config/raw"),NULL)&&mg_strcmp(hm->method,mg_str("POST"))==0){ if(hm->body.len==0||hm->body.len>65536){mg_http_reply(c,400,"","bad body");return;} if(!is_valid_sm_cfg(hm->body.buf,hm->body.len)){ mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"ok\":false,\"err\":\"invalid\"}");return;} FILE *fp=fopen(CONFIG_PATH,"w"); if(!fp){mg_http_reply(c,500,"","write err");return;} fwrite(hm->body.buf,1,hm->body.len,fp); fclose(fp); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/log/clear"),NULL)){ FILE *lf=fopen("/data/shadowmount/debug.log","w"); if(lf) fclose(lf); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/config/download-usb"),NULL)){ char f[64]={0}; mg_http_get_var(&hm->query,"f",f,sizeof(f)); if(!f[0]||strstr(f,"..")||strncmp(f,"cfg_",4)!=0){mg_http_reply(c,400,"","bad param");return;} char usb[32]={0}; int found=0; for(int i=0;i<=7&&!found;i++){ char p[24]; snprintf(p,sizeof(p),"/mnt/usb%d",i); DIR *d=opendir(p); if(d){ struct dirent *de; int n=0; while((de=readdir(d))&&n<4) n++; closedir(d); if(n>2){strncpy(usb,p,31);found=1;} } } if(!found){mg_http_reply(c,404,"","no usb");return;} char src_path[128]; snprintf(src_path,sizeof(src_path),"%s/SMPlusGui/%s",usb,f); FILE *cf=fopen(src_path,"r"); if(!cf){mg_http_reply(c,404,"","not found");return;} fseek(cf,0,SEEK_END); long sz=ftell(cf); fseek(cf,0,SEEK_SET); char *buf=malloc(sz+1); if(!buf){fclose(cf);mg_http_reply(c,500,"","OOM");return;} fread(buf,1,sz,cf); fclose(cf); buf[sz]=0; char hdr2[256]; snprintf(hdr2,sizeof(hdr2), "Content-Type: text/plain; charset=utf-8\r\n" "Content-Disposition: attachment; filename=\"%s\"\r\n",f); mg_http_reply(c,200,hdr2,"%s",buf); free(buf); } else if(mg_match(hm->uri,mg_str("/api/log"),NULL)){ FILE *lf=fopen("/data/shadowmount/debug.log","r"); if(!lf){ mg_http_reply(c,200,"Content-Type: text/plain; charset=utf-8\r\nCache-Control: no-cache\r\n","(log not found)"); return; } /* seek backwards to find start of last 200 lines */ fseek(lf,0,SEEK_END); long fsz=ftell(lf); int lines=0; long pos=fsz; while(pos>0&&lines<200){pos--;fseek(lf,pos,SEEK_SET);if(fgetc(lf)=='\n')lines++;} if(pos>0)fseek(lf,pos+1,SEEK_SET); else fseek(lf,0,SEEK_SET); long maxr=fsz-ftell(lf)+1; char *lb=malloc(maxr+2); if(!lb){fclose(lf);mg_http_reply(c,500,"","OOM");return;} size_t nr=fread(lb,1,maxr,lf); fclose(lf); lb[nr]=0; /* same pattern as main page (262KB) - proven to work for large content */ mg_http_reply(c,200,"Content-Type: text/plain; charset=utf-8\r\nCache-Control: no-cache\r\n","%s",lb); free(lb); } else if(mg_match(hm->uri,mg_str("/api/prefs/get"),NULL)){ SMPrefs p; read_prefs(&p); mg_http_reply(c,200,"Content-Type: application/json\r\n", "{\"auto_start\":%d,\"preferred_elf\":\"%s\"}",p.auto_start,p.preferred_elf); } else if(mg_match(hm->uri,mg_str("/api/prefs/save"),NULL)){ char v[8]={0},ef[512]={0},vif[8]={0},vars[8]={0}; mg_http_get_var(&hm->body,"auto_start",v,sizeof(v)); mg_http_get_var(&hm->body,"preferred_elf",ef,sizeof(ef)); mg_http_get_var(&hm->body,"icon_always_front",vif,sizeof(vif)); mg_http_get_var(&hm->body,"always_restart_sm",vars,sizeof(vars)); SMPrefs p; read_prefs(&p); if(v[0]) p.auto_start=atoi(v); if(vif[0]) p.icon_always_front=atoi(vif); if(vars[0]) p.always_restart_sm=atoi(vars); if(ef[0]||mg_http_get_var(&hm->body,"preferred_elf",ef,1)>=0) strncpy(p.preferred_elf,ef,511); /* update extra_scan if present */ char clr[4]={0}; mg_http_get_var(&hm->body,"extra_scan_clear",clr,sizeof(clr)); if(clr[0]){ p.extra_scan_count=0; } else if(sm_memmem(hm->body.buf,hm->body.len,"extra_scan",10)){ /* parse all extra_scan[]= entries from body */ p.extra_scan_count=0; const char *key="extra_scan[]="; const char *pos=hm->body.buf; const char *end=hm->body.buf+hm->body.len; while(pos0&&vlen<255){ /* url-decode */ char *dst=p.extra_scan[p.extra_scan_count]; size_t di=0; for(size_t ci=0;ci0?vlen:1); } } write_prefs(&p); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); } else if(mg_match(hm->uri,mg_str("/api/manual/add"),NULL)){ /* add image path to SM's manual.lst and touch it to trigger a scan */ #define SM_MANUAL "/data/shadowmount/manual.lst" char path[512]={0}; mg_http_get_var(&hm->query,"path",path,sizeof(path)); if(!path[0]) mg_http_get_var(&hm->body,"path",path,sizeof(path)); /* reject empty, relative, or traversal paths */ if(!path[0]||path[0]!='/'||strstr(path,"..")) {mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"error\":\"bad path\"}");return;} /* append path to manual.lst (create if absent) */ FILE *ml=fopen(SM_MANUAL,"a"); if(!ml) {mg_http_reply(c,500,"Content-Type: application/json\r\n","{\"error\":\"open manual.lst\"}");return;} fprintf(ml,"%s\n",path); fclose(ml); chmod(SM_MANUAL,0666); /* touch manual.lst so SM detects the change */ struct timeval tv[2]; gettimeofday(&tv[0],NULL); struct stat mst; if(stat(SM_MANUAL,&mst)==0&&tv[0].tv_sec<=mst.st_mtime) tv[0].tv_sec=mst.st_mtime+1; tv[1]=tv[0]; utimes(SM_MANUAL,tv); mg_http_reply(c,200,"Content-Type: application/json\r\n","{\"ok\":true}"); #undef SM_MANUAL } else if(mg_match(hm->uri,mg_str("/api/game/icon"),NULL)){ char tid[64]={0}; mg_http_get_var(&hm->query,"title_id",tid,sizeof(tid)); /* sanitise: only alphanumeric title IDs allowed */ for(int i=0;tid[i];i++) if(!isalnum((unsigned char)tid[i])){tid[0]=0;break;} if(!tid[0]){mg_http_reply(c,404,"","");return;} #define SMG_ICON_CACHE "/data/SMPlusGui/icon-cache" char cache[128]; snprintf(cache,sizeof(cache),SMG_ICON_CACHE "/%s.png",tid); /* serve from our own cache if available */ FILE *f=fopen(cache,"rb"); /* if not cached, try live mount paths and populate cache */ if(!f){ static const char *icon_tpl[]={ "/data/homebrew/%s/sce_sys/icon0.png", "/data/etaHEN/games/%s/sce_sys/icon0.png", "/mnt/ext0/homebrew/%s/sce_sys/icon0.png", "/mnt/usb0/%s/sce_sys/icon0.png", "/mnt/usb1/%s/sce_sys/icon0.png", "/system_data/priv/appmeta/%s/icon0.png", NULL }; char src[256]; FILE *sf=NULL; for(int i=0;icon_tpl[i]&&!sf;i++){ snprintf(src,sizeof(src),icon_tpl[i],tid); sf=fopen(src,"rb"); } /* scan /mnt/shadowmnt/ for {titleId}_* (SM's actual mount location) */ if(!sf){ DIR *sd=opendir("/mnt/shadowmnt"); if(sd){ struct dirent *de; size_t tlen=strlen(tid); while(!sf&&(de=readdir(sd))!=NULL){ if(strncmp(de->d_name,tid,tlen)==0&&de->d_name[tlen]=='_'){ snprintf(src,sizeof(src),"/mnt/shadowmnt/%s/sce_sys/icon0.png",de->d_name); sf=fopen(src,"rb"); } } closedir(sd); } } if(sf){ /* read source icon */ fseek(sf,0,SEEK_END); long isz=ftell(sf); fseek(sf,0,SEEK_SET); if(isz>0&&isz<=524288){ char *ibuf=malloc(isz); if(ibuf&&(fread(ibuf,1,isz,sf)==(size_t)isz)){ /* save to our cache */ mkdir("/data/SMPlusGui",0777); mkdir(SMG_ICON_CACHE,0777); FILE *cf=fopen(cache,"wb"); if(cf){fwrite(ibuf,1,isz,cf);fclose(cf);} /* serve directly from buffer */ fclose(sf); free(ibuf); /* re-open from cache so code path is unified */ f=fopen(cache,"rb"); } else { free(ibuf); fclose(sf); } } else { fclose(sf); } } } if(!f){mg_http_reply(c,404,"","");return;} fseek(f,0,SEEK_END); long sz=ftell(f); fseek(f,0,SEEK_SET); if(sz<=0||sz>524288){fclose(f);mg_http_reply(c,404,"","");return;} char *buf=malloc(sz); if(!buf){fclose(f);mg_http_reply(c,500,"","");return;} fread(buf,1,sz,f); fclose(f); mg_http_reply(c,200,"Content-Type: image/png\r\nCache-Control: max-age=86400\r\n","%.*s",(int)sz,buf); free(buf); #undef SMG_ICON_CACHE } else if(mg_match(hm->uri,mg_str("/api/sm/games"),NULL)|| mg_match(hm->uri,mg_str("/api/sm/images"),NULL)){ const char *smpath=mg_match(hm->uri,mg_str("/api/sm/games"),NULL)?"/api/v1/games":"/api/v1/images"; char *r=sm_api_req("POST",smpath,"{}"); if(!r) mg_http_reply(c,503,"Content-Type: application/json\r\n","{\"status\":503,\"error\":\"SM API not available\"}"); else { mg_http_reply(c,200,"Content-Type: application/json\r\n","%s",r); free(r); } } else if(mg_match(hm->uri,mg_str("/api/sm/games/mount"),NULL)|| mg_match(hm->uri,mg_str("/api/sm/games/unmount"),NULL)){ char tid[32]={0}; mg_http_get_var(&hm->query,"title_id",tid,sizeof(tid)); if(!tid[0]) { mg_http_reply(c,400,"Content-Type: application/json\r\n","{\"error\":\"missing title_id\"}"); } else { char body[64]; snprintf(body,sizeof(body),"{\"title_id\":\"%s\"}",tid); const char *smpath=mg_match(hm->uri,mg_str("/api/sm/games/mount"),NULL)?"/api/v1/games/mount":"/api/v1/games/unmount"; char *r=sm_api_req("POST",smpath,body); if(!r) mg_http_reply(c,503,"Content-Type: application/json\r\n","{\"status\":503,\"error\":\"SM API not available\"}"); else { mg_http_reply(c,200,"Content-Type: application/json\r\n","%s",r); free(r); } } } else mg_http_reply(c,404,"","404"); } /* SIGCONT signals standby resume on PS5 */ static volatile sig_atomic_t smg_resume = 0; static void on_resume(int s){(void)s;smg_resume=1;} int payload_main(void) { signal(SIGCONT, on_resume); (void)syscall(SYS_thr_set_name,-1,PAYLOAD_NAME); terminate_existing_instances(PAYLOAD_NAME); char _ip[48] = {0}; get_local_ip(_ip, sizeof(_ip)); char _url[64]; if (_ip[0]) snprintf(_url, sizeof(_url), "http://%s:" HTTP_PORT, _ip); else snprintf(_url, sizeof(_url), "Port " HTTP_PORT); _notify_send("SMPlusGui v" SMPLUS_VERSION, _url); /* simple debug notification like other payloads */ { char dmsg[128]; snprintf(dmsg,sizeof(dmsg),"SMPlusGui v" SMPLUS_VERSION "\n%s",_url); notify_simple(dmsg); } struct mg_mgr mgr; smg_mgr = &mgr; mg_mgr_init(&mgr); mg_http_listen(&mgr,"http://0.0.0.0:" HTTP_PORT,fn,NULL); int polls=0, as_done=0, net_timer=0; char cur_ip[48]; strncpy(cur_ip,_ip,47); { SMPrefs _p; read_prefs(&_p); /* read icon pref once at start */ while(1){ mg_mgr_poll(&mgr,1000); usleep(100000); if(++polls==3) smplus_install_if_needed(_p.icon_always_front); /* send pending ELF after browser got its {ok:true} response */ if(smg_pending_elf[0]){ send_elf_to_elfldr(smg_pending_elf); smg_pending_elf[0]=0; } if(polls==6 && !as_done){ /* ~6s after start — give elfldr time to initialize */ as_done=1; SMPrefs prefs; read_prefs(&prefs); if(prefs.auto_start){ /* check if SM already running unless always_restart_sm is set */ int sm_up=0; if(!prefs.always_restart_sm){ int ts=socket(AF_INET,SOCK_STREAM,0); if(ts>=0){ struct timeval tv={1,0}; setsockopt(ts,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)); setsockopt(ts,SOL_SOCKET,SO_SNDTIMEO,&tv,sizeof(tv)); struct sockaddr_in ta; memset(&ta,0,sizeof(ta)); ta.sin_family=AF_INET; ta.sin_port=htons(10101); ta.sin_addr.s_addr=inet_addr("127.0.0.1"); if(connect(ts,(struct sockaddr*)&ta,sizeof(ta))==0) sm_up=1; close(ts); } } if(!sm_up){ char elf[SM_EPATH]={0}; if(prefs.preferred_elf[0]) strncpy(elf,prefs.preferred_elf,SM_EPATH-1); else{ char elfs[SM_MAX_ELFS][SM_EPATH]; if(sm_find_elfs(elfs)>0) strncpy(elf,elfs[0],SM_EPATH-1); } if(elf[0]) send_elf_to_elfldr(elf); } } } /* Network watchdog — restart Mongoose after standby/resume */ if(++net_timer>=10){ net_timer=0; char new_ip[48]={0}; get_local_ip(new_ip,sizeof(new_ip)); int ip_changed = (new_ip[0] && strcmp(new_ip,cur_ip)!=0); int ip_restored = (!cur_ip[0] && new_ip[0]); if(ip_changed||ip_restored||smg_resume){ smg_resume=0; if(new_ip[0]) strncpy(cur_ip,new_ip,47); mg_mgr_free(&mgr); usleep(500000); mg_mgr_init(&mgr); mg_http_listen(&mgr,"http://0.0.0.0:" HTTP_PORT,fn,NULL); smg_mgr=&mgr; /* simple debug notification on restore — no icon */ char rmsg[128]; snprintf(rmsg,sizeof(rmsg),"Restored Service: SMPlusGui\nhttp://%s:" HTTP_PORT,cur_ip); notify_simple(rmsg); } else if(!new_ip[0]&&cur_ip[0]) { cur_ip[0]=0; /* IP lost — next cycle with IP will trigger restore */ } } } } mg_mgr_free(&mgr); return 0; } int main(void){ return payload_main(); }