import chromeLocation from 'chrome-location' import * as Path from 'path' import * as fs from 'fs/promises' import * as os from 'os' import {spawn} from 'child_process' import {pid2title, URL, makeWebDriver, Cookies} from '../helpers' import {until, By, WebDriver} from 'selenium-webdriver' const delayAsync = (ms: number) => new Promise((res) => setTimeout(res, ms)) const isRunning = (pid: number): boolean => { try { // @see https://nodejs.org/api/process.html#process_process_kill_pid_signal return process.kill(pid, 0) } catch (error) { return error.code === 'EPERM' } } interface ITempDirectory { path: string remove: () => Promise } const createTmpDir = async (prefix: string): Promise => { const path = await fs.mkdtemp(Path.join(os.tmpdir(), prefix)) const remove = () => fs.rm(path, {recursive: true, force: true, maxRetries: 3}) return {path, remove} } // "Chrome didn't shut down correctly" dialog is caused by their // crash handler detecting the process was killed. This might be confusing // to the end user, so we remove it by editing profile preferences const chromePreventCrashDialog = async (profilePath: string): Promise => { const preferencesPath = Path.join(profilePath, 'Default', 'Preferences') const jsonBuf = await fs.readFile(preferencesPath) const json = JSON.parse(jsonBuf.toString('utf-8')) json.profile.exit_type = 'Normal' await fs.writeFile(preferencesPath, JSON.stringify(json)) } const hasNotLoggedIn = async (pid: number): Promise => { // The final destination title after a successful login // includes '- YouTube Studio' regardless of language return pid2title(pid) .then((title) => !title.includes('个人中心')) .catch((e) => { console.error(e) return true }) } const runUncontrolledChrome = async (userDataDir: string): Promise => { // Spawn a chrome WITHOUT automation features like --remote-debugging-port or --enable automation because they disable google login in most cases // This uncontrolled chrome instance saves cookies to the tempDir. Login status is tracked on windows by process title. const chromeProcess = spawn(chromeLocation, [ URL.BAIDU_MY, '--no-first-run', '--no-default-browser-check', '--disable-translate', '--disable-default-apps', '--disable-popup-blocking', '--disable-zero-browsers-open-for-tests', `--user-data-dir=${userDataDir}`, // possibly confusing "save password" prompt is not possible to // hide because it's only possible by "--enable-automation" ]) const pid = chromeProcess.pid do { await delayAsync(1000) } while (isRunning(pid) && (await hasNotLoggedIn(pid))) await delayAsync(1000*30);//should not been killed immediately chromeProcess.kill() await delayAsync(1000) await chromePreventCrashDialog(userDataDir) } const makeLoggedInChromeProfile = async (): Promise => { const modulePrefix = 'node-apiless-youtube-upload-' const tempDir = await createTmpDir(modulePrefix) // Adding a removal exit hook for tempDir is a bad idea, because it cant be // done synchronously for EBUSY reasons (and no async hooks I tried did not // work). Therefore we do a cleanup of previous runs rather than trying to // clean up the current one on exit const {base: tmpBase, dir} = Path.parse(tempDir.path) for (const file of await fs.readdir(dir)) { if (file === tmpBase || !file.startsWith(modulePrefix)) continue const prevProfilePath = Path.join(dir, file) console.log('Removing temp profile from previous run', prevProfilePath) await fs.rm(prevProfilePath, {recursive: true, force: true, maxRetries: 3}) } return runUncontrolledChrome(tempDir.path) .then(() => tempDir) .catch((err) => tempDir.remove().then(() => Promise.reject(err))) } const fetchCookies = async (driver: WebDriver): Promise => { await driver.get(URL.HAOKAN) if (!(await driver.findElements(By.css('.userinfo-list'))).length) { throw new Error( 'The login session could not be loaded (maybe user never logged in)' ) } const webDriverCookies = await driver.manage().getCookies() return new Cookies(webDriverCookies) } export default async (): Promise => { return new Promise((resolve, reject) => { (async () => { let profilePath: ITempDirectory let webDriver: WebDriver profilePath = await makeLoggedInChromeProfile() webDriver = await makeWebDriver({automation: true, userDataDir: profilePath.path}) fetchCookies(webDriver).then(function (cookies) { resolve(cookies); }).catch(function (e) { reject(e); }).finally(async function () { if (webDriver) await webDriver.quit(); if (profilePath) await profilePath.remove(); }); })(); }); }