diff --git a/TODO-RELEASE b/TODO-RELEASE index 82368ff47..964ffe111 100644 --- a/TODO-RELEASE +++ b/TODO-RELEASE @@ -35,3 +35,6 @@ libosmo-mgcp-client remove public API These public API items have not been calle mgcp_client_cancel() mgcp_msg_gen() mgcp_msg_trans_id() +libosmo-mgcp-client deprecate public API New code should no longer use codecs[], instead use ptmap[].codec. There + is backwards compat code that moves codecs[] entries, if any, over to + ptmap[], so callers may migrate at own leisure. diff --git a/include/osmocom/mgcp_client/mgcp_client.h b/include/osmocom/mgcp_client/mgcp_client.h index a96ae197f..1d336905a 100644 --- a/include/osmocom/mgcp_client/mgcp_client.h +++ b/include/osmocom/mgcp_client/mgcp_client.h @@ -79,6 +79,8 @@ struct ptmap { unsigned int pt; }; +int ptmap_cmp(const struct ptmap *a, const struct ptmap *b); + enum mgcp_verb { MGCP_VERB_CRCX, MGCP_VERB_MDCX, diff --git a/include/osmocom/mgcp_client/mgcp_client_fsm.h b/include/osmocom/mgcp_client/mgcp_client_fsm.h index 4e9ba8977..dbd51288e 100644 --- a/include/osmocom/mgcp_client/mgcp_client_fsm.h +++ b/include/osmocom/mgcp_client/mgcp_client_fsm.h @@ -29,11 +29,11 @@ struct mgcp_conn_peer { /*! RTP packetization interval (optional) */ unsigned int ptime; - /*! RTP codec list (optional) */ - enum mgcp_codecs codecs[MGCP_MAX_CODECS]; - - /*! Number of codecs in RTP codec list (optional) */ - unsigned int codecs_len; + /*! Deprecated. Use only ptmap[].codec in new code. */ + enum mgcp_codecs codecs[MGCP_MAX_CODECS] + OSMO_DEPRECATED_OUTSIDE_LIBOSMOMGCPCLIENT("use ptmap[i].codec instead"); + unsigned int codecs_len + OSMO_DEPRECATED_OUTSIDE_LIBOSMOMGCPCLIENT("use ptmap[] and ptmap_len instead"); /*! RTP payload type map (optional, only needed when payload types are * used that differ from what IANA/3GPP defines) */ diff --git a/include/osmocom/mgcp_client/mgcp_client_internal.h b/include/osmocom/mgcp_client/mgcp_client_internal.h index c3619bbc6..423c00e66 100644 --- a/include/osmocom/mgcp_client/mgcp_client_internal.h +++ b/include/osmocom/mgcp_client/mgcp_client_internal.h @@ -38,8 +38,6 @@ struct mgcp_response { uint16_t audio_port; char audio_ip[INET6_ADDRSTRLEN]; unsigned int ptime; - enum mgcp_codecs codecs[MGCP_MAX_CODECS]; - unsigned int codecs_len; struct ptmap ptmap[MGCP_MAX_CODECS]; unsigned int ptmap_len; }; @@ -84,8 +82,6 @@ struct mgcp_msg { char *audio_ip; enum mgcp_connection_mode conn_mode; unsigned int ptime; - enum mgcp_codecs codecs[MGCP_MAX_CODECS]; - unsigned int codecs_len; struct ptmap ptmap[MGCP_MAX_CODECS]; unsigned int ptmap_len; uint32_t x_osmo_ign; diff --git a/src/libosmo-mgcp-client/mgcp_client.c b/src/libosmo-mgcp-client/mgcp_client.c index cd7339105..60c54a65b 100644 --- a/src/libosmo-mgcp-client/mgcp_client.c +++ b/src/libosmo-mgcp-client/mgcp_client.c @@ -302,7 +302,7 @@ static int mgcp_parse_audio_port_pt(struct mgcp_response *r, char *line) char *pt_str; char *pt_end; unsigned long int pt; - unsigned int count = 0; + unsigned int ptmap_len; unsigned int i; /* Extract port information */ @@ -316,10 +316,15 @@ static int mgcp_parse_audio_port_pt(struct mgcp_response *r, char *line) if (!line) goto exit; + /* Clear any previous entries before writing over r->ptmap */ + r->ptmap_len = 0; + /* Keep a local ptmap_len to show only the full list after parsing succeeded in whole. */ + ptmap_len = 0; + pt_str = strtok(line, " "); while (1) { /* Do not allow excessive payload types */ - if (count >= ARRAY_SIZE(r->codecs)) + if (ptmap_len >= ARRAY_SIZE(r->ptmap)) goto response_parse_failure_pt; pt_str = strtok(NULL, " "); @@ -335,21 +340,23 @@ static int mgcp_parse_audio_port_pt(struct mgcp_response *r, char *line) goto response_parse_failure_pt; /* Do not allow duplicate payload types */ - for (i = 0; i < count; i++) - if (r->codecs[i] == pt) + for (i = 0; i < ptmap_len; i++) + if (r->ptmap[i].pt == pt) goto response_parse_failure_pt; - /* Note: The payload type we store may not necessarly match - * the codec types we have defined in enum mgcp_codecs. To - * ensure that the end result only contains codec types which - * match enum mgcp_codecs, we will go through afterwards and - * remap the affected entries with the inrofmation we learn - * from rtpmap */ - r->codecs[count] = pt; - count++; + /* Some payload type numbers imply a specific codec. For those, using the PT number as enum mgcp_codecs + * yields the correct result. If no more specific information on the codec follows in "a=rtpmap:N" + * lines, then this default number takes over. This only applies for PT below the dynamic range (<96). */ + if (pt < 96) + r->ptmap[ptmap_len].codec = pt; + else + r->ptmap[ptmap_len].codec = -1; + r->ptmap[ptmap_len].pt = pt; + ptmap_len++; } - r->codecs_len = count; + /* Parsing succeeded, publish all entries. */ + r->ptmap_len = ptmap_len; exit: return 0; @@ -365,10 +372,11 @@ response_parse_failure_pt: return -EINVAL; } -/* Parse a line like "m=audio 16002 RTP/AVP 98", extract port and payload types */ +/* Parse an 'a=...' parameter */ static int mgcp_parse_audio_ptime_rtpmap(struct mgcp_response *r, const char *line) { unsigned int pt; + unsigned int i; char codec_resp[64]; int rc; @@ -387,18 +395,39 @@ static int mgcp_parse_audio_ptime_rtpmap(struct mgcp_response *r, const char *li "Failed to parse SDP parameter, invalid rtpmap: %s\n", osmo_quote_str(line, -1)); return -EINVAL; } - if (r->ptmap_len >= ARRAY_SIZE(r->ptmap)) { - LOGP(DLMGCP, LOGL_ERROR, "No more space in ptmap array (len=%u)\n", r->ptmap_len); - return -ENOSPC; - } rc = map_str_to_codec(codec_resp); if (rc < 0) { LOGP(DLMGCP, LOGL_ERROR, "Failed to parse SDP parameter, can't parse codec in rtpmap: %s\n", osmo_quote_str(line, -1)); return -EINVAL; } - r->ptmap[r->ptmap_len].pt = pt; - r->ptmap[r->ptmap_len].codec = rc; + + /* Earlier, a line like "m=audio 16002 RTP/AVP 98 112 3" established the desired order of payloads, now + * enrich it with actual codec information provided by "a=rtpmap:..." entries. + * For each, find the entry with the right pt number and add the info there. */ + + for (i = 0; i < r->ptmap_len; i++) { + if (r->ptmap[i].pt != pt) + continue; + r->ptmap[i].codec = rc; + return 0; + } + + /* No entry was found. This is an error in the MGCP protocol, but let's just add another entry + * anyway, to not make it look like it was never there. */ + LOGP(DLMGCP, LOGL_ERROR, + "error in MGCP message: 'a=rtpmap:%u' has no matching entry in 'm=audio ... %u'\n", + pt, pt); + if (r->ptmap_len >= ARRAY_SIZE(r->ptmap)) { + LOGP(DLMGCP, LOGL_ERROR, + "cannot parse all codecs: can only store up to %zu rtpmap entries.\n", + ARRAY_SIZE(r->ptmap)); + return -ENOSPC; + } + r->ptmap[r->ptmap_len] = (struct ptmap){ + .pt = pt, + .codec = rc, + }; r->ptmap_len++; } @@ -508,7 +537,6 @@ int mgcp_response_parse_params(struct mgcp_response *r) int rc; char *data; char *data_ptr; - int i; /* Since this functions performs a destructive parsing, we create a * local copy of the body data */ @@ -553,10 +581,6 @@ int mgcp_response_parse_params(struct mgcp_response *r) } } - /* See also note in mgcp_parse_audio_port_pt() */ - for (i = 0; i < r->codecs_len; i++) - r->codecs[i] = map_pt_to_codec(r->ptmap, r->ptmap_len, r->codecs[i]); - rc = 0; exit: talloc_free(data); @@ -1234,7 +1258,6 @@ static int add_lco(struct msgb *msg, struct mgcp_msg *mgcp_msg) { unsigned int i; const char *codec; - unsigned int pt; #define MSGB_PRINTF_OR_RET(FMT, ARGS...) do { \ if (msgb_printf(msg, FMT, ##ARGS) != 0) { \ @@ -1248,11 +1271,10 @@ static int add_lco(struct msgb *msg, struct mgcp_msg *mgcp_msg) if (mgcp_msg->ptime) MSGB_PRINTF_OR_RET(" p:%u,", mgcp_msg->ptime); - if (mgcp_msg->codecs_len) { + if (mgcp_msg->ptmap_len) { MSGB_PRINTF_OR_RET(" a:"); - for (i = 0; i < mgcp_msg->codecs_len; i++) { - pt = mgcp_msg->codecs[i]; - codec = get_value_string_or_null(osmo_mgcpc_codec_names, pt); + for (i = 0; i < mgcp_msg->ptmap_len; i++) { + codec = get_value_string_or_null(osmo_mgcpc_codec_names, mgcp_msg->ptmap[i].codec); /* Note: Use codec descriptors from enum mgcp_codecs * in mgcp_client only! */ @@ -1260,7 +1282,7 @@ static int add_lco(struct msgb *msg, struct mgcp_msg *mgcp_msg) return -EINVAL; MSGB_PRINTF_OR_RET("%s", extract_codec_name(codec)); - if (i < mgcp_msg->codecs_len - 1) + if (i < mgcp_msg->ptmap_len - 1) MSGB_PRINTF_OR_RET(";"); } MSGB_PRINTF_OR_RET(","); @@ -1338,21 +1360,19 @@ static int add_sdp(struct msgb *msg, struct mgcp_msg *mgcp_msg, struct mgcp_clie return -EINVAL; } MSGB_PRINTF_OR_RET("m=audio %u RTP/AVP", mgcp_msg->audio_port); - for (i = 0; i < mgcp_msg->codecs_len; i++) { - pt = map_codec_to_pt(mgcp_msg->ptmap, mgcp_msg->ptmap_len, mgcp_msg->codecs[i]); - MSGB_PRINTF_OR_RET(" %u", pt); - - } + for (i = 0; i < mgcp_msg->ptmap_len; i++) + MSGB_PRINTF_OR_RET(" %u", mgcp_msg->ptmap[i].pt); MSGB_PRINTF_OR_RET("\r\n"); } /* Add optional codec parameters (fmtp) */ if (mgcp_msg->param_present) { - for (i = 0; i < mgcp_msg->codecs_len; i++) { + for (i = 0; i < mgcp_msg->ptmap_len; i++) { /* The following is only applicable for AMR */ - if (mgcp_msg->codecs[i] != CODEC_AMR_8000_1 && mgcp_msg->codecs[i] != CODEC_AMRWB_16000_1) + if (mgcp_msg->ptmap[i].codec != CODEC_AMR_8000_1 + && mgcp_msg->ptmap[i].codec != CODEC_AMRWB_16000_1) continue; - pt = map_codec_to_pt(mgcp_msg->ptmap, mgcp_msg->ptmap_len, mgcp_msg->codecs[i]); + pt = mgcp_msg->ptmap[i].pt; if (mgcp_msg->param.amr_octet_aligned_present && mgcp_msg->param.amr_octet_aligned) MSGB_PRINTF_OR_RET("a=fmtp:%u octet-align=1\r\n", pt); else if (mgcp_msg->param.amr_octet_aligned_present && !mgcp_msg->param.amr_octet_aligned) @@ -1360,14 +1380,14 @@ static int add_sdp(struct msgb *msg, struct mgcp_msg *mgcp_msg, struct mgcp_clie } } - for (i = 0; i < mgcp_msg->codecs_len; i++) { - pt = map_codec_to_pt(mgcp_msg->ptmap, mgcp_msg->ptmap_len, mgcp_msg->codecs[i]); + for (i = 0; i < mgcp_msg->ptmap_len; i++) { + pt = mgcp_msg->ptmap[i].pt; /* Note: Only dynamic payload type from the range 96-127 * require to be explained further via rtpmap. All others * are implcitly definedby the number in m=audio */ if (pt >= 96 && pt <= 127) { - codec = get_value_string_or_null(osmo_mgcpc_codec_names, mgcp_msg->codecs[i]); + codec = get_value_string_or_null(osmo_mgcpc_codec_names, mgcp_msg->ptmap[i].codec); /* Note: Use codec descriptors from enum mgcp_codecs * in mgcp_client only! */ @@ -1575,3 +1595,20 @@ const char *mgcp_client_name(const struct mgcp_client *mgcp) else return mgcp_client_endpoint_domain(mgcp); } + +/*! Return typical cmp result, comparing a to b. + * Return 0 if a == b, -1 if a < b, 1 if a > b; comparing all members of ptmap in turn. */ +int ptmap_cmp(const struct ptmap *a, const struct ptmap *b) +{ + int rc; + if (a == b) + return 0; + if (!a) + return -1; + if (!b) + return 1; + rc = OSMO_CMP(a->codec, b->codec); + if (rc) + return rc; + return OSMO_CMP(a->pt, b->pt); +} diff --git a/src/libosmo-mgcp-client/mgcp_client_fsm.c b/src/libosmo-mgcp-client/mgcp_client_fsm.c index 43f0f50f7..e638d1e64 100644 --- a/src/libosmo-mgcp-client/mgcp_client_fsm.c +++ b/src/libosmo-mgcp-client/mgcp_client_fsm.c @@ -115,12 +115,10 @@ static void make_crcx_msg(struct mgcp_msg *mgcp_msg, struct mgcp_conn_peer *info .call_id = info->call_id, .conn_mode = MGCP_CONN_RECV_ONLY, .ptime = info->ptime, - .codecs_len = info->codecs_len, .ptmap_len = info->ptmap_len, .param_present = info->param_present }; osmo_strlcpy(mgcp_msg->endpoint, info->endpoint, MGCP_ENDPOINT_MAXLEN); - memcpy(mgcp_msg->codecs, info->codecs, sizeof(mgcp_msg->codecs)); memcpy(mgcp_msg->ptmap, info->ptmap, sizeof(mgcp_msg->ptmap)); memcpy(&mgcp_msg->param, &info->param, sizeof(mgcp_msg->param)); @@ -173,12 +171,10 @@ static struct msgb *make_mdcx_msg(struct mgcp_ctx *mgcp_ctx) .audio_ip = mgcp_ctx->conn_peer_local.addr, .audio_port = mgcp_ctx->conn_peer_local.port, .ptime = mgcp_ctx->conn_peer_local.ptime, - .codecs_len = mgcp_ctx->conn_peer_local.codecs_len, .ptmap_len = mgcp_ctx->conn_peer_local.ptmap_len, .param_present = mgcp_ctx->conn_peer_local.param_present }; osmo_strlcpy(mgcp_msg.endpoint, mgcp_ctx->conn_peer_remote.endpoint, MGCP_ENDPOINT_MAXLEN); - memcpy(mgcp_msg.codecs, mgcp_ctx->conn_peer_local.codecs, sizeof(mgcp_msg.codecs)); memcpy(mgcp_msg.ptmap, mgcp_ctx->conn_peer_local.ptmap, sizeof(mgcp_msg.ptmap)); memcpy(&mgcp_msg.param, &mgcp_ctx->conn_peer_local.param, sizeof(mgcp_ctx->conn_peer_local.param)); @@ -628,6 +624,72 @@ static struct osmo_fsm fsm_mgcp_client = { .log_subsys = DLMGCP, }; +/* Provide backwards compat for deprecated conn_peer->codecs[]: when the caller passes in an mgcp_conn_peer instance + * that has codecs[] set, apply it to ptmap[] instead. */ +static void mgcp_conn_peer_compat(struct mgcp_conn_peer *conn_peer) +{ + struct ptmap ptmap[MGCP_MAX_CODECS]; + unsigned int ptmap_len; + + if (!conn_peer->codecs_len) + return; + + /* Before dropping codecs[], codecs[] would indicate the order in which the codecs should appear in SDP. ptmap[] + * would indicate payload type numbers when not using a default payload type number (may omit entries). + * Now, ptmap[] just indicates both at the same time; codecs[] should be empty, and ptmap[] lists all codecs. + * So if any codecs[] are present, recreate ptmap[] in the order of codecs[]. */ + + ptmap_len = 0; + for (int i = 0; i < conn_peer->codecs_len; i++) { + enum mgcp_codecs codec = conn_peer->codecs[i]; + struct ptmap *found = NULL; + + /* Look up whether a specific pt was indicated for this codec */ + for (int p = 0; p < conn_peer->ptmap_len; p++) { + if (conn_peer->ptmap[p].codec != codec) + continue; + found = &conn_peer->ptmap[p]; + break; + } + if (found) { + ptmap[ptmap_len] = *found; + } else { + ptmap[ptmap_len] = (struct ptmap){ + .codec = codec, + /* some enum mgcp_codecs correspond to their standard PT nr, so for compat: */ + .pt = codec, + }; + } + ptmap_len++; + } + + /* Are there any entries in the old ptmap that were omitted by codecs[]? */ + for (int p = 0; p < conn_peer->ptmap_len; p++) { + bool exists = false; + for (int i = 0; i < ptmap_len; i++) { + if (ptmap_cmp(&ptmap[i], &conn_peer->ptmap[p])) + continue; + exists = true; + break; + } + + if (exists) + continue; + + if (ptmap_len >= ARRAY_SIZE(ptmap)) + break; + + /* Not present yet, add it to the end */ + ptmap[ptmap_len] = conn_peer->ptmap[p]; + ptmap_len++; + } + + /* Use the new ptmap[], and clear out legacy codecs[]. */ + memcpy(conn_peer->ptmap, ptmap, sizeof(conn_peer->ptmap)); + conn_peer->ptmap_len = ptmap_len; + conn_peer->codecs_len = 0; +} + /*! allocate FSM, and create a new connection on the MGW. * \param[in] mgcp MGCP client descriptor. * \param[in] parent_fi Parent FSM instance. @@ -642,6 +704,7 @@ struct osmo_fsm_inst *mgcp_conn_create(struct mgcp_client *mgcp, struct osmo_fsm struct osmo_fsm_inst *fi; struct in6_addr ip_test; + mgcp_conn_peer_compat(conn_peer); OSMO_ASSERT(parent_fi); OSMO_ASSERT(mgcp); @@ -681,6 +744,8 @@ int mgcp_conn_modify(struct osmo_fsm_inst *fi, uint32_t parent_evt, struct mgcp_ struct mgcp_ctx *mgcp_ctx = fi->priv; struct in6_addr ip_test; + mgcp_conn_peer_compat(conn_peer); + OSMO_ASSERT(mgcp_ctx); OSMO_ASSERT(conn_peer); diff --git a/tests/mgcp_client/mgcp_client_test.c b/tests/mgcp_client/mgcp_client_test.c index a39f19bb4..3883c3a2b 100644 --- a/tests/mgcp_client/mgcp_client_test.c +++ b/tests/mgcp_client/mgcp_client_test.c @@ -107,9 +107,6 @@ void test_response_cb(struct mgcp_response *response, void *priv) printf(" audio_port = %u\n", response->audio_port); printf(" audio_ip = %s\n", response->audio_ip); printf(" ptime = %u\n", response->ptime); - printf(" codecs_len = %u\n", response->codecs_len); - for(i=0;icodecs_len;i++) - printf(" codecs[%u] = %u\n", i, response->codecs[i]); printf(" ptmap_len = %u\n", response->ptmap_len); for(i=0;iptmap_len;i++) { printf(" ptmap[%u].codec = %u\n", i, response->ptmap[i].codec); @@ -149,12 +146,11 @@ void test_mgcp_msg(void) .conn_id = "11", .conn_mode = MGCP_CONN_RECV_SEND, .ptime = 20, - .codecs[0] = CODEC_GSM_8000_1, - .codecs[1] = CODEC_AMR_8000_1, - .codecs[2] = CODEC_GSMEFR_8000_1, - .codecs_len = 1, - .ptmap[0].codec = CODEC_GSMEFR_8000_1, - .ptmap[0].pt = 96, + .ptmap = { + { .codec = CODEC_GSM_8000_1, .pt = CODEC_GSM_8000_1 }, + { .codec = CODEC_AMR_8000_1, .pt = CODEC_AMR_8000_1 }, + { .codec = CODEC_GSMEFR_8000_1, .pt = 96 }, + }, .ptmap_len = 1, .x_osmo_ign = MGCP_X_OSMO_IGN_CALLID, .x_osmo_osmux_cid = -1, /* wildcard */ @@ -179,9 +175,9 @@ void test_mgcp_msg(void) mgcp_msg.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE); - mgcp_msg.codecs_len = 2; + mgcp_msg.ptmap_len = 2; msg = mgcp_msg_gen(mgcp, &mgcp_msg); - mgcp_msg.codecs_len = 1; + mgcp_msg.ptmap_len = 1; printf("%s\n", (char *)msg->data); printf("Generated CRCX message (three codecs, one with custom pt):\n"); @@ -189,9 +185,9 @@ void test_mgcp_msg(void) mgcp_msg.presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE); - mgcp_msg.codecs_len = 3; + mgcp_msg.ptmap_len = 3; msg = mgcp_msg_gen(mgcp, &mgcp_msg); - mgcp_msg.codecs_len = 1; + mgcp_msg.ptmap_len = 1; printf("%s\n", (char *)msg->data); printf("Generated MDCX message:\n"); @@ -209,9 +205,9 @@ void test_mgcp_msg(void) (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE | MGCP_MSG_PRESENCE_AUDIO_IP | MGCP_MSG_PRESENCE_AUDIO_PORT); - mgcp_msg.codecs_len = 2; + mgcp_msg.ptmap_len = 2; msg = mgcp_msg_gen(mgcp, &mgcp_msg); - mgcp_msg.codecs_len = 1; + mgcp_msg.ptmap_len = 1; printf("%s\n", (char *)msg->data); printf("Generated MDCX message (three codecs, one with custom pt):\n"); @@ -220,9 +216,9 @@ void test_mgcp_msg(void) (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE | MGCP_MSG_PRESENCE_AUDIO_IP | MGCP_MSG_PRESENCE_AUDIO_PORT); - mgcp_msg.codecs_len = 3; + mgcp_msg.ptmap_len = 3; msg = mgcp_msg_gen(mgcp, &mgcp_msg); - mgcp_msg.codecs_len = 1; + mgcp_msg.ptmap_len = 1; printf("%s\n", (char *)msg->data); printf("Generated DLCX message:\n"); @@ -330,8 +326,10 @@ void test_mgcp_client_cancel(void) .presence = (MGCP_MSG_PRESENCE_ENDPOINT | MGCP_MSG_PRESENCE_CALL_ID | MGCP_MSG_PRESENCE_CONN_ID | MGCP_MSG_PRESENCE_CONN_MODE), .ptime = 20, - .codecs[0] = CODEC_AMR_8000_1, - .codecs_len = 1 + .ptmap = { + { .codec = CODEC_AMR_8000_1, .pt = CODEC_AMR_8000_1 }, + }, + .ptmap_len = 1 }; printf("\n%s():\n", __func__);