1 /*
2 * tkMacOSXHLEvents.c --
3 *
4 * Implements high level event support for the Macintosh.
5 *
6 * Copyright © 1995-1997 Sun Microsystems, Inc.
7 * Copyright © 2001-2009 Apple Inc.
8 * Copyright © 2006-2009 Daniel A. Steffen <das@users.sourceforge.net>
9 * Copyright © 2015-2019 Marc Culler
10 * Copyright © 2019 Kevin Walzer/WordTech Communications LLC.
11 *
12 * See the file "license.terms" for information on usage and redistribution
13 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
14 */
15
16 #include "tkMacOSXPrivate.h"
17 #include <sys/param.h>
18 #define URL_MAX_LENGTH (17 + MAXPATHLEN)
19
20 /*
21 * This is a Tcl_Event structure that the Quit AppleEvent handler uses to
22 * schedule the ReallyKillMe function.
23 */
24
25 typedef struct KillEvent {
26 Tcl_Event header; /* Information that is standard for all
27 * events. */
28 Tcl_Interp *interp; /* Interp that was passed to the Quit
29 * AppleEvent */
30 } KillEvent;
31
32 /*
33 * When processing an AppleEvent as an idle task, a pointer to one
34 * of these structs is passed as the clientData.
35 */
36
37 typedef struct AppleEventInfo {
38 Tcl_Interp *interp;
39 const char *procedure;
40 Tcl_DString command;
41 NSAppleEventDescriptor *replyEvent; /* Only used for DoScriptText. */
42 int retryCount;
43 } AppleEventInfo;
44
45 /*
46 * Static functions used only in this file.
47 */
48
49 static int MissedAnyParameters(const AppleEvent *theEvent);
50 static int ReallyKillMe(Tcl_Event *eventPtr, int flags);
51 static void ProcessAppleEvent(ClientData clientData);
52
53 /*
54 * Names of the procedures which can be used to process AppleEvents.
55 */
56
57 static const char openDocumentProc[] = "::tk::mac::OpenDocument";
58 static const char launchURLProc[] = "::tk::mac::LaunchURL";
59 static const char printDocProc[] = "::tk::mac::PrintDocument";
60 static const char scriptFileProc[] = "::tk::mac::DoScriptFile";
61 static const char scriptTextProc[] = "::tk::mac::DoScriptText";
62
63 #pragma mark TKApplication(TKHLEvents)
64
65 @implementation TKApplication(TKHLEvents)
66 - (void) terminate: (id) sender
67 {
68 (void)sender;
69 [self handleQuitApplicationEvent:Nil withReplyEvent:Nil];
70 }
71
72 - (void) superTerminate: (id) sender
73 {
74 (void) sender;
75 [super terminate:nil];
76 }
77
78 - (void) preferences: (id) sender
79 {
80 (void)sender;
81 [self handleShowPreferencesEvent:Nil withReplyEvent:Nil];
82 }
83
84 - (void) handleQuitApplicationEvent: (NSAppleEventDescriptor *)event
85 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
86 {
87 KillEvent *eventPtr;
88 (void)event;
89 (void)replyEvent;
90
91 if (_eventInterp) {
92 /*
93 * Call the exit command from the event loop, since you are not
94 * supposed to call ExitToShell in an Apple Event Handler. We put this
95 * at the head of Tcl's event queue because this message usually comes
96 * when the Mac is shutting down, and we want to kill the shell as
97 * quickly as possible.
98 */
99
100 eventPtr = (KillEvent *)ckalloc(sizeof(KillEvent));
101 eventPtr->header.proc = ReallyKillMe;
102 eventPtr->interp = _eventInterp;
103
104 Tcl_QueueEvent((Tcl_Event *) eventPtr, TCL_QUEUE_HEAD);
105 }
106 }
107
108 - (void) handleOpenApplicationEvent: (NSAppleEventDescriptor *)event
109 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
110 {
111 (void)event;
112 (void)replyEvent;
113
114 if (_eventInterp &&
115 Tcl_FindCommand(_eventInterp, "::tk::mac::OpenApplication", NULL, 0)){
116 int code = Tcl_EvalEx(_eventInterp, "::tk::mac::OpenApplication",
117 -1, TCL_EVAL_GLOBAL);
118 if (code != TCL_OK) {
119 Tcl_BackgroundException(_eventInterp, code);
120 }
121 }
122 }
123
124 - (void) handleReopenApplicationEvent: (NSAppleEventDescriptor *)event
125 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
126 {
127 (void)event;
128 (void)replyEvent;
129
130 [NSApp activateIgnoringOtherApps: YES];
131 if (_eventInterp && Tcl_FindCommand(_eventInterp,
132 "::tk::mac::ReopenApplication", NULL, 0)) {
133 int code = Tcl_EvalEx(_eventInterp, "::tk::mac::ReopenApplication",
134 -1, TCL_EVAL_GLOBAL);
135 if (code != TCL_OK){
136 Tcl_BackgroundException(_eventInterp, code);
137 }
138 }
139 }
140
141 - (void) handleShowPreferencesEvent: (NSAppleEventDescriptor *)event
142 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
143 {
144 (void)event;
145 (void)replyEvent;
146
147 if (_eventInterp &&
148 Tcl_FindCommand(_eventInterp, "::tk::mac::ShowPreferences", NULL, 0)){
149 int code = Tcl_EvalEx(_eventInterp, "::tk::mac::ShowPreferences",
150 -1, TCL_EVAL_GLOBAL);
151 if (code != TCL_OK) {
152 Tcl_BackgroundException(_eventInterp, code);
153 }
154 }
155 }
156
157 - (void) handleOpenDocumentsEvent: (NSAppleEventDescriptor *)event
158 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
159 {
160 Tcl_Encoding utf8;
161 const AEDesc *fileSpecDesc = nil;
162 AEDesc contents;
163 char URLString[1 + URL_MAX_LENGTH];
164 NSURL *fileURL;
165 DescType type;
166 Size actual;
167 long count, index;
168 AEKeyword keyword;
169 Tcl_DString pathName;
170 (void)replyEvent;
171
172 /*
173 * Do nothing if we don't have an interpreter.
174 */
175
176 if (!_eventInterp) {
177 return;
178 }
179
180 fileSpecDesc = [event aeDesc];
181 if (fileSpecDesc == nil ) {
182 return;
183 }
184
185 /*
186 * The AppleEvent's descriptor should either contain a value of
187 * typeObjectSpecifier or typeAEList. In the first case, the descriptor
188 * can be treated as a list of size 1 containing a value which can be
189 * coerced into a fileURL. In the second case we want to work with the list
190 * itself. Values in the list will be coerced into fileURL's if possible;
191 * otherwise they will be ignored.
192 */
193
194 /* Get a copy of the AppleEvent's descriptor. */
195 AEGetParamDesc(fileSpecDesc, keyDirectObject, typeWildCard, &contents);
196 if (contents.descriptorType == typeAEList) {
197 fileSpecDesc = &contents;
198 }
199
200 if (AECountItems(fileSpecDesc, &count) != noErr) {
201 AEDisposeDesc(&contents);
202 return;
203 }
204
205 /*
206 * Construct a Tcl expression which calls the ::tk::mac::OpenDocument
207 * procedure, passing the paths contained in the AppleEvent as arguments.
208 */
209
210 AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo));
211 Tcl_DString *openCommand = &AEInfo->command;
212 Tcl_DStringInit(openCommand);
213 Tcl_DStringAppend(openCommand, openDocumentProc, -1);
214 utf8 = Tcl_GetEncoding(NULL, "utf-8");
215
216 for (index = 1; index <= count; index++) {
217 if (noErr != AEGetNthPtr(fileSpecDesc, index, typeFileURL, &keyword,
218 &type, (Ptr) URLString, URL_MAX_LENGTH, &actual)) {
219 continue;
220 }
221 if (type != typeFileURL) {
222 continue;
223 }
224 URLString[actual] = '\0';
225 fileURL = [NSURL URLWithString:[NSString stringWithUTF8String:(char*)URLString]];
226 if (fileURL == nil) {
227 continue;
228 }
229 Tcl_ExternalToUtfDString(utf8, [[fileURL path] UTF8String], -1, &pathName);
230 Tcl_DStringAppendElement(openCommand, Tcl_DStringValue(&pathName));
231 Tcl_DStringFree(&pathName);
232 }
233
234 Tcl_FreeEncoding(utf8);
235 AEDisposeDesc(&contents);
236 AEInfo->interp = _eventInterp;
237 AEInfo->procedure = openDocumentProc;
238 AEInfo->replyEvent = nil;
239 Tcl_DoWhenIdle(ProcessAppleEvent, (ClientData)AEInfo);
240 AEInfo->retryCount = 0;
241
242 if (Tcl_FindCommand(_eventInterp, "::tk::mac::OpenDocuments", NULL, 0)){
243 ProcessAppleEvent((ClientData)AEInfo);
244 } else {
245 Tcl_CreateTimerHandler(500, ProcessAppleEvent, (ClientData)AEInfo);
246 }
247 }
248
249 - (void) handlePrintDocumentsEvent: (NSAppleEventDescriptor *)event
250 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
251 {
252 NSString* file = [[event paramDescriptorForKeyword:keyDirectObject]
253 stringValue];
254 const char *printFile = [file UTF8String];
255 AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo));
256 Tcl_DString *printCommand = &AEInfo->command;
257 (void)replyEvent;
258
259 Tcl_DStringInit(printCommand);
260 Tcl_DStringAppend(printCommand, printDocProc, -1);
261 Tcl_DStringAppendElement(printCommand, printFile);
262 AEInfo->interp = _eventInterp;
263 AEInfo->procedure = printDocProc;
264 AEInfo->replyEvent = nil;
265 Tcl_DoWhenIdle(ProcessAppleEvent, (ClientData)AEInfo);
266 AEInfo->retryCount = 0;
267 ProcessAppleEvent((ClientData)AEInfo);
268 }
269
270 - (void) handleDoScriptEvent: (NSAppleEventDescriptor *)event
271 withReplyEvent: (NSAppleEventDescriptor *)replyEvent
272 {
273 OSStatus err;
274 const AEDesc *theDesc = nil;
275 DescType type = 0, initialType = 0;
276 Size actual;
277 char URLBuffer[1 + URL_MAX_LENGTH];
278 char errString[128];
279
280 /*
281 * The DoScript event receives one parameter that should be text data or a
282 * fileURL.
283 */
284
285 theDesc = [event aeDesc];
286 if (theDesc == nil) {
287 return;
288 }
289
290 err = AEGetParamPtr(theDesc, keyDirectObject, typeWildCard, &initialType,
291 NULL, 0, NULL);
292 if (err != noErr) {
293 sprintf(errString, "AEDoScriptHandler: GetParamDesc error %d", (int)err);
294 AEPutParamPtr((AppleEvent*)[replyEvent aeDesc], keyErrorString,
295 typeChar, errString, strlen(errString));
296 return;
297 }
298
299 if (MissedAnyParameters((AppleEvent*)theDesc)) {
300 sprintf(errString, "AEDoScriptHandler: extra parameters");
301 AEPutParamPtr((AppleEvent*)[replyEvent aeDesc], keyErrorString,
302 typeChar,errString, strlen(errString));
303 return;
304 }
305
306 if (initialType == typeFileURL || initialType == typeAlias) {
307
308 /*
309 * This descriptor can be coerced to a file url. Construct a Tcl
310 * expression which passes the file path as a string argument to
311 * ::tk::mac::DoScriptFile.
312 */
313
314 if (noErr == AEGetParamPtr(theDesc, keyDirectObject, typeFileURL, &type,
315 (Ptr) URLBuffer, URL_MAX_LENGTH, &actual)) {
316 if (actual > 0) {
317 URLBuffer[actual] = '\0';
318 NSString *urlString = [NSString stringWithUTF8String:(char*)URLBuffer];
319 NSURL *fileURL = [NSURL URLWithString:urlString];
320 AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo));
321 Tcl_DString *scriptFileCommand = &AEInfo->command;
322 Tcl_DStringInit(scriptFileCommand);
323 Tcl_DStringAppend(scriptFileCommand, scriptFileProc, -1);
324 Tcl_DStringAppendElement(scriptFileCommand, [[fileURL path] UTF8String]);
325 AEInfo->interp = _eventInterp;
326 AEInfo->procedure = scriptFileProc;
327 AEInfo->replyEvent = nil;
328 Tcl_DoWhenIdle(ProcessAppleEvent, (ClientData)AEInfo);
329 AEInfo->retryCount = 0;
330 ProcessAppleEvent((ClientData)AEInfo);
331 }
332 }
333 } else if (noErr == AEGetParamPtr(theDesc, keyDirectObject, typeUTF8Text, &type,
334 NULL, 0, &actual)) {
335 /*
336 * The descriptor cannot be coerced to a file URL but can be coerced to
337 * text. Construct a Tcl expression which passes the text as a string
338 * argument to ::tk::mac::DoScriptText.
339 */
340
341 if (actual > 0) {
342 char *data = (char *)ckalloc(actual + 1);
343 if (noErr == AEGetParamPtr(theDesc, keyDirectObject,
344 typeUTF8Text, &type,
345 data, actual, NULL)) {
346 data[actual] = '\0';
347 AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo));
348 Tcl_DString *scriptTextCommand = &AEInfo->command;
349 Tcl_DStringInit(scriptTextCommand);
350 Tcl_DStringAppend(scriptTextCommand, scriptTextProc, -1);
351 Tcl_DStringAppendElement(scriptTextCommand, data);
352 AEInfo->interp = _eventInterp;
353 AEInfo->procedure = scriptTextProc;
354 AEInfo->retryCount = 0;
355 if (Tcl_FindCommand(AEInfo->interp, AEInfo->procedure, NULL, 0)) {
356 AEInfo->replyEvent = replyEvent;
357 ProcessAppleEvent(AEInfo);
358 } else {
359 AEInfo->replyEvent = nil;
360 Tcl_DoWhenIdle(ProcessAppleEvent, AEInfo);
361 ProcessAppleEvent(AEInfo);
362 }
363 }
364 }
365 }
366 }
367
368 - (void)handleURLEvent:(NSAppleEventDescriptor*)event
369 withReplyEvent:(NSAppleEventDescriptor*)replyEvent
370 {
371 NSString* url = [[event paramDescriptorForKeyword:keyDirectObject]
372 stringValue];
373 const char *cURL=[url UTF8String];
374 AppleEventInfo *AEInfo = (AppleEventInfo *)ckalloc(sizeof(AppleEventInfo));
375 Tcl_DString *launchCommand = &AEInfo->command;
376 (void)replyEvent;
377
378 Tcl_DStringInit(launchCommand);
379 Tcl_DStringAppend(launchCommand, launchURLProc, -1);
380 Tcl_DStringAppendElement(launchCommand, cURL);
381 AEInfo->interp = _eventInterp;
382 AEInfo->procedure = launchURLProc;
383 AEInfo->replyEvent = nil;
384 Tcl_DoWhenIdle(ProcessAppleEvent, (ClientData)AEInfo);
385 AEInfo->retryCount = 0;
386 ProcessAppleEvent((ClientData)AEInfo);
387 }
388
389 @end
390
391 #pragma mark -
392
393 /*
394 *----------------------------------------------------------------------
395 *
396 * ProcessAppleEvent --
397 *
398 * Usually used as an idle task which evaluates a Tcl expression generated
399 * from an AppleEvent. If the AppleEventInfo passed as the client data
400 * has a non-null replyEvent, the result of evaluating the expression will
401 * be added to the reply. This must not be done when this function is
402 * called as an idle task, but is done when handling DoScriptText events
403 * when this function is called directly.
404 *
405 * Results:
406 * None.
407 *
408 * Side effects:
409 * The expression will be evaluated and the clientData will be freed.
410 * The replyEvent may be modified to contain the result of evaluating
411 * a Tcl expression.
412 *
413 *----------------------------------------------------------------------
414 */
415
ProcessAppleEvent(ClientData clientData)416 static void ProcessAppleEvent(
417 ClientData clientData)
418 {
419 int code;
420 AppleEventInfo *AEInfo = (AppleEventInfo*) clientData;
421
422 if (!AEInfo->interp) {
423 return;
424 }
425
426 /*
427 * Apple events that are delivered during the app startup can arrive
428 * before the Tcl procedure for handling the events has been defined.
429 * If the command is not found we create a timer handler to process
430 * the event later, hopefully after the command has been created.
431 * We retry up to 2 times before giving up.
432 */
433
434 if (!Tcl_FindCommand(AEInfo->interp, AEInfo->procedure, NULL, 0)) {
435 if (AEInfo->retryCount < 2) {
436 AEInfo->retryCount++;
437 Tcl_CreateTimerHandler(200, ProcessAppleEvent, clientData);
438 } else {
439 ckfree(clientData);
440 }
441 return;
442 }
443 code = Tcl_EvalEx(AEInfo->interp, Tcl_DStringValue(&AEInfo->command),
444 Tcl_DStringLength(&AEInfo->command), TCL_EVAL_GLOBAL);
445
446 if (AEInfo->replyEvent && code >= 0) {
447 int reslen;
448 const char *result = Tcl_GetStringFromObj(Tcl_GetObjResult(AEInfo->interp),
449 &reslen);
450 if (code == TCL_OK) {
451 AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc],
452 keyDirectObject, typeChar, result, reslen);
453 } else {
454 AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc],
455 keyErrorString, typeChar, result, reslen);
456 AEPutParamPtr((AppleEvent*)[AEInfo->replyEvent aeDesc],
457 keyErrorNumber, typeSInt32, (Ptr) &code, sizeof(int));
458 }
459 } else if (code != TCL_OK) {
460 Tcl_BackgroundException(AEInfo->interp, code);
461 }
462
463 Tcl_DStringFree(&AEInfo->command);
464 ckfree(clientData);
465 }
466
467 /*
468 *----------------------------------------------------------------------
469 *
470 * TkMacOSXInitAppleEvents --
471 *
472 * Register AppleEvent handlers with the NSAppleEventManager for
473 * this NSApplication.
474 *
475 * Results:
476 * None.
477 *
478 * Side effects:
479 * None.
480 *
481 *----------------------------------------------------------------------
482 */
483
484 void
TkMacOSXInitAppleEvents(TCL_UNUSED (Tcl_Interp *))485 TkMacOSXInitAppleEvents(
486 TCL_UNUSED(Tcl_Interp *))
487 {
488 NSAppleEventManager *aeManager = [NSAppleEventManager sharedAppleEventManager];
489 static Boolean initialized = FALSE;
490
491 if (!initialized) {
492 initialized = TRUE;
493
494 [aeManager setEventHandler:NSApp
495 andSelector:@selector(handleQuitApplicationEvent:withReplyEvent:)
496 forEventClass:kCoreEventClass andEventID:kAEQuitApplication];
497
498 [aeManager setEventHandler:NSApp
499 andSelector:@selector(handleOpenApplicationEvent:withReplyEvent:)
500 forEventClass:kCoreEventClass andEventID:kAEOpenApplication];
501
502 [aeManager setEventHandler:NSApp
503 andSelector:@selector(handleReopenApplicationEvent:withReplyEvent:)
504 forEventClass:kCoreEventClass andEventID:kAEReopenApplication];
505
506 [aeManager setEventHandler:NSApp
507 andSelector:@selector(handleShowPreferencesEvent:withReplyEvent:)
508 forEventClass:kCoreEventClass andEventID:kAEShowPreferences];
509
510 [aeManager setEventHandler:NSApp
511 andSelector:@selector(handleOpenDocumentsEvent:withReplyEvent:)
512 forEventClass:kCoreEventClass andEventID:kAEOpenDocuments];
513
514 [aeManager setEventHandler:NSApp
515 andSelector:@selector(handlePrintDocumentsEvent:withReplyEvent:)
516 forEventClass:kCoreEventClass andEventID:kAEPrintDocuments];
517
518 [aeManager setEventHandler:NSApp
519 andSelector:@selector(handleDoScriptEvent:withReplyEvent:)
520 forEventClass:kAEMiscStandards andEventID:kAEDoScript];
521
522 [aeManager setEventHandler:NSApp
523 andSelector:@selector(handleURLEvent:withReplyEvent:)
524 forEventClass:kInternetEventClass andEventID:kAEGetURL];
525
526 }
527 }
528
529 /*
530 *----------------------------------------------------------------------
531 *
532 * TkMacOSXDoHLEvent --
533 *
534 * Dispatch an AppleEvent.
535 *
536 * Results:
537 * None.
538 *
539 * Side effects:
540 * Depend on the AppleEvent.
541 *
542 *----------------------------------------------------------------------
543 */
544
545 int
TkMacOSXDoHLEvent(void * theEvent)546 TkMacOSXDoHLEvent(
547 void *theEvent)
548 {
549 /* According to the NSAppleEventManager reference:
550 * "The theReply parameter always specifies a reply Apple event, never
551 * nil. However, the handler should not fill out the reply if the
552 * descriptor type for the reply event is typeNull, indicating the sender
553 * does not want a reply."
554 * The specified way to build such a non-nil descriptor is used here. But
555 * on OSX 10.11, the compiler nonetheless generates a warning. I am
556 * supressing the warning here -- maybe the warnings will stop in a future
557 * compiler release.
558 */
559 #ifdef __clang__
560 #pragma clang diagnostic push
561 #pragma clang diagnostic ignored "-Wnonnull"
562 #endif
563
564 NSAppleEventDescriptor* theReply = [NSAppleEventDescriptor nullDescriptor];
565 NSAppleEventManager *aeManager = [NSAppleEventManager sharedAppleEventManager];
566
567 return [aeManager dispatchRawAppleEvent:(const AppleEvent*)theEvent
568 withRawReply: (AppleEvent *)theReply
569 handlerRefCon: (SRefCon)0];
570
571 #ifdef __clang__
572 #pragma clang diagnostic pop
573 #endif
574 }
575
576 /*
577 *----------------------------------------------------------------------
578 *
579 * ReallyKillMe --
580 *
581 * This procedure tries to kill the shell by running exit, called from
582 * an event scheduled by the "Quit" AppleEvent handler.
583 *
584 * Results:
585 * Runs the "exit" command which might kill the shell.
586 *
587 * Side effects:
588 * None.
589 *
590 *----------------------------------------------------------------------
591 */
592
593 static int
ReallyKillMe(Tcl_Event * eventPtr,TCL_UNUSED (int))594 ReallyKillMe(
595 Tcl_Event *eventPtr,
596 TCL_UNUSED(int))
597 {
598 Tcl_Interp *interp = ((KillEvent *) eventPtr)->interp;
599 int quit = Tcl_FindCommand(interp, "::tk::mac::Quit", NULL, 0)!=NULL;
600
601 if (!quit) {
602 Tcl_Exit(0);
603 }
604
605 int code = Tcl_EvalEx(interp, "::tk::mac::Quit", -1, TCL_EVAL_GLOBAL);
606 if (code != TCL_OK) {
607
608 /*
609 * Should be never reached...
610 */
611
612 Tcl_BackgroundException(interp, code);
613 }
614 return 1;
615 }
616
617 /*
618 *----------------------------------------------------------------------
619 *
620 * MissedAnyParameters --
621 *
622 * Checks to see if parameters are still left in the event.
623 *
624 * Results:
625 * True or false.
626 *
627 * Side effects:
628 * None.
629 *
630 *----------------------------------------------------------------------
631 */
632
633 static int
MissedAnyParameters(const AppleEvent * theEvent)634 MissedAnyParameters(
635 const AppleEvent *theEvent)
636 {
637 DescType returnedType;
638 Size actualSize;
639 OSStatus err;
640
641 err = AEGetAttributePtr(theEvent, keyMissedKeywordAttr,
642 typeWildCard, &returnedType, NULL, 0, &actualSize);
643
644 return (err != errAEDescNotFound);
645 }
646
647 /*
648 * Local Variables:
649 * mode: objc
650 * c-basic-offset: 4
651 * fill-column: 79
652 * coding: utf-8
653 * End:
654 */
655