;;; POSIX system-call Scheme binding.
;;; Copyright (c) 1993 by Olin Shivers.

;;; Scheme48 implementation.

;;; Need to rationalise names here. getgid. get-gid. "effective" as morpheme?

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Import a C function and convert the exception os-error to a syscall-error
;;;
;;; 1.) Import a C function
;;; 2.) Turn os-error into syscall-error
;;; 3.) Retry on EINTR
;;; The call/cc and the record is due to S48's broken exception system:
;;; You can't throw an error within a handler
;;;

;;; Move this to somewhere else as soon as Marc published his SRFI
(define (continuation-capture receiver)
  ((call-with-current-continuation
    (lambda (cont)
      (lambda () (receiver cont))))))

(define (continuation-graft cont thunk)
  (cont thunk))

(define (continuation-return cont . returned-values)
  (continuation-graft
   cont
   (lambda () (apply values returned-values))))

(define-syntax import-os-error-syscall
  (syntax-rules ()
    ((import-os-error-syscall syscall (%arg ...) c-name)
     (begin 
       (import-lambda-definition syscall/eintr (%arg ...) c-name)
       (define (syscall %arg ...)
	 (let ((arg %arg) ...)
	   (continuation-capture 
	    (lambda (cont)
	      (let loop ()
		(with-handler
		 (lambda (condition more)
		   (if (and (exception? condition) (eq? (exception-reason condition) 
							'os-error))
		       (let ((stuff (exception-arguments condition)))
			 (if (= (cadr stuff) errno/intr)
			     (loop)
			     (continuation-graft
			      cont
			      (lambda ()
				(apply errno-error 
				       (cadr stuff)   ; errno
				       (caddr stuff)  ;msg
				       syscall 
				       (cdddr stuff)))))) ;packet
		       (more)))
		 (lambda ()
		   (syscall/eintr %arg ...))))))))))))
  
;;; Process
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; we can't algin env here, because exec-path/env calls
;; %%exec/errno directly  F*&% *P
(import-os-error-syscall %%exec (prog argv env) "scheme_exec")

(define (%exec prog arg-list env)
  (let ((argv (mapv! stringify (list->vector arg-list)))
	(prog (stringify prog))
	(env (if (eq? env #t) #t (alist->env-vec env))))
    (%%exec prog argv env)))


(import-os-error-syscall exit/errno ; errno -- misnomer.
  (status) "scsh_exit")

(import-os-error-syscall %exit/errno ; errno -- misnomer
  (status) "scsh__exit")

(define (%exit . maybe-status)
  (%exit/errno (:optional maybe-status 0))
  (error "Yikes! %exit returned."))


(import-os-error-syscall %%fork () "scsh_fork")

;;; Posix waitpid(2) call.
(import-os-error-syscall %wait-pid/list (pid options) "wait_pid")

(define (%wait-pid pid options)
  (apply values (%wait-pid/list pid options)))

;;; Miscellaneous process state
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Working directory

(import-os-error-syscall %chdir (directory) "scsh_chdir")

;;; These calls change/reveal the process working directory
;;; 

(define (process-chdir . maybe-dir)
  (let ((dir (:optional maybe-dir (home-dir))))
    (%chdir (ensure-file-name-is-nondirectory dir))))

;; TODO: we get an error if cwd does not exist on startup
(import-os-error-syscall process-cwd () "scheme_cwd")

;;; GID

(import-os-error-syscall user-gid  () "scsh_getgid")

(import-os-error-syscall user-effective-gid () "scsh_getegid")

(import-os-error-syscall set-gid (gid) "scsh_setgid")

(import-os-error-syscall set-effective-gid (gid) "scsh_setegid")

(import-os-error-syscall user-supplementary-gids () "get_groups")

;;; UID
(import-os-error-syscall user-uid  () "scsh_getuid")

(import-os-error-syscall user-effective-uid () "scsh_geteuid")

(import-os-error-syscall set-uid (uid) "scsh_setuid")

(import-os-error-syscall set-effective-uid (uid) "scsh_seteuid")

(import-os-error-syscall %user-login-name () "my_username")
 
(define (user-login-name)
  (or (%user-login-name)
      (error "Cannot get your name")))

;;; PID

(import-os-error-syscall pid () "scsh_getpid")
(import-os-error-syscall parent-pid () "scsh_getppid")
;;; Process groups and session ids
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(import-os-error-syscall process-group () "scsh_getpgrp")

(import-os-error-syscall %set-process-group (pid groupid) "scsh_setpgid")

(define (set-process-group arg1 . maybe-arg2)
  (receive (pid pgrp) (if (null? maybe-arg2)
			  (values (pid) arg1)
			  (values arg1 (car maybe-arg2)))
	   (%set-process-group pid pgrp)))


(import-os-error-syscall become-session-leader () "scsh_setsid")

;;; UMASK

(import-os-error-syscall set-process-umask (mask) "scsh_umask")

(define (process-umask)
  (let ((m (set-process-umask 0)))
    (set-process-umask m)
    m))


;;; PROCESS TIMES

;;; OOPS: The POSIX times() has a mildly useful ret value we are throwing away.


(import-os-error-syscall process-times/list () "process_times")

(define (process-times)
  (apply values (process-times/list)))

(import-os-error-syscall cpu-ticks/sec () "cpu_clock_ticks_per_sec")

;;; File system
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Useful little utility for generic ops that work on filenames, fd's or
;;; ports.

(define (generic-file-op thing fd-op fname-op)
  (if (string? thing) (with-cwd-aligned (fname-op thing))
      (call/fdes thing fd-op)))


(import-os-error-syscall %set-file-mode (path mode) "scsh_chmod")

(import-os-error-syscall %set-fdes-mode (path mode) "scsh_fchmod")

(define (set-file-mode thing mode)
  (generic-file-op thing
		   (lambda (fd)    (%set-fdes-mode fd    mode))
		   (lambda (fname) (%set-file-mode fname mode))))


(import-os-error-syscall set-file-uid&gid (path uid gid) "scsh_chown")

(import-os-error-syscall set-fdes-uid&gid (fd uid gid) "scsh_fchown")

(define (set-file-owner thing uid)
  (generic-file-op thing
		   (lambda (fd)    (set-fdes-uid&gid fd    uid -1))
		   (lambda (fname) (set-file-uid&gid fname uid -1))))

(define (set-file-group thing gid)
  (generic-file-op thing
		   (lambda (fd)    (set-fdes-uid&gid fd    -1 gid))
		   (lambda (fname) (set-file-uid&gid fname -1 gid))))


;;; Uses real uid and gid, not effective. I don't use this anywhere.

(import-os-error-syscall %file-ruid-access-not? (path perms) "scsh_access")

;(define (file-access? path perms)
;  (not (%file-access-not? path perms)))
;
;(define (file-executable? fname)
;  (file-access? fname 1))
;
;(define (file-writable? fname)
;  (file-access? fname 2))
;
;(define (file-readable? fname)
;  (file-access? fname 4))


(import-os-error-syscall %create-hard-link (original-name new-name) 
  "scsh_link")

(import-os-error-syscall %create-fifo (path mode) "scsh_mkfifo")

(import-os-error-syscall %%create-directory (path mode) "scsh_mkdir")

(define (%create-directory path . maybe-mode)
  (let ((mode (:optional maybe-mode #o777))
	(fname (ensure-file-name-is-nondirectory path)))
    (%%create-directory fname mode)))

(import-os-error-syscall read-symlink (path) "scsh_readlink")

(import-os-error-syscall %rename-file (old-name new-name) "scsh_rename")

(import-os-error-syscall delete-directory (path) "scsh_rmdir")

(import-os-error-syscall %utime (path ac m) "scm_utime") 

(import-os-error-syscall %utime-now (path) "scm_utime_now")

;;; (SET-FILE-TIMES path [access-time mod-time])

(define (set-file-times path . maybe-times)
  (if (pair? maybe-times)
      (let* ((access-time (real->exact-integer (car maybe-times)))
	     (mod-time (if (pair? (cddr maybe-times))
			   (error "Too many arguments to set-file-times/errno"
				  (cons path maybe-times))
			   (real->exact-integer (cadr maybe-times)))))
	(%utime path access-time 
		           mod-time ))
      (%utime-now path)))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; STAT

(import-os-error-syscall stat-file (path data chase?) "scheme_stat")

(import-os-error-syscall stat-fdes (fd data) "scheme_fstat")

(define-record file-info
  type
  device
  inode
  mode
  nlinks
  uid
  gid
  size
  atime
  mtime
  ctime
  )


;;; Should be redone to return multiple-values.
(define (%file-info fd/port/fname chase?)
  (let ((ans-vec (make-vector 11))
	(file-type (lambda (type-code)
		     (vector-ref '#(block-special char-special directory fifo
						  regular socket symlink)
				 type-code))))
    (generic-file-op fd/port/fname
		     (lambda (fd)
		       (stat-fdes fd ans-vec))
		     (lambda (fname)
		       (stat-file fname ans-vec chase?)))
    (make-file-info (file-type (vector-ref ans-vec 0))
		    (vector-ref ans-vec 1)
		    (vector-ref ans-vec 2)
		    (vector-ref ans-vec 3)
		    (vector-ref ans-vec 4)
		    (vector-ref ans-vec 5)
		    (vector-ref ans-vec 6)
		    (vector-ref ans-vec 7)
		    (vector-ref ans-vec 8)
		    (vector-ref ans-vec 9)
		    (vector-ref ans-vec 10))))
						      
		
(define (file-info fd/port/fname . maybe-chase?)
 (with-cwd-aligned
   (let ((chase? (:optional maybe-chase? #t)))
     (%file-info fd/port/fname chase?))))


(define file-attributes
  (deprecated-proc file-info "file-attributes" "Use file-info instead."))


;;; "no-declare" as there is no agreement among the OS's as to whether or not
;;; the OLD-NAME arg is "const". It *should* be const.

(import-os-error-syscall %create-symlink (old-name new-name) "scsh_symlink")
  
;;; "no-declare" as there is no agreement among the OS's as to whether or not
;;; the PATH arg is "const". It *should* be const.

(import-os-error-syscall %truncate-file (path length) "scsh_truncate")

(import-os-error-syscall %truncate-fdes (path length) "scsh_ftruncate")

(define (truncate-file thing length)
  (generic-file-op thing
		   (lambda (fd)    (%truncate-fdes fd    length))
		   (lambda (fname) (%truncate-file fname length))))

(import-os-error-syscall %delete-file (path) "scsh_unlink")

(define (delete-file path)
  (with-cwd-aligned (%delete-file path)))

(import-os-error-syscall %sync-file (fd) "scsh_fsync")

(define (sync-file fd/port)
  (if (output-port? fd/port) (force-output fd/port))
  (sleazy-call/fdes fd/port %sync-file))


;;; Amazingly bogus syscall -- doesn't *actually* sync the filesys.
(import-os-error-syscall sync-file-system () "scsh_sync")

;;; I/O
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(import-os-error-syscall %close-fdes (fd) "scsh_close")

(import-os-error-syscall %dup (fd) "scsh_dup")

(import-os-error-syscall %dup2 (fd-from fd-to) "scsh_dup2")

(import-os-error-syscall %fd-seek (fd offset whence) "scsh_lseek")


(define seek/set 0)			;Unix codes for "whence"
(define seek/delta 1)
(define seek/end 2)

(define (seek fd/port offset . maybe-whence)
  (let ((whence (:optional maybe-whence seek/set))
	(fd (if (integer? fd/port) fd/port (port->fdes fd/port))))
    (%fd-seek fd offset whence)))

(define (tell fd/port)
  (let ((fd (if (integer? fd/port) fd/port (port->fdes fd/port))))
    (%fd-seek fd 0 seek/delta)))

(import-os-error-syscall %char-ready-fdes? (fd) "char_ready_fdes")

(import-os-error-syscall %open (path flags mode) "scsh_open")

(define (open-fdes path flags . maybe-mode) ; mode defaults to 0666
    (with-cwd-aligned
     (with-umask-aligned
      (%open path flags (:optional maybe-mode #o666)))))


(import-os-error-syscall pipe-fdes () "scheme_pipe")

(define (pipe)
  (apply (lambda (r-fd w-fd)
	   (let ((r (fdes->inport  r-fd))
		 (w (fdes->outport w-fd)))
	     (release-port-handle r)
	     (release-port-handle w)
	     (values r w)))
	 (pipe-fdes)))

;;; Signals (rather incomplete)
;;; ---------------------------

(import-os-error-syscall signal-pid (pid signal) "scsh_kill")

(define (signal-process proc signal)
  (signal-pid (cond ((proc? proc)    (proc:pid proc))
		    ((integer? proc) proc)
		    (else (error "Illegal proc passed to signal-process" proc)))
	      signal))

(define (signal-process-group proc-group signal)
  (signal-pid (- (cond ((proc? proc-group)    (proc:pid proc-group))
		       ((integer? proc-group) proc-group)
		       (else (error "Illegal proc passed to signal-process-group"
                                    proc-group))))
              signal))
(define (itimer sec)
  ((structure-ref sigevents schedule-timer-interrupt!) (* sec 1000)))
;;; SunOS, not POSIX:
;;; (define-foreign signal-process-group/errno
;;;   (killpg (integer proc-group) (integer signal))
;;;   (to-scheme integer errno_or_false))
;;; 
;;; (define-errno-syscall (signal-process-group proc-group signal)
;;;   signal-process-group/errno)

(define (pause-until-interrupt)
  (next-sigevent (most-recent-sigevent) full-interrupt-set))


;;; User info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define-record user-info
  name uid gid home-dir shell

  ;; Make user-info records print like #{user-info shivers}.
  ((disclose ui)
   (list "user-info" (user-info:name ui))))


(import-os-error-syscall
 %uid->user-info 
 (uid user-info-record)
 "user_info_uid")

(import-os-error-syscall
 %name->user-info 
 (name user-info-record)
 "user_info_name")

(define (uid->user-info uid)
  (let ((empty-user-info (make-user-info #f uid #f #f #f)))
    (if (%uid->user-info uid empty-user-info)
	empty-user-info
	(error "Cannot get user's information" uid->user-info uid)))) 


(define (name->user-info name)
  (let ((empty-user-info (make-user-info name #f #f #f #f)))
    (if (%name->user-info name empty-user-info)
	empty-user-info
	(error "Cannot get user's information" name->user-info name))))

(define (user-info uid/name)
  ((cond ((string?  uid/name) name->user-info)
	 ((integer? uid/name) uid->user-info)
	 (else (error "user-info arg must be string or integer" uid/name)))
   uid/name))

;;; Derived functions

(define (->uid uid/name)
  (user-info:uid (user-info uid/name)))

(define (->username uid/name)
  (user-info:name (user-info uid/name)))

(define (%homedir uid/name)
  (user-info:home-dir (user-info uid/name)))


;;; Group info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(define-record group-info
  name gid members

  ;; Make group-info records print like #{group-info wheel}.
  ((disclose gi) (list "group-info" (group-info:name gi))))

(import-os-error-syscall
 %gid->group-info 
 (gid group-info-record)
 "group_info_gid")

(import-os-error-syscall
 %name->group-info 
 (name group-info-record)
 "group_info_name")

(define (gid->group-info  gid)
  (let ((empty-group-info (make-group-info #f gid #f)))
    (if (%gid->group-info gid empty-group-info)
	empty-group-info
	(error "Cannot get group's information for gid" gid))))
							
(define (name->group-info name)
  (let ((empty-group-info (make-group-info name #f #f)))
    (if (%name->group-info name empty-group-info)
	empty-group-info
	(error "Cannot get group's information for name" name))))

(define (group-info gid/name)
  ((cond ((string?  gid/name) name->group-info)
	 ((integer? gid/name) gid->group-info)
	 (else (error "group-info arg must be string or integer" gid/name)))
   gid/name))

;;; Derived functions

(define (->gid name)
  (group-info:gid (group-info name)))

(define (->groupname gid)
  (group-info:name (group-info gid)))

;;; Directory stuff
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(import-os-error-syscall %open-dir (dir-name) "open_dir")

(define (directory-files . args)
  (with-cwd-aligned
   (let-optionals args ((dir       ".")
			(dotfiles? #f))
    (check-arg string? dir directory-files)
    (let* ((files (%open-dir (ensure-file-name-is-nondirectory dir)))
	   (files-sorted ((structure-ref sort sort-list!) files filename<=)))
      (if dotfiles? files-sorted
	  (filter (lambda (f) (not (dotfile? f)))
		  files-sorted))))))

(define (dotfile? f)
  (char=? (string-ref f 0) #\.))

(define (filename<= f1 f2)
  (if (dotfile? f1)
      (if (dotfile? f2)
	  (string<= f1 f2)
	  #t)
      (if (dotfile? f2)
	  #f
	  (string<= f1 f2))))

;;; I do this one in C, I'm not sure why:
;;; It is used by MATCH-FILES.
;;; 99/7: No one is using this function, so I'm commenting it out.
;;; Later, we could tune up the globber or regexp file-matcher to use
;;; it (but should shift it into the rx directory). But I should also go
;;; to a file-at-a-time cursor model for directory fetching. -Olin

;(define-foreign %filter-C-strings!
;  (filter_stringvec (string pattern) ((C "char const ** ~a") cvec))
;  static-string	; error message -- #f if no error.
;  integer)	; number of files that pass the filter.


;(define (match-files regexp . maybe-dir)
;  (let ((dir (:optional maybe-dir ".")))
;    (check-arg string? dir match-files)
;    (receive (err cvec numfiles)
;	     (%open-dir (ensure-file-name-is-nondirectory dir))
;      (if err (errno-error err match-files regexp dir))
;      (receive (err numfiles) (%filter-C-strings! regexp cvec)
;	(if err (error err match-files))
;	(%sort-file-vector cvec numfiles)
;	(let ((files (C-string-vec->Scheme&free cvec numfiles)))
;	  (vector->list files))))))


;;; Environment manipulation
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; (var . val) / "var=val" rep conversion:

 

(define (split-env-string var=val)
  (let ((i (string-index var=val #\=)))
    (if i (values (substring var=val 0 i)
		  (substring var=val (+ i 1) (string-length var=val)))
	(error "No \"=\" in environment string" var=val))))

(define (env-list->alist env-list)
  (map (lambda (var=val)
	 (call-with-values (lambda () (split-env-string var=val))
			   cons))
       env-list))

(define (alist->env-vec alist)
  (list->vector (map (lambda (var.val)
		       (string-append (car var.val) "="
				      (let ((val (cdr var.val)))
					(if (string? val) val
					    (string-join val ":")))))
		     alist)))

;;; ENV->ALIST

(import-os-error-syscall %load-env () "scm_envvec")

(define (environ-env->alist) 
  (let ((env-list.envvec (%load-env)))
    (cons (env-list->alist (car env-list.envvec)) 
	  (cdr env-list.envvec))))

	   
;;; ALIST->ENV

;;; (%create-env ((vector 'X) -> address))
(import-os-error-syscall %create-env (envvec) "create_env")

;;; assumes aligned env
(define (envvec-alist->env alist)
  (%create-env (alist->env-vec alist)))

(import-os-error-syscall %align-env (envvec) "align_env")

(import-os-error-syscall %free-env (envvec) "free_envvec")


;;; Fd-ports
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(import-os-error-syscall %set-cloexec (fd val) "set_cloexec")

;;; Some of fcntl()
;;;;;;;;;;;;;;;;;;;

(import-os-error-syscall %fcntl-read (fd command) "fcntl_read")
(import-os-error-syscall %fcntl-write (fd command val) "fcntl_write")

;;; fcntl()'s F_GETFD and F_SETFD.  Note that the SLEAZY- prefix on the
;;; CALL/FDES isn't an optimisation; it's *required* for the correct behaviour
;;; of these procedures. Straight CALL/FDES modifies unrevealed file
;;; descriptors by clearing their CLOEXEC bit when it reveals them -- so it
;;; would interfere with the reading and writing of that bit!

(define (fdes-flags fd/port)
  (sleazy-call/fdes fd/port
    (lambda (fd) (%fcntl-read fd fcntl/get-fdes-flags))))

(define (set-fdes-flags fd/port flags)
  (sleazy-call/fdes fd/port
    (lambda (fd) (%fcntl-write fd fcntl/set-fdes-flags flags))))

;;; fcntl()'s F_GETFL and F_SETFL.
;;; Get: Returns open flags + get-status flags (below)
;;; Set: append, sync, async, nbio, nonblocking, no-delay

(define (fdes-status fd/port)
  (sleazy-call/fdes fd/port
    (lambda (fd) (%fcntl-read fd fcntl/get-status-flags))))

(define (set-fdes-status fd/port flags)
  (sleazy-call/fdes fd/port
    (lambda (fd) (%fcntl-write fd fcntl/set-status-flags flags))))

;;; Miscellaneous
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; usleep(3): Try to sleep for USECS microseconds.
;;; sleep(3):  Try to sleep for SECS seconds.

; De-released -- not POSIX and not on SGI systems.
; (define-foreign usleep (usleep (integer usecs)) integer)

(define (process-sleep secs) (process-sleep-until (+ secs (time))))

(define (process-sleep-until when)
  (let* ((when (floor when))	; Painful to do real->int in Scheme.
	 (when (if (exact? when) when (inexact->exact when))))
    (let lp ()
      (or (%sleep-until when) (lp)))))

(import-os-error-syscall %sleep-until (secs) "sleep_until")

(import-os-error-syscall %gethostname () "scm_gethostname")

(define system-name %gethostname)

(import-os-error-syscall errno-msg (i) "errno_msg")

(import-os-error-syscall %crypt (key salt) "scm_crypt")

(define (crypt key salt)
  (let* ((allowed-char-set  (rx (| alpha digit "." "/")))
	 (salt-regexp  (rx (: ,allowed-char-set ,allowed-char-set))))
    (if (not (= (string-length salt) 2)) (error "salt must have length 2"))
    (if (not (regexp-search? salt-regexp salt))
	(error "illegal char in salt " salt))
    (if (> (string-length key) 8) (error "key too long "  (string-length key)))
    (%crypt key salt)))
