]> git.argeo.org Git - gpl/argeo-slc.git/blob - org.argeo.slc.core/src/org/argeo/slc/core/execution/tasks/SystemCall.java
8f031847edaec1db4e6ee269fa05cb092318f0e9
[gpl/argeo-slc.git] / org.argeo.slc.core / src / org / argeo / slc / core / execution / tasks / SystemCall.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.argeo.slc.core.execution.tasks;
17
18 import java.io.File;
19 import java.io.FileOutputStream;
20 import java.io.FileWriter;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.io.PipedInputStream;
25 import java.io.PipedOutputStream;
26 import java.io.Writer;
27 import java.util.ArrayList;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.UUID;
33
34 import javax.security.auth.callback.CallbackHandler;
35
36 import org.apache.commons.exec.CommandLine;
37 import org.apache.commons.exec.DefaultExecutor;
38 import org.apache.commons.exec.ExecuteException;
39 import org.apache.commons.exec.ExecuteResultHandler;
40 import org.apache.commons.exec.ExecuteStreamHandler;
41 import org.apache.commons.exec.ExecuteWatchdog;
42 import org.apache.commons.exec.Executor;
43 import org.apache.commons.exec.LogOutputStream;
44 import org.apache.commons.exec.PumpStreamHandler;
45 import org.apache.commons.exec.ShutdownHookProcessDestroyer;
46 import org.apache.commons.io.FileUtils;
47 import org.apache.commons.io.IOUtils;
48 import org.apache.commons.logging.Log;
49 import org.apache.commons.logging.LogFactory;
50 import org.argeo.slc.SlcException;
51 import org.argeo.slc.UnsupportedException;
52 import org.argeo.slc.core.execution.ExecutionResources;
53 import org.argeo.slc.core.test.SimpleResultPart;
54 import org.argeo.slc.test.TestResult;
55 import org.argeo.slc.test.TestStatus;
56 import org.springframework.core.io.Resource;
57 import org.springframework.security.core.Authentication;
58 import org.springframework.security.core.context.SecurityContextHolder;
59
60 /** Execute an OS specific system call. */
61 public class SystemCall implements Runnable {
62 public final static String LOG_STDOUT = "System.out";
63
64 private final Log log = LogFactory.getLog(getClass());
65
66 private String execDir;
67
68 private String cmd = null;
69 private List<Object> command = null;
70
71 private Executor executor = new DefaultExecutor();
72 private Boolean synchronous = true;
73
74 private String stdErrLogLevel = "ERROR";
75 private String stdOutLogLevel = "INFO";
76
77 private Resource stdOutFile = null;
78 private Resource stdErrFile = null;
79
80 private Resource stdInFile = null;
81 /**
82 * If no {@link #stdInFile} provided, writing to this stream will write to
83 * the stdin of the process.
84 */
85 private OutputStream stdInSink = null;
86
87 private Boolean redirectStdOut = false;
88
89 private List<SystemCallOutputListener> outputListeners = Collections
90 .synchronizedList(new ArrayList<SystemCallOutputListener>());
91
92 private Map<String, List<Object>> osCommands = new HashMap<String, List<Object>>();
93 private Map<String, String> osCmds = new HashMap<String, String>();
94 private Map<String, String> environmentVariables = new HashMap<String, String>();
95
96 private Boolean logCommand = false;
97 private Boolean redirectStreams = true;
98 private Boolean exceptionOnFailed = true;
99 private Boolean mergeEnvironmentVariables = true;
100
101 private Authentication authentication;
102
103 private String osConsole = null;
104 private String generateScript = null;
105
106 /** 24 hours */
107 private Long watchdogTimeout = 24 * 60 * 60 * 1000l;
108
109 private TestResult testResult;
110
111 private ExecutionResources executionResources;
112
113 /** Sudo the command, as root if empty or as user if not. */
114 private String sudo = null;
115 // TODO make it more secure and robust, test only once
116 private final String sudoPrompt = UUID.randomUUID().toString();
117 private String askPassProgram = "/usr/libexec/openssh/ssh-askpass";
118 private boolean firstLine = true;
119 private CallbackHandler callbackHandler;
120 /** Chroot to the this path (must not be empty) */
121 private String chroot = null;
122
123 // Current
124 /** Current watchdog, null if process is completed */
125 ExecuteWatchdog currentWatchdog = null;
126
127 /** Empty constructor */
128 public SystemCall() {
129
130 }
131
132 /**
133 * Constructor based on the provided command list.
134 *
135 * @param command
136 * the command list
137 */
138 public SystemCall(List<Object> command) {
139 this.command = command;
140 }
141
142 /**
143 * Constructor based on the provided command.
144 *
145 * @param cmd
146 * the command. If the provided string contains no space a
147 * command list is initialized with the argument as first
148 * component (useful for chained construction)
149 */
150 public SystemCall(String cmd) {
151 if (cmd.indexOf(' ') < 0) {
152 command = new ArrayList<Object>();
153 command.add(cmd);
154 } else {
155 this.cmd = cmd;
156 }
157 }
158
159 /** Executes the system call. */
160 public void run() {
161 authentication = SecurityContextHolder.getContext().getAuthentication();
162
163 // Manage streams
164 Writer stdOutWriter = null;
165 OutputStream stdOutputStream = null;
166 Writer stdErrWriter = null;
167 InputStream stdInStream = null;
168 if (stdOutFile != null)
169 if (redirectStdOut)
170 stdOutputStream = createOutputStream(stdOutFile);
171 else
172 stdOutWriter = createWriter(stdOutFile, true);
173
174 if (stdErrFile != null) {
175 stdErrWriter = createWriter(stdErrFile, true);
176 } else {
177 if (stdOutFile != null && !redirectStdOut)
178 stdErrWriter = createWriter(stdOutFile, true);
179 }
180
181 try {
182 if (stdInFile != null)
183 stdInStream = stdInFile.getInputStream();
184 else {
185 stdInStream = new PipedInputStream();
186 stdInSink = new PipedOutputStream(
187 (PipedInputStream) stdInStream);
188 }
189 } catch (IOException e2) {
190 throw new SlcException("Cannot open a stream for " + stdInFile, e2);
191 }
192
193 if (log.isTraceEnabled()) {
194 log.debug("os.name=" + System.getProperty("os.name"));
195 log.debug("os.arch=" + System.getProperty("os.arch"));
196 log.debug("os.version=" + System.getProperty("os.version"));
197 }
198
199 // Execution directory
200 File dir = new File(getExecDirToUse());
201 // if (!dir.exists())
202 // dir.mkdirs();
203
204 // Watchdog to check for lost processes
205 Executor executorToUse;
206 if (executor != null)
207 executorToUse = executor;
208 else
209 executorToUse = new DefaultExecutor();
210 executorToUse.setWatchdog(createWatchdog());
211
212 if (redirectStreams) {
213 // Redirect standard streams
214 executorToUse.setStreamHandler(createExecuteStreamHandler(
215 stdOutWriter, stdOutputStream, stdErrWriter, stdInStream));
216 } else {
217 // Dummy stream handler (otherwise pump is used)
218 executorToUse.setStreamHandler(new DummyexecuteStreamHandler());
219 }
220
221 executorToUse.setProcessDestroyer(new ShutdownHookProcessDestroyer());
222 executorToUse.setWorkingDirectory(dir);
223
224 // Command line to use
225 final CommandLine commandLine = createCommandLine();
226 if (logCommand)
227 log.info("Execute command:\n" + commandLine
228 + "\n in working directory: \n" + dir + "\n");
229
230 // Env variables
231 Map<String, String> environmentVariablesToUse = null;
232 environmentVariablesToUse = new HashMap<String, String>();
233 if (mergeEnvironmentVariables)
234 environmentVariablesToUse.putAll(System.getenv());
235 if (environmentVariables.size() > 0)
236 environmentVariablesToUse.putAll(environmentVariables);
237
238 // Execute
239 ExecuteResultHandler executeResultHandler = createExecuteResultHandler(commandLine);
240
241 //
242 // THE EXECUTION PROPER
243 //
244 try {
245 if (synchronous)
246 try {
247 int exitValue = executorToUse.execute(commandLine,
248 environmentVariablesToUse);
249 executeResultHandler.onProcessComplete(exitValue);
250 } catch (ExecuteException e1) {
251 if (e1.getExitValue() == Executor.INVALID_EXITVALUE) {
252 Thread.currentThread().interrupt();
253 return;
254 }
255 // Sleep 1s in order to make sure error logs are flushed
256 Thread.sleep(1000);
257 executeResultHandler.onProcessFailed(e1);
258 }
259 else
260 executorToUse.execute(commandLine, environmentVariablesToUse,
261 executeResultHandler);
262 } catch (SlcException e) {
263 throw e;
264 } catch (Exception e) {
265 throw new SlcException("Could not execute command " + commandLine,
266 e);
267 } finally {
268 IOUtils.closeQuietly(stdOutWriter);
269 IOUtils.closeQuietly(stdErrWriter);
270 IOUtils.closeQuietly(stdInStream);
271 IOUtils.closeQuietly(stdInSink);
272 }
273
274 }
275
276 public synchronized String function() {
277 final StringBuffer buf = new StringBuffer("");
278 SystemCallOutputListener tempOutputListener = new SystemCallOutputListener() {
279 private Long lineCount = 0l;
280
281 public void newLine(SystemCall systemCall, String line,
282 Boolean isError) {
283 if (!isError) {
284 if (lineCount != 0l)
285 buf.append('\n');
286 buf.append(line);
287 lineCount++;
288 }
289 }
290 };
291 addOutputListener(tempOutputListener);
292 run();
293 removeOutputListener(tempOutputListener);
294 return buf.toString();
295 }
296
297 public String asCommand() {
298 return createCommandLine().toString();
299 }
300
301 @Override
302 public String toString() {
303 return asCommand();
304 }
305
306 /**
307 * Build a command line based on the properties. Can be overridden by
308 * specific command wrappers.
309 */
310 protected CommandLine createCommandLine() {
311 // Check if an OS specific command overrides
312 String osName = System.getProperty("os.name");
313 List<Object> commandToUse = null;
314 if (osCommands.containsKey(osName))
315 commandToUse = osCommands.get(osName);
316 else
317 commandToUse = command;
318 String cmdToUse = null;
319 if (osCmds.containsKey(osName))
320 cmdToUse = osCmds.get(osName);
321 else
322 cmdToUse = cmd;
323
324 CommandLine commandLine = null;
325
326 // Which command definition to use
327 if (commandToUse == null && cmdToUse == null)
328 throw new SlcException("Please specify a command.");
329 else if (commandToUse != null && cmdToUse != null)
330 throw new SlcException(
331 "Specify the command either as a line or as a list.");
332 else if (cmdToUse != null) {
333 if (chroot != null && !chroot.trim().equals(""))
334 cmdToUse = "chroot \"" + chroot + "\" " + cmdToUse;
335 if (sudo != null) {
336 environmentVariables.put("SUDO_ASKPASS", askPassProgram);
337 if (!sudo.trim().equals(""))
338 cmdToUse = "sudo -p " + sudoPrompt + " -u " + sudo + " "
339 + cmdToUse;
340 else
341 cmdToUse = "sudo -p " + sudoPrompt + " " + cmdToUse;
342 }
343
344 // GENERATE COMMAND LINE
345 commandLine = CommandLine.parse(cmdToUse);
346 } else if (commandToUse != null) {
347 if (commandToUse.size() == 0)
348 throw new SlcException("Command line is empty.");
349
350 if (chroot != null && sudo != null) {
351 commandToUse.add(0, "chroot");
352 commandToUse.add(1, chroot);
353 }
354
355 if (sudo != null) {
356 environmentVariables.put("SUDO_ASKPASS", askPassProgram);
357 commandToUse.add(0, "sudo");
358 commandToUse.add(1, "-p");
359 commandToUse.add(2, sudoPrompt);
360 if (!sudo.trim().equals("")) {
361 commandToUse.add(3, "-u");
362 commandToUse.add(4, sudo);
363 }
364 }
365
366 // GENERATE COMMAND LINE
367 commandLine = new CommandLine(commandToUse.get(0).toString());
368
369 for (int i = 1; i < commandToUse.size(); i++) {
370 if (log.isTraceEnabled())
371 log.debug(commandToUse.get(i));
372 commandLine.addArgument(commandToUse.get(i).toString());
373 }
374 } else {
375 // all cases covered previously
376 throw new UnsupportedException();
377 }
378
379 if (generateScript != null) {
380 File scriptFile = new File(getExecDirToUse() + File.separator
381 + generateScript);
382 try {
383 FileUtils.writeStringToFile(scriptFile,
384 (osConsole != null ? osConsole + " " : "")
385 + commandLine.toString());
386 } catch (IOException e) {
387 throw new SlcException("Could not generate script "
388 + scriptFile, e);
389 }
390 commandLine = new CommandLine(scriptFile);
391 } else {
392 if (osConsole != null)
393 commandLine = CommandLine.parse(osConsole + " "
394 + commandLine.toString());
395 }
396
397 return commandLine;
398 }
399
400 /**
401 * Creates a {@link PumpStreamHandler} which redirects streams to the custom
402 * logging mechanism.
403 */
404 protected ExecuteStreamHandler createExecuteStreamHandler(
405 final Writer stdOutWriter, final OutputStream stdOutputStream,
406 final Writer stdErrWriter, final InputStream stdInStream) {
407
408 // Log writers
409 OutputStream stdout = stdOutputStream != null ? stdOutputStream
410 : new LogOutputStream() {
411 protected void processLine(String line, int level) {
412 // if (firstLine) {
413 // if (sudo != null && callbackHandler != null
414 // && line.startsWith(sudoPrompt)) {
415 // try {
416 // PasswordCallback pc = new PasswordCallback(
417 // "sudo password", false);
418 // Callback[] cbs = { pc };
419 // callbackHandler.handle(cbs);
420 // char[] pwd = pc.getPassword();
421 // char[] arr = Arrays.copyOf(pwd,
422 // pwd.length + 1);
423 // arr[arr.length - 1] = '\n';
424 // IOUtils.write(arr, stdInSink);
425 // stdInSink.flush();
426 // } catch (Exception e) {
427 // throw new SlcException(
428 // "Cannot retrieve sudo password", e);
429 // }
430 // }
431 // firstLine = false;
432 // }
433
434 if (line != null && !line.trim().equals(""))
435 logStdOut(line);
436
437 if (stdOutWriter != null)
438 appendLineToFile(stdOutWriter, line);
439 }
440 };
441
442 OutputStream stderr = new LogOutputStream() {
443 protected void processLine(String line, int level) {
444 if (line != null && !line.trim().equals(""))
445 logStdErr(line);
446 if (stdErrWriter != null)
447 appendLineToFile(stdErrWriter, line);
448 }
449 };
450
451 PumpStreamHandler pumpStreamHandler = new PumpStreamHandler(stdout,
452 stderr, stdInStream) {
453
454 @Override
455 public void stop() throws IOException {
456 // prevents the method to block when joining stdin
457 if (stdInSink != null)
458 IOUtils.closeQuietly(stdInSink);
459
460 super.stop();
461 }
462 };
463 return pumpStreamHandler;
464 }
465
466 /** Creates the default {@link ExecuteResultHandler}. */
467 protected ExecuteResultHandler createExecuteResultHandler(
468 final CommandLine commandLine) {
469 return new ExecuteResultHandler() {
470
471 public void onProcessComplete(int exitValue) {
472 String msg = "System call '" + commandLine
473 + "' properly completed.";
474 if (log.isTraceEnabled())
475 log.trace(msg);
476 if (testResult != null) {
477 forwardPath(testResult);
478 testResult.addResultPart(new SimpleResultPart(
479 TestStatus.PASSED, msg));
480 }
481 releaseWatchdog();
482 }
483
484 public void onProcessFailed(ExecuteException e) {
485
486 String msg = "System call '" + commandLine + "' failed.";
487 if (testResult != null) {
488 forwardPath(testResult);
489 testResult.addResultPart(new SimpleResultPart(
490 TestStatus.ERROR, msg, e));
491 } else {
492 if (exceptionOnFailed)
493 throw new SlcException(msg, e);
494 else
495 log.error(msg, e);
496 }
497 releaseWatchdog();
498 }
499 };
500 }
501
502 @Deprecated
503 protected void forwardPath(TestResult testResult) {
504 // TODO: allocate a TreeSPath
505 }
506
507 /**
508 * Shortcut method getting the execDir to use
509 */
510 protected String getExecDirToUse() {
511 try {
512 if (execDir != null) {
513 return execDir;
514 }
515 return System.getProperty("user.dir");
516 } catch (Exception e) {
517 throw new SlcException("Cannot find exec dir", e);
518 }
519 }
520
521 protected void logStdOut(String line) {
522 for (SystemCallOutputListener outputListener : outputListeners)
523 outputListener.newLine(this, line, false);
524 log(stdOutLogLevel, line);
525 }
526
527 protected void logStdErr(String line) {
528 for (SystemCallOutputListener outputListener : outputListeners)
529 outputListener.newLine(this, line, true);
530 log(stdErrLogLevel, line);
531 }
532
533 /** Log from the underlying streams. */
534 protected void log(String logLevel, String line) {
535 // TODO optimize
536 if (SecurityContextHolder.getContext().getAuthentication() == null) {
537 SecurityContextHolder.getContext()
538 .setAuthentication(authentication);
539 }
540
541 if ("ERROR".equals(logLevel))
542 log.error(line);
543 else if ("WARN".equals(logLevel))
544 log.warn(line);
545 else if ("INFO".equals(logLevel))
546 log.info(line);
547 else if ("DEBUG".equals(logLevel))
548 log.debug(line);
549 else if ("TRACE".equals(logLevel))
550 log.trace(line);
551 else if (LOG_STDOUT.equals(logLevel))
552 System.out.println(line);
553 else if ("System.err".equals(logLevel))
554 System.err.println(line);
555 else
556 throw new SlcException("Unknown log level " + logLevel);
557 }
558
559 /** Append line to a log file. */
560 protected void appendLineToFile(Writer writer, String line) {
561 try {
562 writer.append(line).append('\n');
563 } catch (IOException e) {
564 log.error("Cannot write to log file", e);
565 }
566 }
567
568 /** Creates the writer for the output/err files. */
569 protected Writer createWriter(Resource target, Boolean append) {
570 FileWriter writer = null;
571 try {
572
573 final File file;
574 if (executionResources != null)
575 file = new File(executionResources.getAsOsPath(target, true));
576 else
577 file = target.getFile();
578 writer = new FileWriter(file, append);
579 } catch (IOException e) {
580 log.error("Cannot get file for " + target, e);
581 IOUtils.closeQuietly(writer);
582 }
583 return writer;
584 }
585
586 /** Creates an outputstream for the output/err files. */
587 protected OutputStream createOutputStream(Resource target) {
588 FileOutputStream out = null;
589 try {
590
591 final File file;
592 if (executionResources != null)
593 file = new File(executionResources.getAsOsPath(target, true));
594 else
595 file = target.getFile();
596 out = new FileOutputStream(file, false);
597 } catch (IOException e) {
598 log.error("Cannot get file for " + target, e);
599 IOUtils.closeQuietly(out);
600 }
601 return out;
602 }
603
604 /** Append the argument (for chaining) */
605 public SystemCall arg(String arg) {
606 if (command == null)
607 command = new ArrayList<Object>();
608 command.add(arg);
609 return this;
610 }
611
612 /** Append the argument (for chaining) */
613 public SystemCall arg(String arg, String value) {
614 if (command == null)
615 command = new ArrayList<Object>();
616 command.add(arg);
617 command.add(value);
618 return this;
619 }
620
621 // CONTROL
622 public synchronized Boolean isRunning() {
623 return currentWatchdog != null;
624 }
625
626 private synchronized ExecuteWatchdog createWatchdog() {
627 // if (currentWatchdog != null)
628 // throw new SlcException("A process is already being monitored");
629 currentWatchdog = new ExecuteWatchdog(watchdogTimeout);
630 return currentWatchdog;
631 }
632
633 private synchronized void releaseWatchdog() {
634 currentWatchdog = null;
635 }
636
637 public synchronized void kill() {
638 if (currentWatchdog != null)
639 currentWatchdog.destroyProcess();
640 }
641
642 /** */
643 public void setCmd(String command) {
644 this.cmd = command;
645 }
646
647 public void setCommand(List<Object> command) {
648 this.command = command;
649 }
650
651 public void setExecDir(String execdir) {
652 this.execDir = execdir;
653 }
654
655 public void setStdErrLogLevel(String stdErrLogLevel) {
656 this.stdErrLogLevel = stdErrLogLevel;
657 }
658
659 public void setStdOutLogLevel(String stdOutLogLevel) {
660 this.stdOutLogLevel = stdOutLogLevel;
661 }
662
663 public void setSynchronous(Boolean synchronous) {
664 this.synchronous = synchronous;
665 }
666
667 public void setOsCommands(Map<String, List<Object>> osCommands) {
668 this.osCommands = osCommands;
669 }
670
671 public void setOsCmds(Map<String, String> osCmds) {
672 this.osCmds = osCmds;
673 }
674
675 public void setEnvironmentVariables(Map<String, String> environmentVariables) {
676 this.environmentVariables = environmentVariables;
677 }
678
679 public Map<String, String> getEnvironmentVariables() {
680 return environmentVariables;
681 }
682
683 public void setWatchdogTimeout(Long watchdogTimeout) {
684 this.watchdogTimeout = watchdogTimeout;
685 }
686
687 public void setStdOutFile(Resource stdOutFile) {
688 this.stdOutFile = stdOutFile;
689 }
690
691 public void setStdErrFile(Resource stdErrFile) {
692 this.stdErrFile = stdErrFile;
693 }
694
695 public void setStdInFile(Resource stdInFile) {
696 this.stdInFile = stdInFile;
697 }
698
699 public void setTestResult(TestResult testResult) {
700 this.testResult = testResult;
701 }
702
703 public void setLogCommand(Boolean logCommand) {
704 this.logCommand = logCommand;
705 }
706
707 public void setRedirectStreams(Boolean redirectStreams) {
708 this.redirectStreams = redirectStreams;
709 }
710
711 public void setExceptionOnFailed(Boolean exceptionOnFailed) {
712 this.exceptionOnFailed = exceptionOnFailed;
713 }
714
715 public void setMergeEnvironmentVariables(Boolean mergeEnvironmentVariables) {
716 this.mergeEnvironmentVariables = mergeEnvironmentVariables;
717 }
718
719 public void setOsConsole(String osConsole) {
720 this.osConsole = osConsole;
721 }
722
723 public void setGenerateScript(String generateScript) {
724 this.generateScript = generateScript;
725 }
726
727 public void setExecutionResources(ExecutionResources executionResources) {
728 this.executionResources = executionResources;
729 }
730
731 public void setRedirectStdOut(Boolean redirectStdOut) {
732 this.redirectStdOut = redirectStdOut;
733 }
734
735 public void addOutputListener(SystemCallOutputListener outputListener) {
736 outputListeners.add(outputListener);
737 }
738
739 public void removeOutputListener(SystemCallOutputListener outputListener) {
740 outputListeners.remove(outputListener);
741 }
742
743 public void setOutputListeners(
744 List<SystemCallOutputListener> outputListeners) {
745 this.outputListeners = outputListeners;
746 }
747
748 public void setExecutor(Executor executor) {
749 this.executor = executor;
750 }
751
752 public void setSudo(String sudo) {
753 this.sudo = sudo;
754 }
755
756 public void setCallbackHandler(CallbackHandler callbackHandler) {
757 this.callbackHandler = callbackHandler;
758 }
759
760 public void setChroot(String chroot) {
761 this.chroot = chroot;
762 }
763
764 private class DummyexecuteStreamHandler implements ExecuteStreamHandler {
765
766 public void setProcessErrorStream(InputStream is) throws IOException {
767 }
768
769 public void setProcessInputStream(OutputStream os) throws IOException {
770 }
771
772 public void setProcessOutputStream(InputStream is) throws IOException {
773 }
774
775 public void start() throws IOException {
776 }
777
778 public void stop() {
779 }
780
781 }
782 }