Upstream version 10.38.212.0
[platform/framework/web/crosswalk.git] / src / xwalk / application / tools / tizen / xwalk_package_installer.cc
1 // Copyright (c) 2014 Intel Corporation. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "xwalk/application/tools/tizen/xwalk_package_installer.h"
6
7 #include <sys/types.h>
8 #include <pwd.h>
9 #include <unistd.h>
10 #include <pkgmgr/pkgmgr_parser.h>
11
12 #include <algorithm>
13 #include <map>
14 #include <string>
15
16 #include "base/file_util.h"
17 #include "base/files/file_enumerator.h"
18 #include "base/logging.h"
19 #include "base/path_service.h"
20 #include "base/version.h"
21 #include "third_party/libxml/chromium/libxml_utils.h"
22 #include "xwalk/application/common/application_data.h"
23 #include "xwalk/application/common/application_file_util.h"
24 #include "xwalk/application/common/application_manifest_constants.h"
25 #include "xwalk/application/common/id_util.h"
26 #include "xwalk/application/common/manifest_handlers/tizen_application_handler.h"
27 #include "xwalk/application/common/manifest_handlers/tizen_metadata_handler.h"
28 #include "xwalk/application/common/permission_policy_manager.h"
29 #include "xwalk/application/common/tizen/application_storage.h"
30 #include "xwalk/application/tools/tizen/xwalk_packageinfo_constants.h"
31 #include "xwalk/application/tools/tizen/xwalk_platform_installer.h"
32 #include "xwalk/runtime/common/xwalk_paths.h"
33
34 namespace info = application_packageinfo_constants;
35
36 using xwalk::application::ApplicationData;
37 using xwalk::application::ApplicationStorage;
38 using xwalk::application::FileDeleter;
39 using xwalk::application::Manifest;
40 using xwalk::application::Package;
41
42 namespace {
43
44 const base::FilePath::CharType kApplicationsDir[] =
45     FILE_PATH_LITERAL("applications");
46
47 const base::FilePath::CharType kInstallTempDir[] =
48     FILE_PATH_LITERAL("install_temp");
49
50 const base::FilePath::CharType kUpdateTempDir[] =
51     FILE_PATH_LITERAL("update_temp");
52
53 namespace widget_keys = xwalk::application_widget_keys;
54
55 const base::FilePath kXWalkLauncherBinary("/usr/bin/xwalk-launcher");
56
57 const base::FilePath kDefaultIcon(
58     "/usr/share/icons/default/small/crosswalk.png");
59
60 const std::string kServicePrefix("xwalk-service.");
61 const std::string kAppIdPrefix("xwalk.");
62
63 bool CopyDirectoryContents(const base::FilePath& from,
64     const base::FilePath& to) {
65   base::FileEnumerator iter(from, false,
66       base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
67   for (base::FilePath path = iter.Next(); !path.empty(); path = iter.Next()) {
68     if (iter.GetInfo().IsDirectory()) {
69       if (!base::CopyDirectory(path, to, true))
70         return false;
71     } else if (!base::CopyFile(path, to.Append(path.BaseName()))) {
72         return false;
73     }
74   }
75
76   return true;
77 }
78
79 void WriteMetaDataElement(
80     XmlWriter& writer, // NOLINT
81     xwalk::application::TizenMetaDataInfo* info) {
82   if (!info)
83     return;
84
85   const std::map<std::string, std::string>& metadata = info->metadata();
86   std::map<std::string, std::string>::const_iterator it;
87   for (it = metadata.begin(); it != metadata.end(); ++it) {
88     writer.StartElement("metadata");
89     writer.AddAttribute("key", it->first);
90     writer.AddAttribute("value", it->second);
91     writer.EndElement();
92   }
93 }
94
95 bool GeneratePkgInfoXml(xwalk::application::ApplicationData* application,
96                         const std::string& icon_name,
97                         const base::FilePath& app_dir,
98                         const base::FilePath& xml_path) {
99   if (!base::PathExists(app_dir) &&
100       !base::CreateDirectory(app_dir))
101     return false;
102
103   std::string package_id =
104       xwalk::application::AppIdToPkgId(application->ID());
105
106   base::FilePath execute_path =
107       app_dir.AppendASCII("bin/").AppendASCII(application->ID());
108   std::string stripped_name = application->Name();
109
110   FILE* file = base::OpenFile(xml_path, "w");
111
112   XmlWriter xml_writer;
113   xml_writer.StartWriting();
114   xml_writer.StartElement("manifest");
115   xml_writer.AddAttribute("xmlns", "http://tizen.org/ns/packages");
116   xml_writer.AddAttribute("package", package_id);
117   xml_writer.AddAttribute("type", "wgt");
118   xml_writer.AddAttribute("version", application->VersionString());
119   xml_writer.WriteElement("label", application->Name());
120   xml_writer.WriteElement("description", application->Description());
121
122   xml_writer.StartElement("ui-application");
123   xml_writer.AddAttribute("appid", application->ID());
124   xml_writer.AddAttribute("exec", execute_path.MaybeAsASCII());
125   xml_writer.AddAttribute("type", "webapp");
126   xml_writer.AddAttribute("taskmanage", "true");
127   xml_writer.WriteElement("label", application->Name());
128
129   xwalk::application::TizenMetaDataInfo* info =
130       static_cast<xwalk::application::TizenMetaDataInfo*>(
131       application->GetManifestData(widget_keys::kTizenMetaDataKey));
132   WriteMetaDataElement(xml_writer, info);
133
134   if (icon_name.empty())
135     xml_writer.WriteElement("icon", info::kDefaultIconName);
136   else
137     xml_writer.WriteElement("icon",
138                             kServicePrefix + application->ID() + ".png");
139   xml_writer.EndElement();  // Ends "ui-application"
140
141   xml_writer.EndElement();  // Ends "manifest" element.
142   xml_writer.StopWriting();
143
144   base::WriteFile(xml_path,
145                   xml_writer.GetWrittenString().c_str(),
146                   xml_writer.GetWrittenString().size());
147
148   base::CloseFile(file);
149   LOG(INFO) << "Converting manifest.json into "
150             << xml_path.BaseName().MaybeAsASCII()
151             << " for installation. [DONE]";
152   return true;
153 }
154
155 bool CreateAppSymbolicLink(const base::FilePath& app_dir,
156                            const std::string& app_id) {
157   base::FilePath execute_path =
158       app_dir.AppendASCII("bin/").AppendASCII(app_id);
159
160   if (!base::CreateDirectory(execute_path.DirName())) {
161     LOG(ERROR) << "Could not create directory '"
162                << execute_path.DirName().value() << "'.";
163     return false;
164   }
165
166   if (!base::CreateSymbolicLink(kXWalkLauncherBinary, execute_path)) {
167     LOG(ERROR) << "Could not create symbolic link to launcher from '"
168                << execute_path.value() << "'.";
169     return false;
170   }
171   return true;
172 }
173
174 }  // namespace
175
176 PackageInstaller::PackageInstaller(ApplicationStorage* storage)
177   : storage_(storage),
178     quiet_(false) {
179 }
180
181 PackageInstaller::~PackageInstaller() {
182 }
183
184 scoped_ptr<PackageInstaller> PackageInstaller::Create(
185     ApplicationStorage* storage) {
186   return scoped_ptr<PackageInstaller>(new PackageInstaller(storage));
187 }
188
189 void PackageInstaller::SetQuiet(bool quiet) {
190   quiet_ = quiet;
191 }
192
193 void PackageInstaller::SetInstallationKey(const std::string& key) {
194   key_ = key;
195 }
196
197 std::string PackageInstaller::PrepareUninstallationID(
198     const std::string& id) {
199   // this function fix pkg_id to app_id
200   // if installer was launched with pkg_id
201   if (xwalk::application::IsValidPkgID(id)) {
202     LOG(INFO) << "The package id is given " << id << " Will find app_id...";
203     std::string appid = xwalk::application::PkgIdToAppId(id);
204     if (!appid.empty())
205       return appid;
206   }
207   return id;
208 }
209
210 bool PackageInstaller::PlatformInstall(ApplicationData* app_data) {
211   std::string app_id(app_data->ID());
212   base::FilePath data_dir;
213   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &data_dir));
214
215   base::FilePath app_dir =
216       data_dir.AppendASCII(info::kAppDir).AppendASCII(app_id);
217   base::FilePath xml_path = data_dir.AppendASCII(info::kAppDir).AppendASCII(
218       app_id + std::string(info::kXmlExtension));
219
220   std::string icon_name;
221   if (!app_data->GetManifest()->GetString(
222       GetIcon128Key(app_data->manifest_type()), &icon_name))
223     LOG(WARNING) << "'icon' not included in manifest";
224
225   // This will clean everything inside '<data dir>/<app id>'.
226   FileDeleter app_dir_cleaner(app_dir, true);
227
228   if (!GeneratePkgInfoXml(app_data, icon_name, app_dir, xml_path)) {
229     LOG(ERROR) << "Failed to create XML metadata file '"
230                << xml_path.value() << "'.";
231     return false;
232   }
233
234   if (!CreateAppSymbolicLink(app_dir, app_id)) {
235     LOG(ERROR) << "Failed to create symbolic link for " << app_id;
236     return false;
237   }
238
239   base::FilePath icon =
240       icon_name.empty() ? kDefaultIcon : app_dir.AppendASCII(icon_name);
241
242   // args for pkgmgr
243   const char* pkgmgr_argv[5];
244   pkgmgr_argv[2] = "-k";
245   pkgmgr_argv[3] = key_.c_str();
246   pkgmgr_argv[4] = "-q";
247
248   PlatformInstaller platform_installer(app_id);
249
250   if (xml_path.empty() || icon.empty()) {
251     LOG(ERROR) << "Xml or icon path is empty";
252     return false;
253   }
254
255   if (!key_.empty()) {
256     pkgmgr_argv[0] = "-i";
257     pkgmgr_argv[1] = app_id.c_str();  // this value is ignored by pkgmgr
258     platform_installer.InitializePkgmgrSignal((quiet_ ? 5 : 4), pkgmgr_argv);
259   }
260
261   if (!platform_installer.InstallApplication(xml_path, icon))
262     return false;
263
264   app_dir_cleaner.Dismiss();
265
266   return true;
267 }
268
269 bool PackageInstaller::PlatformUninstall(ApplicationData* app_data) {
270   std::string app_id(app_data->ID());
271   base::FilePath data_dir;
272   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &data_dir));
273
274   // args for pkgmgr
275   const char* pkgmgr_argv[5];
276   pkgmgr_argv[2] = "-k";
277   pkgmgr_argv[3] = key_.c_str();
278   pkgmgr_argv[4] = "-q";
279
280   PlatformInstaller platform_installer(app_id);
281
282   if (!key_.empty()) {
283     pkgmgr_argv[0] = "-d";
284     pkgmgr_argv[1] = app_id.c_str();  // this value is ignored by pkgmgr
285     platform_installer.InitializePkgmgrSignal((quiet_ ? 5 : 4), pkgmgr_argv);
286   }
287
288   return platform_installer.UninstallApplication();
289 }
290
291 bool PackageInstaller::PlatformUpdate(ApplicationData* app_data) {
292   std::string app_id(app_data->ID());
293   base::FilePath data_dir;
294   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &data_dir));
295
296   base::FilePath app_dir =
297       data_dir.AppendASCII(info::kAppDir).AppendASCII(app_id);
298   base::FilePath new_xml_path = data_dir.AppendASCII(info::kAppDir).AppendASCII(
299       app_id + ".new" + std::string(info::kXmlExtension));
300
301   std::string icon_name;
302   if (!app_data->GetManifest()->GetString(
303       GetIcon128Key(app_data->manifest_type()), &icon_name))
304     LOG(WARNING) << "'icon' not included in manifest";
305
306   // This will clean everything inside '<data dir>/<app id>' and the new XML.
307   FileDeleter app_dir_cleaner(app_dir, true);
308   FileDeleter new_xml_cleaner(new_xml_path, true);
309
310   if (!GeneratePkgInfoXml(app_data, icon_name, app_dir, new_xml_path)) {
311     LOG(ERROR) << "Could not create new XML metadata file '"
312                << new_xml_path.value() << "'.";
313     return false;
314   }
315
316   if (!CreateAppSymbolicLink(app_dir, app_id))
317     return false;
318
319   base::FilePath icon =
320       icon_name.empty() ? kDefaultIcon : app_dir.AppendASCII(icon_name);
321
322   // args for pkgmgr
323   const char* pkgmgr_argv[5];
324   pkgmgr_argv[2] = "-k";
325   pkgmgr_argv[3] = key_.c_str();
326   pkgmgr_argv[4] = "-q";
327
328   PlatformInstaller platform_installer(app_id);
329
330   if (!key_.empty()) {
331     pkgmgr_argv[0] = "-i";
332     pkgmgr_argv[1] = app_id.c_str();  // this value is ignored by pkgmgr
333     platform_installer.InitializePkgmgrSignal((quiet_ ? 5 : 4), pkgmgr_argv);
334   }
335
336   if (!platform_installer.InstallApplication(new_xml_path, icon))
337     return false;
338
339   app_dir_cleaner.Dismiss();
340   return true;
341 }
342
343 bool PackageInstaller::PlatformReinstall(const base::FilePath& path) {
344   // args for pkgmgr
345   const char* pkgmgr_argv[5];
346   pkgmgr_argv[2] = "-k";
347   pkgmgr_argv[3] = key_.c_str();
348   pkgmgr_argv[4] = "-q";
349
350   PlatformInstaller platform_installer;
351
352   if (!key_.empty()) {
353     pkgmgr_argv[0] = "-r";
354     pkgmgr_argv[1] = path.value().c_str();  // this value is ignored by pkgmgr
355     platform_installer.InitializePkgmgrSignal((quiet_ ? 5 : 4), pkgmgr_argv);
356   }
357
358   return platform_installer.ReinstallApplication();
359 }
360
361 bool PackageInstaller::Install(const base::FilePath& path, std::string* id) {
362   // FIXME(leandro): Installation is not robust enough -- should any step
363   // fail, it can't roll back to a consistent state.
364   if (!base::PathExists(path))
365     return false;
366
367   base::FilePath data_dir, install_temp_dir;
368   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &data_dir));
369   install_temp_dir = data_dir.Append(kInstallTempDir);
370   data_dir = data_dir.Append(kApplicationsDir);
371
372   // Make sure the kApplicationsDir exists under data_path, otherwise,
373   // the installation will always fail because of moving application
374   // resources into an invalid directory.
375   if (!base::DirectoryExists(data_dir) &&
376       !base::CreateDirectory(data_dir))
377     return false;
378
379   if (!base::DirectoryExists(install_temp_dir) &&
380       !base::CreateDirectory(install_temp_dir))
381     return false;
382
383   std::string app_id;
384   base::FilePath unpacked_dir;
385   scoped_ptr<Package> package;
386   FileDeleter tmp_path(install_temp_dir.Append(path.BaseName()), false);
387   if (!base::DirectoryExists(path)) {
388     if (tmp_path.path() != path &&
389         !base::CopyFile(path, tmp_path.path()))
390       return false;
391     package = Package::Create(tmp_path.path());
392     if (!package->IsValid())
393       return false;
394     package->ExtractToTemporaryDir(&unpacked_dir);
395     app_id = package->Id();
396   } else {
397     unpacked_dir = path;
398   }
399
400   std::string error;
401   scoped_refptr<ApplicationData> app_data = LoadApplication(
402       unpacked_dir, app_id, ApplicationData::LOCAL_DIRECTORY,
403       package->manifest_type(), &error);
404   if (!app_data) {
405     LOG(ERROR) << "Error during application installation: " << error;
406     return false;
407   }
408
409   // FIXME: Probably should be removed, as we should not handle permissions
410   // inside XWalk.
411   xwalk::application::PermissionPolicyManager permission_policy_handler;
412   if (!permission_policy_handler.
413       InitApplicationPermission(app_data)) {
414     LOG(ERROR) << "Application permission data is invalid";
415     return false;
416   }
417
418   if (storage_->Contains(app_data->ID())) {
419     *id = app_data->ID();
420     LOG(INFO) << "Already installed: " << *id;
421     return false;
422   }
423
424   base::FilePath app_dir = data_dir.AppendASCII(app_data->ID());
425   if (base::DirectoryExists(app_dir)) {
426     if (!base::DeleteFile(app_dir, true))
427       return false;
428   }
429   if (!package) {
430     if (!base::CreateDirectory(app_dir))
431       return false;
432     if (!CopyDirectoryContents(unpacked_dir, app_dir))
433       return false;
434   } else {
435     if (!base::Move(unpacked_dir, app_dir))
436       return false;
437   }
438
439   app_data->set_path(app_dir);
440
441   if (!storage_->AddApplication(app_data)) {
442     LOG(ERROR) << "Application with id " << app_data->ID()
443                << " couldn't be installed due to a Storage error";
444     base::DeleteFile(app_dir, true);
445     return false;
446   }
447
448   if (!PlatformInstall(app_data)) {
449     LOG(ERROR) << "Application with id " << app_data->ID()
450                << " couldn't be installed due to a platform error";
451     storage_->RemoveApplication(app_data->ID());
452     base::DeleteFile(app_dir, true);
453     return false;
454   }
455
456   LOG(INFO) << "Installed application with id: " << app_data->ID()
457             << "to" << app_dir.MaybeAsASCII() << " successfully.";
458   *id = app_data->ID();
459
460   return true;
461 }
462
463 bool PackageInstaller::Update(const std::string& app_id,
464                               const base::FilePath& path) {
465   if (!xwalk::application::IsValidApplicationID(app_id)) {
466     LOG(ERROR) << "The given application id " << app_id << " is invalid.";
467     return false;
468   }
469
470   if (!base::PathExists(path)) {
471     LOG(ERROR) << "The XPK/WGT package file " << path.value() << " is invalid.";
472     return false;
473   }
474
475   if (base::DirectoryExists(path)) {
476     LOG(WARNING) << "Cannot update an unpacked XPK/WGT package.";
477     return false;
478   }
479
480   base::FilePath unpacked_dir, update_temp_dir;
481   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &update_temp_dir));
482   update_temp_dir = update_temp_dir.Append(kUpdateTempDir);
483   if (!base::DirectoryExists(update_temp_dir) &&
484       !base::CreateDirectory(update_temp_dir))
485     return false;
486
487   FileDeleter tmp_path(update_temp_dir.Append(path.BaseName()), false);
488   if (tmp_path.path() != path &&
489       !base::CopyFile(path, tmp_path.path()))
490     return false;
491
492   scoped_ptr<Package> package = Package::Create(tmp_path.path());
493   if (!package) {
494     LOG(ERROR) << "XPK/WGT file is invalid.";
495     return false;
496   }
497
498   if (app_id.compare(package->Id()) != 0) {
499     LOG(ERROR) << "The XPK/WGT file is invalid, the application id is not the"
500                << "same as the installed application has.";
501     return false;
502   }
503
504   if (!package->ExtractToTemporaryDir(&unpacked_dir))
505     return false;
506
507   std::string error;
508   scoped_refptr<ApplicationData> new_app_data =
509       LoadApplication(unpacked_dir, app_id, ApplicationData::TEMP_DIRECTORY,
510                       package->manifest_type(), &error);
511   if (!new_app_data) {
512     LOG(ERROR) << "An error occurred during application updating: " << error;
513     return false;
514   }
515
516   scoped_refptr<ApplicationData> old_app_data =
517       storage_->GetApplicationData(app_id);
518   if (!old_app_data) {
519     LOG(INFO) << "Application haven't installed yet: " << app_id;
520     return false;
521   }
522
523   // For Tizen WGT package, downgrade to a lower version or reinstall
524   // is permitted when using Tizen WRT, Crosswalk runtime need to follow
525   // this behavior on Tizen platform.
526   if (package->manifest_type() != Manifest::TYPE_WIDGET &&
527       old_app_data->Version()->CompareTo(
528           *(new_app_data->Version())) >= 0) {
529     LOG(INFO) << "The version number of new XPK/WGT package "
530                  "should be higher than "
531               << old_app_data->VersionString();
532     return false;
533   }
534
535   const base::FilePath& app_dir = old_app_data->path();
536   const base::FilePath tmp_dir(app_dir.value()
537                                + FILE_PATH_LITERAL(".tmp"));
538
539   if (!base::Move(app_dir, tmp_dir) ||
540       !base::Move(unpacked_dir, app_dir))
541     return false;
542
543   new_app_data = LoadApplication(
544       app_dir, app_id, ApplicationData::LOCAL_DIRECTORY,
545       package->manifest_type(), &error);
546   if (!new_app_data) {
547     LOG(ERROR) << "Error during loading new package: " << error;
548     base::DeleteFile(app_dir, true);
549     base::Move(tmp_dir, app_dir);
550     return false;
551   }
552
553   if (!storage_->UpdateApplication(new_app_data)) {
554     LOG(ERROR) << "Fail to update application, roll back to the old one.";
555     base::DeleteFile(app_dir, true);
556     base::Move(tmp_dir, app_dir);
557     return false;
558   }
559
560   if (!PlatformUpdate(new_app_data)) {
561     LOG(ERROR) << "Fail to update application, roll back to the old one.";
562     base::DeleteFile(app_dir, true);
563     if (!storage_->UpdateApplication(old_app_data)) {
564       LOG(ERROR) << "Fail to revert old application info, "
565                  << "remove the application as a last resort.";
566       storage_->RemoveApplication(old_app_data->ID());
567       base::DeleteFile(tmp_dir, true);
568       return false;
569     }
570     base::Move(tmp_dir, app_dir);
571     return false;
572   }
573
574   base::DeleteFile(tmp_dir, true);
575
576   return true;
577 }
578
579 bool PackageInstaller::Uninstall(const std::string& id) {
580   std::string app_id = PrepareUninstallationID(id);
581
582   if (!xwalk::application::IsValidApplicationID(app_id)) {
583     LOG(ERROR) << "The given application id " << app_id << " is invalid.";
584     return false;
585   }
586
587   bool result = true;
588   scoped_refptr<ApplicationData> app_data =
589       storage_->GetApplicationData(app_id);
590   if (!app_data) {
591     LOG(ERROR) << "Failed to find application with id " << app_id
592                << " among the installed ones.";
593     result = false;
594   }
595
596   if (!storage_->RemoveApplication(app_id)) {
597     LOG(ERROR) << "Cannot uninstall application with id " << app_id
598                << "; application is not installed.";
599     result = false;
600   }
601
602   base::FilePath resources;
603   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &resources));
604   resources = resources.Append(kApplicationsDir).AppendASCII(app_id);
605   if (base::DirectoryExists(resources) &&
606       !base::DeleteFile(resources, true)) {
607     LOG(ERROR) << "Error occurred while trying to remove application with id "
608                << app_id << "; Cannot remove all resources.";
609     result = false;
610   }
611
612   if (!PlatformUninstall(app_data))
613     result = false;
614
615   return result;
616 }
617
618 bool PackageInstaller::Reinstall(const base::FilePath& path) {
619   return PlatformReinstall(path);
620 }
621
622 void PackageInstaller::ContinueUnfinishedTasks() {
623   base::FilePath config_dir;
624   CHECK(PathService::Get(xwalk::DIR_DATA_PATH, &config_dir));
625
626   base::FilePath install_temp_dir = config_dir.Append(kInstallTempDir),
627       update_temp_dir = config_dir.Append(kUpdateTempDir);
628   FileDeleter install_cleaner(install_temp_dir, true),
629       update_cleaner(update_temp_dir, true);
630
631   if (base::DirectoryExists(install_temp_dir)) {
632     base::FileEnumerator install_iter(
633         install_temp_dir, false, base::FileEnumerator::FILES);
634     for (base::FilePath file = install_iter.Next();
635          !file.empty(); file = install_iter.Next()) {
636       std::string app_id;
637       Install(file, &app_id);
638     }
639   }
640
641   if (base::DirectoryExists(update_temp_dir)) {
642     base::FileEnumerator update_iter(
643         update_temp_dir, false, base::FileEnumerator::FILES);
644     for (base::FilePath file = update_iter.Next();
645          !file.empty(); file = update_iter.Next()) {
646       std::string app_id;
647       if (!Install(file, &app_id) && storage_->Contains(app_id)) {
648         LOG(INFO) << "trying to update %s" << app_id;
649         Update(app_id, file);
650       }
651     }
652   }
653 }