more sms storage WIP
This commit is contained in:
parent
8ec1e3b61d
commit
ef712780bc
|
@ -243,6 +243,7 @@ AC_OUTPUT(
|
|||
tests/atlocal
|
||||
tests/smpp/Makefile
|
||||
tests/db_sms/Makefile
|
||||
tests/sms_storage/Makefile
|
||||
tests/sms_queue/Makefile
|
||||
tests/msc_vlr/Makefile
|
||||
tests/sdp_msg/Makefile
|
||||
|
|
|
@ -280,6 +280,8 @@ enum gsm_sms_source_id {
|
|||
SMS_SOURCE_SMPP, /* received via SMPP */
|
||||
};
|
||||
|
||||
extern const struct value_string gsm_sms_source_name[];
|
||||
|
||||
#define SMS_TEXT_SIZE 256
|
||||
|
||||
struct gsm_sms_addr {
|
||||
|
@ -288,8 +290,21 @@ struct gsm_sms_addr {
|
|||
char addr[21+1];
|
||||
};
|
||||
|
||||
enum gsm_sms_state {
|
||||
GSM_SMS_ST_ALLOCATED, /* memory allocated */
|
||||
GSM_SMS_ST_STORAGE_PENDING, /* waiting for it to be stored on disk */
|
||||
GSM_SMS_ST_DELIVERY_PENDING,
|
||||
GSM_SMS_ST_PAGING, /* delivery pending, paging started */
|
||||
GSM_SMS_ST_DELIVERING, /* delivery ongoing (after paging succeeded) */
|
||||
GSM_SMS_ST_DELIVERED, /* successfully delivered */
|
||||
};
|
||||
|
||||
extern const struct value_string gsm_sms_state_name[];
|
||||
|
||||
struct gsm_sms {
|
||||
struct llist_head list;
|
||||
struct llist_head list; /* entry in global list of pending SMS */
|
||||
struct llist_head vsub_list; /* entry in vlr_subscr.sms.pending */
|
||||
enum gsm_sms_state state;
|
||||
unsigned long long id;
|
||||
struct vlr_subscr *receiver;
|
||||
struct gsm_sms_addr src, dst;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <limits.h>
|
||||
|
||||
struct sms_storage_inst;
|
||||
struct gsm_sms;
|
||||
|
||||
|
||||
/* configuration of SMS storage */
|
||||
struct sms_storage_cfg {
|
||||
char storage_dir[PATH_MAX+1];
|
||||
/* unlink messages after delivery, or just move them? */
|
||||
bool unlink_delivered;
|
||||
/* unlink messages after expiration, or just move them? */
|
||||
bool unlink_expired;
|
||||
};
|
||||
|
||||
enum smss_delete_cause {
|
||||
SMSS_DELETE_CAUSE_UNKNOWN,
|
||||
SMSS_DELETE_CAUSE_DELIVERED,
|
||||
SMSS_DELETE_CAUSE_EXPIRED,
|
||||
};
|
||||
|
||||
|
||||
struct sms_storage_inst *sms_storage_init(void *ctx, const struct sms_storage_cfg *scfg);
|
||||
|
||||
int sms_storage_to_disk_req(struct sms_storage_inst *ssi, struct gsm_sms *sms);
|
||||
|
||||
int sms_storage_delete_from_disk_req(struct sms_storage_inst *ssi, unsigned long long id,
|
||||
enum smss_delete_cause cause);
|
|
@ -61,6 +61,26 @@
|
|||
#include "smpp_smsc.h"
|
||||
#endif
|
||||
|
||||
const struct value_string gsm_sms_source_name[] = {
|
||||
{ SMS_SOURCE_UNKNOWN, "UNKNOWN" },
|
||||
{ SMS_SOURCE_MS_GSM, "MS-GSM" },
|
||||
{ SMS_SOURCE_MS_UMTS, "MS-UMTS" },
|
||||
{ SMS_SOURCE_MS_SGS, "MS-SGs" },
|
||||
{ SMS_SOURCE_VTY, "VTY" },
|
||||
{ SMS_SOURCE_SMPP, "SMPP" },
|
||||
{ 0, NULL }
|
||||
};
|
||||
|
||||
const struct value_string gsm_sms_state_name[] = {
|
||||
{ GSM_SMS_ST_ALLOCATED, "ALLOCATED" },
|
||||
{ GSM_SMS_ST_STORAGE_PENDING, "STORAGE_PENDING" },
|
||||
{ GSM_SMS_ST_DELIVERY_PENDING, "DELIVERY_PENDING" },
|
||||
{ GSM_SMS_ST_PAGING, "PAGING" },
|
||||
{ GSM_SMS_ST_DELIVERING, "DELIVERING" },
|
||||
{ GSM_SMS_ST_DELIVERED, "DELIVERED" },
|
||||
{ 0, NULL }
|
||||
};
|
||||
|
||||
void *tall_gsms_ctx;
|
||||
static pthread_mutex_t tall_sms_mutex;
|
||||
static uint32_t new_callref = 0x40000001;
|
||||
|
@ -76,12 +96,17 @@ struct gsm_sms *sms_alloc(void)
|
|||
pthread_mutex_lock(&tall_sms_mutex);
|
||||
sms = talloc_zero(tall_gsms_ctx, struct gsm_sms);
|
||||
pthread_mutex_unlock(&tall_sms_mutex);
|
||||
if (sms)
|
||||
sms->state = GSM_SMS_ST_ALLOCATED;
|
||||
return sms;
|
||||
}
|
||||
|
||||
/* MUST ONLY BE CALLED ON MAIN THREAD */
|
||||
void sms_free(struct gsm_sms *sms)
|
||||
{
|
||||
llist_del(&sms->list);
|
||||
llist_del(&sms->vsub_list);
|
||||
|
||||
/* drop references to subscriber structure */
|
||||
if (sms->receiver)
|
||||
vlr_subscr_put(sms->receiver, VSUB_USE_SMS_RECEIVER);
|
||||
|
@ -893,6 +918,10 @@ static int gsm411_rx_rp_ack(struct gsm_trans *trans,
|
|||
/* mark this SMS as sent in database */
|
||||
sms_storage_delete_from_disk_req(trans->net->sms_storage, sms->id, SMSS_DELETE_CAUSE_DELIVERED);
|
||||
|
||||
/* delete from lists before sending signal, as the latter will attempt to send another SMS */
|
||||
llist_del(&sms->list);
|
||||
llist_del(&sms->vsub_list);
|
||||
|
||||
send_signal(S_SMS_DELIVERED, trans, sms, 0);
|
||||
|
||||
if (sms->status_rep_req)
|
||||
|
@ -1192,6 +1221,8 @@ int gsm411_send_sms(struct gsm_network *net,
|
|||
struct msgb *msg;
|
||||
int rc;
|
||||
|
||||
sms->state = GSM_SMS_ST_DELIVERING;
|
||||
|
||||
/* Allocate a new transaction for MT SMS */
|
||||
trans = gsm411_alloc_mt_trans(net, vsub);
|
||||
if (!trans) {
|
||||
|
|
|
@ -1180,6 +1180,7 @@ DEFUN(show_subscr_cache, show_subscr_cache_cmd,
|
|||
return CMD_SUCCESS;
|
||||
}
|
||||
|
||||
#if 0
|
||||
DEFUN(sms_send_pend,
|
||||
sms_send_pend_cmd,
|
||||
"sms send pending",
|
||||
|
@ -1237,6 +1238,7 @@ DEFUN(sms_delete_expired,
|
|||
vty_out(vty, "Deleted %llu expired SMS from database%s", num_deleted, VTY_NEWLINE);
|
||||
return CMD_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
static int _send_sms_str(struct vlr_subscr *receiver,
|
||||
const char *sender_msisdn,
|
||||
|
@ -1255,15 +1257,13 @@ static int _send_sms_str(struct vlr_subscr *receiver,
|
|||
sms->source = SMS_SOURCE_VTY;
|
||||
|
||||
/* store in database for the queue */
|
||||
if (db_sms_store(sms) != 0) {
|
||||
if (sms_storage_to_disk_req(net->sms_storage, sms) != 0) {
|
||||
LOGP(DLSMS, LOGL_ERROR, "Failed to store SMS in Database\n");
|
||||
sms_free(sms);
|
||||
return CMD_WARNING;
|
||||
}
|
||||
LOGP(DLSMS, LOGL_DEBUG, "SMS stored in DB\n");
|
||||
|
||||
sms_free(sms);
|
||||
sms_queue_trigger(net->sms_queue);
|
||||
return CMD_SUCCESS;
|
||||
}
|
||||
|
||||
|
@ -1333,6 +1333,7 @@ DEFUN_DEPRECATED(subscriber_create, subscriber_create_cmd,
|
|||
return CMD_WARNING;
|
||||
}
|
||||
|
||||
#if 0
|
||||
DEFUN(subscriber_send_pending_sms,
|
||||
subscriber_send_pending_sms_cmd,
|
||||
"subscriber " SUBSCR_TYPES " ID sms pending-send",
|
||||
|
@ -1380,6 +1381,7 @@ DEFUN(subscriber_sms_delete_all,
|
|||
|
||||
return CMD_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
DEFUN(subscriber_send_sms,
|
||||
subscriber_send_sms_cmd,
|
||||
|
@ -2085,8 +2087,8 @@ void msc_vty_init(struct gsm_network *msc_network)
|
|||
install_element_ve(&show_msc_transaction_cmd);
|
||||
install_element_ve(&show_nri_cmd);
|
||||
|
||||
install_element_ve(&sms_send_pend_cmd);
|
||||
install_element_ve(&sms_delete_expired_cmd);
|
||||
//install_element_ve(&sms_send_pend_cmd);
|
||||
//install_element_ve(&sms_delete_expired_cmd);
|
||||
|
||||
install_element_ve(&subscriber_create_cmd);
|
||||
install_element_ve(&subscriber_send_sms_cmd);
|
||||
|
@ -2101,8 +2103,8 @@ void msc_vty_init(struct gsm_network *msc_network)
|
|||
install_element_ve(&logging_fltr_imsi_cmd);
|
||||
|
||||
install_element(ENABLE_NODE, &ena_subscr_expire_cmd);
|
||||
install_element(ENABLE_NODE, &subscriber_send_pending_sms_cmd);
|
||||
install_element(ENABLE_NODE, &subscriber_sms_delete_all_cmd);
|
||||
//install_element(ENABLE_NODE, &subscriber_send_pending_sms_cmd);
|
||||
//install_element(ENABLE_NODE, &subscriber_sms_delete_all_cmd);
|
||||
|
||||
install_element(CONFIG_NODE, &cfg_mncc_int_cmd);
|
||||
install_node(&mncc_int_node, config_write_mncc_int);
|
||||
|
|
|
@ -106,28 +106,11 @@ static const struct rate_ctr_group_desc smsq_ctrg_desc = {
|
|||
osmo_stat_item_set(osmo_stat_item_group_get_item((smsq)->statg, idx), val)
|
||||
|
||||
|
||||
/* One in-RAM record of a "pending SMS". This is not the SMS itself, but merely
|
||||
* a pointer to the database record. It holds a reference on the vlr_subscriber
|
||||
* and some counters. While this object exists in RAM, we are regularly attempting
|
||||
* to deliver the related SMS. */
|
||||
#if 0
|
||||
struct gsm_sms_pending {
|
||||
struct llist_head entry; /* gsm_sms_queue.pending_sms */
|
||||
|
||||
struct vlr_subscr *vsub; /* destination subscriber for this SMS */
|
||||
struct msc_a *msc_a; /* MSC_A associated with this SMS */
|
||||
unsigned long long sms_id; /* unique ID (in SQL database) of this SMS */
|
||||
int failed_attempts; /* count of failed deliver attempts so far */
|
||||
int resend; /* should we try re-sending it (now) ? */
|
||||
};
|
||||
#endif
|
||||
|
||||
/* (global) state of the SMS queue. */
|
||||
struct gsm_sms_queue {
|
||||
struct osmo_timer_list resend_pending; /* timer triggering sms_resend_pending() */
|
||||
struct osmo_timer_list push_queue; /* timer triggering sms_submit_pending() */
|
||||
struct gsm_network *network;
|
||||
struct llist_head pending_sms; /* list of gsm_sms_pending */
|
||||
struct sms_queue_config *cfg;
|
||||
int pending; /* current number of gsm_sms_pending in RAM */
|
||||
|
||||
|
@ -149,6 +132,7 @@ static void _gsm411_send_sms(struct gsm_network *net, struct vlr_subscr *vsub, s
|
|||
static int sms_subscr_cb(unsigned int, unsigned int, void *, void *);
|
||||
static int sms_sms_cb(unsigned int, unsigned int, void *, void *);
|
||||
|
||||
#if 0
|
||||
/* look-up a 'gsm_sms_pending' for the given sms_id; return NULL if none */
|
||||
static struct gsm_sms_pending *sms_find_pending(struct gsm_sms_queue *smsq,
|
||||
unsigned long long sms_id)
|
||||
|
@ -168,94 +152,42 @@ int sms_queue_sms_is_pending(struct gsm_sms_queue *smsq, unsigned long long sms_
|
|||
{
|
||||
return sms_find_pending(smsq, sms_id) != NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* find the first pending SMS (in RAM) for the given subscriber */
|
||||
static struct gsm_sms_pending *sms_subscriber_find_pending(
|
||||
struct gsm_sms_queue *smsq,
|
||||
struct vlr_subscr *vsub)
|
||||
static struct gsm_sms *sms_subscriber_find_pending(struct vlr_subscr *vsub)
|
||||
{
|
||||
struct gsm_sms_pending *pending;
|
||||
struct gsm_sms *sms;
|
||||
|
||||
llist_for_each_entry(pending, &smsq->pending_sms, entry) {
|
||||
if (pending->vsub == vsub)
|
||||
return pending;
|
||||
llist_for_each_entry(sms, &vsub->sms.pending, vsub_list) {
|
||||
if (sms->state == GSM_SMS_ST_DELIVERY_PENDING)
|
||||
return sms;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
#if 0
|
||||
/* do we have any pending SMS (in RAM) for the given subscriber? */
|
||||
static int sms_subscriber_is_pending(struct gsm_sms_queue *smsq,
|
||||
struct vlr_subscr *vsub)
|
||||
{
|
||||
return sms_subscriber_find_pending(smsq, vsub) != NULL;
|
||||
return !llist_empty(&vsub->sms.pending);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* allocate a new gsm_sms_pending record and fill it with information from 'sms' */
|
||||
static struct gsm_sms_pending *sms_pending_from(struct gsm_sms_queue *smsq,
|
||||
struct gsm_sms *sms)
|
||||
{
|
||||
struct gsm_sms_pending *pending;
|
||||
|
||||
pending = talloc_zero(smsq, struct gsm_sms_pending);
|
||||
if (!pending)
|
||||
return NULL;
|
||||
|
||||
vlr_subscr_get(sms->receiver, VSUB_USE_SMS_PENDING);
|
||||
pending->vsub = sms->receiver;
|
||||
pending->sms_id = sms->id;
|
||||
llist_add_tail(&pending->entry, &smsq->pending_sms);
|
||||
|
||||
smsq->pending += 1;
|
||||
smsq_stat_item_inc(smsq, SMSQ_STAT_SMS_RAM_PENDING);
|
||||
|
||||
return pending;
|
||||
}
|
||||
|
||||
/* release a gsm_sms_pending object */
|
||||
static void sms_pending_free(struct gsm_sms_queue *smsq, struct gsm_sms_pending *pending)
|
||||
{
|
||||
smsq->pending -= 1;
|
||||
smsq_stat_item_dec(smsq, SMSQ_STAT_SMS_RAM_PENDING);
|
||||
vlr_subscr_put(pending->vsub, VSUB_USE_SMS_PENDING);
|
||||
llist_del(&pending->entry);
|
||||
talloc_free(pending);
|
||||
}
|
||||
|
||||
/* this sets the 'resend' flag of the gsm_sms_pending and schedules
|
||||
* the timer for re-sending */
|
||||
static void sms_pending_resend(struct gsm_sms_pending *pending)
|
||||
{
|
||||
struct gsm_network *net = pending->vsub->vlr->user_ctx;
|
||||
struct gsm_sms_queue *smsq;
|
||||
LOGP(DLSMS, LOGL_DEBUG,
|
||||
"Scheduling resend of SMS %llu.\n", pending->sms_id);
|
||||
|
||||
pending->resend = 1;
|
||||
|
||||
smsq = net->sms_queue;
|
||||
if (osmo_timer_pending(&smsq->resend_pending))
|
||||
return;
|
||||
|
||||
osmo_timer_schedule(&smsq->resend_pending, 1, 0);
|
||||
}
|
||||
|
||||
/* call-back when a pending SMS has failed; try another re-send if number of
|
||||
* attempts is < smsq->max_fail */
|
||||
static void sms_pending_failed(struct gsm_sms_pending *pending, int paging_error)
|
||||
static void sms_pending_failed(struct gsm_sms_queue *smsq, struct gsm_sms *sms, int paging_error)
|
||||
{
|
||||
struct gsm_network *net = pending->vsub->vlr->user_ctx;
|
||||
struct gsm_sms_queue *smsq;
|
||||
|
||||
pending->failed_attempts++;
|
||||
sms->failed_attempts++;
|
||||
sms->state = GSM_SMS_ST_DELIVERY_PENDING;
|
||||
LOGP(DLSMS, LOGL_NOTICE, "Sending SMS %llu failed %d times.\n",
|
||||
pending->sms_id, pending->failed_attempts);
|
||||
sms->id, sms->failed_attempts);
|
||||
|
||||
smsq = net->sms_queue;
|
||||
if (pending->failed_attempts < smsq->cfg->max_fail)
|
||||
return sms_pending_resend(pending);
|
||||
|
||||
sms_pending_free(smsq, pending);
|
||||
if (sms->failed_attempts >= smsq->cfg->max_fail)
|
||||
sms_free(sms);
|
||||
}
|
||||
|
||||
/* Resend all SMS that are scheduled for a resend. This is done to
|
||||
|
@ -264,27 +196,22 @@ static void sms_pending_failed(struct gsm_sms_pending *pending, int paging_error
|
|||
* DB and attempts to send them via _gsm411_send_sms() */
|
||||
static void sms_resend_pending(void *_data)
|
||||
{
|
||||
struct gsm_sms_pending *pending, *tmp;
|
||||
#if 0
|
||||
struct gsm_sms *sms, *tmp;
|
||||
struct gsm_sms_queue *smsq = _data;
|
||||
|
||||
llist_for_each_entry_safe(pending, tmp, &smsq->pending_sms, entry) {
|
||||
struct gsm_sms *sms;
|
||||
llist_for_each_entry_safe(sms, tmp, &smsq->pending_sms, list) {
|
||||
if (!pending->resend)
|
||||
continue;
|
||||
|
||||
sms = db_sms_get(smsq->network, pending->sms_id);
|
||||
|
||||
/* the sms is gone? Move to the next */
|
||||
if (!sms) {
|
||||
sms_pending_free(smsq, pending);
|
||||
sms_queue_trigger(smsq);
|
||||
} else {
|
||||
pending->resend = 0;
|
||||
_gsm411_send_sms(smsq->network, sms->receiver, sms);
|
||||
}
|
||||
/* FIXME: limit the number of concurrent attempted SMS */
|
||||
/* FIXME: have some per-sms state to avoid sending the same twice */
|
||||
_gsm411_send_sms(smsq->network, sms->receiver, sms);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if 0
|
||||
/* Find the next pending SMS by cycling through the recipients. We could also
|
||||
* cycle through the pending SMS, but that might cause us to keep trying to
|
||||
* send SMS to the same few subscribers repeatedly while not servicing other
|
||||
|
@ -335,6 +262,7 @@ struct gsm_sms *smsq_take_next_sms(struct gsm_network *net,
|
|||
DEBUGP(DLSMS, "SMS queue: no SMS to be sent\n");
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* read up to 'max_pending' pending SMS from the database and add them to the in-memory
|
||||
* sms_queue; trigger the first delivery attempt. 'submit' in this context means
|
||||
|
@ -342,6 +270,7 @@ struct gsm_sms *smsq_take_next_sms(struct gsm_network *net,
|
|||
* confused with the SMS SUBMIT operation a MS performs when sending a MO-SMS. */
|
||||
static void sms_submit_pending(void *_data)
|
||||
{
|
||||
#if 0
|
||||
struct gsm_sms_queue *smsq = _data;
|
||||
int attempts = smsq->cfg->max_pending - smsq->pending;
|
||||
int initialized = 0;
|
||||
|
@ -415,45 +344,19 @@ static void sms_submit_pending(void *_data)
|
|||
} while (attempted < attempts && rounds < 1000);
|
||||
|
||||
LOGP(DLSMS, LOGL_DEBUG, "SMSqueue added %d messages in %d rounds\n", attempted, rounds);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* obtain the next pending SMS for given subscriber from database,
|
||||
* create gsm_sms_pending object and attempt first delivery. If there
|
||||
* are no SMS pending for the given subscriber, call sms_submit_pending()
|
||||
* to read more SMS (for any subscriber) into the in-RAM pending queue */
|
||||
/* obtain the next pending SMS for given subscriber and attempt to deliver it. */
|
||||
static void sms_send_next(struct vlr_subscr *vsub)
|
||||
{
|
||||
struct gsm_network *net = vsub->vlr->user_ctx;
|
||||
struct gsm_sms_queue *smsq = net->sms_queue;
|
||||
struct gsm_sms_pending *pending;
|
||||
struct gsm_sms *sms;
|
||||
struct gsm_sms *sms = sms_subscriber_find_pending(vsub);
|
||||
|
||||
/* the subscriber should not be in the queue */
|
||||
OSMO_ASSERT(!sms_subscriber_is_pending(smsq, vsub));
|
||||
|
||||
/* check for more messages for this subscriber */
|
||||
sms = db_sms_get_unsent_for_subscr(vsub, INT_MAX);
|
||||
if (!sms)
|
||||
goto no_pending_sms;
|
||||
return;
|
||||
|
||||
/* The sms should not be scheduled right now */
|
||||
OSMO_ASSERT(!sms_queue_sms_is_pending(smsq, sms->id));
|
||||
|
||||
/* Remember that we deliver this SMS and send it */
|
||||
pending = sms_pending_from(smsq, sms);
|
||||
if (!pending) {
|
||||
LOGP(DLSMS, LOGL_ERROR,
|
||||
"Failed to create pending SMS entry.\n");
|
||||
sms_free(sms);
|
||||
goto no_pending_sms;
|
||||
}
|
||||
|
||||
_gsm411_send_sms(smsq->network, sms->receiver, sms);
|
||||
return;
|
||||
|
||||
no_pending_sms:
|
||||
/* Try to send the SMS to avoid the queue being stuck */
|
||||
sms_submit_pending(net->sms_queue);
|
||||
_gsm411_send_sms(net, sms->receiver, sms);
|
||||
}
|
||||
|
||||
/* Trigger a call to sms_submit_pending() in one second */
|
||||
|
@ -504,7 +407,7 @@ int sms_queue_start(struct gsm_network *network)
|
|||
goto err_statg;
|
||||
|
||||
network->sms_queue = sms;
|
||||
INIT_LLIST_HEAD(&sms->pending_sms);
|
||||
//INIT_LLIST_HEAD(&sms->pending_sms);
|
||||
sms->network = network;
|
||||
osmo_timer_setup(&sms->push_queue, sms_submit_pending, sms);
|
||||
osmo_timer_setup(&sms->resend_pending, sms_resend_pending, sms);
|
||||
|
@ -512,6 +415,7 @@ int sms_queue_start(struct gsm_network *network)
|
|||
osmo_signal_register_handler(SS_SUBSCR, sms_subscr_cb, network);
|
||||
osmo_signal_register_handler(SS_SMS, sms_sms_cb, network);
|
||||
|
||||
#if 0
|
||||
if (db_init(sms, sms->cfg->db_file_path, true)) {
|
||||
LOGP(DMSC, LOGL_FATAL, "DB: Failed to init database: %s\n",
|
||||
osmo_quote_str(sms->cfg->db_file_path, -1));
|
||||
|
@ -522,6 +426,7 @@ int sms_queue_start(struct gsm_network *network)
|
|||
LOGP(DMSC, LOGL_FATAL, "DB: Failed to prepare database.\n");
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
sms_submit_pending(sms);
|
||||
|
||||
|
@ -538,9 +443,6 @@ err_free:
|
|||
/* call-back: Given subscriber is now ready for short messages. */
|
||||
static int sub_ready_for_sm(struct gsm_network *net, struct vlr_subscr *vsub)
|
||||
{
|
||||
struct gsm_sms *sms;
|
||||
struct gsm_sms_pending *pending;
|
||||
|
||||
/*
|
||||
* The code used to be very clever and tried to submit
|
||||
* a SMS during the Location Updating Request. This has
|
||||
|
@ -555,21 +457,8 @@ static int sub_ready_for_sm(struct gsm_network *net, struct vlr_subscr *vsub)
|
|||
* We need to be careful in what we try here.
|
||||
*/
|
||||
|
||||
/* check if we have pending requests */
|
||||
pending = sms_subscriber_find_pending(net->sms_queue, vsub);
|
||||
if (pending) {
|
||||
LOGP(DMSC, LOGL_NOTICE,
|
||||
"Pending paging while subscriber %llu attached.\n",
|
||||
vsub->id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Now try to deliver any pending SMS to this sub */
|
||||
sms = db_sms_get_unsent_for_subscr(vsub, INT_MAX);
|
||||
if (!sms)
|
||||
return -1;
|
||||
|
||||
_gsm411_send_sms(net, vsub, sms);
|
||||
/* check if we have pending SMS + send the first of them */
|
||||
sms_send_next(vsub);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -593,8 +482,6 @@ static int sms_sms_cb(unsigned int subsys, unsigned int signal,
|
|||
struct gsm_network *network = handler_data;
|
||||
struct gsm_sms_queue *smq = network->sms_queue;
|
||||
struct sms_signal_data *sig_sms = signal_data;
|
||||
struct gsm_sms_pending *pending;
|
||||
struct vlr_subscr *vsub;
|
||||
|
||||
/* We got a new SMS and maybe should launch the queue again. */
|
||||
if (signal == S_SMS_SUBMITTED || signal == S_SMS_SMMA) {
|
||||
|
@ -607,28 +494,16 @@ static int sms_sms_cb(unsigned int subsys, unsigned int signal,
|
|||
return -1;
|
||||
|
||||
|
||||
/*
|
||||
* Find the entry of our queue. The SMS subsystem will submit
|
||||
* sms that are not in our control as we just have a channel
|
||||
* open anyway.
|
||||
*/
|
||||
pending = sms_find_pending(smq, sig_sms->sms->id);
|
||||
if (!pending)
|
||||
return 0;
|
||||
|
||||
switch (signal) {
|
||||
case S_SMS_DELIVERED:
|
||||
/* core SMS code has already requested deletion from storage */
|
||||
smsq_rate_ctr_inc(smq, SMSQ_CTR_SMS_DELIVERY_ACK);
|
||||
/* ask SMS thread to delete message from storage */
|
||||
ms_storage_delete_from_disk_req(network->sms_storage, sms->id,
|
||||
SMSS_DELETE_CAUSE_DELIVERED);
|
||||
sms_free(network->sms_storage, sms);
|
||||
/* Attempt to send another SMS to this subscriber */
|
||||
sms_send_next(vsub);
|
||||
/* Attempt to send another SMS (if any pending) to this subscriber */
|
||||
sms_send_next(sig_sms->sms->receiver);
|
||||
break;
|
||||
case S_SMS_MEM_EXCEEDED:
|
||||
smsq_rate_ctr_inc(smq, SMSQ_CTR_SMS_DELIVERY_NOMEM);
|
||||
sms_pending_free(smq, pending);
|
||||
/* TODO: set flag for subscriber in VLR; skip delivery until cleared */
|
||||
sms_queue_trigger(smq);
|
||||
break;
|
||||
case S_SMS_UNKNOWN_ERROR:
|
||||
|
@ -645,11 +520,10 @@ static int sms_sms_cb(unsigned int subsys, unsigned int signal,
|
|||
if (sig_sms->paging_result) {
|
||||
smsq_rate_ctr_inc(smq, SMSQ_CTR_SMS_DELIVERY_ERR);
|
||||
/* BAD SMS? */
|
||||
db_sms_inc_deliver_attempts(sig_sms->sms);
|
||||
sms_pending_failed(pending, 0);
|
||||
sms_pending_failed(smq, sig_sms->sms, 0);
|
||||
} else {
|
||||
smsq_rate_ctr_inc(smq, SMSQ_CTR_SMS_DELIVERY_TIMEOUT);
|
||||
sms_pending_failed(pending, 1);
|
||||
sms_pending_failed(smq, sig_sms->sms, 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -660,6 +534,7 @@ static int sms_sms_cb(unsigned int subsys, unsigned int signal,
|
|||
return 0;
|
||||
}
|
||||
|
||||
#if 0
|
||||
/* VTY helper functions */
|
||||
int sms_queue_stats(struct gsm_sms_queue *smsq, struct vty *vty)
|
||||
{
|
||||
|
@ -687,3 +562,4 @@ int sms_queue_clear(struct gsm_sms_queue *smsq)
|
|||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -83,10 +83,11 @@
|
|||
#include <osmocom/msc/gsm_data.h>
|
||||
#include <osmocom/msc/gsm_04_11.h>
|
||||
#include <osmocom/msc/sms_storage.h>
|
||||
#include <osmocom/msc/vlr.h>
|
||||
|
||||
/* all the state of a SMS storage instance */
|
||||
struct sms_storage_inst {
|
||||
struct sms_storage_cfg *cfg;
|
||||
const struct sms_storage_cfg *cfg;
|
||||
pthread_t thread;
|
||||
|
||||
struct {
|
||||
|
@ -102,6 +103,10 @@ struct sms_storage_inst {
|
|||
int wd;
|
||||
} inotify;
|
||||
#endif
|
||||
|
||||
/* global list of penidng SMSs */
|
||||
struct llist_head pending;
|
||||
|
||||
/* inter-thread message queues for both directions */
|
||||
struct {
|
||||
struct osmo_it_q *itq;
|
||||
|
@ -226,7 +231,7 @@ static int _sms_gen_fq_path(struct sms_storage_inst *ssi, char *fq_path, size_t
|
|||
int rc;
|
||||
|
||||
rc = snprintf(fq_path, fq_path_len, "%s/%s/%llu.osms", ssi->cfg->storage_dir, subdir, id);
|
||||
if (rc >= sizeof(fq_path)) {
|
||||
if (rc >= fq_path_len) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Overflowing buffer while composing file path\n");
|
||||
return -EINVAL;
|
||||
}
|
||||
|
@ -677,6 +682,9 @@ static void boot_read_tmr_cb(void *data)
|
|||
|
||||
/* skip anything that's not a normal file */
|
||||
if (dent->d_type != DT_REG) {
|
||||
/* suppress printing log messages about . and .. */
|
||||
if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
|
||||
goto next;
|
||||
LOGP(DSMSS, LOGL_NOTICE, "bootstrap read: skipping '%s' (not a regular file)\n",
|
||||
dent->d_name);
|
||||
goto next;
|
||||
|
@ -710,6 +718,8 @@ static void boot_read_tmr_cb(void *data)
|
|||
if (rc < 0)
|
||||
s2m_free(ssi, evt);
|
||||
|
||||
ssi->boot_read.count++;
|
||||
|
||||
next:
|
||||
/* read next message in 50ms to avoid overloading the it_q or the MSC in general */
|
||||
osmo_timer_schedule(&ssi->boot_read.timer, 0, 50000);
|
||||
|
@ -719,8 +729,13 @@ next:
|
|||
static void *sms_storage_main(void *arg)
|
||||
{
|
||||
struct sms_storage_inst *ssi = arg;
|
||||
char current_dir[PATH_MAX+8+1];
|
||||
|
||||
ssi->boot_read.dir = opendir(ssi->cfg->storage_dir);
|
||||
osmo_ctx_init("sms-storage");
|
||||
osmo_select_init();
|
||||
|
||||
snprintf(current_dir, sizeof(current_dir), "%s/%s", ssi->cfg->storage_dir, SUBDIR_CURRENT);
|
||||
ssi->boot_read.dir = opendir(current_dir);
|
||||
if (!ssi->boot_read.dir) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Cannot open SMS directory '%s': %s\n",
|
||||
ssi->cfg->storage_dir, strerror(errno));
|
||||
|
@ -753,18 +768,34 @@ static void storage2main_read_cb(struct osmo_it_q *q, struct llist_head *item)
|
|||
{
|
||||
struct smss_s2m_evt *evt = container_of(item, struct smss_s2m_evt, list);
|
||||
struct sms_storage_inst *ssi = q->data;
|
||||
struct gsm_sms *sms = NULL;
|
||||
|
||||
switch (evt->op) {
|
||||
case SMSS_S2M_OP_NULL:
|
||||
break;
|
||||
case SMSS_S2M_OP_SMS_FROM_DISK_IND:
|
||||
/* SMS storage has read a SMS from disk, asks main thread to add it to queue */
|
||||
sms = evt->sms_from_disk_ind.sms;
|
||||
sms->state = GSM_SMS_ST_DELIVERY_PENDING;
|
||||
/* add to global list of pending SMS */
|
||||
llist_add_tail(&sms->list, &ssi->pending);
|
||||
/* add to per-subscriber list of pending SMS */
|
||||
if (sms->receiver)
|
||||
llist_add_tail(&sms->vsub_list, &sms->receiver->sms.pending);
|
||||
break;
|
||||
case SMSS_S2M_OP_SMS_TO_DISK_CFM:
|
||||
/* SMS storage confirms having written SMS to disk; main thread adds it to queue */
|
||||
sms = evt->sms_to_disk_cfm.sms;
|
||||
sms->state = GSM_SMS_ST_DELIVERY_PENDING;
|
||||
/* add to global list of pending SMS */
|
||||
llist_add_tail(&sms->list, &ssi->pending);
|
||||
/* add to per-subscriber list of pending SMS */
|
||||
if (sms->receiver)
|
||||
llist_add_tail(&sms->vsub_list, &sms->receiver->sms.pending);
|
||||
break;
|
||||
case SMSS_S2M_OP_SMS_DELETED_ON_DISK_IND:
|
||||
/* SMS storage has detected a sms was deleted from disk; main thread must forget it */
|
||||
sms_free(sms);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -778,16 +809,19 @@ static void storage2main_read_cb(struct osmo_it_q *q, struct llist_head *item)
|
|||
int sms_storage_to_disk_req(struct sms_storage_inst *ssi, struct gsm_sms *sms)
|
||||
{
|
||||
struct smss_m2s_evt *evt = m2s_alloc(ssi, SMSS_M2S_OP_SMS_TO_DISK_REQ);
|
||||
enum gsm_sms_state st = sms->state;
|
||||
int rc;
|
||||
|
||||
if (!evt)
|
||||
return -ENOMEM;
|
||||
|
||||
sms->state = GSM_SMS_ST_STORAGE_PENDING;
|
||||
evt->sms_to_disk_req.sms = sms;
|
||||
|
||||
rc = osmo_it_q_enqueue(ssi->main2storage.itq, evt, list);
|
||||
if (rc < 0) {
|
||||
m2s_free(ssi, evt);
|
||||
sms->state = st;
|
||||
return rc;
|
||||
}
|
||||
return 0;
|
||||
|
@ -819,31 +853,97 @@ int sms_storage_delete_from_disk_req(struct sms_storage_inst *ssi, unsigned long
|
|||
* Initialization
|
||||
***********************************************************************/
|
||||
|
||||
int sms_storage_init(void *ctx, struct sms_storage_cfg *scfg)
|
||||
static int sms_storage_ensure_subdir(const struct sms_storage_cfg *scfg, const char *subdir)
|
||||
{
|
||||
char sub_dir[PATH_MAX+8+1];
|
||||
struct stat st;
|
||||
int rc;
|
||||
|
||||
snprintf(sub_dir, sizeof(sub_dir), "%s/%s", scfg->storage_dir, subdir);
|
||||
|
||||
rc = stat(sub_dir, &st);
|
||||
if (rc < 0) {
|
||||
if (errno == ENOENT) {
|
||||
LOGP(DSMSS, LOGL_NOTICE, "SMS storage sub-dir '%s' doesn't exist, attempting to "
|
||||
"create it\n", sub_dir);
|
||||
if (mkdir(sub_dir, 0700) != 0) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Unable to create SMS storage sub-dir '%s': %s\n",
|
||||
sub_dir, strerror(errno));
|
||||
return -errno;
|
||||
}
|
||||
} else {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Unable to access SMS storage sub-dir '%s': %s\n",
|
||||
sub_dir, strerror(errno));
|
||||
return -errno;
|
||||
}
|
||||
}
|
||||
/* TODO: test if we can write */
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sms_storage_ensure_subdirs(const struct sms_storage_cfg *scfg)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = sms_storage_ensure_subdir(scfg, SUBDIR_CURRENT);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
|
||||
rc = sms_storage_ensure_subdir(scfg, SUBDIR_DELIVERED);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
|
||||
rc = sms_storage_ensure_subdir(scfg, SUBDIR_EXPIRED);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
struct sms_storage_inst *sms_storage_init(void *ctx, const struct sms_storage_cfg *scfg)
|
||||
{
|
||||
struct sms_storage_inst *ssi = talloc_zero(ctx, struct sms_storage_inst);
|
||||
struct stat st;
|
||||
int rc, ret = -1;
|
||||
int rc;
|
||||
|
||||
if (!ssi)
|
||||
return -ENOMEM;
|
||||
return NULL;
|
||||
|
||||
/* test if scfq->storage_dir exists */
|
||||
ssi->cfg = scfg;
|
||||
INIT_LLIST_HEAD(&ssi->pending);
|
||||
|
||||
/* test if scfg->storage_dir exists */
|
||||
rc = stat(scfg->storage_dir, &st);
|
||||
if (rc < 0) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Unable to access storage path '%s': %s\n",
|
||||
scfg->storage_dir, strerror(errno));
|
||||
return -errno;
|
||||
if (errno == ENOENT) {
|
||||
LOGP(DSMSS, LOGL_NOTICE, "SMS storage path '%s' doesn't exist, attempting to "
|
||||
"create it\n", scfg->storage_dir);
|
||||
if (mkdir(scfg->storage_dir, 0700) != 0) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Unable to create SMS storage dir '%s': %s\n",
|
||||
scfg->storage_dir, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Unable to access storage path '%s': %s\n",
|
||||
scfg->storage_dir, strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
/* TODO: test if we can write */
|
||||
|
||||
rc = sms_storage_ensure_subdirs(scfg);
|
||||
if (rc < 0)
|
||||
goto out_free;
|
||||
|
||||
ssi->main2storage.itq = osmo_it_q_alloc(ssi, "sms_main2storage", 1000, main2storage_read_cb, ssi);
|
||||
if (!ssi->main2storage.itq)
|
||||
goto out_free;
|
||||
pthread_mutex_init(&ssi->main2storage.ctx_mutex, NULL);
|
||||
|
||||
ssi->storage2main.itq = osmo_it_q_alloc(ssi, "sms_storage2main", 1000, storage2main_read_cb, ssi);
|
||||
if (!ssi->main2storage.itq)
|
||||
if (!ssi->storage2main.itq)
|
||||
goto out_main2storage;
|
||||
pthread_mutex_init(&ssi->storage2main.ctx_mutex, NULL);
|
||||
|
||||
|
@ -858,7 +958,6 @@ int sms_storage_init(void *ctx, struct sms_storage_cfg *scfg)
|
|||
|
||||
if (inotify_fd < 0) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Error during inotify_init(): %s\n", strerror(errno));
|
||||
ret = inotify_fd;
|
||||
goto out_m2s_unreg;
|
||||
}
|
||||
/* just setup, don't register. We later register this in the storage thread! */
|
||||
|
@ -869,7 +968,6 @@ int sms_storage_init(void *ctx, struct sms_storage_cfg *scfg)
|
|||
if (rc < 0) {
|
||||
LOGP(DSMSS, LOGL_ERROR, "Cannot add inotify watcher for '%s': %s\n",
|
||||
current_dir, strerror(errno));
|
||||
ret = -errno;
|
||||
goto out_close_inotify;
|
||||
}
|
||||
ssi->inotify.wd = rc;
|
||||
|
@ -880,7 +978,7 @@ int sms_storage_init(void *ctx, struct sms_storage_cfg *scfg)
|
|||
goto out_all;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return ssi;
|
||||
|
||||
out_all:
|
||||
#ifdef HAVE_INOTIFY
|
||||
|
@ -896,5 +994,5 @@ out_main2storage:
|
|||
out_free:
|
||||
talloc_free(ssi);
|
||||
|
||||
return ret;
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
@ -117,6 +117,19 @@ DEFUN(cfg_sms_def_val_per, cfg_sms_def_val_per_cmd,
|
|||
* View / Enable Node
|
||||
***********************************************************************/
|
||||
|
||||
void vty_out_sms(struct vty *vty, const struct gsm_sms *sms)
|
||||
{
|
||||
vty_out(vty, "SMS ID: %llu, State: %s, Source: %s, %s -> %s %s", sms->id,
|
||||
get_value_string(gsm_sms_state_name, sms->state),
|
||||
get_value_string(gsm_sms_source_name, sms->source),
|
||||
sms->src.addr, sms->dst.addr, VTY_NEWLINE);
|
||||
vty_out(vty, " Created: %s, Validity Minutes: %lu, Failed Attempts: %d%s",
|
||||
ctime(&sms->created), sms->validity_minutes, sms->failed_attempts, VTY_NEWLINE);
|
||||
vty_out(vty, " PID: 0x%02x, DCS: 0x%02x, MsgRef: 0x%02x, UDHI: %d, IsReport: %d%s",
|
||||
sms->protocol_id, sms->data_coding_scheme, sms->msg_ref, sms->ud_hdr_ind, sms->is_report,
|
||||
VTY_NEWLINE);
|
||||
}
|
||||
|
||||
DEFUN(show_smsqueue,
|
||||
show_smsqueue_cmd,
|
||||
"show sms-queue",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
SUBDIRS = \
|
||||
sms_storage \
|
||||
sms_queue \
|
||||
msc_vlr \
|
||||
db_sms \
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
AM_CPPFLAGS = \
|
||||
$(all_includes) \
|
||||
-I$(top_srcdir)/include \
|
||||
$(NULL)
|
||||
|
||||
AM_CFLAGS = \
|
||||
-Wall \
|
||||
-ggdb3 \
|
||||
$(LIBOSMOCORE_CFLAGS) \
|
||||
$(LIBOSMOGSM_CFLAGS) \
|
||||
$(LIBOSMOVTY_CFLAGS) \
|
||||
$(LIBOSMOABIS_CFLAGS) \
|
||||
$(LIBOSMONETIF_CFLAGS) \
|
||||
$(LIBOSMOSIGTRAN_CFLAGS) \
|
||||
$(LIBOSMORANAP_CFLAGS) \
|
||||
$(LIBASN1C_CFLAGS) \
|
||||
$(LIBOSMOMGCPCLIENT_CFLAGS) \
|
||||
$(LIBOSMOGSUPCLIENT_CFLAGS) \
|
||||
$(NULL)
|
||||
|
||||
EXTRA_DIST = \
|
||||
sms_storage_test.ok \
|
||||
sms_storage_test.err \
|
||||
$(NULL)
|
||||
|
||||
check_PROGRAMS = \
|
||||
sms_storage_test \
|
||||
$(NULL)
|
||||
|
||||
sms_storage_test_SOURCES = \
|
||||
sms_storage_test.c \
|
||||
$(NULL)
|
||||
|
||||
sms_storage_test_LDADD = \
|
||||
-lsctp \
|
||||
$(top_builddir)/src/libmsc/libmsc.a \
|
||||
$(top_builddir)/src/libvlr/libvlr.a \
|
||||
$(LIBSMPP34_LIBS) \
|
||||
$(LIBOSMOCORE_LIBS) \
|
||||
$(LIBOSMOGSM_LIBS) \
|
||||
$(LIBOSMOVTY_LIBS) \
|
||||
$(LIBOSMOABIS_LIBS) \
|
||||
$(LIBOSMONETIF_LIBS) \
|
||||
$(LIBOSMOSIGTRAN_LIBS) \
|
||||
$(LIBOSMORANAP_LIBS) \
|
||||
$(LIBASN1C_LIBS) \
|
||||
$(LIBOSMOMGCPCLIENT_LIBS) \
|
||||
$(LIBOSMOGSUPCLIENT_LIBS) \
|
||||
$(LIBRARY_GSM) \
|
||||
$(NULL)
|
||||
|
||||
sms_storage_test_LDFLAGS = \
|
||||
-Wl,--wrap=db_sms_get_next_unsent_rr_msisdn \
|
||||
$(NULL)
|
|
@ -0,0 +1,49 @@
|
|||
#include <unistd.h>
|
||||
|
||||
#include <osmocom/core/utils.h>
|
||||
|
||||
#include <osmocom/msc/gsm_data.h>
|
||||
#include <osmocom/msc/gsm_04_11.h>
|
||||
#include <osmocom/msc/sms_storage.h>
|
||||
|
||||
static const struct sms_storage_cfg scfg = {
|
||||
.storage_dir = "/tmp/sms_storage",
|
||||
.unlink_delivered = false,
|
||||
.unlink_expired = false,
|
||||
};
|
||||
static struct sms_storage_inst *g_ssi;
|
||||
|
||||
static struct gsm_sms *generate_sms(unsigned long long id, const char *src, const char *dst,
|
||||
uint8_t pid, uint8_t dcs, uint8_t msg_ref)
|
||||
{
|
||||
struct gsm_sms *sms = sms_alloc();
|
||||
OSMO_ASSERT(sms);
|
||||
|
||||
sms->id = id;
|
||||
OSMO_STRLCPY_ARRAY(sms->src.addr, src);
|
||||
OSMO_STRLCPY_ARRAY(sms->dst.addr, dst);
|
||||
sms->protocol_id = pid;
|
||||
sms->data_coding_scheme = dcs;
|
||||
sms->msg_ref = msg_ref;
|
||||
|
||||
return sms;
|
||||
}
|
||||
|
||||
static void to_storage(void)
|
||||
{
|
||||
struct gsm_sms *sms = generate_sms(1234, "1111", "2222", 1, 2, 3);
|
||||
sms_storage_to_disk_req(g_ssi, sms);
|
||||
sms_storage_delete_from_disk_req(g_ssi, sms->id, SMSS_DELETE_CAUSE_DELIVERED);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
void *ctx = NULL;
|
||||
|
||||
g_ssi = sms_storage_init(ctx, &scfg);
|
||||
|
||||
to_storage();
|
||||
|
||||
usleep(10000000);
|
||||
}
|
||||
|
Loading…
Reference in New Issue