add single instance mode
[uwplayer.git] / application / settings.c
1 /*
2  * Copyright 2022 Olaf Wintermann
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a 
5  * copy of this software and associated documentation files (the "Software"), 
6  * to deal in the Software without restriction, including without limitation 
7  * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
8  * and/or sell copies of the Software, and to permit persons to whom the 
9  * Software is furnished to do so, subject to the following conditions:
10  * 
11  * The above copyright notice and this permission notice shall be included in 
12  * all copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
17  * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
19  * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
20  * DEALINGS IN THE SOFTWARE.
21  */
22
23 #include "settings.h"
24
25 #include <stdio.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <errno.h>
29 #include <unistd.h>
30 #include <fcntl.h>
31 #include <sys/stat.h>
32 #include <pthread.h>
33
34 #include <sys/socket.h>
35 #include <sys/un.h>
36
37
38 #include "main.h"
39 #include "utils.h"
40 #include "json.h"
41 #include "window.h"
42 #include "playlist.h"
43
44 #include <cx/map.h>
45 #include <cx/hash_map.h>
46 //include <ucx/properties.h>
47 #include <cx/buffer.h>
48 #include <cx/utils.h>
49 #include <cx/printf.h>
50
51 #define CONFIG_BASE_DIR ".config"
52 #define UWP_CONFIG_DIR  "uwplayer"
53 #define UWP_CONFIG_FILE "uwplayer.conf"
54
55 #define JS_READ_BUFSIZE 4096
56
57 static void* player_bin_search_thread(void *data);
58 static void conf_load_global_settings(void);
59
60 static char *uwp_config_dir;
61
62 static int instance_socket = -1;
63
64 /*
65  * root json config object
66  */
67 static JSONObject *uwp_config;
68
69 /*
70  * global settings from json config converted to key/value pairs
71  */
72 static CxMap *uwp_settings; 
73
74 /*
75  * default settings
76  */
77 static CxMap *uwp_default;
78
79 static int check_config_dir(void) {
80     char *home = getenv("HOME");
81     if(!home) {
82         return 1;
83     }
84     
85     char *cfg_dir = util_concat_path(home, CONFIG_BASE_DIR);
86     int ret = 0;
87     if(mkdir(cfg_dir, S_IRWXU)) {
88         if(errno != EEXIST) {
89             fprintf(stderr, "Error: Cannot access %s: %s\n", cfg_dir, strerror(errno));
90             ret = 1;
91         }
92     }
93     
94     if(!ret) {
95         uwp_config_dir = util_concat_path(cfg_dir, UWP_CONFIG_DIR);
96         if(mkdir(uwp_config_dir, S_IRWXU)) {
97             if(errno != EEXIST) {
98                 fprintf(stderr, "Error: Cannot access %s: %s\n", uwp_config_dir, strerror(errno));
99                 ret = 1;
100             }
101         }
102     }
103     
104     free(cfg_dir);
105     return ret;
106 }
107
108 int load_config(void) {
109     if(check_config_dir()) {
110         return 1;
111     }
112     
113     uwp_settings = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 16);
114     uwp_default = cxHashMapCreate(cxDefaultAllocator, CX_STORE_POINTERS, 32);
115     
116     char *cfgfile_path = util_concat_path(uwp_config_dir, UWP_CONFIG_FILE);
117     FILE *cfgfile = fopen(cfgfile_path, "r");
118     free(cfgfile_path);
119     
120     int ret = 0;
121     if(cfgfile) {
122         JSONParser *parser = json_parser_new();
123         
124         JSONValue *value = NULL;
125         char buf[JS_READ_BUFSIZE];
126         size_t r;
127         
128         while((ret = json_read_value(parser, &value)) >= 0) {
129             if(ret == 0) {
130                 r = fread(buf, 1, JS_READ_BUFSIZE, cfgfile);
131                 if(r == 0) {
132                     break;
133                 }
134                 json_parser_fill(parser, buf, r);
135             } else {
136                 break;
137             }
138         }
139         
140         json_parser_free(parser);
141         
142         if(value) {
143             if(value->type == JSON_OBJECT) {
144                 ret = 0;
145                 uwp_config = &value->value.object;
146                 conf_load_global_settings();
147             } else {
148                 ret = 1;
149             }
150         } else {
151             ret = 1;
152         }
153         
154         
155         fclose(cfgfile);
156
157         if(ret) {
158             return ret;
159         }
160     }
161  
162     
163     // check if mpv or mplayer binaries are configured
164     char *player_bin = cxMapGet(uwp_settings, cx_hash_key_str(UWP_PLAYER_BIN));
165     char *player_type = cxMapGet(uwp_settings, cx_hash_key_str(UWP_PLAYER_TYPE));
166     
167     if(!player_bin) {
168         // try to find the mpv or mplayer binary path
169         pthread_t st;
170         pthread_create(&st, NULL, player_bin_search_thread, NULL);
171     } else if(!player_type) {
172         fprintf(stderr, "Warning: unknown player type (mplayer, mpv)\n");
173     }
174     
175     return 0;
176 }
177
178 static void conf_load_global_settings(void) {
179     JSONValue *settings = json_obj_get(uwp_config, "settings");
180     if(!settings) {
181         return;
182     }
183     
184     if(settings->type != JSON_OBJECT) {
185         fprintf(stderr, "Warning: 'settings' not an object\n");
186         return;
187     }
188     
189     JSONObject *s = &settings->value.object;
190     
191     for(size_t i=0;i<s->size;i++) {
192         JSONObjValue *gs = &s->values[i];
193         if(gs->value->type == JSON_STRING) {
194             cxMapPut(uwp_settings, cx_hash_key_str(gs->name), strdup(gs->value->value.string.string));
195         }
196     }
197 }
198
199 static char* get_which_output(FILE *f, CxBuffer *buf) {
200     buf->pos = 0;
201     buf->size = 0;
202     cx_stream_copy(f, buf, (cx_read_func)fread, (cx_write_func)cxBufferWrite);
203     if(!pclose(f)) {
204         cxBufferPut(buf, 0);
205         size_t i;
206         for(i=0;i<buf->pos;i++) {
207             if(buf->space[i] == '\n') {
208                 buf->space[i] = 0;
209                 break;
210             }
211         }
212         return buf->space;
213     }
214     return NULL;
215 }
216
217 static Boolean finish_bin_search(XtPointer data) {
218     PlayerInfo *playerInfo = data;
219     cxMapPut(uwp_settings, cx_hash_key_str(UWP_PLAYER_BIN), playerInfo->bin);
220     cxMapPut(uwp_settings, cx_hash_key_str(UWP_PLAYER_TYPE), playerInfo->type);
221     free(playerInfo);
222     return 0;
223 }
224
225 static void* player_bin_search_thread(void *data) {
226     CxBuffer buf;
227     cxBufferInit(&buf, NULL, 256, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS);
228     
229     FILE *f = popen("which mpv", "r");
230     if(f) {
231         char *bin = get_which_output(f, &buf);
232         if(bin) {
233             PlayerInfo *playerInfo = malloc(sizeof(PlayerInfo));
234             playerInfo->bin = strdup(bin);
235             playerInfo->type = strdup("mpv");
236             AppExecProc(finish_bin_search, playerInfo);
237             
238             cxBufferDestroy(&buf);
239             return NULL;
240         }
241     }
242     
243     f = popen("which mplayer", "r");
244     if(f) {
245         char *bin = get_which_output(f, &buf);
246         if(bin) {
247             PlayerInfo *playerInfo = malloc(sizeof(PlayerInfo));
248             playerInfo->bin = strdup(bin);
249             playerInfo->type = strdup("mplayer");
250             AppExecProc(finish_bin_search, playerInfo);
251         }
252     }
253     
254     cxBufferDestroy(&buf);
255     return NULL;
256 }
257
258 char* SettingsGetPlayerBin(void) {
259     return cxMapGet(uwp_settings, cx_hash_key_str(UWP_PLAYER_BIN));
260 }
261
262
263 char *InstanceFilePath(Display *dp) {
264     cxmutstr instance_file = cx_asprintf("instance%s", DisplayString(dp));
265     char *path = util_concat_path(uwp_config_dir, instance_file.ptr);
266     free(instance_file.ptr);
267     return path;
268 }
269
270 int ConnectToInstance(const char *path) {
271     int fd = socket(AF_UNIX, SOCK_STREAM, 0);
272     if(!fd) {
273         return -1;
274     }
275     
276     size_t path_len = strlen(path);
277     
278     struct sockaddr_un addr;
279     if(path_len > sizeof(addr.sun_path)-1) {
280         return -1;
281     }
282     
283     memset(&addr, 0, sizeof(addr));
284     addr.sun_family = AF_UNIX;
285     memcpy(addr.sun_path, path, strlen(path) + 1);
286     
287     if(connect(fd, (struct sockaddr*)&addr, sizeof(addr))) {
288         close(fd);
289         return -1;
290     }
291     
292     return fd;
293 }
294
295 int CreateSingleInstanceSocket(Display *dp, Bool *already_running) {
296     char *instance_path = InstanceFilePath(dp);
297     size_t instance_path_len = strlen(instance_path);
298     *already_running = 0;
299     
300     // check path
301     struct sockaddr_un addr;
302     if(instance_path_len > sizeof(addr.sun_path)-1) {
303         fprintf(stderr, "instance path '%s' too long for unix domain socket", instance_path);
304         free(instance_path);
305         return 1;
306     }
307     
308     // check if the socket already exists and is open
309     struct stat s;
310     if(!stat(instance_path, &s)) {
311         int fd = ConnectToInstance(instance_path);
312         close(fd);
313         if(fd >= 0) {
314             *already_running = 1;
315             return 0; // instance already running
316         }
317         
318         // instance not running but socket file exists
319         // remove socket before creating a new socket
320         unlink(instance_path);
321     }
322     
323     memset(&addr, 0, sizeof(addr));
324     addr.sun_family = AF_UNIX;
325     memcpy(addr.sun_path, instance_path, instance_path_len+1);
326     
327     free(instance_path);
328     instance_socket = socket(AF_UNIX, SOCK_STREAM, 0);
329     if(instance_socket < 0) {
330         fprintf(stderr, "cannot create instance socket: %s", strerror(errno));
331         return 1;
332     }
333     
334     if(bind(instance_socket, (struct sockaddr*)&addr, sizeof(addr))) {
335         fprintf(stderr, "cannot bind instance socket: %s", strerror(errno));
336         return 1;
337     }
338     
339     pthread_t tid;
340     if(pthread_create(&tid, NULL, instance_socket_thread, NULL)) {
341         close(instance_socket);
342         instance_socket = -1;
343         return -1;
344     }
345     pthread_detach(tid);
346     
347     return 0;
348 }
349
350 static Boolean cmd_open(XtPointer data) {
351     MainWindow *win = GetMainWindow();
352     char *file = data;
353     printf("open %s\n", file);
354     
355     PlayListClear(win);
356     PlayListAddFile(win, file);
357     PlayListPlayTrack(win, win->playlist.tracks->size-1);
358     
359     free(data);
360     return 0;
361 }
362
363 static void process_msg(CxBuffer *msgbuf, size_t *rpos) {
364     cxstring msg = cx_strn(NULL, 0);
365     for(size_t i=*rpos;i<msgbuf->size;i++) {
366         if(msgbuf->space[i] == '\n') {
367             msg.ptr = msgbuf->space + *rpos;
368             msg.length = i - *rpos;
369             *rpos = i+1;
370             break;
371         }
372     }
373     
374     if(msg.length > 0) {
375         if(cx_strprefix(msg, CX_STR("open "))) {
376             cxstring file = cx_strsubs(msg, 5);
377             cxmutstr mfile = cx_strdup(file);
378             AppExecProc(cmd_open, mfile.ptr);
379         } else {
380             fprintf(stderr, "unknown instance command: {%.*s}\n", (int)msg.length, msg.ptr);
381         }
382         
383         if(*rpos < msgbuf->size) {
384             process_msg(msgbuf, rpos);
385         }
386     }
387 }
388
389 void* instance_socket_thread(void *data) {
390     listen(instance_socket, 8);
391     
392     CxBuffer msgbuf;
393     cxBufferInit(&msgbuf, NULL, 1024, cxDefaultAllocator, CX_BUFFER_AUTO_EXTEND|CX_BUFFER_FREE_CONTENTS);
394     
395     char buf[1024];
396     
397     while(TRUE) {
398         printf("accept instance socket\n");
399         int fd = accept(instance_socket, NULL, 0);
400         if(fd < 0) {
401             break;
402         }
403         printf("accept instance connection\n");
404         
405         msgbuf.pos = 0;
406         msgbuf.size = 0;
407         size_t rpos = 0;
408         
409         ssize_t r;
410         while((r = read(fd, buf, 1024)) > 0) {
411             cxBufferWrite(buf, 1, r, &msgbuf);
412             process_msg(&msgbuf, &rpos);
413         }
414         
415         printf("close instance connection\n");
416         close(fd);
417     }
418     
419     cxBufferDestroy(&msgbuf);
420     
421     printf("instance socket shutdown\n");
422     
423     return NULL;
424 }
425
426 void ShutdownInstanceSocket(Display *dp) {
427     if(instance_socket < 0) {
428         return;
429     }
430     
431     shutdown(instance_socket, SHUT_RDWR);
432     close(instance_socket);
433     instance_socket = -1;
434     
435     char *instance_path = InstanceFilePath(dp);
436     unlink(instance_path);
437     free(instance_path);
438 }