diff --git a/NEWS.md b/NEWS.md index d51a206f6a7..556aeddbc09 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,24 +2,33 @@ ### New #### RStudio +- RStudio now supports code formatting using the 'styler' R package, as well via other external applications (#2563) - Use native file and message dialogs by default on Linux desktop (#14683; Linux Desktop) - Added www-socket option to rserver.conf to enable server to listen on a Unix domain socket (#14938; Open-Source Server) +- RStudio now supports syntax highlighting for Fortran source files (#10403) +- Display label "Publish" next to the publish icon on editor toolbar (#13604) +- RStudio supports `usethis.description` option values when creating projects via the RStudio New Project wizard (#15070) #### Posit Workbench - ### Fixed #### RStudio +- Fixed an issue in the data viewer where list-column cell navigation worked incorrectly when a search filter was active (#9960) +- Fixed an issue where debugger breakpoints did not function correctly in some cases with R 4.4 (#15072) +- Fixed an issue where autocompletion results within piped expressions were incorrect in some cases (#13611) - Fixed being unable to save file after cancelling the "Choose Encoding" window (#14896) - Fixed problems creating new files and projects on a UNC path (#14963, #14964; Windows Desktop) - Prevent attempting to start Copilot on a non-main thread (#14952) - Reformat Code no longer inserts whitespace around '^' operator (#14973) - Prompt for personal access token instead of password when using github via https (#14103) - RStudio now forward the current 'repos' option for actions taken in the Build pane (#5793) +- Executing `options(warn = ...)` in an R code chunk now persists beyond chunk execution (#15030) #### Posit Workbench -- +- Fixed an issue with Workbench login not respecting "Stay signed in when browser closes" when using Single Sign-On (rstudio-pro#5392) ### Dependencies -- Updated Electron to version 31.3.0 (#14982; Desktop) +- Updated GWT to version 2.10.1 (#15011) +- Updated Electron to version 31.4.0 (#14982; Desktop) diff --git a/dependencies/common/install-quarto b/dependencies/common/install-quarto index 2e7fa5a75fd..974d7ef6854 100755 --- a/dependencies/common/install-quarto +++ b/dependencies/common/install-quarto @@ -22,7 +22,7 @@ section "Installing Quarto" # variables that control download + installation process # specify a version to pin for releases -QUARTO_VERSION=1.5.54 +QUARTO_VERSION=1.5.57 # update to latest Quarto release # QUARTO_VERSION=`curl https://quarto.org/docs/download/_download.json | jq ".version" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+'` diff --git a/dependencies/tools/upload-quarto.sh b/dependencies/tools/upload-quarto.sh index 769fc1c99f6..9c90573d02f 100755 --- a/dependencies/tools/upload-quarto.sh +++ b/dependencies/tools/upload-quarto.sh @@ -5,7 +5,7 @@ # tools (awscli) installed, and configured with a valid AWS account. # Modify to set the Quarto version to upload -QUARTO_VERSION=1.5.54 +QUARTO_VERSION=1.5.57 # Check that we're logged in with AWS diff --git a/dependencies/windows/install-dependencies.cmd b/dependencies/windows/install-dependencies.cmd index 31db072001b..b4bcb6afd22 100644 --- a/dependencies/windows/install-dependencies.cmd +++ b/dependencies/windows/install-dependencies.cmd @@ -43,7 +43,7 @@ set PANDOC_NAME=pandoc-%PANDOC_VERSION% set PANDOC_FILE=%PANDOC_NAME%-windows-x86_64.zip REM Pin to specific Quarto version for releases -set QUARTO_VERSION=1.5.54 +set QUARTO_VERSION=1.5.57 REM Get latest Quarto release version REM cd install-quarto diff --git a/docs/news/index.qmd b/docs/news/index.qmd index c8315d85662..568c643274c 100644 --- a/docs/news/index.qmd +++ b/docs/news/index.qmd @@ -15,6 +15,15 @@ This page provides the release notes associated with each release of RStudio and >Date: 2024-06-10 +### New + +#### Posit Workbench + +- Added a Generic Architectures page to the Workbench Admin Guide +- Added an AWS Single Server reference architecture to the Workbench Admin Guide +- Added an AWS EKS reference architecture to the Workbench Admin Guide +- Added a Background Jobs vs Workbench jobs page to the Workbench Admin Guide + ### Fixed #### RStudio @@ -26,6 +35,7 @@ This page provides the release notes associated with each release of RStudio and #### Posit Workbench - Fixed an issue introduced in 2024.04.0 with the create-container-user feature for job launcher plugins that reuse a container for more than one session (rstudio/rstudio-pro#6408) +- Improved some AWS documentation links in the reference architecture section ### Dependencies diff --git a/git_hooks/secrets/.secrets.baseline b/git_hooks/secrets/.secrets.baseline index 06e3a5b8610..4d1859e4698 100644 --- a/git_hooks/secrets/.secrets.baseline +++ b/git_hooks/secrets/.secrets.baseline @@ -90,6 +90,10 @@ { "path": "detect_secrets.filters.allowlist.is_line_allowlisted" }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": "git_hooks/secrets/.secrets.baseline" + }, { "path": "detect_secrets.filters.heuristic.is_indirect_reference" }, @@ -463,7 +467,7 @@ "filename": "src/gwt/src/org/rstudio/core/client/ElementIds.java", "hashed_secret": "ebf2ef4cd605aceb5c72fa4f558d883b98fb58d8", "is_verified": false, - "line_number": 204, + "line_number": 205, "is_secret": false } ], @@ -513,7 +517,7 @@ "filename": "src/gwt/src/org/rstudio/studio/client/server/remote/RemoteServer.java", "hashed_secret": "1c60262d3df7b0c6d2d5c52dd5014c985004219f", "is_verified": false, - "line_number": 7203, + "line_number": 7227, "is_secret": false }, { @@ -521,7 +525,7 @@ "filename": "src/gwt/src/org/rstudio/studio/client/server/remote/RemoteServer.java", "hashed_secret": "12106b07b5b299b5e91117791ec7017f0820f84e", "is_verified": false, - "line_number": 7245, + "line_number": 7269, "is_secret": false } ], @@ -612,5 +616,5 @@ } ] }, - "generated_at": "2024-07-05T19:33:55Z" + "generated_at": "2024-07-23T07:03:14Z" } diff --git a/git_hooks/secrets/Dockerfile b/git_hooks/secrets/Dockerfile index 3841e121542..b6aaab16c56 100644 --- a/git_hooks/secrets/Dockerfile +++ b/git_hooks/secrets/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12.3-bookworm +FROM python:3.13.0b4-bookworm COPY requirements.txt /tmp/requirements.txt diff --git a/jenkins/Jenkinsfile.linux b/jenkins/Jenkinsfile.linux index 29398c75ced..92b9739573f 100644 --- a/jenkins/Jenkinsfile.linux +++ b/jenkins/Jenkinsfile.linux @@ -364,7 +364,7 @@ pipeline { script { if (FLAVOR == "Electron") { utils.publishToDailiesSite TAR_PACKAGE_FILE, "${DAILIES_PATH}-xcopy", AWS_PATH - utils.optionalPublishToDailies PACKAGE_FILE, "${DAILIES_PATH}-xcopy", AWS_PATH + utils.optionalPublishToDailies TAR_PACKAGE_FILE, "${DAILIES_PATH}-xcopy", AWS_PATH } } } @@ -441,7 +441,7 @@ pipeline { steps { build( wait: false, - job: "IDE/rstudio-ide-automation-desktop/main", + job: "IDE/qa-desktop-automation", parameters: [ string(name: 'RSTUDIO_VERSION', value: "${RSTUDIO_VERSION}"), string(name: 'GITHUB_BRANCH', value: "${env.BRANCH_NAME}") diff --git a/jenkins/Jenkinsfile.pull-request b/jenkins/Jenkinsfile.pull-request index e0ed2573dd8..d695cb0f082 100644 --- a/jenkins/Jenkinsfile.pull-request +++ b/jenkins/Jenkinsfile.pull-request @@ -35,8 +35,9 @@ pipeline { RSTUDIO_VERSION_FLOWER = readFile(file: 'version/RELEASE').replaceAll(" ", "-").toLowerCase().trim() IS_PRO = JOB_URL.contains('Pro') BASE_IMAGE = "jenkins/ide:pro-jammy-x86_64-${RSTUDIO_VERSION_FLOWER}" - // Passing true makes this return true if there are any changes outside of 'docs' - BUILD = utils.hasChangesIn('docs/', true) + // Invert the check and use regex - passing true makes this return true if + //there are any changes outside of 'docs' and 'version/news' + BUILD = utils.hasChangesIn('docs/|version/news/', true) } } } diff --git a/jenkins/utils.groovy b/jenkins/utils.groovy index d7a5b367410..3e47eb5a002 100644 --- a/jenkins/utils.groovy +++ b/jenkins/utils.groovy @@ -4,9 +4,11 @@ * Returns true if branch has changes in the specified path with the target branch. * If invertMatch is true, returns true if branch has changes that do not match the specified path. */ -boolean hasChangesIn(String module, boolean invertMatch = false) { +boolean hasChangesIn(String module, boolean invertMatch = false, boolean useRegex = false) { sh "echo 'Comparing changes in ${module} with ${env.CHANGE_TARGET}..${env.BRANCH_NAME}'" - grepArgs = invertMatch ? '-v' : '' + grepArgs = invertMatch ? 'v' : '' + grepArgs = useRegex ? 'P' : grepArgs + grepArgs = grepArgs.isEmpty() ? '' : "-${grepArgs}" mergeBase = sh( returnStdout: true, script: "git merge-base origin/${env.BRANCH_NAME} origin/${env.CHANGE_TARGET}").trim() return !env.CHANGE_TARGET || diff --git a/package/osx/scripts/package-lock.json b/package/osx/scripts/package-lock.json index e1b8a3ba9e7..e2be5b07f2d 100644 --- a/package/osx/scripts/package-lock.json +++ b/package/osx/scripts/package-lock.json @@ -1,11 +1,11 @@ { - "name": "relock-npm-lock-v2-BkXhFR", - "lockfileVersion": 2, + "name": "scripts", + "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "@electron/universal": "^1.5.1" + "@electron/universal": "2.0.1" } }, "node_modules/@electron/asar": { @@ -25,28 +25,50 @@ "node": ">=10.12.0" } }, + "node_modules/@electron/asar/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@electron/asar/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@electron/universal": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", - "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", + "integrity": "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==", "license": "MIT", "dependencies": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", + "@electron/asar": "^3.2.7", + "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" }, "engines": { - "node": ">=8.6" + "node": ">=16.4" } }, "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", "funding": [ { "type": "individual", @@ -57,19 +79,21 @@ "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" } ], + "license": "Apache-2.0", "dependencies": { "cross-spawn": "^7.0.1" }, "engines": { - "node": ">= 10" + "node": ">= 12.13.0" } }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">=10.0.0" } }, "node_modules/balanced-match": { @@ -95,28 +119,16 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", - "license": "MIT", - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "balanced-match": "^1.0.0" } }, "node_modules/commander": { @@ -138,6 +150,7 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -148,9 +161,10 @@ } }, "node_modules/debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -164,27 +178,49 @@ } }, "node_modules/dir-compare": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", - "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", + "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "license": "MIT", "dependencies": { - "buffer-equal": "^1.0.0", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dir-compare/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/dir-compare/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=14.14" } }, "node_modules/fs.realpath": { @@ -214,10 +250,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/inflight": { "version": "1.0.6", @@ -239,12 +298,14 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -253,21 +314,25 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" }, "node_modules/once": { "version": "1.4.0", @@ -278,6 +343,21 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -291,26 +371,30 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "license": "MIT", "dependencies": { + "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" + "xmlbuilder": "^15.1.1" }, "engines": { - "node": ">=6" + "node": ">=10.4.0" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -322,14 +406,16 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -338,6 +424,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -355,250 +442,25 @@ "license": "ISC" }, "node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", "engines": { - "node": ">=4.0" - } - } - }, - "dependencies": { - "@electron/asar": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz", - "integrity": "sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==", - "requires": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" + "node": ">=8.0" } }, - "@electron/universal": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-1.5.1.tgz", - "integrity": "sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw==", - "requires": { - "@electron/asar": "^3.2.1", - "@malept/cross-spawn-promise": "^1.1.0", - "debug": "^4.3.1", - "dir-compare": "^3.0.0", - "fs-extra": "^9.0.1", - "minimatch": "^3.0.4", - "plist": "^3.0.4" - } - }, - "@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", - "requires": { - "cross-spawn": "^7.0.1" - } - }, - "at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==" - }, - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", - "requires": { - "ms": "2.1.2" - } - }, - "dir-compare": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", - "integrity": "sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==", - "requires": { - "buffer-equal": "^1.0.0", - "minimatch": "^3.0.4" - } - }, - "fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^2.0.0" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" - }, - "plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==", - "requires": { - "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" - }, - "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" - }, - "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" } } } diff --git a/package/osx/scripts/package.json b/package/osx/scripts/package.json index 200be112b8a..b3a3c939e4f 100644 --- a/package/osx/scripts/package.json +++ b/package/osx/scripts/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@electron/universal": "1.5.1" + "@electron/universal": "2.0.1" } } diff --git a/src/cpp/core/include/core/system/PosixSystem.hpp b/src/cpp/core/include/core/system/PosixSystem.hpp index 3f373a2bb9f..1e2fa2b41f6 100644 --- a/src/cpp/core/include/core/system/PosixSystem.hpp +++ b/src/cpp/core/include/core/system/PosixSystem.hpp @@ -156,7 +156,8 @@ void setProcessLimits(ProcessLimits limits); struct ProcessConfig { ProcessConfig() - : stdStreamBehavior(StdStreamInherit) + : stdStreamBehavior(StdStreamInherit), + user(User(true)) { } core::system::Options args; @@ -164,6 +165,7 @@ struct ProcessConfig std::string stdInput; StdStreamBehavior stdStreamBehavior; ProcessLimits limits; + User user; }; core::Error waitForProcessExit(PidType processId); diff --git a/src/cpp/core/system/PosixChildProcess.cpp b/src/cpp/core/system/PosixChildProcess.cpp index 09e8938e721..b5b9b0fcf95 100644 --- a/src/cpp/core/system/PosixChildProcess.cpp +++ b/src/cpp/core/system/PosixChildProcess.cpp @@ -445,6 +445,11 @@ bool ChildProcess::hasRecentOutput() const Error ChildProcess::run() { + // verify that the executable pointed at via 'exe_' is executable + int status = ::access(exe_.c_str(), X_OK); + if (status == -1) + return systemError(errno, ERROR_LOCATION); + // declarations PidType pid = 0; int fdInput[2] = {0,0}; @@ -460,9 +465,10 @@ Error ChildProcess::run() std::vector args; args.push_back(exe_); args.insert(args.end(), args_.begin(), args_.end()); + using core::system::ProcessArgs; - ProcessArgs* pProcessArgs = new ProcessArgs(args); - ProcessArgs* pEnvironment = nullptr; + std::unique_ptr pProcessArgs(new ProcessArgs(args)); + std::unique_ptr pEnvironment = nullptr; // get rlimit for max files // in the thread-safe fork approach, this needs to be provided @@ -477,12 +483,12 @@ Error ChildProcess::run() // build env (on heap, see comment above) std::vector env; const Options& options = options_.environment.get(); - for (Options::const_iterator - it = options.begin(); it != options.end(); ++it) + for (auto it = options.begin(); it != options.end(); ++it) { env.push_back(it->first + "=" + it->second); } - pEnvironment = new ProcessArgs(env); + + pEnvironment.reset(new ProcessArgs(env)); } boost::optional runAsUser; @@ -813,30 +819,34 @@ Error ChildProcess::run() LOG_ERROR(systemError(errno, ERROR_LOCATION)); #endif } - + + int savedErrno = -1; if (options_.environment) { // execute ::execve(exe_.c_str(), pProcessArgs->args(), pEnvironment->args()); + savedErrno = errno; } else { // execute ::execv(exe_.c_str(), pProcessArgs->args()); + savedErrno = errno; } + // in the normal case control should never return from execv (it starts + // anew at main of the process pointed to by path). therefore, if we get + // here then there was an error if (!options_.threadSafe) { - // in the normal case control should never return from execv (it starts - // anew at main of the process pointed to by path). therefore, if we get - // here then there was an error Error error = systemError(errno, ERROR_LOCATION); error.addProperty("exe", exe_); LOG_ERROR(error); - ::exit(EXIT_FAILURE); } - else - ::_exit(errno); + + // a forked child should quit using _exit -- otherwise, we can run into + // hangs and other surprising issues when static destructors are run + ::_exit(savedErrno == -1 ? EXIT_FAILURE : savedErrno); } // parent @@ -867,9 +877,6 @@ Error ChildProcess::run() pImpl_->init(pid, fdInput[WRITE], fdOutput[READ], fdError[READ]); } - delete pProcessArgs; - delete pEnvironment; - if (options_.threadSafe) { // send the list of the child proc's fds to the child so diff --git a/src/cpp/core/system/PosixSystem.cpp b/src/cpp/core/system/PosixSystem.cpp index 138641cc810..fc3d7d74513 100644 --- a/src/cpp/core/system/PosixSystem.cpp +++ b/src/cpp/core/system/PosixSystem.cpp @@ -2214,6 +2214,27 @@ Error launchChildProcess(std::string path, ProcessConfigFilter configFilter, PidType* pProcessId) { + // Ensure the config.user is populated before the fork so runProcess is not accessing the password db in the + // weird after-fork-before-exec state (i.e. skip the getCurrentUser call in runProcess) + if (config.user.isEmpty()) + { + if (!runAsUser.empty()) + { + Error error = getUserFromUsername(runAsUser, config.user); + if (error) + { + LOG_DEBUG_MESSAGE("Error from getUserFromUsername in launchChildProcess: " + error.asString()); + return error; + } + } + else + { + Error error = User::getCurrentUser(config.user); + if (error) + return error; + } + } + PidType pid = ::fork(); // error @@ -2286,12 +2307,21 @@ Error runProcess(const std::string& path, if (error) return error; - // get current user (before closing file handles since - // we might be using a PAM module that has open FDs...) User user; - error = User::getCurrentUser(user); - if (error) - return error; + + // Hopefully, not calling getCurrentUser since this is "after fork, before exec" and life here is wonky + if (config.user.isEmpty()) + { + // get current user (before closing file handles since + // we might be using a PAM module that has open FDs...) + error = User::getCurrentUser(user); + if (error) + return error; + } + else + { + user = config.user; + } // if we don't have privilege, the only user we can run as is ourselves, so if runAsUser is // specified, make sure it's the same account we're running from. diff --git a/src/cpp/r/R/Tools.R b/src/cpp/r/R/Tools.R index 46eba57641a..f3efeaa7db5 100644 --- a/src/cpp/r/R/Tools.R +++ b/src/cpp/r/R/Tools.R @@ -1311,7 +1311,7 @@ environment(.rs.Env[[".rs.addFunction"]]) <- .rs.Env .rs.addFunction("heredoc", function(text, ...) { # remove leading, trailing whitespace - trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) + trimmed <- gsub("^[^\\S\\r\\n]*\\n|\\n[^\\S\\r\\n]$", "", text, perl = TRUE) # split into lines lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] @@ -1332,6 +1332,7 @@ environment(.rs.Env[[".rs.addFunction"]]) <- .rs.Env rendered }) +# Wait until some 'predicate()' expression returns TRUE .rs.addFunction("waitUntil", function(reason, predicate, retryCount = 100L, @@ -1354,6 +1355,24 @@ environment(.rs.Env[[".rs.addFunction"]]) <- .rs.Env stop(sprintf("timed out waiting until '%s'", reason)) }) +# Wait for a callback to return a non-error result, +# and then produce that result after finishing +.rs.addFunction("waitFor", function(reason, + callback, + retryCount = 100L, + waitTimeSecs = 1) +{ + result <- NULL + + .rs.waitUntil(reason, function() + { + result <<- tryCatch(callback(), error = identity) + !inherits(result, "error") + }, retryCount = retryCount, waitTimeSecs = waitTimeSecs) + + result +}) + .rs.addFunction("bugReport", function(pro = NULL) { # collect information about the running version of R / RStudio @@ -1460,6 +1479,40 @@ environment(.rs.Env[[".rs.addFunction"]]) <- .rs.Env utils::browseURL(url) }) +.rs.addFunction("mapChr", function(x, f, ...) +{ + f <- match.fun(f) + vapply(x, f, ..., FUN.VALUE = character(1)) +}) + +.rs.addFunction("mapDbl", function(x, f, ...) +{ + f <- match.fun(f) + vapply(x, f, ..., FUN.VALUE = double(1)) +}) + +.rs.addFunction("mapInt", function(x, f, ...) +{ + f <- match.fun(f) + vapply(x, f, ..., FUN.VALUE = integer(1)) +}) + +.rs.addFunction("mapLgl", function(x, f, ...) +{ + f <- match.fun(f) + vapply(x, f, ..., FUN.VALUE = logical(1)) +}) + +# An R data.frame may have so-called "compact row names", where +# row names are set with an integer placeholder that defines the +# number of rows in the data.frame, without actually having a +# fully materialized vector of that length. +.rs.addFunction("hasCompactRowNames", function(data) +{ + info <- .row_names_info(data, type = 0L) + is.integer(info) && length(info) == 2L && is.na(info[[1L]]) +}) + .rs.addFunction("initTools", function() { ostype <- .Platform$OS.type diff --git a/src/cpp/r/ROptions.cpp b/src/cpp/r/ROptions.cpp index fb8e0b5d624..bf98cd692aa 100644 --- a/src/cpp/r/ROptions.cpp +++ b/src/cpp/r/ROptions.cpp @@ -80,15 +80,36 @@ int getBuildOptionWidth() return s_buildWidth; } -SEXP getOption(const std::string& name) +SEXP getOptionCell(const std::string& name) { if (!ASSERT_MAIN_THREAD("Reading R option: " + name)) { return R_NilValue; } - // NOTE: Values returned from Rf_GetOption() are protected implicitly by R - return Rf_GetOption(Rf_install(name.c_str()), R_BaseEnv); + // keep reference to R options list + static SEXP optionsSEXP = + Rf_findVarInFrame(R_BaseNamespace, Rf_install(".Options")); + + // we search through the options list directly and return + // the underlying value to avoid duplicating the underlying + // R object -- this allows us to detect changes if necessary + for (SEXP elSEXP = optionsSEXP; + elSEXP != R_NilValue; + elSEXP = CDR(elSEXP)) + { + SEXP tagSEXP = TAG(elSEXP); + if (CHAR(PRINTNAME(tagSEXP)) == name) + return elSEXP; + } + + return R_NilValue; +} + +SEXP getOption(const std::string& name) +{ + SEXP cellSEXP = getOptionCell(name); + return CAR(cellSEXP); } SEXP setErrorOption(SEXP value) diff --git a/src/cpp/r/include/r/ROptions.hpp b/src/cpp/r/include/r/ROptions.hpp index 2e3b6f618b3..cba087f4bf0 100644 --- a/src/cpp/r/include/r/ROptions.hpp +++ b/src/cpp/r/include/r/ROptions.hpp @@ -50,8 +50,8 @@ int getOptionWidth(); void setBuildOptionWidth(int width); int getBuildOptionWidth(); -// generic get and set - +// generic get and set +SEXP getOptionCell(const std::string& name); SEXP getOption(const std::string& name); template diff --git a/src/cpp/r/session/RSession.cpp b/src/cpp/r/session/RSession.cpp index 221ef1e2408..79a2824fafb 100644 --- a/src/cpp/r/session/RSession.cpp +++ b/src/cpp/r/session/RSession.cpp @@ -34,14 +34,15 @@ #include #include -#include -#include +#include #include +#include +#include +#include #include #include -#include -#include #include +#include #include #include #include @@ -68,9 +69,6 @@ #include -extern "C" { -int Rf_countContexts(int, int); -} #define CTXT_BROWSER 16 // get rid of windows TRUE and FALSE definitions @@ -507,7 +505,16 @@ bool isSuspendable(const std::string& currentPrompt) bool browserContextActive() { - return Rf_countContexts(CTXT_BROWSER, 1) > 0; + using namespace r::context; + for (auto it = RCntxt::begin(); it != RCntxt::end(); ++it) + { + if (it->callflag() & CTXT_BROWSER) + { + return true; + } + } + + return false; } namespace utils { diff --git a/src/cpp/server/db/README.md b/src/cpp/server/db/README.md index f379ab9bdb7..44a21e531b5 100644 --- a/src/cpp/server/db/README.md +++ b/src/cpp/server/db/README.md @@ -26,4 +26,27 @@ In order to run any new schemas, you simply need to restart `rstudio-server` to ### SQL Naming -Within your schema files, make sure to use `snake_case` naming for all table and column names. There are several reasons for preferring this. For more information, see https://github.com/rstudio/rstudio/issues/65899. +Within your schema files, make sure to use `snake_case` naming for all table and column names. There are several reasons for preferring this. For more information, see https://github.com/rstudio/rstudio/issues/6589. + +### Make Compatible Schema Changes + +Ensure old code will work with the new schema for when users upgrade and downgrade. Use default values that makes sense for new columns. Always use column names in the schema (no wildcards), no destructive changes, no name changes. It's ok to abandon columns, that could eventually get cleaned up once affected versions are out of support. + +## Documentation + +When you change the database schema, ensure you also update the data dictionary in the documentation, found in `docs/server/data_dictionary`. + +### Updating Schema Migration Tests + +For each workbench version where there's a schema change, we generate a database dump of the previous version to test against the 'alter' script we are adding. + +Generating these dumps is automated. Run the script: +``` +./build-version-dump.sh +``` +It will prompt you for the previous version's flower and version number, i.e. the current released version, and postgres user/password. It must be run on a system with psql and sqlite3 installed. It generates +files for the previous version in src/cpp/server/db/test that you commit with your schema changes. + +Also update `ServerDatabaseMigrationTests.cpp` and `ServerDatabaseDataset.hpp` to add to the enum and and where it points to the new files in db/test that were just created. + +If you are making the schema change in OS, run the script OS with the OS version, then again once the changes have been merged to pro with the pro version. The database dumps are specific to the CreateTables files that are different in OS and pro and so must be generated separately on each branch. diff --git a/src/cpp/server/db/build-version-dump.sh b/src/cpp/server/db/build-version-dump.sh new file mode 100755 index 00000000000..0f9e5e76f1b --- /dev/null +++ b/src/cpp/server/db/build-version-dump.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +if [ ! -d test ] ; then + echo "Must be run in the /src/cpp/server/db directory" + exit 1 +fi + +echo "This script is used to create database dumps from the previous release for testing schema upgrades." +echo "Run it the first time you make a schema change for any given release (i.e. when you run make-schema.sh)" +echo "Run it in the repo where the alter script is being committed - either rstudio or rstudio-pro" +echo "If running it in rstudio, run it again in rstudio-pro after merging" +echo "Normally, provide the name and version number of the last release. It uses git to get the" +echo "CreateTables files and populates temp databases with them that are then dumped into db/test to be added to your PR." + +read -p "Enter the flower name of the *released* version e.g. Chocolate Cosmos: " flowerName +read -p "Enter the version string, e.g. 2024.04.2+764 for rstudio, otherwise 2024.04.2+764.pro1: " relVersion +read -p "Enter postgres database user: " psqluser +read -p "Enter postgres database password: " PGPASSWORD + +encodedFlower=$(echo $flowerName | tr '[:upper:]' '[:lower:]' | tr -s ' ' '-') +versionSuffix=${encodedFlower}-$(echo $relVersion | tr '+' '-' | sed -e s/.pro.*//) + +if [[ "$relVersion" == *"pro"* ]]; then + versionSuffix="${versionSuffix}.workbench" + echo "Generating schema files for workbench" +else + echo "Generating schema files for open source" +fi + +echo "version-suffix: $versionSuffix" + +if git show v${relVersion}:./CreateTables.postgresql > /tmp/${versionSuffix}.psql ; then + echo "Found postgres schema for ${relVersion}" +else + echo "Failed to find CreateTables.postgresql with tag: ${relVersion}" + exit +fi + +export PGPASSWORD + +db_name=tmp_db_$$ + +# Create a new database +if createdb -U $psqluser $db_name ; then + echo "Temp database created successfully" +else + echo "Failed to create database with: $psqluser and $PGPASSWORD" + exit 1 +fi + + +# Import .sql file into the new database +psql -U $psqluser -d $db_name -f /tmp/${versionSuffix}.psql + +# Create a dump file of the newly created database +pg_dump -U $psqluser $db_name > "./test/${versionSuffix}.postgresql" + +if git show v${relVersion}:./CreateTables.sqlite > /tmp/${versionSuffix}.sqlite ; then + echo "Found sqlite schema for ${relVersion}" +else + echo "Failed to find CreateTables.sqlite with tag: ${relVersion}" + exit +fi + +# Create the SQLite database and import the SQL file +sqlite3 "/tmp/${db_name}.db" < /tmp/${versionSuffix}.sqlite + +# Dump the database contents to another file +sqlite3 "/tmp/${db_name}.db" ".output ./test/${versionSuffix}.sqlite.sql" ".dump" diff --git a/src/cpp/session/SessionClientEvent.cpp b/src/cpp/session/SessionClientEvent.cpp index 8e0c904262d..c3e903ac1a9 100644 --- a/src/cpp/session/SessionClientEvent.cpp +++ b/src/cpp/session/SessionClientEvent.cpp @@ -213,6 +213,8 @@ const int kPresentationPreview = 195; const int kSuspendBlocked = 196; const int kClipboardAction = 197; const int kDeploymentRecordsUpdated = 198; +const int kRunAutomation = 199; + } void ClientEvent::init(int type, const json::Value& data) @@ -594,6 +596,8 @@ std::string ClientEvent::typeName() const return "clipboard_action"; case client_events::kDeploymentRecordsUpdated: return "deployment_records_updated"; + case client_events::kRunAutomation: + return "run_automation"; default: LOG_WARNING_MESSAGE("unexpected event type: " + safe_convert::numberToString(type_)); diff --git a/src/cpp/session/SessionClientInit.cpp b/src/cpp/session/SessionClientInit.cpp index d80534946f8..f9ead19c5cd 100644 --- a/src/cpp/session/SessionClientInit.cpp +++ b/src/cpp/session/SessionClientInit.cpp @@ -78,6 +78,8 @@ #include +#include "SessionConsoleInput.hpp" + #include "session-config.h" #ifdef RSTUDIO_SERVER @@ -103,7 +105,7 @@ std::string userIdentityDisplay(const http::Request& request) } #ifdef RSTUDIO_SERVER -Error makePortTokenCookie(boost::shared_ptr ptrConnection, +Error makePortTokenCookie(boost::shared_ptr ptrConnection, http::Response& response) { // extract the base URL @@ -160,9 +162,9 @@ Error makePortTokenCookie(boost::shared_ptr ptrConnection, // create the cookie; don't set an expiry date as this will be a session cookie http::Cookie cookie( - ptrConnection->request(), - kPortTokenCookie, - persistentState().portToken(), + ptrConnection->request(), + kPortTokenCookie, + persistentState().portToken(), path, options().sameSite(), true, // HTTP only -- client doesn't get to read this token @@ -181,15 +183,15 @@ void handleClientInit(const boost::function& initFunction, { // notify that we're about to initialize module_context::events().onBeforeClientInit(); - + // alias options Options& options = session::options(); - - // check for valid CSRF headers in server mode - if (options.programMode() == kSessionProgramModeServer && + + // check for valid CSRF headers in server mode + if (options.programMode() == kSessionProgramModeServer && !core::http::validateCSRFHeaders(ptrConnection->request())) { - LOG_WARNING_MESSAGE("Client init request to " + ptrConnection->request().uri() + + LOG_WARNING_MESSAGE("Client init request to " + ptrConnection->request().uri() + " has missing or mismatched " + std::string(kCSRFTokenCookie) + " cookie or " + std::string(kCSRFTokenHeader) + " header"); // Send an error that shows up in the alert box of the browser - if we send unauthorized here, it causes an infinite sign in loop @@ -216,7 +218,7 @@ void handleClientInit(const boost::function& initFunction, // client. so, clear out the events which might be pending in the // client event service and/or queue bool clearEvents = resumed; - + // reset the client event service for the new client (will cause // outstanding http requests from old clients to fail with // InvalidClientId). note that we can't simply stop() the @@ -233,7 +235,7 @@ void handleClientInit(const boost::function& initFunction, // set RSTUDIO_USER_IDENTITY_DISPLAY environment variable based on // header value (complements RSTUDIO_USER_IDENTITY) - core::system::setenv("RSTUDIO_USER_IDENTITY_DISPLAY", + core::system::setenv("RSTUDIO_USER_IDENTITY_DISPLAY", userIdentityDisplay(ptrConnection->request())); // read display name from upstream if set @@ -245,7 +247,7 @@ void handleClientInit(const boost::function& initFunction, } } - // prepare session info + // prepare session info json::Object sessionInfo; sessionInfo["clientId"] = clientId; sessionInfo["mode"] = options.programMode(); @@ -255,7 +257,7 @@ void handleClientInit(const boost::function& initFunction, initOptions["restore_workspace"] = options.rRestoreWorkspace(); initOptions["run_rprofile"] = options.rRunRprofile(); sessionInfo["init_options"] = initOptions; - + sessionInfo["userIdentity"] = userIdentityDisplay(ptrConnection->request()); sessionInfo["systemUsername"] = core::system::username(); @@ -268,47 +270,47 @@ void handleClientInit(const boost::function& initFunction, // R_LIBS_USER sessionInfo["r_libs_user"] = module_context::rLibsUser(); - + // user home path sessionInfo["user_home_path"] = session::options().userHomePath().getAbsolutePath(); - + // installed client version sessionInfo["client_version"] = http_methods::clientVersion(); - + // default prompt - sessionInfo["prompt"] = rstudio::r::options::getOption("prompt"); + sessionInfo["prompt"] = module_context::rPrompt(); // client state json::Object clientStateObject; rstudio::r::session::clientState().currentState(&clientStateObject); sessionInfo["client_state"] = clientStateObject; - + // source documents json::Array jsonDocs; Error error = modules::source::clientInitDocuments(&jsonDocs); if (error) LOG_ERROR(error); sessionInfo["source_documents"] = jsonDocs; - + // docs url sessionInfo["docsURL"] = session::options().docsURL(); // get alias to console_actions and get limit rstudio::r::session::ConsoleActions& consoleActions = rstudio::r::session::consoleActions(); sessionInfo["console_actions_limit"] = consoleActions.capacity(); - + // check if reticulate's Python session has been initialized sessionInfo["python_initialized"] = modules::reticulate::isPythonInitialized(); - + // check if the Python REPL is active sessionInfo["python_repl_active"] = modules::reticulate::isReplActive(); - + // propagate RETICULATE_PYTHON if set std::string reticulate_python = core::system::getenv("RETICULATE_PYTHON"); if (reticulate_python.empty()) reticulate_python = core::system::getenv("RETICULATE_PYTHON_FALLBACK"); sessionInfo["reticulate_python"] = reticulate_python; - + // get current console language sessionInfo["console_language"] = modules::reticulate::isReplActive() ? "Python" : "R"; @@ -367,7 +369,7 @@ void handleClientInit(const boost::function& initFunction, sessionInfo["have_advanced_step_commands"] = modules::breakpoints::haveAdvancedStepCommands(); - + // initial working directory std::string initialWorkingDir = module_context::createAliasedPath( dirs::getInitialWorkingDirectory()); @@ -387,9 +389,9 @@ void handleClientInit(const boost::function& initFunction, sessionInfo["active_project_name"] = projects::projectContext().projectName(); sessionInfo["project_ui_prefs"] = projects::projectContext().uiPrefs(); sessionInfo["project_open_docs"] = projects::projectContext().openDocs(); - sessionInfo["project_supports_sharing"] = + sessionInfo["project_supports_sharing"] = projects::projectContext().supportsSharing(); - sessionInfo["project_parent_browseable"] = + sessionInfo["project_parent_browseable"] = projects::projectContext().parentBrowseable(); sessionInfo["project_user_data_directory"] = module_context::createAliasedPath( @@ -473,12 +475,12 @@ void handleClientInit(const boost::function& initFunction, sessionInfo["has_pkg_vig"] = false; } - sessionInfo["blogdown_config"] = modules::rmarkdown::blogdown::blogdownConfig(); + sessionInfo["blogdown_config"] = modules::rmarkdown::blogdown::blogdownConfig(!console_input::executing()); sessionInfo["is_bookdown_project"] = module_context::isBookdownProject(); sessionInfo["is_distill_project"] = module_context::isDistillProject(); sessionInfo["quarto_config"] = quarto::quartoConfigJSON(); - + sessionInfo["graphics_backends"] = modules::graphics::supportedBackends(); sessionInfo["presentation_state"] = modules::presentation::presentationStateAsJson(); @@ -534,7 +536,7 @@ void handleClientInit(const boost::function& initFunction, // allow opening shared projects if it's enabled, and if there's shared // storage from which we can discover the shared projects - sessionInfo["allow_open_shared_projects"] = + sessionInfo["allow_open_shared_projects"] = core::system::getenv(kRStudioDisableProjectSharing).empty() && !options.getOverlayOption(kSessionSharedStoragePath).empty(); @@ -560,9 +562,9 @@ void handleClientInit(const boost::function& initFunction, modules::rmarkdown::rmarkdownPackageAvailable(); sessionInfo["knit_params_available"] = modules::rmarkdown::knitParamsAvailable(); - sessionInfo["knit_working_dir_available"] = + sessionInfo["knit_working_dir_available"] = modules::rmarkdown::knitWorkingDirAvailable(); - sessionInfo["ppt_available"] = + sessionInfo["ppt_available"] = modules::rmarkdown::pptAvailable(); sessionInfo["clang_available"] = modules::clang::isAvailable(); @@ -582,7 +584,7 @@ void handleClientInit(const boost::function& initFunction, sessionInfo["show_user_home_page"] = options.showUserHomePage(); sessionInfo["user_home_page_url"] = json::Value(); - + sessionInfo["r_addins"] = modules::r_addins::addinRegistryAsJson(); sessionInfo["package_provided_extensions"] = modules::ppe::indexer().getPayload(); @@ -597,7 +599,7 @@ void handleClientInit(const boost::function& initFunction, sessionInfo["drivers_support_licensing"] = options.supportsDriversLicensing(); sessionInfo["quit_child_processes_on_exit"] = options.quitChildProcessesOnExit(); - + sessionInfo["git_commit_large_file_size"] = options.gitCommitLargeFileSize(); sessionInfo["default_rsconnect_server"] = options.defaultRSConnectServer(); @@ -650,13 +652,13 @@ void handleClientInit(const boost::function& initFunction, // session route for load balanced sessions sessionInfo["session_node"] = session::modules::overlay::sessionNode(); - + // copilot sessionInfo["copilot_enabled"] = options.copilotEnabled(); - + // automation agent sessionInfo["is_automation_agent"] = options.isAutomationAgent(); - + if (projects::projectContext().hasProject()) { projects::RProjectCopilotOptions options; @@ -701,10 +703,10 @@ void handleClientInit(const boost::function& initFunction, // complete initialization of session init::ensureSessionInitialized(); - + // notify modules of the client init module_context::events().onClientInit(); - + // call the init function initFunction(); diff --git a/src/cpp/session/SessionConsoleInput.cpp b/src/cpp/session/SessionConsoleInput.cpp index c3c268407f3..991c891b0e2 100644 --- a/src/cpp/session/SessionConsoleInput.cpp +++ b/src/cpp/session/SessionConsoleInput.cpp @@ -110,7 +110,7 @@ void enqueueConsoleInput(const rstudio::r::session::RConsoleInput& input) data[kConsoleText] = input.text + "\n"; data[kConsoleId] = input.console; data[kConsoleFlags] = input.flags; - + ClientEvent inputEvent(client_events::kConsoleWriteInput, data); clientEventQueue().add(inputEvent); } @@ -123,25 +123,25 @@ bool canSuspend(const std::string& prompt) std::string override = core::system::getenv("RS_IDLE_SUSPEND_ENABLED"); return string_utils::isTruthy(override, true); })(); - + if (!idleSuspendEnabled) return false; #endif - + bool suspendIsBlocked = false; - + suspendIsBlocked |= session::suspend::checkBlockingOp(main_process::haveDurableChildren(), suspend::kChildProcess); suspendIsBlocked |= session::suspend::checkBlockingOp(!modules::jobs::isSuspendable(), suspend::kActiveJob); suspendIsBlocked |= session::suspend::checkBlockingOp(!rstudio::r::session::isSuspendable(prompt), suspend::kCommandPrompt); if (session::options().sessionConnectionsBlockSuspend()) suspendIsBlocked |= session::suspend::checkBlockingOp(!modules::connections::isSuspendable(), suspend::kConnection); - + if (session::options().sessionExternalPointersBlockSuspend()) suspendIsBlocked |= session::suspend::checkBlockingOp(!modules::environment::isSuspendable(), suspend::kExternalPointer); - + suspendIsBlocked |= !modules::overlay::isSuspendable(); - + return !suspendIsBlocked; } @@ -156,14 +156,14 @@ void consolePrompt(const std::string& prompt, bool addToHistory) json::Object data; data["prompt"] = prompt; data["history"] = addToHistory; - bool isDefaultPrompt = - prompt == rstudio::r::options::getOption("prompt"); + bool isDefaultPrompt = + prompt == module_context::rPrompt(); data["default"] = isDefaultPrompt; data["language"] = modules::reticulate::isReplActive() ? "Python" : "R"; - + ClientEvent consolePromptEvent(client_events::kConsolePrompt, data); clientEventQueue().add(consolePromptEvent); - + // allow modules to detect changes after execution of previous REPL module_context::events().onDetectChanges(module_context::ChangeSourceREPL); @@ -183,19 +183,19 @@ Error extractConsoleInput(const json::JsonRpcRequest& request) std::string text; std::string console; int flags = 0; - + Error error = core::json::readParams( request.params, &text, &console, &flags); - + if (error) return error; - + using namespace r::session; addToConsoleInputBuffer(RConsoleInput(text, console, flags)); - + return Success(); } @@ -233,47 +233,47 @@ namespace { void fixupPendingConsoleInput() { using namespace r::session; - + // get next input auto input = s_consoleInputBuffer.front(); - + // nothing to do if this is a cancel if (input.isCancel() || input.isEof()) return; - + // if this has no newlines, then nothing to do auto index = input.text.find('\n'); if (index == std::string::npos) return; - + // if we're about to send code to the Python REPL, then // we need to fix whitespace in the code before sending bool pyReplActive = modules::reticulate::isReplActive(); - + // pop off current input (we're going to split and re-push now) s_consoleInputBuffer.pop_front(); - + // does this Python line start an indented block? // NOTE: should consider using tokenizer here boost::regex reBlockStart(":\\s*(?:#|$)"); - + // used to detect whitespace-only lines boost::regex reWhitespace("^\\s*$"); - + // keep track of the indentation used for the current block // of Python code (default to no indent) std::string blockIndent; - + // pending console input (we'll need to push this to the front of the queue) std::vector pendingInputs; - + // split input into list of commands std::vector lines = core::algorithm::split(input.text, "\n"); for (std::size_t i = 0, n = lines.size(); i < n; i++) { // get current line std::string line = lines[i]; - + // fix up indentation if necessary if (pyReplActive) { @@ -284,14 +284,14 @@ void fixupPendingConsoleInput() { line = blockIndent; } - + // if this line would exit the reticulate REPL, then update that state else if (line == "quit" || line == "exit") { blockIndent.clear(); pyReplActive = false; } - + // if it looks like we're starting a new Python block, // then update our indent. perform a lookahead for the // next non-blank line, and use that line's indent @@ -300,7 +300,7 @@ void fixupPendingConsoleInput() for (std::size_t j = i + 1; j < n; j++) { const std::string& lookahead = lines[j]; - + // skip blank / whitespace-only lines, to allow // for cases like: // @@ -312,12 +312,12 @@ void fixupPendingConsoleInput() // the function definition and the start of its body if (regex_utils::match(lookahead, reWhitespace)) continue; - + blockIndent = string_utils::extractIndent(lookahead); break; } } - + // if the indent for this line has _decreased_, then we've // closed an inner block; e.g. for something like: // @@ -343,13 +343,13 @@ void fixupPendingConsoleInput() pyReplActive = true; } } - + // add to buffer pendingInputs.push_back( RConsoleInput(line, input.console, input.flags)); - + } - + // now push the pending inputs to the front of the queue for (auto it = pendingInputs.rbegin(); it != pendingInputs.rend(); @@ -392,7 +392,7 @@ bool rConsoleRead(const std::string& prompt, { popConsoleInput(pConsoleInput); } - + // otherwise prompt and wait for console_input from the client else { @@ -447,7 +447,7 @@ bool rConsoleRead(const std::string& prompt, pConsoleInput->text); } - if (!pConsoleInput->isNoEcho()) + if (!pConsoleInput->isNoEcho()) { ClientEvent promptEvent(client_events::kConsoleWritePrompt, prompt); clientEventQueue().add(promptEvent); @@ -463,7 +463,7 @@ void addToConsoleInputBuffer(const rstudio::r::session::RConsoleInput& consoleIn s_consoleInputBuffer.push_back(consoleInput); } -} // namespace console_input +} // namespace console_input } // namespace session } // namespace rstudio diff --git a/src/cpp/session/SessionMain.cpp b/src/cpp/session/SessionMain.cpp index dac1dbbe9f4..b07a6df79f1 100644 --- a/src/cpp/session/SessionMain.cpp +++ b/src/cpp/session/SessionMain.cpp @@ -584,7 +584,8 @@ Error rInit(const rstudio::r::session::RInitInfo& rInitInfo) // console processes (console_process::initialize) - + + // http methods (http_methods::initialize) // r utils @@ -1402,33 +1403,10 @@ void rRunTests() exitEarly(status); } -void rRunAutomationImpl() -{ - // run tests - Error error = modules::automation::run(); - if (error) - LOG_ERROR(error); - - // run cleanup delayed - auto cleanup = []() - { - rCleanup(true); - exitEarly(0); - }; - - module_context::scheduleDelayedWork( - boost::posix_time::milliseconds(3000), - cleanup); - -} - void rRunAutomation() { - // delay execution of automation tests just so we can be sure - // the IDE has fully materialized - module_context::scheduleDelayedWork( - boost::posix_time::milliseconds(3000), - rRunAutomationImpl); + ClientEvent event(client_events::kRunAutomation); + module_context::enqueClientEvent(event); } void ensureRProfile() diff --git a/src/cpp/session/SessionModuleContext.cpp b/src/cpp/session/SessionModuleContext.cpp index 3e96628c895..e1f140012f9 100644 --- a/src/cpp/session/SessionModuleContext.cpp +++ b/src/cpp/session/SessionModuleContext.cpp @@ -64,6 +64,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,7 @@ #include "SessionRpc.hpp" #include "SessionClientEventQueue.hpp" +#include "SessionConsoleInput.hpp" #include "SessionMainProcess.hpp" #include @@ -99,7 +101,7 @@ using namespace rstudio::core; using namespace boost::placeholders; namespace rstudio { -namespace session { +namespace session { namespace module_context { bool isSessionSslEnabled() @@ -113,33 +115,33 @@ core::Error sendSessionRequest(const std::string& uri, { return session::http::sendSessionRequest(uri, body, isSessionSslEnabled(), pResponse); } - + namespace { // simple service for handling console_input rpc requests class ConsoleInputService : boost::noncopyable { public: - + ConsoleInputService() { core::thread::safeLaunchThread( boost::bind(&ConsoleInputService::run, this), &thread_); } - + ~ConsoleInputService() { enqueue("!"); } - + void enqueue(const std::string& input) { requests_.enque(input); } - + private: - + void run() { while (true) @@ -149,7 +151,7 @@ class ConsoleInputService : boost::noncopyable { if (input == "!") return; - + core::http::Response response; Error error = session::module_context::sendSessionRequest( "/rpc/console_input", @@ -158,11 +160,11 @@ class ConsoleInputService : boost::noncopyable if (error) LOG_ERROR(error); } - + requests_.wait(); } } - + boost::thread thread_; core::thread::ThreadsafeQueue requests_; }; @@ -181,10 +183,10 @@ SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP) // ignore forked sessions if (main_process::wasForked()) return R_NilValue; - + // extract name std::string name = r::sexp::asString(nameSEXP); - + // extract json value (for primitive types we only support scalars // since this is the most common type of event data). to return an // array of primitives you need to wrap them in a list/object @@ -196,19 +198,19 @@ SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP) { // do nothing, data will be a null json value break; - } + } case VECSXP: { extractError = r::json::jsonValueFromList(dataSEXP, &data); break; - } + } default: { extractError = r::json::jsonValueFromScalar(dataSEXP, &data); break; } } - + // check for error if (extractError) { @@ -216,7 +218,7 @@ SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP) throw r::exec::RErrorException( "Couldn't extract json value from event data"); } - + // determine the event type from the event name int type = -1; if (name == "package_status_changed") @@ -307,7 +309,7 @@ SEXP rs_enqueClientEvent(SEXP nameSEXP, SEXP dataSEXP) r::exec::error(e.message()); } CATCH_UNEXPECTED_EXCEPTION - + return R_NilValue; } @@ -332,7 +334,7 @@ SEXP rs_logErrorMessage(SEXP messageSEXP) std::string message = r::sexp::asString(messageSEXP); LOG_ERROR_MESSAGE(message); return R_NilValue; -} +} // log warning message from R SEXP rs_logWarningMessage(SEXP messageSEXP) @@ -340,8 +342,8 @@ SEXP rs_logWarningMessage(SEXP messageSEXP) std::string message = r::sexp::asString(messageSEXP); LOG_WARNING_MESSAGE(message); return R_NilValue; -} - +} + // sleep the main thread (debugging function used to test rpc/abort) SEXP rs_threadSleep(SEXP secondsSEXP) { @@ -366,8 +368,17 @@ SEXP rs_rstudioEdition() // get version SEXP rs_rstudioVersion() { + std::string numericVersion(RSTUDIO_VERSION_MAJOR); + numericVersion.append(".") + .append(RSTUDIO_VERSION_MINOR).append(".") + .append(RSTUDIO_VERSION_PATCH).append(".") + .append(boost::regex_replace( + std::string(RSTUDIO_VERSION_SUFFIX), + boost::regex("[a-zA-Z\\-+]"), + "")); + r::sexp::Protect rProtect; - return r::sexp::create(rstudioVersion(true), &rProtect); + return r::sexp::create(numericVersion, &rProtect); } // get long form version @@ -440,7 +451,7 @@ SEXP rs_packageLoaded(SEXP pkgnameSEXP) { if (main_process::wasForked()) return R_NilValue; - + std::string pkgname = r::sexp::safeAsString(pkgnameSEXP); // fire server event @@ -459,13 +470,13 @@ SEXP rs_packageUnloaded(SEXP pkgnameSEXP) { if (main_process::wasForked()) return R_NilValue; - + std::string pkgname = r::sexp::safeAsString(pkgnameSEXP); ClientEvent packageUnloadedEvent( client_events::kPackageUnloaded, json::Value(pkgname)); enqueClientEvent(packageUnloadedEvent); - + return R_NilValue; } @@ -495,7 +506,7 @@ SEXP rs_restartR(SEXP afterRestartSEXP, SEXP cleanSEXP) { std::string afterRestart = r::sexp::safeAsString(afterRestartSEXP); bool clean = r::sexp::asLogical(cleanSEXP); - + json::Object dataJson; json::Object suspendOptionsJson; suspendOptionsJson["save_minimal"] = clean; @@ -503,10 +514,10 @@ SEXP rs_restartR(SEXP afterRestartSEXP, SEXP cleanSEXP) suspendOptionsJson["exclude_packages"] = clean; suspendOptionsJson["after_restart"] = afterRestart; dataJson["options"] = suspendOptionsJson; - + ClientEvent event(client_events::kSuspendAndRestart, dataJson); module_context::enqueClientEvent(event); - + return R_NilValue; } @@ -714,45 +725,45 @@ FilePath registerMonitoredUserScratchDir(const std::string& dirName, namespace { - + // manage signals used for custom save and restore -class SuspendHandlers : boost::noncopyable +class SuspendHandlers : boost::noncopyable { public: SuspendHandlers() : nextGroup_(0) {} - -public: + +public: void add(const SuspendHandler& handler) { int group = nextGroup_++; suspendSignal_.connect(group, handler.suspend()); resumeSignal_.connect(group, handler.resume()); } - + void suspend(const r::session::RSuspendOptions& options, Settings* pSettings) { suspendSignal_(options, pSettings); } - + void resume(const Settings& settings) { resumeSignal_(settings); } - + private: - - // use groups to ensure signal order. call suspend handlers in order + + // use groups to ensure signal order. call suspend handlers in order // of subscription and call resume handlers in reverse order of // subscription. - + int nextGroup_; - + RSTUDIO_BOOST_SIGNAL, int, std::less > suspendSignal_; - + RSTUDIO_BOOST_SIGNAL, int, @@ -765,21 +776,21 @@ SuspendHandlers& suspendHandlers() static SuspendHandlers instance; return instance; } - + } // anonymous namespace - + void addSuspendHandler(const SuspendHandler& handler) { suspendHandlers().add(handler); } - + void onSuspended(const r::session::RSuspendOptions& options, Settings* pPersistentState) { pPersistentState->beginUpdate(); suspendHandlers().suspend(options, pPersistentState); pPersistentState->endUpdate(); - + } void onResumed(const Settings& persistentState) @@ -1087,11 +1098,11 @@ std::string createAliasedPath(const FileInfo& fileInfo) { return createAliasedPath(FilePath(fileInfo.absolutePath())); } - + std::string createAliasedPath(const FilePath& path) { return FilePath::createAliasedPath(path, userHomePath()); -} +} FilePath resolveAliasedPath(const std::string& aliasedPath) { @@ -1157,7 +1168,18 @@ std::string rLibsUser() { return core::system::getenv("R_LIBS_USER"); } - + +std::string rPrompt() { + static std::string res = ""; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = rstudio::r::options::getOption("prompt"); + return res; +} + bool isVisibleUserFile(const FilePath& filePath) { return (filePath.isWithin(module_context::userHomePath()) && @@ -1168,7 +1190,7 @@ FilePath safeCurrentPath() { return FilePath::safeCurrentPath(userHomePath()); } - + FilePath tempFile(const std::string& prefix, const std::string& extension) { return r::session::utils::tempFile(prefix, extension); @@ -1209,10 +1231,10 @@ bool addTinytexToPathIfNecessary() static bool s_added = false; if (s_added) return true; - + if (!module_context::findProgram("pdflatex").isEmpty()) return false; - + SEXP binDirSEXP = R_NilValue; r::sexp::Protect protect; Error error = r::exec::RFunction(".rs.tinytexBin").call(&binDirSEXP, &protect); @@ -1221,15 +1243,15 @@ bool addTinytexToPathIfNecessary() LOG_ERROR(error); return false; } - + if (!r::sexp::isString(binDirSEXP)) return false; - + std::string binDir = r::sexp::asString(binDirSEXP); FilePath binPath = module_context::resolveAliasedPath(binDir); if (!binPath.exists()) return false; - + s_added = true; core::system::addToSystemPath(binPath); return true; @@ -1282,20 +1304,20 @@ bool isTextFile(const FilePath& targetPath) { if (hasTextMimeType(targetPath)) return true; - + if (isJsonFile(targetPath)) return true; if (hasBinaryMimeType(targetPath)) return false; - + if (targetPath.getSize() == 0) return true; #ifndef _WIN32 - + std::string fileCommand = "file"; - + // the behavior of the 'file' command in the macOS High Sierra beta // changed such that '--mime' no longer ensured that mime-type strings // were actually emitted. using '-I' instead appears to work around this. @@ -1306,7 +1328,7 @@ bool isTextFile(const FilePath& targetPath) #else const char * const kMimeTypeArg = "--mime"; #endif - + core::shell_utils::ShellCommand cmd(fileCommand); cmd << "--dereference"; cmd << kMimeTypeArg; @@ -1398,10 +1420,11 @@ Error rScriptPath(FilePath* pRScriptPath) return error; #ifdef _WIN32 -*pRScriptPath = rHomeBinPath.completePath("Rterm.exe"); + *pRScriptPath = rHomeBinPath.completePath("Rterm.exe"); #else -*pRScriptPath = rHomeBinPath.completePath("R"); + *pRScriptPath = rHomeBinPath.completePath("R"); #endif + return Success(); } @@ -1473,7 +1496,14 @@ bool isPackageVersionInstalled(const std::string& packageName, bool isMinimumDevtoolsInstalled() { - return isPackageVersionInstalled("devtools", "1.4.1"); + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = isPackageVersionInstalled("devtools", "1.4.1"); + return res; } bool isMinimumRoxygenInstalled() @@ -1487,7 +1517,7 @@ std::string packageVersion(const std::string& packageName) Error error = r::exec::RFunction(".rs.packageVersionString") .addParam(packageName) .call(&version); - + if (error) { LOG_ERROR(error); @@ -1506,10 +1536,10 @@ Error packageVersion(const std::string& packageName, Error error = r::exec::RFunction(".rs.packageVersionString") .addParam(packageName) .call(&version); - + if (error) return error; - + *pVersion = Version(version); return Success(); } @@ -1770,7 +1800,7 @@ SEXP rs_base64decode(SEXP dataSEXP, SEXP binarySEXP) SEXP rs_htmlEscape(SEXP textSEXP, SEXP attributeSEXP) { - std::string escaped = string_utils::htmlEscape(r::sexp::safeAsString(textSEXP), + std::string escaped = string_utils::htmlEscape(r::sexp::safeAsString(textSEXP), r::sexp::asLogical(attributeSEXP)); r::sexp::Protect protect; return r::sexp::create(escaped, &protect); @@ -1815,7 +1845,7 @@ json::Object createFileSystemItem(const FileInfo& fileInfo) e.what()); entry["length"] = 0; } - + entry["exists"] = FilePath(fileInfo.absolutePath()).exists(); entry["lastModified"] = date_time::millisecondsSinceEpoch( @@ -1830,7 +1860,12 @@ json::Object createFileSystemItem(const FilePath& filePath) std::string rVersion() { - std::string rVersion; + static std::string rVersion; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return rVersion; + Error error = rstudio::r::exec::RFunction(".rs.rVersionString") .call(&rVersion); if (error) @@ -1848,7 +1883,12 @@ std::string rVersionLabel() std::string rHomeDir() { // get the current R home directory - std::string rVersionHome; + static std::string rVersionHome; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return rVersionHome; + Error error = rstudio::r::exec::RFunction("R.home").call(&rVersionHome); if (error) LOG_ERROR(error); @@ -1930,8 +1970,8 @@ r_util::ActiveSessions& activeSessions() std::shared_ptr storage; Error error = storage::activeSessionsStorage(&storage); - // The only real error we can get here is if the current user can't - // be retrieved, but if that's the case we should have exited with a + // The only real error we can get here is if the current user can't + // be retrieved, but if that's the case we should have exited with a // failure during start-up. We'll probably SegFault in any calls to the // ActiveSession object, but the process is in a very broken state anyway // Log before we crash so we can know what went wrong @@ -1940,7 +1980,7 @@ r_util::ActiveSessions& activeSessions() pSessions.reset(new r_util::ActiveSessions(storage, userScratchPath())); } - + return *pSessions; } @@ -2012,7 +2052,7 @@ Error sourceModuleRFileWithResult(const std::string& rSourceFile, return core::system::runProgram(rBin, args, "", options, pResult); } - + void enqueClientEvent(const ClientEvent& event) { session::clientEventQueue().add(event); @@ -2079,15 +2119,15 @@ bool fileListingFilter(const core::FileInfo& fileInfo, bool hideObjectFiles) } } } - + // Check for hidden files if (filePath.isHidden()) return false; - + // Check for object files if (hideObjectFiles && (ext == ".o" || ext == ".so" || ext == ".dll")) return false; - + // ok, passed our filters return true; } @@ -2162,22 +2202,22 @@ void enqueFileChangedEvents(const core::FilePath& vcsStatusRoot, Error enqueueConsoleInput(const std::string& consoleInput) { using namespace r::session; - + // construct our JSON RPC json::Array jsonParams = RConsoleInput(consoleInput).toJsonArray(); - + json::Object jsonRpc; jsonRpc["method"] = "console_input"; jsonRpc["params"] = jsonParams; jsonRpc["clientId"] = clientEventService().clientId(); - + // serialize for transmission std::ostringstream oss; jsonRpc.write(oss); - + // and fire it off consoleInputService().enqueue(oss.str()); - + return Success(); } @@ -2379,12 +2419,12 @@ FilePath sourceDiagnostics() { FilePath diagnosticsPath = options().coreRSourcePath().completeChildPath("Diagnostics.R"); - + Error error = r::exec::RFunction("source") .addParam(string_utils::utf8ToSystem(diagnosticsPath.getAbsolutePath())) .addParam("chdir", true) .call(); - + if (error) { LOG_ERROR(error); @@ -2400,7 +2440,7 @@ FilePath sourceDiagnostics() return module_context::resolveAliasedPath(reportPath); } } - + namespace { void beginRpcHandler(json::JsonRpcFunction function, @@ -2414,10 +2454,10 @@ void beginRpcHandler(json::JsonRpcFunction function, BOOST_ASSERT(!response.hasAfterResponse()); if (error) response.setError(error); - + if (!response.hasField(kEventsPending)) response.setField(kEventsPending, "false"); - + json::Object value; value["handle"] = asyncHandle; value["response"] = response.getRawResponse(); @@ -2606,7 +2646,7 @@ bool isPathViewAllowed(const FilePath& filePath) // Viewing content in the home directory is always allowed if (filePath.isWithin(userHomePath().getParent())) return true; - + // Viewing content in the session temporary files path is always allowed if (isSessionTempPath(filePath)) return true; @@ -2816,7 +2856,7 @@ Please run: in a terminal to accept the Xcode license, and then restart RStudio. )EOF"; - + std::cerr << msg << std::endl; } #endif @@ -2836,7 +2876,7 @@ bool hasMacOSDeveloperTools() { if (!isMacOS()) return false; - + core::system::ProcessResult result; Error error = core::system::runCommand( "/usr/bin/xcrun --find --show-sdk-path", @@ -2859,63 +2899,63 @@ bool hasMacOSCommandLineTools() { if (!isMacOS()) return false; - + return FilePath("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk").exists(); } void checkXcodeLicense() { #ifdef __APPLE__ - + // avoid repeatedly warning the user static bool s_licenseChecked; if (s_licenseChecked) return; - + s_licenseChecked = true; - + core::system::ProcessResult result; Error error = core::system::runCommand( "/usr/bin/xcrun --find --show-sdk-path", core::system::ProcessOptions(), &result); - + // if an error occurs, log it but avoid otherwise annoying the user if (error) { LOG_ERROR(error); return; } - + // exit code 69 implies license error if (result.exitStatus == 69) warnXcodeLicense(); - + #endif } std::vector ignoreContentDirs() { std::vector ignoreDirs; - + if (projects::projectContext().hasProject()) { // python virtual environments ignoreDirs = projects::projectContext().pythonEnvs(); quarto::QuartoConfig quartoConf = quarto::quartoConfig(); - + // quarto site output dir if (quartoConf.is_project) { FilePath quartoProjDir = module_context::resolveAliasedPath(quartoConf.project_dir); - + std::string quartoOutputDir = quartoConf.project_output_dir; if (!quartoOutputDir.empty()) ignoreDirs.push_back(quartoProjDir.completeChildPath(quartoOutputDir)); - + ignoreDirs.push_back(quartoProjDir.completeChildPath("_freeze")); } - + // rmarkdown site output dir if (module_context::isWebsiteProject()) { @@ -2925,7 +2965,7 @@ std::vector ignoreContentDirs() ignoreDirs.push_back(buildTargetPath.completeChildPath(outputDir)); } } - + return ignoreDirs; } @@ -2955,7 +2995,7 @@ Error adaptToLanguage(const std::string& language) { // check to see what language is active in main console using namespace r::exec; - + // check to see what language is currently active (but default to r) std::string activeLanguage = getActiveLanguage(); @@ -2969,13 +3009,13 @@ Error adaptToLanguage(const std::string& language) static RSTUDIO_BOOST_CONNECTION conn; if (conn.connected()) return Success(); - + // establish the connection, and then simply disconnect once we // receive the signal conn = module_context::events().onConsolePrompt.connect([&](const std::string&) { conn.disconnect(); }); - + Error error; if (activeLanguage == "R" && language == "Python") @@ -2992,25 +3032,8 @@ Error adaptToLanguage(const std::string& language) if (error) LOG_ERROR(error); } - - return Success(); -} -std::string rstudioVersion(bool normalizeSuffix) -{ - std::string suffix = RSTUDIO_VERSION_SUFFIX; - if (normalizeSuffix) - { - boost::regex reNonDigit("[^0-9]"); - suffix = boost::regex_replace(suffix, reNonDigit, ""); - } - - return fmt::format( - "{}.{}.{}.{}", - RSTUDIO_VERSION_MAJOR, - RSTUDIO_VERSION_MINOR, - RSTUDIO_VERSION_PATCH, - suffix); + return Success(); } Error initialize() @@ -3059,6 +3082,6 @@ Error initialize() } -} // namespace module_context +} // namespace module_context } // namespace session } // namespace rstudio diff --git a/src/cpp/session/SessionSourceDatabaseSupervisor.cpp b/src/cpp/session/SessionSourceDatabaseSupervisor.cpp index 869d1398819..6131578c590 100644 --- a/src/cpp/session/SessionSourceDatabaseSupervisor.cpp +++ b/src/cpp/session/SessionSourceDatabaseSupervisor.cpp @@ -146,8 +146,9 @@ Error removeSessionDir(const FilePath& sessionDir) // first remove children std::vector children; Error error = sessionDir.getChildren(children); - if (error) + if (error && !isFileNotFoundError(error)) LOG_ERROR(error); + for (const FilePath& filePath : children) { error = filePath.remove(); diff --git a/src/cpp/session/http/SessionHttpConnectionListenerImpl.hpp b/src/cpp/session/http/SessionHttpConnectionListenerImpl.hpp index 1edc0b99f20..a091b2b45d7 100644 --- a/src/cpp/session/http/SessionHttpConnectionListenerImpl.hpp +++ b/src/cpp/session/http/SessionHttpConnectionListenerImpl.hpp @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -46,8 +47,11 @@ #include #include #include +#include #include "SessionHttpConnectionImpl.hpp" +#include "../SessionConsoleInput.hpp" +#include "../SessionClientInit.hpp" #include "../SessionUriHandlers.hpp" #include "../SessionHttpMethods.hpp" #include "../SessionRpc.hpp" @@ -79,7 +83,7 @@ class UploadVisitor : public boost::static_visitor class HttpConnectionListenerImpl : public HttpConnectionListener, boost::noncopyable -{ +{ protected: HttpConnectionListenerImpl() : started_(false) {} @@ -89,7 +93,7 @@ class HttpConnectionListenerImpl : public HttpConnectionListener, } // COPYING: boost::noncopyable - + public: virtual core::Error start() { @@ -105,7 +109,7 @@ class HttpConnectionListenerImpl : public HttpConnectionListener, // accept next connection (asynchronously) acceptNextConnection(); - + // refresh locks core::FileLock::refreshPeriodically(acceptorService_.ioService()); @@ -330,12 +334,45 @@ class HttpConnectionListenerImpl : public HttpConnectionListener, return; } + if (init::isSessionInitializedAndRestored() && console_input::executing()) { + if (connection::isMethod(ptrHttpConnection, kClientInit)) { + eventsActive_ = false; + + // if we were not `executing` we would need to console_input::sendConsolePrompt instead + // (though we need a version that just sends the event without calling + // local event listeners) + + ClientEvent busyEvent(client_events::kBusy, true); + client_init::handleClientInit( + boost::bind(rstudio::session::module_context::enqueClientEvent, busyEvent), + ptrConnection + ); + + return; + } + + if (connection::isMethod(ptrHttpConnection, "read_config_json") || + connection::isMethod(ptrHttpConnection, "get_rversion_info") || + connection::isMethod(ptrHttpConnection, "list_files") || + connection::isMethod(ptrHttpConnection, "get_environment_state")) { + http_methods::handleConnection(ptrConnection, http_methods::ForegroundConnection); + return; + } + + std::string uri = ptrConnection->request().uri(); + if (boost::algorithm::starts_with(uri, "/mathjax/") || + boost::algorithm::starts_with(uri, "/theme/default/textmate.rstheme")) { + http_methods::handleConnection(ptrConnection, http_methods::ForegroundConnection); + return; + } + } + // check for a suspend_session. done here as well as in foreground to // allow clients without the requisite client-id and/or version header // to also initiate a suspend (e.g. an admin/supervisor process) if (connection::checkForSuspend(ptrHttpConnection)) return; - + if (connection::checkForInterrupt(ptrHttpConnection)) return; @@ -374,7 +411,7 @@ class HttpConnectionListenerImpl : public HttpConnectionListener, } if (options().asyncRpcEnabled() && options().asyncRpcTimeoutMs() == 0 && - http_methods::isAsyncJsonRpcRequest(ptrHttpConnection) && + http_methods::isAsyncJsonRpcRequest(ptrHttpConnection) && eventsActive_) { if (http_methods::protocolDebugEnabled()) diff --git a/src/cpp/session/include/session/SessionModuleContext.hpp b/src/cpp/session/include/session/SessionModuleContext.hpp index d36d15e3120..9f6372407d0 100644 --- a/src/cpp/session/include/session/SessionModuleContext.hpp +++ b/src/cpp/session/include/session/SessionModuleContext.hpp @@ -24,18 +24,22 @@ #include #include +#include +#include + #include #include +#include #include -#include -#include -#include -#include #include #include -#include #include -#include +#include +#include +#include +#include +#include +#include #include #include @@ -51,12 +55,6 @@ namespace core { class FilePath; class FileInfo; class Settings; - namespace system { - class ProcessSupervisor; - struct ProcessResult; - struct ProcessOptions; - - } namespace shell_utils { class ShellCommand; } @@ -72,7 +70,7 @@ namespace session { } namespace rstudio { -namespace session { +namespace session { namespace module_context { @@ -84,8 +82,8 @@ enum PackageCompatStatus COMPAT_TOO_NEW = 3, COMPAT_UNKNOWN = 4 }; - -// paths + +// paths core::FilePath userHomePath(); std::string createAliasedPath(const core::FileInfo& fileInfo); std::string createAliasedPath(const core::FilePath& path); @@ -109,7 +107,7 @@ core::json::Object createFileSystemItem(const core::FilePath& filePath); core::FilePath rPostbackPath(); core::FilePath rPostbackScriptsDir(); core::FilePath rPostbackScriptsPath(const std::string& scriptName); - + // r session info std::string rVersion(); std::string rVersionLabel(); @@ -121,13 +119,15 @@ core::r_util::ActiveSession& activeSession(); core::r_util::ActiveSessions& activeSessions(); // get a temp file -core::FilePath tempFile(const std::string& prefix, +core::FilePath tempFile(const std::string& prefix, const std::string& extension); core::FilePath tempDir(); std::string rLibsUser(); +std::string rPrompt(); + // find out the location of a binary core::FilePath findProgram(const std::string& name); @@ -193,11 +193,11 @@ bool isUnmonitoredPackageSourceFile(const core::FilePath& filePath); // register a handler for rBrowseUrl typedef boost::function RBrowseUrlHandler; core::Error registerRBrowseUrlHandler(const RBrowseUrlHandler& handler); - + // register a handler for rBrowseFile typedef boost::function RBrowseFileHandler; core::Error registerRBrowseFileHandler(const RBrowseFileHandler& handler); - + // register an inbound uri handler (include a leading slash) core::Error registerAsyncUriHandler( const std::string& name, @@ -226,7 +226,7 @@ core::Error registerLocalUriHandler( typedef boost::function PostbackHandlerContinuation; -// register a postback handler. see docs in SessionPostback.cpp for +// register a postback handler. see docs in SessionPostback.cpp for // details on the requirements of postback handlers typedef boost::function PostbackHandlerFunction; @@ -234,7 +234,7 @@ core::Error registerPostbackHandler( const std::string& name, const PostbackHandlerFunction& handlerFunction, std::string* pShellCommand); - + // register an async rpc method core::Error registerAsyncRpcMethod( const std::string& name, @@ -319,7 +319,7 @@ enum ChangeSource ChangeSourceRPC, ChangeSourceURI }; - + // custom slot combiner that takes the first non empty value template @@ -441,7 +441,7 @@ core::Error sourceModuleRFile(const std::string& rSourceFile); core::Error sourceModuleRFileWithResult(const std::string& rSourceFile, const core::FilePath& workingDir, core::system::ProcessResult* pResult); - + // enque client events (note R methods can do this via .rs.enqueClientEvent) void enqueClientEvent(const ClientEvent& event); @@ -468,14 +468,14 @@ core::FilePath registerMonitoredUserScratchDir(const std::string& dirName, // enqueue new console input core::Error enqueueConsoleInput(const std::string& input); -// write output to the console (convenience wrapper for enquing a +// write output to the console (convenience wrapper for enquing a // kConsoleWriteOutput event) void consoleWriteOutput(const std::string& output); - -// write an error to the console (convenience wrapper for enquing a + +// write an error to the console (convenience wrapper for enquing a // kConsoleWriteOutput event) void consoleWriteError(const std::string& message); - + // show an error dialog (convenience wrapper for enquing kShowErrorMessage) void showErrorMessage(const std::string& title, const std::string& message); @@ -533,7 +533,7 @@ std::string normalizeVcsOverride(const std::string& vcsOverride); core::FilePath shellWorkingDirectory(); // persist state across suspend and resume - + typedef boost::function SuspendFunction; typedef boost::function ResumeFunction; @@ -546,17 +546,17 @@ class SuspendHandler : suspend_(suspend), resume_(resume) { } - + // COPYING: via compiler - + const SuspendFunction& suspend() const { return suspend_; } const ResumeFunction& resume() const { return resume_; } - + private: SuspendFunction suspend_; ResumeFunction resume_; }; - + void addSuspendHandler(const SuspendHandler& handler); bool rSessionResumed(); @@ -776,12 +776,12 @@ struct QuartoNavigate QuartoNavigate() : website(false) {} - + bool empty() const { return !website && source.empty(); } - + static QuartoNavigate navigate( const std::string& source, const std::string& output, @@ -795,7 +795,7 @@ struct QuartoNavigate nav.website = isWebsite; return nav; } - + std::string source; std::string output; std::string job_id; @@ -845,7 +845,7 @@ struct SourceMarker Warning = 1, Box = 2, Info = 3, - Style = 4, + Style = 4, Usage = 5, Empty = 99 }; @@ -888,7 +888,7 @@ struct SourceMarker isCustom(isCustom) { } - + explicit operator bool() const { return type != Empty; @@ -908,7 +908,7 @@ SourceMarker::Type sourceMarkerTypeFromString(const std::string& type); core::json::Array sourceMarkersAsJson(const std::vector& markers); struct SourceMarkerSet -{ +{ SourceMarkerSet() {} SourceMarkerSet(const std::string& name, @@ -1045,8 +1045,6 @@ core::Error sendSessionRequest(const std::string& uri, const std::string& body, core::http::Response* pResponse); -std::string rstudioVersion(bool normalizeSuffix = false); - } // namespace module_context } // namespace session } // namespace rstudio diff --git a/src/cpp/session/include/session/SessionQuarto.hpp b/src/cpp/session/include/session/SessionQuarto.hpp index f66a53c7bf3..c83c5c43c98 100644 --- a/src/cpp/session/include/session/SessionQuarto.hpp +++ b/src/cpp/session/include/session/SessionQuarto.hpp @@ -55,6 +55,7 @@ struct QuartoConfig std::string version; std::string bin_path; std::string resources_path; + std::string pandoc_path; // project info bool is_project; diff --git a/src/cpp/session/include/session/SessionSourceDatabase.hpp b/src/cpp/session/include/session/SessionSourceDatabase.hpp index 1713f5ab144..d469f6bfe86 100644 --- a/src/cpp/session/include/session/SessionSourceDatabase.hpp +++ b/src/cpp/session/include/session/SessionSourceDatabase.hpp @@ -89,7 +89,7 @@ class SourceDocument : boost::noncopyable core::Error updateDirty(); core::Error contentsMatchDisk(bool* pMatches); - + // set dirty void setDirty(bool dirty) { diff --git a/src/cpp/session/include/session/prefs/UserPrefValues.hpp b/src/cpp/session/include/session/prefs/UserPrefValues.hpp index d50161203c3..447bce3d25e 100644 --- a/src/cpp/session/include/session/prefs/UserPrefValues.hpp +++ b/src/cpp/session/include/session/prefs/UserPrefValues.hpp @@ -437,6 +437,13 @@ namespace prefs { #define kRunBackgroundJobDefaultWorkingDir "run_background_job_default_working_dir" #define kRunBackgroundJobDefaultWorkingDirProject "project" #define kRunBackgroundJobDefaultWorkingDirScript "script" +#define kCodeFormatter "code_formatter" +#define kCodeFormatterNone "none" +#define kCodeFormatterStyler "styler" +#define kCodeFormatterExternal "external" +#define kCodeFormatterStylerStrict "code_formatter_styler_strict" +#define kCodeFormatterExternalCommand "code_formatter_external_command" +#define kReformatOnSave "reformat_on_save" class UserPrefValues: public Preferences { @@ -1972,6 +1979,30 @@ class UserPrefValues: public Preferences std::string runBackgroundJobDefaultWorkingDir(); core::Error setRunBackgroundJobDefaultWorkingDir(std::string val); + /** + * The formatter to use when reformatting code. + */ + std::string codeFormatter(); + core::Error setCodeFormatter(std::string val); + + /** + * When set, strict transformers will be used when formatting code. See the `styler` package documentation for more details. + */ + bool codeFormatterStylerStrict(); + core::Error setCodeFormatterStylerStrict(bool val); + + /** + * The external command to be used when reformatting code. + */ + std::string codeFormatterExternalCommand(); + core::Error setCodeFormatterExternalCommand(std::string val); + + /** + * When set, the selected formatter will be used to reformat documents on save. + */ + bool reformatOnSave(); + core::Error setReformatOnSave(bool val); + }; diff --git a/src/cpp/session/include/session/prefs/UserStateValues.hpp b/src/cpp/session/include/session/prefs/UserStateValues.hpp index bf9498d983b..9e74602cf0d 100644 --- a/src/cpp/session/include/session/prefs/UserStateValues.hpp +++ b/src/cpp/session/include/session/prefs/UserStateValues.hpp @@ -62,6 +62,7 @@ namespace prefs { #define kExportPlotOptionsKeepRatio "keepRatio" #define kExportPlotOptionsViewAfterSave "viewAfterSave" #define kExportPlotOptionsCopyAsMetafile "copyAsMetafile" +#define kExportPlotOptionsUseDevicePixelRatio "useDevicePixelRatio" #define kExportViewerOptions "export_viewer_options" #define kExportViewerOptionsWidth "width" #define kExportViewerOptionsHeight "height" diff --git a/src/cpp/session/include/session/worker_safe/session/SessionClientEvent.hpp b/src/cpp/session/include/session/worker_safe/session/SessionClientEvent.hpp index dc9f065b760..e5b4fb6282a 100644 --- a/src/cpp/session/include/session/worker_safe/session/SessionClientEvent.hpp +++ b/src/cpp/session/include/session/worker_safe/session/SessionClientEvent.hpp @@ -214,6 +214,8 @@ extern const int kPresentationPreview; extern const int kSuspendBlocked; extern const int kClipboardAction; extern const int kDeploymentRecordsUpdated; +extern const int kRunAutomation; + } class ClientEvent diff --git a/src/cpp/session/modules/ModuleTools.R b/src/cpp/session/modules/ModuleTools.R index 3f556ff15a4..181bc38f7dc 100644 --- a/src/cpp/session/modules/ModuleTools.R +++ b/src/cpp/session/modules/ModuleTools.R @@ -316,17 +316,20 @@ invisible(.Call(method, prefName, .rs.scalar(value), PACKAGE = "(embedding)")) }) -.rs.addFunction("readApiPref", function(prefName) { - .rs.readPrefInternal("rs_readApiPref", prefName) +.rs.addFunction("readApiPref", function(prefName, default = NULL) { + value <- .rs.readPrefInternal("rs_readApiPref", prefName) + if (is.null(value)) default else value }) .rs.addFunction("writeApiPref", function(prefName, value) { .rs.writePrefInternal("rs_writeApiPref", prefName, value) }) -.rs.addFunction("readUiPref", function(prefName) { - .rs.readPrefInternal("rs_readUserPref", prefName) +.rs.addFunction("readUiPref", function(prefName, default = NULL) { + value <- .rs.readPrefInternal("rs_readUserPref", prefName) + if (is.null(value)) default else value }) + .rs.addFunction("readUserPref", .rs.readUiPref) .rs.addFunction("writeUiPref", function(prefName, value) { diff --git a/src/cpp/session/modules/SessionAuthoring.cpp b/src/cpp/session/modules/SessionAuthoring.cpp index d4d947c3845..7e55c050931 100644 --- a/src/cpp/session/modules/SessionAuthoring.cpp +++ b/src/cpp/session/modules/SessionAuthoring.cpp @@ -42,11 +42,13 @@ #include "tex/SessionSynctex.hpp" #include "tex/SessionViewPdf.hpp" +#include "../SessionConsoleInput.hpp" + using namespace rstudio::core; namespace rstudio { namespace session { -namespace modules { +namespace modules { namespace authoring { namespace { @@ -185,7 +187,11 @@ json::Array supportedLatexProgramTypes() json::Object texCapabilitiesAsJson() { - json::Object obj; + static json::Object obj; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return obj; obj["tex_installed"] = tex::pdflatex::isInstalled(); diff --git a/src/cpp/session/modules/SessionBreakpoints.R b/src/cpp/session/modules/SessionBreakpoints.R index 78caeea9b53..5b65f8c95fd 100644 --- a/src/cpp/session/modules/SessionBreakpoints.R +++ b/src/cpp/session/modules/SessionBreakpoints.R @@ -161,51 +161,55 @@ # given a traced function body and the original function body, recursively copy # the source references from the original body to the traced body, adding # source references to the injected trace code from the line being traced -.rs.addFunction("tracedSourceRefs",function(funBody, originalFunBody) +.rs.addFunction("tracedSourceRefs", function(funBody, originalFunBody) { - if (is.symbol(funBody) || is.symbol(originalFunBody)) - { - return (funBody) - } - - # start with a copy of the original source references - attr(funBody, "srcref") <- attr(originalFunBody, "srcref") - - for (idx in 1:length(funBody)) - { - # Check to see if this is one of the several types of objects we can't do - # equality testing for. Note that these object types are all leaf nodes in - # the parse tree, so it's safe to stop recursion here. Also note that we - # can't use the helpful is.na() here since that function emits warnings - # for some types of objects in the parse tree. - if (is.null(funBody[[idx]]) || - identical(funBody[[idx]], NA) || - identical(funBody[[idx]], NA_character_) || - identical(funBody[[idx]], NA_complex_) || - identical(funBody[[idx]], NA_integer_) || - identical(funBody[[idx]], NA_real_) || - identical(funBody[[idx]], NaN) || - is.pairlist(funBody[[idx]])) - next - - # if this expression was replaced by trace(), copy the source references - # from the original expression over each expression injected by trace() - if (length(funBody[[idx]]) != length(originalFunBody[[idx]]) || - isTRUE(sum(funBody[[idx]] != originalFunBody[[idx]]) > 0)) - { - attr(funBody[[idx]], "srcref") <- - rep(list(attr(originalFunBody, "srcref")[[idx]]), length(funBody[[idx]])) - } - - # recurse to symbol level - else if (is.language(funBody[[idx]])) - { - funBody[[idx]] <- .rs.tracedSourceRefs( - funBody[[idx]], - originalFunBody[[idx]]) - } - } - return(funBody) + if (is.call(funBody) && + is.call(originalFunBody) && + length(funBody) == length(originalFunBody)) + { + for (i in seq_along(funBody)) + { + # Check for an invocation of trace. + # + # { + # .doTrace(browser()) + # + # } + # + isTraceCall <- + is.call(funBody[[i]]) && + length(funBody[[i]]) >= 2 && + identical(funBody[[i]][[1L]], as.symbol("{")) && + is.call(funBody[[i]][[2L]]) && + identical(funBody[[i]][[2L]][[1L]], as.symbol(".doTrace")) + + if (isTraceCall) + { + # We found a trace call; copy the source references from + # the original function body into each node of the call + # to `.doTrace(browser())`. + srcRefs <- .rs.nullCoalesce( + attr(funBody, "srcref")[[i]], + attr(originalFunBody, "srcref")[[i]] + ) + + repSrcRefs <- rep(list(srcRefs), length(funBody[[i]])) + attr(funBody[[i]], "srcref") <- repSrcRefs + } + else if (is.call(funBody[[i]]) && + is.call(originalFunBody[[i]]) && + length(funBody[[i]]) == length(originalFunBody[[i]])) + { + # Recurse into non-traced body elements + funBody[[i]] <- .rs.tracedSourceRefs( + funBody[[i]], + originalFunBody[[i]] + ) + } + } + } + + funBody }) .rs.addFunction("getFunctionSteps", function(fun, functionName, lineNumbers) diff --git a/src/cpp/session/modules/SessionBreakpoints.cpp b/src/cpp/session/modules/SessionBreakpoints.cpp index d427908d584..f08c0266aa0 100644 --- a/src/cpp/session/modules/SessionBreakpoints.cpp +++ b/src/cpp/session/modules/SessionBreakpoints.cpp @@ -39,6 +39,8 @@ #include #include +#include "../SessionConsoleInput.hpp" + using namespace rstudio::core; using namespace rstudio::r::sexp; using namespace rstudio::r::exec; @@ -215,7 +217,7 @@ Error getFunctionState(const json::JsonRpcRequest& request, json::Object response; std::string functionName, fileName, packageName; int lineNumber = 0; - + Error error = json::readParams(request.params, &functionName, &fileName, &lineNumber); if (error) return error; @@ -232,7 +234,7 @@ Error getFunctionState(const json::JsonRpcRequest& request, .call(&inSync); if (error) LOG_ERROR(error); - + response["sync_state"] = inSync; response["package_name"] = packageName; response["is_package_function"] = packageName.length() > 0; @@ -504,7 +506,7 @@ Error initBreakpoints() json::isType(breakpointStateValue)) { json::Object breakpointState = breakpointStateValue.getObject(); - + // Protect against the breakpoint array being serialized as an // empty object json::Value jsonBreakpointArray = breakpointState["breakpoints"]; @@ -588,7 +590,12 @@ Error removeAllBreakpoints(const json::JsonRpcRequest&, bool haveAdvancedStepCommands() { - bool haveCommands = false; + static bool haveCommands = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return haveCommands; + Error error = r::exec::RFunction(".rs.haveAdvancedSteppingCommands") .call(&haveCommands); if (error) diff --git a/src/cpp/session/modules/SessionCodeTools.R b/src/cpp/session/modules/SessionCodeTools.R index 2f1698b368e..5085c06a1e7 100644 --- a/src/cpp/session/modules/SessionCodeTools.R +++ b/src/cpp/session/modules/SessionCodeTools.R @@ -2750,9 +2750,14 @@ result }) -.rs.addFunction("nullCoalesce", function(x, y) +.rs.addFunction("nullCoalesce", function(...) { - if (is.null(x)) y else x + for (i in seq_len(...length())) + { + value <- ...elt(i) + if (!is.null(value)) + return(value) + } }) .rs.addFunction("truncate", function(string, n = 200, marker = "<...>") diff --git a/src/cpp/session/modules/SessionDataViewer.R b/src/cpp/session/modules/SessionDataViewer.R index bbb8b758223..daf8ce00db7 100644 --- a/src/cpp/session/modules/SessionDataViewer.R +++ b/src/cpp/session/modules/SessionDataViewer.R @@ -339,24 +339,42 @@ .rs.addFunction("formatRowNames", function(x, start, len) { - # detect whether this is a data.frame that contains - # row names, or if the row names are stored compactly - if (is.data.frame(x)) + # check for a data.frame with compact row names + if (.rs.hasCompactRowNames(x)) { + # the second element indicates the number of rows, and + # is negative if they're so-called "automatic" row names info <- .row_names_info(x, type = 0L) - if (is.integer(info) && length(info) > 0 && is.na(info[[1]])) - { - # the second element indicates the number of rows, and is negative if they're - # automatic - n <- abs(info[[2]]) - range <- seq(from = start, to = min(n, start + len)) - return(as.character(range)) - } + n <- abs(info[[2L]]) + range <- seq(from = start, to = min(n, start + len)) + return(as.character(range)) + } + + # retrieve row names; use .row_names_info for data.frame so + # we can detect internal non-character row names + rowNames <- if (is.data.frame(x)) + { + .row_names_info(x, type = 0L) + } + else + { + row.names(x) } - # otherwise, extract row names and subset as usual - rownames <- row.names(x) - rownames[start:min(length(rownames), start + len)] + # subset the retrieved row names + rowNames <- rowNames[start:min(length(rowNames), start + len)] + + # encode strings as JSON to force quoting + handle escaping + # this also lets us differentiate numeric (automatic) row names + # from explicitly-set row names + if (is.character(rowNames)) + { + .rs.mapChr(rowNames, .rs.toJSON, unbox = TRUE) + } + else + { + as.character(rowNames) + } }) # wrappers for nrow/ncol which will report the class of object for which we @@ -391,14 +409,20 @@ cols }) -.rs.addFunction("toDataFrame", function(x, name, flatten) { - - # if we have a data.table, convert it to a data.frame explicitly - if (inherits(x, "data.table")) - x <- as.data.frame(x) +.rs.addFunction("toDataFrame", function(x, name, flatten) +{ + # force a non-subclassed data.frame -- this is necessary to ensure + # that row names (or row numbers) are not dropped when subsetting + # data, since those row names are used when generating cell-specific + # callbacks (e.g. for viewing a cell of a list column) + if (is.data.frame(x)) + { + class(x) <- "data.frame" + } # if it's not already a frame, coerce it to a frame - if (!is.data.frame(x)) { + if (!is.data.frame(x)) + { frame <- NULL # attempt to coerce to a data frame--this can throw errors in the case # where we're watching a named object in an environment and the user @@ -533,19 +557,25 @@ Encoding(colfilter) <- "UTF-8" colfilter }, "") + if (Encoding(search) == "unknown") Encoding(search) <- "UTF-8" # coerce argument to data frame--data.table objects (for example) report that # they're data frames, but don't actually support the subsetting operations # needed for search/sort/filter without an explicit cast + # + # similarly, we need to convert tibbles to regular data.frames so that we can + # properly invoke the list / data viewer on filtered rows x <- .rs.toDataFrame(x, "transformed", TRUE) # apply columnwise filters - for (i in seq_along(filtered)) { - if (nchar(filtered[i]) > 0 && length(x[[i]]) > 0) { + for (i in seq_along(filtered)) + { + if (nchar(filtered[i]) > 0 && length(x[[i]]) > 0) + { # split filter--string format is "type|value" (e.g. "numeric|12-25") - filter <- strsplit(filtered[i], split="|", fixed = TRUE)[[1]] + filter <- strsplit(filtered[i], split = "|", fixed = TRUE)[[1]] if (length(filter) < 2) { # no filter type information @@ -596,10 +626,31 @@ # apply global search if (!is.null(search) && nchar(search) > 0) { - x <- x[Reduce("|", lapply(x, function(column) { - grepl(paste("\\Q", search, "\\E", sep = ""), column, perl = TRUE, - ignore.case = TRUE) - })), , drop = FALSE] + # get columns for search + searchColumns <- unclass(x) + + # also apply on row names if available + if (is.data.frame(x)) + { + info <- .row_names_info(x, type = 0L) + if (is.character(info)) + { + searchColumns[[length(searchColumns) + 1]] <- info + } + } + + # apply global search on data columns + pattern <- paste0("\\Q", search, "\\E") + matches <- lapply(searchColumns, function(column) { + grepl(pattern, column, perl = TRUE, ignore.case = TRUE) + }) + + # collapse into single vector + matches <- Reduce(`|`, matches) + + # update based on matches + x <- x[matches, , drop = FALSE] + } # apply sort diff --git a/src/cpp/session/modules/SessionEnvironment.R b/src/cpp/session/modules/SessionEnvironment.R index 34df9ed1297..fc145792b18 100644 --- a/src/cpp/session/modules/SessionEnvironment.R +++ b/src/cpp/session/modules/SessionEnvironment.R @@ -345,7 +345,12 @@ return(c(0L, 0L, 0L, 0L, 0L, 0L)) pos <- gregexpr(calltext, singleline, fixed = TRUE)[[1]] - if (length(pos) > 1) + if (length(linepref) == 0L || linepref <= 0L) + { + pos <- pos[[1L]] + endpos <- pos + attr(pos, "match.length") + } + else if (length(pos) > 1) { # There is more than one instance of the call text in the function; try # to pick the first match past the preferred line. @@ -365,6 +370,7 @@ } else { + pos <- pos[[1L]] endpos <- pos + attr(pos, "match.length") } diff --git a/src/cpp/session/modules/SessionGraphics.cpp b/src/cpp/session/modules/SessionGraphics.cpp index b5f796366cd..01c65ce6936 100644 --- a/src/cpp/session/modules/SessionGraphics.cpp +++ b/src/cpp/session/modules/SessionGraphics.cpp @@ -28,6 +28,8 @@ #include #include +#include "../SessionConsoleInput.hpp" + using namespace rstudio::core; using namespace boost::placeholders; @@ -57,7 +59,7 @@ void syncWithPrefs() r::options::setOption( kGraphicsOptionBackend, prefs::userPrefs().graphicsBackend()); - + r::options::setOption( kGraphicsOptionAntialias, prefs::userPrefs().graphicsAntialiasing()); @@ -79,6 +81,12 @@ SEXP rs_devicePixelRatio() core::json::Array supportedBackends() { + static core::json::Array backendsJson; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return backendsJson; + r::sexp::Protect protect; SEXP backends; Error error = r::exec::RFunction(".rs.graphics.supportedBackends").call(&backends, &protect); @@ -87,15 +95,14 @@ core::json::Array supportedBackends() LOG_ERROR(error); return json::Array(); } - - core::json::Array backendsJson; + error = r::json::jsonValueFromVector(backends, &backendsJson); if (error) { LOG_ERROR(error); return json::Array(); } - + return backendsJson; } @@ -109,14 +116,14 @@ SEXP rs_traceGraphicsDevice(SEXP enableSEXP) core::Error initialize() { using namespace module_context; - + events().onPreferencesSaved.connect(onPreferencesSaved); - + syncWithPrefs(); - + RS_REGISTER_CALL_METHOD(rs_devicePixelRatio); RS_REGISTER_CALL_METHOD(rs_traceGraphicsDevice); - + using boost::bind; ExecBlock initBlock; initBlock.addFunctions() diff --git a/src/cpp/session/modules/SessionHTMLPreview.cpp b/src/cpp/session/modules/SessionHTMLPreview.cpp index f3d3fedb4f4..07eb34a8bab 100644 --- a/src/cpp/session/modules/SessionHTMLPreview.cpp +++ b/src/cpp/session/modules/SessionHTMLPreview.cpp @@ -51,6 +51,8 @@ #include #include +#include "../SessionConsoleInput.hpp" + #define kHTMLPreview "html_preview" #define kHTMLPreviewLocation "/" kHTMLPreview "/" @@ -59,7 +61,7 @@ using namespace boost::placeholders; namespace rstudio { namespace session { -namespace modules { +namespace modules { namespace html_preview { namespace { @@ -505,7 +507,7 @@ Error previewHTML(const json::JsonRpcRequest& request, if (isNotebook) file = deriveNotebookPath(file); - + FilePath filePath = module_context::resolveAliasedPath(file); // if we have a preview already running then just return false @@ -1034,7 +1036,7 @@ SEXP rs_showPageViewer(SEXP urlSEXP, SEXP titleSEXP, SEXP selfContainedSEXP) { viewerFilePath = filePath; } - + // set url to localhost previewer std::string tempPath = viewerFilePath.getRelativePath(module_context::tempDir()); url = module_context::sessionTempDirUrl(tempPath); @@ -1083,9 +1085,9 @@ SEXP rs_showPageViewer(SEXP urlSEXP, SEXP titleSEXP, SEXP selfContainedSEXP) } - + } // anonymous namespace - + void addFileSpecificHeaders(const FilePath& filePath, http::Response* pResponse) { @@ -1104,7 +1106,12 @@ void addFileSpecificHeaders(const FilePath& filePath, http::Response* pResponse) core::json::Object capabilitiesAsJson() { // default to unsupported - json::Object capsJson; + static json::Object capsJson; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return capsJson; + capsJson["r_markdown_supported"] = false; capsJson["stitch_supported"] = false; @@ -1136,7 +1143,7 @@ core::json::Object capabilitiesAsJson() Error initialize() -{ +{ RS_REGISTER_CALL_METHOD(rs_showPageViewer, 3); using boost::bind; @@ -1152,7 +1159,7 @@ Error initialize() ; return initBlock.execute(); } - + } // namespace html_preview diff --git a/src/cpp/session/modules/SessionPackages.R b/src/cpp/session/modules/SessionPackages.R index f29ea6f87d3..9560fe5ffc3 100644 --- a/src/cpp/session/modules/SessionPackages.R +++ b/src/cpp/session/modules/SessionPackages.R @@ -962,29 +962,49 @@ if (identical(as.character(Sys.info()["sysname"]), "Darwin") && ## Create a DESCRIPTION file # Fill some bits based on devtools options if they're available. - # Protect against vectors with length > 1 - getDevtoolsOption <- function(optionName, default, collapse = " ") + # Protect against vectors with length > 1. + getDevtoolsOption <- function(optionName, + default = NULL, + collapse = " ") { - devtoolsDesc <- getOption("devtools.desc") - if (!length(devtoolsDesc)) - return(default) - - option <- devtoolsDesc[[optionName]] - if (is.null(option)) - return(default) + for (descKey in c("usethis.description", "devtools.desc")) + { + descValue <- getOption(descKey) + if (length(descValue) == 0L) + next + + optionValue <- descValue[[optionName]] + if (length(optionValue) == 0L) + next + + # Check for 'person' objects, and expand those in a way + # that will be formatted nicely in the DESCRIPTION file. + if (inherits(optionValue, "person")) + optionValue <- .rs.formatPerson(optionValue) + + return(paste(optionValue, collapse = collapse)) + } - paste(option, collapse = collapse) + default } + authorsDefault <- .rs.heredoc(' + c( + person( + "Jane", "Doe", + email = "jane@example.com", + role = c("aut", "cre") + ) + ) + ') - Author <- getDevtoolsOption("Author", "Who wrote it") - - Maintainer <- getDevtoolsOption( - "Maintainer", - "The package maintainer " + authors <- getDevtoolsOption( + "Authors@R", + gsub("\n", "\n ", authorsDefault, fixed = TRUE), + collapse = "\n" ) - License <- getDevtoolsOption( + license <- getDevtoolsOption( "License", "What license is it under?", ", " @@ -995,13 +1015,12 @@ if (identical(as.character(Sys.info()["sysname"]), "Darwin") && Type = "Package", Title = "What the Package Does (Title Case)", Version = "0.1.0", - Author = Author, - Maintainer = Maintainer, + "Authors@R" = authors, Description = c( - "More about what it does (maybe more than one line)", - "Use four spaces when indenting paragraphs within the Description." + "More about what it does (maybe more than one line).", + "Continuation lines should be indented." ), - License = License, + License = license, Encoding = "UTF-8", LazyData = "true" ) @@ -1041,19 +1060,27 @@ if (identical(as.character(Sys.info()["sysname"]), "Darwin") && } # Get other fields from devtools options - if (length(getOption("devtools.desc.suggests"))) - DESCRIPTION$Suggests <- getOption("devtools.desc.suggests") + suggests <- .rs.nullCoalesce( + getOption("devtools.desc.suggests"), + getDevtoolsOption("Suggests") + ) - if (length(getOption("devtools.desc"))) + if (length(suggests)) + DESCRIPTION$Suggests <- suggests + + # Add in any other options + desc <- .rs.nullCoalesce( + getOption("usethis.description"), + getOption("devtools.desc") + ) + + .rs.enumerate(desc, function(key, value) { - devtools.desc <- getOption("devtools.desc") - for (i in seq_along(devtools.desc)) - { - name <- names(devtools.desc)[[i]] - value <- devtools.desc[[i]] - DESCRIPTION[[name]] <- value - } - } + DESCRIPTION[[key]] <<- .rs.nullCoalesce( + DESCRIPTION[[key]], + value + ) + }) # If we are using 'testthat' and 'devtools' is available, use it to # add test infrastructure @@ -1086,10 +1113,11 @@ if (identical(as.character(Sys.info()["sysname"]), "Darwin") && if (grepl("MIT\\s+\\+\\s+file\\s+LICEN[SC]E", DESCRIPTION$License, perl = TRUE)) { # Guess the copyright holder - holder <- if (!is.null(getOption("devtools.name"))) - Author - else + holder <- .rs.nullCoalesce( + getOption("usethis.full_name"), + getOption("devtools.name"), "" + ) msg <- c( paste("YEAR:", format(Sys.time(), "%Y")), @@ -1346,6 +1374,55 @@ if (identical(as.character(Sys.info()["sysname"]), "Darwin") && }) +# Formats a person object in a way suitable for the Authors@R +# section of a DESCRIPTION file. +.rs.addFunction("formatPerson", function(person) +{ + personFields <- unclass(person) + if (length(personFields) == 1L) + { + formattedPerson <- .rs.formatPersonImpl(personFields[[1L]]) + return(gsub("\n", "\n ", formattedPerson)) + } + + formattedPersons <- paste( + .rs.mapChr(personFields, .rs.formatPersonImpl, indent = " "), + collapse = ",\n" + ) + + paste(c("c(", formattedPersons, " )"), collapse = "\n") +}) + +.rs.addFunction("formatPersonImpl", function(fields, indent = identity) +{ + if (is.character(indent)) + { + indentWidth <- indent + indent <- function(x) sprintf("%s%s", indentWidth, x) + } + + # Build header from given + family name. + fullName <- c(fields$given, fields$family, fields$middle) + header <- paste(shQuote(fullName, type = "cmd"), collapse = ", ") + + # Build body from remaining fields + rest <- fields[setdiff(names(fields), c("given", "family", "middle"))] + other <- .rs.enumerate(rest, function(key, value) + { + sprintf("%s = %s", key, .rs.deparse(value)) + }) + + body <- as.character(c(header, other)) + body <- sprintf(" %s", body) + + parts <- c( + indent("person("), + paste(indent(body), collapse = ",\n"), + indent(")") + ) + + paste(parts, collapse = "\n") +}) .rs.addFunction("secureDownloadMethod", function() { diff --git a/src/cpp/session/modules/SessionPackrat.cpp b/src/cpp/session/modules/SessionPackrat.cpp index 951b2fb77ac..15eea3f5f12 100644 --- a/src/cpp/session/modules/SessionPackrat.cpp +++ b/src/cpp/session/modules/SessionPackrat.cpp @@ -35,6 +35,8 @@ #include "SessionPackages.hpp" #include "session-config.h" +#include "../SessionConsoleInput.hpp" + using namespace rstudio::core; #ifdef TRACE_PACKRAT_OUTPUT @@ -66,14 +68,14 @@ using namespace rstudio::core; namespace rstudio { namespace session { -namespace modules { +namespace modules { namespace packrat { namespace { // Current Packrat actions and state ----------------------------------------- -enum PackratActionType +enum PackratActionType { PACKRAT_ACTION_NONE = 0, PACKRAT_ACTION_SNAPSHOT = 1, @@ -122,13 +124,13 @@ enum PendingSnapshotAction PackratActionType packratAction(const std::string& str) { - if (str == kPackratActionSnapshot) + if (str == kPackratActionSnapshot) return PACKRAT_ACTION_SNAPSHOT; else if (str == kPackratActionRestore) return PACKRAT_ACTION_RESTORE; else if (str == kPackratActionClean) return PACKRAT_ACTION_CLEAN; - else + else return PACKRAT_ACTION_UNKNOWN; } @@ -138,7 +140,7 @@ std::string packratActionName(PackratActionType action) case PACKRAT_ACTION_SNAPSHOT: return kPackratActionSnapshot; break; - case PACKRAT_ACTION_RESTORE: + case PACKRAT_ACTION_RESTORE: return kPackratActionRestore; break; case PACKRAT_ACTION_CLEAN: @@ -163,7 +165,7 @@ void pendingSnapshot(PendingSnapshotAction action); bool getPendingActions(PackratActionType action, bool useCached, const std::string& libraryHash, const std::string& lockfileHash, json::Value* pActions); -bool resolveStateAfterAction(PackratActionType action, +bool resolveStateAfterAction(PackratActionType action, PackratHashType hashType); std::string computeLockfileHash(); std::string computeLibraryHash(); @@ -191,20 +193,20 @@ std::string getHash(PackratHashType hashType, PackratHashState hashState) return computeLibraryHash(); } else - return persistentState().getStoredHash(keyOfHashType(hashType, + return persistentState().getStoredHash(keyOfHashType(hashType, hashState)); } void setStoredHash(PackratHashType hashType, PackratHashState hashState, const std::string& hashValue) { - PACKRAT_TRACE("updating " << keyOfHashType(hashType, hashState) << + PACKRAT_TRACE("updating " << keyOfHashType(hashType, hashState) << " -> " << hashValue); - persistentState().setStoredHash(keyOfHashType(hashType, hashState), + persistentState().setStoredHash(keyOfHashType(hashType, hashState), hashValue); } -std::string updateHash(PackratHashType hashType, PackratHashState hashState, +std::string updateHash(PackratHashType hashType, PackratHashState hashState, const std::string& computedHash = std::string()) { // compute the hash if not already provided @@ -219,7 +221,7 @@ std::string updateHash(PackratHashType hashType, PackratHashState hashState, return newHash; } -// adds content from the given file to the given file if it's a +// adds content from the given file to the given file if it's a // DESCRIPTION file (used to summarize library content for hashing) void addDescContent(const FilePath& path, std::string* pDescContent) { @@ -274,7 +276,7 @@ std::string computeLockfileHash() FilePath lockFilePath = projects::projectContext().directory().completePath(kPackratLockfilePath); - if (!lockFilePath.exists()) + if (!lockFilePath.exists()) return ""; std::string lockFileContent; @@ -284,12 +286,12 @@ std::string computeLockfileHash() LOG_ERROR(error); return ""; } - + return hash::crc32HexHash(lockFileContent); } void checkHashes( - PackratHashType hashType, + PackratHashType hashType, PackratHashState hashState, boost::function onMismatch) { @@ -304,11 +306,11 @@ void checkHashes( // hashes match, no work needed if (oldHash == newHash) return; - else + else onMismatch(oldHash, newHash); } -bool hashStatesMatch(PackratHashType hashType, PackratHashState state1, +bool hashStatesMatch(PackratHashType hashType, PackratHashState state1, PackratHashState state2) { std::string hash1 = getHash(hashType, state1); @@ -320,7 +322,7 @@ bool hashStatesMatch(PackratHashType hashType, PackratHashState state1, bool isHashUnresolved(PackratHashType hashType) { - return !hashStatesMatch(hashType, HASH_STATE_OBSERVED, + return !hashStatesMatch(hashType, HASH_STATE_OBSERVED, HASH_STATE_RESOLVED); } @@ -330,7 +332,7 @@ class AutoSnapshot: public async_r::AsyncRProcess { public: static boost::shared_ptr create( - const FilePath& projectDir, + const FilePath& projectDir, const std::string& targetHash) { boost::shared_ptr pSnapshot(new AutoSnapshot()); @@ -343,7 +345,7 @@ class AutoSnapshot: public async_r::AsyncRProcess PACKRAT_TRACE("starting auto snapshot, R command: " << snapshotCmd); pSnapshot->setTargetHash(targetHash); - pSnapshot->start(snapshotCmd.c_str(), projectDir, + pSnapshot->start(snapshotCmd.c_str(), projectDir, async_r::R_PROCESS_VANILLA); return pSnapshot; } @@ -352,7 +354,7 @@ class AutoSnapshot: public async_r::AsyncRProcess { return targetHash_; } - + private: void setTargetHash(const std::string& targetHash) { @@ -368,7 +370,7 @@ class AutoSnapshot: public async_r::AsyncRProcess { PACKRAT_TRACE("(auto snapshot) " << output); } - + void onCompleted(int exitStatus) { PACKRAT_TRACE("finished auto snapshot, exit status = " << exitStatus); @@ -407,8 +409,8 @@ void pendingSnapshot(PendingSnapshotAction action) else if (action == COMPLETE_SNAPSHOT) { s_autoSnapshotRunning = false; - // if there are remaining actions, re-emit the state to the client - if (!resolveStateAfterAction(PACKRAT_ACTION_SNAPSHOT, + // if there are remaining actions, re-emit the state to the client + if (!resolveStateAfterAction(PACKRAT_ACTION_SNAPSHOT, HASH_TYPE_LOCKFILE)) { s_packageStateChanged = true; @@ -419,7 +421,7 @@ void pendingSnapshot(PendingSnapshotAction action) } -// Checks Packrat options to see whether auto-snapshotting is enabled +// Checks Packrat options to see whether auto-snapshotting is enabled bool isAutoSnapshotEnabled() { bool enabled = kAutoSnapshotDefault; @@ -438,15 +440,15 @@ bool isAutoSnapshotEnabled() // Performs an automatic snapshot of the Packrat library, either immediately // or later (if queue == false). In either case, does not perform a snapshot -// if one is already running for the requested state, or if there are +// if one is already running for the requested state, or if there are // unresolved changes in the lockfile. void performAutoSnapshot(const std::string& newHash, bool queue) { static boost::shared_ptr pAutoSnapshot; - if (pAutoSnapshot && + if (pAutoSnapshot && pAutoSnapshot->isRunning()) { - // is the requested snapshot for the same state we're already + // is the requested snapshot for the same state we're already // snapshotting? if it is, ignore the request if (pAutoSnapshot->getTargetHash() == newHash) { @@ -498,7 +500,7 @@ bool getPendingActions(PackratActionType action, bool useCached, const std::string& libraryHash, const std::string& lockfileHash, json::Value* pActions) { - // checking for actions can be expensive--if this call is for the same + // checking for actions can be expensive--if this call is for the same // action with the same library and lockfile states for which we previously // queried for that action, serve cached state static std::string cachedLibraryHash[PACKRAT_ACTION_MAX]; @@ -509,7 +511,7 @@ bool getPendingActions(PackratActionType action, bool useCached, lockfileHash == cachedLockfileHash[action] && useCached) { - PACKRAT_TRACE("using cached action list for action '" << + PACKRAT_TRACE("using cached action list for action '" << packratActionName(action) << "' (" << libraryHash << ", " << lockfileHash << ")"); if (pActions && !cachedActions[action].isNull()) @@ -517,7 +519,7 @@ bool getPendingActions(PackratActionType action, bool useCached, return cachedResult[action]; } - PACKRAT_TRACE("caching action list for action '" << + PACKRAT_TRACE("caching action list for action '" << packratActionName(action) << "' (" << libraryHash << ", " << lockfileHash << ")"); @@ -529,13 +531,13 @@ bool getPendingActions(PackratActionType action, bool useCached, // get the list of actions from Packrat SEXP actions; r::sexp::Protect protect; - Error error = r::exec::RFunction(".rs.pendingActions", + Error error = r::exec::RFunction(".rs.pendingActions", packratActionName(action), projects::projectContext().directory().getAbsolutePath()) .call(&actions, &protect); // if an error occurs, presume that there are pending actions (i.e. don't - // resolve the state) + // resolve the state) if (error) { LOG_ERROR(error); @@ -563,14 +565,14 @@ void onLockfileUpdate(const std::string& oldHash, const std::string& newHash) void onLibraryUpdate(const std::string& oldHash, const std::string& newHash) { // perform an auto-snapshot if we don't have a pending restore - if (!isHashUnresolved(HASH_TYPE_LOCKFILE)) + if (!isHashUnresolved(HASH_TYPE_LOCKFILE)) { performAutoSnapshot(newHash, true); } - else + else { - PACKRAT_TRACE("lockfile observed hash " << - getHash(HASH_TYPE_LOCKFILE, HASH_STATE_OBSERVED) << + PACKRAT_TRACE("lockfile observed hash " << + getHash(HASH_TYPE_LOCKFILE, HASH_STATE_OBSERVED) << " doesn't match resolved hash " << getHash(HASH_TYPE_LOCKFILE, HASH_STATE_RESOLVED) << ", skipping auto snapshot"); @@ -586,7 +588,7 @@ void onFileChanged(FilePath sourceFilePath) // ignore file changes while Packrat is running if (s_runningPackratAction != PACKRAT_ACTION_NONE) return; - + // we only care about mutations to files in the Packrat library directory // (and packrat.lock) FilePath libraryPath = @@ -598,11 +600,11 @@ void onFileChanged(FilePath sourceFilePath) module_context::executeOnMainThread(boost::bind(checkHashes, HASH_TYPE_LOCKFILE, HASH_STATE_OBSERVED, onLockfileUpdate)); } - else if (sourceFilePath.isWithin(libraryPath) && - (sourceFilePath.isDirectory() || + else if (sourceFilePath.isWithin(libraryPath) && + (sourceFilePath.isDirectory() || sourceFilePath.getFilename() == "DESCRIPTION")) { - // ignore changes in the RStudio-managed manipulate and rstudio + // ignore changes in the RStudio-managed manipulate and rstudio // directories and the files within them if (sourceFilePath.getFilename() == "manipulate" || sourceFilePath.getFilename() == "rstudio" || @@ -712,7 +714,7 @@ Error initPackratMonitoring() if (!lockfilePath.exists()) return Success(); - // listen for changes to the project files + // listen for changes to the project files PACKRAT_TRACE("found " << lockfilePath.getAbsolutePath() << ", init monitoring"); @@ -728,33 +730,33 @@ Error initPackratMonitoring() } // runs after an (auto) snapshot or restore; returns whether the state was -// resolved successfully -bool resolveStateAfterAction(PackratActionType action, +// resolved successfully +bool resolveStateAfterAction(PackratActionType action, PackratHashType hashType) { // compute the new library and lockfile states - std::string newLibraryHash = + std::string newLibraryHash = getHash(HASH_TYPE_LIBRARY, HASH_STATE_COMPUTED); - std::string newLockfileHash = + std::string newLockfileHash = getHash(HASH_TYPE_LOCKFILE, HASH_STATE_COMPUTED); // mark the library resolved if there are no pending snapshot actions - bool hasPendingSnapshotActions = + bool hasPendingSnapshotActions = getPendingActions(PACKRAT_ACTION_SNAPSHOT, true, newLibraryHash, newLockfileHash, nullptr); if (!hasPendingSnapshotActions) updateHash(HASH_TYPE_LIBRARY, HASH_STATE_RESOLVED, newLibraryHash); - // mark the lockfile resolved if there are no pending restore actions - bool hasPendingRestoreActions = + // mark the lockfile resolved if there are no pending restore actions + bool hasPendingRestoreActions = getPendingActions(PACKRAT_ACTION_RESTORE, true, newLibraryHash, newLockfileHash, nullptr); if (hasPendingRestoreActions) { // if we just finished a snapshot and there are pending restore actions, - // dirty the lockfile so they'll get applied - if (action == PACKRAT_ACTION_SNAPSHOT && !hasPendingSnapshotActions) - setStoredHash(HASH_TYPE_LOCKFILE, HASH_STATE_RESOLVED, + // dirty the lockfile so they'll get applied + if (action == PACKRAT_ACTION_SNAPSHOT && !hasPendingSnapshotActions) + setStoredHash(HASH_TYPE_LOCKFILE, HASH_STATE_RESOLVED, kInvalidHashValue); } else @@ -762,7 +764,7 @@ bool resolveStateAfterAction(PackratActionType action, // if the action changed the underlying store, send the new state to the // client - bool hashChangedState = + bool hashChangedState = !hashStatesMatch(hashType, HASH_STATE_OBSERVED, HASH_STATE_COMPUTED); if (hashChangedState) s_packageStateChanged = true; @@ -787,14 +789,14 @@ void onPackratAction(const std::string& project, if (running && (s_runningPackratAction != PACKRAT_ACTION_NONE)) { - PACKRAT_TRACE("warning: '" << action << "' executed while action " << + PACKRAT_TRACE("warning: '" << action << "' executed while action " << s_runningPackratAction << " was already running"); } PACKRAT_TRACE("packrat action '" << action << "' " << (running ? "started" : "finished")); // action started, cache it and return - if (running) + if (running) { s_runningPackratAction = packratAction(action); return; @@ -840,7 +842,7 @@ void detectReposChanges() else if (reposSEXP != s_lastReposSEXP) { s_lastReposSEXP = reposSEXP; - performAutoSnapshot(getHash(HASH_TYPE_LIBRARY, HASH_STATE_COMPUTED), + performAutoSnapshot(getHash(HASH_TYPE_LIBRARY, HASH_STATE_COMPUTED), false); } } @@ -870,11 +872,11 @@ void onDetectChanges(module_context::ChangeSource source) void activatePackagesIfPendingActions() { - // activate the packages pane if the library or lockfile states are + // activate the packages pane if the library or lockfile states are // unresolved (i.e. there is a pending snapshot or restore) - if (!(hashStatesMatch(HASH_TYPE_LOCKFILE, HASH_STATE_COMPUTED, + if (!(hashStatesMatch(HASH_TYPE_LOCKFILE, HASH_STATE_COMPUTED, HASH_STATE_RESOLVED) && - hashStatesMatch(HASH_TYPE_LIBRARY, HASH_STATE_COMPUTED, + hashStatesMatch(HASH_TYPE_LIBRARY, HASH_STATE_COMPUTED, HASH_STATE_RESOLVED))) { module_context::activatePane("packages"); @@ -943,13 +945,13 @@ Error getPackratActions(const json::JsonRpcRequest& request, json::Value snapshotActions; json::Object json; - std::string oldLibraryHash = + std::string oldLibraryHash = getHash(HASH_TYPE_LIBRARY, HASH_STATE_OBSERVED); // compute new hashes and mark them observed - std::string newLibraryHash = + std::string newLibraryHash = updateHash(HASH_TYPE_LIBRARY, HASH_STATE_OBSERVED); - std::string newLockfileHash = + std::string newLockfileHash = updateHash(HASH_TYPE_LOCKFILE, HASH_STATE_OBSERVED); // take an auto-snapshot if the library hashes don't match: it's necessary @@ -959,25 +961,25 @@ Error getPackratActions(const json::JsonRpcRequest& request, performAutoSnapshot(newLibraryHash, false); // if we're waiting for an auto snapshot or Packrat is doing work, don't - // bug the user with a list of actions until that work is finished. - if (!s_autoSnapshotPending && !s_autoSnapshotRunning && - s_runningPackratAction == PACKRAT_ACTION_NONE) + // bug the user with a list of actions until that work is finished. + if (!s_autoSnapshotPending && !s_autoSnapshotRunning && + s_runningPackratAction == PACKRAT_ACTION_NONE) { // check for pending restore and snapshot actions - bool hasPendingSnapshotActions = + bool hasPendingSnapshotActions = getPendingActions(PACKRAT_ACTION_SNAPSHOT, false, newLibraryHash, newLockfileHash, &snapshotActions); - bool hasPendingRestoreActions = + bool hasPendingRestoreActions = getPendingActions(PACKRAT_ACTION_RESTORE, false, newLibraryHash, newLockfileHash, &restoreActions); - + // if the state could be interpreted as either a pending restore or a // pending snapsot, try to guess which is appropriate if (hasPendingRestoreActions && hasPendingSnapshotActions) { - bool libraryDirty = + bool libraryDirty = newLibraryHash != getHash(HASH_TYPE_LIBRARY, HASH_STATE_RESOLVED); - bool lockfileDirty = + bool lockfileDirty = newLockfileHash != getHash(HASH_TYPE_LOCKFILE, HASH_STATE_RESOLVED); // hide the list of pending restore actions if we think a snapshot is @@ -988,10 +990,10 @@ Error getPackratActions(const json::JsonRpcRequest& request, snapshotActions = json::Value(); } } - + json["restore_actions"] = restoreActions; json["snapshot_actions"] = snapshotActions; - + pResponse->setResult(json); return Success(); } @@ -1030,8 +1032,15 @@ namespace module_context { bool isRequiredPackratInstalled() { - return getPackageCompatStatus("packrat", "0.4.6", + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = getPackageCompatStatus("packrat", "0.4.6", kPackratRStudioProtocolVersion) == COMPAT_OK; + return res; } PackratContext packratContext() @@ -1050,7 +1059,7 @@ PackratContext packratContext() FilePath projectDir = projects::projectContext().directory(); std::string projectPath = string_utils::utf8ToSystem(projectDir.getAbsolutePath()); - + // check and see if the project has been packified Error error = r::exec::RFunction(".rs.isPackified") .addParam(projectPath) @@ -1146,7 +1155,7 @@ json::Object packratOptionsAsJson() r::sexp::Protect rProtect; SEXP optionsSEXP; - Error error = modules::packrat::getPackratOptions(&optionsSEXP, + Error error = modules::packrat::getPackratOptions(&optionsSEXP, &rProtect); if (error) { diff --git a/src/cpp/session/modules/SessionPlots.cpp b/src/cpp/session/modules/SessionPlots.cpp index 1778e36669a..07a7d6c72de 100644 --- a/src/cpp/session/modules/SessionPlots.cpp +++ b/src/cpp/session/modules/SessionPlots.cpp @@ -43,6 +43,8 @@ #include +#include "../SessionConsoleInput.hpp" + using namespace rstudio::core; using namespace boost::placeholders; @@ -50,7 +52,7 @@ namespace rstudio { namespace session { namespace modules { namespace plots { - + namespace { #define MAX_FIG_SIZE 3840*2 @@ -58,36 +60,36 @@ namespace { // locations #define kGraphics "/graphics" -Error getPlotTempdir(const json::JsonRpcRequest& request, +Error getPlotTempdir(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) { std::string tempdir; Error error = r::exec::RFunction("tempdir").callUtf8(&tempdir); if (error) LOG_ERROR(error); - + pResponse->setResult(tempdir); return Success(); } -Error nextPlot(const json::JsonRpcRequest& request, +Error nextPlot(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) -{ +{ r::session::graphics::Display& display = r::session::graphics::display(); return display.setActivePlot(display.activePlotIndex() + 1); -} - -Error previousPlot(const json::JsonRpcRequest& request, +} + +Error previousPlot(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) -{ +{ r::session::graphics::Display& display = r::session::graphics::display(); return display.setActivePlot(display.activePlotIndex() - 1); -} +} + - Error removePlot(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) -{ +{ r::session::graphics::Display& display = r::session::graphics::display(); if (display.plotCount() < 1) @@ -104,10 +106,10 @@ Error removePlot(const json::JsonRpcRequest& request, int activePlot = display.activePlotIndex(); return display.removePlot(activePlot); } -} - - -Error clearPlots(const json::JsonRpcRequest& request, +} + + +Error clearPlots(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) { r::session::graphics::display().clear(); @@ -439,46 +441,46 @@ Error getSavePlotContext(const json::JsonRpcRequest& request, return Success(); } - + template bool extractSizeParams(const http::Request& request, - T min, + T min, T max, T* pWidth, T* pHeight, http::Response* pResponse) -{ +{ // get the width and height parameters - if (!request.queryParamValue("width", - predicate::range(min, max), + if (!request.queryParamValue("width", + predicate::range(min, max), pWidth)) { pResponse->setError(http::status::BadRequest, "invalid width"); return false; } - if (!request.queryParamValue("height", + if (!request.queryParamValue("height", predicate::range(min, max), pHeight)) { pResponse->setError(http::status::BadRequest, "invalid height"); return false; } - + // got two valid params return true; } - + void setImageFileResponse(const FilePath& imageFilePath, - const http::Request& request, + const http::Request& request, http::Response* pResponse) { // set content type pResponse->setContentType(imageFilePath.getMimeContentType()); - + // attempt gzip if (request.acceptsEncoding(http::kGzipEncoding)) pResponse->setContentEncoding(http::kGzipEncoding); - + // set file Error error = pResponse->setBody(imageFilePath); if (error) @@ -490,16 +492,16 @@ void setImageFileResponse(const FilePath& imageFilePath, } } -void setTemporaryFileResponse(const FilePath& filePath, - const http::Request& request, +void setTemporaryFileResponse(const FilePath& filePath, + const http::Request& request, http::Response* pResponse) { // no cache (dynamic content) pResponse->setNoCacheHeaders(); - + // return the file pResponse->setFile(filePath, request); - + // delete the file Error error = filePath.remove(); if (error) @@ -571,7 +573,7 @@ void handleZoomRequest(const http::Request& request, http::Response* pResponse) pResponse->setBody(templateStream, filter); pResponse->setContentType("text/html"); } - + void handleZoomPngRequest(const http::Request& request, http::Response* pResponse) { @@ -592,21 +594,21 @@ void handleZoomPngRequest(const http::Request& request, true); if (saveError) { - pResponse->setError(http::status::InternalServerError, + pResponse->setError(http::status::InternalServerError, saveError.getMessage()); return; } - + // send it back setImageFileResponse(imagePath, request, pResponse); - + // delete the temp file Error error = imagePath.remove(); if (error) LOG_ERROR(error); } -void handlePngRequest(const http::Request& request, +void handlePngRequest(const http::Request& request, http::Response* pResponse) { // get the width and height parameters @@ -649,14 +651,14 @@ void handlePngRequest(const http::Request& request, // plot's png). to handle this redirection we should always maintain an // entry point with these semantics. if we wish to have an entry point // for obtaining arbitrary pngs then it should be separate from this. - -void handleGraphicsRequest(const http::Request& request, + +void handleGraphicsRequest(const http::Request& request, http::Response* pResponse) -{ +{ // extract plot key from request (take everything after the last /) std::string uri = request.uri(); std::size_t lastSlashPos = uri.find_last_of('/'); - if (lastSlashPos == std::string::npos || + if (lastSlashPos == std::string::npos || lastSlashPos == (uri.length() - 1)) { std::string errmsg = "invalid graphics uri: " + uri; @@ -665,11 +667,11 @@ void handleGraphicsRequest(const http::Request& request, return; } std::string filename = uri.substr(lastSlashPos+1); - + // calculate the path to the png using namespace rstudio::r::session; FilePath imagePath = graphics::display().imagePath(filename); - + // if it exists then return it if (imagePath.exists()) { @@ -688,7 +690,7 @@ void handleGraphicsRequest(const http::Request& request, std::string imageFilename = graphics::display().imageFilename(); std::string imageLocation = std::string(kGraphics "/") + imageFilename; - + // redirect to it pResponse->setMovedTemporarily(request, imageLocation); } @@ -700,7 +702,7 @@ void handleGraphicsRequest(const http::Request& request, } } - + void enquePlotsChanged(const r::session::graphics::DisplayState& displayState, bool activatePlots, bool showManipulator) { @@ -715,14 +717,14 @@ void enquePlotsChanged(const r::session::graphics::DisplayState& displayState, jsonPlotsState["activatePlots"] = activatePlots && (displayState.plotCount > 0); jsonPlotsState["showManipulator"] = showManipulator; - ClientEvent plotsStateChangedEvent(client_events::kPlotsStateChanged, + ClientEvent plotsStateChangedEvent(client_events::kPlotsStateChanged, jsonPlotsState); - + // fire it module_context::enqueClientEvent(plotsStateChangedEvent); - + } - + void renderGraphicsOutput(bool activatePlots, bool showManipulator) { using namespace rstudio::r::session; @@ -867,8 +869,8 @@ SEXP rs_savePlotAsImage(SEXP fileSEXP, } -} // anonymous namespace - +} // anonymous namespace + bool haveCairoPdf() { // make sure there is a real x server running on osx @@ -878,6 +880,12 @@ bool haveCairoPdf() return false; #endif + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + SEXP functionSEXP = R_NilValue; r::sexp::Protect rProtect; r::exec::RFunction f(".rs.getPackageFunction", "cairo_pdf", "grDevices"); @@ -888,7 +896,8 @@ bool haveCairoPdf() return false; } - return functionSEXP != R_NilValue; + res = functionSEXP != R_NilValue; + return res; } Events& events() @@ -914,7 +923,7 @@ Error initialize() // connect to onShowManipulator using namespace rstudio::r::session; graphics::display().onShowManipulator().connect(bind(onShowManipulator)); - + using namespace module_context; ExecBlock initBlock; initBlock.addFunctions() @@ -941,9 +950,9 @@ Error initialize() return initBlock.execute(); } - + } // namespace plots -} // namespace modules +} // namespace modules } // namespace session } // namespace rstudio - + diff --git a/src/cpp/session/modules/SessionRCompletions.R b/src/cpp/session/modules/SessionRCompletions.R index 56ff6ca34a5..1f306850ea2 100644 --- a/src/cpp/session/modules/SessionRCompletions.R +++ b/src/cpp/session/modules/SessionRCompletions.R @@ -2169,7 +2169,7 @@ assign(x = ".rs.acCompletionTypes", .rs.addFunction("isBrowserActive", function() { - .Call("rs_isBrowserActive") + .Call("rs_isBrowserActive", PACKAGE = "(embedding)") }) # NOTE: This function attempts to find an active frame (if @@ -2448,23 +2448,22 @@ assign(x = ".rs.acCompletionTypes", ## For magrittr completions, we may see a pipe thrown in as part of the 'string' ## and 'functionCallString'. In such a case, we need to strip off everything before - ## a '%>%' and augment the numCommas + ## a '%>%' and augment the numCommas. isPiped <- nzchar(chainObjectName) - if (length(string)) + functionContexts <- which(context == .rs.acContextTypes$FUNCTION) + if (length(functionContexts) == 1L) { + i <- functionContexts pipes <- c("%>%", "%<>%", "%T>%", "%>>%", "\\|>") pattern <- paste(pipes, collapse = "|") - stringPipeMatches <- gregexpr( - pattern, string[[1]], perl = TRUE - )[[1]] - + stringPipeMatches <- gregexpr(pattern, string[[i]], perl = TRUE)[[1]] if (!identical(c(stringPipeMatches), -1L)) { n <- length(stringPipeMatches) idx <- stringPipeMatches[n] + attr(stringPipeMatches, "match.length")[n] isPiped <- TRUE - string[[1]] <- gsub("^[\\s\\n]*", "", substring(string[[1]], idx), perl = TRUE) + string[[i]] <- gsub("^[\\s\\n]*", "", substring(string[[i]], idx), perl = TRUE) ## Figure out the 'parent object' of the call. We munge the ## function call and place that back in, so S3 dispatch and such @@ -2489,7 +2488,9 @@ assign(x = ".rs.acCompletionTypes", fixed = TRUE ) - } else if (nzchar(chainObjectName)) { + } + else if (nzchar(chainObjectName)) + { functionCallString <- sub( "(", @@ -2817,6 +2818,8 @@ assign(x = ".rs.acCompletionTypes", # otherwise, look through the contexts and pick up completions else { + didGetFunctionCompletions <- FALSE + for (i in seq_along(string)) { # Don't provide function completions if we just provided @@ -2839,6 +2842,16 @@ assign(x = ".rs.acCompletionTypes", if (skipFunctionCompletions) next + # Only retrieve function completions for the 'nearest' + # function in the stack + if (context[[i]] == .rs.acContextTypes$FUNCTION) + { + if (didGetFunctionCompletions) + next + + didGetFunctionCompletions <- TRUE + } + completions <- .rs.appendCompletions( completions, .rs.getRCompletions(token, diff --git a/src/cpp/session/modules/SessionRenv.cpp b/src/cpp/session/modules/SessionRenv.cpp index caaa0eef94c..8c14dd9b396 100644 --- a/src/cpp/session/modules/SessionRenv.cpp +++ b/src/cpp/session/modules/SessionRenv.cpp @@ -24,6 +24,7 @@ #include #include +#include "../SessionConsoleInput.hpp" using namespace rstudio::core; @@ -33,7 +34,14 @@ namespace module_context { bool isRequiredRenvInstalled() { - return isPackageVersionInstalled("renv", "0.9.2"); + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = isPackageVersionInstalled("renv", "0.9.2"); + return res; } bool isRenvActive() diff --git a/src/cpp/session/modules/SessionReticulate.cpp b/src/cpp/session/modules/SessionReticulate.cpp index 22c1a381fb3..0389ce94460 100644 --- a/src/cpp/session/modules/SessionReticulate.cpp +++ b/src/cpp/session/modules/SessionReticulate.cpp @@ -26,6 +26,7 @@ #include #include +#include <../SessionConsoleInput.hpp> using namespace rstudio::core; using namespace boost::placeholders; @@ -72,7 +73,7 @@ SEXP rs_reticulateInitialized() { // set initialized flag s_pythonInitialized = true; - + // Python will register its own console control handler, // which also blocks signals from reaching any previously // defined handlers (including RStudio's own). re-initialize @@ -105,7 +106,12 @@ bool isPythonInitialized() bool isReplActive() { - bool active = false; + static bool active = false; + + // return the last known value if the session is busy + if (console_input::executing()) + return active; + Error error = r::exec::RFunction(".rs.reticulate.replIsActive").call(&active); if (error) LOG_ERROR(error); @@ -122,7 +128,7 @@ std::string reticulatePython() Error initialize() { using namespace module_context; - + events().onDeferredInit.connect(onDeferredInit); RS_REGISTER_CALL_METHOD(rs_reticulateInitialized); @@ -130,7 +136,7 @@ Error initialize() ExecBlock initBlock; initBlock.addFunctions() (bind(sourceModuleRFile, "SessionReticulate.R")); - + return initBlock.execute(); } diff --git a/src/cpp/session/modules/SessionSource.R b/src/cpp/session/modules/SessionSource.R index cf5cc178f89..cfe55934f0b 100644 --- a/src/cpp/session/modules/SessionSource.R +++ b/src/cpp/session/modules/SessionSource.R @@ -322,8 +322,42 @@ { if (is.null(path) || !file.exists(path)) return(NULL) - + path <- normalizePath(path, winslash = "/", mustWork = TRUE) .Call("rs_getDocumentProperties", path, includeContents) }) +.rs.addFunction("generateStylerFormatDocumentScript", function(documentPath, + scriptPath) +{ + # only invoke 'styler' on supported file types + ext <- tools::file_ext(documentPath) + if (!tolower(ext) %in% c("r", "rmd", "rmarkdown", "qmd", "rnw")) + return() + + # figure out where 'styler' is installed + stylerPath <- find.package("styler") + libraryPaths <- .libPaths(c(dirname(stylerPath), .libPaths())) + + # create a tidyverse styler, using the current indentation settings + indent <- .rs.readUserPref("num_spaces_for_tab", 2L) + strict <- .rs.readUserPref("code_formatter_styler_strict", TRUE) + + # try to infer the base indentation + contents <- readLines(documentPath, warn = FALSE) + indents <- regexpr("\\S", contents, perl = TRUE) + baseIndent <- min(indents[indents >= 0]) - 1L + + # generate and write code to file + expr <- rlang::expr({ + .libPaths(!!libraryPaths) + styler::style_file( + path = !!documentPath, + indent_by = !!indent, + strict = !!strict, + base_indention = !!baseIndent + ) + }) + + writeLines(deparse(expr), con = scriptPath) +}) diff --git a/src/cpp/session/modules/SessionSource.cpp b/src/cpp/session/modules/SessionSource.cpp index ef24d9303e9..f03930eaaac 100644 --- a/src/cpp/session/modules/SessionSource.cpp +++ b/src/cpp/session/modules/SessionSource.cpp @@ -25,23 +25,22 @@ #include #include -#include +#include +#include #include #include -#include -#include #include #include #include -#include +#include #include #include - -#include - +#include #include +#include #include +#include #include #include @@ -652,6 +651,171 @@ Error saveDocumentDiff(const json::JsonRpcRequest& request, return Success(); } +Error onFormatError( + const Error& error, + const json::JsonRpcFunctionContinuation& continuation) +{ + if (error) + LOG_ERROR(error); + + json::JsonRpcResponse response; + continuation(error, &response); + return error; +} + +template +Error formatDocumentImpl( + const std::string& documentPath, + const json::JsonRpcFunctionContinuation& continuation, + F&& callback) +{ + auto onError = [&](const Error& error) + { + return onFormatError(error, continuation); + }; + + Error error; + + core::system::ProcessOptions options; + if (projects::projectContext().hasProject()) + options.workingDir = projects::projectContext().directory(); + + core::system::ProcessCallbacks callbacks; + + callbacks.onExit = [=](int exitStatus) + { + json::JsonRpcResponse response = callback(); + continuation(Success(), &response); + }; + + std::string formatType = prefs::userPrefs().codeFormatter(); + if (formatType == kCodeFormatterNone || formatType == kCodeFormatterStyler) + { + FilePath rScriptPath; + error = module_context::rScriptPath(&rScriptPath); + if (error) + return onError(error); + + FilePath formatScriptPath = module_context::tempFile("rstudio-format-", "R"); + error = r::exec::RFunction(".rs.generateStylerFormatDocumentScript") + .addUtf8Param(documentPath) + .addUtf8Param(formatScriptPath) + .call(); + if (error) + return onError(error); + + else if (!formatScriptPath.exists()) + return onError(fileNotFoundError(formatScriptPath, ERROR_LOCATION)); + + // TODO: How should we handle the case where a formatter is already + // running on the current file? + error = module_context::processSupervisor().runProgram( + rScriptPath.getAbsolutePath(), + { "-f", formatScriptPath.getAbsolutePath() }, + options, + callbacks); + + if (error) + return onError(error); + + return Success(); + } + else if (formatType == kCodeFormatterExternal) + { + FilePath resolvedPath = module_context::resolveAliasedPath(documentPath); + std::string command = fmt::format( + "{} {}", + prefs::userPrefs().codeFormatterExternalCommand(), + shell_utils::escape(resolvedPath)); + + error = module_context::processSupervisor().runCommand( + command, + options, + callbacks); + + if (error) + return onError(error); + + return Success(); + } + else + { + Error error(boost::system::errc::invalid_argument, ERROR_LOCATION); + error.addProperty("type", formatType); + return onError(error); + } +} + +Error formatDocument( + const json::JsonRpcRequest& request, + const json::JsonRpcFunctionContinuation& continuation) +{ + auto onError = [&](const Error& error) + { + return onFormatError(error, continuation); + }; + + Error error; + + std::string documentId, documentPath; + error = json::readParams(request.params, &documentId, &documentPath); + if (error) + return onError(error); + + return formatDocumentImpl( + documentPath, + continuation, + [=]() + { + return json::JsonRpcResponse(); + }); + +} + +Error formatCode( + const json::JsonRpcRequest& request, + const json::JsonRpcFunctionContinuation& continuation) +{ + auto onError = [&](const Error& error) + { + return onFormatError(error, continuation); + }; + + Error error; + + std::string code; + error = json::readParams(request.params, &code); + if (error) + return onError(error); + + FilePath documentPath = module_context::tempFile("rstudio-format-", "R"); + error = writeStringToFile(documentPath, code); + if (error) + return onError(error); + + return formatDocumentImpl( + documentPath.getAbsolutePath(), + continuation, + [=]() + { + std::string code; + Error error = readStringFromFile(documentPath, &code); + if (error) + LOG_ERROR(error); + + // trim a final newline in the formatted selection + if (boost::algorithm::ends_with(code, "\r\n")) + code = code.substr(0, code.length() - 2); + else if (boost::algorithm::ends_with(code, "\n")) + code = code.substr(0, code.length() - 1); + + json::JsonRpcResponse response; + response.setResult(code); + + return response; + }); +} + Error checkForExternalEdit(const json::JsonRpcRequest& request, json::JsonRpcResponse* pResponse) { @@ -1505,6 +1669,8 @@ Error initialize() using namespace rstudio::r::function_hook; ExecBlock initBlock; initBlock.addFunctions() + (bind(registerAsyncRpcMethod, "format_document", formatDocument)) + (bind(registerAsyncRpcMethod, "format_code", formatCode)) (bind(registerRpcMethod, "new_document", newDocument)) (bind(registerRpcMethod, "open_document", openDocument)) (bind(registerRpcMethod, "save_document", saveDocument)) diff --git a/src/cpp/session/modules/automation/SessionAutomation.R b/src/cpp/session/modules/automation/SessionAutomation.R index 2e9564c6452..56bedef5382 100644 --- a/src/cpp/session/modules/automation/SessionAutomation.R +++ b/src/cpp/session/modules/automation/SessionAutomation.R @@ -27,6 +27,11 @@ .rs.setVar("automation.agentProcess", NULL) +.rs.addFunction("automation.httrGet", function(url) +{ + httr::GET(url, config = httr::timeout(1)) +}) + .rs.addFunction("automation.installRequiredPackages", function() { packages <- c("here", "httr", "later", "processx", "ps", "usethis", "websocket", "withr", "xml2") @@ -115,6 +120,15 @@ params <- list() } + # Convert jsobject to character. + for (i in seq_along(params)) + { + if (inherits(params[[i]], "jsObject")) + { + params[[i]] <- as.character(unclass(params[[i]])) + } + } + # Generate an id for this request. id <- .rs.automation.messageId .rs.setVar("automation.messageId", .rs.automation.messageId + 1L) @@ -166,7 +180,8 @@ # Handle errors. error <- response[["error"]] - if (!is.null(error)) { + if (!is.null(error)) + { fmt <- "execution of '%s' failed: %s [error code %i]" msg <- sprintf(fmt, method, error[["message"]], error[["code"]]) stop(msg, call. = FALSE) @@ -209,6 +224,21 @@ ) }) +.rs.addFunction("automation.killAutomationServer", function(...) +{ + procs <- subset(ps::ps(), name == "rserver") + for (i in seq_len(nrow(procs))) + { + proc <- procs[i, ] + conns <- ps::ps_connections(proc$ps_handle[[1L]]) + if (8788L %in% conns$lport) + { + handle <- ps::ps_handle(pid = proc$pid) + return(ps::ps_kill(handle)) + } + } +}) + .rs.addFunction("automation.ensureRunningServerInstance", function() { # Check and see if we already have an rserver instance listening. @@ -227,15 +257,16 @@ parentEnv <- ps::ps_environ(parentHandle) parentPwd <- parentEnv[["PWD"]] automationScript <- file.path(parentPwd, "rserver-automation") - if (file.exists(automationScript)) - { - message("-- Starting rserver-automation ...") - withr::with_dir(parentPwd, system2(automationScript, wait = FALSE)) - } - else - { + if (!file.exists(automationScript)) stop("rserver does not appear to be running on port 8788") - } + + message("-- Starting rserver-automation ...") + withr::with_dir(parentPwd, system2(automationScript, wait = FALSE)) + + # Kill the process on exit + reg.finalizer(globalenv(), .rs.automation.killAutomationServer, onexit = TRUE) + + }) .rs.addFunction("automation.initialize", function(appPath = NULL, @@ -256,7 +287,7 @@ # Check for an existing session we can attach to. baseUrl <- sprintf("http://localhost:%i", port) jsonVersionUrl <- file.path(baseUrl, "json/version") - response <- .rs.tryCatch(httr::GET(jsonVersionUrl)) + response <- .rs.tryCatch(.rs.automation.httrGet(jsonVersionUrl)) if (!inherits(response, "error")) return(.rs.automation.attach(baseUrl, mode)) @@ -311,6 +342,9 @@ envVars[["RS_CRASH_HANDLER_PROMPT"]] <- "false" envVars[["RSTUDIO_DISABLE_CHECK_FOR_UPDATES"]] <- "1" + # Avoid crashing on arm64 Linux. + envVars[["RSTUDIO_QUERY_FONTS"]] <- "0" + # Build argument list. # https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md @@ -325,7 +359,7 @@ baseArgs, sprintf("--remote-debugging-port=%i", port), sprintf("--user-data-dir=%s", tempdir()), - if (mode == "desktop") "--automation-agent", + if (mode == "desktop") c("--automation-agent"), if (mode == "server") c( "--no-default-browser-check", "--no-first-run", @@ -342,13 +376,22 @@ # Wait until the process is running. while (process$get_status() != "running") + { + status <- process$get_exit_status() + if (!is.null(status)) + { + fmt <- "RStudio agent exited unexpectedly [error code %i]" + stop(sprintf(fmt, status)) + } + Sys.sleep(0.1) + } # Start pinging the Chromium HTTP server. response <- NULL .rs.waitUntil("Chromium HTTP server available", function() { - response <<- .rs.tryCatch(httr::GET(jsonVersionUrl)) + response <<- .rs.tryCatch(.rs.automation.httrGet(jsonVersionUrl)) !inherits(response, "error") }) @@ -371,7 +414,7 @@ # Get the websocket debugger URL. url <- .rs.nullCoalesce(url, { jsonVersionUrl <- file.path(baseUrl, "json/version") - response <- httr::GET(jsonVersionUrl) + response <- .rs.automation.httrGet(jsonVersionUrl) jsonResponse <- .rs.fromJSON(rawToChar(response$content)) jsonResponse$webSocketDebuggerUrl }) @@ -550,12 +593,17 @@ automationMode = NULL, gitRef = NULL) { + on.exit(.rs.automation.onFinishedRunningAutomation(), add = TRUE) + # Resolve the project root. Note that test are expected to be found # within the 'src/cpp/session/automation' sub-directory of this path. projectRoot <- .rs.nullCoalesce(projectRoot, { Sys.getenv("RSTUDIO_AUTOMATION_ROOT", unset = NA) }) + # Resolve the report file from session options if provided. + reportFile <- .rs.nullCoalesce(reportFile, .rs.automation.reportFile()) + # If the path to a test directory was provided, use that. if (!is.na(projectRoot)) return(.rs.automation.runImpl(projectRoot, reportFile, automationMode)) @@ -623,7 +671,7 @@ fmt <- "https://api.github.com/repos/rstudio/rstudio/contents/%s?ref=%s" url <- sprintf(fmt, path, ref) - response <- httr::GET(url) + response <- .rs.automation.httrGet(url) result <- httr::content(response, as = "parsed") # Iterate through the directory contents, and get download links. @@ -647,3 +695,18 @@ Sys.getenv("RSTUDIO_AUTOMATION_MODE", unset = defaultMode) }) }) + +.rs.addFunction("automation.reportFile", function() +{ + .Call("rs_automationReportFile", PACKAGE = "(embedding)") +}) + +.rs.addFunction("automation.onFinishedRunningAutomation", function() +{ + isJenkins <- Sys.getenv("JENKINS_URL", unset = NA) + if (!is.na(isJenkins)) + quit(status = 0L) + + message("- Automated tests have finished running.") + message("- You can now close this instance of RStudio.") +}) diff --git a/src/cpp/session/modules/automation/SessionAutomation.cpp b/src/cpp/session/modules/automation/SessionAutomation.cpp index 42fb5c7b82e..dfae75cd0eb 100644 --- a/src/cpp/session/modules/automation/SessionAutomation.cpp +++ b/src/cpp/session/modules/automation/SessionAutomation.cpp @@ -22,6 +22,8 @@ #include #include +#include +#include #include @@ -36,23 +38,24 @@ namespace automation { namespace { -} // end anonymous namespace - -Error run() +SEXP rs_automationReportFile() { FilePath reportFile = session::options().automationReportFile(); if (reportFile.isEmpty()) - reportFile = module_context::tempFile("automation-", ".xml"); + return R_NilValue; - return r::exec::RFunction(".rs.automation.run") - .addParam("reportFile", reportFile.getAbsolutePath()) - .call(); + r::sexp::Protect protect; + return r::sexp::create(reportFile.getAbsolutePath(), &protect); } +} // end anonymous namespace + Error initialize() { using namespace module_context; + RS_REGISTER_CALL_METHOD(rs_automationReportFile); + ExecBlock initBlock; initBlock.addFunctions() (boost::bind(sourceModuleRFile, "SessionAutomation.R")) diff --git a/src/cpp/session/modules/automation/SessionAutomation.hpp b/src/cpp/session/modules/automation/SessionAutomation.hpp index 1114d68a870..eb2a7d2a9ca 100644 --- a/src/cpp/session/modules/automation/SessionAutomation.hpp +++ b/src/cpp/session/modules/automation/SessionAutomation.hpp @@ -27,8 +27,6 @@ namespace session { namespace modules { namespace automation { -core::Error run(); - core::Error initialize(); } // namespace source_control diff --git a/src/cpp/session/modules/automation/SessionAutomationRemote.R b/src/cpp/session/modules/automation/SessionAutomationRemote.R index fa8dff3dcea..dc585607a26 100644 --- a/src/cpp/session/modules/automation/SessionAutomationRemote.R +++ b/src/cpp/session/modules/automation/SessionAutomationRemote.R @@ -24,17 +24,29 @@ # !diagnostics suppress=client,self .rs.defineVar("automation.remotePrivateEnv", new.env(parent = .rs.toolsEnv())) .rs.defineVar("automation.remote", new.env(parent = emptyenv())) - -.rs.setVar("automation.remoteInstance", NULL) +.rs.defineVar("automation.remoteInstance", NULL) .rs.addFunction("automation.newRemote", function(mode = NULL) { + # Generate the remote instance. mode <- .rs.automation.resolveMode(mode) client <- .rs.automation.initialize(mode = mode) assign("client", client, envir = .rs.automation.remote) assign("self", .rs.automation.remote, envir = .rs.automation.remotePrivateEnv) .rs.setVar("automation.remoteInstance", .rs.automation.remote) - .rs.automation.remoteInstance + remote <- .rs.automation.remoteInstance + + # Load testthat, and provide a 'test_that' override which automatically cleans + # up various state when a test is run. + library(testthat) + .rs.addGlobalFunction("test_that", function(desc, code) + { + on.exit(.rs.automation.remoteInstance$sessionReset(), add = TRUE) + testthat::test_that(desc, code) + }) + + # Return the remote instance. + remote }) .rs.addFunction("automation.deleteRemote", function() @@ -65,8 +77,43 @@ .rs.automation.addRemoteFunction("commandExecute", function(command) { - code <- .rs.deparse(call(".rs.api.executeCommand", as.character(command))) - self$consoleExecute(code) + jsCode <- deparse(substitute( + window.rstudioCallbacks.commandExecute(command), + list(command = command) + )) + + self$jsExec(jsCode) +}) + +.rs.automation.addRemoteFunction("completionsRequest", function(text = "") +{ + # Generate the autocomplete pop-up. + self$keyboardExecute(text, "") + + # Get the completion list from the pop-up + completionListEl <- self$jsObjectViaSelector("#rstudio_popup_completions") + completionText <- completionListEl$innerText + + # Dismiss the popup. + self$keyboardExecute("") + + # Remove any inserted code. + for (i in seq_len(nchar(text))) + { + self$keyboardExecute("") + } + + # Extract just the completion items (remove package annotations) + parts <- strsplit(completionText, "\n{2,}")[[1]] + parts <- gsub("\\n.*", "", parts) + + # Return those parts + parts +}) + +.rs.automation.addRemoteFunction("consoleClear", function() +{ + self$keyboardExecute("", "", "", "", "") }) .rs.automation.addRemoteFunction("consoleExecuteExpr", function(expr) @@ -102,6 +149,7 @@ { # Write document contents to file. documentPath <- tempfile("document-", fileext = ext) + documentPath <- chartr("\\", "/", documentPath) writeLines(contents, con = documentPath) # Open that document in the attached editor. @@ -138,6 +186,9 @@ # Get a reference to the editor in that instance. editor <- self$editorGetInstance() + # Wait a small bit, so Ace can tokenize the document. + Sys.sleep(0.2) + # Invoke callback with the editor instance. callback(editor) }) @@ -162,14 +213,22 @@ nodeId }) -.rs.automation.addRemoteFunction("domClickElement", function(selector, button = "left") +.rs.automation.addRemoteFunction("domClickElement", function(selector, + objectId = NULL, + verticalOffset = 0L, + horizontalOffset = 0L, + button = "left") { - # Query for the requested node. - nodeId <- self$domGetNodeId(selector) + objectId <- .rs.nullCoalesce(objectId, { + + # Query for the requested node. + nodeId <- self$domGetNodeId(selector) + + # Get a JavaScript object ID associated with this node. + response <- self$client$DOM.resolveNode(nodeId) + response$object$objectId + }) - # Get a JavaScript object ID associated with this node. - response <- self$client$DOM.resolveNode(nodeId) - objectId <- response$object$objectId # Use that object ID to request the object's bounding rectangle. code <- "function() { @@ -178,24 +237,28 @@ response <- self$client$Runtime.callFunctionOn( functionDeclaration = code, - objectId = objectId, + objectId = objectId ) + # Compute coordinates for click action. We'll try to target + # the center of the requested element. domRect <- .rs.fromJSON(response$result$value) + x <- domRect$x + (domRect$width / 2) + horizontalOffset + y <- domRect$y + (domRect$height / 2) + verticalOffset # Use the position of that element to simulate a click. self$client$Input.dispatchMouseEvent( type = "mousePressed", - x = domRect$x + (domRect$width / 2), - y = domRect$y + (domRect$height / 2), + x = x, + y = y, button = button, clickCount = 1L ) self$client$Input.dispatchMouseEvent( type = "mouseReleased", - x = domRect$x + (domRect$width / 2), - y = domRect$y + (domRect$height / 2), + x = x, + y = y, button = button, clickCount = 1L ) @@ -203,40 +266,39 @@ .rs.automation.addRemoteFunction("editorGetInstance", function() { - jsCode <- .rs.heredoc(r'{ + jsCode <- .rs.heredoc(' var id = $RStudio.last_focused_editor_id; var container = document.getElementById(id); container.env.editor - }') + ') response <- self$client$Runtime.evaluate(expression = jsCode) .rs.automation.wrapJsResponse(self, response) }) -.rs.automation.addRemoteFunction("jsExec", function(expression) +.rs.automation.addRemoteFunction("jsExec", function(jsExpr) { # Implicit return for single-line expressions. - expression <- strsplit(expression, "\n", fixed = TRUE)[[1L]] - if (length(expression) == 1L && !.rs.startsWith(expression, "return ")) - expression <- paste("return", expression) + jsExpr <- strsplit(jsExpr, "\n", fixed = TRUE)[[1L]] + if (length(jsExpr) == 1L && !.rs.startsWith(jsExpr, "return ")) + jsExpr <- paste("return", jsExpr) # Build a command that executes the Javascript, and returns as JSON. - jsonExpression <- sprintf( + jsStringifyExpr <- sprintf( "JSON.stringify((function() { %s })())", - paste(expression, collapse = "\n") + paste(jsExpr, collapse = "\n") ) # Execute it. - self$client$Runtime.evaluate(expression = jsonExpression) - jsonResponse <- self$client$Runtime.evaluate(expression = jsonExpression) + jsResponse <- self$client$Runtime.evaluate(expression = jsStringifyExpr) # Check for error. - if (!is.null(jsonResponse$exceptionDetails)) - stop(jsonResponse$exceptionDetails$exception$description) + if (!is.null(jsResponse$exceptionDetails)) + stop(jsResponse$exceptionDetails$exception$description) # Marshal values back to R. - .rs.fromJSON(jsonResponse$result$value) + .rs.fromJSON(jsResponse$result$value) }) .rs.automation.addRemoteFunction("jsCall", function(objectId, jsFunc) @@ -262,14 +324,22 @@ .rs.automation.addRemoteFunction("jsObjectViaExpression", function(expression) { - response <- self$client$Runtime.evaluate(expression) + response <- .rs.waitFor(expression, function() + { + self$client$Runtime.evaluate(expression) + }) + .rs.automation.wrapJsResponse(self, response) }) .rs.automation.addRemoteFunction("jsObjectViaSelector", function(selector) { - nodeId <- self$domGetNodeId(selector) - response <- self$client$DOM.resolveNode(nodeId) + response <- .rs.waitFor(selector, function() + { + nodeId <- self$domGetNodeId(selector) + self$client$DOM.resolveNode(nodeId) + }) + .rs.automation.wrapJsResponse(self, response) }) @@ -283,7 +353,7 @@ shortcut <- sub(reShortcut, "\\1", input, perl = TRUE) self$shortcutExecute(shortcut) } - else + else if (nzchar(input)) { self$client$Input.insertText(input) } @@ -314,6 +384,20 @@ invisible(alive) }) +.rs.automation.addRemoteFunction("sessionReset", function() +{ + # Clear any popups that might be visible. + self$keyboardExecute("") + + # Clear any text that might be set. + self$keyboardExecute("", "") + + # Remove any existing R objects. + self$commandExecute("closeAllSourceDocs") + self$consoleExecuteExpr(rm(list = ls())) + self$keyboardExecute("") +}) + .rs.automation.addRemoteFunction("shortcutExecute", function(shortcut) { parts <- tolower(strsplit(shortcut, "\\s*\\+\\s*", perl = TRUE)[[1L]]) @@ -328,6 +412,13 @@ if ("shift" %in% parts) modifiers <- bitwOr(modifiers, 8L) + # 'cmd' means 'meta' on macOS, 'ctrl' otherwise + if ("cmd" %in% parts || "command" %in% parts) + { + modifier <- ifelse(.rs.platform.isMacos, 4L, 2L) + modifiers <- bitwOr(modifiers, modifier) + } + key <- tail(parts, n = 1L) code <- .rs.automationConstants.keyToKeyCodeMap[[key]] if (is.null(code)) diff --git a/src/cpp/session/modules/automation/SessionAutomationRemoteObject.R b/src/cpp/session/modules/automation/SessionAutomationRemoteObject.R index cf94ada0417..b3728b09307 100644 --- a/src/cpp/session/modules/automation/SessionAutomationRemoteObject.R +++ b/src/cpp/session/modules/automation/SessionAutomationRemoteObject.R @@ -68,11 +68,11 @@ .rs.addFunction("automation.invokeJsFunction", function(self, objectId, parentObjectId, params) { # Create function we'll invoke. - jsFunc <- .rs.heredoc(r'{ + jsFunc <- .rs.heredoc(' function(context) { return this.apply(context, [].slice.call(arguments, 1)); } - }') + ') # Initialize arguments array. arguments <- vector("list", length(params) + 1L) @@ -117,7 +117,7 @@ self <- attr(x, "self", exact = TRUE) - callback <- .rs.heredoc(r"{ + callback <- .rs.heredoc(' function() { var result = {}; for (var key in this) { @@ -125,7 +125,7 @@ } return JSON.stringify(result); } - }") + ') objectId <- attr(x, "id", exact = TRUE) response <- self$jsCall(objectId, callback) diff --git a/src/cpp/session/modules/build/SessionBuild.cpp b/src/cpp/session/modules/build/SessionBuild.cpp index 747d8eea5ac..48e2b34759a 100644 --- a/src/cpp/session/modules/build/SessionBuild.cpp +++ b/src/cpp/session/modules/build/SessionBuild.cpp @@ -36,6 +36,9 @@ #include #include +#include +#include "../modules/rmarkdown/SessionRMarkdown.hpp" + #ifdef _WIN32 # include #endif @@ -47,10 +50,9 @@ #include #include +#include #include -#include #include -#include #include #include "SessionBuildErrors.hpp" @@ -354,7 +356,7 @@ class Build : boost::noncopyable, core::system::setenv(&environment, "R_LIBS", rLibs); // pass along RSTUDIO_VERSION - core::system::setenv(&environment, "RSTUDIO_VERSION", module_context::rstudioVersion(true)); + core::system::setenv(&environment, "RSTUDIO_VERSION", modules::rmarkdown::parsableRStudioVersion()); core::system::setenv(&environment, "RSTUDIO_LONG_VERSION", RSTUDIO_VERSION); options.environment = environment; diff --git a/src/cpp/session/modules/data/DataViewer.cpp b/src/cpp/session/modules/data/DataViewer.cpp index bc6d95d7f7f..1797f7a9090 100644 --- a/src/cpp/session/modules/data/DataViewer.cpp +++ b/src/cpp/session/modules/data/DataViewer.cpp @@ -24,13 +24,14 @@ #include #include -#include #include +#include + +#include #include #include #include #include -#include #define R_INTERNAL_FUNCTIONS #include @@ -538,10 +539,10 @@ json::Value getColSlice(SEXP dataSEXP, // NB: may throw exceptions! these are expected to be handled by the handlers // in getGridData, where they will be marshaled to JSON and displayed on the // client. -json::Value getData(SEXP dataSEXP, - int maxRows, - int maxCols, - const http::Fields& fields) +json::Object getData(SEXP dataSEXP, + int maxRows, + int maxCols, + const http::Fields& fields) { Error error; r::sexp::Protect protect; @@ -834,13 +835,14 @@ json::Value getData(SEXP dataSEXP, // all done, add row data data.push_back(rowData); } - + json::Object result; result["draw"] = draw; result["recordsTotal"] = nrow; result["recordsFiltered"] = filteredNRow; result["data"] = data; - return std::move(result); + + return result; } Error getGridData(const http::Request& request, @@ -957,7 +959,7 @@ Error getGridData(const http::Request& request, } } } - catch(r::exec::RErrorException& e) + catch (r::exec::RErrorException& e) { // marshal R errors to the client in the format DataTables (and our own // error handling code) expects diff --git a/src/cpp/session/modules/environment/SessionEnvironment.cpp b/src/cpp/session/modules/environment/SessionEnvironment.cpp index ccda1e9ec84..bb0820bfc5b 100644 --- a/src/cpp/session/modules/environment/SessionEnvironment.cpp +++ b/src/cpp/session/modules/environment/SessionEnvironment.cpp @@ -47,6 +47,8 @@ #include "EnvironmentUtils.hpp" +#include "../../SessionConsoleInput.hpp" + #if defined(_WIN32) # define kLibraryName "R.dll" #elif defined(__APPLE__) @@ -60,7 +62,7 @@ using namespace boost::placeholders; namespace rstudio { namespace session { -namespace modules { +namespace modules { namespace environment { namespace { @@ -83,7 +85,7 @@ std::string s_monitoredPythonModule; // browser state by pushing new contexts / frames on the stack bool s_browserActive = false; -// are we currently monitoring the environment? this is almost always true, but can be +// are we currently monitoring the environment? this is almost always true, but can be // disabled by the user to help mitigate pathological cases in which environment monitoring // has undesirable side effects. bool s_monitoring = true; @@ -143,54 +145,54 @@ bool isSerializableImpl(SEXP valueSEXP) // Check for SEXP types that we know can be safely serialized. switch (TYPEOF(valueSEXP)) { - + // Assume that promises can be safely serialized. // (We just want to avoid evaluating them here.) case PROMSXP: return true; - + // Assume that functions can be serialized. // Technically, a function's closure _could_ contain arbitrary objects, // which might not be serializable, but that should be exceedingly rare. case CLOSXP: return true; - + } - + // Check for SEXP types that we know cannot be safely serialized. switch (TYPEOF(valueSEXP)) { - + // External pointers and weak references cannot be serialized. case EXTPTRSXP: case WEAKREFSXP: return false; - + } - + // Assume base environments can be serialized. if (valueSEXP == R_BaseEnv || valueSEXP == R_BaseNamespace) return true; - + if (TYPEOF(valueSEXP) == ENVSXP) { // Assume package environments + namespaces can be serialized. if (R_IsNamespaceEnv(valueSEXP) || R_IsPackageEnv(valueSEXP)) return true; } - + // Check for 'known-safe' object classes. auto safeClasses = { "data.frame", "grf", "igraph" }; for (auto&& safeClass : safeClasses) if (Rf_inherits(valueSEXP, safeClass)) return true; - + // Check for 'known-unsafe' object classes. auto unsafeClasses = { "ArrowObject", "DBIConnection", "python.builtin.object" }; for (auto&& unsafeClass : unsafeClasses) if (Rf_inherits(valueSEXP, unsafeClass)) return false; - + // Assume other objects can be serialized. return true; } @@ -208,10 +210,10 @@ bool isGlobalEnvironmentSerializable() { // Start building a new cache of serialized object state. SerializationCache newCache; - + // Flag tracking whether we found an object which cannot be serialized. bool allValuesSerializable = true; - + // Iterate over values in the global environment, and compute whether they can be serialized. SEXP hashTableSEXP = HASHTAB(R_GlobalEnv); R_xlen_t n = Rf_xlength(hashTableSEXP); @@ -237,11 +239,11 @@ bool isGlobalEnvironmentSerializable() } } } - + // Set the new cache. s_serializationCache.clear(); s_serializationCache = newCache; - + // Return true only if all values can be serialized. return allValuesSerializable; } @@ -288,21 +290,21 @@ bool listHasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) return false; } -bool frameBindingIsActive(SEXP binding) +bool frameBindingIsActive(SEXP binding) { static unsigned int ACTIVE_BINDING_MASK = (1<<15); return reinterpret_cast(binding)->gp & ACTIVE_BINDING_MASK; } -bool frameBindingHasExternalPointer(SEXP b, bool nullPtr, std::set& visited) +bool frameBindingHasExternalPointer(SEXP b, bool nullPtr, std::set& visited) { if (frameBindingIsActive(b)) return false; // ->extra is only used for immediate bindings: this needs special care - // before we call CAR() because it might error with "bad binding access": - // from Rinlinedfuns.h : - // + // before we call CAR() because it might error with "bad binding access": + // from Rinlinedfuns.h : + // // INLINE_FUN SEXP CAR(SEXP e) // { // if (BNDCELL_TAG(e)) @@ -317,22 +319,22 @@ bool frameBindingHasExternalPointer(SEXP b, bool nullPtr, std::set& visite { reinterpret_cast(b)->extra = 0; } - else + else { switch(typetag) { case INTSXP: - case REALSXP: + case REALSXP: case LGLSXP: // this is an immediate binding, R_expand_binding_value() would expand to a scalar return false; - + default: // otherwise (not sure this even hapens), ->extra should not be set: unset it reinterpret_cast(b)->extra = 0; } } } - + // now safe to test the value in CAR() return hasExternalPointer(CAR(b), nullPtr, visited); } @@ -343,7 +345,7 @@ bool frameHasExternalPointer(SEXP frame, bool nullPtr, std::set& visited) { if (frameBindingHasExternalPointer(frame, nullPtr, visited)) return true; - + frame = CDR(frame); } @@ -355,7 +357,7 @@ bool envHasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) SEXP hash = HASHTAB(obj); if (hash == R_NilValue) return frameHasExternalPointer(FRAME(obj), nullPtr, visited); - + R_xlen_t n = XLENGTH(hash); for (R_xlen_t i = 0; i < n; i++) { @@ -401,9 +403,9 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) visited.insert(obj); // check if this is an external pointer - if (r::sexp::isExternalPointer(obj)) + if (r::sexp::isExternalPointer(obj)) { - // NOTE: this includes UserDefinedDatabase, aka + // NOTE: this includes UserDefinedDatabase, aka // external pointers to R_ObjectTable // when nullPtr is true, only return true for null pointer xp @@ -420,10 +422,10 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) switch(TYPEOF(obj)) { - case SYMSXP: + case SYMSXP: return false; - case ENVSXP: + case ENVSXP: { if (envHasExternalPointer(obj, nullPtr, visited)) return true; @@ -436,7 +438,7 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) return true; break; } - + case LISTSXP: case LANGSXP: { @@ -444,7 +446,7 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) return true; break; } - + case WEAKREFSXP: { if (weakrefHasExternalPointer(obj, nullPtr, visited)) @@ -452,7 +454,7 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) break; } - case PROMSXP: + case PROMSXP: { SEXP value = PRVALUE(obj); if (value != R_UnboundValue) @@ -460,7 +462,7 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) if (hasExternalPointer(value, nullPtr, visited)) return true; } - else + else { if (hasExternalPointer(PRCODE(obj), nullPtr, visited)) return true; @@ -486,13 +488,13 @@ bool hasExternalPointer(SEXP obj, bool nullPtr, std::set& visited) default: break; } - + // altrep objects use ATTRIB() to hold class info, so no need - // to check ATTRIB() on them, but altrepHasExternalPointer() + // to check ATTRIB() on them, but altrepHasExternalPointer() // checks for their data1 and data2, aka CAR() and CDR() if (isAltrep(obj)) return altrepHasExternalPointer(obj, nullPtr, visited); - + // check attributes, this includes slots for S4 objects if (hasExternalPointer(ATTRIB(obj), nullPtr, visited)) return true; @@ -510,7 +512,7 @@ bool hasExternalPtr(SEXP obj, // environment to search for external pointer SEXP rs_hasExternalPointer(SEXP objSEXP, SEXP nullSEXP) { r::sexp::Protect protect; - + bool nullPtr = r::sexp::asLogical(nullSEXP); return r::sexp::create(hasExternalPtr(objSEXP, nullPtr), &protect); } @@ -523,7 +525,7 @@ SEXP rs_hasAltrep(SEXP obj) return r::sexp::create(hasAltrep(obj), &protect); } -// Is an object an R ALTREP object? +// Is an object an R ALTREP object? SEXP rs_isAltrep(SEXP obj) { r::sexp::Protect protect; @@ -538,10 +540,10 @@ SEXP rs_dim(SEXP objectSEXP) // default values for rows, columns int numRows = -1; int numCols = r::sexp::length(objectSEXP); - + SEXP rowNamesInfoSEXP = R_NilValue; r::sexp::Protect protect; - + Error error = r::exec::RFunction("base:::.row_names_info") .addParam(objectSEXP) .addParam(0) @@ -551,7 +553,7 @@ SEXP rs_dim(SEXP objectSEXP) LOG_ERROR(error); return R_NilValue; } - + // Avoid materializing certain ALTREP representations. // // https://github.com/rstudio/rstudio/issues/13907 @@ -577,7 +579,7 @@ SEXP rs_dim(SEXP objectSEXP) canComputeRows = false; } } - + // Detect compact row names. if (canComputeRows) { @@ -590,13 +592,13 @@ SEXP rs_dim(SEXP objectSEXP) numRows = r::sexp::length(rowNamesInfoSEXP); } } - + SEXP resultSEXP = Rf_allocVector(INTSXP, 2); INTEGER(resultSEXP)[0] = numRows; INTEGER(resultSEXP)[1] = numCols; return resultSEXP; } - + // Otherwise, just call 'dim()' directly r::sexp::Protect protect; SEXP dimSEXP = R_NilValue; @@ -605,7 +607,7 @@ SEXP rs_dim(SEXP objectSEXP) .call(&dimSEXP, &protect); if (error) LOG_ERROR(error); - + return dimSEXP; } @@ -632,13 +634,13 @@ SEXP simulatedSourceRefsOfContext(const r::context::RCntxt& context, { SEXP simulatedSrcref = R_NilValue; r::sexp::Protect protect; - + // The objects we will later transmit to .rs.simulateSourceRefs below // include language objects that we need to protect from early evaluation. // Attach them to a carrier SEXP as attributes rather than passing directly. SEXP info = r::sexp::create("_rs_sourceinfo", &protect); r::sexp::setAttrib(info, "_rs_callfun", context.callfun()); - + if (lineContext) { r::sexp::setAttrib(info, "_rs_callobj", lineContext.call()); @@ -647,11 +649,11 @@ SEXP simulatedSourceRefsOfContext(const r::context::RCntxt& context, { SEXP lastDebugSEXP = r::sexp::create(pLineDebugState->lastDebugText, &protect); r::sexp::setAttrib(info, "_rs_calltext", lastDebugSEXP); - + SEXP lastLineSEXP = r::sexp::create(pLineDebugState->lastDebugLine, &protect); r::sexp::setAttrib(info, "_rs_lastline", lastLineSEXP); } - + Error error = r::exec::RFunction(".rs.simulateSourceRefs", info) .call(&simulatedSrcref, &protect); if (error) @@ -668,13 +670,13 @@ json::Array callFramesAsJson( { Error error; using namespace r::context; - + RCntxt prevContext; RCntxt srcContext = globalContext(); json::Array listFrames; int contextDepth = 0; std::map envSrcrefCtx; - + // We want to treat the function associated with the top-level // browser context specially. This allows us to do so. enum BrowseContextState { @@ -682,7 +684,7 @@ json::Array callFramesAsJson( BrowserContextFound, BrowserContextUsed, }; - + SEXP browserCloenv = R_NilValue; BrowseContextState browserContextState = BrowserContextNone; @@ -692,7 +694,7 @@ json::Array callFramesAsJson( bool isFunctionContext = (context->callflag() & (CTXT_FUNCTION | CTXT_BROWSER)); if (!isFunctionContext) continue; - + // if this context has a valid srcref, use it to supply the srcrefs for // debugging in the environment of the callee. note that there may be // multiple srcrefs on the stack for a given closure; in this case we @@ -700,21 +702,21 @@ json::Array callFramesAsJson( SEXP srcref = context->contextSourceRefs(); if (!isValidSrcref(srcref)) continue; - + RCntxt nextContext = context->nextcontext(); if (nextContext.isNull()) continue; - + SEXP cloenv = context->nextcontext().cloenv(); if (cloenv == R_NilValue) continue; - + if (envSrcrefCtx.find(cloenv) != envSrcrefCtx.end()) continue; - + envSrcrefCtx[cloenv] = *context; } - + for (auto context = RCntxt::begin(); context != r::context::RCntxt::end(); context++) { if (browserContextState == BrowserContextNone) @@ -725,7 +727,7 @@ json::Array callFramesAsJson( browserContextState = BrowserContextFound; } } - + if (context->callflag() & CTXT_FUNCTION) { json::Object varFrame; @@ -735,7 +737,7 @@ json::Array callFramesAsJson( error = context->functionName(&functionName); if (error) LOG_ERROR(error); - + varFrame["function_name"] = functionName; varFrame["is_error_handler"] = context->isErrorHandler(); varFrame["is_hidden"] = context->isDebugHidden(); @@ -756,7 +758,7 @@ json::Array callFramesAsJson( } SEXP srcref = srcContext.contextSourceRefs(); - + // mark this as a source-equivalent function if it's evaluating user // code into the global environment varFrame["is_source_equiv"] = @@ -767,7 +769,7 @@ json::Array callFramesAsJson( error = srcContext.fileName(&filename); if (error) LOG_ERROR(error); - + varFrame["file_name"] = filename; varFrame["aliased_file_name"] = module_context::createAliasedPath(FilePath(filename)); @@ -776,22 +778,22 @@ json::Array callFramesAsJson( { varFrame["real_sourceref"] = true; sourceRefToJson(srcref, &varFrame); - + std::string lines; Error error = r::exec::RFunction(".rs.readSrcrefLines") .addParam(srcref) .addParam(true) .call(&lines); - + if (error) LOG_ERROR(error); - + varFrame["lines"] = lines; } else { varFrame["real_sourceref"] = false; - + // if this frame is being debugged, we simulate the sourceref // using the output of the last debugged statement; if it isn't, // we construct it by deparsing calls in the context stack. @@ -803,7 +805,7 @@ json::Array callFramesAsJson( simulatedSrcref = simulatedSourceRefsOfContext( *context, RCntxt(), pLineDebugState); - + if (isValidSrcref(simulatedSrcref)) { int lastDebugLine = INTEGER(simulatedSrcref)[0] - 1; @@ -821,7 +823,7 @@ json::Array callFramesAsJson( } varFrame["function_line_number"] = 1; - + std::string callSummary; error = context->callSummary(&callSummary); if (error) @@ -831,7 +833,7 @@ json::Array callFramesAsJson( // If this is a Shiny function, provide its label varFrame["shiny_function_label"] = context->shinyFunctionLabel(); - + if (depth == contextDepth) { *pContext = *context; @@ -842,7 +844,7 @@ json::Array callFramesAsJson( prevContext = *context; } } - + return listFrames; } @@ -947,7 +949,7 @@ Error setEnvironment(boost::shared_ptr pContextDepth, return error; s_monitoredPythonModule = std::string(); - + if (s_environmentLanguage == kEnvironmentLanguageR) { error = setEnvironmentName(*pContextDepth, @@ -959,7 +961,7 @@ Error setEnvironment(boost::shared_ptr pContextDepth, if (environmentName != s_monitoredPythonModule) { s_monitoredPythonModule = environmentName; - + ClientEvent event(client_events::kEnvironmentRefresh); module_context::enqueClientEvent(event); } @@ -968,7 +970,7 @@ Error setEnvironment(boost::shared_ptr pContextDepth, { LOG_WARNING_MESSAGE("Unexpected language '" + s_environmentLanguage + "'"); } - + if (error) return error; @@ -1012,7 +1014,7 @@ bool functionIsOutOfSync(const r::context::RCntxt& context, .addParam(true) .addParam(true) .call(&sexpCode, &protect); - + if (error) { LOG_ERROR(error); @@ -1069,13 +1071,13 @@ json::Object pythonEnvironmentStateData(const std::string& environment) r::exec::RFunction(".rs.reticulate.environmentState") .addParam(environment) .call(&state, &protect); - + if (error) { LOG_ERROR(error); return json::Object(); } - + json::Object jsonState; error = r::json::jsonValueFromObject(state, &jsonState); if (error) @@ -1083,7 +1085,7 @@ json::Object pythonEnvironmentStateData(const std::string& environment) LOG_ERROR(error); return json::Object(); } - + jsonState["environment_monitoring"] = s_monitoring; return jsonState; } @@ -1097,21 +1099,26 @@ json::Object commonEnvironmentStateData( bool includeContents, LineDebugState* pLineDebugState) { + static json::Object varJson; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return varJson; + bool hasCodeInFrame = false; - json::Object varJson; bool useProvidedSource = false; std::string functionCode; bool inFunctionEnvironment = false; - + r::context::RCntxt context; r::context::RCntxt srcContext; json::Array callFramesJson = callFramesAsJson(depth, &context, &srcContext, pLineDebugState); - + // emit the current list of values in the environment, but only if not monitoring (as the intent // of the monitoring switch is to avoid implicit environment listing) varJson["environment_monitoring"] = s_monitoring; varJson["environment_list"] = includeContents ? environmentListAsJson() : json::Array(); - + varJson["context_depth"] = depth; varJson["call_frames"] = callFramesJson; varJson["function_name"] = ""; @@ -1134,15 +1141,15 @@ json::Object commonEnvironmentStateData( if (env != R_GlobalEnv && env == context.cloenv()) { varJson["environment_name"] = functionName + "()"; - + std::string envLocation; error = r::exec::RFunction(".rs.environmentName") .addParam(ENCLOS(context.cloenv())) .call(&envLocation); - + if (error) LOG_ERROR(error); - + varJson["function_environment_name"] = envLocation; varJson["environment_is_local"] = true; inFunctionEnvironment = true; @@ -1157,7 +1164,7 @@ json::Object commonEnvironmentStateData( core::json::readObject(currentFrameJson.getObject(), "file_name", filename, "lines", lines); - + // TODO: Need to check if srcref code is in sync with file? if (!lines.empty()) { @@ -1166,7 +1173,7 @@ json::Object commonEnvironmentStateData( functionCode = lines; } } - + if (!hasCodeInFrame) { // The eval and evalq functions receive special treatment since they @@ -1181,7 +1188,7 @@ json::Object commonEnvironmentStateData( functionCode != "NULL"; } } - + varJson["function_name"] = functionName; } } @@ -1208,7 +1215,7 @@ json::Object commonEnvironmentStateData( varJson["environment_name"] = environmentName; varJson["environment_is_local"] = local; } - + // If we have source references while we're stepping through, then // we can accurately provide the current context even for the top-most frame. if (isDebugStepping && hasCodeInFrame) @@ -1277,9 +1284,9 @@ Error getEnvironmentState(boost::shared_ptr pContextDepth, Error error = json::readParams(request.params, &language, &environment); if (error) LOG_ERROR(error); - + json::Object jsonState; - + if (language == kEnvironmentLanguageR) { jsonState = commonEnvironmentStateData( @@ -1296,7 +1303,7 @@ Error getEnvironmentState(boost::shared_ptr pContextDepth, { LOG_WARNING_MESSAGE("Unexpected language '" + language + "'"); } - + pResponse->setResult(jsonState); return Success(); } @@ -1322,11 +1329,11 @@ void onDetectChanges(module_context::ChangeSource /* source */) r::exec::RFunction(".rs.reticulate.detectChanges") .addParam(s_monitoredPythonModule) .call(); - + if (error) LOG_ERROR(error); } - + // Check active environment for changes s_pEnvironmentMonitor->checkForChanges(); } @@ -1338,13 +1345,13 @@ SEXP inferDebugSrcrefs( boost::shared_ptr pLineDebugState) { using namespace r::context; - + // check to see if we have real source references for the currently // executing context SEXP srcref = r::context::globalContext().srcref(); if (isValidSrcref(srcref)) return srcref; - + r::context::RCntxt context; r::context::RCntxt srcContext; json::Array callFramesJson = callFramesAsJson( @@ -1352,10 +1359,14 @@ SEXP inferDebugSrcrefs( &context, &srcContext, pLineDebugState.get()); - + srcref = simulatedSourceRefsOfContext(srcContext, RCntxt(), pLineDebugState.get()); if (pLineDebugState && isValidSrcref(srcref)) - pLineDebugState->lastDebugLine = INTEGER(srcref)[0] - 1; + { + int lastDebugLine = INTEGER(srcref)[0] - 1; + pLineDebugState->lastDebugLine = lastDebugLine; + } + return srcref; } @@ -1369,14 +1380,14 @@ void onConsolePrompt(boost::shared_ptr pContextDepth, // Prevent recursive calls to this function DROP_RECURSIVE_CALLS; - + int depth = 0; SEXP environmentTop = nullptr; r::context::RCntxt context; // End debug output capture every time a console prompt occurs *pCapturingDebugOutput = false; - + // Update session suspendable state s_isGlobalEnvironmentSerializable = isGlobalEnvironmentSerializable(); @@ -1396,7 +1407,7 @@ void onConsolePrompt(boost::shared_ptr pContextDepth, // allow the user to explore other code. context = r::context::getFunctionContext(BROWSER_FUNCTION, &depth, &environmentTop); } - + if (environmentTop != s_pEnvironmentMonitor->getMonitoredEnvironment() || depth != *pContextDepth || context != *pCurrentContext) @@ -1423,14 +1434,14 @@ void onConsolePrompt(boost::shared_ptr pContextDepth, *pCurrentContext = context; enqueContextDepthChangedEvent(true, depth, pLineDebugState.get()); } - + // if we're debugging and stayed in the same frame, update the line number else if (depth > 0 && !r::context::inDebugHiddenContext()) { SEXP srcref = inferDebugSrcrefs(depth, pLineDebugState); enqueBrowserLineChangedEvent(srcref); } - + } void onBeforeExecute() @@ -1471,22 +1482,22 @@ Error getEnvironmentNames(boost::shared_ptr pContextDepth, LOG_ERROR(error); return Success(); } - + if (language == kEnvironmentLanguagePython) { Error error; - + SEXP environments = R_NilValue; r::sexp::Protect protect; error = r::exec::RFunction(".rs.reticulate.listLoadedModules") .call(&environments, &protect); - + if (error) { LOG_ERROR(error); return Success(); } - + json::Value environmentsJson; error = r::json::jsonValueFromObject(environments, &environmentsJson); if (error) @@ -1494,7 +1505,7 @@ Error getEnvironmentNames(boost::shared_ptr pContextDepth, LOG_ERROR(error); return Success(); } - + pResponse->setResult(environmentsJson); return Success(); } @@ -1511,7 +1522,7 @@ Error getEnvironmentNames(boost::shared_ptr pContextDepth, { LOG_WARNING_MESSAGE("Unexpected language '" + language + "'"); } - + return Success(); } @@ -1571,7 +1582,7 @@ Error removeObjects(const json::JsonRpcRequest& request, .addParam(objectNames) .addParam(s_pEnvironmentMonitor->getMonitoredEnvironment()) .call(); - + if (error) return error; @@ -1591,7 +1602,7 @@ Error removeAllObjects(const json::JsonRpcRequest& request, .addParam(includeHidden) .addParam( s_pEnvironmentMonitor->getMonitoredEnvironment()) .call(); - + if (error) return error; @@ -1609,14 +1620,14 @@ Error getObjectContents(const json::JsonRpcRequest& request, Error error = json::readParam(request.params, 0, &objectName); if (error) return error; - + SEXP objContents; r::sexp::Protect protect; error = r::exec::RFunction(".rs.getObjectContents") .addParam(objectName) .addParam(s_pEnvironmentMonitor->getMonitoredEnvironment()) .call(&objContents, &protect); - + if (error) return error; @@ -1644,7 +1655,7 @@ Error requeryContext(boost::shared_ptr pContextDepth, pLineDebugState, boost::make_shared(false), pCurrentContext); - + return Success(); } @@ -1657,15 +1668,15 @@ Error environmentSetLanguage(const json::JsonRpcRequest& request, Error error = json::readParams(request.params, &language); if (error) LOG_ERROR(error); - + s_environmentLanguage = language; - + // reset Python module to 'main' after changing language if (language == kEnvironmentLanguagePython) { s_monitoredPythonModule = "__main__"; } - + return Success(); } @@ -1684,7 +1695,7 @@ void onConsoleOutput(boost::shared_ptr pLineDebugState, *pCapturingDebugOutput = false; return; } - + // When printing things which are not symbols / calls in // the debugger, R will prepend a '[1] ' as these objects // will be printed in the "regular" way. Strip that off @@ -1698,19 +1709,19 @@ void onConsoleOutput(boost::shared_ptr pLineDebugState, pLineDebugState->lastDebugText.append(output); } } - + else if (type == module_context::ConsoleOutputNormal) { boost::smatch match; boost::regex reDebugAtPosition("debug at ([^#]*)#([^:]+): "); - + // start capturing debug output when R outputs "debug: " if (output == "debug: ") { *pCapturingDebugOutput = true; pLineDebugState->lastDebugText = ""; } - + // emitted when browsing with srcref else if (boost::regex_match(output, match, reDebugAtPosition)) { @@ -1723,7 +1734,7 @@ void onConsoleOutput(boost::shared_ptr pLineDebugState, pLineDebugState->lastDebugLine = *lineNumber; } } - + // emitted by R when a 'browser()' statement is encountered else if (output.find("Called from: ") == 0) { @@ -1753,18 +1764,18 @@ json::Value environmentStateAsJson() { if (s_environmentLanguage == kEnvironmentLanguagePython) return pythonEnvironmentStateData(s_monitoredPythonModule); - + int contextDepth = 0; r::context::getFunctionContext(BROWSER_FUNCTION, &contextDepth); - + // If there's no browser on the stack, stay at the top level even if // there are functions on the stack--this is not a user debug session. if (!r::context::inBrowseContext()) contextDepth = 0; - + return commonEnvironmentStateData( false, - contextDepth, + contextDepth, s_monitoring, // include contents if actively monitoring nullptr); } @@ -1794,10 +1805,10 @@ Error initialize() boost::shared_ptr pContextDepth = boost::make_shared(0); - + boost::shared_ptr pCurrentContext = boost::make_shared(r::context::globalContext()); - + // get reference to INTEGER_OR_NULL if provided by this version of R { using core::system::Library; @@ -1810,7 +1821,7 @@ Error initialize() // for the purpose of reconstructing references when none are present. boost::shared_ptr pLineDebugState = boost::make_shared(); - + boost::shared_ptr pCapturingDebugOutput = boost::make_shared(false); @@ -1877,7 +1888,7 @@ Error initialize() return initBlock.execute(); } - + } // namespace environment } // namespace modules } // namespace session diff --git a/src/cpp/session/modules/quarto/SessionQuarto.cpp b/src/cpp/session/modules/quarto/SessionQuarto.cpp index 5a30da6fc87..530772940fb 100644 --- a/src/cpp/session/modules/quarto/SessionQuarto.cpp +++ b/src/cpp/session/modules/quarto/SessionQuarto.cpp @@ -60,9 +60,18 @@ const char * const kRStudioQuarto = "RSTUDIO_QUARTO"; #ifndef _WIN32 # define kQuartoCmd "quarto" # define kQuartoExe "quarto" +# define kPandocExe "pandoc" #else # define kQuartoCmd "quarto.cmd" # define kQuartoExe "quarto.exe" +# define kPandocExe "pandoc.exe" +#endif + + +#ifdef __aarch64__ +# define kArchDir "aarch64" +#else +# define kArchDir "x86_64" #endif namespace rstudio { @@ -77,13 +86,34 @@ const char * const kQuartoXt = "quarto-document"; FilePath s_userInstalledPath; FilePath s_quartoPath; std::string s_quartoVersion; +QuartoConfig s_quartoConfig; -/* -bool haveRequiredQuartoVersion(const std::string& version) +FilePath quartoPandocPath() { - return Version(s_quartoVersion) >= Version(version); + FilePath quartoPandoc; + + // find quarto pandoc -- its location has moved over time, + // so we check a variety of locations just in case + FilePath quartoBinPath(s_quartoConfig.bin_path); + quartoPandoc = quartoBinPath.completeChildPath("tools/" kArchDir "/" kPandocExe); + if (quartoPandoc.exists()) + return quartoPandoc; + + quartoPandoc = quartoBinPath.completeChildPath("tools/" kPandocExe); + if (quartoPandoc.exists()) + return quartoPandoc; + + quartoPandoc = quartoBinPath.completeChildPath(kPandocExe); + if (quartoPandoc.exists()) + return quartoPandoc; + + // all else fails, just try to find pandoc on the PATH + Error error = core::system::findProgramOnPath(kPandocExe, &quartoPandoc); + if (error) + LOG_ERROR(error); + + return quartoPandoc; } -*/ Version readQuartoVersion(const core::FilePath& quartoBinPath) { @@ -850,8 +880,6 @@ Error quartoCreateProject(const json::JsonRpcRequest& request, return Success(); } -static QuartoConfig s_quartoConfig; - void readQuartoConfig() { // detect installation @@ -879,6 +907,7 @@ void readQuartoConfig() { s_quartoConfig.bin_path = string_utils::systemToUtf8(paths[0]); s_quartoConfig.resources_path = string_utils::systemToUtf8(paths[1]); + s_quartoConfig.pandoc_path = quartoPandocPath().getAbsolutePath(); } else { @@ -886,6 +915,7 @@ void readQuartoConfig() s_quartoConfig = QuartoConfig(); return; } + } using namespace session::projects; diff --git a/src/cpp/session/modules/quarto/SessionQuartoXRefs.cpp b/src/cpp/session/modules/quarto/SessionQuartoXRefs.cpp index 397cb29d26f..c56ffaf1306 100644 --- a/src/cpp/session/modules/quarto/SessionQuartoXRefs.cpp +++ b/src/cpp/session/modules/quarto/SessionQuartoXRefs.cpp @@ -163,27 +163,6 @@ json::Array readXRefIndex(const FilePath& indexPath, const std::string& filename return xrefs; } -FilePath quartoPandocPath(const QuartoConfig& config) -{ -#ifndef WIN32 - std::string target = "pandoc"; -#else - std::string target = "pandoc.exe"; -#endif - - // find quarto pandoc (it could be directly in the bin_path or it could be in the "tools"dir) - FilePath quartoPandoc = FilePath(config.bin_path).completeChildPath(target); - if (!quartoPandoc.exists()) - { - FilePath quartoTools = FilePath(config.bin_path).completeChildPath("tools"); - if (quartoTools.exists()) - { - quartoPandoc = quartoTools.completeChildPath(target); - } - } - return quartoPandoc; -} - json::Array indexSourceFile(const std::string& contents, const std::string& filename) { QuartoConfig config = quartoConfig(); @@ -255,14 +234,13 @@ json::Array indexSourceFile(const std::string& contents, const std::string& file args.push_back(core::string_utils::utf8ToSystem(dataDirPath.getAbsolutePath())); core::system::ProcessResult result; - error = module_context::runPandoc( - quartoPandocPath(config).getAbsolutePath(), - args, - contents, - options, - &result - ); + config.pandoc_path, + args, + contents, + options, + &result); + if (!error) { if (result.exitStatus == EXIT_SUCCESS) diff --git a/src/cpp/session/modules/rmarkdown/NotebookChunkOptions.hpp b/src/cpp/session/modules/rmarkdown/NotebookChunkOptions.hpp index a264393ddc4..a58e089d8d3 100644 --- a/src/cpp/session/modules/rmarkdown/NotebookChunkOptions.hpp +++ b/src/cpp/session/modules/rmarkdown/NotebookChunkOptions.hpp @@ -32,19 +32,27 @@ class ChunkOptions ChunkOptions(const core::json::Object& defaultOptions, const core::json::Object& chunkOptions); - template T getOverlayOption(const std::string& key, - T defaultValue) const + template + T getOverlayOption(const std::string& key, T defaultValue) const { + using namespace core::json; + // check overlay first - core::Error error = core::json::readObject(chunkOptions_, key, - defaultValue); + core::Error error = readObject(chunkOptions_, key, defaultValue); // no overlay option, check base if (error) - core::json::readObject(defaultOptions_, key, defaultValue); + readObject(defaultOptions_, key, defaultValue); return defaultValue; } + + bool hasOverlayOption(const std::string& key) + { + return + chunkOptions_.hasMember(key) || + defaultOptions_.hasMember(key); + } // return overlay only const core::json::Object& chunkOptions() const; diff --git a/src/cpp/session/modules/rmarkdown/NotebookExec.cpp b/src/cpp/session/modules/rmarkdown/NotebookExec.cpp index c0248f455ee..047e3cad5cc 100644 --- a/src/cpp/session/modules/rmarkdown/NotebookExec.cpp +++ b/src/cpp/session/modules/rmarkdown/NotebookExec.cpp @@ -259,21 +259,43 @@ void ChunkExecContext::connect() if (error) LOG_ERROR(error); - // log warnings immediately (unless user's changed the default warning - // level) - r::sexp::Protect protect; - SEXP warnSEXP; - error = r::exec::RFunction("getOption", "warn").call(&warnSEXP, &protect); + // log warnings immediately + // (unless user's changed the default warning level) + int rWarningLevel = 1; + error = r::exec::RFunction("getOption", "warn").call(&rWarningLevel); if (!error) { - prevWarn_.set(warnSEXP); + // save the current warning level + rGlobalWarningLevel_.set(Rf_ScalarInteger(rWarningLevel)); // default warning setting is 1 (log immediately), but if the warning // option is set to FALSE, we want to set it to -1 (ignore warnings) - bool warning = options_.getOverlayOption("warning", true); - error = r::options::setOption("warn", warning ? 1 : -1); - if (error) - LOG_ERROR(error); + int chunkWarningLevel; + if (options_.hasOverlayOption("warning")) + { + bool warningsEnabled = options_.getOverlayOption("warning", true); + chunkWarningLevel = warningsEnabled ? 1 : -1; + } + + // ensure that warnings are shown by default + else if (rWarningLevel == 0) + { + chunkWarningLevel = 1; + } + + // otherwise, just preserve the current warning level + else + { + chunkWarningLevel = rWarningLevel; + } + + // update warning level for this chunk + if (rWarningLevel != chunkWarningLevel) + { + rChunkWarningLevel_.set(Rf_ScalarInteger(chunkWarningLevel)); + SEXP cellSEXP = r::options::getOptionCell("warn"); + SETCAR(cellSEXP, rChunkWarningLevel_.get()); + } } // broadcast that we're executing in a Notebook @@ -528,10 +550,14 @@ void ChunkExecContext::disconnect() // restore width value r::options::setOptionWidth(prevCharWidth_); - // restore preserved warning level, if any - if (!prevWarn_.isNil()) + // restore warn (if it wasn't changed in the chunk) + // note that we intentionally compare the pointers here; + // we only want to take action if the SEXP pointer returned + // via 'getOption()' has changed + SEXP warningSEXP = r::options::getOption("warn"); + if (warningSEXP == rChunkWarningLevel_.get()) { - error = r::options::setOption("warn", prevWarn_.get()); + error = r::options::setOption("warn", rGlobalWarningLevel_.get()); if (error) LOG_ERROR(error); } diff --git a/src/cpp/session/modules/rmarkdown/NotebookExec.hpp b/src/cpp/session/modules/rmarkdown/NotebookExec.hpp index 086165168b9..cad6b2f39ec 100644 --- a/src/cpp/session/modules/rmarkdown/NotebookExec.hpp +++ b/src/cpp/session/modules/rmarkdown/NotebookExec.hpp @@ -108,7 +108,13 @@ class ChunkExecContext : public NotebookCapture int prevCharWidth_; int lastOutputType_; ExecScope execScope_; - r::sexp::PreservedSEXP prevWarn_; + + // we save both the previous R warning level, + // as well as the chunk warning level, so that + // we can detect if users try to set options(warn = 2) + // within a chunk directly (affecting global state) + r::sexp::PreservedSEXP rGlobalWarningLevel_; + r::sexp::PreservedSEXP rChunkWarningLevel_; core::FilePath consoleChunkOutputFile_; bool hasOutput_; diff --git a/src/cpp/session/modules/rmarkdown/SessionRMarkdown.cpp b/src/cpp/session/modules/rmarkdown/SessionRMarkdown.cpp index 219f38c122d..b700579f2db 100644 --- a/src/cpp/session/modules/rmarkdown/SessionRMarkdown.cpp +++ b/src/cpp/session/modules/rmarkdown/SessionRMarkdown.cpp @@ -20,6 +20,8 @@ #include #include "SessionRmdNotebook.hpp" +#include "../SessionHTMLPreview.hpp" +#include "../build/SessionBuildErrors.hpp" #include #include @@ -37,7 +39,6 @@ #include #include #include -#include #include #include @@ -46,6 +47,7 @@ #include #include +#include #include #include #include @@ -57,7 +59,7 @@ #include "SessionBlogdown.hpp" #include "RMarkdownPresentation.hpp" -#include "../SessionHTMLPreview.hpp" +#include "../../SessionConsoleInput.hpp" #define kRmdOutput "rmd_output" #define kRmdOutputLocation "/" kRmdOutput "/" @@ -101,10 +103,10 @@ std::string utf8ToConsole(const std::string& string) LOG_ERROR(LAST_SYSTEM_ERROR()); return string; } - + std::ostringstream output; char buffer[16]; - + // force C locale (ensures that any non-ASCII characters // will fail to convert and hence must be unicode escaped) const char* locale = ::setlocale(LC_CTYPE, nullptr); @@ -113,7 +115,7 @@ std::string utf8ToConsole(const std::string& string) for (int i = 0; i < chars; i++) { int n = ::wctomb(buffer, wide[i]); - + // use Unicode escaping for characters that cannot be represented // as well as for single-byte upper ASCII if (n == -1 || (n == 1 && static_cast(buffer[0]) > 127)) @@ -125,11 +127,11 @@ std::string utf8ToConsole(const std::string& string) output.write(buffer, n); } } - + ::setlocale(LC_CTYPE, locale); - + return output.str(); - + } #else @@ -141,7 +143,7 @@ std::string utf8ToConsole(const std::string& string) #endif -enum +enum { RExecutionReady = 0, RExecutionBusy = 1 @@ -322,9 +324,9 @@ std::string assignOutputUrl(const std::string& outputFile) .callUtf8(&renderedPath); if (error) LOG_ERROR(error); - + s_renderOutputs[s_currentRenderOutput] = renderedPath; - + // compute relative path to target file and append it to the path std::string relativePath = outputPath.getRelativePath(websiteDir); path += relativePath; @@ -402,7 +404,7 @@ class RenderRmd : public async_r::AsyncRProcess sourceLine, sourceNavigation, asShiny)); - pRender->start(format, encoding, paramsFile, asTempfile, + pRender->start(format, encoding, paramsFile, asTempfile, existingOutputFile, workingDir, viewerType); return pRender; } @@ -549,7 +551,7 @@ class RenderRmd : public async_r::AsyncRProcess // (other render functions may not accept knit_root_dir) if (!workingDir.empty() && renderFunc == kStandardRenderFunc) { - renderOptions += ", knit_root_dir = '" + + renderOptions += ", knit_root_dir = '" + utf8ToConsole(workingDir) + "'"; } @@ -602,12 +604,12 @@ class RenderRmd : public async_r::AsyncRProcess string_utils::singleQuotedStrEscape(targetFile) % extraParams % renderOptions); - + // un-escape unicode escapes #ifdef _WIN32 cmd = boost::algorithm::replace_all_copy(cmd, "\\\\u{", "\\u{"); #endif - + // environment core::system::Options environment; std::string tempDir; @@ -618,12 +620,12 @@ class RenderRmd : public async_r::AsyncRProcess LOG_ERROR(error); // pass along the RSTUDIO_VERSION - environment.push_back(std::make_pair("RSTUDIO_VERSION", module_context::rstudioVersion(true))); + environment.push_back(std::make_pair("RSTUDIO_VERSION", parsableRStudioVersion())); environment.push_back(std::make_pair("RSTUDIO_LONG_VERSION", RSTUDIO_VERSION)); // inform that this runs in the Render pane environment.push_back(std::make_pair("RSTUDIO_CHILD_PROCESS_PANE", "render")); - + // set the not cran env var environment.push_back(std::make_pair("NOT_CRAN", "true")); @@ -632,16 +634,16 @@ class RenderRmd : public async_r::AsyncRProcess error = r::exec::RFunction(".rs.inferReticulatePython").call(&reticulatePython); if (error) LOG_ERROR(error); - + // pass along current PATH std::string currentPath = core::system::getenv("PATH"); core::system::setenv(&environment, "PATH", currentPath); - + if (!reticulatePython.empty()) { // we found a Python version; forward it environment.push_back({"RETICULATE_PYTHON_FALLBACK", reticulatePython}); - + // also update the PATH so this version of Python is visible core::system::addToPath( &environment, @@ -724,7 +726,7 @@ class RenderRmd : public async_r::AsyncRProcess "^" kAnsiEscapeRegex "(?:Listening on |Browse at )?(https?://[^\033]+)" kAnsiEscapeRegex "$"); - + boost::smatch matches; if (regex_utils::match(outputLine, matches, shinyListening)) { @@ -822,7 +824,7 @@ class RenderRmd : public async_r::AsyncRProcess std::string outputFile = createAliasedPath(outputFile_); resultJson["output_file"] = outputFile; - + std::vector knitrErrors; if (renderErrorMarker_) { @@ -899,7 +901,7 @@ class RenderRmd : public async_r::AsyncRProcess const std::string& output) { using namespace module_context; - + if (type == kCompileOutputError && sourceNavigation_) { if (renderErrorMarker_) @@ -920,7 +922,7 @@ class RenderRmd : public async_r::AsyncRProcess // parse to to gather information for a source marker const char* renderErrorPattern = "(?:.*?)Quitting from lines (\\d+)-(\\d+) \\(([^)]+)\\)(.*)"; - + boost::regex reRenderError(renderErrorPattern); boost::smatch matches; if (regex_utils::match(output, matches, reRenderError)) @@ -934,7 +936,7 @@ class RenderRmd : public async_r::AsyncRProcess } } } - + // always enque quarto as normal output (it does it's own colorizing of error output) if (isQuarto_) type = module_context::kCompileOutputNormal; @@ -1047,7 +1049,7 @@ void initEnvironment() std::string rstudioPandoc = core::system::getenv(kRStudioPandoc); if (rstudioPandoc.empty()) rstudioPandoc = session::options().pandocPath().getAbsolutePath(); - + r::exec::RFunction sysSetenv("Sys.setenv"); sysSetenv.addParam(kRStudioPandoc, rstudioPandoc); @@ -1173,7 +1175,7 @@ Error renderRmd(const json::JsonRpcRequest& request, { // if this is a notebook, it's pre-rendered FilePath inputFile = module_context::resolveAliasedPath(file); - FilePath outputFile = inputFile.getParent().completePath(inputFile.getStem() + + FilePath outputFile = inputFile.getParent().completePath(inputFile.getStem() + kNotebookExt); // extract the output format @@ -1208,7 +1210,7 @@ Error renderRmd(const json::JsonRpcRequest& request, { // not a notebook, do render work doRenderRmd(file, line, format, encoding, paramsFile, - true, asTempfile, type == kRenderTypeShiny, existingOutputFile, + true, asTempfile, type == kRenderTypeShiny, existingOutputFile, workingDir, viewerType, pResponse); } @@ -1601,7 +1603,7 @@ Error rmdSaveBase64Images(const json::JsonRpcRequest& request, error = imagesPath.ensureDirectory(); if (error) return error; - + // build list of target image paths std::vector createdImages; @@ -1617,7 +1619,7 @@ Error rmdSaveBase64Images(const json::JsonRpcRequest& request, "(;base64)?" // optional base64 declaration ",(.*)$" // comma separating prefix from data ); - + boost::smatch match; if (boost::regex_match(image, match, reDataImage)) { @@ -1629,23 +1631,23 @@ Error rmdSaveBase64Images(const json::JsonRpcRequest& request, if (error) LOG_ERROR(error); } - + // figure out an appropriate extension std::string mimeType = match[1]; std::string fileExtension = mimeType; if (mimeType == "svg+xml") fileExtension = "svg"; - + // create the file path std::string crcHash = core::hash::crc32Hash(rawData); std::string fileName = fmt::format("clipboard-{}.{}", crcHash, fileExtension); FilePath imagePath = imagesPath.completeChildPath(fileName); - + // write to file Error error = core::writeStringToFile(imagePath, rawData); if (error) LOG_ERROR(error); - + // return path to generated image std::string resolvedPath = fmt::format("images/{}", fileName); createdImages.push_back(resolvedPath); @@ -1653,7 +1655,7 @@ Error rmdSaveBase64Images(const json::JsonRpcRequest& request, else { static const boost::regex rePrefix("^(data:[^,]+,)"); - + boost::smatch match; if (boost::regex_match(image, match, rePrefix)) { @@ -1666,7 +1668,7 @@ Error rmdSaveBase64Images(const json::JsonRpcRequest& request, } } } - + // send back new image paths to client json::Array createdImagesJson = core::json::toJsonArray(createdImages); pResponse->setResult(createdImagesJson); @@ -1702,7 +1704,7 @@ SEXP rs_getWebsiteOutputDir() void onShutdown(bool terminatedNormally) { - Error error = core::writeStringVectorToFile(outputCachePath(), + Error error = core::writeStringVectorToFile(outputCachePath(), s_renderOutputs); if (error) LOG_ERROR(error); @@ -1716,7 +1718,7 @@ void onShutdown(bool terminatedNormally) } #endif } - + void onSuspend(const r::session::RSuspendOptions&, core::Settings*) { onShutdown(true); @@ -1750,18 +1752,41 @@ Error evaluateRmdParams(const std::string& docId) bool knitParamsAvailable() { - return module_context::isPackageVersionInstalled("rmarkdown", "0.7.3") && - module_context::isPackageVersionInstalled("knitr", "1.10.18"); + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = module_context::isPackageVersionInstalled("rmarkdown", "0.7.3") && + module_context::isPackageVersionInstalled("knitr", "1.10.18"); + + return res; } bool knitWorkingDirAvailable() { - return module_context::isPackageVersionInstalled("rmarkdown", "1.1.9017"); + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = module_context::isPackageVersionInstalled("rmarkdown", "1.1.9017"); + + return res; } bool pptAvailable() { - return module_context::isPackageVersionInstalled("rmarkdown", "1.8.10"); + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = module_context::isPackageVersionInstalled("rmarkdown", "1.8.10"); + return res; } bool rmarkdownPackageAvailable() @@ -1891,7 +1916,12 @@ bool isBookdownProject() if (!projects::projectContext().hasProject()) return false; - bool isBookdown = false; + static bool isBookdown = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return isBookdown; + std::string encoding = projects::projectContext().defaultEncoding(); Error error = r::exec::RFunction(".rs.isBookdownDir", projectBuildDir(), encoding).call(&isBookdown); @@ -1904,8 +1934,15 @@ bool isDistillProject() { if (!isWebsiteProject()) return false; - - return session::modules::rmarkdown::isSiteProject("distill_website"); + + static bool res = false; + + // return the last known value if the session is busy to avoid touching the R runtime + if (console_input::executing()) + return res; + + res = session::modules::rmarkdown::isSiteProject("distill_website"); + return res; } diff --git a/src/cpp/session/modules/rmarkdown/SessionRMarkdown.hpp b/src/cpp/session/modules/rmarkdown/SessionRMarkdown.hpp index c861787b2a6..6c298cf0d9d 100644 --- a/src/cpp/session/modules/rmarkdown/SessionRMarkdown.hpp +++ b/src/cpp/session/modules/rmarkdown/SessionRMarkdown.hpp @@ -45,6 +45,8 @@ bool pptAvailable(); core::Error evaluateRmdParams(const std::string& docId); +std::string parsableRStudioVersion(); + core::Error initialize(); } // namespace rmarkdown diff --git a/src/cpp/session/modules/tex/SessionCompilePdf.cpp b/src/cpp/session/modules/tex/SessionCompilePdf.cpp index ea1390259ad..f9427f6e958 100644 --- a/src/cpp/session/modules/tex/SessionCompilePdf.cpp +++ b/src/cpp/session/modules/tex/SessionCompilePdf.cpp @@ -711,7 +711,7 @@ class AsyncPdfCompiler : boost::noncopyable, "cat('Compiling document with tinytex ... ');" "invisible(tinytex::latexmk(" + arguments + "))"; - codePath_ = module_context::tempFile("tinytex-runner-", ".R"); + codePath_ = module_context::tempFile("tinytex-runner-", "R"); error = writeStringToFile(codePath_, code); if (error) { diff --git a/src/cpp/session/prefs/UserPrefValues.cpp b/src/cpp/session/prefs/UserPrefValues.cpp index 43dd865bcf9..a9636b17ced 100644 --- a/src/cpp/session/prefs/UserPrefValues.cpp +++ b/src/cpp/session/prefs/UserPrefValues.cpp @@ -3338,6 +3338,58 @@ core::Error UserPrefValues::setRunBackgroundJobDefaultWorkingDir(std::string val return writePref("run_background_job_default_working_dir", val); } +/** + * The formatter to use when reformatting code. + */ +std::string UserPrefValues::codeFormatter() +{ + return readPref("code_formatter"); +} + +core::Error UserPrefValues::setCodeFormatter(std::string val) +{ + return writePref("code_formatter", val); +} + +/** + * When set, strict transformers will be used when formatting code. See the `styler` package documentation for more details. + */ +bool UserPrefValues::codeFormatterStylerStrict() +{ + return readPref("code_formatter_styler_strict"); +} + +core::Error UserPrefValues::setCodeFormatterStylerStrict(bool val) +{ + return writePref("code_formatter_styler_strict", val); +} + +/** + * The external command to be used when reformatting code. + */ +std::string UserPrefValues::codeFormatterExternalCommand() +{ + return readPref("code_formatter_external_command"); +} + +core::Error UserPrefValues::setCodeFormatterExternalCommand(std::string val) +{ + return writePref("code_formatter_external_command", val); +} + +/** + * When set, the selected formatter will be used to reformat documents on save. + */ +bool UserPrefValues::reformatOnSave() +{ + return readPref("reformat_on_save"); +} + +core::Error UserPrefValues::setReformatOnSave(bool val) +{ + return writePref("reformat_on_save", val); +} + std::vector UserPrefValues::allKeys() { return std::vector({ @@ -3596,6 +3648,10 @@ std::vector UserPrefValues::allKeys() kCopilotIndexingEnabled, kProjectName, kRunBackgroundJobDefaultWorkingDir, + kCodeFormatter, + kCodeFormatterStylerStrict, + kCodeFormatterExternalCommand, + kReformatOnSave, }); } diff --git a/src/cpp/session/resources/dependencies/r-packages.json b/src/cpp/session/resources/dependencies/r-packages.json index 4091a15d411..68fa2bd3823 100644 --- a/src/cpp/session/resources/dependencies/r-packages.json +++ b/src/cpp/session/resources/dependencies/r-packages.json @@ -290,6 +290,11 @@ "location": "cran", "source": false }, + "styler": { + "version": "1.10.3", + "location": "cran", + "source": false + }, "testthat": { "version": "2.0.0", "location": "cran", @@ -443,6 +448,13 @@ "rstan" ] }, + "styler": { + "description": "styler", + "packages": [ + "styler" + ] + }, + "tinytex": { "description": "TinyTeX", "packages": [ diff --git a/src/cpp/session/resources/grid/dtviewer.js b/src/cpp/session/resources/grid/dtviewer.js index 6b7b194b85a..bf27a754058 100644 --- a/src/cpp/session/resources/grid/dtviewer.js +++ b/src/cpp/session/resources/grid/dtviewer.js @@ -294,20 +294,28 @@ return 'NA'; } + var didHighlight = false; + + // handle row names specially + if (rowNumbers && meta.col === 0) { + data = JSON.parse(data).toString(); + } + if (clazz === "dataCell") { // a little ugly: R deparses data cell values as list(col = val, col = val...). when rendering // a data cell which appears to have this format, count the assignment tokens to get a variable // count and show that as a summary. if (data.substring(0, 5) === "list(" && data.indexOf("=") > 0) { var varCount = data.split("=").length - 1; - data = "" + varCount + " variable"; - if (varCount > 1) data += "s"; + var varLabel = varCount > 1 ? "variables" : "variable"; + data = `${varCount} ${varLabel}`; } } else if (cachedSearch.length > 0) { // if row matches because of a global search, highlight that var idx = data.toLowerCase().indexOf(cachedSearch.toLowerCase()); if (idx >= 0) { - return highlightSearchMatch(data, cachedSearch, idx); + data = highlightSearchMatch(data, cachedSearch, idx); + didHighlight = true; } } @@ -318,30 +326,37 @@ colSearch = decodeURIComponent(parseSearchString(colSearch)); var colIdx = data.toLowerCase().indexOf(colSearch.toLowerCase()); if (colIdx >= 0) { - return highlightSearchMatch(data, colSearch, colIdx); + data = highlightSearchMatch(data, colSearch, colIdx); + didHighlight = true; } } } - var escaped = escapeHtml(data); + var escaped = didHighlight ? data : escapeHtml(data); // special additional rendering for cells which themselves contain data frames or lists: // these include an icon that can be clicked to view contents if (clazz === "dataCell" || clazz === "listCell") { - escaped = - "" + - escaped + - " " + - '' + - ''; + + // rather than generate HTML by hand (and deal with escaping issues), + // create actual elements for each cell, and let the browser handle + // escaping of fields where required + var cbName = clazz === "dataCell" ? "dataViewerCallback" : "listViewerCallback"; + var cbRow = row[0]; + var cbCol = meta.col + columnOffset; + var href = `javascript:window.${cbName}(${cbRow}, ${cbCol})`; + + var linkEl = document.createElement("a"); + linkEl.setAttribute("class", "viewerLink"); + linkEl.setAttribute("href", href); + + var imgEl = document.createElement("img"); + imgEl.setAttribute("class", "viewerImage"); + imgEl.setAttribute("src", clazz === "dataCell" ? "data-viewer.png" : "object-viewer.png"); + linkEl.appendChild(imgEl); + + escaped = "" + escaped + " " + linkEl.outerHTML; + } return escaped; diff --git a/src/cpp/session/resources/schema/user-prefs-schema.json b/src/cpp/session/resources/schema/user-prefs-schema.json index 078bcb640d5..76cd7672127 100644 --- a/src/cpp/session/resources/schema/user-prefs-schema.json +++ b/src/cpp/session/resources/schema/user-prefs-schema.json @@ -1733,6 +1733,31 @@ "default": "project", "title": "Default working directory for background jobs", "description": "Default working directory in background job dialog." + }, + "code_formatter": { + "type": "string", + "enum": ["none", "styler", "external"], + "enumReadable": ["(None)", "styler", "External"], + "default": "none", + "title": "Code formatter", + "description": "The formatter to use when reformatting code." + }, + "code_formatter_styler_strict": { + "type": "boolean", + "default": true, + "title": "Use strict transformers when formatting code", + "description": "When set, strict transformers will be used when formatting code. See the `styler` package documentation for more details." + }, + "code_formatter_external_command": { + "type": "string", + "default": "", + "description": "The external command to be used when reformatting code." + }, + "reformat_on_save": { + "type": "boolean", + "default": false, + "title": "Reformat documents on save", + "description": "When set, the selected formatter will be used to reformat documents on save." } } } diff --git a/src/cpp/session/resources/schema/user-state-schema.json b/src/cpp/session/resources/schema/user-state-schema.json index ef95605f40e..988e4aa3d2a 100644 --- a/src/cpp/session/resources/schema/user-state-schema.json +++ b/src/cpp/session/resources/schema/user-state-schema.json @@ -212,6 +212,9 @@ }, "copyAsMetafile": { "type": "boolean" + }, + "useDevicePixelRatio": { + "type": "boolean" } }, "default": { @@ -220,7 +223,8 @@ "keepRatio": false, "format": "PNG", "viewAfterSave": false, - "copyAsMetafile": false + "copyAsMetafile": false, + "useDevicePixelRatio": true }, "description": "The most recently used plot export options." }, diff --git a/src/cpp/tests/automation/testthat/test-automation-build-pane.R b/src/cpp/tests/automation/testthat/test-automation-build-pane.R index e1fda285855..77d22020a43 100644 --- a/src/cpp/tests/automation/testthat/test-automation-build-pane.R +++ b/src/cpp/tests/automation/testthat/test-automation-build-pane.R @@ -2,7 +2,8 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) + test_that("we can test a file in the build pane", { diff --git a/src/cpp/tests/automation/testthat/test-automation-completions.R b/src/cpp/tests/automation/testthat/test-automation-completions.R index d8f2f84f257..8494a80224c 100644 --- a/src/cpp/tests/automation/testthat/test-automation-completions.R +++ b/src/cpp/tests/automation/testthat/test-automation-completions.R @@ -2,12 +2,12 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) # https://github.com/rstudio/rstudio/issues/14784 test_that("autocompletion doesn't trigger active bindings", { - code <- .rs.heredoc(r'{ + code <- .rs.heredoc(' Test <- R6::R6Class("Test", active = list(active_test = function(value) print("active")), private = list(private_test = function(value) print("private")), @@ -16,7 +16,7 @@ test_that("autocompletion doesn't trigger active bindings", { n <- Test$new() nms <- .rs.getNames(n) - }') + ') remote$consoleExecute(code) output <- remote$consoleOutput() @@ -27,3 +27,98 @@ test_that("autocompletion doesn't trigger active bindings", { expect_false(tail(output, 1) == "[1] \"active\"") }) + +test_that("autocompletion in console produces the expected completion list for new variables", { + + remote$consoleExecute(' + foobar <- 42 + foobaz <- 42 + ') + + completions <- remote$completionsRequest("foo") + expect_equal(completions, c("foobar", "foobaz")) + +}) + +# https://github.com/rstudio/rstudio/issues/13196 +test_that("autocompletion in console produces the expected completion list in an existing base function", { + + completions <- remote$completionsRequest("cat(") + expect_equal(completions, c("... =", "file =", "sep =", "fill =", "labels =", "append =")) + +}) + +# https://github.com/rstudio/rstudio/issues/13196 +test_that("autocompletion in console produces the expected completion list in an existing non-base function", { + + completions <- remote$completionsRequest("stats::rnorm(") + expect_equal(completions, c("n =", "mean =", "sd =")) + +}) + +# https://github.com/rstudio/rstudio/issues/13196 +test_that("autocompletion in console produces the expected completion list when using a new function", { + + # Define a function accepting some parameters. + remote$keyboardExecute("a <- function(x, y, z) { print(x + y) }", "") + + # Request completions for that function. + completions <- remote$completionsRequest("a(") + expect_equal(completions, c("x =", "y =", "z =")) + +}) + +# https://github.com/rstudio/rstudio/issues/13291 +test_that("list names are provided as completions following '$'", { + + code <- .rs.heredoc(' + test_df <- data.frame( + col1 = rep(1, 3), + col2 = rep(2, 3), + col3 = rep(3, 3) + ) + + test_ls <- list( + a = test_df, + b = test_df + ) + ') + + remote$consoleExecute(code) + + completions <- remote$completionsRequest("test_ls$") + expect_equal(completions, c("a", "b")) + +}) + +# https://github.com/rstudio/rstudio/issues/12678 +test_that("autocompletion in console shows local variables first.", { + + code <- .rs.heredoc(' + library(dplyr) + left_table <- tibble(x = 1) + ') + + remote$consoleExecute(code) + + parts <- remote$completionsRequest("lef") + expect_identical(parts, c("left_table", "left_join")) + +}) + +# https://github.com/rstudio/rstudio/issues/13611 +test_that("autocompletions within piped expressions work at start of document", { + + code <- .rs.heredoc(' + + mtcars |> mutate(x = mean()) + ') + + remote$documentExecute(".R", code, function(editor) + { + editor$gotoLine(2L, 26L) + completions <- remote$completionsRequest("") + expect_equal(completions[1:2], c("x =", "... =")) + }) + +}) diff --git a/src/cpp/tests/automation/testthat/test-automation-data-viewer.R b/src/cpp/tests/automation/testthat/test-automation-data-viewer.R index de491a867e4..53542d4094c 100644 --- a/src/cpp/tests/automation/testthat/test-automation-data-viewer.R +++ b/src/cpp/tests/automation/testthat/test-automation-data-viewer.R @@ -2,7 +2,8 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) + # https://github.com/rstudio/rstudio/pull/14657 test_that("we can use the data viewer with temporary R expressions", { @@ -11,3 +12,37 @@ test_that("we can use the data viewer with temporary R expressions", { expect_true(grepl("gridviewer.html", viewerFrame$src)) remote$commandExecute("closeSourceDoc") }) + +test_that("viewer filters function as expected", { + + # Start viewing a data.frame with a list column. + remote$consoleExecuteExpr({ + data <- data.frame(x = letters) + data$y <- lapply(letters, as.list) + row.names(data) <- LETTERS + View(data) + }) + + # Filter to entries with a 'K'. + remote$domClickElement("#data_editing_toolbar .search") + remote$keyboardExecute("K", "") + + # Try to find the viewer link in the table. + # Confirm it has the expected href. + viewerFrame <- remote$jsObjectViaSelector("#rstudio_data_viewer_frame") + linkEl <- viewerFrame$contentWindow$document$querySelector(".viewerLink") + expect_equal(linkEl$href, "javascript:window.listViewerCallback(\"K\", 2)") + + # Try to click it. + linkEl$focus() + linkEl$click() + + # Confirm that a new explorer tab was opened. + currentTabEl <- remote$jsObjectViaSelector(".rstudio_source_panel .gwt-TabLayoutPanelTab-selected") + tabTitle <- .rs.trimWhitespace(currentTabEl$innerText) + expect_equal(tabTitle, "data[\"K\", 2]") + + # Close any open documents. + remote$keyboardExecute("", "", "") + +}) diff --git a/src/cpp/tests/automation/testthat/test-automation-debugger.R b/src/cpp/tests/automation/testthat/test-automation-debugger.R new file mode 100644 index 00000000000..7dca11a2e40 --- /dev/null +++ b/src/cpp/tests/automation/testthat/test-automation-debugger.R @@ -0,0 +1,59 @@ + +library(testthat) + +self <- remote <- .rs.automation.newRemote() +withr::defer(.rs.automation.deleteRemote()) + +# https://github.com/rstudio/rstudio/issues/15072 +test_that("the debug position is correct in braced expressions", { + + contents <- .rs.heredoc(' + f <- function() { + 1 + 1 + { + 2 + 2 + } + } + ') + + remote$documentOpen(".R", contents) + on.exit(remote$documentClose(), add = TRUE) + + # Click to set a breakpoint. + gutterLayer <- remote$jsObjectViaSelector(".ace_gutter-layer") + gutterCell <- gutterLayer$children[[3]] + remote$domClickElement(objectId = gutterCell, horizontalOffset = -6L) + + # Clear the current selection if we have one. + editor <- remote$editorGetInstance() + editor$clearSelection() + + # Source the file. + remote$commandExecute("sourceActiveDocument") + + # Execute the function. + remote$consoleExecute("f()") + + # Check that debug highlighting was set on the fourth row. + gutterCell <- remote$jsObjectViaSelector(".ace_executing-line") + gutterParent <- gutterCell$parentElement + gutterChild <- gutterParent$children[[3]] + expect_equal(gutterCell$innerText, gutterChild$innerText) + + # Get the screen position of the debug rectangle. + debugLine <- remote$jsObjectViaSelector(".ace_active_debug_line") + debugRect <- debugLine$getBoundingClientRect() + + # Figure out what row that maps to in the editor. + screenCoords <- editor$session$renderer$pixelToScreenCoordinates( + debugRect$x + debugRect$width / 2, + debugRect$y + debugRect$height / 2 + ) + expect_equal(screenCoords$row, 3) + + # Exit the debugger. + remote$keyboardExecute("", "c", "") + remote$consoleExecuteExpr(rm(list = "f")) + remote$keyboardExecute("") + +}) diff --git a/src/cpp/tests/automation/testthat/test-automation-reformat.R b/src/cpp/tests/automation/testthat/test-automation-reformat.R new file mode 100644 index 00000000000..ac23bc3243a --- /dev/null +++ b/src/cpp/tests/automation/testthat/test-automation-reformat.R @@ -0,0 +1,24 @@ + +library(testthat) + +self <- remote <- .rs.automation.newRemote() +withr::defer(.rs.automation.deleteRemote()) + + +test_that("Documents can be reformatted on save", { + + remote$consoleExecute(".rs.writeUserPref(\"reformat_on_save\", TRUE)") + remote$consoleExecute(".rs.writeUserPref(\"code_formatter\", \"styler\")") + + documentContents <- .rs.heredoc("2+2") + + remote$documentOpen(".R", documentContents) + editor <- remote$editorGetInstance() + editor$insert("1+1; ") + remote$keyboardExecute("") + Sys.sleep(1) + contents <- editor$session$doc$getValue() + expect_equal(contents, "1 + 1\n2 + 2\n") + remote$documentClose() + +}) diff --git a/src/cpp/tests/automation/testthat/test-automation-restart.R b/src/cpp/tests/automation/testthat/test-automation-restart.R index 6504e69a614..704b66726ea 100644 --- a/src/cpp/tests/automation/testthat/test-automation-restart.R +++ b/src/cpp/tests/automation/testthat/test-automation-restart.R @@ -2,7 +2,8 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) + # https://github.com/rstudio/rstudio/issues/14636 test_that("variables can be referenced after restart", { diff --git a/src/cpp/tests/automation/testthat/test-automation-rmarkdown.R b/src/cpp/tests/automation/testthat/test-automation-rmarkdown.R new file mode 100644 index 00000000000..d6e0dae5138 --- /dev/null +++ b/src/cpp/tests/automation/testthat/test-automation-rmarkdown.R @@ -0,0 +1,38 @@ + +library(testthat) + +self <- remote <- .rs.automation.newRemote() +withr::defer(.rs.automation.deleteRemote()) + +test_that("the warn option is preserved when running chunks", { + + contents <- .rs.heredoc(' + --- + title: Chunk Warnings + --- + + ```{r warning=TRUE} + # check current option + getOption("warn") + # setting a global option + options(warn = 2) + ``` + ') + + remote$consoleExecuteExpr({ options(warn = 0) }) + remote$consoleExecuteExpr({ getOption("warn") }) + output <- remote$consoleOutput() + expect_equal(tail(output, n = 1L), "[1] 0") + + id <- remote$documentOpen(".Rmd", contents) + editor <- remote$editorGetInstance() + editor$gotoLine(6) + remote$keyboardExecute("") + remote$consoleExecuteExpr({ getOption("warn") }) + output <- remote$consoleOutput() + expect_equal(tail(output, n = 1L), "[1] 2") + + remote$documentClose() + remote$keyboardExecute("") + +}) diff --git a/src/cpp/tests/automation/testthat/test-automation-sweave.R b/src/cpp/tests/automation/testthat/test-automation-sweave.R index 4258d15d232..d62dd3adc58 100644 --- a/src/cpp/tests/automation/testthat/test-automation-sweave.R +++ b/src/cpp/tests/automation/testthat/test-automation-sweave.R @@ -2,7 +2,7 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) test_that("Braces are inserted and highlighted correctly in Sweave documents", { @@ -18,6 +18,7 @@ test_that("Braces are inserted and highlighted correctly in Sweave documents", { remote$documentExecute(".Rnw", documentContents, function(editor) { editor$gotoLine(4L, 0L) remote$client$Input.insertText(text = "{ 1 + 1 }") + Sys.sleep(1) # wait for Ace to tokenize tokens <- as.vector(editor$session$getTokens(3L)) values <- vapply(tokens, `[[`, "value", FUN.VALUE = character(1)) expected <- c("{", " ", "1", " ", "+", " ", "1", " ", "}") diff --git a/src/cpp/tests/automation/testthat/test-automation-syntax-highlighting.R b/src/cpp/tests/automation/testthat/test-automation-syntax-highlighting.R index 6bd953e3b20..eb9529d54bb 100644 --- a/src/cpp/tests/automation/testthat/test-automation-syntax-highlighting.R +++ b/src/cpp/tests/automation/testthat/test-automation-syntax-highlighting.R @@ -2,7 +2,8 @@ library(testthat) self <- remote <- .rs.automation.newRemote() -on.exit(.rs.automation.deleteRemote(), add = TRUE) +withr::defer(.rs.automation.deleteRemote()) + test_that("Quarto Documents are highlighted as expected", { diff --git a/src/gwt/acesupport/acemode/r_code_model.js b/src/gwt/acesupport/acemode/r_code_model.js index 6de7e546119..d661ef35437 100644 --- a/src/gwt/acesupport/acemode/r_code_model.js +++ b/src/gwt/acesupport/acemode/r_code_model.js @@ -501,17 +501,13 @@ var RCodeModel = function(session, tokenizer, addDplyrArguments(clone.cloneCursor(), data, tokenCursor, value); // Move off of identifier, on to new infix operator. - // Note that we may already be at the start of the document, - // so check for that. + // If this fails (e.g. we're already at the start of the document) + // then just return the associated data object. if (!clone.moveToPreviousToken()) { - if (clone.$row === 0 && clone.$offset === 0) - { - tokenCursor.$row = 0; - tokenCursor.$offset = 0; - return data; - } - return false; + tokenCursor.$row = clone.$row; + tokenCursor.$offset = clone.$offset; + return data; } // Move over '::' qualifiers diff --git a/src/gwt/lib/gwt/gwt-rstudio/about.html b/src/gwt/lib/gwt/gwt-rstudio/about.html index d48d59957c6..1f306f900b9 100644 --- a/src/gwt/lib/gwt/gwt-rstudio/about.html +++ b/src/gwt/lib/gwt/gwt-rstudio/about.html @@ -2,7 +2,7 @@ - Google Web Toolkit 2.10.0 + Google Web Toolkit 2.10.1 \nsnippet sub\n ${1}\nsnippet summary\n \n ${1}\n \nsnippet sup\n ${1}\nsnippet table\n \n ${2}\n
\nsnippet table.\n \n ${3}\n
\nsnippet table#\n \n ${3}\n
\nsnippet tbody\n \n ${1}\n \nsnippet td\n ${1}\nsnippet td.\n ${2}\nsnippet td#\n ${2}\nsnippet td+\n ${1}\n td+${2}\nsnippet textarea\n ${6}\nsnippet tfoot\n \n ${1}\n \nsnippet th\n ${1}\nsnippet th.\n ${2}\nsnippet th#\n ${2}\nsnippet th+\n ${1}\n th+${2}\nsnippet thead\n \n ${1}\n \nsnippet time\n