mirror of https://github.com/apache/cassandra
cqlsh: Remove ASSUME command
patch by Aleksey Yeschenko; reviewed by Brandon Williams for CASSANDRA-5331
This commit is contained in:
parent
effdb08b34
commit
207cdf3013
|
|
@ -18,6 +18,7 @@
|
||||||
* Allow custom configuration loader (CASSANDRA-5045)
|
* Allow custom configuration loader (CASSANDRA-5045)
|
||||||
* Remove memory emergency pressure valve logic (CASSANDRA-3534)
|
* Remove memory emergency pressure valve logic (CASSANDRA-3534)
|
||||||
* Reduce request latency with eager retry (CASSANDRA-4705)
|
* Reduce request latency with eager retry (CASSANDRA-4705)
|
||||||
|
* cqlsh: Remove ASSUME command (CASSANDRA-5331)
|
||||||
|
|
||||||
|
|
||||||
1.2.4
|
1.2.4
|
||||||
|
|
|
||||||
3
NEWS.txt
3
NEWS.txt
|
|
@ -22,6 +22,9 @@ Upgrading
|
||||||
Use CacheServiceMBean.set{Key,Row}CacheCapacityInMB() instead.
|
Use CacheServiceMBean.set{Key,Row}CacheCapacityInMB() instead.
|
||||||
- authority option in cassandra.yaml has been deprecated since 1.2.0,
|
- authority option in cassandra.yaml has been deprecated since 1.2.0,
|
||||||
but it has been completely removed in 2.0. Please use 'authorizer' option.
|
but it has been completely removed in 2.0. Please use 'authorizer' option.
|
||||||
|
- ASSUME command has been removed from cqlsh. Use CQL3 blobAsType() and
|
||||||
|
typeAsBlob() conversion functions instead.
|
||||||
|
See https://cassandra.apache.org/doc/cql3/CQL.html#blobFun for details.
|
||||||
|
|
||||||
Operations
|
Operations
|
||||||
----------
|
----------
|
||||||
|
|
|
||||||
171
bin/cqlsh
171
bin/cqlsh
|
|
@ -38,7 +38,6 @@ from StringIO import StringIO
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from contextlib import contextmanager, closing
|
from contextlib import contextmanager, closing
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from functools import partial
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import cmd
|
import cmd
|
||||||
|
|
@ -102,8 +101,7 @@ except ImportError, e:
|
||||||
|
|
||||||
import cql.decoders
|
import cql.decoders
|
||||||
from cql.cursor import _VOID_DESCRIPTION
|
from cql.cursor import _VOID_DESCRIPTION
|
||||||
from cql.cqltypes import (cql_types, cql_typename, lookup_casstype, lookup_cqltype,
|
from cql.cqltypes import (cql_types, cql_typename, lookup_casstype, CassandraType)
|
||||||
CassandraType)
|
|
||||||
|
|
||||||
# cqlsh should run correctly when run out of a Cassandra source tree,
|
# cqlsh should run correctly when run out of a Cassandra source tree,
|
||||||
# out of an unpacked Cassandra tarball, and after a proper package install.
|
# out of an unpacked Cassandra tarball, and after a proper package install.
|
||||||
|
|
@ -183,7 +181,6 @@ my_commands_ending_with_newline = (
|
||||||
'describe',
|
'describe',
|
||||||
'desc',
|
'desc',
|
||||||
'show',
|
'show',
|
||||||
'assume',
|
|
||||||
'source',
|
'source',
|
||||||
'capture',
|
'capture',
|
||||||
'debug',
|
'debug',
|
||||||
|
|
@ -207,7 +204,6 @@ cqlsh_extra_syntax_rules = r'''
|
||||||
<specialCommand> ::= <describeCommand>
|
<specialCommand> ::= <describeCommand>
|
||||||
| <consistencyCommand>
|
| <consistencyCommand>
|
||||||
| <showCommand>
|
| <showCommand>
|
||||||
| <assumeCommand>
|
|
||||||
| <sourceCommand>
|
| <sourceCommand>
|
||||||
| <captureCommand>
|
| <captureCommand>
|
||||||
| <copyCommand>
|
| <copyCommand>
|
||||||
|
|
@ -239,18 +235,9 @@ cqlsh_extra_syntax_rules = r'''
|
||||||
| "EACH_QUORUM"
|
| "EACH_QUORUM"
|
||||||
;
|
;
|
||||||
|
|
||||||
<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" | "ASSUMPTIONS" )
|
<showCommand> ::= "SHOW" what=( "VERSION" | "HOST" )
|
||||||
;
|
;
|
||||||
|
|
||||||
<assumeCommand> ::= "ASSUME" cf=<columnFamilyName> <assumeTypeDef>
|
|
||||||
( "," <assumeTypeDef> )*
|
|
||||||
;
|
|
||||||
|
|
||||||
<assumeTypeDef> ::= "NAMES" "ARE" names=<storageType>
|
|
||||||
| "VALUES" "ARE" values=<storageType>
|
|
||||||
| "(" colname=<colname> ")" "VALUES" "ARE" colvalues=<storageType>
|
|
||||||
;
|
|
||||||
|
|
||||||
<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
|
<sourceCommand> ::= "SOURCE" fname=<stringLiteral>
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -291,16 +278,6 @@ cqlsh_extra_syntax_rules = r'''
|
||||||
def complete_help(ctxt, cqlsh):
|
def complete_help(ctxt, cqlsh):
|
||||||
return sorted([ t.upper() for t in cqldocs.get_help_topics() + cqlsh.get_help_topics() ])
|
return sorted([ t.upper() for t in cqldocs.get_help_topics() + cqlsh.get_help_topics() ])
|
||||||
|
|
||||||
@cqlsh_syntax_completer('assumeTypeDef', 'colname')
|
|
||||||
def complete_assume_col(ctxt, cqlsh):
|
|
||||||
ks = ctxt.get_binding('ks', None)
|
|
||||||
ks = cqlsh.cql_unprotect_name(ks) if ks is not None else None
|
|
||||||
cf = cqlsh.cql_unprotect_name(ctxt.get_binding('cf'))
|
|
||||||
cfdef = cqlsh.get_columnfamily(cf, ksname=ks)
|
|
||||||
cols = [cm.name for cm in cfdef.column_metadata]
|
|
||||||
cols.append(cfdef.key_alias or 'KEY')
|
|
||||||
return cqlsh.cql_protect_names(cols)
|
|
||||||
|
|
||||||
def complete_source_quoted_filename(ctxt, cqlsh):
|
def complete_source_quoted_filename(ctxt, cqlsh):
|
||||||
partial = ctxt.get_binding('partial', '')
|
partial = ctxt.get_binding('partial', '')
|
||||||
head, tail = os.path.split(partial)
|
head, tail = os.path.split(partial)
|
||||||
|
|
@ -512,7 +489,6 @@ class Shell(cmd.Cmd):
|
||||||
self.statement = StringIO()
|
self.statement = StringIO()
|
||||||
self.lineno = 1
|
self.lineno = 1
|
||||||
self.in_comment = False
|
self.in_comment = False
|
||||||
self.schema_overrides = {}
|
|
||||||
|
|
||||||
self.prompt = ''
|
self.prompt = ''
|
||||||
if stdin is None:
|
if stdin is None:
|
||||||
|
|
@ -575,32 +551,6 @@ class Shell(cmd.Cmd):
|
||||||
vers['cql'] = self.cql_version
|
vers['cql'] = self.cql_version
|
||||||
print "[cqlsh %(shver)s | Cassandra %(build)s | CQL spec %(cql)s | Thrift protocol %(thrift)s]" % vers
|
print "[cqlsh %(shver)s | Cassandra %(build)s | CQL spec %(cql)s | Thrift protocol %(thrift)s]" % vers
|
||||||
|
|
||||||
def show_assumptions(self):
|
|
||||||
all_overrides = self.schema_overrides.items()
|
|
||||||
all_overrides.sort()
|
|
||||||
if all_overrides:
|
|
||||||
print
|
|
||||||
else:
|
|
||||||
print 'No overrides.'
|
|
||||||
return
|
|
||||||
for keyspace, ksoverrides in groupby(all_overrides, key=lambda x:x[0][0]):
|
|
||||||
keyspace = self.cql_protect_name(keyspace)
|
|
||||||
print 'USE %s;' % keyspace
|
|
||||||
print
|
|
||||||
for (ks, cf), override in ksoverrides:
|
|
||||||
cf = self.cql_protect_name(cf)
|
|
||||||
if override.default_name_type:
|
|
||||||
print 'ASSUME %s NAMES ARE %s;' \
|
|
||||||
% (cf, cql_typename(override.default_name_type))
|
|
||||||
if override.default_value_type:
|
|
||||||
print 'ASSUME %s VALUES ARE %s;' \
|
|
||||||
% (cf, cql_typename(override.default_value_type))
|
|
||||||
for colname, vtype in override.value_types.items():
|
|
||||||
colname = self.cql_protect_name(colname)
|
|
||||||
print 'ASSUME %s(%s) VALUES ARE %s;' \
|
|
||||||
% (cf, colname, cql_typename(vtype))
|
|
||||||
print
|
|
||||||
|
|
||||||
def get_connection_versions(self):
|
def get_connection_versions(self):
|
||||||
try:
|
try:
|
||||||
self.cursor.execute("select * from system.local where key = 'local'")
|
self.cursor.execute("select * from system.local where key = 'local'")
|
||||||
|
|
@ -969,13 +919,12 @@ class Shell(cmd.Cmd):
|
||||||
if ksname is not None:
|
if ksname is not None:
|
||||||
ksname = self.cql_unprotect_name(ksname)
|
ksname = self.cql_unprotect_name(ksname)
|
||||||
cfname = self.cql_unprotect_name(parsed.get_binding('cfname'))
|
cfname = self.cql_unprotect_name(parsed.get_binding('cfname'))
|
||||||
decoder = self.determine_decoder_for(cfname, ksname=ksname)
|
|
||||||
statement = parsed.extract_orig()
|
statement = parsed.extract_orig()
|
||||||
with_default_limit = parsed.get_binding('limit') is None
|
with_default_limit = parsed.get_binding('limit') is None
|
||||||
if with_default_limit:
|
if with_default_limit:
|
||||||
statement = "%s LIMIT %d;" % (statement[:-1], DEFAULT_SELECT_LIMIT)
|
statement = "%s LIMIT %d;" % (statement[:-1], DEFAULT_SELECT_LIMIT)
|
||||||
self.perform_statement(statement,
|
self.perform_statement(statement,
|
||||||
decoder=decoder,
|
decoder=ErrorHandlingSchemaDecoder,
|
||||||
with_default_limit=with_default_limit)
|
with_default_limit=with_default_limit)
|
||||||
|
|
||||||
def perform_statement(self, statement, decoder=None, with_default_limit=False):
|
def perform_statement(self, statement, decoder=None, with_default_limit=False):
|
||||||
|
|
@ -1042,15 +991,6 @@ class Shell(cmd.Cmd):
|
||||||
def parseable_as_cql2(self, statement):
|
def parseable_as_cql2(self, statement):
|
||||||
return cqlhandling.CqlRuleSet.lex_and_whole_match(statement) is not None
|
return cqlhandling.CqlRuleSet.lex_and_whole_match(statement) is not None
|
||||||
|
|
||||||
def determine_decoder_for(self, cfname, ksname=None):
|
|
||||||
decoder = ErrorHandlingSchemaDecoder
|
|
||||||
if ksname is None:
|
|
||||||
ksname = self.current_keyspace
|
|
||||||
overrides = self.schema_overrides.get((ksname, cfname), None)
|
|
||||||
if overrides:
|
|
||||||
decoder = partial(decoder, overrides=overrides)
|
|
||||||
return decoder
|
|
||||||
|
|
||||||
def get_nametype(self, cursor, num):
|
def get_nametype(self, cursor, num):
|
||||||
"""
|
"""
|
||||||
Determine the Cassandra type of a column name from the current row of
|
Determine the Cassandra type of a column name from the current row of
|
||||||
|
|
@ -1748,11 +1688,6 @@ class Shell(cmd.Cmd):
|
||||||
SHOW HOST
|
SHOW HOST
|
||||||
|
|
||||||
Shows where cqlsh is currently connected.
|
Shows where cqlsh is currently connected.
|
||||||
|
|
||||||
SHOW ASSUMPTIONS
|
|
||||||
|
|
||||||
Outputs the current list of type assumptions as specified by the
|
|
||||||
user. See the help for the ASSUME command for more information.
|
|
||||||
"""
|
"""
|
||||||
showwhat = parsed.get_binding('what').lower()
|
showwhat = parsed.get_binding('what').lower()
|
||||||
if showwhat == 'version':
|
if showwhat == 'version':
|
||||||
|
|
@ -1760,73 +1695,9 @@ class Shell(cmd.Cmd):
|
||||||
self.show_version()
|
self.show_version()
|
||||||
elif showwhat == 'host':
|
elif showwhat == 'host':
|
||||||
self.show_host()
|
self.show_host()
|
||||||
elif showwhat == 'assumptions':
|
|
||||||
self.show_assumptions()
|
|
||||||
else:
|
else:
|
||||||
self.printerr('Wait, how do I show %r?' % (showwhat,))
|
self.printerr('Wait, how do I show %r?' % (showwhat,))
|
||||||
|
|
||||||
def do_assume(self, parsed):
|
|
||||||
"""
|
|
||||||
ASSUME [cqlsh only]
|
|
||||||
|
|
||||||
Instruct cqlsh to consider certain column names or values to be of a
|
|
||||||
specified type, even if that type information is not specified in
|
|
||||||
the table's metadata. Data will be deserialized according to the
|
|
||||||
given type, and displayed appropriately when retrieved.
|
|
||||||
|
|
||||||
Use thus:
|
|
||||||
|
|
||||||
ASSUME [<keyspace>.]<tablename> NAMES ARE <type>;
|
|
||||||
|
|
||||||
Treat all column names in the given table as being of the
|
|
||||||
given type.
|
|
||||||
|
|
||||||
ASSUME [<keyspace>.]<tablename> VALUES ARE <type>;
|
|
||||||
|
|
||||||
Treat all column values in the given table as being of the
|
|
||||||
given type, unless there is more information about the specific
|
|
||||||
column being deserialized. That is, a column-specific ASSUME will
|
|
||||||
take precedence here, as will column-specific metadata in the
|
|
||||||
table's definition.
|
|
||||||
|
|
||||||
ASSUME [<keyspace>.]<tablename>(<colname>) VALUES ARE <type>;
|
|
||||||
|
|
||||||
Treat all values in the given column in the given table as
|
|
||||||
being of the specified type. This overrides any other information
|
|
||||||
about the type of a value.
|
|
||||||
|
|
||||||
Assign multiple overrides at once for the same table by
|
|
||||||
separating with commas:
|
|
||||||
|
|
||||||
ASSUME ks.table NAMES ARE uuid, VALUES ARE int, (col) VALUES ARE ascii
|
|
||||||
|
|
||||||
See HELP TYPES for information on the supported data storage types.
|
|
||||||
"""
|
|
||||||
ks = self.cql_unprotect_name(parsed.get_binding('ksname', None))
|
|
||||||
cf = self.cql_unprotect_name(parsed.get_binding('cfname'))
|
|
||||||
colname = self.cql_unprotect_name(parsed.get_binding('colname', None))
|
|
||||||
|
|
||||||
params = {}
|
|
||||||
for paramname in ('names', 'values', 'colvalues'):
|
|
||||||
val = parsed.get_binding(paramname, None)
|
|
||||||
params[paramname] = self.cql_unprotect_value(val)
|
|
||||||
if ks is None:
|
|
||||||
if self.current_keyspace is None:
|
|
||||||
self.printerr('Error: not in any keyspace.')
|
|
||||||
return
|
|
||||||
ks = self.current_keyspace
|
|
||||||
|
|
||||||
for overridetype in ('names', 'values', 'colvalues'):
|
|
||||||
cqltype = params[overridetype]
|
|
||||||
if cqltype is None:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
validator_class = lookup_cqltype(cqltype).cass_parameterized_type()
|
|
||||||
except KeyError:
|
|
||||||
self.printerr('Error: validator type %s not found.' % cqltype)
|
|
||||||
else:
|
|
||||||
self.add_assumption(ks, cf, colname, overridetype, validator_class)
|
|
||||||
|
|
||||||
def do_source(self, parsed):
|
def do_source(self, parsed):
|
||||||
"""
|
"""
|
||||||
SOURCE [cqlsh only]
|
SOURCE [cqlsh only]
|
||||||
|
|
@ -2067,41 +1938,7 @@ class Shell(cmd.Cmd):
|
||||||
text = '%s:%d:%s' % (self.stdin.name, self.lineno, text)
|
text = '%s:%d:%s' % (self.stdin.name, self.lineno, text)
|
||||||
self.writeresult(text, color, newline=newline, out=sys.stderr)
|
self.writeresult(text, color, newline=newline, out=sys.stderr)
|
||||||
|
|
||||||
def add_assumption(self, ksname, cfname, colname, valtype, valclass):
|
class ErrorHandlingSchemaDecoder(cql.decoders.SchemaDecoder):
|
||||||
try:
|
|
||||||
v_info = self.schema_overrides[(ksname, cfname)]
|
|
||||||
except KeyError:
|
|
||||||
v_info = self.schema_overrides[(ksname, cfname)] = FakeCqlMetadata()
|
|
||||||
if valtype == 'names':
|
|
||||||
v_info.default_name_type = valclass
|
|
||||||
elif valtype == 'values':
|
|
||||||
v_info.default_value_type = valclass
|
|
||||||
elif valtype == 'colvalues':
|
|
||||||
v_info.value_types[colname] = valclass
|
|
||||||
|
|
||||||
class FakeCqlMetadata:
|
|
||||||
def __init__(self):
|
|
||||||
self.name_types = {}
|
|
||||||
self.value_types = {}
|
|
||||||
self.default_name_type = None
|
|
||||||
self.default_value_type = None
|
|
||||||
|
|
||||||
class OverrideableSchemaDecoder(cql.decoders.SchemaDecoder):
|
|
||||||
def __init__(self, schema, overrides=None):
|
|
||||||
cql.decoders.SchemaDecoder.__init__(self, schema)
|
|
||||||
self.apply_schema_overrides(overrides)
|
|
||||||
|
|
||||||
def apply_schema_overrides(self, overrides):
|
|
||||||
if overrides is None:
|
|
||||||
return
|
|
||||||
if overrides.default_name_type is not None:
|
|
||||||
self.schema.default_name_type = overrides.default_name_type
|
|
||||||
if overrides.default_value_type is not None:
|
|
||||||
self.schema.default_value_type = overrides.default_value_type
|
|
||||||
self.schema.name_types.update(overrides.name_types)
|
|
||||||
self.schema.value_types.update(overrides.value_types)
|
|
||||||
|
|
||||||
class ErrorHandlingSchemaDecoder(OverrideableSchemaDecoder):
|
|
||||||
def name_decode_error(self, err, namebytes, expectedtype):
|
def name_decode_error(self, err, namebytes, expectedtype):
|
||||||
return DecodeError(namebytes, err, expectedtype)
|
return DecodeError(namebytes, err, expectedtype)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ class TestCqlshCommands(BaseTestCase):
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_assume(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_show(self):
|
def test_show(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ class TestCqlshCompletion_CQL2(CqlshCompletionCase):
|
||||||
module = cqlsh.cqlhandling
|
module = cqlsh.cqlhandling
|
||||||
|
|
||||||
def test_complete_on_empty_string(self):
|
def test_complete_on_empty_string(self):
|
||||||
self.trycompletions('', choices=('?', 'ALTER', 'ASSUME', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
|
self.trycompletions('', choices=('?', 'ALTER', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
|
||||||
'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
|
'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
|
||||||
'DROP', 'HELP', 'INSERT', 'SELECT', 'SHOW', 'SOURCE',
|
'DROP', 'HELP', 'INSERT', 'SELECT', 'SHOW', 'SOURCE',
|
||||||
'TRACING', 'TRUNCATE', 'UPDATE', 'USE', 'exit', 'quit'))
|
'TRACING', 'TRUNCATE', 'UPDATE', 'USE', 'exit', 'quit'))
|
||||||
|
|
@ -178,7 +178,7 @@ class TestCqlshCompletion_CQL3final(TestCqlshCompletion_CQL2):
|
||||||
module = cqlsh.cql3handling
|
module = cqlsh.cql3handling
|
||||||
|
|
||||||
def test_complete_on_empty_string(self):
|
def test_complete_on_empty_string(self):
|
||||||
self.trycompletions('', choices=('?', 'ALTER', 'ASSUME', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
|
self.trycompletions('', choices=('?', 'ALTER', 'BEGIN', 'CAPTURE', 'CONSISTENCY',
|
||||||
'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
|
'COPY', 'CREATE', 'DEBUG', 'DELETE', 'DESC', 'DESCRIBE',
|
||||||
'DROP', 'GRANT', 'HELP', 'INSERT', 'LIST', 'REVOKE',
|
'DROP', 'GRANT', 'HELP', 'INSERT', 'LIST', 'REVOKE',
|
||||||
'SELECT', 'SHOW', 'SOURCE', 'TRACING', 'TRUNCATE', 'UPDATE',
|
'SELECT', 'SHOW', 'SOURCE', 'TRACING', 'TRUNCATE', 'UPDATE',
|
||||||
|
|
|
||||||
|
|
@ -894,28 +894,6 @@ class TestCqlshOutput(BaseTestCase):
|
||||||
self.assertRegexpMatches(output, '^Connected to .* at %s:%d\.$'
|
self.assertRegexpMatches(output, '^Connected to .* at %s:%d\.$'
|
||||||
% (re.escape(TEST_HOST), TEST_PORT))
|
% (re.escape(TEST_HOST), TEST_PORT))
|
||||||
|
|
||||||
def test_show_assumptions_output(self):
|
|
||||||
expected_output = '\nUSE %s;\n\n' % quote_name('', get_test_keyspace())
|
|
||||||
|
|
||||||
with testrun_cqlsh(tty=True) as c:
|
|
||||||
output = c.cmd_and_response('show assumptions')
|
|
||||||
self.assertEqual(output, 'No overrides.\n')
|
|
||||||
|
|
||||||
c.cmd_and_response('assume dynamic_values VALUES aRe uuid;')
|
|
||||||
expected_output += 'ASSUME dynamic_values VALUES ARE uuid;\n'
|
|
||||||
output = c.cmd_and_response('show assumptions')
|
|
||||||
self.assertEqual(output, expected_output + '\n')
|
|
||||||
|
|
||||||
c.cmd_and_response('Assume has_all_types names arE float;')
|
|
||||||
expected_output += 'ASSUME has_all_types NAMES ARE float;\n'
|
|
||||||
output = c.cmd_and_response('show assumptions')
|
|
||||||
self.assertEqual(output, expected_output + '\n')
|
|
||||||
|
|
||||||
c.cmd_and_response('assume twenty_rows_table ( b ) values are decimal;')
|
|
||||||
expected_output += 'ASSUME twenty_rows_table(b) VALUES ARE decimal;\n'
|
|
||||||
output = c.cmd_and_response('show assumptions')
|
|
||||||
self.assertEqual(output, expected_output + '\n')
|
|
||||||
|
|
||||||
def test_eof_prints_newline(self):
|
def test_eof_prints_newline(self):
|
||||||
with testrun_cqlsh(tty=True) as c:
|
with testrun_cqlsh(tty=True) as c:
|
||||||
c.send(CONTROL_D)
|
c.send(CONTROL_D)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue