FreeFOAM The Cross-Platform CFD Toolkit
POSIX.C
Go to the documentation of this file.
1 /*---------------------------------------------------------------------------*\
2  ========= |
3  \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
4  \\ / O peration |
5  \\ / A nd | Copyright (C) 1991-2010 OpenCFD Ltd.
6  \\/ M anipulation |
7 -------------------------------------------------------------------------------
8 License
9  This file is part of OpenFOAM.
10 
11  OpenFOAM is free software: you can redistribute it and/or modify it
12  under the terms of the GNU General Public License as published by
13  the Free Software Foundation, either version 3 of the License, or
14  (at your option) any later version.
15 
16  OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
17  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
19  for more details.
20 
21  You should have received a copy of the GNU General Public License
22  along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
23 
24 Description
25  POSIX versions of the functions declared in OSspecific.H
26 
27 \*---------------------------------------------------------------------------*/
28 
29 #ifdef solarisGcc
30 # define _SYS_VNODE_H
31 #endif
32 
33 #include <OpenFOAM/OSspecific.H>
34 #include "POSIX.H"
35 #include <OpenFOAM/foamVersion.H>
36 #include <OpenFOAM/fileName.H>
37 #include "fileStat.H"
38 #include "timer.H"
39 
40 #include <fstream>
41 #include <cstdlib>
42 #include <cctype>
43 
44 #include <stdio.h>
45 #include <unistd.h>
46 #include <dirent.h>
47 #include <pwd.h>
48 #include <errno.h>
49 #include <sys/types.h>
50 #include <sys/stat.h>
51 #include <sys/socket.h>
52 #include <netdb.h>
53 
54 #include <netinet/in.h>
55 
56 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
57 
58 defineTypeNameAndDebug(Foam::POSIX, 0);
59 
60 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
61 
62 pid_t Foam::pid()
63 {
64  return getpid();
65 }
66 
67 pid_t Foam::ppid()
68 {
69  return getppid();
70 }
71 
72 pid_t Foam::pgid()
73 {
74  return getpgrp();
75 }
76 
77 bool Foam::env(const word& envName)
78 {
79  return getenv(envName.c_str()) != NULL;
80 }
81 
82 
84 {
85  char* env = getenv(envName.c_str());
86 
87  if (env)
88  {
89  return string(env);
90  }
91  else
92  {
93  // Return null-constructed string rather than string::null
94  // to avoid cyclic dependencies in the construction of globals
95  return string();
96  }
97 }
98 
99 
100 bool Foam::setEnv
101 (
102  const word& envName,
103  const string& value,
104  const bool overwrite
105 )
106 {
107  return setenv(envName.c_str(), value.c_str(), overwrite) == 0;
108 }
109 
110 
112 {
113  char buffer[256];
114  gethostname(buffer, 256);
115 
116  return buffer;
117 }
118 
119 
121 {
122  struct passwd* pw = getpwuid(getuid());
123 
124  if (pw != NULL)
125  {
126  return pw->pw_name;
127  }
128  else
129  {
130  return word::null;
131  }
132 }
133 
134 
135 // use $HOME environment variable or passwd info
137 {
138  char* env = getenv("HOME");
139 
140  if (env != NULL)
141  {
142  return fileName(env);
143  }
144  else
145  {
146  struct passwd* pw = getpwuid(getuid());
147 
148  if (pw != NULL)
149  {
150  return pw->pw_dir;
151  }
152  else
153  {
154  return fileName::null;
155  }
156  }
157 }
158 
159 
161 {
162  struct passwd* pw;
163 
164  if (userName.size())
165  {
166  pw = getpwnam(userName.c_str());
167  }
168  else
169  {
170  char* env = getenv("HOME");
171 
172  if (env != NULL)
173  {
174  return fileName(env);
175  }
176 
177  pw = getpwuid(getuid());
178  }
179 
180  if (pw != NULL)
181  {
182  return pw->pw_dir;
183  }
184  else
185  {
186  return fileName::null;
187  }
188 }
189 
190 
192 {
193  char buf[255];
194  if (getcwd(buf, 255))
195  {
196  return buf;
197  }
198  else
199  {
200  FatalErrorIn("Foam::cwd()")
201  << "Couldn't get the current working directory"
202  << exit(FatalError);
203 
204  return fileName::null;
205  }
206 }
207 
208 
209 bool Foam::chDir(const fileName& dir)
210 {
211  return chdir(dir.c_str()) != 0;
212 }
213 
214 
216 {
217  /* New search strategy:
218  * - first search in FREEFOAM_CONFIG_DIR
219  * - then in the users directory
220  * - finally in the ${FOAM_INSTALL_CONFIG_PATH}
221  */
222 
223  // Search installation files:
224  // ~~~~~~~~~~~~~~~~~~~~~~~~~~
225  fileName searchDir;
226  if(env("FREEFOAM_CONFIG_DIR"))
227  {
228  searchDir = getEnv("FREEFOAM_CONFIG_DIR");
229  }
230  if (isDir(searchDir))
231  {
232  // Check for shipped FreeFOAM file in $FREEFOAM_CONFIG_DIR
233  fileName fullName = searchDir/name;
234  if (isFile(fullName))
235  {
236  return fullName;
237  }
238  }
239 
240  // Search user files:
241  // ~~~~~~~~~~~~~~~~~~
242  searchDir = home()/".FreeFOAM";
243  if (isDir(searchDir))
244  {
245  // Check for user file in ~/.FreeFOAM/VERSION
246  fileName fullName = searchDir/FOAMversion/name;
247  if (isFile(fullName))
248  {
249  return fullName;
250  }
251 
252  // Check for version-independent user file in ~/.FreeFOAM
253  fullName = searchDir/name;
254  if (isFile(fullName))
255  {
256  return fullName;
257  }
258  }
259 
260  // Search installed config directory
261  // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
262  searchDir = FOAMConfigDir;
263  if (isDir(searchDir))
264  {
265  // Check for installed FreeFOAM file in FOAMConfigDir
266  fileName fullName = searchDir/name;
267  if (isFile(fullName))
268  {
269  return fullName;
270  }
271  }
272 
273  // Not found
274  // abort if the file is mandatory, otherwise return null
275  if (mandatory)
276  {
277  cerr<< "--> FOAM FATAL ERROR in Foam::findEtcFile() :"
278  " could not find mandatory file\n '"
279  << name.c_str() << "'\n\n" << std::endl;
280  ::exit(1);
281  }
282 
283  // Return null-constructed fileName rather than fileName::null
284  // to avoid cyclic dependencies in the construction of globals
285  return fileName();
286 }
287 
288 
289 bool Foam::mkDir(const fileName& pathName, mode_t mode)
290 {
291  // empty names are meaningless
292  if (pathName.empty())
293  {
294  return false;
295  }
296 
297  // Construct instance path directory if does not exist
298  if (::mkdir(pathName.c_str(), mode) == 0)
299  {
300  // Directory made OK so return true
301  return true;
302  }
303  else
304  {
305  switch (errno)
306  {
307  case EPERM:
308  {
309  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
310  << "The filesystem containing " << pathName
311  << " does not support the creation of directories."
312  << exit(FatalError);
313 
314  return false;
315  }
316 
317  case EEXIST:
318  {
319  // Directory already exists so simply return true
320  return true;
321  }
322 
323  case EFAULT:
324  {
325  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
326  << "" << pathName
327  << " points outside your accessible address space."
328  << exit(FatalError);
329 
330  return false;
331  }
332 
333  case EACCES:
334  {
335  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
336  << "The parent directory does not allow write "
337  "permission to the process,"<< nl
338  << "or one of the directories in " << pathName
339  << " did not allow search (execute) permission."
340  << exit(FatalError);
341 
342  return false;
343  }
344 
345  case ENAMETOOLONG:
346  {
347  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
348  << "" << pathName << " is too long."
349  << exit(FatalError);
350 
351  return false;
352  }
353 
354  case ENOENT:
355  {
356  // Part of the path does not exist so try to create it
357  if (pathName.path().size() && mkDir(pathName.path(), mode))
358  {
359  return mkDir(pathName, mode);
360  }
361  else
362  {
363  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
364  << "Couldn't create directory " << pathName
365  << exit(FatalError);
366 
367  return false;
368  }
369  }
370 
371  case ENOTDIR:
372  {
373  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
374  << "A component used as a directory in " << pathName
375  << " is not, in fact, a directory."
376  << exit(FatalError);
377 
378  return false;
379  }
380 
381  case ENOMEM:
382  {
383  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
384  << "Insufficient kernel memory was available to make "
385  "directory " << pathName << '.'
386  << exit(FatalError);
387 
388  return false;
389  }
390 
391  case EROFS:
392  {
393  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
394  << "" << pathName
395  << " refers to a file on a read-only filesystem."
396  << exit(FatalError);
397 
398  return false;
399  }
400 
401  case ELOOP:
402  {
403  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
404  << "Too many symbolic links were encountered in resolving "
405  << pathName << '.'
406  << exit(FatalError);
407 
408  return false;
409  }
410 
411  case ENOSPC:
412  {
413  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
414  << "The device containing " << pathName
415  << " has no room for the new directory or "
416  << "the user's disk quota is exhausted."
417  << exit(FatalError);
418 
419  return false;
420  }
421 
422  default:
423  {
424  FatalErrorIn("Foam::mkDir(const fileName&, mode_t)")
425  << "Couldn't create directory " << pathName
426  << exit(FatalError);
427 
428  return false;
429  }
430  }
431  }
432 }
433 
434 
435 // Set the file mode
436 bool Foam::chMod(const fileName& name, const mode_t m)
437 {
438  return ::chmod(name.c_str(), m) == 0;
439 }
440 
441 
442 // Return the file mode
443 mode_t Foam::mode(const fileName& name)
444 {
445  fileStat fileStatus(name);
446  if (fileStatus.isValid())
447  {
448  return fileStatus.status().st_mode;
449  }
450  else
451  {
452  return 0;
453  }
454 }
455 
456 
457 // Return the file type: FILE or DIRECTORY
459 {
460  mode_t m = mode(name);
461 
462  if (S_ISREG(m))
463  {
464  return fileName::FILE;
465  }
466  else if (S_ISDIR(m))
467  {
468  return fileName::DIRECTORY;
469  }
470  else
471  {
472  return fileName::UNDEFINED;
473  }
474 }
475 
476 
477 // Does the name exist in the filing system?
478 bool Foam::exists(const fileName& name, const bool checkGzip)
479 {
480  return mode(name) || isFile(name, checkGzip);
481 }
482 
483 
484 // Does the directory exist?
486 {
487  return S_ISDIR(mode(name));
488 }
489 
490 
491 // Does the file exist?
492 bool Foam::isFile(const fileName& name, const bool checkGzip)
493 {
494  return S_ISREG(mode(name)) || (checkGzip && S_ISREG(mode(name + ".gz")));
495 }
496 
497 
498 // Return size of file
500 {
501  fileStat fileStatus(name);
502  if (fileStatus.isValid())
503  {
504  return fileStatus.status().st_size;
505  }
506  else
507  {
508  return -1;
509  }
510 }
511 
512 
513 // Return time of last file modification
515 {
516  fileStat fileStatus(name);
517  if (fileStatus.isValid())
518  {
519  return fileStatus.status().st_mtime;
520  }
521  else
522  {
523  return 0;
524  }
525 }
526 
527 
528 // Read a directory and return the entries as a string list
530 (
531  const fileName& directory,
532  const fileName::Type type,
533  const bool filtergz
534 )
535 {
536  // Initial filename list size
537  // also used as increment if initial size found to be insufficient
538  static const int maxNnames = 100;
539 
540  if (POSIX::debug)
541  {
542  Info<< "readDir(const fileName&, const fileType, const bool filtergz)"
543  << " : reading directory " << directory << endl;
544  }
545 
546  // Setup empty string list MAXTVALUES long
547  fileNameList dirEntries(maxNnames);
548 
549  // Pointers to the directory entries
550  DIR *source;
551  struct dirent *list;
552 
553  // Temporary variables and counters
554  label nEntries = 0;
555 
556  // Attempt to open directory and set the structure pointer
557  if ((source = opendir(directory.c_str())) == NULL)
558  {
559  dirEntries.setSize(0);
560 
561  if (POSIX::debug)
562  {
563  Info<< "readDir(const fileName&, const fileType, "
564  "const bool filtergz) : cannot open directory "
565  << directory << endl;
566  }
567  }
568  else
569  {
570  // Read and parse all the entries in the directory
571  while ((list = readdir(source)) != NULL)
572  {
573  fileName fName(list->d_name);
574 
575  // ignore files begining with ., i.e. '.', '..' and '.*'
576  if (fName.size() && fName[0] != '.')
577  {
578  word fExt = fName.ext();
579 
580  if
581  (
582  (type == fileName::DIRECTORY)
583  ||
584  (
585  type == fileName::FILE
586  && fName[fName.size()-1] != '~'
587  && fExt != "bak"
588  && fExt != "BAK"
589  && fExt != "old"
590  && fExt != "save"
591  )
592  )
593  {
594  if ((directory/fName).type() == type)
595  {
596  if (nEntries >= dirEntries.size())
597  {
598  dirEntries.setSize(dirEntries.size() + maxNnames);
599  }
600 
601  if (filtergz && fExt == "gz")
602  {
603  dirEntries[nEntries++] = fName.lessExt();
604  }
605  else
606  {
607  dirEntries[nEntries++] = fName;
608  }
609  }
610  }
611  }
612  }
613 
614  // Reset the length of the entries list
615  dirEntries.setSize(nEntries);
616 
617  closedir(source);
618  }
619 
620  return dirEntries;
621 }
622 
623 
624 // Copy, recursively if necessary, the source to the destination
625 bool Foam::cp(const fileName& src, const fileName& dest)
626 {
627  // Make sure source exists.
628  if (!exists(src))
629  {
630  return false;
631  }
632 
633  fileName destFile(dest);
634 
635  // Check type of source file.
636  if (src.type() == fileName::FILE)
637  {
638  // If dest is a directory, create the destination file name.
639  if (destFile.type() == fileName::DIRECTORY)
640  {
641  destFile = destFile/src.name();
642  }
643 
644  // Make sure the destination directory exists.
645  if (!isDir(destFile.path()) && !mkDir(destFile.path()))
646  {
647  return false;
648  }
649 
650  // Open and check streams.
651  std::ifstream srcStream(src.c_str());
652  if (!srcStream)
653  {
654  return false;
655  }
656 
657  std::ofstream destStream(destFile.c_str());
658  if (!destStream)
659  {
660  return false;
661  }
662 
663  // Copy character data.
664  char ch;
665  while (srcStream.get(ch))
666  {
667  destStream.put(ch);
668  }
669 
670  // Final check.
671  if (!srcStream.eof() || !destStream)
672  {
673  return false;
674  }
675  }
676  else if (src.type() == fileName::DIRECTORY)
677  {
678  // If dest is a directory, create the destination file name.
679  if (destFile.type() == fileName::DIRECTORY)
680  {
681  destFile = destFile/src.component(src.components().size() -1);
682  }
683 
684  // Make sure the destination directory exists.
685  if (!isDir(destFile) && !mkDir(destFile))
686  {
687  return false;
688  }
689 
690  // Copy files
691  fileNameList contents = readDir(src, fileName::FILE, false);
692  forAll(contents, i)
693  {
694  if (POSIX::debug)
695  {
696  Info<< "Copying : " << src/contents[i]
697  << " to " << destFile/contents[i] << endl;
698  }
699 
700  // File to file.
701  cp(src/contents[i], destFile/contents[i]);
702  }
703 
704  // Copy sub directories.
705  fileNameList subdirs = readDir(src, fileName::DIRECTORY);
706  forAll(subdirs, i)
707  {
708  if (POSIX::debug)
709  {
710  Info<< "Copying : " << src/subdirs[i]
711  << " to " << destFile << endl;
712  }
713 
714  // Dir to Dir.
715  cp(src/subdirs[i], destFile);
716  }
717  }
718 
719  return true;
720 }
721 
722 
723 // Create a softlink. dst should not exist. Returns true if successful.
724 bool Foam::ln(const fileName& src, const fileName& dst)
725 {
726  if (POSIX::debug)
727  {
728  Info<< "Create softlink from : " << src << " to " << dst
729  << endl;
730  }
731 
732  if (exists(dst))
733  {
734  WarningIn("ln(const fileName&, const fileName&)")
735  << "destination " << dst << " already exists. Not linking."
736  << endl;
737  return false;
738  }
739 
740  if (!exists(src))
741  {
742  WarningIn("ln(const fileName&, const fileName&)")
743  << "source " << src << " does not exist." << endl;
744  return false;
745  }
746 
747  if (symlink(src.c_str(), dst.c_str()) == 0)
748  {
749  return true;
750  }
751  else
752  {
753  WarningIn("ln(const fileName&, const fileName&)")
754  << "symlink from " << src << " to " << dst << " failed." << endl;
755  return false;
756  }
757 }
758 
759 
760 // Rename srcFile dstFile
761 bool Foam::mv(const fileName& src, const fileName& dst)
762 {
763  if (POSIX::debug)
764  {
765  Info<< "Move : " << src << " to " << dst << endl;
766  }
767 
768  if
769  (
770  dst.type() == fileName::DIRECTORY
771  && src.type() != fileName::DIRECTORY
772  )
773  {
774  const fileName dstName(dst/src.name());
775 
776  return rename(src.c_str(), dstName.c_str()) == 0;
777  }
778  else
779  {
780  return rename(src.c_str(), dst.c_str()) == 0;
781  }
782 }
783 
784 
785 //- Rename to a corresponding backup file
786 // If the backup file already exists, attempt with "01" .. "99" index
787 bool Foam::mvBak(const fileName& src, const std::string& ext)
788 {
789  if (POSIX::debug)
790  {
791  Info<< "mvBak : " << src << " to extension " << ext << endl;
792  }
793 
794  if (exists(src, false))
795  {
796  const int maxIndex = 99;
797  char index[3];
798 
799  for (int n = 0; n <= maxIndex; n++)
800  {
801  fileName dstName(src + "." + ext);
802  if (n)
803  {
804  sprintf(index, "%02d", n);
805  dstName += index;
806  }
807 
808  // avoid overwriting existing files, except for the last
809  // possible index where we have no choice
810  if (!exists(dstName, false) || n == maxIndex)
811  {
812  return rename(src.c_str(), dstName.c_str()) == 0;
813  }
814 
815  }
816  }
817 
818  // fall-through: nothing to do
819  return false;
820 }
821 
822 
823 
824 // Remove a file, returning true if successful otherwise false
825 bool Foam::rm(const fileName& file)
826 {
827  if (POSIX::debug)
828  {
829  Info<< "Removing : " << file << endl;
830  }
831 
832  // Try returning plain file name; if not there, try with .gz
833  if (remove(file.c_str()) == 0)
834  {
835  return true;
836  }
837  else
838  {
839  return remove(string(file + ".gz").c_str()) == 0;
840  }
841 }
842 
843 
844 // Remove a dirctory and its contents
845 bool Foam::rmDir(const fileName& directory)
846 {
847  if (POSIX::debug)
848  {
849  Info<< "rmDir(const fileName&) : "
850  << "removing directory " << directory << endl;
851  }
852 
853  // Pointers to the directory entries
854  DIR *source;
855  struct dirent *list;
856 
857  // Attempt to open directory and set the structure pointer
858  if ((source = opendir(directory.c_str())) == NULL)
859  {
860  WarningIn("rmDir(const fileName&)")
861  << "cannot open directory " << directory << endl;
862 
863  return false;
864  }
865  else
866  {
867  // Read and parse all the entries in the directory
868  while ((list = readdir(source)) != NULL)
869  {
870  fileName fName(list->d_name);
871 
872  if (fName != "." && fName != "..")
873  {
874  fileName path = directory/fName;
875 
876  if (path.type() == fileName::DIRECTORY)
877  {
878  if (!rmDir(path))
879  {
880  WarningIn("rmDir(const fileName&)")
881  << "failed to remove directory " << fName
882  << " while removing directory " << directory
883  << endl;
884 
885  closedir(source);
886 
887  return false;
888  }
889  }
890  else
891  {
892  if (!rm(path))
893  {
894  WarningIn("rmDir(const fileName&)")
895  << "failed to remove file " << fName
896  << " while removing directory " << directory
897  << endl;
898 
899  closedir(source);
900 
901  return false;
902  }
903  }
904  }
905 
906  }
907 
908  if (!rm(directory))
909  {
910  WarningIn("rmDir(const fileName&)")
911  << "failed to remove directory " << directory << endl;
912 
913  closedir(source);
914 
915  return false;
916  }
917 
918  closedir(source);
919 
920  return true;
921  }
922 }
923 
924 
925 unsigned int Foam::sleep(const unsigned int s)
926 {
927  return ::sleep(s);
928 }
929 
930 
931 void Foam::fdClose(const int fd)
932 {
933  if (close(fd) != 0)
934  {
936  (
937  "fdClose(const int fd)"
938  ) << "close error on " << fd << endl
939  << abort(FatalError);
940  }
941 }
942 
943 
944 bool Foam::ping
945 (
946  const word& destName,
947  const label destPort,
948  const label timeOut
949 )
950 {
951  char *serverAddress;
952  struct in_addr *ptr;
953  struct hostent *hostPtr;
954  volatile int sockfd;
955  struct sockaddr_in destAddr; // will hold the destination addr
956  u_int addr;
957 
958  if ((hostPtr = gethostbyname(destName.c_str())) == NULL)
959  {
961  (
962  "Foam::ping(const word&, const label)"
963  ) << "gethostbyname error " << h_errno << " for host " << destName
964  << abort(FatalError);
965  }
966 
967  // Get first of the SLL of addresses
968  serverAddress = *(hostPtr->h_addr_list);
969  ptr = reinterpret_cast<struct in_addr*>(serverAddress);
970  addr = ptr->s_addr;
971 
972  // Allocate socket
973  sockfd = socket(AF_INET, SOCK_STREAM, 0);
974  if (sockfd < 0)
975  {
977  (
978  "Foam::ping(const word&, const label)"
979  ) << "socket error"
980  << abort(FatalError);
981  }
982 
983  // Fill sockaddr_in structure with dest address and port
984  memset (reinterpret_cast<char *>(&destAddr), '\0', sizeof(destAddr));
985  destAddr.sin_family = AF_INET;
986  destAddr.sin_port = htons(ushort(destPort));
987  destAddr.sin_addr.s_addr = addr;
988 
989 
990  timer myTimer(timeOut);
991 
992  if (timedOut(myTimer))
993  {
994  // Setjmp from timer jumps back to here
995  fdClose(sockfd);
996  return false;
997  }
998 
999  if
1000  (
1001  connect
1002  (
1003  sockfd,
1004  reinterpret_cast<struct sockaddr*>(&destAddr),
1005  sizeof(struct sockaddr)
1006  ) != 0
1007  )
1008  {
1009  // Connection refused. Check if network was actually used or not.
1010 
1011  int connectErr = errno;
1012 
1013  fdClose(sockfd);
1014 
1015  if (connectErr == ECONNREFUSED)
1016  {
1017  return true;
1018  }
1019  //perror("connect");
1020 
1021  return false;
1022  }
1023 
1024  fdClose(sockfd);
1025 
1026  return true;
1027 }
1028 
1029 
1030 bool Foam::ping(const word& hostname, const label timeOut)
1031 {
1032  return ping(hostname, 222, timeOut) || ping(hostname, 22, timeOut);
1033 }
1034 
1035 
1036 int Foam::system(const string& command)
1037 {
1038  return ::system(command.c_str());
1039 }
1040 
1041 
1042 // ************************ vim: set sw=4 sts=4 et: ************************ //