diff --git a/codeworld-server/codeworld-server.cabal b/codeworld-server/codeworld-server.cabal index d3fbac94..dfb909ae 100644 --- a/codeworld-server/codeworld-server.cabal +++ b/codeworld-server/codeworld-server.cabal @@ -15,38 +15,25 @@ Description: Executable codeworld-server Hs-source-dirs: src Main-is: Main.hs - Other-modules: Model, Util, Config + Other-modules: Config Build-depends: - aeson, base, - base64-bytestring, bytestring, codeworld-compiler, containers, - cryptonite, - data-default, directory, extra, - fast-logger, - filelock, filepath, - haskell-src-exts < 1.21, - http-conduit >= 2.3.0 && < 2.3.9, lifted-base, - memory, mtl, ormolu >= 0.1, - process, - regex-compat, regex-tdfa, SafeSemaphore, snap-core, snap-server, temporary, text, - unix, - vector, yaml Ghc-options: -threaded diff --git a/codeworld-server/src/Main.hs b/codeworld-server/src/Main.hs index d456a395..19919755 100644 --- a/codeworld-server/src/Main.hs +++ b/codeworld-server/src/Main.hs @@ -3,10 +3,6 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# OPTIONS_GHC -fno-warn-incomplete-patterns - -fno-warn-name-shadowing - -fno-warn-unused-imports - -fno-warn-unused-matches #-} {- Copyright 2020 The CodeWorld Authors. All rights reserved. @@ -25,49 +21,44 @@ -} module Main where -import CodeWorld.Compile -import CodeWorld.Compile.Base -import Config -import Control.Applicative +import CodeWorld.Compile (CompileStatus (..), Stage (..), compileSource) +import CodeWorld.Compile.Base (baseVersion, generateBaseBundle) +import Config (CompilerConfig (..), Config (..), PreviewConfig (..), loadConfig) +import Control.Applicative ((<|>)) import Control.Concurrent (forkIO) import Control.Concurrent.MSem (MSem) -import qualified Control.Concurrent.MSem as MSem -import Control.Exception (SomeException, bracket_, catch) +import qualified Control.Concurrent.MSem as MSem (new, peekAvail, with) +import Control.Exception (SomeException, catch) import qualified Control.Exception.Lifted as CE (catch) -import Control.Monad -import Control.Monad.Trans -import Data.Aeson -import qualified Data.ByteString as B -import Data.ByteString.Builder (toLazyByteString) -import qualified Data.ByteString.Lazy as LB -import Data.Char (isSpace) -import Data.List +import Control.Monad (when) +import Control.Monad.Trans (liftIO) +import qualified Data.ByteString as B (ByteString, empty, hPutStr, readFile, writeFile) +import qualified Data.ByteString.Lazy as LB (fromStrict) +import Data.List (isPrefixOf) import Data.List.Extra (replace) -import qualified Data.Map as M -import Data.Maybe -import Data.Monoid +import qualified Data.Map as M (Map, lookup) +import Data.Maybe (mapMaybe) import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import qualified Data.Text.IO as T -import qualified Data.Vector as V -import Model -import Network.HTTP.Simple +import qualified Data.Text as T (drop, intercalate, lines, pack, splitAt, splitOn, unlines, unpack) +import qualified Data.Text.Encoding as T (decodeUtf8, encodeUtf8) +import qualified Data.Text.IO as T (writeFile) import Ormolu (OrmoluException, defaultConfig, ormolu) -import Snap.Core -import Snap.Http.Server (httpServe, ConfigLog (ConfigIoLog)) -import qualified Snap.Http.Server.Config as S (commandLineConfig, defaultConfig, setPort, setErrorLog) -import Snap.Util.FileServe -import Snap.Util.FileUploads -import System.Directory -import System.FileLock -import System.FilePath -import System.IO (hPutStrLn, stderr) -import System.IO.Temp +import Snap.Core (Snap, getParam, modifyResponse, redirect, route, setContentType, setResponseCode, writeBS, writeLBS) +import Snap.Http.Server (ConfigLog (ConfigIoLog), httpServe) +import qualified Snap.Http.Server.Config as S (commandLineConfig, defaultConfig, setErrorLog, setPort) +import Snap.Util.FileServe (serveDirectory, serveFile) +import Snap.Util.FileUploads (UploadPolicy, defaultUploadPolicy, handleMultipart, setMaximumFormInputSize) +import System.Directory (createDirectoryIfMissing, doesFileExist) import System.Environment (lookupEnv) -import Util +import System.FilePath (()) +import System.IO (hPutStrLn, stderr) +import System.IO.Temp (withSystemTempDirectory) import Text.Read (readMaybe) -import Text.Regex.TDFA +import Text.Regex.TDFA (getAllMatches, (=~)) + + +newtype BuildMode = BuildMode String + deriving (Eq) data Context = Context { compileSem :: MSem Int, @@ -85,9 +76,9 @@ main = do ctx <- makeContext cfg port <- maybe Nothing readMaybe <$> lookupEnv "PORT" :: IO (Maybe Int) let customDefaultConfig = S.setErrorLog customErrorLog ((maybe id (\p -> S.setPort p) port) S.defaultConfig) - cfg <- S.commandLineConfig customDefaultConfig + snapCfg <- S.commandLineConfig customDefaultConfig forkIO $ baseVersion >>= buildBaseIfNeeded ctx >> return () - httpServe cfg $ (processBody >> site ctx) <|> site ctx + httpServe snapCfg $ (processBody >> site ctx) <|> site ctx makeContext :: Config -> IO Context makeContext cfg = do @@ -113,12 +104,12 @@ codeworldUploadPolicy = #if MIN_VERSION_snap_core(1,0,0) processBody :: Snap () processBody = do - handleMultipart codeworldUploadPolicy (\x y -> return ()) + handleMultipart codeworldUploadPolicy (\_ _ -> return ()) return () #else processBody :: Snap () processBody = do - handleMultipart codeworldUploadPolicy (\x -> return ()) + handleMultipart codeworldUploadPolicy (\_ -> return ()) return () #endif @@ -154,60 +145,46 @@ tryOr :: a -> IO a -> IO a tryOr fallback action = catch action (\(_ :: SomeException) -> pure fallback) --- A DirectoryConfig that sets the cache-control header to avoid errors when new --- changes are made to JavaScript. -dirConfig :: DirectoryConfig Snap -dirConfig = defaultDirectoryConfig {preServeHook = disableCache} - where - disableCache _ = modifyRequest (addHeader "Cache-control" "no-cache") - -withProgramLock :: BuildMode -> ProgramId -> IO a -> IO a -withProgramLock (BuildMode mode) (ProgramId hash) action = do - tmpDir <- getTemporaryDirectory - let tmpFile = tmpDir "codeworld" <.> T.unpack hash <.> mode - withFileLock tmpFile Exclusive (const action) - - -runCompile :: Context -> ProgramId -> BuildMode -> Text -> IO (CompileStatus, Either Text (Text,Text)) -runCompile ctx programId mode source = withSystemTempDirectory "codeworld" $ \tempDir -> do +runCompile :: Context -> BuildMode -> Text -> IO (CompileStatus, Either Text (Text,Text)) +runCompile ctx mode source = withSystemTempDirectory "codeworld" $ \tempDir -> do let sourceDir = tempDir "source" buildDir = tempDir "build" createDirectoryIfMissing True sourceDir createDirectoryIfMissing True buildDir - status <- withProgramLock mode programId $ do - T.writeFile (sourceDir sourceFile programId) source - compileIfNeeded ctx tempDir mode programId + status <- do + T.writeFile (sourceDir "program.hs") source + compileIfNeeded ctx tempDir mode - hasResultFile <- doesFileExist (buildDir resultFile programId) + hasResultFile <- doesFileExist (buildDir "err.txt") case status of CompileSuccess | hasResultFile -> do - content <- readFile (buildDir resultFile programId) - target <- readFile (buildDir targetFile programId) + content <- readFile (buildDir "err.txt") + target <- readFile (buildDir "program.js") pure (status, Right (T.pack content,T.pack target)) _ | hasResultFile -> do - content <- readFile (buildDir resultFile programId) + content <- readFile (buildDir "err.txt") pure (status, Left $ T.pack content) _ -> pure (status, Left "Something went wrong") replaceUndefinedWithHole :: Text -> (Int, Text) -replaceUndefinedWithHole txt = (length matches, replace matches 0 txt) +replaceUndefinedWithHole txt = (length matches, replaceFn matches 0 txt) where undefinedRegex = "\\bundefined\\b" :: Text matches = getAllMatches (txt =~ undefinedRegex) :: [(Int,Int)] - replace [] _ t = t - replace ((targetIndex,_):xs) cursor t = + replaceFn [] _ t = t + replaceFn ((targetIndex,_):xs) cursor t = let (before, rest) = T.splitAt (targetIndex - cursor) t - in before <> "_" <> replace xs (targetIndex + 9) (T.drop 9 rest) + in before <> "_" <> replaceFn xs (targetIndex + 9) (T.drop 9 rest) replaceHolesWithDefaultValue :: [(Int,Int,Text)] -> M.Map Text Text -> Text -> Maybe Text -replaceHolesWithDefaultValue holes defaults input = T.unlines <$> replaceHolesInLines lines +replaceHolesWithDefaultValue holes defaults input = T.unlines <$> replaceHolesInLines codeLines where - lines = zip [1 :: Int ..] $ T.lines input + codeLines = zip [1 :: Int ..] $ T.lines input - replaceHolesInLines lines = traverse (\(num,line) -> replaceHolesInLine (filter (\(r,_,_) -> r == num) holes) 1 line) lines + replaceHolesInLines codeLines' = traverse (\(num,line) -> replaceHolesInLine (filter (\(r,_,_) -> r == num) holes) 1 line) codeLines' replaceHolesInLine [] _ line = Just line replaceHolesInLine ((_,c,ty):xs) cursor line = @@ -219,8 +196,8 @@ replaceHolesWithDefaultValue holes defaults input = T.unlines <$> replaceHolesIn pure $ before <> "(" <> defaultValue <> ")" <> newRest extractHolesFromErrorText :: Text -> [(Int,Int,Text)] -extractHolesFromErrorText error = - let errorSplit = T.splitOn "\n\n" error +extractHolesFromErrorText err = + let errorSplit = T.splitOn "\n\n" err regex = "^program\\.hs:([[:digit:]]+):([[:digit:]]+): error:[[:cntrl:]] +[^F]+Found hole: _ :: ([[:print:]]+)[[:cntrl:]]" :: Text matches = concatMap (\block -> block =~ regex :: [[Text]]) errorSplit textToInt = read . T.unpack @@ -232,10 +209,7 @@ compileHandler ctx = do let previewConf = previewConfig $ config ctx Just source <- (T.decodeUtf8 <$>) <$> getParam "source" mPreview <- getParam "enablePreview" - let programId = sourceToProgramId $ T.encodeUtf8 source - id = unProgramId programId - did = "deploy_id" - previewsEnabled = case mPreview of + let previewsEnabled = case mPreview of Just "True" -> True Just "true" -> True Just "False" -> False @@ -243,7 +217,7 @@ compileHandler ctx = do _ -> enabledByDefault previewConf (compileStatus, result) <- liftIO $ do - (originalStatus, originalResult) <- runCompile ctx programId mode source + (originalStatus, originalResult) <- runCompile ctx mode source tryOr (originalStatus, originalResult) $ do assert previewsEnabled @@ -252,21 +226,21 @@ compileHandler ctx = do let (replaceCount, sourceWithHolePlaceholders) = replaceUndefinedWithHole source assert $ replaceCount > 0 - (_,Left error) <- runCompile ctx programId mode sourceWithHolePlaceholders + (_,Left err) <- runCompile ctx mode sourceWithHolePlaceholders - let holes = extractHolesFromErrorText error + let holes = extractHolesFromErrorText err replacementMap = defaultHoleValues previewConf Just withDefaultValues = replaceHolesWithDefaultValue holes replacementMap source - (status', res') <- runCompile ctx programId mode withDefaultValues + (status', res') <- runCompile ctx mode withDefaultValues assert $ status' == CompileSuccess pure (status', res') let responseBody = T.intercalate "\n=======================\n" $ case result of - Right (content, target) -> [id,did,content,target] - Left errorMessage -> [id,did,errorMessage] + Right (content, target) -> [content,target] + Left errorMessage -> [errorMessage] modifyResponse $ setResponseCode (responseCodeFromCompileStatus compileStatus) modifyResponse $ setContentType "text/plain" @@ -283,27 +257,15 @@ errorCheckHandler ctx = do CompileSuccess -> writeBS "" _ -> writeBS output -getHashParam :: Bool -> BuildMode -> Snap ProgramId -getHashParam allowDeploy mode = do - maybeHash <- getParam "hash" - case maybeHash of - Just h -> return (ProgramId (T.decodeUtf8 h)) - Nothing -> pass - runBaseHandler :: CodeWorldHandler runBaseHandler ctx = do maybeVer <- fmap T.decodeUtf8 <$> getParam "version" - hasProgram <- - (\mode hash dhash -> mode && (hash || dhash)) - <$> hasParam "mode" <*> hasParam "hash" <*> hasParam "dhash" case maybeVer of - Just ver -> serveFile (baseCodeFile ver) + Just ver -> serveFile ("data/base" T.unpack ver "base.js") Nothing -> do ver <- liftIO baseVersion liftIO $ buildBaseIfNeeded ctx ver - serveFile (baseCodeFile ver) - where - hasParam name = (/= Nothing) <$> getParam name + serveFile ("data/base" T.unpack ver "base.js") escapeCode :: String -> String escapeCode input = foldr @@ -315,7 +277,7 @@ escapeCode input = foldr toBeEscaped = ["${","`"] serveEditor :: CodeWorldHandler -serveEditor ctx = do +serveEditor _ = do msource <- getParam "source" modifyResponse $ setContentType "text/html" template <- liftIO $ readFile "web/env.html" @@ -324,8 +286,7 @@ serveEditor ctx = do writeBS $ T.encodeUtf8 $ T.pack content indentHandler :: CodeWorldHandler -indentHandler ctx = do - mode <- getBuildMode +indentHandler _ = do Just source <- getParam "source" reformat source `CE.catch` handleError where @@ -338,7 +299,7 @@ indentHandler ctx = do writeLBS $ LB.fromStrict $ T.encodeUtf8 $ T.pack (show e) runHandler :: CodeWorldHandler -runHandler ctx = do +runHandler _ = do msource <- getParam "source" modifyResponse $ setContentType "text/html" template <- liftIO $ readFile "web/run.html" @@ -358,57 +319,57 @@ waitAndLogExhausted name sem action = do hPutStrLn stderr $ name ++ " has exhausted its available resources, but there is further demand." MSem.with sem action -compileIfNeeded :: Context -> FilePath -> BuildMode -> ProgramId -> IO CompileStatus -compileIfNeeded ctx basePath mode programId = do - hasResult <- doesFileExist (basePath "build" resultFile programId) - hasTarget <- doesFileExist (basePath "build" targetFile programId) +compileIfNeeded :: Context -> FilePath -> BuildMode -> IO CompileStatus +compileIfNeeded ctx basePath mode = do + hasResult <- doesFileExist (basePath "build" "err.txt") + hasTarget <- doesFileExist (basePath "build" "program.js") if | hasResult && hasTarget -> return CompileSuccess | hasResult -> return CompileError | otherwise -> - waitAndLogExhausted "Compile" (compileSem ctx) $ compileProgram ctx basePath mode programId + waitAndLogExhausted "Compile" (compileSem ctx) $ compileProgram ctx basePath mode -compileProgram :: Context -> FilePath -> BuildMode -> ProgramId -> IO CompileStatus -compileProgram ctx basePath mode programId = do +compileProgram :: Context -> FilePath -> BuildMode -> IO CompileStatus +compileProgram ctx basePath mode = do ver <- baseVersion baseStatus <- buildBaseIfNeeded ctx ver case baseStatus of CompileSuccess -> do - status <- compileIncrementally ctx basePath mode programId ver - T.writeFile (basePath "build" baseVersionFile programId) ver + status <- compileIncrementally ctx basePath mode ver + T.writeFile (basePath "build" "basever") ver -- It's possible that a new library was built during the compile. If so, then the code -- we've just built is suspect, and it's better to just build it anew! checkVer <- baseVersion if ver == checkVer then return status - else compileProgram ctx basePath mode programId + else compileProgram ctx basePath mode _ -> return CompileAborted -compileIncrementally :: Context -> FilePath -> BuildMode -> ProgramId -> Text -> IO CompileStatus -compileIncrementally ctx basePath mode programId ver = +compileIncrementally :: Context -> FilePath -> BuildMode -> Text -> IO CompileStatus +compileIncrementally ctx basePath mode ver = compileSource stage source (projectModuleFinder (Just sourceDir) mode) extraExt result (getMode mode) False where sourceDir = basePath "source" - source = sourceDir sourceFile programId - target = basePath "build" targetFile programId - result = basePath "build" resultFile programId + source = sourceDir "program.hs" + target = basePath "build" "program.js" + result = basePath "build" "err.txt" baseURL = "runBaseJS?version=" ++ T.unpack ver - stage = UseBase target (baseSymbolFile ver) baseURL + stage = UseBase target ("data/base" T.unpack ver "base.symbs") baseURL extraExt = extraExtensions $ config ctx projectModuleFinder :: Maybe FilePath -> BuildMode -> String -> IO (Maybe FilePath) -projectModuleFinder mSourceDir mode modName +projectModuleFinder mSourceDir _ modName | length modName /= 23 || '.' `elem` modName = return Nothing - | "P" `isPrefixOf` modName = go (ProgramId (T.pack modName)) + | "P" `isPrefixOf` modName = go | otherwise = return Nothing where - go programId = do + go = do case mSourceDir of Nothing -> return Nothing Just sourceDir -> do - let path = sourceDir sourceFile programId + let path = sourceDir "program.hs" exists <- doesFileExist path if exists then return (Just path) else return Nothing @@ -417,15 +378,15 @@ noModuleFinder _ = return Nothing buildBaseIfNeeded :: Context -> Text -> IO CompileStatus buildBaseIfNeeded ctx ver = do - codeExists <- doesFileExist (baseCodeFile ver) - symbolsExist <- doesFileExist (baseSymbolFile ver) + codeExists <- doesFileExist ("data/base" T.unpack ver "base.js") + symbolsExist <- doesFileExist ("data/base" T.unpack ver "base.symbs") if not codeExists || not symbolsExist then waitAndLogExhausted "Base" (baseSem ctx) $ withSystemTempDirectory "genbase" $ \tmpdir -> do let linkMain = tmpdir "LinkMain.hs" let linkBase = tmpdir "LinkBase.hs" let err = tmpdir "output.txt" generateBaseBundle basePaths baseIgnore "codeworld" linkMain linkBase - let stage = GenBase "LinkBase" linkBase (baseCodeFile ver) (baseSymbolFile ver) + let stage = GenBase "LinkBase" linkBase ("data/base" T.unpack ver "base.js") ("data/base" T.unpack ver "base.symbs") let extraExt = extraExtensions $ config ctx compileSource stage linkMain noModuleFinder extraExt err "codeworld" False else return CompileSuccess diff --git a/codeworld-server/src/Model.hs b/codeworld-server/src/Model.hs deleted file mode 100644 index d14d47a0..00000000 --- a/codeworld-server/src/Model.hs +++ /dev/null @@ -1,95 +0,0 @@ -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-unused-imports #-} - -{- - Copyright 2020 The CodeWorld Authors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --} -module Model where - -import Control.Applicative -import Control.Monad -import Data.Aeson -import Data.ByteString (ByteString) -import Data.Text (Text) -import GHC.Generics (Generic) -import System.FilePath (FilePath) - -data Project = Project - { projectName :: Text, - projectSource :: Text, - projectHistory :: Value - } - -instance FromJSON Project where - parseJSON (Object v) = - Project <$> v .: "name" <*> v .: "source" <*> v .: "history" - parseJSON _ = mzero - -instance ToJSON Project where - toJSON p = - object - [ "name" .= projectName p, - "source" .= projectSource p, - "history" .= projectHistory p, - "type" .= ("project" :: Text) - ] - -data FileSystemEntryType = Dir | Proj deriving (Eq, Ord, Show) - -instance ToJSON FileSystemEntryType where - toJSON Dir = Data.Aeson.String "directory" - toJSON Proj = Data.Aeson.String "project" - -instance FromJSON FileSystemEntryType where - parseJSON (Data.Aeson.String "directory") = return $ Dir - parseJSON (Data.Aeson.String "project") = return $ Proj - parseJSON _ = mzero - -data FileSystemEntry = FSEntry - { fsEntryIndex :: Int, - fsEntryName :: Text, - fsEntryType :: FileSystemEntryType, - fsEntryChildren :: Maybe [FileSystemEntry] - } - deriving (Generic, Eq, Ord, Show) - -fsEntryJSONOptions :: Options -fsEntryJSONOptions = - defaultOptions - { fieldLabelModifier = \f -> case f of - "fsEntryIndex" -> "index" - "fsEntryName" -> "name" - "fsEntryType" -> "type" - "fsEntryChildren" -> "children" - _ -> f, - omitNothingFields = True - } - -instance ToJSON FileSystemEntry where - toJSON = genericToJSON fsEntryJSONOptions - -instance FromJSON FileSystemEntry where - parseJSON = genericParseJSON fsEntryJSONOptions - -data CompileResult = CompileResult - { compileHash :: Text, - compileDeployHash :: Text - } - -instance ToJSON CompileResult where - toJSON cr = - object ["hash" .= compileHash cr, "dhash" .= compileDeployHash cr] - diff --git a/codeworld-server/src/Util.hs b/codeworld-server/src/Util.hs deleted file mode 100644 index 7255ed8a..00000000 --- a/codeworld-server/src/Util.hs +++ /dev/null @@ -1,304 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# OPTIONS_GHC -fno-warn-name-shadowing #-} - -{- - Copyright 2020 The CodeWorld Authors. All rights reserved. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --} -module Util where - -import Control.Exception -import Control.Monad -import qualified Crypto.Hash as Crypto -import Data.Aeson -import Data.ByteArray (convert) -import Data.ByteString (ByteString) -import qualified Data.ByteString as B -import qualified Data.ByteString.Base64 as B64 -import qualified Data.ByteString.Lazy as LB -import Data.List (sort, sortOn) -import Data.Maybe -import Data.Text (Text) -import qualified Data.Text as T -import qualified Data.Text.Encoding as T -import qualified Data.Text.IO as T -import Model -import System.Directory -import System.FilePath -import System.IO.Error -import System.Posix.Files - -newtype BuildMode = BuildMode String - deriving (Eq) - -newtype ProgramId = ProgramId - { unProgramId :: Text - } - deriving (Eq) - -newtype ProjectId = ProjectId - { unProjectId :: Text - } - deriving (Eq) - -newtype DeployId = DeployId - { unDeployId :: Text - } - deriving (Eq) - -newtype DirId = DirId - { unDirId :: Text - } - deriving (Eq) - -newtype ShareId = ShareId - { unShareId :: Text - } - deriving (Eq) - -type BaseVersion = Text - -autocompletePath :: FilePath -autocompletePath = "web/codeworld-base.txt" - -clientIdPath :: FilePath -clientIdPath = "web/clientId.txt" - -baseRootDir :: FilePath -baseRootDir = "data/base" - -baseCodeFile :: BaseVersion -> FilePath -baseCodeFile ver = baseRootDir T.unpack ver "base.js" - -baseSymbolFile :: BaseVersion -> FilePath -baseSymbolFile ver = baseRootDir T.unpack ver "base.symbs" - -sourceBase :: ProgramId -> FilePath -sourceBase (ProgramId p) = T.unpack p - -sourceFile :: ProgramId -> FilePath -sourceFile programId = sourceBase programId <.> "hs" - -sourceXML :: ProgramId -> FilePath -sourceXML programId = sourceBase programId <.> "xml" - -targetFile :: ProgramId -> FilePath -targetFile programId = sourceBase programId <.> "js" - -resultFile :: ProgramId -> FilePath -resultFile programId = sourceBase programId <.> "err.txt" - -baseVersionFile :: ProgramId -> FilePath -baseVersionFile programId = sourceBase programId <.> "basever" - -auxiliaryFiles :: ProgramId -> [FilePath] -auxiliaryFiles programId = - [ sourceBase programId <.> "js_hi", - sourceBase programId <.> "js_o", - sourceBase programId <.> "jsexe" "index.html", - sourceBase programId <.> "jsexe" "lib.js", - sourceBase programId <.> "jsexe" "manifest.webapp", - sourceBase programId <.> "jsexe" "out.js", - sourceBase programId <.> "jsexe" "out.stats", - sourceBase programId <.> "jsexe" "rts.js", - sourceBase programId <.> "jsexe" "runmain.js" - ] - -deployLink :: DeployId -> FilePath -deployLink (DeployId d) = - let s = T.unpack d - in take 3 s s - -shareLink :: ShareId -> FilePath -shareLink (ShareId sh) = - let s = T.unpack sh - in take 3 s s - -projectBase :: ProjectId -> FilePath -projectBase (ProjectId p) = T.unpack p - -projectFile :: ProjectId -> FilePath -projectFile projectId = projectBase projectId <.> "cw" - -sourceToProgramId :: ByteString -> ProgramId -sourceToProgramId = ProgramId . T.map dashToUnderscore . hashToId "P" - -sourceToDeployId :: ByteString -> DeployId -sourceToDeployId = DeployId . T.map dashToUnderscore . hashToId "D" . ("DEPLOY_ID" <>) - -dashToUnderscore :: Char -> Char -dashToUnderscore '-' = '_' -dashToUnderscore c = c - -nameToProjectId :: Text -> ProjectId -nameToProjectId = ProjectId . hashToId "S" . T.encodeUtf8 - -dirBase :: DirId -> FilePath -dirBase (DirId d) = T.unpack d - -nameToDirId :: Text -> DirId -nameToDirId = DirId . hashToId "D" . T.encodeUtf8 - -listDirectoryWithPrefix :: FilePath -> IO [FilePath] -listDirectoryWithPrefix filePath = map (filePath ) <$> listDirectory filePath - -listDirectoryWithPrefixRecursive :: FilePath -> IO [FilePath] -listDirectoryWithPrefixRecursive filePath = do - subpaths <- map (filePath ) <$> listDirectory filePath - dirs <- filterM doesDirectoryExist subpaths - subtrees <- mapM listDirectoryWithPrefixRecursive dirs - return $ subpaths ++ (concat subtrees) - -dirFilter :: [FilePath] -> Char -> [FilePath] -dirFilter dirs char = filter (\x -> head (takeBaseName x) == char) dirs - -fsEntries :: Bool -> FilePath -> IO [FileSystemEntry] -fsEntries recurse dir = do - subHashedDirs <- listDirectoryWithPrefix dir - let hashedFiles = dirFilter subHashedDirs 'S' - hashedDirs = dirFilter subHashedDirs 'D' - projNames <- sort <$> mapM projName hashedFiles - dirNames <- sort <$> catMaybes <$> mapM dirName hashedDirs - haveSavedOrderFile <- doesFileExist $ dir "order.info" - shallowResult <- case haveSavedOrderFile of - True -> do - dumpedEntries <- fromJust . decode <$> LB.readFile (dir "order.info") - let (dumpedDirs, dumpedProjects) = span (\x -> fsEntryType x == Dir) $ sortOn fsEntryType dumpedEntries - existingDirs = onlyExisting (sortOn fsEntryName dumpedDirs) dirNames - existingProjects = onlyExisting (sortOn fsEntryName dumpedProjects) projNames - return $ - updateOrder Dir existingDirs dirNames - ++ updateOrder Proj existingProjects projNames - False -> return $ updateOrder Dir [] dirNames ++ updateOrder Proj [] projNames - if recurse then recurseInto shallowResult else return shallowResult - where - onlyExisting :: [FileSystemEntry] -> [Text] -> [FileSystemEntry] - onlyExisting dumped existing = filter (\d -> fsEntryName d `elem` existing) dumped - updateOrder :: FileSystemEntryType -> [FileSystemEntry] -> [Text] -> [FileSystemEntry] - updateOrder _ [] [] = [] - updateOrder _ (_ : _) [] = [] - updateOrder defType [] (name : names) = (FSEntry 0 name defType Nothing) : updateOrder defType [] names - updateOrder defType (entry@(FSEntry _ dumpedName _ _) : entries) (name : names) - | dumpedName == name = entry : updateOrder defType entries names - | otherwise = (FSEntry 0 name defType Nothing) : updateOrder defType (entry : entries) names - projName path = do - Just project <- decode <$> LB.readFile path - return $ projectName project - dirName path = do - hasInfo <- doesFileExist (path "dir.info") - if hasInfo then Just <$> T.readFile (path "dir.info") else return Nothing - recurseInto :: [FileSystemEntry] -> IO [FileSystemEntry] - recurseInto [] = return [] - recurseInto (e : es) = (:) <$> expand e <*> recurseInto es - expand :: FileSystemEntry -> IO FileSystemEntry - expand (FSEntry i name Dir _) = - FSEntry i name Dir <$> Just <$> fsEntries True (dir dirBase (nameToDirId name)) - expand otherEntry = return otherEntry - -projectFileNames :: FilePath -> IO [Text] -projectFileNames dir = do - subHashedDirs <- listDirectoryWithPrefix dir - let hashedFiles = dirFilter subHashedDirs 'S' - projects <- fmap catMaybes - $ forM hashedFiles - $ \f -> do - exists <- doesFileExist f - if exists - then decode <$> LB.readFile f - else return Nothing - return $ map projectName projects - -projectDirNames :: FilePath -> IO [Text] -projectDirNames dir = do - subHashedDirs <- listDirectoryWithPrefix dir - let hashedDirs = dirFilter subHashedDirs 'D' - dirNames <- mapM (\x -> T.readFile $ x "dir.info") hashedDirs - return dirNames - - -isDir :: FilePath -> IO Bool -isDir path = do - status <- getFileStatus path - return $ isDirectory status - -getFilesRecursive :: FilePath -> IO [FilePath] -getFilesRecursive path = do - dirBool <- isDir path - case dirBool of - True -> do - contents <- listDirectory path - concat <$> mapM (getFilesRecursive . (path )) contents - False -> return [path] - -dirToCheckSum :: FilePath -> IO Text -dirToCheckSum path = do - files <- getFilesRecursive path - fileContents <- mapM B.readFile files - let cryptoContext = Crypto.hashInitWith Crypto.MD5 - return - $ (T.pack "F" <>) - . T.takeWhile (/= '=') - . T.map toWebSafe - . T.decodeUtf8 - . B64.encode - . convert - . Crypto.hashFinalize - . Crypto.hashUpdates cryptoContext - $ fileContents - where - toWebSafe '/' = '_' - toWebSafe '+' = '-' - toWebSafe c = c - -hashToId :: Text -> ByteString -> Text -hashToId pfx = - (pfx <>) - . T.takeWhile (/= '=') - . T.map toWebSafe - . T.decodeUtf8 - . B64.encode - . convert - . Crypto.hashWith Crypto.MD5 - where - toWebSafe '/' = '_' - toWebSafe '+' = '-' - toWebSafe c = c - -copyDirIfExists :: FilePath -> FilePath -> IO () -copyDirIfExists src dst = do - contents <- listDirectory src - dstExists <- doesDirectoryExist dst - when (not dstExists) $ createDirectoryIfMissing True dst - forM_ contents $ \f -> do - let srcPath = src f - let dstPath = dst f - isDir <- doesDirectoryExist srcPath - if isDir - then copyDirIfExists srcPath dstPath - else copyFile srcPath dstPath - -removeFileIfExists :: FilePath -> IO () -removeFileIfExists fileName = removeFile fileName `catch` handleExists - where - handleExists e - | isDoesNotExistError e = return () - | otherwise = throwIO e - -removeDirectoryIfExists :: FilePath -> IO () -removeDirectoryIfExists dirName = - removeDirectoryRecursive dirName `catch` handleExists - where - handleExists e - | isDoesNotExistError e = return () - | otherwise = throwIO e diff --git a/web/js/codeworld.js b/web/js/codeworld.js index f3057032..144297d2 100644 --- a/web/js/codeworld.js +++ b/web/js/codeworld.js @@ -30,6 +30,7 @@ import { run, toggleObsoleteCodeAlert, warnIfUnsaved, + sha256digest, } from './codeworld_shared.js'; import * as Alert from './utils/alert.js'; @@ -177,22 +178,10 @@ async function init() { if(window.buildMode === 'codeworld') document.querySelector("#docButton").style.display = "none"; - let hash = location.hash.slice(1); - if (hash.length > 0) { - if (hash.slice(-2) === '==') { - hash = hash.slice(0, -2); - } - } + const savedCode = localStorage.getItem(`${window.buildMode}-${window.location.hash.slice(1)}`); - if (hash.length > 0) { - if (hash.slice(-2) === '==') { - hash = hash.slice(0, -2); - } - if (hash[0] === 'P') { - setCode(localStorage.getItem(`${window.buildMode}-${hash}`) || ''); - } else if (hash[0] !== 'F') { - setCode(''); - } + if (savedCode) { + setCode(savedCode); } else { if(window.buildMode === "codeworld") setCode(`import Prelude hiding (rotated, translated, colored, lettering, scaled, polyline, Text, Number) @@ -1013,52 +1002,41 @@ function compile() { stopRun(); }); - sendHttp('POST', 'compile', data, (request) => { + sendHttp('POST', 'compile', data, async (request) => { if (compileFinished) return; const { status, responseText } = request; window.cancelCompile(); - const success = status === 200; const parts = responseText.split('\n=======================\n') - let hash, dhash, msg; - if (status < 500) { - if (responseText.length === 23) { - // will not happen - hash = responseText; - dhash = null; - } else { - try { - hash = parts[0]; - dhash = parts[1]; - msg = parts[2]; - if(msg) { - msg = msg.replace(/^[\r\n]+|[\r\n]+$/g, ''); - } else { - msg = 'Sorry! Your program couldn\'t be run right now.'; - } + if(status < 500) { - window.program = parts[3]; - run(hash,dhash,msg,false,compileGeneration); - localStorage.setItem(`${window.buildMode}-${hash}`, window.codeworldEditor.getValue()); - } catch (e) { - hash = ''; - } + const compilerMessage = parts[0]; + let compiledProgram = parts[1]; + + if(!compilerMessage) { + compilerMessage = 'Sorry! Your program couldn\'t be run right now.'; } - } - if (!hash) { - sweetAlert({ - title: Alert.title('Could not compile'), - text: 'The compiler is unavailable. Please try again later.', - type: 'error', - }); - return; - } else { + const codeHash = await sha256digest(src.trim()); + + window.program = compiledProgram; + run(codeHash,"deploy_hash",compilerMessage,false,compileGeneration); + localStorage.setItem(`${window.buildMode}-${codeHash}`, src); + + sweetAlert.close(); + return; } + + sweetAlert({ + title: Alert.title('Could not compile'), + text: 'The compiler is unavailable. Please try again later.', + type: 'error', + }); + }); } diff --git a/web/js/codeworld_shared.js b/web/js/codeworld_shared.js index e2942e1d..1d7833ea 100644 --- a/web/js/codeworld_shared.js +++ b/web/js/codeworld_shared.js @@ -1110,6 +1110,15 @@ function parseCompileErrors(rawErrors) { return errors; } +async function sha256digest(data) { + const buffer = new TextEncoder().encode(data); + return await crypto.subtle.digest('SHA-256', buffer).then((hash) => { + return Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + }); +} + export { clearMessages, definePanelExtension, @@ -1126,4 +1135,5 @@ export { run, toggleObsoleteCodeAlert, warnIfUnsaved, + sha256digest, }; diff --git a/web/js/run.js b/web/js/run.js index 1fab8e5a..3800f43f 100644 --- a/web/js/run.js +++ b/web/js/run.js @@ -275,7 +275,7 @@ async function init() { const { status, responseText } = request; if(status < 500) { const parts = responseText.split('\n=======================\n'); - const program = parts[3]; + const program = parts[1]; const loadScript = document.createElement('script'); loadScript.setAttribute('type', 'text/javascript'); loadScript.innerHTML = program;