1 | /* Copyright (c) 2010, 2016, Oracle and/or its affiliates. |
2 | Copyright (c) 2011, 2016, MariaDB |
3 | |
4 | This program is free software; you can redistribute it and/or modify |
5 | it under the terms of the GNU General Public License as published by |
6 | the Free Software Foundation; version 2 of the License. |
7 | |
8 | This program is distributed in the hope that it will be useful, |
9 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | GNU General Public License for more details. |
12 | |
13 | You should have received a copy of the GNU General Public License |
14 | along with this program; if not, write to the Free Software |
15 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ |
16 | |
17 | #include "mariadb.h" |
18 | #include "sql_reload.h" |
19 | #include "sql_priv.h" |
20 | #include "mysqld.h" // select_errors |
21 | #include "sql_class.h" // THD |
22 | #include "sql_acl.h" // acl_reload |
23 | #include "sql_servers.h" // servers_reload |
24 | #include "sql_connect.h" // reset_mqh |
25 | #include "sql_base.h" // close_cached_tables |
26 | #include "sql_db.h" // my_dbopt_cleanup |
27 | #include "hostname.h" // hostname_cache_refresh |
28 | #include "sql_repl.h" // reset_master, reset_slave |
29 | #include "rpl_mi.h" // Master_info::data_lock |
30 | #include "sql_show.h" |
31 | #include "debug_sync.h" |
32 | #include "des_key_file.h" |
33 | #include "transaction.h" |
34 | |
35 | static void disable_checkpoints(THD *thd); |
36 | |
37 | /** |
38 | Reload/resets privileges and the different caches. |
39 | |
40 | @param thd Thread handler (can be NULL!) |
41 | @param options What should be reset/reloaded (tables, privileges, slave...) |
42 | @param tables Tables to flush (if any) |
43 | @param write_to_binlog < 0 if there was an error while interacting with the binary log inside |
44 | reload_acl_and_cache, |
45 | 0 if we should not write to the binary log, |
46 | > 0 if we can write to the binlog. |
47 | |
48 | |
49 | @note Depending on 'options', it may be very bad to write the |
50 | query to the binlog (e.g. FLUSH SLAVE); this is a |
51 | pointer where reload_acl_and_cache() will put 0 if |
52 | it thinks we really should not write to the binlog. |
53 | Otherwise it will put 1. |
54 | |
55 | @return Error status code |
56 | @retval 0 Ok |
57 | @retval !=0 Error; thd->killed is set or thd->is_error() is true |
58 | */ |
59 | |
60 | bool reload_acl_and_cache(THD *thd, unsigned long long options, |
61 | TABLE_LIST *tables, int *write_to_binlog) |
62 | { |
63 | bool result=0; |
64 | select_errors=0; /* Write if more errors */ |
65 | int tmp_write_to_binlog= *write_to_binlog= 1; |
66 | |
67 | DBUG_ASSERT(!thd || !thd->in_sub_stmt); |
68 | |
69 | #ifndef NO_EMBEDDED_ACCESS_CHECKS |
70 | if (options & REFRESH_GRANT) |
71 | { |
72 | THD *tmp_thd= 0; |
73 | /* |
74 | If reload_acl_and_cache() is called from SIGHUP handler we have to |
75 | allocate temporary THD for execution of acl_reload()/grant_reload(). |
76 | */ |
77 | if (unlikely(!thd) && (thd= (tmp_thd= new THD(0)))) |
78 | { |
79 | thd->thread_stack= (char*) &tmp_thd; |
80 | thd->store_globals(); |
81 | } |
82 | |
83 | if (likely(thd)) |
84 | { |
85 | bool reload_acl_failed= acl_reload(thd); |
86 | bool reload_grants_failed= grant_reload(thd); |
87 | bool reload_servers_failed= servers_reload(thd); |
88 | |
89 | if (reload_acl_failed || reload_grants_failed || reload_servers_failed) |
90 | { |
91 | result= 1; |
92 | /* |
93 | When an error is returned, my_message may have not been called and |
94 | the client will hang waiting for a response. |
95 | */ |
96 | my_error(ER_UNKNOWN_ERROR, MYF(0)); |
97 | } |
98 | } |
99 | opt_noacl= 0; |
100 | |
101 | if (unlikely(tmp_thd)) |
102 | { |
103 | delete tmp_thd; |
104 | thd= 0; |
105 | } |
106 | reset_mqh((LEX_USER *)NULL, TRUE); |
107 | } |
108 | #endif |
109 | if (options & REFRESH_LOG) |
110 | { |
111 | /* |
112 | Flush the normal query log, the update log, the binary log, |
113 | the slow query log, the relay log (if it exists) and the log |
114 | tables. |
115 | */ |
116 | |
117 | options|= REFRESH_BINARY_LOG; |
118 | options|= REFRESH_RELAY_LOG; |
119 | options|= REFRESH_SLOW_LOG; |
120 | options|= REFRESH_GENERAL_LOG; |
121 | options|= REFRESH_ENGINE_LOG; |
122 | options|= REFRESH_ERROR_LOG; |
123 | } |
124 | |
125 | if (options & REFRESH_ERROR_LOG) |
126 | if (unlikely(flush_error_log())) |
127 | { |
128 | /* |
129 | When flush_error_log() failed, my_error() has not been called. |
130 | So, we have to do it here to keep the protocol. |
131 | */ |
132 | my_error(ER_UNKNOWN_ERROR, MYF(0)); |
133 | result= 1; |
134 | } |
135 | |
136 | if ((options & REFRESH_SLOW_LOG) && global_system_variables.sql_log_slow) |
137 | logger.flush_slow_log(); |
138 | |
139 | if ((options & REFRESH_GENERAL_LOG) && opt_log) |
140 | logger.flush_general_log(); |
141 | |
142 | if (options & REFRESH_ENGINE_LOG) |
143 | if (ha_flush_logs(NULL)) |
144 | result= 1; |
145 | |
146 | if (options & REFRESH_BINARY_LOG) |
147 | { |
148 | /* |
149 | Writing this command to the binlog may result in infinite loops |
150 | when doing mysqlbinlog|mysql, and anyway it does not really make |
151 | sense to log it automatically (would cause more trouble to users |
152 | than it would help them) |
153 | */ |
154 | tmp_write_to_binlog= 0; |
155 | if (mysql_bin_log.is_open()) |
156 | { |
157 | DYNAMIC_ARRAY *drop_gtid_domain= |
158 | (thd && (thd->lex->delete_gtid_domain.elements > 0)) ? |
159 | &thd->lex->delete_gtid_domain : NULL; |
160 | if (mysql_bin_log.rotate_and_purge(true, drop_gtid_domain)) |
161 | *write_to_binlog= -1; |
162 | |
163 | if (WSREP_ON) |
164 | { |
165 | /* Wait for last binlog checkpoint event to be logged. */ |
166 | mysql_bin_log.wait_for_last_checkpoint_event(); |
167 | } |
168 | } |
169 | } |
170 | if (options & REFRESH_RELAY_LOG) |
171 | { |
172 | #ifdef HAVE_REPLICATION |
173 | LEX_CSTRING connection_name; |
174 | Master_info *mi; |
175 | if (thd) |
176 | connection_name= thd->lex->relay_log_connection_name; |
177 | else |
178 | { |
179 | connection_name.str= (char*) "" ; |
180 | connection_name.length= 0; |
181 | } |
182 | |
183 | /* |
184 | Writing this command to the binlog may cause problems as the |
185 | slave is not likely to have the same connection names. |
186 | */ |
187 | tmp_write_to_binlog= 0; |
188 | if (connection_name.length == 0) |
189 | { |
190 | if (master_info_index->flush_all_relay_logs()) |
191 | *write_to_binlog= -1; |
192 | } |
193 | else if (!(mi= (get_master_info(&connection_name, |
194 | Sql_condition::WARN_LEVEL_ERROR)))) |
195 | { |
196 | result= 1; |
197 | } |
198 | else |
199 | { |
200 | mysql_mutex_lock(&mi->data_lock); |
201 | if (rotate_relay_log(mi)) |
202 | *write_to_binlog= -1; |
203 | mysql_mutex_unlock(&mi->data_lock); |
204 | mi->release(); |
205 | } |
206 | #endif |
207 | } |
208 | #ifdef HAVE_QUERY_CACHE |
209 | if (options & REFRESH_QUERY_CACHE_FREE) |
210 | { |
211 | query_cache.pack(thd); // FLUSH QUERY CACHE |
212 | options &= ~REFRESH_QUERY_CACHE; // Don't flush cache, just free memory |
213 | } |
214 | if (options & (REFRESH_TABLES | REFRESH_QUERY_CACHE)) |
215 | { |
216 | query_cache.flush(); // RESET QUERY CACHE |
217 | } |
218 | #endif /*HAVE_QUERY_CACHE*/ |
219 | |
220 | DBUG_ASSERT(!thd || thd->locked_tables_mode || |
221 | !thd->mdl_context.has_locks() || |
222 | thd->handler_tables_hash.records || |
223 | thd->ull_hash.records || |
224 | thd->global_read_lock.is_acquired()); |
225 | |
226 | /* |
227 | Note that if REFRESH_READ_LOCK bit is set then REFRESH_TABLES is set too |
228 | (see sql_yacc.yy) |
229 | */ |
230 | if (options & (REFRESH_TABLES | REFRESH_READ_LOCK)) |
231 | { |
232 | if ((options & REFRESH_READ_LOCK) && thd) |
233 | { |
234 | /* |
235 | On the first hand we need write lock on the tables to be flushed, |
236 | on the other hand we must not try to aspire a global read lock |
237 | if we have a write locked table as this would lead to a deadlock |
238 | when trying to reopen (and re-lock) the table after the flush. |
239 | */ |
240 | if (thd->locked_tables_mode) |
241 | { |
242 | my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); |
243 | return 1; |
244 | } |
245 | /* |
246 | Writing to the binlog could cause deadlocks, as we don't log |
247 | UNLOCK TABLES |
248 | */ |
249 | tmp_write_to_binlog= 0; |
250 | if (thd->global_read_lock.lock_global_read_lock(thd)) |
251 | return 1; // Killed |
252 | if (close_cached_tables(thd, tables, |
253 | ((options & REFRESH_FAST) ? FALSE : TRUE), |
254 | thd->variables.lock_wait_timeout)) |
255 | { |
256 | /* |
257 | NOTE: my_error() has been already called by reopen_tables() within |
258 | close_cached_tables(). |
259 | */ |
260 | thd->global_read_lock.unlock_global_read_lock(thd); |
261 | return 1; |
262 | } |
263 | |
264 | if (thd->global_read_lock.make_global_read_lock_block_commit(thd)) // Killed |
265 | { |
266 | /* Don't leave things in a half-locked state */ |
267 | thd->global_read_lock.unlock_global_read_lock(thd); |
268 | return 1; |
269 | } |
270 | if (options & REFRESH_CHECKPOINT) |
271 | disable_checkpoints(thd); |
272 | /* |
273 | We need to do it second time after wsrep appliers were blocked in |
274 | make_global_read_lock_block_commit(thd) above since they could have |
275 | modified the tables too. |
276 | */ |
277 | if (WSREP(thd) && |
278 | close_cached_tables(thd, tables, (options & REFRESH_FAST) ? |
279 | FALSE : TRUE, TRUE)) |
280 | result= 1; |
281 | } |
282 | else |
283 | { |
284 | if (thd && thd->locked_tables_mode) |
285 | { |
286 | /* |
287 | If we are under LOCK TABLES we should have a write |
288 | lock on tables which we are going to flush. |
289 | */ |
290 | if (tables) |
291 | { |
292 | for (TABLE_LIST *t= tables; t; t= t->next_local) |
293 | if (!find_table_for_mdl_upgrade(thd, t->db.str, t->table_name.str, false)) |
294 | return 1; |
295 | } |
296 | else |
297 | { |
298 | /* |
299 | It is not safe to upgrade the metadata lock without GLOBAL IX lock. |
300 | This can happen with FLUSH TABLES <list> WITH READ LOCK as we in |
301 | these cases don't take a GLOBAL IX lock in order to be compatible |
302 | with global read lock. |
303 | */ |
304 | if (thd->open_tables && |
305 | !thd->mdl_context.is_lock_owner(MDL_key::GLOBAL, "" , "" , |
306 | MDL_INTENTION_EXCLUSIVE)) |
307 | { |
308 | my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), |
309 | thd->open_tables->s->table_name.str); |
310 | return true; |
311 | } |
312 | |
313 | for (TABLE *tab= thd->open_tables; tab; tab= tab->next) |
314 | { |
315 | if (! tab->mdl_ticket->is_upgradable_or_exclusive()) |
316 | { |
317 | my_error(ER_TABLE_NOT_LOCKED_FOR_WRITE, MYF(0), |
318 | tab->s->table_name.str); |
319 | return 1; |
320 | } |
321 | } |
322 | } |
323 | } |
324 | |
325 | #ifdef WITH_WSREP |
326 | if (thd && thd->wsrep_applier) |
327 | { |
328 | /* |
329 | In case of applier thread, do not wait for table share(s) to be |
330 | removed from table definition cache. |
331 | */ |
332 | options|= REFRESH_FAST; |
333 | } |
334 | #endif |
335 | if (close_cached_tables(thd, tables, |
336 | ((options & REFRESH_FAST) ? FALSE : TRUE), |
337 | (thd ? thd->variables.lock_wait_timeout : |
338 | LONG_TIMEOUT))) |
339 | { |
340 | /* |
341 | NOTE: my_error() has been already called by reopen_tables() within |
342 | close_cached_tables(). |
343 | */ |
344 | result= 1; |
345 | } |
346 | } |
347 | my_dbopt_cleanup(); |
348 | } |
349 | if (options & REFRESH_HOSTS) |
350 | hostname_cache_refresh(); |
351 | if (thd && (options & REFRESH_STATUS)) |
352 | refresh_status(thd); |
353 | if (options & REFRESH_THREADS) |
354 | flush_thread_cache(); |
355 | #ifdef HAVE_REPLICATION |
356 | if (options & REFRESH_MASTER) |
357 | { |
358 | DBUG_ASSERT(thd); |
359 | tmp_write_to_binlog= 0; |
360 | if (reset_master(thd, NULL, 0, thd->lex->next_binlog_file_number)) |
361 | { |
362 | /* NOTE: my_error() has been already called by reset_master(). */ |
363 | result= 1; |
364 | } |
365 | } |
366 | #endif |
367 | #ifdef HAVE_OPENSSL |
368 | if (options & REFRESH_DES_KEY_FILE) |
369 | { |
370 | if (des_key_file && load_des_key_file(des_key_file)) |
371 | { |
372 | /* NOTE: my_error() has been already called by load_des_key_file(). */ |
373 | result= 1; |
374 | } |
375 | } |
376 | #endif |
377 | #ifdef HAVE_REPLICATION |
378 | if (options & REFRESH_SLAVE) |
379 | { |
380 | LEX_MASTER_INFO* lex_mi= &thd->lex->mi; |
381 | Master_info *mi; |
382 | tmp_write_to_binlog= 0; |
383 | |
384 | if (!(mi= get_master_info(&lex_mi->connection_name, |
385 | Sql_condition::WARN_LEVEL_ERROR))) |
386 | { |
387 | result= 1; |
388 | } |
389 | else |
390 | { |
391 | /* The following will fail if slave is running */ |
392 | if (reset_slave(thd, mi)) |
393 | { |
394 | mi->release(); |
395 | /* NOTE: my_error() has been already called by reset_slave(). */ |
396 | result= 1; |
397 | } |
398 | else if (mi->connection_name.length && thd->lex->reset_slave_info.all) |
399 | { |
400 | /* If not default connection and 'all' is used */ |
401 | mi->release(); |
402 | mysql_mutex_lock(&LOCK_active_mi); |
403 | if (master_info_index->remove_master_info(mi)) |
404 | result= 1; |
405 | mysql_mutex_unlock(&LOCK_active_mi); |
406 | } |
407 | else |
408 | mi->release(); |
409 | } |
410 | } |
411 | #endif |
412 | if (options & REFRESH_USER_RESOURCES) |
413 | reset_mqh((LEX_USER *) NULL, 0); /* purecov: inspected */ |
414 | if (options & REFRESH_GENERIC) |
415 | { |
416 | List_iterator_fast<LEX_CSTRING> li(thd->lex->view_list); |
417 | LEX_CSTRING *ls; |
418 | while ((ls= li++)) |
419 | { |
420 | ST_SCHEMA_TABLE *table= find_schema_table(thd, ls); |
421 | if (table->reset_table()) |
422 | result= 1; |
423 | } |
424 | } |
425 | if (*write_to_binlog != -1) |
426 | *write_to_binlog= tmp_write_to_binlog; |
427 | /* |
428 | If the query was killed then this function must fail. |
429 | */ |
430 | return result || (thd ? thd->killed : 0); |
431 | } |
432 | |
433 | |
434 | /** |
435 | Implementation of FLUSH TABLES <table_list> WITH READ LOCK |
436 | and FLUSH TABLES <table_list> FOR EXPORT |
437 | |
438 | In brief: take exclusive locks, expel tables from the table |
439 | cache, reopen the tables, enter the 'LOCKED TABLES' mode, |
440 | downgrade the locks. |
441 | Note: the function is written to be called from |
442 | mysql_execute_command(), it is not reusable in arbitrary |
443 | execution context. |
444 | |
445 | Required privileges |
446 | ------------------- |
447 | Since the statement implicitly enters LOCK TABLES mode, |
448 | it requires LOCK TABLES privilege on every table. |
449 | But since the rest of FLUSH commands require |
450 | the global RELOAD_ACL, it also requires RELOAD_ACL. |
451 | |
452 | Compatibility with the global read lock |
453 | --------------------------------------- |
454 | We don't wait for the GRL, since neither the |
455 | 5.1 combination that this new statement is intended to |
456 | replace (LOCK TABLE <list> WRITE; FLUSH TABLES;), |
457 | nor FLUSH TABLES WITH READ LOCK do. |
458 | @todo: this is not implemented, Dmitry disagrees. |
459 | Currently we wait for GRL in another connection, |
460 | but are compatible with a GRL in our own connection. |
461 | |
462 | Behaviour under LOCK TABLES |
463 | --------------------------- |
464 | Bail out: i.e. don't perform an implicit UNLOCK TABLES. |
465 | This is not consistent with LOCK TABLES statement, but is |
466 | in line with behaviour of FLUSH TABLES WITH READ LOCK, and we |
467 | try to not introduce any new statements with implicit |
468 | semantics. |
469 | |
470 | Compatibility with parallel updates |
471 | ----------------------------------- |
472 | As a result, we will wait for all open transactions |
473 | against the tables to complete. After the lock downgrade, |
474 | new transactions will be able to read the tables, but not |
475 | write to them. |
476 | |
477 | Differences from FLUSH TABLES <list> |
478 | ------------------------------------- |
479 | - you can't flush WITH READ LOCK a non-existent table |
480 | - you can't flush WITH READ LOCK under LOCK TABLES |
481 | |
482 | Effect on views and temporary tables. |
483 | ------------------------------------ |
484 | You can only apply this command to existing base tables. |
485 | If a view with such name exists, ER_WRONG_OBJECT is returned. |
486 | If a temporary table with such name exists, it's ignored: |
487 | if there is a base table, it's used, otherwise ER_NO_SUCH_TABLE |
488 | is returned. |
489 | |
490 | Handling of MERGE tables |
491 | ------------------------ |
492 | For MERGE table this statement will open and lock child tables |
493 | for read (it is impossible to lock parent table without it). |
494 | Child tables won't be flushed unless they are explicitly present |
495 | in the statement's table list. |
496 | |
497 | Implicit commit |
498 | --------------- |
499 | This statement causes an implicit commit before and |
500 | after it. |
501 | |
502 | HANDLER SQL |
503 | ----------- |
504 | If this connection has HANDLERs open against |
505 | some of the tables being FLUSHed, these handlers |
506 | are implicitly flushed (lose their position). |
507 | */ |
508 | |
509 | bool flush_tables_with_read_lock(THD *thd, TABLE_LIST *all_tables) |
510 | { |
511 | Lock_tables_prelocking_strategy lock_tables_prelocking_strategy; |
512 | TABLE_LIST *table_list; |
513 | |
514 | /* |
515 | This is called from SQLCOM_FLUSH, the transaction has |
516 | been committed implicitly. |
517 | */ |
518 | |
519 | if (thd->locked_tables_mode) |
520 | { |
521 | my_error(ER_LOCK_OR_ACTIVE_TRANSACTION, MYF(0)); |
522 | goto error; |
523 | } |
524 | |
525 | if (thd->lex->type & REFRESH_READ_LOCK) |
526 | { |
527 | /* |
528 | Acquire SNW locks on tables to be flushed. Don't acquire global |
529 | IX and database-scope IX locks on the tables as this will make |
530 | this statement incompatible with FLUSH TABLES WITH READ LOCK. |
531 | */ |
532 | if (lock_table_names(thd, all_tables, NULL, |
533 | thd->variables.lock_wait_timeout, |
534 | MYSQL_OPEN_SKIP_SCOPED_MDL_LOCK)) |
535 | goto error; |
536 | |
537 | DEBUG_SYNC(thd,"flush_tables_with_read_lock_after_acquire_locks" ); |
538 | |
539 | for (table_list= all_tables; table_list; |
540 | table_list= table_list->next_global) |
541 | { |
542 | /* Request removal of table from cache. */ |
543 | tdc_remove_table(thd, TDC_RT_REMOVE_UNUSED, |
544 | table_list->db.str, |
545 | table_list->table_name.str, FALSE); |
546 | /* Reset ticket to satisfy asserts in open_tables(). */ |
547 | table_list->mdl_request.ticket= NULL; |
548 | } |
549 | } |
550 | |
551 | thd->variables.option_bits|= OPTION_TABLE_LOCK; |
552 | |
553 | /* |
554 | Before opening and locking tables the below call also waits |
555 | for old shares to go away, so the fact that we don't pass |
556 | MYSQL_OPEN_IGNORE_FLUSH flag to it is important. |
557 | Also we don't pass MYSQL_OPEN_HAS_MDL_LOCK flag as we want |
558 | to open underlying tables if merge table is flushed. |
559 | For underlying tables of the merge the below call has to |
560 | acquire SNW locks to ensure that they can be locked for |
561 | read without further waiting. |
562 | */ |
563 | if (open_and_lock_tables(thd, all_tables, FALSE, |
564 | MYSQL_OPEN_SKIP_SCOPED_MDL_LOCK, |
565 | &lock_tables_prelocking_strategy)) |
566 | goto error_reset_bits; |
567 | |
568 | if (thd->lex->type & REFRESH_FOR_EXPORT) |
569 | { |
570 | // Check if all storage engines support FOR EXPORT. |
571 | for (TABLE_LIST *table_list= all_tables; table_list; |
572 | table_list= table_list->next_global) |
573 | { |
574 | if (!(table_list->table->file->ha_table_flags() & HA_CAN_EXPORT)) |
575 | { |
576 | my_error(ER_ILLEGAL_HA, MYF(0),table_list->table->file->table_type(), |
577 | table_list->db.str, table_list->table_name.str); |
578 | goto error_reset_bits; |
579 | } |
580 | } |
581 | } |
582 | |
583 | if (thd->locked_tables_list.init_locked_tables(thd)) |
584 | goto error_reset_bits; |
585 | |
586 | |
587 | /* |
588 | We don't downgrade MDL_SHARED_NO_WRITE here as the intended |
589 | post effect of this call is identical to LOCK TABLES <...> READ, |
590 | and we didn't use thd->in_lock_talbes and |
591 | thd->sql_command= SQLCOM_LOCK_TABLES hacks to enter the LTM. |
592 | */ |
593 | |
594 | return FALSE; |
595 | |
596 | error_reset_bits: |
597 | trans_rollback_stmt(thd); |
598 | close_thread_tables(thd); |
599 | thd->variables.option_bits&= ~OPTION_TABLE_LOCK; |
600 | error: |
601 | return TRUE; |
602 | } |
603 | |
604 | |
605 | /** |
606 | Disable checkpoints for all handlers |
607 | This is released in unlock_global_read_lock() |
608 | */ |
609 | |
610 | static void disable_checkpoints(THD *thd) |
611 | { |
612 | if (!thd->global_disable_checkpoint) |
613 | { |
614 | thd->global_disable_checkpoint= 1; |
615 | if (!global_disable_checkpoint++) |
616 | ha_checkpoint_state(1); // Disable checkpoints |
617 | } |
618 | } |
619 | |
620 | |