mirror of
https://github.com/curl/curl.git
synced 2024-11-27 05:50:21 +08:00
multi: split multi_runsingle into sub functions
Introduce five functions named after the state they serve: - state_connect for MSTATE_CONNECT - state_do for MSTATE_DO - state_performing for MSTATE_PERFORMING - state_ratelimiting for MSTATE_RATELIMITING - state_resolving for MSTATE_RESOLVING Closes #15418
This commit is contained in:
parent
522c89a134
commit
e77326403d
917
lib/multi.c
917
lib/multi.c
@ -2116,19 +2116,490 @@ static CURLcode multi_follow(struct Curl_easy *data,
|
|||||||
#endif /* CURL_DISABLE_HTTP */
|
#endif /* CURL_DISABLE_HTTP */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static CURLMcode state_performing(struct Curl_easy *data,
|
||||||
|
struct curltime *nowp,
|
||||||
|
bool *stream_errorp,
|
||||||
|
CURLcode *resultp)
|
||||||
|
{
|
||||||
|
char *newurl = NULL;
|
||||||
|
bool retry = FALSE;
|
||||||
|
timediff_t recv_timeout_ms = 0;
|
||||||
|
timediff_t send_timeout_ms = 0;
|
||||||
|
CURLMcode rc = CURLM_OK;
|
||||||
|
CURLcode result = *resultp = CURLE_OK;
|
||||||
|
*stream_errorp = FALSE;
|
||||||
|
|
||||||
|
/* check if over send speed */
|
||||||
|
if(data->set.max_send_speed)
|
||||||
|
send_timeout_ms = Curl_pgrsLimitWaitTime(&data->progress.ul,
|
||||||
|
data->set.max_send_speed,
|
||||||
|
*nowp);
|
||||||
|
|
||||||
|
/* check if over recv speed */
|
||||||
|
if(data->set.max_recv_speed)
|
||||||
|
recv_timeout_ms = Curl_pgrsLimitWaitTime(&data->progress.dl,
|
||||||
|
data->set.max_recv_speed,
|
||||||
|
*nowp);
|
||||||
|
|
||||||
|
if(send_timeout_ms || recv_timeout_ms) {
|
||||||
|
Curl_ratelimit(data, *nowp);
|
||||||
|
multistate(data, MSTATE_RATELIMITING);
|
||||||
|
if(send_timeout_ms >= recv_timeout_ms)
|
||||||
|
Curl_expire(data, send_timeout_ms, EXPIRE_TOOFAST);
|
||||||
|
else
|
||||||
|
Curl_expire(data, recv_timeout_ms, EXPIRE_TOOFAST);
|
||||||
|
return CURLM_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read/write data if it is ready to do so */
|
||||||
|
result = Curl_sendrecv(data, nowp);
|
||||||
|
|
||||||
|
if(data->req.done || (result == CURLE_RECV_ERROR)) {
|
||||||
|
/* If CURLE_RECV_ERROR happens early enough, we assume it was a race
|
||||||
|
* condition and the server closed the reused connection exactly when we
|
||||||
|
* wanted to use it, so figure out if that is indeed the case.
|
||||||
|
*/
|
||||||
|
CURLcode ret = Curl_retry_request(data, &newurl);
|
||||||
|
if(!ret)
|
||||||
|
retry = (newurl) ? TRUE : FALSE;
|
||||||
|
else if(!result)
|
||||||
|
result = ret;
|
||||||
|
|
||||||
|
if(retry) {
|
||||||
|
/* if we are to retry, set the result to OK and consider the
|
||||||
|
request as done */
|
||||||
|
result = CURLE_OK;
|
||||||
|
data->req.done = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if((CURLE_HTTP2_STREAM == result) &&
|
||||||
|
Curl_h2_http_1_1_error(data)) {
|
||||||
|
CURLcode ret = Curl_retry_request(data, &newurl);
|
||||||
|
|
||||||
|
if(!ret) {
|
||||||
|
infof(data, "Downgrades to HTTP/1.1");
|
||||||
|
streamclose(data->conn, "Disconnect HTTP/2 for HTTP/1");
|
||||||
|
data->state.httpwant = CURL_HTTP_VERSION_1_1;
|
||||||
|
/* clear the error message bit too as we ignore the one we got */
|
||||||
|
data->state.errorbuf = FALSE;
|
||||||
|
if(!newurl)
|
||||||
|
/* typically for HTTP_1_1_REQUIRED error on first flight */
|
||||||
|
newurl = strdup(data->state.url);
|
||||||
|
/* if we are to retry, set the result to OK and consider the request
|
||||||
|
as done */
|
||||||
|
retry = TRUE;
|
||||||
|
result = CURLE_OK;
|
||||||
|
data->req.done = TRUE;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
result = ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result) {
|
||||||
|
/*
|
||||||
|
* The transfer phase returned error, we mark the connection to get closed
|
||||||
|
* to prevent being reused. This is because we cannot possibly know if the
|
||||||
|
* connection is in a good shape or not now. Unless it is a protocol which
|
||||||
|
* uses two "channels" like FTP, as then the error happened in the data
|
||||||
|
* connection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if(!(data->conn->handler->flags & PROTOPT_DUAL) &&
|
||||||
|
result != CURLE_HTTP2_STREAM)
|
||||||
|
streamclose(data->conn, "Transfer returned error");
|
||||||
|
|
||||||
|
multi_posttransfer(data);
|
||||||
|
multi_done(data, result, TRUE);
|
||||||
|
}
|
||||||
|
else if(data->req.done && !Curl_cwriter_is_paused(data)) {
|
||||||
|
|
||||||
|
/* call this even if the readwrite function returned error */
|
||||||
|
multi_posttransfer(data);
|
||||||
|
|
||||||
|
/* When we follow redirects or is set to retry the connection, we must to
|
||||||
|
go back to the CONNECT state */
|
||||||
|
if(data->req.newurl || retry) {
|
||||||
|
followtype follow = FOLLOW_NONE;
|
||||||
|
if(!retry) {
|
||||||
|
/* if the URL is a follow-location and not just a retried request then
|
||||||
|
figure out the URL here */
|
||||||
|
free(newurl);
|
||||||
|
newurl = data->req.newurl;
|
||||||
|
data->req.newurl = NULL;
|
||||||
|
follow = FOLLOW_REDIR;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
follow = FOLLOW_RETRY;
|
||||||
|
(void)multi_done(data, CURLE_OK, FALSE);
|
||||||
|
/* multi_done() might return CURLE_GOT_NOTHING */
|
||||||
|
result = multi_follow(data, newurl, follow);
|
||||||
|
if(!result) {
|
||||||
|
multistate(data, MSTATE_SETUP);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* after the transfer is done, go DONE */
|
||||||
|
|
||||||
|
/* but first check to see if we got a location info even though we are
|
||||||
|
not following redirects */
|
||||||
|
if(data->req.location) {
|
||||||
|
free(newurl);
|
||||||
|
newurl = data->req.location;
|
||||||
|
data->req.location = NULL;
|
||||||
|
result = multi_follow(data, newurl, FOLLOW_FAKE);
|
||||||
|
if(result) {
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
result = multi_done(data, result, TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!result) {
|
||||||
|
multistate(data, MSTATE_DONE);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(data->state.select_bits && !Curl_xfer_is_blocked(data)) {
|
||||||
|
/* This avoids CURLM_CALL_MULTI_PERFORM so that a very fast transfer does
|
||||||
|
not get stuck on this transfer at the expense of other concurrent
|
||||||
|
transfers */
|
||||||
|
Curl_expire(data, 0, EXPIRE_RUN_NOW);
|
||||||
|
}
|
||||||
|
free(newurl);
|
||||||
|
*resultp = result;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURLMcode state_do(struct Curl_easy *data,
|
||||||
|
bool *stream_errorp,
|
||||||
|
CURLcode *resultp)
|
||||||
|
{
|
||||||
|
CURLMcode rc = CURLM_OK;
|
||||||
|
CURLcode result = CURLE_OK;
|
||||||
|
if(data->set.fprereq) {
|
||||||
|
int prereq_rc;
|
||||||
|
|
||||||
|
/* call the prerequest callback function */
|
||||||
|
Curl_set_in_callback(data, TRUE);
|
||||||
|
prereq_rc = data->set.fprereq(data->set.prereq_userp,
|
||||||
|
data->info.primary.remote_ip,
|
||||||
|
data->info.primary.local_ip,
|
||||||
|
data->info.primary.remote_port,
|
||||||
|
data->info.primary.local_port);
|
||||||
|
Curl_set_in_callback(data, FALSE);
|
||||||
|
if(prereq_rc != CURL_PREREQFUNC_OK) {
|
||||||
|
failf(data, "operation aborted by pre-request callback");
|
||||||
|
/* failure in pre-request callback - do not do any other processing */
|
||||||
|
result = CURLE_ABORTED_BY_CALLBACK;
|
||||||
|
multi_posttransfer(data);
|
||||||
|
multi_done(data, result, FALSE);
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data->set.connect_only == 1) {
|
||||||
|
/* keep connection open for application to use the socket */
|
||||||
|
connkeep(data->conn, "CONNECT_ONLY");
|
||||||
|
multistate(data, MSTATE_DONE);
|
||||||
|
result = CURLE_OK;
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bool dophase_done = FALSE;
|
||||||
|
/* Perform the protocol's DO action */
|
||||||
|
result = multi_do(data, &dophase_done);
|
||||||
|
|
||||||
|
/* When multi_do() returns failure, data->conn might be NULL! */
|
||||||
|
|
||||||
|
if(!result) {
|
||||||
|
if(!dophase_done) {
|
||||||
|
#ifndef CURL_DISABLE_FTP
|
||||||
|
/* some steps needed for wildcard matching */
|
||||||
|
if(data->state.wildcardmatch) {
|
||||||
|
struct WildcardData *wc = data->wildcard;
|
||||||
|
if(wc->state == CURLWC_DONE || wc->state == CURLWC_SKIP) {
|
||||||
|
/* skip some states if it is important */
|
||||||
|
multi_done(data, CURLE_OK, FALSE);
|
||||||
|
|
||||||
|
/* if there is no connection left, skip the DONE state */
|
||||||
|
multistate(data, data->conn ?
|
||||||
|
MSTATE_DONE : MSTATE_COMPLETED);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
goto end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
/* DO was not completed in one function call, we must continue
|
||||||
|
DOING... */
|
||||||
|
multistate(data, MSTATE_DOING);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* after DO, go DO_DONE... or DO_MORE */
|
||||||
|
else if(data->conn->bits.do_more) {
|
||||||
|
/* we are supposed to do more, but we need to sit down, relax and wait
|
||||||
|
a little while first */
|
||||||
|
multistate(data, MSTATE_DOING_MORE);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* we are done with the DO, now DID */
|
||||||
|
multistate(data, MSTATE_DID);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if((CURLE_SEND_ERROR == result) &&
|
||||||
|
data->conn->bits.reuse) {
|
||||||
|
/*
|
||||||
|
* In this situation, a connection that we were trying to use may have
|
||||||
|
* unexpectedly died. If possible, send the connection back to the
|
||||||
|
* CONNECT phase so we can try again.
|
||||||
|
*/
|
||||||
|
char *newurl = NULL;
|
||||||
|
followtype follow = FOLLOW_NONE;
|
||||||
|
CURLcode drc;
|
||||||
|
|
||||||
|
drc = Curl_retry_request(data, &newurl);
|
||||||
|
if(drc) {
|
||||||
|
/* a failure here pretty much implies an out of memory */
|
||||||
|
result = drc;
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
multi_posttransfer(data);
|
||||||
|
drc = multi_done(data, result, FALSE);
|
||||||
|
|
||||||
|
/* When set to retry the connection, we must go back to the CONNECT
|
||||||
|
* state */
|
||||||
|
if(newurl) {
|
||||||
|
if(!drc || (drc == CURLE_SEND_ERROR)) {
|
||||||
|
follow = FOLLOW_RETRY;
|
||||||
|
drc = multi_follow(data, newurl, follow);
|
||||||
|
if(!drc) {
|
||||||
|
multistate(data, MSTATE_SETUP);
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
result = CURLE_OK;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Follow failed */
|
||||||
|
result = drc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* done did not return OK or SEND_ERROR */
|
||||||
|
result = drc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* Have error handler disconnect conn if we cannot retry */
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
}
|
||||||
|
free(newurl);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
/* failure detected */
|
||||||
|
multi_posttransfer(data);
|
||||||
|
if(data->conn)
|
||||||
|
multi_done(data, result, FALSE);
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end:
|
||||||
|
*resultp = result;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURLMcode state_ratelimiting(struct Curl_easy *data,
|
||||||
|
struct curltime *nowp,
|
||||||
|
CURLcode *resultp)
|
||||||
|
{
|
||||||
|
CURLcode result = CURLE_OK;
|
||||||
|
CURLMcode rc = CURLM_OK;
|
||||||
|
DEBUGASSERT(data->conn);
|
||||||
|
/* if both rates are within spec, resume transfer */
|
||||||
|
if(Curl_pgrsUpdate(data))
|
||||||
|
result = CURLE_ABORTED_BY_CALLBACK;
|
||||||
|
else
|
||||||
|
result = Curl_speedcheck(data, *nowp);
|
||||||
|
|
||||||
|
if(result) {
|
||||||
|
if(!(data->conn->handler->flags & PROTOPT_DUAL) &&
|
||||||
|
result != CURLE_HTTP2_STREAM)
|
||||||
|
streamclose(data->conn, "Transfer returned error");
|
||||||
|
|
||||||
|
multi_posttransfer(data);
|
||||||
|
multi_done(data, result, TRUE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
timediff_t recv_timeout_ms = 0;
|
||||||
|
timediff_t send_timeout_ms = 0;
|
||||||
|
if(data->set.max_send_speed)
|
||||||
|
send_timeout_ms =
|
||||||
|
Curl_pgrsLimitWaitTime(&data->progress.ul,
|
||||||
|
data->set.max_send_speed,
|
||||||
|
*nowp);
|
||||||
|
|
||||||
|
if(data->set.max_recv_speed)
|
||||||
|
recv_timeout_ms =
|
||||||
|
Curl_pgrsLimitWaitTime(&data->progress.dl,
|
||||||
|
data->set.max_recv_speed,
|
||||||
|
*nowp);
|
||||||
|
|
||||||
|
if(!send_timeout_ms && !recv_timeout_ms) {
|
||||||
|
multistate(data, MSTATE_PERFORMING);
|
||||||
|
Curl_ratelimit(data, *nowp);
|
||||||
|
/* start performing again right away */
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
}
|
||||||
|
else if(send_timeout_ms >= recv_timeout_ms)
|
||||||
|
Curl_expire(data, send_timeout_ms, EXPIRE_TOOFAST);
|
||||||
|
else
|
||||||
|
Curl_expire(data, recv_timeout_ms, EXPIRE_TOOFAST);
|
||||||
|
}
|
||||||
|
*resultp = result;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURLMcode state_resolving(struct Curl_multi *multi,
|
||||||
|
struct Curl_easy *data,
|
||||||
|
bool *stream_errorp,
|
||||||
|
CURLcode *resultp)
|
||||||
|
{
|
||||||
|
struct Curl_dns_entry *dns = NULL;
|
||||||
|
struct connectdata *conn = data->conn;
|
||||||
|
const char *hostname;
|
||||||
|
CURLcode result = CURLE_OK;
|
||||||
|
CURLMcode rc = CURLM_OK;
|
||||||
|
|
||||||
|
DEBUGASSERT(conn);
|
||||||
|
#ifndef CURL_DISABLE_PROXY
|
||||||
|
if(conn->bits.httpproxy)
|
||||||
|
hostname = conn->http_proxy.host.name;
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
if(conn->bits.conn_to_host)
|
||||||
|
hostname = conn->conn_to_host.name;
|
||||||
|
else
|
||||||
|
hostname = conn->host.name;
|
||||||
|
|
||||||
|
/* check if we have the name resolved by now */
|
||||||
|
dns = Curl_fetch_addr(data, hostname, conn->primary.remote_port);
|
||||||
|
|
||||||
|
if(dns) {
|
||||||
|
#ifdef CURLRES_ASYNCH
|
||||||
|
data->state.async.dns = dns;
|
||||||
|
data->state.async.done = TRUE;
|
||||||
|
#endif
|
||||||
|
result = CURLE_OK;
|
||||||
|
infof(data, "Hostname '%s' was found in DNS cache", hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!dns)
|
||||||
|
result = Curl_resolv_check(data, &dns);
|
||||||
|
|
||||||
|
/* Update sockets here, because the socket(s) may have been closed and the
|
||||||
|
application thus needs to be told, even if it is likely that the same
|
||||||
|
socket(s) will again be used further down. If the name has not yet been
|
||||||
|
resolved, it is likely that new sockets have been opened in an attempt to
|
||||||
|
contact another resolver. */
|
||||||
|
rc = singlesocket(multi, data);
|
||||||
|
if(rc)
|
||||||
|
return rc;
|
||||||
|
|
||||||
|
if(dns) {
|
||||||
|
bool connected;
|
||||||
|
/* Perform the next step in the connection phase, and then move on to the
|
||||||
|
WAITCONNECT state */
|
||||||
|
result = Curl_once_resolved(data, &connected);
|
||||||
|
|
||||||
|
if(result)
|
||||||
|
/* if Curl_once_resolved() returns failure, the connection struct is
|
||||||
|
already freed and gone */
|
||||||
|
data->conn = NULL; /* no more connection */
|
||||||
|
else {
|
||||||
|
/* call again please so that we get the next socket setup */
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
if(connected)
|
||||||
|
multistate(data, MSTATE_PROTOCONNECT);
|
||||||
|
else {
|
||||||
|
multistate(data, MSTATE_CONNECTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result)
|
||||||
|
/* failure detected */
|
||||||
|
*stream_errorp = TRUE;
|
||||||
|
|
||||||
|
*resultp = result;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CURLMcode state_connect(struct Curl_multi *multi,
|
||||||
|
struct Curl_easy *data,
|
||||||
|
struct curltime *nowp,
|
||||||
|
CURLcode *resultp)
|
||||||
|
{
|
||||||
|
/* Connect. We want to get a connection identifier filled in. This state can
|
||||||
|
be entered from SETUP and from PENDING. */
|
||||||
|
bool connected;
|
||||||
|
bool async;
|
||||||
|
CURLMcode rc = CURLM_OK;
|
||||||
|
CURLcode result = Curl_connect(data, &async, &connected);
|
||||||
|
if(CURLE_NO_CONNECTION_AVAILABLE == result) {
|
||||||
|
/* There was no connection available. We will go to the pending state and
|
||||||
|
wait for an available connection. */
|
||||||
|
multistate(data, MSTATE_PENDING);
|
||||||
|
/* unlink from process list */
|
||||||
|
Curl_node_remove(&data->multi_queue);
|
||||||
|
/* add handle to pending list */
|
||||||
|
Curl_llist_append(&multi->pending, data, &data->multi_queue);
|
||||||
|
*resultp = CURLE_OK;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
process_pending_handles(data->multi);
|
||||||
|
|
||||||
|
if(!result) {
|
||||||
|
*nowp = Curl_pgrsTime(data, TIMER_POSTQUEUE);
|
||||||
|
if(async)
|
||||||
|
/* We are now waiting for an asynchronous name lookup */
|
||||||
|
multistate(data, MSTATE_RESOLVING);
|
||||||
|
else {
|
||||||
|
/* after the connect has been sent off, go WAITCONNECT unless the
|
||||||
|
protocol connect is already done and we can go directly to WAITDO or
|
||||||
|
DO! */
|
||||||
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
|
|
||||||
|
if(connected) {
|
||||||
|
if(!data->conn->bits.reuse &&
|
||||||
|
Curl_conn_is_multiplex(data->conn, FIRSTSOCKET)) {
|
||||||
|
/* new connection, can multiplex, wake pending handles */
|
||||||
|
process_pending_handles(data->multi);
|
||||||
|
}
|
||||||
|
multistate(data, MSTATE_PROTOCONNECT);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
multistate(data, MSTATE_CONNECTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*resultp = result;
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
||||||
struct curltime *nowp,
|
struct curltime *nowp,
|
||||||
struct Curl_easy *data)
|
struct Curl_easy *data)
|
||||||
{
|
{
|
||||||
struct Curl_message *msg = NULL;
|
struct Curl_message *msg = NULL;
|
||||||
bool connected;
|
bool connected;
|
||||||
bool async;
|
|
||||||
bool protocol_connected = FALSE;
|
bool protocol_connected = FALSE;
|
||||||
bool dophase_done = FALSE;
|
bool dophase_done = FALSE;
|
||||||
CURLMcode rc;
|
CURLMcode rc;
|
||||||
CURLcode result = CURLE_OK;
|
CURLcode result = CURLE_OK;
|
||||||
timediff_t recv_timeout_ms;
|
|
||||||
timediff_t send_timeout_ms;
|
|
||||||
int control;
|
int control;
|
||||||
|
|
||||||
if(!GOOD_EASY_HANDLE(data))
|
if(!GOOD_EASY_HANDLE(data))
|
||||||
@ -2173,8 +2644,8 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
|||||||
|
|
||||||
switch(data->mstate) {
|
switch(data->mstate) {
|
||||||
case MSTATE_INIT:
|
case MSTATE_INIT:
|
||||||
/* Transitional state. init this transfer. A handle never comes
|
/* Transitional state. init this transfer. A handle never comes back to
|
||||||
back to this state. */
|
this state. */
|
||||||
result = Curl_pretransfer(data);
|
result = Curl_pretransfer(data);
|
||||||
if(result)
|
if(result)
|
||||||
break;
|
break;
|
||||||
@ -2200,119 +2671,13 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
|||||||
FALLTHROUGH();
|
FALLTHROUGH();
|
||||||
|
|
||||||
case MSTATE_CONNECT:
|
case MSTATE_CONNECT:
|
||||||
/* Connect. We want to get a connection identifier filled in. This state
|
rc = state_connect(multi, data, nowp, &result);
|
||||||
can be entered from SETUP and from PENDING. */
|
|
||||||
result = Curl_connect(data, &async, &connected);
|
|
||||||
if(CURLE_NO_CONNECTION_AVAILABLE == result) {
|
|
||||||
/* There was no connection available. We will go to the pending
|
|
||||||
state and wait for an available connection. */
|
|
||||||
multistate(data, MSTATE_PENDING);
|
|
||||||
/* unlink from process list */
|
|
||||||
Curl_node_remove(&data->multi_queue);
|
|
||||||
/* add handle to pending list */
|
|
||||||
Curl_llist_append(&multi->pending, data, &data->multi_queue);
|
|
||||||
result = CURLE_OK;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
process_pending_handles(data->multi);
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
*nowp = Curl_pgrsTime(data, TIMER_POSTQUEUE);
|
|
||||||
if(async)
|
|
||||||
/* We are now waiting for an asynchronous name lookup */
|
|
||||||
multistate(data, MSTATE_RESOLVING);
|
|
||||||
else {
|
|
||||||
/* after the connect has been sent off, go WAITCONNECT unless the
|
|
||||||
protocol connect is already done and we can go directly to
|
|
||||||
WAITDO or DO! */
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
|
|
||||||
if(connected) {
|
|
||||||
if(!data->conn->bits.reuse &&
|
|
||||||
Curl_conn_is_multiplex(data->conn, FIRSTSOCKET)) {
|
|
||||||
/* new connection, can multiplex, wake pending handles */
|
|
||||||
process_pending_handles(data->multi);
|
|
||||||
}
|
|
||||||
multistate(data, MSTATE_PROTOCONNECT);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
multistate(data, MSTATE_CONNECTING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSTATE_RESOLVING:
|
case MSTATE_RESOLVING:
|
||||||
/* awaiting an asynch name resolve to complete */
|
/* awaiting an asynch name resolve to complete */
|
||||||
{
|
rc = state_resolving(multi, data, &stream_error, &result);
|
||||||
struct Curl_dns_entry *dns = NULL;
|
break;
|
||||||
struct connectdata *conn = data->conn;
|
|
||||||
const char *hostname;
|
|
||||||
|
|
||||||
DEBUGASSERT(conn);
|
|
||||||
#ifndef CURL_DISABLE_PROXY
|
|
||||||
if(conn->bits.httpproxy)
|
|
||||||
hostname = conn->http_proxy.host.name;
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
if(conn->bits.conn_to_host)
|
|
||||||
hostname = conn->conn_to_host.name;
|
|
||||||
else
|
|
||||||
hostname = conn->host.name;
|
|
||||||
|
|
||||||
/* check if we have the name resolved by now */
|
|
||||||
dns = Curl_fetch_addr(data, hostname, conn->primary.remote_port);
|
|
||||||
|
|
||||||
if(dns) {
|
|
||||||
#ifdef CURLRES_ASYNCH
|
|
||||||
data->state.async.dns = dns;
|
|
||||||
data->state.async.done = TRUE;
|
|
||||||
#endif
|
|
||||||
result = CURLE_OK;
|
|
||||||
infof(data, "Hostname '%s' was found in DNS cache", hostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!dns)
|
|
||||||
result = Curl_resolv_check(data, &dns);
|
|
||||||
|
|
||||||
/* Update sockets here, because the socket(s) may have been
|
|
||||||
closed and the application thus needs to be told, even if it
|
|
||||||
is likely that the same socket(s) will again be used further
|
|
||||||
down. If the name has not yet been resolved, it is likely
|
|
||||||
that new sockets have been opened in an attempt to contact
|
|
||||||
another resolver. */
|
|
||||||
rc = singlesocket(multi, data);
|
|
||||||
if(rc)
|
|
||||||
return rc;
|
|
||||||
|
|
||||||
if(dns) {
|
|
||||||
/* Perform the next step in the connection phase, and then move on
|
|
||||||
to the WAITCONNECT state */
|
|
||||||
result = Curl_once_resolved(data, &connected);
|
|
||||||
|
|
||||||
if(result)
|
|
||||||
/* if Curl_once_resolved() returns failure, the connection struct
|
|
||||||
is already freed and gone */
|
|
||||||
data->conn = NULL; /* no more connection */
|
|
||||||
else {
|
|
||||||
/* call again please so that we get the next socket setup */
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
if(connected)
|
|
||||||
multistate(data, MSTATE_PROTOCONNECT);
|
|
||||||
else {
|
|
||||||
multistate(data, MSTATE_CONNECTING);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(result) {
|
|
||||||
/* failure detected */
|
|
||||||
stream_error = TRUE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
#ifndef CURL_DISABLE_HTTP
|
#ifndef CURL_DISABLE_HTTP
|
||||||
case MSTATE_TUNNELING:
|
case MSTATE_TUNNELING:
|
||||||
@ -2353,9 +2718,10 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
|||||||
|
|
||||||
case MSTATE_PROTOCONNECT:
|
case MSTATE_PROTOCONNECT:
|
||||||
if(!result && data->conn->bits.reuse) {
|
if(!result && data->conn->bits.reuse) {
|
||||||
/* ftp seems to hang when protoconnect on reused connection
|
/* ftp seems to hang when protoconnect on reused connection since we
|
||||||
* since we handle PROTOCONNECT in general inside the filers, it
|
* handle PROTOCONNECT in general inside the filers, it seems wrong to
|
||||||
* seems wrong to restart this on a reused connection. */
|
* restart this on a reused connection.
|
||||||
|
*/
|
||||||
multistate(data, MSTATE_DO);
|
multistate(data, MSTATE_DO);
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
rc = CURLM_CALL_MULTI_PERFORM;
|
||||||
break;
|
break;
|
||||||
@ -2397,135 +2763,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MSTATE_DO:
|
case MSTATE_DO:
|
||||||
if(data->set.fprereq) {
|
rc = state_do(data, &stream_error, &result);
|
||||||
int prereq_rc;
|
|
||||||
|
|
||||||
/* call the prerequest callback function */
|
|
||||||
Curl_set_in_callback(data, TRUE);
|
|
||||||
prereq_rc = data->set.fprereq(data->set.prereq_userp,
|
|
||||||
data->info.primary.remote_ip,
|
|
||||||
data->info.primary.local_ip,
|
|
||||||
data->info.primary.remote_port,
|
|
||||||
data->info.primary.local_port);
|
|
||||||
Curl_set_in_callback(data, FALSE);
|
|
||||||
if(prereq_rc != CURL_PREREQFUNC_OK) {
|
|
||||||
failf(data, "operation aborted by pre-request callback");
|
|
||||||
/* failure in pre-request callback - do not do any other
|
|
||||||
processing */
|
|
||||||
result = CURLE_ABORTED_BY_CALLBACK;
|
|
||||||
multi_posttransfer(data);
|
|
||||||
multi_done(data, result, FALSE);
|
|
||||||
stream_error = TRUE;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(data->set.connect_only == 1) {
|
|
||||||
/* keep connection open for application to use the socket */
|
|
||||||
connkeep(data->conn, "CONNECT_ONLY");
|
|
||||||
multistate(data, MSTATE_DONE);
|
|
||||||
result = CURLE_OK;
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* Perform the protocol's DO action */
|
|
||||||
result = multi_do(data, &dophase_done);
|
|
||||||
|
|
||||||
/* When multi_do() returns failure, data->conn might be NULL! */
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
if(!dophase_done) {
|
|
||||||
#ifndef CURL_DISABLE_FTP
|
|
||||||
/* some steps needed for wildcard matching */
|
|
||||||
if(data->state.wildcardmatch) {
|
|
||||||
struct WildcardData *wc = data->wildcard;
|
|
||||||
if(wc->state == CURLWC_DONE || wc->state == CURLWC_SKIP) {
|
|
||||||
/* skip some states if it is important */
|
|
||||||
multi_done(data, CURLE_OK, FALSE);
|
|
||||||
|
|
||||||
/* if there is no connection left, skip the DONE state */
|
|
||||||
multistate(data, data->conn ?
|
|
||||||
MSTATE_DONE : MSTATE_COMPLETED);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
/* DO was not completed in one function call, we must continue
|
|
||||||
DOING... */
|
|
||||||
multistate(data, MSTATE_DOING);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* after DO, go DO_DONE... or DO_MORE */
|
|
||||||
else if(data->conn->bits.do_more) {
|
|
||||||
/* we are supposed to do more, but we need to sit down, relax
|
|
||||||
and wait a little while first */
|
|
||||||
multistate(data, MSTATE_DOING_MORE);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* we are done with the DO, now DID */
|
|
||||||
multistate(data, MSTATE_DID);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if((CURLE_SEND_ERROR == result) &&
|
|
||||||
data->conn->bits.reuse) {
|
|
||||||
/*
|
|
||||||
* In this situation, a connection that we were trying to use
|
|
||||||
* may have unexpectedly died. If possible, send the connection
|
|
||||||
* back to the CONNECT phase so we can try again.
|
|
||||||
*/
|
|
||||||
char *newurl = NULL;
|
|
||||||
followtype follow = FOLLOW_NONE;
|
|
||||||
CURLcode drc;
|
|
||||||
|
|
||||||
drc = Curl_retry_request(data, &newurl);
|
|
||||||
if(drc) {
|
|
||||||
/* a failure here pretty much implies an out of memory */
|
|
||||||
result = drc;
|
|
||||||
stream_error = TRUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
multi_posttransfer(data);
|
|
||||||
drc = multi_done(data, result, FALSE);
|
|
||||||
|
|
||||||
/* When set to retry the connection, we must go back to the CONNECT
|
|
||||||
* state */
|
|
||||||
if(newurl) {
|
|
||||||
if(!drc || (drc == CURLE_SEND_ERROR)) {
|
|
||||||
follow = FOLLOW_RETRY;
|
|
||||||
drc = multi_follow(data, newurl, follow);
|
|
||||||
if(!drc) {
|
|
||||||
multistate(data, MSTATE_SETUP);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
result = CURLE_OK;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* Follow failed */
|
|
||||||
result = drc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* done did not return OK or SEND_ERROR */
|
|
||||||
result = drc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* Have error handler disconnect conn if we cannot retry */
|
|
||||||
stream_error = TRUE;
|
|
||||||
}
|
|
||||||
free(newurl);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* failure detected */
|
|
||||||
multi_posttransfer(data);
|
|
||||||
if(data->conn)
|
|
||||||
multi_done(data, result, FALSE);
|
|
||||||
stream_error = TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSTATE_DOING:
|
case MSTATE_DOING:
|
||||||
@ -2598,195 +2836,12 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MSTATE_RATELIMITING: /* limit-rate exceeded in either direction */
|
case MSTATE_RATELIMITING: /* limit-rate exceeded in either direction */
|
||||||
DEBUGASSERT(data->conn);
|
rc = state_ratelimiting(data, nowp, &result);
|
||||||
/* if both rates are within spec, resume transfer */
|
|
||||||
if(Curl_pgrsUpdate(data))
|
|
||||||
result = CURLE_ABORTED_BY_CALLBACK;
|
|
||||||
else
|
|
||||||
result = Curl_speedcheck(data, *nowp);
|
|
||||||
|
|
||||||
if(result) {
|
|
||||||
if(!(data->conn->handler->flags & PROTOPT_DUAL) &&
|
|
||||||
result != CURLE_HTTP2_STREAM)
|
|
||||||
streamclose(data->conn, "Transfer returned error");
|
|
||||||
|
|
||||||
multi_posttransfer(data);
|
|
||||||
multi_done(data, result, TRUE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
send_timeout_ms = 0;
|
|
||||||
if(data->set.max_send_speed)
|
|
||||||
send_timeout_ms =
|
|
||||||
Curl_pgrsLimitWaitTime(&data->progress.ul,
|
|
||||||
data->set.max_send_speed,
|
|
||||||
*nowp);
|
|
||||||
|
|
||||||
recv_timeout_ms = 0;
|
|
||||||
if(data->set.max_recv_speed)
|
|
||||||
recv_timeout_ms =
|
|
||||||
Curl_pgrsLimitWaitTime(&data->progress.dl,
|
|
||||||
data->set.max_recv_speed,
|
|
||||||
*nowp);
|
|
||||||
|
|
||||||
if(!send_timeout_ms && !recv_timeout_ms) {
|
|
||||||
multistate(data, MSTATE_PERFORMING);
|
|
||||||
Curl_ratelimit(data, *nowp);
|
|
||||||
/* start performing again right away */
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
else if(send_timeout_ms >= recv_timeout_ms)
|
|
||||||
Curl_expire(data, send_timeout_ms, EXPIRE_TOOFAST);
|
|
||||||
else
|
|
||||||
Curl_expire(data, recv_timeout_ms, EXPIRE_TOOFAST);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MSTATE_PERFORMING:
|
case MSTATE_PERFORMING:
|
||||||
{
|
rc = state_performing(data, nowp, &stream_error, &result);
|
||||||
char *newurl = NULL;
|
|
||||||
bool retry = FALSE;
|
|
||||||
/* check if over send speed */
|
|
||||||
send_timeout_ms = 0;
|
|
||||||
if(data->set.max_send_speed)
|
|
||||||
send_timeout_ms = Curl_pgrsLimitWaitTime(&data->progress.ul,
|
|
||||||
data->set.max_send_speed,
|
|
||||||
*nowp);
|
|
||||||
|
|
||||||
/* check if over recv speed */
|
|
||||||
recv_timeout_ms = 0;
|
|
||||||
if(data->set.max_recv_speed)
|
|
||||||
recv_timeout_ms = Curl_pgrsLimitWaitTime(&data->progress.dl,
|
|
||||||
data->set.max_recv_speed,
|
|
||||||
*nowp);
|
|
||||||
|
|
||||||
if(send_timeout_ms || recv_timeout_ms) {
|
|
||||||
Curl_ratelimit(data, *nowp);
|
|
||||||
multistate(data, MSTATE_RATELIMITING);
|
|
||||||
if(send_timeout_ms >= recv_timeout_ms)
|
|
||||||
Curl_expire(data, send_timeout_ms, EXPIRE_TOOFAST);
|
|
||||||
else
|
|
||||||
Curl_expire(data, recv_timeout_ms, EXPIRE_TOOFAST);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* read/write data if it is ready to do so */
|
|
||||||
result = Curl_sendrecv(data, nowp);
|
|
||||||
|
|
||||||
if(data->req.done || (result == CURLE_RECV_ERROR)) {
|
|
||||||
/* If CURLE_RECV_ERROR happens early enough, we assume it was a race
|
|
||||||
* condition and the server closed the reused connection exactly when
|
|
||||||
* we wanted to use it, so figure out if that is indeed the case.
|
|
||||||
*/
|
|
||||||
CURLcode ret = Curl_retry_request(data, &newurl);
|
|
||||||
if(!ret)
|
|
||||||
retry = (newurl) ? TRUE : FALSE;
|
|
||||||
else if(!result)
|
|
||||||
result = ret;
|
|
||||||
|
|
||||||
if(retry) {
|
|
||||||
/* if we are to retry, set the result to OK and consider the
|
|
||||||
request as done */
|
|
||||||
result = CURLE_OK;
|
|
||||||
data->req.done = TRUE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if((CURLE_HTTP2_STREAM == result) &&
|
|
||||||
Curl_h2_http_1_1_error(data)) {
|
|
||||||
CURLcode ret = Curl_retry_request(data, &newurl);
|
|
||||||
|
|
||||||
if(!ret) {
|
|
||||||
infof(data, "Downgrades to HTTP/1.1");
|
|
||||||
streamclose(data->conn, "Disconnect HTTP/2 for HTTP/1");
|
|
||||||
data->state.httpwant = CURL_HTTP_VERSION_1_1;
|
|
||||||
/* clear the error message bit too as we ignore the one we got */
|
|
||||||
data->state.errorbuf = FALSE;
|
|
||||||
if(!newurl)
|
|
||||||
/* typically for HTTP_1_1_REQUIRED error on first flight */
|
|
||||||
newurl = strdup(data->state.url);
|
|
||||||
/* if we are to retry, set the result to OK and consider the request
|
|
||||||
as done */
|
|
||||||
retry = TRUE;
|
|
||||||
result = CURLE_OK;
|
|
||||||
data->req.done = TRUE;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
result = ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(result) {
|
|
||||||
/*
|
|
||||||
* The transfer phase returned error, we mark the connection to get
|
|
||||||
* closed to prevent being reused. This is because we cannot possibly
|
|
||||||
* know if the connection is in a good shape or not now. Unless it is
|
|
||||||
* a protocol which uses two "channels" like FTP, as then the error
|
|
||||||
* happened in the data connection.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if(!(data->conn->handler->flags & PROTOPT_DUAL) &&
|
|
||||||
result != CURLE_HTTP2_STREAM)
|
|
||||||
streamclose(data->conn, "Transfer returned error");
|
|
||||||
|
|
||||||
multi_posttransfer(data);
|
|
||||||
multi_done(data, result, TRUE);
|
|
||||||
}
|
|
||||||
else if(data->req.done && !Curl_cwriter_is_paused(data)) {
|
|
||||||
|
|
||||||
/* call this even if the readwrite function returned error */
|
|
||||||
multi_posttransfer(data);
|
|
||||||
|
|
||||||
/* When we follow redirects or is set to retry the connection, we must
|
|
||||||
to go back to the CONNECT state */
|
|
||||||
if(data->req.newurl || retry) {
|
|
||||||
followtype follow = FOLLOW_NONE;
|
|
||||||
if(!retry) {
|
|
||||||
/* if the URL is a follow-location and not just a retried request
|
|
||||||
then figure out the URL here */
|
|
||||||
free(newurl);
|
|
||||||
newurl = data->req.newurl;
|
|
||||||
data->req.newurl = NULL;
|
|
||||||
follow = FOLLOW_REDIR;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
follow = FOLLOW_RETRY;
|
|
||||||
(void)multi_done(data, CURLE_OK, FALSE);
|
|
||||||
/* multi_done() might return CURLE_GOT_NOTHING */
|
|
||||||
result = multi_follow(data, newurl, follow);
|
|
||||||
if(!result) {
|
|
||||||
multistate(data, MSTATE_SETUP);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/* after the transfer is done, go DONE */
|
|
||||||
|
|
||||||
/* but first check to see if we got a location info even though we
|
|
||||||
are not following redirects */
|
|
||||||
if(data->req.location) {
|
|
||||||
free(newurl);
|
|
||||||
newurl = data->req.location;
|
|
||||||
data->req.location = NULL;
|
|
||||||
result = multi_follow(data, newurl, FOLLOW_FAKE);
|
|
||||||
if(result) {
|
|
||||||
stream_error = TRUE;
|
|
||||||
result = multi_done(data, result, TRUE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!result) {
|
|
||||||
multistate(data, MSTATE_DONE);
|
|
||||||
rc = CURLM_CALL_MULTI_PERFORM;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(data->state.select_bits && !Curl_xfer_is_blocked(data)) {
|
|
||||||
/* This avoids CURLM_CALL_MULTI_PERFORM so that a very fast transfer
|
|
||||||
will not get stuck on this transfer at the expense of other
|
|
||||||
concurrent transfers */
|
|
||||||
Curl_expire(data, 0, EXPIRE_RUN_NOW);
|
|
||||||
}
|
|
||||||
free(newurl);
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
case MSTATE_DONE:
|
case MSTATE_DONE:
|
||||||
/* this state is highly transient, so run another loop after this */
|
/* this state is highly transient, so run another loop after this */
|
||||||
|
Loading…
Reference in New Issue
Block a user