🔐 提高 Linux 上 Electron 应用的权限:获取管理员权限修改目录读写权限
问题描述: UOS 系统安装 deb 安装包后,发现写入本地缓存文件失败。
原因分析:
没有写入权限。uos 普通用户没有安装程序的权限,执行 dpkg i ${package-name}或者使用自带 package 管理器安装软件包时,需要提供管理员密码,以管理员身份安装( root:root 755)。以至于当前用户拥有读取和运行权限,而没有写入权限。
解决方案:
在运行时请求管理员密码,修改目录写入权限。尝试过调用afterinstallhook, 在安装后自动执行chmod命令,发现会损坏安装包资源,导致客户端无法正常运行。最终决定在运行时请求管理员密码,通过child_process调用 shell 更改用户目录权限。类似于 switchhost 请求密码写入host。
设计思路:

添加图片注释,不超过 140 字(可选)
核心代码:
core.ts。实现功能:
typescriptCopy
export type EventName = "error" | "success" | "requirePassword";
export class PrivilegesMain {
/**
* 消息中心
*/
public readonly message = new EventBus<EventName>();
/**
* 写入密码
*/
public setPassword: (password: string) => void;
/**
* 更改权限
*/
public changeDirWritePrivileges(dirName: string) {
/* 是否可写 */
if (!isDirctoryWritable(dirName)) {
// 更改目录权限
const command = `chmod -R 777 ${dirName}`;
const options = { shell: true, sudo: true };
const childProcess = spawn("sudo", ["-S"].concat(command.split(" ")), options);
// 向子进程写入管理员密码
this.setPassword = (password: string) => {
childProcess.stdin.write(`${password}\n`);
};
// 监听子进程的标准输出
childProcess.stdout.on("data", (data: any) => {
this.message.emit("data", `stdout: ${data}`);
});
// 监听子进程的异常
childProcess.stderr.on("data", (data: any) => {
if (data.toString() === "Sorry, try again.\n") {
// 密码错误
this.message.emit("error", <PrivilegeErrorResponse>{
type: "wrongPassword",
message: "Sorry, try again.",
});
} else if (data.toString() === "sudo: 3 incorrect password attempts\n") {
// 超出上限
this.message.emit("error", <PrivilegeErrorResponse>{
type: "failed",
message: "3 incorrect password attempts",
});
}
});
// 监听子进程的退出事件
childProcess.on("exit", (code: any, signal: any) => {
if (code === 0) {
this.message.emit("success", "Successfully changed dirctory priveleges!");
childProcess.stdin.end();
} else {
this.message.emit("error", "Failed to change priveleges");
}
});
// 没有写入权限,请求管理员密码
this.message.emit("requirePassword");
}
}
}
helper.ts。实现功能:
typescriptCopy
import { constants, accessSync } from "original-fs";
/**
* 是否是当前是否为root用户
*/
export const isRoot = () => process.geteuid && process.geteuid() === 0;
/**
* 是否有写入权限
*/
export const isDirctoryWritable = (dirname: string): boolean => {
try {
accessSync(`${dirname}`, constants.W_OK);
return true;
} catch {
return false;
}
};
event-bus.ts。实现功能:
typescriptCopy
/**
* 事件中心
*/
export class EventBus<EventName> {
constructor() {}
/**
* 任务栈
*/
public taskStack = new Map();
/**
* 事件注册中心
*/
public on(name: EventName, callback: Function): void {
this.taskStack.set(name, callback);
}
/**
* 触发回调事件
*/
public emit(name: EventName, ...args: any[]): void {
const event = this.taskStack.get(name);
event && event(...args);
}
/**
* 取消订阅事件
*/
public off(name: EventName): void {
this.taskStack.delete(name);
}
/**
* 销毁组件 (手动触发)
*/
public destory(): void {
this.taskStack.clear();
}
}
使用:
typescriptCopy
export class ElectronPrivileges {
public privilegesMain: PrivilegesMain;
/**
* 更改目录权限
*/
public changeAppDirPrivileges(dirname: string): void {
this.privilegesMain.changeDirWritePrivileges(`${rootPath}/${dirname}`);
}
public setPassword(password: string): void {
if (this.privilegesMain.setPassword) {
this.privilegesMain.setPassword(password);
}
}
// 初始化
public init(): void {
this.privilegesMain = new PrivilegesMain();
// 订阅错误消息
this.privilegesMain.message.on("error", (data: any) => {
/*
* 发送错误信息
* requestPassowrd
*/
});
this.privilegesMain.message.on("requirePassword", (dirname: string) => {
/*
* 发送请求密码消息
* requestPassowrd
*/
});
this.privilegesMain.message.on("success", () => {
/*
* 发送更改成功消息
* changePrivilegesSuccessfully
*/
});
/**
* 订阅请求修改目录权限
* 执行 this.changeAppDirPrivileges
*/
/**
* 订阅请求修改目录权限
* 执行 this.setPassword
*/
}
}
