// Scavenger.bcpl -- Alto file system scavenger
// Copyright Xerox Corporation 1979, 1980, 1981, 1982
// Last modified May 12, 1982  1:21 AM by Boggs

// Design and construction: James H. Morris, 1974.
// Maintenance: Richard K. Johnsson, 1976-1981.
// Renovation: David R. Boggs, 1982.

get "AltoDefs.d"
get "AltoFileSys.d"
get "Streams.d"
get "SysDefs.d"
get "Disks.d"
get "BFS.d"
get "Scavenger.decl"

external
[
// outgoing procedures
SplitPuts

// incoming procedures
InitScavenger
FixDirs; ScanDisk; ForAllFDs
Confirm; GetBit; Ws; WouldHave
StartLog; StopLog; XferPage; TryDisk

Endofs; Gets; Puts; Closes; Resets
BFSInit; CloseDisk; VirtualDiskDA; RealDiskDA
OpenFile; ReadBlock; WriteBlock; TruncateDiskStream
PositionPage; SetFilePos; GetCompleteFa; FileLength
MoveBlock; Zero; DisableInterrupts; DoubleAdd; RetryCall; Noop
AddToZone; Allocate; Free; Enqueue; Dequeue; Junta; CallSwat
LoadRam; InitBcplRuntime; EtherBoot; PutTemplate; SimpleDspPuts
InitializeFstream; SetupFstream; CurrentPos

// outgoing statics
log; logFlag; pt; bt

// incoming statics
sysZone; sysDisk; keys; dsp
maxVDA; alterFlag; label; data
usedBT; badBT
RamImage
]

static
[
log; logFlag; pt; bt
lenHashTab; hashTab; ramFlag
]

//-----------------------------------------------------------------------------------------
let Scavenger(blv, upe, cfa) be
//-----------------------------------------------------------------------------------------
[
ramFlag = LoadRam(RamImage) eq 0
if ramFlag then InitBcplRuntime()
Junta((ramFlag? levBuffer, levBcpl), AfterJunta)
]

//-----------------------------------------------------------------------------------------
and AfterJunta() be
//-----------------------------------------------------------------------------------------
[
InitScavenger()
let freeEnd = sysZone -1  //ahem...
let freeBegin = ramFlag? InitScavenger, InitBcplRuntime
AddToZone(sysZone, freeBegin, freeEnd-freeBegin)
pt = Allocate(sysZone, maxVDA+1)
bt = Allocate(sysZone, (maxVDA/16+1)*2)
   [
   ScanDisk()
   FixSNs()
   FixDirs()
   if alterFlag then
      [
      FixDD()
      CloseDisk(sysDisk)
      sysDisk = BFSInit(sysZone, true)
      if sysDisk eq 0 then CallSwat("BFSInit failed")
      ]
   StopLog()
   FixGarbage()
   let message = logFlag? (log? "See Scavenger.Log$", "Errors found"),
    "You have a beautiful file system!"
   if log ne 0 then [ Closes(log); log = 0 ]
   PutTemplate(dsp, "*N$S", message)
   if alterFlag then WriteRemCm(message)
   unless logFlag & not alterFlag do Finish()
   unless Confirm("*NDo it again, altering the disk to correct errors? ") do Finish()
   alterFlag, logFlag, log = true, false, StartLog()
   ] repeat
]

//-----------------------------------------------------------------------------------------
and FixDD() be
//-----------------------------------------------------------------------------------------
[
let dd = OpenFile("DiskDescriptor")
SetFilePos(dd, 0, lKDHeader+sysDisk>>BFSDSK.diskBTsize)
TruncateDiskStream(dd)  //so Closes won't truncate
Resets(dd)
WriteBlock(dd, sysDisk>>DSK.diskKd, lKDHeader)
WriteBlock(dd, usedBT, sysDisk>>BFSDSK.diskBTsize)
Closes(dd)
]

//-----------------------------------------------------------------------------------------
and WriteRemCm(message) be
//-----------------------------------------------------------------------------------------
[
let fp = vec lFP; Zero(fp, lFP)
let out = OpenFile("Rem.Cm", ksTypeWriteOnly, charItem, 0, fp)
let in = OpenFile("Rem.Cm", ksTypeReadOnly, charItem, 0, fp)
PutTemplate(out, "// $S*N", message)
until Endofs(in) do Puts(out, Gets(in))
Closes(in)
Closes(out)
]

//-----------------------------------------------------------------------------------------
and FixSNs() be
//-----------------------------------------------------------------------------------------
// This routine insures that the serial numbers of all files
//  are different by re-writing label blocks if necessary.
// It also updates sysDisk's lastSN to be the largest SN of any file.
[
lenHashTab = 1
ForAllFDs(FindMaxSN)
hashTab = Allocate(sysZone, lenHashTab)
Zero(hashTab, lenHashTab)
ForAllFDs(ChangeSN)
Free(sysZone, hashTab)
]

