GROS CHANTIER
parent
ea882d37dc
commit
6df133c2c1
|
@ -0,0 +1,3 @@
|
|||
*.a
|
||||
*.so
|
||||
*.swp
|
|
@ -72,6 +72,23 @@ int service_close (const char *fifopath)
|
|||
return 0;
|
||||
}
|
||||
|
||||
struct process * srv_process_copy (const struct process *p)
|
||||
{
|
||||
if (p == NULL)
|
||||
return NULL;
|
||||
|
||||
struct process * copy = malloc (sizeof(struct process));
|
||||
memcpy (copy, p, sizeof (struct process));
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
int srv_process_eq (const struct process *p1, const struct process *p2)
|
||||
{
|
||||
return (p1->pid == p2->pid && p1>version == p2->version
|
||||
&& p1->index == p2->index);
|
||||
}
|
||||
|
||||
int service_get_new_process (struct process *proc, const char * spath)
|
||||
{
|
||||
if (spath == NULL) {
|
||||
|
|
|
@ -36,6 +36,10 @@ struct service {
|
|||
|
||||
int service_path (char *buf, const char *sname);
|
||||
|
||||
struct process * srv_process_copy (struct process *p);
|
||||
|
||||
int srv_process_eq (const struct process *p1, const struct process *p2);
|
||||
|
||||
void gen_process_structure (struct process *p
|
||||
, pid_t pid, unsigned int index, unsigned int version);
|
||||
|
||||
|
@ -53,6 +57,7 @@ int service_get_new_process (struct process *proc, const char * spath);
|
|||
void service_get_new_processes (struct process ***, int *nproc, char *spath);
|
||||
void service_free_processes (struct process **, int nproc);
|
||||
|
||||
|
||||
void process_print (struct process *);
|
||||
int process_create (struct process *, int index); // called by the application
|
||||
int process_destroy (struct process *); // called by the application
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,171 @@
|
|||
/* $OpenBSD: queue.h,v 1.43 2015/12/28 19:38:40 millert Exp $ */
|
||||
/* $NetBSD: queue.h,v 1.11 1996/05/16 05:17:14 mycroft Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 1991, 1993
|
||||
* The Regents of the University of California. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. Neither the name of the University nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* @(#)queue.h 8.5 (Berkeley) 8/20/94
|
||||
*/
|
||||
|
||||
#ifndef _SYS_QUEUE_H_
|
||||
#define _SYS_QUEUE_H_
|
||||
|
||||
/*
|
||||
* This file defines five types of data structures: singly-linked lists,
|
||||
* lists, simple queues, tail queues and XOR simple queues.
|
||||
*
|
||||
*
|
||||
* A singly-linked list is headed by a single forward pointer. The elements
|
||||
* are singly linked for minimum space and pointer manipulation overhead at
|
||||
* the expense of O(n) removal for arbitrary elements. New elements can be
|
||||
* added to the list after an existing element or at the head of the list.
|
||||
* Elements being removed from the head of the list should use the explicit
|
||||
* macro for this purpose for optimum efficiency. A singly-linked list may
|
||||
* only be traversed in the forward direction. Singly-linked lists are ideal
|
||||
* for applications with large datasets and few or no removals or for
|
||||
* implementing a LIFO queue.
|
||||
*
|
||||
* A list is headed by a single forward pointer (or an array of forward
|
||||
* pointers for a hash table header). The elements are doubly linked
|
||||
* so that an arbitrary element can be removed without a need to
|
||||
* traverse the list. New elements can be added to the list before
|
||||
* or after an existing element or at the head of the list. A list
|
||||
* may only be traversed in the forward direction.
|
||||
*
|
||||
* A simple queue is headed by a pair of pointers, one to the head of the
|
||||
* list and the other to the tail of the list. The elements are singly
|
||||
* linked to save space, so elements can only be removed from the
|
||||
* head of the list. New elements can be added to the list before or after
|
||||
* an existing element, at the head of the list, or at the end of the
|
||||
* list. A simple queue may only be traversed in the forward direction.
|
||||
*
|
||||
* A tail queue is headed by a pair of pointers, one to the head of the
|
||||
* list and the other to the tail of the list. The elements are doubly
|
||||
* linked so that an arbitrary element can be removed without a need to
|
||||
* traverse the list. New elements can be added to the list before or
|
||||
* after an existing element, at the head of the list, or at the end of
|
||||
* the list. A tail queue may be traversed in either direction.
|
||||
*
|
||||
* An XOR simple queue is used in the same way as a regular simple queue.
|
||||
* The difference is that the head structure also includes a "cookie" that
|
||||
* is XOR'd with the queue pointer (first, last or next) to generate the
|
||||
* real pointer value.
|
||||
*
|
||||
* For details on the use of these macros, see the queue(3) manual page.
|
||||
*/
|
||||
|
||||
#if defined(QUEUE_MACRO_DEBUG) || (defined(_KERNEL) && defined(DIAGNOSTIC))
|
||||
#define _Q_INVALIDATE(a) (a) = ((void *)-1)
|
||||
#else
|
||||
#define _Q_INVALIDATE(a)
|
||||
#endif
|
||||
|
||||
/*
|
||||
* List definitions.
|
||||
*/
|
||||
#define LIST_HEAD(name, type) \
|
||||
struct name { \
|
||||
struct type *lh_first; /* first element */ \
|
||||
}
|
||||
|
||||
#define LIST_HEAD_INITIALIZER(head) \
|
||||
{ NULL }
|
||||
|
||||
#define LIST_ENTRY(type) \
|
||||
struct { \
|
||||
struct type *le_next; /* next element */ \
|
||||
struct type **le_prev; /* address of previous next element */ \
|
||||
}
|
||||
|
||||
/*
|
||||
* List access methods.
|
||||
*/
|
||||
#define LIST_FIRST(head) ((head)->lh_first)
|
||||
#define LIST_END(head) NULL
|
||||
#define LIST_EMPTY(head) (LIST_FIRST(head) == LIST_END(head))
|
||||
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
|
||||
|
||||
#define LIST_FOREACH(var, head, field) \
|
||||
for((var) = LIST_FIRST(head); \
|
||||
(var)!= LIST_END(head); \
|
||||
(var) = LIST_NEXT(var, field))
|
||||
|
||||
#define LIST_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = LIST_FIRST(head); \
|
||||
(var) && ((tvar) = LIST_NEXT(var, field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
/*
|
||||
* List functions.
|
||||
*/
|
||||
#define LIST_INIT(head) do { \
|
||||
LIST_FIRST(head) = LIST_END(head); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_AFTER(listelm, elm, field) do { \
|
||||
if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \
|
||||
(listelm)->field.le_next->field.le_prev = \
|
||||
&(elm)->field.le_next; \
|
||||
(listelm)->field.le_next = (elm); \
|
||||
(elm)->field.le_prev = &(listelm)->field.le_next; \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
|
||||
(elm)->field.le_prev = (listelm)->field.le_prev; \
|
||||
(elm)->field.le_next = (listelm); \
|
||||
*(listelm)->field.le_prev = (elm); \
|
||||
(listelm)->field.le_prev = &(elm)->field.le_next; \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_HEAD(head, elm, field) do { \
|
||||
if (((elm)->field.le_next = (head)->lh_first) != NULL) \
|
||||
(head)->lh_first->field.le_prev = &(elm)->field.le_next;\
|
||||
(head)->lh_first = (elm); \
|
||||
(elm)->field.le_prev = &(head)->lh_first; \
|
||||
} while (0)
|
||||
|
||||
#define LIST_REMOVE(elm, field) do { \
|
||||
if ((elm)->field.le_next != NULL) \
|
||||
(elm)->field.le_next->field.le_prev = \
|
||||
(elm)->field.le_prev; \
|
||||
*(elm)->field.le_prev = (elm)->field.le_next; \
|
||||
_Q_INVALIDATE((elm)->field.le_prev); \
|
||||
_Q_INVALIDATE((elm)->field.le_next); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_REPLACE(elm, elm2, field) do { \
|
||||
if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \
|
||||
(elm2)->field.le_next->field.le_prev = \
|
||||
&(elm2)->field.le_next; \
|
||||
(elm2)->field.le_prev = (elm)->field.le_prev; \
|
||||
*(elm2)->field.le_prev = (elm2); \
|
||||
_Q_INVALIDATE((elm)->field.le_prev); \
|
||||
_Q_INVALIDATE((elm)->field.le_next); \
|
||||
} while (0)
|
||||
|
||||
#endif /* _SYS_QUEUE_H_ */
|
|
@ -0,0 +1,22 @@
|
|||
CC=gcc
|
||||
CFLAGS=-Wall -g
|
||||
LDFLAGS=
|
||||
CFILES=$(wildcard *.c) # CFILES => recompiles everything on a C file change
|
||||
EXEC=$(basename $(wildcard *.c))
|
||||
SOURCES=$(wildcard ../lib/*.c)
|
||||
OBJECTS=$(SOURCES:.c=.o)
|
||||
TESTS=$(addsuffix .test, $(EXEC))
|
||||
|
||||
all: $(SOURCES) $(EXEC)
|
||||
|
||||
$(EXEC): $(OBJECTS) $(CFILES)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) $(OBJECTS) $@.c -o $@
|
||||
|
||||
.c.o:
|
||||
$(CC) -c $(CFLAGS) $< -o $@
|
||||
|
||||
clean:
|
||||
-rm $(OBJECTS)
|
||||
|
||||
mrproper: clean
|
||||
rm $(EXEC)
|
|
@ -1,48 +0,0 @@
|
|||
#include <stdlib.h>
|
||||
|
||||
#include "list.h"
|
||||
|
||||
List*
|
||||
list_new(size_t element_size)
|
||||
{
|
||||
List* l = malloc(sizeof(*l));
|
||||
|
||||
l->element_size = element_size;
|
||||
l->head = NULL;
|
||||
l->tail = NULL;
|
||||
l->length = 0;
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
void*
|
||||
list_append(List* l)
|
||||
{
|
||||
struct link* link = malloc(sizeof(*link) + l->element_size);
|
||||
|
||||
link->next = l->tail;
|
||||
l->tail = link;
|
||||
|
||||
if (!l->head)
|
||||
l->tail = link;
|
||||
|
||||
l->length++;
|
||||
|
||||
return (void*) link->value;
|
||||
}
|
||||
|
||||
void
|
||||
list_free(List* l)
|
||||
{
|
||||
struct link* next;
|
||||
struct link* link;
|
||||
|
||||
for (link = l->head; link; link = next) {
|
||||
next = link->next;
|
||||
|
||||
free(link);
|
||||
}
|
||||
|
||||
free(l);
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
#ifndef LIST_H
|
||||
#define LIST_H
|
||||
|
||||
struct link {
|
||||
struct link* next;
|
||||
char value[];
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
struct link* head;
|
||||
struct link* tail;
|
||||
size_t element_size;
|
||||
size_t length;
|
||||
} List;
|
||||
|
||||
List* list_new(size_t);
|
||||
void* list_append(List*);
|
||||
void list_remove(List*, size_t);
|
||||
void list_free(List*);
|
||||
|
||||
#endif
|
||||
|
254
pubsub/pubsubd.c
254
pubsub/pubsubd.c
|
@ -1,91 +1,191 @@
|
|||
#include "../lib/communication.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <communication.h>
|
||||
|
||||
#include "list.h"
|
||||
|
||||
typedef struct {
|
||||
int test;
|
||||
} Publisher;
|
||||
|
||||
typedef struct {
|
||||
int test;
|
||||
} Subscriber;
|
||||
|
||||
const char* service_name = "pubsub";
|
||||
|
||||
void
|
||||
ohshit(int rvalue, const char* str) {
|
||||
fprintf(stderr, "%s\n", str);
|
||||
|
||||
exit(rvalue);
|
||||
fprintf(stderr, "%s\n", str);
|
||||
exit(rvalue);
|
||||
}
|
||||
|
||||
// init lists
|
||||
void pubsubd_channels_init (struct channels *chans) { LIST_INIT(chans); }
|
||||
void pubsubd_subscriber_init (struct app_list *al) { LIST_INIT(al); }
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
pubsubd_channels_eq (const struct channel *c1, const struct channel *c2)
|
||||
{
|
||||
List* subscribers;
|
||||
List* publishers;
|
||||
int r;
|
||||
char s_path[PATH_MAX];
|
||||
int s_pipe;
|
||||
return (strncmp (c1->chan, c2->chan, c1->chanlen) == 0);
|
||||
}
|
||||
struct channels * pubsubd_channels_copy (struct channels *c);
|
||||
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
void
|
||||
pubsubd_channels_add (struct channels *chans, struct channels *c)
|
||||
{
|
||||
if(!chans || !c)
|
||||
return;
|
||||
|
||||
service_path(s_path, service_name);
|
||||
|
||||
printf("Listening on %s.\n", s_path);
|
||||
|
||||
if ((r = service_create(s_path)))
|
||||
ohshit(1, "service_create error");
|
||||
|
||||
publishers = list_new(sizeof(Publisher));
|
||||
subscribers = list_new(sizeof(Subscriber));
|
||||
|
||||
if (!publishers && !subscribers)
|
||||
ohshit(1, "out of memory, already...");
|
||||
|
||||
/* ?!?!?!?!? */
|
||||
mkfifo(s_path, S_IRUSR);
|
||||
|
||||
s_pipe = open(s_path, S_IRUSR);
|
||||
|
||||
for (;;) {
|
||||
struct process* proc;
|
||||
int proc_count, i;
|
||||
|
||||
service_get_new_processes(&proc, &proc_count, s_pipe);
|
||||
|
||||
printf("> %i proc\n", proc_count);
|
||||
|
||||
for (i = 0; i < proc_count; i++) {
|
||||
size_t message_size = BUFSIZ;
|
||||
char buffer[BUFSIZ];
|
||||
|
||||
process_print(proc + i);
|
||||
|
||||
if ((r = process_read(&proc[i], &buffer, &message_size))) {
|
||||
ohshit(1, "process_read error");
|
||||
}
|
||||
|
||||
printf(": %s\n", buffer);
|
||||
|
||||
|
||||
}
|
||||
|
||||
service_free_processes(&proc, proc_count);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
close(s_pipe);
|
||||
|
||||
list_free(publishers);
|
||||
list_free(subscribers);
|
||||
|
||||
service_close(s_path);
|
||||
|
||||
return 0;
|
||||
struct process *n = pubsubd_channels_copy (c);
|
||||
LIST_INSERT_HEAD(al, n, entries);
|
||||
}
|
||||
|
||||
void
|
||||
pubsubd_subscriber_del (struct app_list *al, struct process *p)
|
||||
{
|
||||
struct process *todel = srv_subscriber_get (al, p);
|
||||
if(todel != NULL) {
|
||||
LIST_REMOVE(todel, entries);
|
||||
srv_process_free (mfree, todel);
|
||||
mfree (todel);
|
||||
todel = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
pubsubd_subscriber_add (struct app_list *al, struct process *p)
|
||||
{
|
||||
if(!al || !p)
|
||||
return;
|
||||
|
||||
struct process *n = srv_process_copy (p);
|
||||
LIST_INSERT_HEAD(al, n, entries);
|
||||
}
|
||||
|
||||
struct process *
|
||||
pubsubd_subscriber_get (const struct app_list *al
|
||||
, const struct process *p)
|
||||
{
|
||||
struct process *np, *res = NULL;
|
||||
LIST_FOREACH(np, al, entries) {
|
||||
if(srv_process_eq (np, p)) {
|
||||
res = np;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
pubsubd_subscriber_del (struct app_list *al, struct process *p)
|
||||
{
|
||||
struct process *todel = srv_subscriber_get (al, p);
|
||||
if(todel != NULL) {
|
||||
LIST_REMOVE(todel, entries);
|
||||
srv_process_free (mfree, todel);
|
||||
mfree (todel);
|
||||
todel = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void pubsubd_msg_send (struct service *s, struct message * m, struct process *p)
|
||||
{
|
||||
}
|
||||
void pubsubd_msg_recv (struct service *s, struct message * m, struct process *p)
|
||||
{
|
||||
}
|
||||
void pubsub_msg_send (struct service *s, struct message * m)
|
||||
{
|
||||
}
|
||||
void pubsub_msg_recv (struct service *s, struct message * m)
|
||||
{
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char* argv[])
|
||||
{
|
||||
// gets the service path, such as /tmp/<service>
|
||||
char s_path[PATH_MAX];
|
||||
service_path (s_path, service_name);
|
||||
printf ("Listening on %s.\n", s_path);
|
||||
|
||||
// creates the service named pipe, that listens to client applications
|
||||
if (service_create (s_path))
|
||||
ohshit(1, "service_create error");
|
||||
|
||||
struct channels chans;
|
||||
pubsubd_channels_init (&chans);
|
||||
|
||||
for (;;) {
|
||||
struct process proc;
|
||||
int proc_count, i;
|
||||
|
||||
service_get_new_process (&proc, s_path);
|
||||
|
||||
printf("> %i proc\n", proc_count);
|
||||
|
||||
for (i = 0; i < proc_count; i++) {
|
||||
size_t message_size = BUFSIZ;
|
||||
char buffer[BUFSIZ];
|
||||
|
||||
process_print(proc + i);
|
||||
|
||||
if (process_read (&proc[i], &buffer, &message_size))
|
||||
ohshit(1, "process_read error");
|
||||
|
||||
printf(": %s\n", buffer);
|
||||
|
||||
|
||||
}
|
||||
|
||||
service_free_processes(&proc, proc_count);
|
||||
}
|
||||
|
||||
// the application will shut down, and remove the service named pipe
|
||||
if (service_close (s_path))
|
||||
ohshit(1, "service_close error");
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* main loop
|
||||
*
|
||||
* opens the application pipes,
|
||||
* reads then writes the same message,
|
||||
* then closes the pipes
|
||||
*/
|
||||
|
||||
void main_loop (const char *spath)
|
||||
{
|
||||
int ret;
|
||||
struct process proc;
|
||||
|
||||
int cnt = 10;
|
||||
|
||||
while (cnt--) {
|
||||
// -1 : error, 0 = no new process, 1 = new process
|
||||
ret = service_get_new_process (&proc, spath);
|
||||
if (ret == -1) {
|
||||
fprintf (stderr, "error service_get_new_process\n");
|
||||
continue;
|
||||
} else if (ret == 0) { // that should not happen
|
||||
continue;
|
||||
}
|
||||
|
||||
// printf ("before print\n");
|
||||
process_print (&proc);
|
||||
// printf ("after print\n");
|
||||
|
||||
// about the message
|
||||
size_t msize = BUFSIZ;
|
||||
char buf[BUFSIZ];
|
||||
bzero(buf, BUFSIZ);
|
||||
|
||||
// printf ("before read\n");
|
||||
if ((ret = service_read (&proc, &buf, &msize))) {
|
||||
fprintf(stdout, "error service_read %d\n", ret);
|
||||
continue;
|
||||
}
|
||||
// printf ("after read\n");
|
||||
printf ("read, size %ld : %s\n", msize, buf);
|
||||
|
||||
// printf ("before proc write\n");
|
||||
if ((ret = service_write (&proc, &buf, msize))) {
|
||||
fprintf(stdout, "error service_write %d\n", ret);
|
||||
continue;
|
||||
}
|
||||
// printf ("after proc write\n");
|
||||
printf ("\033[32mStill \033[31m%d\033[32m applications to serve\n",cnt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
#ifndef __PUBSUBD_H__
|
||||
#define __PUBSUBD_H__
|
||||
|
||||
#include "queue.h"
|
||||
|
||||
struct message {
|
||||
unsigned char *chan;
|
||||
size_t chanlen;
|
||||
unsigned char *data;
|
||||
size_t datalen;
|
||||
unsigned char type; // message type : alert, notification, …
|
||||
};
|
||||
|
||||
struct channel {
|
||||
unsigned char *chan;
|
||||
size_t chanlen;
|
||||
};
|
||||
|
||||
struct channels {
|
||||
struct channel *chan;
|
||||
LIST_ENTRY(channels) entries;
|
||||
};
|
||||
|
||||
int pubsubd_channels_eq (const struct channels *c1, const struct channels *c2);
|
||||
|
||||
struct app_list {
|
||||
struct process *p;
|
||||
LIST_ENTRY(app_list) entries;
|
||||
};
|
||||
|
||||
void pubsubd_msg_send (struct service *, struct message *msg, struct process *p);
|
||||
void pubsubd_msg_recv (struct service *, struct message *msg, struct process *p);
|
||||
|
||||
struct process * pubsubd_subscriber_get (const struct app_list *
|
||||
, const struct process *);
|
||||
void pubsubd_subscriber_del (struct app_list *al, struct process *p);
|
||||
|
||||
void pubsub_msg_send (struct service *, struct message *msg);
|
||||
void pubsub_msg_recv (struct service *, struct message *msg);
|
||||
|
||||
#endif
|
Reference in New Issue