/* If ME_CONFIG_FILE is defined, use that as the base name for appweb.conf. Otherwise, use "programName.conf". Search order is: "."/BASE ME_SERVER_ROOT/BASE EXE/../BASE EXE/../appweb.conf */ static int findAppwebConf() { char *base, *filename; if (ME_CONFIG_FILE) { base = sclone(ME_CONFIG_FILE); } else { base = mprJoinPathExt(mprGetAppName(), ".conf"); } #if !ME_ROM filename = base; if (!mprPathExists(filename, R_OK)) { filename = mprJoinPath(app->home, base); if (!mprPathExists(filename, R_OK)) { filename = mprJoinPath(mprGetPathParent(mprGetAppDir()), base); if (!mprPathExists(filename, R_OK)) { filename = mprJoinPath(mprGetPathParent(mprGetAppDir()), "appweb.conf"); if (!mprPathExists(filename, R_OK)) { mprError("Cannot find config file %s", base); return MPR_ERR_CANT_OPEN; } } } } #endif app->configFile = filename; return 0; }
/* Check if the layout has changed. Returns false if the layout does not exist. */ static bool layoutIsStale(EspRoute *eroute, cchar *source, cchar *module) { char *data, *lpath, *quote; cchar *layout, *layoutsDir; ssize len; bool stale; int recompile; stale = 0; layoutsDir = httpGetDir(eroute->route, "LAYOUTS"); if ((data = mprReadPathContents(source, &len)) != 0) { if ((lpath = scontains(data, "@ layout \"")) != 0) { lpath = strim(&lpath[10], " ", MPR_TRIM_BOTH); if ((quote = schr(lpath, '"')) != 0) { *quote = '\0'; } layout = (layoutsDir && *lpath) ? mprJoinPath(layoutsDir, lpath) : 0; } else { layout = (layoutsDir) ? mprJoinPath(layoutsDir, "default.esp") : 0; } if (layout) { stale = espModuleIsStale(layout, module, &recompile); if (stale) { mprLog("info esp", 4, "esp layout %s is newer than module %s", layout, module); } } } return stale; }
/* Check if the target/filename.ext is registered as a view or exists as a file */ static cchar *checkView(HttpConn *conn, cchar *target, cchar *filename, cchar *ext) { MprPath info; EspRoute *eroute; cchar *path; if (filename) { target = mprJoinPath(target, filename); } if (ext && *ext) { if (!smatch(mprGetPathExt(target), ext)) { target = sjoin(target, ".", ext, NULL); } } eroute = conn->rx->route->eroute; if (mprLookupKey(eroute->views, target)) { return target; } path = mprJoinPath(conn->rx->route->documents, target); if (mprGetPathInfo(path, &info) == 0 && !info.isDir) { return target; } if (conn->rx->route->map && !(conn->tx->flags & HTTP_TX_NO_MAP)) { path = httpMapContent(conn, path); if (mprGetPathInfo(path, &info) == 0 && !info.isDir) { return target; } } return 0; }
/* function WebSocket(uri: Uri, protocols = null, options) options = { certificate: Path, verify: Boolean, } */ static EjsWebSocket *wsConstructor(Ejs *ejs, EjsWebSocket *ws, int argc, EjsObj **argv) { EjsAny *certificate; bool verify; assert(ejsIsPot(ejs, ws)); ejsLoadHttpService(ejs); ws->ejs = ejs; verify = 0; ws->uri = httpUriToString(((EjsUri*) argv[0])->uri, 0); if (argc >= 2) { if (ejsIs(ejs, argv[1], Array)) { ws->protocols = sclone((ejsToString(ejs, argv[1]))->value); } else if (ejsIs(ejs, argv[1], String)) { ws->protocols = sclone(((EjsString*) argv[1])->value); } else { ws->protocols = sclone("chat"); } } else { ws->protocols = sclone("chat"); } if (*ws->protocols == '\0') { ejsThrowArgError(ejs, "Bad protocol"); return 0; } if (argc >= 3) { ws->frames = ejsGetPropertyByName(ejs, argv[2], EN("frames")) == ESV(true); verify = ejsGetPropertyByName(ejs, argv[2], EN("verify")) == ESV(true); if ((certificate = ejsGetPropertyByName(ejs, argv[2], EN("certificate"))) != 0) { ws->certFile = ejsToMulti(ejs, argv[0]); } } if ((ws->conn = httpCreateConn(MPR->httpService, NULL, ejs->dispatcher)) == 0) { ejsThrowMemoryError(ejs); return 0; } httpSetAsync(ws->conn, 1); httpPrepClientConn(ws->conn, 0); httpSetConnNotifier(ws->conn, webSocketNotify); httpSetWebSocketProtocols(ws->conn, ws->protocols); httpSetConnContext(ws->conn, ws); if (sstarts(ws->uri, "wss")) { ws->ssl = mprCreateSsl(0); mprVerifySslIssuer(ws->ssl, verify); mprVerifySslPeer(ws->ssl, verify); #if FUTURE if (!hp->caFile) { //MOB - Some define for this. hp->caFile = mprJoinPath(mprGetAppDir(), "http-ca.crt"); } mprSetSslCaFile(hp->ssl, hp->caFile); mprSetSslCaFile(hp->ssl, mprJoinPath(mprGetAppDir(), "http-ca.crt")); #endif } startWebSocketRequest(ejs, ws); return ws; }
PUBLIC int maSetPlatform(cchar *platformPath) { MaAppweb *appweb; MprDirEntry *dp; cchar *platform, *dir, *junk, *appwebExe; int next, i, notrace; appweb = MPR->appwebService; notrace = !platformPath; if (!platformPath) { platformPath = appweb->localPlatform; } appweb->platform = appweb->platformDir = 0; platform = mprGetPathBase(platformPath); if (mprPathExists(platformPath, X_OK) && mprIsPathDir(platformPath)) { appweb->platform = platform; appweb->platformDir = sclone(platformPath); } else if (smatch(platform, appweb->localPlatform)) { /* If running inside an appweb source tree, locate the platform directory */ appwebExe = mprJoinPath(mprGetAppDir(), "appweb" BIT_EXE); if (mprPathExists(appwebExe, R_OK)) { appweb->platform = appweb->localPlatform; appweb->platformDir = mprGetPathParent(mprGetAppDir()); } else { /* Check installed appweb */ appwebExe = BIT_VAPP_PREFIX "/bin/appweb" BIT_EXE; if (mprPathExists(appwebExe, R_OK)) { appweb->platform = appweb->localPlatform; appweb->platformDir = sclone(BIT_VAPP_PREFIX); } } } /* Last chance. Search up the tree for a similar platform directory. This permits specifying a partial platform like "vxworks" without architecture and profile. */ if (!appweb->platformDir) { dir = mprGetCurrentPath(); for (i = 0; !mprSamePath(dir, "/") && i < 64; i++) { for (ITERATE_ITEMS(mprGetPathFiles(dir, 0), dp, next)) { if (dp->isDir && sstarts(mprGetPathBase(dp->name), platform)) { appweb->platform = mprGetPathBase(dp->name); appweb->platformDir = mprJoinPath(dir, dp->name); break; } } dir = mprGetPathParent(dir); } }
static void cleanUploadedFiles(HttpConn *conn) { HttpRx *rx; HttpUploadFile *file; cchar *path, *uploadDir; int index; rx = conn->rx; uploadDir = getUploadDir(rx->route); for (ITERATE_ITEMS(rx->files, file, index)) { if (file->filename) { if (rx->autoDelete) { mprDeletePath(file->filename); } else if (rx->renameUploads) { path = mprJoinPath(uploadDir, file->clientFilename); if (rename(file->filename, path) != 0) { mprLog("http error", 0, "Cannot rename %s to %s", file->filename, path); } } file->filename = 0; } } }
/* Search for a module "filename" in the modulePath. Return the result in "result" */ PUBLIC char *mprSearchForModule(cchar *filename) { #if ME_COMPILER_HAS_DYN_LOAD char *path, *f, *searchPath, *dir, *tok; filename = mprNormalizePath(filename); /* Search for the path directly */ if ((path = probe(filename)) != 0) { return path; } /* Search in the searchPath */ searchPath = sclone(mprGetModuleSearchPath()); tok = 0; dir = stok(searchPath, MPR_SEARCH_SEP, &tok); while (dir && *dir) { f = mprJoinPath(dir, filename); if ((path = probe(f)) != 0) { return path; } dir = stok(0, MPR_SEARCH_SEP, &tok); } #endif /* ME_COMPILER_HAS_DYN_LOAD */ return 0; }
/* Write the port so the monitor can manage */ static int writePort(MaServer *server) { HttpEndpoint *endpoint; char numBuf[16], *path; int fd, len; if ((endpoint = mprGetFirstItem(server->http->endpoints)) == 0) { mprLog("error appweb", 0, "No configured endpoints"); return MPR_ERR_CANT_ACCESS; } // FUTURE - should really go to a ME_LOG_DIR (then fix uninstall.sh) path = mprJoinPath(mprGetAppDir(), "../.port.log"); if ((fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666)) < 0) { mprLog("error appweb", 0, "Could not create port file %s", path); return MPR_ERR_CANT_CREATE; } fmt(numBuf, sizeof(numBuf), "%d", endpoint->port); len = (int) strlen(numBuf); numBuf[len++] = '\n'; if (write(fd, numBuf, len) != len) { mprLog("error appweb", 0, "Write to file %s failed", path); return MPR_ERR_CANT_WRITE; } close(fd); return 0; }
/* Search for a module "filename" in the modulePath. Return the result in "result" */ char *mprSearchForModule(cchar *filename) { #if BIT_HAS_DYN_LOAD char *path, *f, *searchPath, *dir, *tok; filename = mprNormalizePath(filename); /* Search for the path directly */ if ((path = probe(filename)) != 0) { mprLog(6, "Found native module %s at %s", filename, path); return path; } /* Search in the searchPath */ searchPath = sclone(mprGetModuleSearchPath()); tok = 0; dir = stok(searchPath, MPR_SEARCH_SEP, &tok); while (dir && *dir) { f = mprJoinPath(dir, filename); if ((path = probe(f)) != 0) { mprLog(6, "Found native module %s at %s", filename, path); return path; } dir = stok(0, MPR_SEARCH_SEP, &tok); } #endif /* BIT_HAS_DYN_LOAD */ return 0; }
PUBLIC int espSaveConfig(HttpRoute *route) { EspRoute *eroute; eroute = route->eroute; return mprSaveJson(eroute->config, mprJoinPath(route->documents, "config.json"), MPR_JSON_PRETTY); }
/* * Write the port so the monitor can manage */ static int writePort(MaHost *host) { char *cp, numBuf[16], *path; int fd, port, len; path = mprJoinPath(host, mprGetAppDir(host), "../.port.log"); if ((fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666)) < 0) { mprError(host, "Could not create port file %s\n", path); return MPR_ERR_CANT_CREATE; } cp = host->ipAddrPort; if ((cp = strchr(host->ipAddrPort, ':')) != 0) { port = atoi(++cp); } else { port = 80; } mprItoa(numBuf, sizeof(numBuf), port, 10); len = (int) strlen(numBuf); numBuf[len++] = '\n'; if (write(fd, numBuf, len) != len) { mprError(host, "Write to file %s failed\n", path); return MPR_ERR_CANT_WRITE; } close(fd); return 0; }
static int processThread(HttpConn *conn, MprEvent *event) { ThreadData *td; cchar *path; char *url; int next; td = mprGetCurrentThread()->data; httpFollowRedirects(conn, !app->nofollow); httpSetTimeout(conn, app->timeout, app->timeout); if (strcmp(app->protocol, "HTTP/1.0") == 0) { httpSetKeepAliveCount(conn, 0); httpSetProtocol(conn, "HTTP/1.0"); } if (app->username) { if (app->password == 0 && !strchr(app->username, ':')) { app->password = getPassword(); } httpSetCredentials(conn, app->username, app->password); } while (!mprShouldDenyNewRequests(conn) && (app->success || app->continueOnErrors)) { if (app->singleStep) waitForUser(); if (app->files && !app->upload) { for (next = 0; (path = mprGetNextItem(app->files, &next)) != 0; ) { /* If URL ends with "/", assume it is a directory on the target and append each file name */ if (app->target[strlen(app->target) - 1] == '/') { url = mprJoinPath(app->target, mprGetPathBase(path)); } else { url = app->target; } app->requestFiles = mprCreateList(-1, MPR_LIST_STATIC_VALUES); mprAddItem(app->requestFiles, path); td->url = url = resolveUrl(conn, url); if (app->verbose) { mprPrintf("putting: %s to %s\n", path, url); } if (doRequest(conn, url, app->requestFiles) < 0) { app->success = 0; break; } } } else { td->url = url = resolveUrl(conn, app->target); if (doRequest(conn, url, app->files) < 0) { app->success = 0; break; } } if (iterationsComplete()) { break; } } httpDestroyConn(conn); finishThread((MprThread*) event->data); return -1; }
PUBLIC int espSaveConfig(HttpRoute *route) { cchar *path; path = mprJoinPath(route->home, "esp.json"); #if KEEP mprBackupLog(path, 3); #endif return mprSaveJson(route->config, path, MPR_JSON_PRETTY | MPR_JSON_QUOTES); }
static int findConfig() { char *base, *filename; base = sclone("http.json"); filename = base; if (!mprPathExists(filename, R_OK)) { filename = mprJoinPath(mprGetPathParent(mprGetAppDir()), base); if (!mprPathExists(filename, R_OK)) { filename = mprJoinPath(mprGetPathParent(mprGetAppDir()), base); if (!mprPathExists(filename, R_OK)) { mprError("Cannot find config file %s", base); return MPR_ERR_CANT_OPEN; } } } app->configFile = filename; return 0; }
/* Path should be an app-relative path to the view file (relative-path.esp) */ void espDefineView(HttpRoute *route, cchar *path, void *view) { Esp *esp; mprAssert(path && *path); mprAssert(view); esp = MPR->espService; path = mprGetPortablePath(mprJoinPath(route->dir, path)); mprAddKey(esp->views, path, view); }
static char *getSearchPath(cchar *dir) { #if WIN // bin : . return sfmt("%s" MPR_SEARCH_SEP ".", dir); #else // bin : /usr/lib/appweb/bin : /usr/lib/appweb/lib : lib : . char *libDir = mprJoinPath(mprGetPathParent(dir), BLD_LIB_NAME); return sfmt("%s" MPR_SEARCH_SEP "%s" MPR_SEARCH_SEP ".", dir, mprSamePath(BLD_BIN_PREFIX, dir) ? BLD_LIB_PREFIX: libDir); #endif }
PUBLIC bool espRenderView(HttpConn *conn, cchar *target, int flags) { HttpRx *rx; HttpRoute *route; EspRoute *eroute; EspViewProc viewProc; rx = conn->rx; route = rx->route; eroute = route->eroute; #if !ME_STATIC if (!eroute->combine && (route->update || !mprLookupKey(eroute->top->views, target))) { cchar *errMsg; /* WARNING: GC yield */ target = sclone(target); mprHold(target); if (espLoadModule(route, conn->dispatcher, "view", mprJoinPath(route->documents, target), &errMsg) < 0) { mprRelease(target); return 0; } mprRelease(target); } #endif if ((viewProc = mprLookupKey(eroute->views, target)) == 0) { return 0; } if (!(flags & ESP_DONT_RENDER)) { if (route->flags & HTTP_ROUTE_XSRF) { /* Add a new unique security token */ httpAddSecurityToken(conn, 1); } httpSetContentType(conn, "text/html"); httpSetFilename(conn, mprJoinPath(route->documents, target), 0); /* WARNING: may GC yield */ (viewProc)(conn); } return 1; }
static int findConfigFile() { cchar *userPath; userPath = app->configFile; if (app->configFile == 0) { app->configFile = mprJoinPathExt(mprGetAppName(), ".conf"); } if (!mprPathExists(app->configFile, R_OK)) { if (!userPath) { app->configFile = mprJoinPath(app->home, "appweb.conf"); if (!mprPathExists(app->configFile, R_OK)) { app->configFile = mprJoinPath(mprGetAppDir(), sfmt("../%s/%s.conf", BLD_LIB_NAME, mprGetAppName())); } } if (!mprPathExists(app->configFile, R_OK)) { mprError("Can't open config file %s", app->configFile); return MPR_ERR_CANT_OPEN; } } return 0; }
PUBLIC void espDefineAction(HttpRoute *route, cchar *target, void *action) { EspRoute *eroute; Esp *esp; assert(route); assert(target && *target); assert(action); esp = MPR->espService; if (target) { eroute = route->eroute; mprAddKey(esp->actions, mprJoinPath(eroute->controllersDir, target), action); } }
static int findAppwebConf() { cchar *userPath; userPath = app->configFile; if (!app->configFile || *app->configFile == '\0') { app->configFile = mprJoinPathExt(mprGetAppName(), ".conf"); } #if !BIT_ROM if (!mprPathExists(app->configFile, R_OK)) { if (!userPath) { app->configFile = mprJoinPath(app->home, "appweb.conf"); if (!mprPathExists(app->configFile, R_OK)) { app->configFile = mprJoinPath(mprGetAppDir(), sfmt("%s.conf", mprGetAppName())); } } if (!mprPathExists(app->configFile, R_OK)) { mprError("Cannot open config file %s", app->configFile); return MPR_ERR_CANT_OPEN; } } #endif return 0; }
/* Path should be an app-relative path to the view file (relative-path.esp) */ PUBLIC void espDefineView(HttpRoute *route, cchar *path, void *view) { Esp *esp; assert(path && *path); assert(view); if (!route) { mprError("Route not defined for view %s", path); return; } esp = MPR->espService; path = mprGetPortablePath(mprJoinPath(route->documents, path)); mprAddKey(esp->views, path, view); }
/* Get local port used by Appweb */ static int getAppwebPort() { char *path, portBuf[32]; int fd; path = mprJoinPath(mprGetAppDir(), "../.port.log"); if ((fd = open(path, O_RDONLY, 0666)) < 0) { mprError("Could not read port file %s", path); return -1; } if (read(fd, portBuf, sizeof(portBuf)) < 0) { mprError("Read from port file %s failed", path); close(fd); return 80; } close(fd); return atoi(portBuf); }
void espDefineAction(HttpRoute *route, cchar *target, void *actionProc) { EspAction *action; EspRoute *eroute; Esp *esp; mprAssert(route); mprAssert(target && *target); mprAssert(actionProc); esp = MPR->espService; if ((action = mprAllocObj(EspAction, espManageAction)) == 0) { return; } action->actionProc = actionProc; if (target) { eroute = route->eroute; mprAddKey(esp->actions, mprJoinPath(eroute->controllersDir, target), action); } }
static int lstOpen(EjsMod *mp, char *moduleFilename, EjsModuleHdr *hdr) { char *path, *name, *ext; mprAssert(mp); name = mprGetPathBase(moduleFilename); if ((ext = strstr(name, EJS_MODULE_EXT)) != 0) { *ext = '\0'; } path = sjoin(name, EJS_LISTING_EXT, NULL); path = mprJoinPath(mp->outputDir, path); if ((mp->file = mprOpenFile(path, O_CREAT | O_WRONLY | O_TRUNC | O_BINARY, 0664)) == 0) { mprError("Can't create %s", path); return EJS_ERR; } mprEnableFileBuffering(mp->file, 0, 0); mprFprintf(mp->file, "#\n# %s -- Module Listing for %s\n#\n", path, moduleFilename); return 0; }
/* Set the platform objects location */ PUBLIC int httpSetPlatformDir(cchar *path) { Http *http; http = HTTP; if (path) { if (mprPathExists(path, X_OK)) { http->platformDir = mprGetAbsPath(path); } else { /* Possible source tree platform directory */ http->platformDir = mprJoinPath(mprGetPathDir(mprGetPathDir(mprGetPathDir(mprGetAppPath()))), path); if (!mprPathExists(http->platformDir, X_OK)) { http->platformDir = mprGetAbsPath(path); } } } else { http->platformDir = mprGetPathDir(mprGetPathDir(mprGetAppPath())); } return 0; }
static void genByteCodeHeader(Mpr *mpr, cchar *dir) { MprFile *file; EjsOptable *op; char *path; path = mprJoinPath(mpr, dir, "ejsByteCode.h"); file = mprOpenFile(mpr, path, O_WRONLY | O_CREAT | O_TRUNC, 0664); if (file == 0) { mprError(mpr, "Cannot open %s", path); return; } header(file, "ejsByteCode", "Ejscript VM Byte Code"); mprFprintf(file, "typedef enum EjsOpCode {\n"); for (op = ejsOptable; op->name; op++) { mprFprintf(file, " EJS_OP_%s,\n", op->name); } mprFprintf(file, "} EjsOpCode;\n"); footer(file); mprCloseFile(file); }
static void genByteGotoHeader(Mpr *mpr, cchar *dir) { MprFile *file; EjsOptable *op; char *path; path = mprJoinPath(mpr, dir, "ejsByteGoto.h"); file = mprOpenFile(mpr, path, O_WRONLY | O_CREAT | O_TRUNC, 0664); if (file == 0) { mprError(mpr, "Cannot open %s", path); return; } // header(file, "ejsByteGoto", "Ejscript Byte Code Jump Labels"); mprFprintf(file, "static void *opcodeJump[] = {\n"); for (op = ejsOptable; op->name; op++) { mprFprintf(file, " &&EJS_OP_%s,\n", op->name); } mprFprintf(file, "};\n"); // footer(file); mprCloseFile(file); }
/* * Create an initialize the Ejscript Web Framework control block */ static int initControlBlock() { control = mprAllocZeroed(mpr, sizeof(EjsWebControl)); if (control == 0) { return MPR_ERR_NO_MEMORY; } /* * These are the callbacks from the web framework into the gateway */ control->defineParams = defineParams; control->discardOutput = discardOutput; control->error = error; control->getHeader = getHeader; control->getVar = getVar; control->redirect = redirect; control->setCookie = setCookie; control->setHeader = setHeader; control->setHttpCode = setHttpCode; control->setMimeType = setMimeType; control->setVar = setVar; control->write = writeBlock; control->serverRoot = mprStrdup(control, "."); control->searchPath = mprJoinPath(control, control->serverRoot, "modules"); if (ejsOpenWebFramework(control, 0) < 0) { return EJS_ERR; } output = mprCreateBuf(mpr, EJS_CGI_MIN_BUF, EJS_CGI_MAX_BUF); headerOutput = mprCreateBuf(mpr, MPR_BUFSIZE, -1); if (output == 0 || headerOutput == 0) { return EJS_ERR; } return 0; }
/* Write the port so the monitor can manage */ static int writePort(MaServer *server) { HttpHost *host; char numBuf[16], *path; int fd, len; host = mprGetFirstItem(server->http->hosts); // TODO - should really go to a BLD_LOG_DIR path = mprJoinPath(mprGetAppDir(), "../.port.log"); if ((fd = open(path, O_CREAT | O_WRONLY | O_TRUNC, 0666)) < 0) { mprError("Could not create port file %s", path); return MPR_ERR_CANT_CREATE; } mprSprintf(numBuf, sizeof(numBuf), "%d", host->port); len = (int) strlen(numBuf); numBuf[len++] = '\n'; if (write(fd, numBuf, len) != len) { mprError("Write to file %s failed", path); return MPR_ERR_CANT_WRITE; } close(fd); return 0; }
static void testJoinPath(MprTestGroup *gp) { #if BIT_WIN_LIKE assert(strcmp(mprJoinPath("\\tmp", "Makefile"), "\\tmp\\Makefile") == 0); assert(strcmp(mprJoinPath("\\tmp", "\\Makefile"), "\\Makefile") == 0); assert(strcmp(mprJoinPath("\\tmp", NULL), "\\tmp") == 0); assert(strcmp(mprJoinPath("\\tmp", "."), "\\tmp") == 0); assert(strcmp(mprJoinPath("\\\\\\tmp\\\\\\", "Makefile\\\\\\"), "\\tmp\\Makefile") == 0); #else assert(strcmp(mprJoinPath("/tmp", "Makefile"), "/tmp/Makefile") == 0); assert(strcmp(mprJoinPath("/tmp", "/Makefile"), "/Makefile") == 0); assert(strcmp(mprJoinPath("/tmp", NULL), "/tmp") == 0); assert(strcmp(mprJoinPath("/tmp", "."), "/tmp") == 0); assert(strcmp(mprJoinPath("///tmp///", "Makefile///"), "/tmp/Makefile") == 0); #endif assert(strcmp(mprJoinPath("", "Makefile"), "Makefile") == 0); assert(strcmp(mprJoinPathExt("/abc", ".exe"), "/abc.exe") == 0); assert(strcmp(mprJoinPathExt("/abc.bat", ".exe"), "/abc.bat") == 0); assert(strcmp(mprJoinPathExt("/abc", ""), "/abc") == 0); assert(strcmp(mprJoinPathExt("ejs.web/file", ".mod"), "ejs.web/file.mod") == 0); }