//-----------------------------------------------------------------------------------------
and FindMaxSN(fd, nil) be
//-----------------------------------------------------------------------------------------
[
lenHashTab = lenHashTab +2
if sysDisk>>BFSDSK.lastSn.part1 gr fd>>FD.sn.part1 return
if sysDisk>>BFSDSK.lastSn.part1 eq fd>>FD.sn.part1 &
   sysDisk>>BFSDSK.lastSn.word2 uge fd>>FD.sn.word2 return
sysDisk>>BFSDSK.lastSn.part1 = fd>>FD.sn.part1
sysDisk>>BFSDSK.lastSn.word2 = fd>>FD.sn.word2
]

//-----------------------------------------------------------------------------------------
and ChangeSN(fd, nil) be
//-----------------------------------------------------------------------------------------
[
let ha = (fd>>FD.sn.word2 & 77777b) rem lenHashTab
   [
   if hashTab!ha eq 0 then [ hashTab!ha = fd; return ]
   if (hashTab!ha)>>FD.sn.word1 eq fd>>FD.sn.word1 &
      (hashTab!ha)>>FD.sn.word2 eq fd>>FD.sn.word2 break
   ha = ha eq lenHashTab-1? 0, ha+1  //collision; reprobe
   ] repeat

// fd is a duplicate SN
DoubleAdd(lv sysDisk>>BFSDSK.lastSn, table [ 0; 1 ])
sysDisk>>BFSDSK.lastSn.directory = 0  //make sure
MoveBlock(lv fd>>FD.sn, lv sysDisk>>BFSDSK.lastSn, lSN)

// change the file's labels
let vda = fd>>FD.firstVDA
   [
   XferPage(DCreadLD, vda)
   if label>>DL.pageNumber eq 0 then
      [
      logFlag = true
      PutTemplate(dsp, "*N$PChanged SerialNumber of $S from $EUOb to $EUOb.",
       WouldHave, nil, lv data>>LD.name, lv label>>DL.fileId.serialNumber, lv fd>>FD.sn)
      ]
   MoveBlock(lv label>>DL.fileId.serialNumber, lv fd>>FD.sn, lSN)
   label>>DL.fileId.version = 1
   if alterFlag then XferPage(DCwriteLD, vda)
   vda = VirtualDiskDA(sysDisk, lv label>>DL.next)
   ] repeatuntil vda eq eofDA
]


//-----------------------------------------------------------------------------------------
and FixGarbage() be
//-----------------------------------------------------------------------------------------
[

let nextBadDA = 0
for vda = 1 to maxVDA do
   if GetBit(badBT, vda) then [ nextBadDA = vda; break ]
unless nextBadDA ne 0 return

let lastGDA, prevGDA = 0, 0
if alterFlag then
   [
   // Capture address of first page
   let stream = OpenFile("Scavenger.Garbage$")
   let cfa = vec lCFA; GetCompleteFa(stream, cfa)
   Closes(stream)

   // Find last page of Scavenger.Garbage$
   lastGDA = cfa>>CFA.fp.leaderVirtualDa

      [
      XferPage(DCreadLD, lastGDA)
      if label>>DL.next eq 0 break
      lastGDA = VirtualDiskDA(sysDisk, lv label>>DL.next)
      ] repeat

   prevGDA = VirtualDiskDA(sysDisk, lv label>>DL.previous)

   
// Fix previous page's forward link
   XferPage(DCreadLD, prevGDA)
   RealDiskDA(sysDisk, nextBadDA, lv label>>DL.next)
   XferPage(DCwriteLD, prevGDA)
   ]


// Splice in the bad pages
PutTemplate(dsp, "*N$PAdded these pages to Scavenger.Garbage$$:", WouldHave, nil)
Ws("*NVDA       New PN   Old PN   Old SN")
let prevBadDA = prevGDA
let currBadDA = nextBadDA
let oldLabel = vec lDL

   [
   XferPage(DCreadLD, currBadDA, 0, oldLabel)
   PutTemplate(dsp, "*N$U5Ob    $U5D    $U5D    $EUOb",
    currBadDA, label>>DL.pageNumber+1, oldLabel>>DL.pageNumber,
    lv oldLabel>>DL.fileId.serialNumber)

   RealDiskDA(sysDisk, prevBadDA, lv label>>DL.previous)
      [
      nextBadDA = nextBadDA +1
      if nextBadDA gr maxVDA then
         [ RealDiskDA(sysDisk, lastGDA, lv label>>DL.next); break ]
      if GetBit(badBT, nextBadDA) then
         [ RealDiskDA(sysDisk, nextBadDA, lv label>>DL.next); break ]
      ] repeat

   label>>DL.pageNumber = label>>DL.pageNumber +1

   if alterFlag then XferPage(DCwriteLD, currBadDA)
   prevBadDA = currBadDA
   currBadDA = nextBadDA
   ] repeatuntil currBadDA gr maxVDA

// Adjust label in last page of Scavenger.Garbage$
label>>DL.next = 0

RealDiskDA(sysDisk, prevBadDA, lv label>>DL.previous)
label>>DL.pageNumber = label>>DL.pageNumber +1
label>>DL.numChars = 0
if alterFlag then XferPage(DCwriteLD, lastGDA)
]

//-----------------------------------------------------------------------------------------
and Finish() be
//-----------------------------------------------------------------------------------------
[
let sys = CheckVitalFile("Sys.boot", 256)
let exec = CheckVitalFile("Executive.run", 100)
let font = CheckVitalFile("SysFont.al", 4)
if alterFlag & sys ne 0 then  //Reinstall sys.boot
   [ XferPage(DCreadLD, sys); XferPage(DCwriteHLD, 0) ]
if sysDisk ne 0 then CloseDisk(sysDisk)

if exec eq 0 % font eq 0 % sys eq 0 then
   [
   Resets(dsp)
   Ws("*NIt's unlikely this disk will boot.  Some vital files are missing or damaged.")
   Ws("*NFollowing each file name are steps outlining a way to recover.")
   unless sys do  Ws("*N   Sys.boot: 1)NetExec 2)NewOS 3)You need to Install.")
   unless exec do Ws("*N   Executive.run: 1)NetExec 2)Ftp 3)Retrieve 'Executive.run'.")
   unless font do Ws("*N   SysFont.al: 1)NetExec 2)Ftp 3)Retrieve a font as 'SysFont.al'.")
   if Confirm("*NShall I get the NetExec? ") then EtherBoot(10b)
   Ws("*NType any character to try booting from the disk anyway:")
   Gets(keys)
   ]
Closes(dsp)

// Boot the OS from the disk without using SIO.
// This is how the microcode does it.
DisableInterrupts()
@displayListHead = 0; for i = 0 to 32000 loop
@2 = TryDisk(0, 0, 0, 0)
MoveBlock(402b, label, lDL)
MoveBlock(1, data, 256)
goto 1
]

//-----------------------------------------------------------------------------------------
and CheckVitalFile(name, length) = valof
//-----------------------------------------------------------------------------------------
// Returns 0 if file is missing or less then length pages long.
// Returns VDA of page 1 otherwise.
[
unless alterFlag resultis true
let s = OpenFile(name, ksTypeReadOnly); if s eq 0 resultis 0
let cfa, pn, vda = vec lCFA, nil, nil
FileLength(s); GetCompleteFa(s, cfa)
pn = cfa>>CFA.fa.pageNumber
PositionPage(s, 1); GetCompleteFa(s, cfa)
vda = cfa>>CFA.fa.da
Closes(s)
resultis pn uls length? 0, vda
]

//-----------------------------------------------------------------------------------------
and StartLog() = valof
//-----------------------------------------------------------------------------------------
[
log = Allocate(sysZone, lFS)
InitializeFstream(log, charItem, LogOverflow, CallSwat)
LogOverflow()
resultis log
]

//-----------------------------------------------------------------------------------------
and LogOverflow(nil, char; numargs na) be
//-----------------------------------------------------------------------------------------
[
manifest [ blkSize = 256; blkChars = 2*(blkSize-2) ]
let buf = Allocate(sysZone, blkSize, true)
test buf eq 0
   ifso log>>ST.puts = Noop  //gulp...
   ifnot
      [
      Enqueue(log, buf)
      buf!1 = blkChars
      SetupFstream(log, buf+2, 0, blkChars)
      if na eq 2 then RetryCall(log, char)
      ]
]

//-----------------------------------------------------------------------------------------
and StopLog() be
//-----------------------------------------------------------------------------------------
[
let s = 0
if logFlag & alterFlag then s = OpenFile("Scavenger.Log$", ksTypeWriteOnly, charItem)
   [
   let buf = log>>ST.par1; if buf eq 0 break
   log>>ST.par1 = buf!0
   if s then
      [
      let nc = buf!0 eq 0? CurrentPos(log), buf!1
      WriteBlock(s, buf+2, nc/2)
      if (nc & 1) eq 1 then Puts(s, ((buf+2)!(nc/2)) rshift 8)
      ]
   Free(sysZone, buf)
   ] repeat
Free(sysZone, log); log = s
]

//-----------------------------------------------------------------------------------------
and SplitPuts(nil, char) be
//-----------------------------------------------------------------------------------------
[
SimpleDspPuts(dsp, char)  //ahem
if log ne 0 then Puts(log, char)
]